1 //! Output of validated RPKI payload. 2 3 use std::{error, fmt, io}; 4 use std::str::FromStr; 5 use std::sync::Arc; 6 use chrono::Utc; 7 use chrono::format::{Item, Numeric, Pad}; 8 use log::{error, info}; 9 use rpki::repository::resources::AsId; 10 use crate::payload; 11 use crate::error::Failed; 12 use crate::payload::{AddressPrefix, OriginInfo, PayloadSnapshot, RouteOrigin}; 13 use crate::metrics::Metrics; 14 use crate::utils::date::format_iso_date; 15 16 17 //------------ OutputFormat -------------------------------------------------- 18 19 /// The output format for VRPs. 20 #[derive(Clone, Copy, Debug)] 21 pub enum OutputFormat { 22 /// CSV format. 23 /// 24 /// Each row has the AS number, prefix, max-length, and TA. 25 Csv, 26 27 /// RIPE NCC Validator compatible CSV format. 28 /// 29 /// This quotes all values and prints the AS number as just the number. 30 CompatCsv, 31 32 /// Extended CSV format. 33 /// 34 /// Each row has URI, ASN, prefix, max-length, not before, not after. 35 ExtendedCsv, 36 37 /// RIPE NCC Validator JSON format. 38 /// 39 /// This is a JSON object with one element `"roas"` which is an array 40 /// of objects, each with the elements `"asn"`, `"prefix"`, `"maxLength"`, 41 /// and `"ta"`. 42 Json, 43 44 /// JSON format with extended information. 45 ExtendedJson, 46 47 /// OpenBGPD configuration format. 48 /// 49 /// Specifically, this produces as `roa-set`. 50 Openbgpd, 51 52 /// BIRD configuration format. 53 /// 54 /// Specifically, this produces as `roa table`. 55 Bird1, 56 57 /// BIRD2 configuration format. 58 /// 59 /// Specifically, this produces as `route table`. 60 Bird2, 61 62 /// RPSL output. 63 /// 64 /// This produces a sequence of RPSL objects with various fields. 65 Rpsl, 66 67 /// Summary output. 68 /// 69 /// Produces a textual summary of the ROAs and VRPS. 70 Summary, 71 72 /// No output. 73 /// 74 /// Seriously: no output. 75 None, 76 } 77 78 impl OutputFormat { 79 /// All known output formats names and their values. 80 const VALUES: &'static [(&'static str, Self)] = &[ 81 ("csv", OutputFormat::Csv), 82 ("csvcompat", OutputFormat::CompatCsv), 83 ("csvext", OutputFormat::ExtendedCsv), 84 ("json", OutputFormat::Json), 85 ("jsonext", OutputFormat::ExtendedJson), 86 ("openbgpd", OutputFormat::Openbgpd), 87 ("bird1", OutputFormat::Bird1), 88 ("bird2", OutputFormat::Bird2), 89 ("rpsl", OutputFormat::Rpsl), 90 ("summary", OutputFormat::Summary), 91 ("none", OutputFormat::None), 92 ]; 93 94 /// The default output format name. 95 pub const DEFAULT_VALUE: &'static str = "csv"; 96 } 97 98 impl OutputFormat { 99 /// Returns the output format for a given request path. from_path(path: &str) -> Option<Self>100 pub fn from_path(path: &str) -> Option<Self> { 101 if !path.starts_with('/') { 102 return None 103 } 104 Self::try_from_str(&path[1..]) 105 } 106 107 /// Returns the output format for the given string if it is valid. try_from_str(value: &str) -> Option<Self>108 fn try_from_str(value: &str) -> Option<Self> { 109 for &(name, res) in Self::VALUES { 110 if name == value { 111 return Some(res) 112 } 113 } 114 None 115 } 116 117 /// Returns the media type string for this output format. content_type(self) -> &'static str118 pub fn content_type(self) -> &'static str { 119 match self { 120 OutputFormat::Csv | OutputFormat::CompatCsv | 121 OutputFormat::ExtendedCsv 122 => "text/csv;charset=utf-8;header=present", 123 OutputFormat::Json | OutputFormat::ExtendedJson 124 => "application/json", 125 _ => "text/plain;charset=utf-8", 126 } 127 } 128 129 /// Outputs a payload snapshot to a writer. output_snapshot<W: io::Write>( self, snapshot: &PayloadSnapshot, selection: Option<&Selection>, metrics: &Metrics, target: &mut W, ) -> Result<(), io::Error>130 pub fn output_snapshot<W: io::Write>( 131 self, 132 snapshot: &PayloadSnapshot, 133 selection: Option<&Selection>, 134 metrics: &Metrics, 135 target: &mut W, 136 ) -> Result<(), io::Error> { 137 let mut stream = OutputStream::new( 138 self, snapshot, selection, metrics 139 ); 140 while stream.write_next(target)? { } 141 Ok(()) 142 } 143 144 /// Creates an output stream for this format. stream( self, snapshot: Arc<PayloadSnapshot>, selection: Option<Selection>, metrics: Arc<Metrics>, ) -> impl Iterator<Item = Vec<u8>>145 pub fn stream( 146 self, 147 snapshot: Arc<PayloadSnapshot>, 148 selection: Option<Selection>, 149 metrics: Arc<Metrics>, 150 ) -> impl Iterator<Item = Vec<u8>> { 151 OutputStream::new(self, snapshot, selection, metrics) 152 } 153 formatter<W: io::Write>(self) -> Box<dyn Formatter<W> + Send>154 fn formatter<W: io::Write>(self) -> Box<dyn Formatter<W> + Send> { 155 match self { 156 OutputFormat::Csv => Box::new(Csv), 157 OutputFormat::CompatCsv => Box::new(CompatCsv), 158 OutputFormat::ExtendedCsv => Box::new(ExtendedCsv), 159 OutputFormat::Json => Box::new(Json), 160 OutputFormat::ExtendedJson => Box::new(ExtendedJson), 161 OutputFormat::Openbgpd => Box::new(Openbgpd), 162 OutputFormat::Bird1 => Box::new(Bird1), 163 OutputFormat::Bird2 => Box::new(Bird2), 164 OutputFormat::Rpsl => Box::new(Rpsl), 165 OutputFormat::Summary => Box::new(Summary), 166 OutputFormat::None => Box::new(NoOutput), 167 } 168 } 169 } 170 171 172 //--- FromStr 173 174 impl FromStr for OutputFormat { 175 type Err = Failed; 176 from_str(value: &str) -> Result<Self, Failed>177 fn from_str(value: &str) -> Result<Self, Failed> { 178 Self::try_from_str(value).ok_or_else(|| { 179 error!("Unknown output format: {}", value); 180 Failed 181 }) 182 } 183 } 184 185 186 //------------ Selection ----------------------------------------------------- 187 188 /// A set of rules defining which payload to include in output. 189 #[derive(Clone, Debug, Default)] 190 pub struct Selection { 191 origins: Vec<SelectOrigin>, 192 } 193 194 impl Selection { 195 /// Creates a new, empty selection. new() -> Self196 pub fn new() -> Self { 197 Selection::default() 198 } 199 200 /// Creates a selection from a HTTP query string. from_query(query: Option<&str>) -> Result<Option<Self>, QueryError>201 pub fn from_query(query: Option<&str>) -> Result<Option<Self>, QueryError> { 202 let query = match query { 203 Some(query) => query, 204 None => return Ok(None) 205 }; 206 207 let mut res = Self::default(); 208 for (key, value) in form_urlencoded::parse(query.as_ref()) { 209 if key == "select-prefix" || key == "filter-prefix" { 210 res.origins.push( 211 SelectOrigin::Prefix(AddressPrefix::from_str(&value)?) 212 ); 213 } 214 else if key == "select-asn" || key == "filter-asn" { 215 res.origins.push( 216 SelectOrigin::AsId( 217 AsId::from_str(&value).map_err(|_| QueryError)? 218 ) 219 ); 220 } 221 else { 222 return Err(QueryError) 223 } 224 } 225 226 Ok(Some(res)) 227 } 228 229 /// Add an origin ASN to select. push_origin_asn(&mut self, asn: AsId)230 pub fn push_origin_asn(&mut self, asn: AsId) { 231 self.origins.push(SelectOrigin::AsId(asn)) 232 } 233 234 /// Add a origin prefix to select. push_origin_prefix(&mut self, prefix: AddressPrefix)235 pub fn push_origin_prefix(&mut self, prefix: AddressPrefix) { 236 self.origins.push(SelectOrigin::Prefix(prefix)) 237 } 238 239 /// Returns whether payload should be included in output. include_origin(&self, origin: RouteOrigin) -> bool240 pub fn include_origin(&self, origin: RouteOrigin) -> bool { 241 for select in &self.origins { 242 if select.include_origin(origin) { 243 return true 244 } 245 } 246 false 247 } 248 } 249 250 impl AsRef<Selection> for Selection { as_ref(&self) -> &Self251 fn as_ref(&self) -> &Self { 252 self 253 } 254 } 255 256 257 //------------ SelectOrigin -------------------------------------------------- 258 259 /// A selection rule for origins. 260 #[derive(Clone, Copy, Debug)] 261 enum SelectOrigin { 262 /// Include resources related to the given ASN. 263 AsId(AsId), 264 265 /// Include resources related to the given prefix. 266 Prefix(AddressPrefix), 267 } 268 269 impl SelectOrigin { 270 /// Returns whether this rule selects payload. include_origin(self, origin: RouteOrigin) -> bool271 fn include_origin(self, origin: RouteOrigin) -> bool { 272 match self { 273 SelectOrigin::AsId(as_id) => origin.as_id() == as_id, 274 SelectOrigin::Prefix(prefix) => origin.prefix().covers(prefix), 275 } 276 } 277 } 278 279 280 //------------ OutputStream -------------------------------------------------- 281 282 struct OutputStream<Snap, Sel, Met, Target> { 283 snapshot: Snap, 284 state: StreamState, 285 formatter: Box<dyn Formatter<Target> + Send>, 286 selection: Option<Sel>, 287 metrics: Met, 288 } 289 290 enum StreamState { 291 Header, 292 Origin { index: usize, first: bool }, 293 Done, 294 } 295 296 impl<Snap, Sel, Met, Target> OutputStream<Snap, Sel, Met, Target> 297 where 298 Snap: AsRef<PayloadSnapshot>, 299 Sel: AsRef<Selection>, 300 Met: AsRef<Metrics>, 301 Target: io::Write, 302 { 303 /// Creates a new output stream. new( format: OutputFormat, snapshot: Snap, selection: Option<Sel>, metrics: Met, ) -> Self304 fn new( 305 format: OutputFormat, 306 snapshot: Snap, 307 selection: Option<Sel>, 308 metrics: Met, 309 ) -> Self { 310 OutputStream { 311 snapshot, 312 state: StreamState::Header, 313 formatter: format.formatter(), 314 selection, 315 metrics, 316 } 317 } 318 319 /// Writes the next item to the target. 320 /// 321 /// Returns whether it wrote an item. write_next(&mut self, target: &mut Target) -> Result<bool, io::Error>322 fn write_next(&mut self, target: &mut Target) -> Result<bool, io::Error> { 323 match self.state { 324 StreamState::Header => { 325 self.formatter.header( 326 self.snapshot.as_ref(), self.metrics.as_ref(), target 327 )?; 328 self.state = StreamState::Origin { index: 0, first: true }; 329 Ok(true) 330 } 331 StreamState::Origin { mut index, first } => { 332 loop { 333 let (origin, info) = match 334 self.snapshot.as_ref().origins().get(index) 335 { 336 Some(item) => (item.0, &item.1), 337 None => { 338 self.formatter.footer( 339 self.metrics.as_ref(), target 340 )?; 341 self.state = StreamState::Done; 342 break 343 } 344 }; 345 index += 1; 346 if let Some(selection) = self.selection.as_ref() { 347 if !selection.as_ref().include_origin(origin) { 348 continue 349 } 350 } 351 if !first { 352 self.formatter.delimiter(target)?; 353 } 354 self.formatter.origin(origin, info, target)?; 355 self.state = StreamState::Origin { index, first: false }; 356 break 357 } 358 Ok(true) 359 } 360 StreamState::Done => Ok(false) 361 } 362 } 363 } 364 365 impl Iterator for OutputStream< 366 Arc<PayloadSnapshot>, Selection, Arc<Metrics>, Vec<u8> 367 > { 368 type Item = Vec<u8>; 369 next(&mut self) -> Option<Self::Item>370 fn next(&mut self) -> Option<Self::Item> { 371 let mut res = Vec::new(); 372 while self.write_next(&mut res).expect("write to vec failed") { 373 if res.len() > 64000 { 374 return Some(res) 375 } 376 } 377 if res.is_empty() { 378 None 379 } 380 else { 381 Some(res) 382 } 383 } 384 } 385 386 387 //------------ QueryError ---------------------------------------------------- 388 389 #[derive(Debug)] 390 pub struct QueryError; 391 392 impl From<payload::ParsePrefixError> for QueryError { from(_: payload::ParsePrefixError) -> Self393 fn from(_: payload::ParsePrefixError) -> Self { 394 QueryError 395 } 396 } 397 398 impl fmt::Display for QueryError { fmt(&self, f: &mut fmt::Formatter) -> fmt::Result399 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 400 f.write_str("invalid query") 401 } 402 } 403 404 impl error::Error for QueryError { } 405 406 407 //------------ Formatter ----------------------------------------------------- 408 409 trait Formatter<W> { header( &self, snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>410 fn header( 411 &self, snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W 412 ) -> Result<(), io::Error> { 413 let _ = (snapshot, metrics, target); 414 Ok(()) 415 } 416 footer( &self, metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>417 fn footer( 418 &self, metrics: &Metrics, target: &mut W 419 ) -> Result<(), io::Error> { 420 let _ = (metrics, target); 421 Ok(()) 422 } 423 origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>424 fn origin( 425 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 426 ) -> Result<(), io::Error>; 427 delimiter(&self, target: &mut W) -> Result<(), io::Error>428 fn delimiter(&self, target: &mut W) -> Result<(), io::Error> { 429 let _ = target; 430 Ok(()) 431 } 432 } 433 434 435 //------------ Csv ----------------------------------------------------------- 436 437 struct Csv; 438 439 impl<W: io::Write> Formatter<W> for Csv { header( &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>440 fn header( 441 &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W 442 ) -> Result<(), io::Error> { 443 writeln!(target, "ASN,IP Prefix,Max Length,Trust Anchor") 444 } 445 origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>446 fn origin( 447 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 448 ) -> Result<(), io::Error> { 449 writeln!(target, "{},{}/{},{},{}", 450 origin.as_id(), 451 origin.address(), origin.address_length(), 452 origin.max_length(), 453 info.tal_name().unwrap_or("N/A"), 454 ) 455 } 456 } 457 458 459 //------------ CompatCsv ----------------------------------------------------- 460 461 struct CompatCsv; 462 463 impl<W: io::Write> Formatter<W> for CompatCsv { header( &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>464 fn header( 465 &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W 466 ) -> Result<(), io::Error> { 467 writeln!( 468 target, "\"ASN\",\"IP Prefix\",\"Max Length\",\"Trust Anchor\"" 469 ) 470 } 471 origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>472 fn origin( 473 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 474 ) -> Result<(), io::Error> { 475 writeln!(target, "\"{}\",\"{}/{}\",\"{}\",\"{}\"", 476 origin.as_id(), 477 origin.address(), origin.address_length(), 478 origin.max_length(), 479 info.tal_name().unwrap_or("N/A"), 480 ) 481 } 482 } 483 484 485 //------------ ExtendedCsv --------------------------------------------------- 486 487 struct ExtendedCsv; 488 489 impl ExtendedCsv { 490 // 2017-08-25 13:12:19 491 const TIME_ITEMS: &'static [Item<'static>] = &[ 492 Item::Numeric(Numeric::Year, Pad::Zero), 493 Item::Literal("-"), 494 Item::Numeric(Numeric::Month, Pad::Zero), 495 Item::Literal("-"), 496 Item::Numeric(Numeric::Day, Pad::Zero), 497 Item::Literal(" "), 498 Item::Numeric(Numeric::Hour, Pad::Zero), 499 Item::Literal(":"), 500 Item::Numeric(Numeric::Minute, Pad::Zero), 501 Item::Literal(":"), 502 Item::Numeric(Numeric::Second, Pad::Zero), 503 ]; 504 } 505 506 impl<W: io::Write> Formatter<W> for ExtendedCsv { header( &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>507 fn header( 508 &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W 509 ) -> Result<(), io::Error> { 510 writeln!(target, "URI,ASN,IP Prefix,Max Length,Not Before,Not After") 511 } 512 origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>513 fn origin( 514 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 515 ) -> Result<(), io::Error> { 516 write!(target, "{},{},{}/{},{},", 517 info.uri().map(|uri| uri.as_str()).unwrap_or("N/A"), 518 origin.as_id(), 519 origin.address(), origin.address_length(), 520 origin.max_length(), 521 )?; 522 match info.validity() { 523 Some(validity) => { 524 writeln!(target, "{},{}", 525 validity.not_before().format_with_items( 526 Self::TIME_ITEMS.iter().cloned() 527 ), 528 validity.not_after().format_with_items( 529 Self::TIME_ITEMS.iter().cloned() 530 ) 531 ) 532 } 533 None => writeln!(target, "N/A,N/A"), 534 } 535 } 536 } 537 538 539 //------------ Json ---------------------------------------------------------- 540 541 struct Json; 542 543 impl<W: io::Write> Formatter<W> for Json { header( &self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>544 fn header( 545 &self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W 546 ) -> Result<(), io::Error> { 547 writeln!(target, 548 "{{\ 549 \n \"metadata\": {{\ 550 \n \"generated\": {},\ 551 \n \"generatedTime\": \"{}\"\ 552 \n }},\ 553 \n \"roas\": [", 554 metrics.time.timestamp(), 555 format_iso_date(metrics.time) 556 ) 557 } 558 footer( &self, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>559 fn footer( 560 &self, _metrics: &Metrics, target: &mut W 561 ) -> Result<(), io::Error> { 562 writeln!(target, "\n ]\n}}") 563 } 564 origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>565 fn origin( 566 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 567 ) -> Result<(), io::Error> { 568 write!(target, 569 " {{ \"asn\": \"{}\", \"prefix\": \"{}/{}\", \ 570 \"maxLength\": {}, \"ta\": \"{}\" }}", 571 origin.as_id(), 572 origin.address(), origin.address_length(), 573 origin.max_length(), 574 info.tal_name().unwrap_or("N/A"), 575 ) 576 } 577 delimiter(&self, target: &mut W) -> Result<(), io::Error>578 fn delimiter(&self, target: &mut W) -> Result<(), io::Error> { 579 writeln!(target, ",") 580 } 581 } 582 583 584 //------------ ExtendedJson -------------------------------------------------- 585 586 struct ExtendedJson; 587 588 impl<W: io::Write> Formatter<W> for ExtendedJson { header( &self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>589 fn header( 590 &self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W 591 ) -> Result<(), io::Error> { 592 writeln!(target, 593 "{{\ 594 \n \"metadata\": {{\ 595 \n \"generated\": {},\ 596 \n \"generatedTime\": \"{}\"\ 597 \n }},\ 598 \n \"roas\": [", 599 metrics.time.timestamp(), 600 format_iso_date(metrics.time) 601 ) 602 } 603 footer( &self, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>604 fn footer( 605 &self, _metrics: &Metrics, target: &mut W 606 ) -> Result<(), io::Error> { 607 writeln!(target, "\n ]\n}}") 608 } 609 origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>610 fn origin( 611 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 612 ) -> Result<(), io::Error> { 613 write!(target, 614 " {{ \"asn\": \"{}\", \"prefix\": \"{}/{}\", \ 615 \"maxLength\": {}, \"source\": [", 616 origin.as_id(), 617 origin.address(), origin.address_length(), 618 origin.max_length(), 619 )?; 620 621 let mut first = true; 622 for item in info { 623 if let Some(roa) = item.roa_info() { 624 if !first { 625 write!(target, ", ")?; 626 } 627 else { 628 first = false; 629 } 630 write!(target, " {{ \"type\": \"roa\", \"uri\": ")?; 631 match roa.uri.as_ref() { 632 Some(uri) => write!(target, "\"{}\"", uri)?, 633 None => write!(target, "null")? 634 } 635 636 write!(target, 637 ", \"validity\": {{ \"notBefore\": \"{}\", \ 638 \"notAfter\": \"{}\" }}, \ 639 \"chainValidity\": {{ \"notBefore\": \"{}\", \ 640 \"notAfter\": \"{}\" }} \ 641 }}", 642 format_iso_date(roa.roa_validity.not_before().into()), 643 format_iso_date(roa.roa_validity.not_after().into()), 644 format_iso_date(roa.chain_validity.not_before().into()), 645 format_iso_date(roa.chain_validity.not_after().into()), 646 )?; 647 } 648 if let Some(exc) = item.exception_info() { 649 if !first { 650 write!(target, ", ")?; 651 } 652 else { 653 first = false; 654 } 655 write!(target, " {{ \"type\": \"exception\", \"path\": ")?; 656 match exc.path.as_ref() { 657 Some(path) => write!(target, "\"{}\"", path.display())?, 658 None => write!(target, "null")?, 659 } 660 if let Some(comment) = exc.comment.as_ref() { 661 write!(target, ", \"comment\": \"{}\"", comment)? 662 } 663 write!(target, " }}")?; 664 } 665 } 666 667 write!(target, "] }}") 668 } 669 delimiter(&self, target: &mut W) -> Result<(), io::Error>670 fn delimiter(&self, target: &mut W) -> Result<(), io::Error> { 671 writeln!(target, ",") 672 } 673 } 674 675 676 //------------ Openbgpd ------------------------------------------------------ 677 678 struct Openbgpd; 679 680 impl<W: io::Write> Formatter<W> for Openbgpd { header( &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>681 fn header( 682 &self, _snapshot: &PayloadSnapshot, _metrics: &Metrics, target: &mut W 683 ) -> Result<(), io::Error> { 684 writeln!(target, "roa-set {{") 685 } 686 footer( &self, _metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>687 fn footer( 688 &self, _metrics: &Metrics, target: &mut W 689 ) -> Result<(), io::Error> { 690 writeln!(target, "}}") 691 } 692 origin( &self, origin: RouteOrigin, _info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>693 fn origin( 694 &self, origin: RouteOrigin, _info: &OriginInfo, target: &mut W 695 ) -> Result<(), io::Error> { 696 write!( 697 target, " {}/{}", origin.address(), origin.address_length() 698 )?; 699 if origin.address_length() < origin.max_length() { 700 write!(target, " maxlen {}", origin.max_length())?; 701 } 702 writeln!(target, " source-as {}", u32::from(origin.as_id())) 703 } 704 } 705 706 707 //------------ Bird1 --------------------------------------------------------- 708 709 struct Bird1; 710 711 impl<W: io::Write> Formatter<W> for Bird1 { origin( &self, origin: RouteOrigin, _info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>712 fn origin( 713 &self, origin: RouteOrigin, _info: &OriginInfo, target: &mut W 714 ) -> Result<(), io::Error> { 715 writeln!(target, "roa {}/{} max {} as {};", 716 origin.address(), origin.address_length(), 717 origin.max_length(), 718 u32::from(origin.as_id()) 719 ) 720 } 721 } 722 723 724 //------------ Bird2 --------------------------------------------------------- 725 726 struct Bird2; 727 728 impl<W: io::Write> Formatter<W> for Bird2 { origin( &self, origin: RouteOrigin, _info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>729 fn origin( 730 &self, origin: RouteOrigin, _info: &OriginInfo, target: &mut W 731 ) -> Result<(), io::Error> { 732 writeln!(target, "route {}/{} max {} as {};", 733 origin.address(), origin.address_length(), 734 origin.max_length(), 735 u32::from(origin.as_id()) 736 ) 737 } 738 } 739 740 741 //------------ Rpsl ---------------------------------------------------------- 742 743 struct Rpsl; 744 745 impl Rpsl { 746 const TIME_ITEMS: &'static [Item<'static>] = &[ 747 Item::Numeric(Numeric::Year, Pad::Zero), 748 Item::Literal("-"), 749 Item::Numeric(Numeric::Month, Pad::Zero), 750 Item::Literal("-"), 751 Item::Numeric(Numeric::Day, Pad::Zero), 752 Item::Literal("T"), 753 Item::Numeric(Numeric::Hour, Pad::Zero), 754 Item::Literal(":"), 755 Item::Numeric(Numeric::Minute, Pad::Zero), 756 Item::Literal(":"), 757 Item::Numeric(Numeric::Second, Pad::Zero), 758 Item::Literal("Z"), 759 ]; 760 } 761 762 impl<W: io::Write> Formatter<W> for Rpsl { origin( &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W ) -> Result<(), io::Error>763 fn origin( 764 &self, origin: RouteOrigin, info: &OriginInfo, target: &mut W 765 ) -> Result<(), io::Error> { 766 let now = Utc::now().format_with_items( 767 Self::TIME_ITEMS.iter().cloned() 768 ); 769 writeln!(target, 770 "\n{}: {}/{}\norigin: {}\n\ 771 descr: RPKI attestation\nmnt-by: NA\ncreated: {}\n\ 772 last-modified: {}\nsource: ROA-{}-RPKI-ROOT\n", 773 if origin.address().is_ipv4() { "route" } 774 else { "route6" }, 775 origin.address(), origin.address_length(), 776 origin.as_id(), now, now, 777 info.tal_name().map(|name| { 778 name.to_uppercase() 779 }).unwrap_or_else(|| "N/A".into()) 780 ) 781 } 782 } 783 784 785 786 //------------ Summary ------------------------------------------------------- 787 788 /// Output only a summary. 789 pub struct Summary; 790 791 impl Summary { produce_header( metrics: &Metrics, mut line: impl FnMut(fmt::Arguments) -> Result<(), io::Error> ) -> Result<(), io::Error>792 fn produce_header( 793 metrics: &Metrics, 794 mut line: impl FnMut(fmt::Arguments) -> Result<(), io::Error> 795 ) -> Result<(), io::Error> { 796 line(format_args!("Summary at {}", metrics.time))?; 797 for tal in &metrics.tals { 798 line(format_args!( 799 "{}: {} verified ROAs, {} verified VRPs, \ 800 {} unsafe VRPs, {} final VRPs.", 801 tal.name(), tal.publication.valid_roas, tal.vrps.valid, 802 tal.vrps.marked_unsafe, tal.vrps.contributed 803 ))?; 804 } 805 line(format_args!( 806 "total: {} verified ROAs, {} verified VRPs, \ 807 {} unsafe VRPs, {} final VRPs.", 808 metrics.publication.valid_roas, 809 metrics.vrps.valid, metrics.vrps.marked_unsafe, 810 metrics.vrps.contributed, 811 )) 812 } 813 log(metrics: &Metrics)814 pub fn log(metrics: &Metrics) { 815 Self::produce_header(metrics, |args| { 816 info!("{}", args); 817 Ok(()) 818 }).unwrap() 819 } 820 } 821 822 impl<W: io::Write> Formatter<W> for Summary { header( &self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W ) -> Result<(), io::Error>823 fn header( 824 &self, _snapshot: &PayloadSnapshot, metrics: &Metrics, target: &mut W 825 ) -> Result<(), io::Error> { 826 Self::produce_header(metrics, |args| { 827 writeln!(target, "{}", args) 828 }) 829 } 830 origin( &self, _origin: RouteOrigin, _info: &OriginInfo, _target: &mut W ) -> Result<(), io::Error>831 fn origin( 832 &self, _origin: RouteOrigin, _info: &OriginInfo, _target: &mut W 833 ) -> Result<(), io::Error> { 834 Ok(()) 835 } 836 } 837 838 839 840 //------------ NoOutput------------------------------------------------------- 841 842 struct NoOutput; 843 844 impl<W: io::Write> Formatter<W> for NoOutput { origin( &self, _origin: RouteOrigin, _info: &OriginInfo, _target: &mut W ) -> Result<(), io::Error>845 fn origin( 846 &self, _origin: RouteOrigin, _info: &OriginInfo, _target: &mut W 847 ) -> Result<(), io::Error> { 848 Ok(()) 849 } 850 } 851 852 853