chiark / gitweb /
config: Add missing ' ' around %(rnets)s
[hippotat.git] / src / config.rs
1 // Copyright 2021 Ian Jackson and contributors to Hippotat
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6
7 use configparser::ini::Ini;
8
9 #[derive(hippotat_macros::ResolveConfig)]
10 #[derive(Debug,Clone)]
11 pub struct InstanceConfig {
12   // Exceptional settings
13   #[special(special_server, SKL::ServerName)] pub server: ServerName,
14   pub                                             secret: Secret,
15   #[special(special_ipif, SKL::Ordinary)]     pub ipif:   String,
16
17   // Capped settings:
18   #[limited]    pub max_batch_down:               u32,
19   #[limited]    pub max_queue_time:               Duration,
20   #[limited]    pub http_timeout:                 Duration,
21   #[limited]    pub target_requests_outstanding:  u32,
22
23   // Ordinary settings:
24   pub addrs:                        Vec<IpAddr>,
25   pub vnetwork:                     Vec<IpNet>,
26   pub vaddr:                        IpAddr,
27   pub vrelay:                       IpAddr,
28   pub port:                         u16,
29   pub mtu:                          u32,
30   pub ifname_server:                String,
31   pub ifname_client:                String,
32
33   // Ordinary settings, used by server only:
34   #[server]  pub max_clock_skew:               Duration,
35
36   // Ordinary settings, used by client only:
37   #[client]  pub http_timeout_grace:           Duration,
38   #[client]  pub max_requests_outstanding:     u32,
39   #[client]  pub max_batch_up:                 u32,
40   #[client]  pub http_retry:                   Duration,
41   #[client]  pub url:                          Uri,
42   #[client]  pub vroutes:                      Vec<IpNet>,
43 }
44
45 static DEFAULT_CONFIG: &str = r#"
46 [COMMON]
47 max_batch_down = 65536
48 max_queue_time = 10
49 target_requests_outstanding = 3
50 http_timeout = 30
51 http_timeout_grace = 5
52 max_requests_outstanding = 6
53 max_batch_up = 4000
54 http_retry = 5
55 port = 80
56 vroutes = ''
57 ifname_client = hippo%%d
58 ifname_server = shippo%%d
59 max_clock_skew = 300
60
61 ipif = userv root ipif %(local)s,%(peer)s,%(mtu)s,slip,%(ifname)s '%(rnets)s'
62
63 mtu = 1500
64
65 vnetwork = 172.24.230.192
66
67 [LIMIT]
68 max_batch_down = 262144
69 max_queue_time = 121
70 http_timeout = 121
71 target_requests_outstanding = 10
72 "#;
73
74 #[derive(StructOpt,Debug)]
75 pub struct Opts {
76   /// Top-level config file or directory
77   ///
78   /// Look for `main.cfg`, `config.d` and `secrets.d` here.
79   ///
80   /// Or if this is a file, just read that file.
81   #[structopt(long, default_value="/etc/hippotat")]
82   pub config: PathBuf,
83   
84   /// Additional config files or dirs, which can override the others
85   #[structopt(long, multiple=true, number_of_values=1)]
86   pub extra_config: Vec<PathBuf>,
87 }
88
89 #[ext]
90 impl<'s> Option<&'s str> {
91   #[throws(AE)]
92   fn value(self) -> &'s str {
93     self.ok_or_else(|| anyhow!("value needed"))?
94   }
95 }
96
97 #[derive(Clone)]
98 pub struct Secret(pub String);
99 impl Parseable for Secret {
100   #[throws(AE)]
101   fn parse(s: Option<&str>) -> Self {
102     let s = s.value()?;
103     if s.is_empty() { throw!(anyhow!("secret value cannot be empty")) }
104     Secret(s.into())
105   }
106   #[throws(AE)]
107   fn default() -> Self { Secret(default()) }
108 }
109 impl Debug for Secret {
110   #[throws(fmt::Error)]
111   fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "Secret(***)")? }
112 }
113
114 #[derive(Debug,Clone,Hash,Eq,PartialEq)]
115 pub enum SectionName {
116   Link(LinkName),
117   Client(ClientName),
118   Server(ServerName), // includes SERVER, which is slightly special
119   ServerLimit(ServerName),
120   GlobalLimit,
121   Common,
122 }
123 pub use SectionName as SN;
124
125 #[derive(Debug,Clone)]
126 struct RawVal { raw: Option<String>, loc: Arc<PathBuf> }
127 type SectionMap = HashMap<String, RawVal>;
128
129 #[derive(Debug)]
130 struct RawValRef<'v,'l,'s> {
131   raw: Option<&'v str>,
132   key: &'static str,
133   loc: &'l Path,
134   section: &'s SectionName,
135 }
136
137 impl<'v> RawValRef<'v,'_,'_> {
138   #[throws(AE)]
139   fn try_map<F,T>(&self, f: F) -> T
140   where F: FnOnce(Option<&'v str>) -> Result<T, AE> {
141     f(self.raw)
142       .with_context(|| format!(r#"file {:?}, section {}, key "{}""#,
143                                self.loc, self.section, self.key))?
144   }
145 }
146
147 pub struct Config {
148   pub opts: Opts,
149 }
150
151 static OUTSIDE_SECTION: &str = "[";
152 static SPECIAL_SERVER_SECTION: &str = "SERVER";
153
154 #[derive(Default,Debug)]
155 struct Aggregate {
156   keys_allowed: HashMap<&'static str, SectionKindList>,
157   sections: HashMap<SectionName, SectionMap>,
158 }
159
160 type OkAnyway<'f,A> = &'f dyn Fn(ErrorKind) -> Option<A>;
161 #[ext]
162 impl<'f,A> OkAnyway<'f,A> {
163   fn ok<T>(self, r: &Result<T, io::Error>) -> Option<A> {
164     let e = r.as_ref().err()?;
165     let k = e.kind();
166     let a = self(k)?;
167     Some(a)
168   }
169 }
170
171 impl FromStr for SectionName {
172   type Err = AE;
173   #[throws(AE)]
174   fn from_str(s: &str) -> Self {
175     match s {
176       "COMMON" => return SN::Common,
177       "LIMIT" => return SN::GlobalLimit,
178       _ => { }
179     };
180     if let Ok(n@ ServerName(_)) = s.parse() { return SN::Server(n) }
181     if let Ok(n@ ClientName(_)) = s.parse() { return SN::Client(n) }
182     let (server, client) = s.split_ascii_whitespace().collect_tuple()
183       .ok_or_else(|| anyhow!(
184         "bad section name {:?} \
185          (must be COMMON, DEFAULT, <server>, <client>, or <server> <client>",
186         s
187       ))?;
188     let server = server.parse().context("server name in link section name")?;
189     if client == "LIMIT" { return SN::ServerLimit(server) }
190     let client = client.parse().context("client name in link section name")?;
191     SN::Link(LinkName { server, client })
192   }
193 }
194 impl Display for InstanceConfig {
195   #[throws(fmt::Error)]
196   fn fmt(&self, f: &mut fmt::Formatter) {
197     write!(f, "[{} {}]", &self.server, &self.vaddr)?;
198   }
199 }
200
201 impl Display for SectionName {
202   #[throws(fmt::Error)]
203   fn fmt(&self, f: &mut fmt::Formatter) {
204     match self {
205       SN::Link  (ref l)      => Display::fmt(l, f)?,
206       SN::Client(ref c)      => write!(f, "[{}]"       , c)?,
207       SN::Server(ref s)      => write!(f, "[{}]"       , s)?,
208       SN::ServerLimit(ref s) => write!(f, "[{} LIMIT] ", s)?,
209       SN::GlobalLimit        => write!(f, "[LIMIT]"       )?,
210       SN::Common             => write!(f, "[COMMON]"      )?,
211     }
212   }
213 }
214
215 impl Aggregate {
216   #[throws(AE)] // AE does not include path
217   fn read_file<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
218   {
219     let f = fs::File::open(path);
220     if let Some(anyway) = anyway.ok(&f) { return Some(anyway) }
221     let mut f = f.context("open")?;
222
223     let mut s = String::new();
224     let y = f.read_to_string(&mut s);
225     if let Some(anyway) = anyway.ok(&y) { return Some(anyway) }
226     y.context("read")?;
227
228     self.read_string(s, path)?;
229     None
230   }
231
232   #[throws(AE)] // AE does not include path
233   fn read_string(&mut self, s: String, path_for_loc: &Path) {
234     let mut ini = Ini::new_cs();
235     ini.set_default_section(OUTSIDE_SECTION);
236     ini.read(s).map_err(|e| anyhow!("{}", e)).context("parse as INI")?;
237     let map = mem::take(ini.get_mut_map());
238     if map.get(OUTSIDE_SECTION).is_some() {
239       throw!(anyhow!("INI file contains settings outside a section"));
240     }
241
242     let loc = Arc::new(path_for_loc.to_owned());
243
244     for (sn, vars) in map {
245       let sn = sn.parse().dcontext(&sn)?;
246
247       for key in vars.keys() {
248         let skl = self.keys_allowed.get(key.as_str()).ok_or_else(
249           || anyhow!("unknown configuration key {:?}", key)
250         )?;
251         if ! skl.contains(&sn) {
252           throw!(anyhow!("configuration key {:?} not applicable \
253                           in this kind of section {:?}", key, &sn))
254         }
255       }
256
257       let ent = self.sections.entry(sn).or_default();
258       for (key, raw) in vars {
259         let raw = match raw {
260           Some(raw) if raw.starts_with('\'') || raw.starts_with('"') => Some(
261             (||{
262               if raw.contains('\\') {
263                 throw!(
264                   anyhow!("quoted value contains backslash, not supported")
265                 );
266               }
267               let unq = raw[1..].strip_suffix(&raw[0..1])
268                 .ok_or_else(
269                   || anyhow!("mismatched quotes around quoted value")
270                 )?
271                 .to_owned();
272               Ok::<_,AE>(unq)
273             })()
274               .with_context(|| format!("key {:?}", key))
275               .dcontext(path_for_loc)?
276           ),
277           x => x,
278         };
279         let key = key.replace('-',"_");
280         ent.insert(key, RawVal { raw, loc: loc.clone() });
281       }
282     }
283   }
284
285   #[throws(AE)] // AE includes path
286   fn read_dir_d<A>(&mut self, path: &Path, anyway: OkAnyway<A>) -> Option<A>
287   {
288     let dir = fs::read_dir(path);
289     if let Some(anyway) = anyway.ok(&dir) { return Some(anyway) }
290     let dir = dir.context("open directory").dcontext(path)?;
291     for ent in dir {
292       let ent = ent.context("read directory").dcontext(path)?;
293       let leaf = ent.file_name();
294       let leaf = leaf.to_str();
295       let leaf = if let Some(leaf) = leaf { leaf } else { continue }; //utf8?
296       if leaf.len() == 0 { continue }
297       if ! leaf.chars().all(
298         |c| c=='-' || c=='_' || c.is_ascii_alphanumeric()
299       ) { continue }
300
301       // OK we want this one
302       let ent = ent.path();
303       self.read_file(&ent, &|_| None::<Void>).dcontext(&ent)?;
304     }
305     None
306   }
307
308   #[throws(AE)] // AE includes everything
309   fn read_toplevel(&mut self, toplevel: &Path) {
310     enum Anyway { None, Dir }
311     match self.read_file(toplevel, &|k| match k {
312       EK::NotFound => Some(Anyway::None),
313       EK::IsADirectory => Some(Anyway::Dir),
314       _ => None,
315     })
316       .dcontext(toplevel).context("top-level config directory (or file)")?
317     {
318       None | Some(Anyway::None) => { },
319
320       Some(Anyway::Dir) => {
321         struct AnywayNone;
322         let anyway_none = |k| match k {
323           EK::NotFound => Some(AnywayNone),
324           _ => None,
325         };
326
327         let mk = |leaf: &str| {
328           [ toplevel, &PathBuf::from(leaf) ]
329             .iter().collect::<PathBuf>()
330         };
331
332         for &(try_main, desc) in &[
333           ("main.cfg", "main config file"),
334           ("master.cfg", "obsolete-named main config file"),
335         ] {
336           let main = mk(try_main);
337
338           match self.read_file(&main, &anyway_none)
339             .dcontext(main).context(desc)?
340           {
341             None => break,
342             Some(AnywayNone) => { },
343           }
344         }
345
346         for &(try_dir, desc) in &[
347           ("config.d", "per-link config directory"),
348           ("secrets.d", "per-link secrets directory"),
349         ] {
350           let dir = mk(try_dir);
351           match self.read_dir_d(&dir, &anyway_none).context(desc)? {
352             None => { },
353             Some(AnywayNone) => { },
354           }
355         }
356       }
357     }
358   }
359
360   #[throws(AE)] // AE includes extra, but does that this is extra
361   fn read_extra(&mut self, extra: &Path) {
362     struct AnywayDir;
363
364     match self.read_file(extra, &|k| match k {
365       EK::IsADirectory => Some(AnywayDir),
366       _ => None,
367     })
368       .dcontext(extra)?
369     {
370       None => return,
371       Some(AnywayDir) => {
372         self.read_dir_d(extra, &|_| None::<Void>)?;
373       }
374     }
375
376   }
377 }
378
379 impl Aggregate {
380   fn instances(&self, only_server: Option<&ServerName>) -> BTreeSet<LinkName> {
381     let mut links:              BTreeSet<LinkName> = default();
382
383     let mut secrets_anyserver:  BTreeSet<&ClientName> = default();
384     let mut secrets_anyclient:  BTreeSet<&ServerName> = default();
385     let mut secret_global       = false;
386
387     let mut putative_servers   = BTreeSet::new();
388     let mut putative_clients   = BTreeSet::new();
389
390     let mut note_server = |s| {
391       if let Some(only) = only_server { if s != only { return false } }
392       putative_servers.insert(s);
393       true
394     };
395     let mut note_client = |c| {
396       putative_clients.insert(c);
397     };
398
399     for (section, vars) in &self.sections {
400       let has_secret = || vars.contains_key("secret");
401
402       match section {
403         SN::Link(l) => {
404           if ! note_server(&l.server) { continue }
405           note_client(&l.client);
406           if has_secret() { links.insert(l.clone()); }
407         },
408         SN::Server(ref s) => {
409           if ! note_server(s) { continue }
410           if has_secret() { secrets_anyclient.insert(s); }
411         },
412         SN::Client(ref c) => {
413           note_client(c);
414           if has_secret() { secrets_anyserver.insert(c); }
415         },
416         SN::Common => {
417           if has_secret() { secret_global = true; }
418         },
419         _ => { },
420       }
421     }
422
423     // Add links which are justified by blanket secrets
424     for (client, server) in iproduct!(
425       putative_clients.into_iter().filter(
426         |c| secret_global || secrets_anyserver.contains(c)
427       ),
428       putative_servers.iter().cloned().filter(
429         |s| secret_global || secrets_anyclient.contains(s)
430       )
431     ) {
432       links.insert(LinkName {
433         client: client.clone(),
434         server: server.clone(),
435       });
436     }
437
438     links
439   }
440 }
441
442 struct ResolveContext<'c> {
443   agg: &'c Aggregate,
444   link: &'c LinkName,
445   end: LinkEnd,
446   all_sections: Vec<SectionName>,
447 }
448
449 trait Parseable: Sized {
450   fn parse(s: Option<&str>) -> Result<Self, AE>;
451   fn default() -> Result<Self, AE> {
452     Err(anyhow!("setting must be specified"))
453   }
454   #[throws(AE)]
455   fn default_for_key(key: &str) -> Self {
456     Self::default().with_context(|| key.to_string())?
457   }
458 }
459
460 impl Parseable for Duration {
461   #[throws(AE)]
462   fn parse(s: Option<&str>) -> Duration {
463     // todo: would be nice to parse with humantime maybe
464     Duration::from_secs( s.value()?.parse()? )
465   }
466 }
467 macro_rules! parseable_from_str { ($t:ty $(, $def:expr)? ) => {
468   impl Parseable for $t {
469     #[throws(AE)]
470     fn parse(s: Option<&str>) -> $t { s.value()?.parse()? }
471     $( #[throws(AE)] fn default() -> Self { $def } )?
472   }
473 } }
474 parseable_from_str!{u16, default() }
475 parseable_from_str!{u32, default() }
476 parseable_from_str!{String, default() }
477 parseable_from_str!{IpNet, default() }
478 parseable_from_str!{IpAddr, Ipv4Addr::UNSPECIFIED.into() }
479 parseable_from_str!{Uri, default() }
480
481 impl<T:Parseable> Parseable for Vec<T> {
482   #[throws(AE)]
483   fn parse(s: Option<&str>) -> Vec<T> {
484     s.value()?
485       .split_ascii_whitespace()
486       .map(|s| Parseable::parse(Some(s)))
487       .collect::<Result<Vec<_>,_>>()?
488   }
489   #[throws(AE)]
490   fn default() -> Self { default() }
491 }
492
493
494 #[derive(Debug,Copy,Clone)]
495 enum SectionKindList {
496   Ordinary,
497   Limited,
498   Limits,
499   ClientAgnostic,
500   ServerName,
501 }
502 use SectionKindList as SKL;
503
504 impl SectionName {
505   fn special_server_section() -> Self { SN::Server(ServerName(
506     SPECIAL_SERVER_SECTION.into()
507   )) }
508 }
509
510 impl SectionKindList {
511   fn contains(self, s: &SectionName) -> bool {
512     match self {
513       SKL::Ordinary       => matches!(s, SN::Link(_)
514                                        | SN::Client(_)
515                                        | SN::Server(_)
516                                        | SN::Common),
517
518       SKL::Limits         => matches!(s, SN::ServerLimit(_)
519                                        | SN::GlobalLimit),
520
521       SKL::ClientAgnostic => matches!(s, SN::Common
522                                        | SN::Server(_)),
523
524       SKL::Limited        => SKL::Ordinary.contains(s)
525                            | SKL::Limits  .contains(s),
526
527       SKL::ServerName     => matches!(s, SN::Common)
528                            | matches!(s, SN::Server(ServerName(name))
529                                          if name == SPECIAL_SERVER_SECTION),
530     }
531   }
532 }
533
534 impl Aggregate {
535   fn lookup_raw<'a,'s,S>(&'a self, key: &'static str, sections: S)
536                        -> Option<RawValRef<'a,'a,'s>>
537   where S: Iterator<Item=&'s SectionName>
538   {
539     for section in sections {
540       if let Some(raw) = self.sections
541         .get(section)
542         .and_then(|vars: &SectionMap| vars.get(key))
543       {
544         return Some(RawValRef {
545           raw: raw.raw.as_deref(),
546           loc: &raw.loc,
547           section, key,
548         })
549       }
550     }
551     None
552   }
553
554   #[throws(AE)]
555   pub fn establish_server_name(&self) -> ServerName {
556     let key = "server";
557     let raw = match self.lookup_raw(
558       key,
559       [ &SectionName::Common, &SN::special_server_section() ].iter().cloned()
560     ) {
561       Some(raw) => raw.try_map(|os| os.value())?,
562       None => SPECIAL_SERVER_SECTION,
563     };
564     ServerName(raw.into())
565   }
566 }
567
568 impl<'c> ResolveContext<'c> {
569   fn first_of_raw(&'c self, key: &'static str, sections: SectionKindList)
570                   -> Option<RawValRef<'c,'c,'c>> {
571     self.agg.lookup_raw(
572       key,
573       self.all_sections.iter()
574         .filter(|s| sections.contains(s))
575     )
576   }
577
578   #[throws(AE)]
579   fn first_of<T>(&self, key: &'static str, sections: SectionKindList)
580                  -> Option<T>
581   where T: Parseable
582   {
583     match self.first_of_raw(key, sections) {
584       None => None,
585       Some(raw) => Some(raw.try_map(Parseable::parse)?),
586     }
587   }
588
589   #[throws(AE)]
590   pub fn ordinary<T>(&self, key: &'static str) -> T
591   where T: Parseable
592   {
593     match self.first_of(key, SKL::Ordinary)? {
594       Some(y) => y,
595       None => Parseable::default_for_key(key)?,
596     }
597   }
598
599   #[throws(AE)]
600   pub fn limited<T>(&self, key: &'static str) -> T
601   where T: Parseable + Ord
602   {
603     let val = self.ordinary(key)?;
604     if let Some(limit) = self.first_of(key, SKL::Limits)? {
605       min(val, limit)
606     } else {
607       val
608     }
609   }
610
611   #[throws(AE)]
612   pub fn client<T>(&self, key: &'static str) -> T
613   where T: Parseable + Default {
614     match self.end {
615       LinkEnd::Client => self.ordinary(key)?,
616       LinkEnd::Server => default(),
617     }
618   }
619   #[throws(AE)]
620   pub fn server<T>(&self, key: &'static str) -> T
621   where T: Parseable + Default {
622     match self.end {
623       LinkEnd::Server => self.ordinary(key)?,
624       LinkEnd::Client => default(),
625     }
626   }
627
628   #[throws(AE)]
629   pub fn special_ipif(&self, key: &'static str) -> String {
630     match self.end {
631       LinkEnd::Client => self.ordinary(key)?,
632       LinkEnd::Server => {
633         self.first_of(key, SKL::ClientAgnostic)?
634           .unwrap_or_default()
635       },
636     }
637   }
638
639   #[throws(AE)]
640   pub fn special_server(&self, _key: &'static str) -> ServerName {
641     self.link.server.clone()
642   }
643 }
644
645 impl InstanceConfig {
646   #[throws(AE)]
647   fn complete(&mut self, end: LinkEnd) {
648     let mut vhosts = self.vnetwork.iter()
649       .map(|n| n.hosts()).flatten()
650       .filter({ let vaddr = self.vaddr; move |v| v != &vaddr });
651
652     if self.vaddr.is_unspecified() {
653       self.vaddr = vhosts.next().ok_or_else(
654         || anyhow!("vnetwork too small to generate vaddrr")
655       )?;
656     }
657     if self.vrelay.is_unspecified() {
658       self.vrelay = vhosts.next().ok_or_else(
659         || anyhow!("vnetwork too small to generate vrelay")
660       )?;
661     }
662
663     match end {
664       LinkEnd::Client => {
665         if &self.url == &default::<Uri>() {
666           let addr = self.addrs.get(0).ok_or_else(
667             || anyhow!("client needs addrs or url set")
668           )?;
669           self.url = format!(
670             "http://{}{}/",
671             match addr {
672               IpAddr::V4(a) => format!("{}", a),
673               IpAddr::V6(a) => format!("[{}]", a),
674             },
675             match self.port {
676               80 => format!(""),
677               p => format!(":{}", p),
678             })
679             .parse().unwrap()
680         }
681       },
682
683       LinkEnd::Server => {
684         if self.addrs.is_empty() {
685           throw!(anyhow!("missing 'addrs' setting"))
686         }
687       },
688     }
689   }
690 }
691
692 #[throws(AE)]
693 pub fn read(end: LinkEnd) -> Vec<InstanceConfig> {
694   let opts = config::Opts::from_args();
695
696   let agg = (||{
697     let mut agg = Aggregate::default();
698     agg.keys_allowed.extend(
699       InstanceConfig::FIELDS.iter().cloned()
700     );
701
702     agg.read_string(DEFAULT_CONFIG.into(),
703                     "<build-in defaults>".as_ref()).unwrap();
704
705     agg.read_toplevel(&opts.config)?;
706     for extra in &opts.extra_config {
707       agg.read_extra(extra).context("extra config")?;
708     }
709
710     //eprintln!("GOT {:#?}", agg);
711
712     Ok::<_,AE>(agg)
713   })().context("read configuration")?;
714
715   let server_name = match end {
716     LinkEnd::Server => Some(agg.establish_server_name()?),
717     LinkEnd::Client => None,
718   };
719
720   let instances = agg.instances(server_name.as_ref());
721   let mut ics = vec![];
722
723   for link in instances {
724     let rctx = ResolveContext {
725       agg: &agg,
726       link: &link,
727       end,
728       all_sections: vec![
729         SN::Link(link.clone()),
730         SN::Client(link.client.clone()),
731         SN::Server(link.server.clone()),
732         SN::Common,
733         SN::ServerLimit(link.server.clone()),
734         SN::GlobalLimit,
735       ],
736     };
737
738     let mut ic = InstanceConfig::resolve_instance(&rctx)
739       .with_context(|| format!("resolve config for {}", &link))?;
740
741     ic.complete(end)
742       .with_context(|| format!("complete config for {}", &link))?;
743
744     ics.push(ic);
745   }
746
747   ics
748 }