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