chiark / gitweb /
config: Substitutions prefer %{...} to %(...)s, document, etc.
[hippotat.git] / src / config.rs
index 0582e1222bd490173fea3085cccc450486591daf..4b94b3ed73357db4659e2c3fde777e4e67b13e66 100644 (file)
@@ -6,6 +6,42 @@ use crate::prelude::*;
 
 use configparser::ini::Ini;
 
+#[derive(hippotat_macros::ResolveConfig)]
+#[derive(Debug,Clone)]
+pub struct InstanceConfig {
+  // Exceptional settings
+  #[special(special_link, SKL::ServerName)]   pub link:   LinkName,
+  pub                                             secret: Secret,
+  #[special(special_ipif, SKL::Ordinary)]     pub ipif:   String,
+
+  // Capped settings:
+  #[limited]    pub max_batch_down:               u32,
+  #[limited]    pub max_queue_time:               Duration,
+  #[limited]    pub http_timeout:                 Duration,
+  #[limited]    pub target_requests_outstanding:  u32,
+
+  // Ordinary settings:
+  pub addrs:                        Vec<IpAddr>,
+  pub vnetwork:                     Vec<IpNet>,
+  pub vaddr:                        IpAddr,
+  pub vrelay:                       IpAddr,
+  pub port:                         u16,
+  pub mtu:                          u32,
+  pub ifname_server:                String,
+  pub ifname_client:                String,
+
+  // Ordinary settings, used by server only:
+  #[server]  pub max_clock_skew:               Duration,
+
+  // Ordinary settings, used by client only:
+  #[client]  pub http_timeout_grace:           Duration,
+  #[client]  pub max_requests_outstanding:     u32,
+  #[client]  pub max_batch_up:                 u32,
+  #[client]  pub http_retry:                   Duration,
+  #[client]  pub url:                          Uri,
+  #[client]  pub vroutes:                      Vec<IpNet>,
+}
+
 static DEFAULT_CONFIG: &str = r#"
 [COMMON]
 max_batch_down = 65536
@@ -18,15 +54,15 @@ max_batch_up = 4000
 http_retry = 5
 port = 80
 vroutes = ''
-ifname_client = hippo%%d
-ifname_server = shippo%%d
+ifname_client = hippo%d
+ifname_server = shippo%d
 max_clock_skew = 300
 
-ipif = userv root ipif %(local)s,%(peer)s,%(mtu)s,slip,%(ifname)s %(rnets)s
+ipif = userv root ipif %{local},%{peer},%{mtu},slip,%{ifname} '%{rnets}'
 
 mtu = 1500
 
-vvnetwork = 172.24.230.192
+vnetwork = 172.24.230.192
 
 [LIMIT]
 max_batch_down = 262144
@@ -75,42 +111,6 @@ impl Debug for Secret {
   fn fmt(&self, f: &mut fmt::Formatter) { write!(f, "Secret(***)")? }
 }
 
-#[derive(hippotat_macros::ResolveConfig)]
-#[derive(Debug,Clone)]
-pub struct InstanceConfig {
-  // Exceptional settings
-  #[special(special_server, SKL::ServerName)] pub server: ServerName,
-  pub                                             secret: Secret,
-  #[special(special_ipif, SKL::Ordinary)]     pub ipif:   String,
-
-  // Capped settings:
-  #[limited]    pub max_batch_down:               u32,
-  #[limited]    pub max_queue_time:               Duration,
-  #[limited]    pub http_timeout:                 Duration,
-  #[limited]    pub target_requests_outstanding:  u32,
-
-  // Ordinary settings:
-  pub addrs:                        Vec<IpAddr>,
-  pub vnetwork:                     Vec<IpNet>,
-  pub vaddr:                        Vec<IpAddr>,
-  pub vrelay:                       IpAddr,
-  pub port:                         u16,
-  pub mtu:                          u32,
-  pub ifname_server:                String,
-  pub ifname_client:                String,
-
-  // Ordinary settings, used by server only:
-  #[server]  pub max_clock_skew:               Duration,
-
-  // Ordinary settings, used by client only:
-  #[client]  pub http_timeout_grace:           Duration,
-  #[client]  pub max_requests_outstanding:     u32,
-  #[client]  pub max_batch_up:                 u32,
-  #[client]  pub http_retry:                   Duration,
-  #[client]  pub url:                          Uri,
-  #[client]  pub vroutes:                      Vec<IpNet>,
-}
-
 #[derive(Debug,Clone,Hash,Eq,PartialEq)]
 pub enum SectionName {
   Link(LinkName),
@@ -126,6 +126,7 @@ pub use SectionName as SN;
 struct RawVal { raw: Option<String>, loc: Arc<PathBuf> }
 type SectionMap = HashMap<String, RawVal>;
 
+#[derive(Debug)]
 struct RawValRef<'v,'l,'s> {
   raw: Option<&'v str>,
   key: &'static str,
@@ -138,13 +139,13 @@ impl<'v> RawValRef<'v,'_,'_> {
   fn try_map<F,T>(&self, f: F) -> T
   where F: FnOnce(Option<&'v str>) -> Result<T, AE> {
     f(self.raw)
-      .with_context(|| format!(r#"file {:?}, section "{:?}", key "{}"#,
+      .with_context(|| format!(r#"file {:?}, section {}, key "{}""#,
                                self.loc, self.section, self.key))?
   }
 }
 
 pub struct Config {
-  opts: Opts,
+  pub opts: Opts,
 }
 
 static OUTSIDE_SECTION: &str = "[";
@@ -152,6 +153,7 @@ static SPECIAL_SERVER_SECTION: &str = "SERVER";
 
 #[derive(Default,Debug)]
 struct Aggregate {
+  keys_allowed: HashMap<&'static str, SectionKindList>,
   sections: HashMap<SectionName, SectionMap>,
 }
 
@@ -189,6 +191,24 @@ impl FromStr for SectionName {
     SN::Link(LinkName { server, client })
   }
 }
+impl Display for InstanceConfig {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) { Display::fmt(&self.link, f)? }
+}
+
+impl Display for SectionName {
+  #[throws(fmt::Error)]
+  fn fmt(&self, f: &mut fmt::Formatter) {
+    match self {
+      SN::Link  (ref l)      => Display::fmt(l, f)?,
+      SN::Client(ref c)      => write!(f, "[{}]"       , c)?,
+      SN::Server(ref s)      => write!(f, "[{}]"       , s)?,
+      SN::ServerLimit(ref s) => write!(f, "[{} LIMIT] ", s)?,
+      SN::GlobalLimit        => write!(f, "[LIMIT]"       )?,
+      SN::Common             => write!(f, "[COMMON]"      )?,
+    }
+  }
+}
 
 impl Aggregate {
   #[throws(AE)] // AE does not include path
@@ -220,20 +240,44 @@ impl Aggregate {
     let loc = Arc::new(path_for_loc.to_owned());
 
     for (sn, vars) in map {
-      //dbg!( InstanceConfig::FIELDS );// check xxx vars are in fields
-
       let sn = sn.parse().dcontext(&sn)?;
-        self.sections.entry(sn)
-        .or_default()
-        .extend(
-          vars.into_iter()
-            .map(|(k,raw)| {
-              (k.replace('-',"_"),
-               RawVal { raw, loc: loc.clone() })
-            })
-        );
+
+      for key in vars.keys() {
+        let skl = self.keys_allowed.get(key.as_str()).ok_or_else(
+          || anyhow!("unknown configuration key {:?}", key)
+        )?;
+        if ! skl.contains(&sn) {
+          throw!(anyhow!("configuration key {:?} not applicable \
+                          in this kind of section {:?}", key, &sn))
+        }
+      }
+
+      let ent = self.sections.entry(sn).or_default();
+      for (key, raw) in vars {
+        let raw = match raw {
+          Some(raw) if raw.starts_with('\'') || raw.starts_with('"') => Some(
+            (||{
+              if raw.contains('\\') {
+                throw!(
+                  anyhow!("quoted value contains backslash, not supported")
+                );
+              }
+              let unq = raw[1..].strip_suffix(&raw[0..1])
+                .ok_or_else(
+                  || anyhow!("mismatched quotes around quoted value")
+                )?
+                .to_owned();
+              Ok::<_,AE>(unq)
+            })()
+              .with_context(|| format!("key {:?}", key))
+              .dcontext(path_for_loc)?
+          ),
+          x => x,
+        };
+        let key = key.replace('-',"_");
+        ent.insert(key, RawVal { raw, loc: loc.clone() });
+      }
     }
-    
   }
 
   #[throws(AE)] // AE includes path
@@ -564,18 +608,18 @@ impl<'c> ResolveContext<'c> {
 
   #[throws(AE)]
   pub fn client<T>(&self, key: &'static str) -> T
-  where T: Parseable {
+  where T: Parseable + Default {
     match self.end {
       LinkEnd::Client => self.ordinary(key)?,
-      LinkEnd::Server => Parseable::default_for_key(key)?,
+      LinkEnd::Server => default(),
     }
   }
   #[throws(AE)]
   pub fn server<T>(&self, key: &'static str) -> T
-  where T: Parseable {
+  where T: Parseable + Default {
     match self.end {
       LinkEnd::Server => self.ordinary(key)?,
-      LinkEnd::Client => Parseable::default_for_key(key)?,
+      LinkEnd::Client => default(),
     }
   }
 
@@ -591,8 +635,112 @@ impl<'c> ResolveContext<'c> {
   }
 
   #[throws(AE)]
-  pub fn special_server(&self, key: &'static str) -> ServerName {
-    self.link.server.clone()
+  pub fn special_link(&self, _key: &'static str) -> LinkName {
+    self.link.clone()
+  }
+}
+
+impl InstanceConfig {
+  #[throws(AE)]
+  fn complete(&mut self, end: LinkEnd) {
+    let mut vhosts = self.vnetwork.iter()
+      .map(|n| n.hosts()).flatten()
+      .filter({ let vaddr = self.vaddr; move |v| v != &vaddr });
+
+    if self.vaddr.is_unspecified() {
+      self.vaddr = vhosts.next().ok_or_else(
+        || anyhow!("vnetwork too small to generate vaddrr")
+      )?;
+    }
+    if self.vrelay.is_unspecified() {
+      self.vrelay = vhosts.next().ok_or_else(
+        || anyhow!("vnetwork too small to generate vrelay")
+      )?;
+    }
+
+    match end {
+      LinkEnd::Client => {
+        if &self.url == &default::<Uri>() {
+          let addr = self.addrs.get(0).ok_or_else(
+            || anyhow!("client needs addrs or url set")
+          )?;
+          self.url = format!(
+            "http://{}{}/",
+            match addr {
+              IpAddr::V4(a) => format!("{}", a),
+              IpAddr::V6(a) => format!("[{}]", a),
+            },
+            match self.port {
+              80 => format!(""),
+              p => format!(":{}", p),
+            })
+            .parse().unwrap()
+        }
+      },
+
+      LinkEnd::Server => {
+        if self.addrs.is_empty() {
+          throw!(anyhow!("missing 'addrs' setting"))
+        }
+      },
+    }
+
+    #[throws(AE)]
+    fn subst(var: &mut String,
+             kv: &mut dyn Iterator<Item=(&'static str, &dyn Display)>
+    ) {
+      let substs = kv
+        .map(|(k,v)| (k.to_string(), v.to_string()))
+        .collect::<HashMap<String, String>>();
+      let bad = parking_lot::Mutex::new(vec![]);
+      *var = regex_replace_all!(
+        r#"%(?:%|\((\w+)\)s|\{(\w+)\}|.)"#,
+        &var,
+        |whole, k1, k2| (|| Ok::<_,String>({
+          if whole == "%%" { "%" }
+          else if let Some(&k) = [k1,k2].iter().find(|&&s| s != "") {
+            substs.get(k).ok_or_else(
+              || format!("unknown key %({})s", k)
+            )?
+          } else {
+            throw!(format!("bad percent escape {:?}", &whole));
+          }
+        }))().unwrap_or_else(|e| { bad.lock().push(e); "" })
+      ).into_owned();
+      let bad = bad.into_inner();
+      if ! bad.is_empty() {
+        throw!(anyhow!("substitution failed: {}", bad.iter().format("; ")));
+      }
+    }
+
+    {
+      use LinkEnd::*;
+      type DD<'d> = &'d dyn Display;
+      fn dv<T:Display>(v: &[T]) -> String {
+        format!("{}", v.iter().format(" "))
+      }
+      let mut ipif = mem::take(&mut self.ipif); // lets us borrow all of self
+      let s = &self; // just for abbreviation, below
+      let vnetwork = dv(&s.vnetwork);
+      let vroutes  = dv(&s.vroutes);
+
+      let keys = &["local",       "peer",    "rnets",   "ifname"];
+      let values = match end {
+ Server => [&s.vaddr as DD      , &s.vrelay, &vnetwork, &s.ifname_server],
+ Client => [&s.link.client as DD, &s.vaddr,  &vroutes,  &s.ifname_client],
+      };
+      let always = [
+        ( "mtu",     &s.mtu as DD ),
+      ];
+
+      subst(
+        &mut ipif,
+        &mut keys.iter().cloned()
+          .zip_eq(values)
+          .chain(always.iter().cloned()),
+      ).context("ipif")?;
+      self.ipif = ipif;
+    }
   }
 }
 
@@ -602,6 +750,9 @@ pub fn read(end: LinkEnd) -> Vec<InstanceConfig> {
 
   let agg = (||{
     let mut agg = Aggregate::default();
+    agg.keys_allowed.extend(
+      InstanceConfig::FIELDS.iter().cloned()
+    );
 
     agg.read_string(DEFAULT_CONFIG.into(),
                     "<build-in defaults>".as_ref()).unwrap();
@@ -611,7 +762,7 @@ pub fn read(end: LinkEnd) -> Vec<InstanceConfig> {
       agg.read_extra(extra).context("extra config")?;
     }
 
-    eprintln!("GOT {:#?}", agg);
+    //eprintln!("GOT {:#?}", agg);
 
     Ok::<_,AE>(agg)
   })().context("read configuration")?;
@@ -639,8 +790,11 @@ pub fn read(end: LinkEnd) -> Vec<InstanceConfig> {
       ],
     };
 
-    let ic = InstanceConfig::resolve_instance(&rctx)
-      .with_context(|| format!(r#"resolve config for "{:?}""#, &link))?;
+    let mut ic = InstanceConfig::resolve_instance(&rctx)
+      .with_context(|| format!("resolve config for {}", &link))?;
+
+    ic.complete(end)
+      .with_context(|| format!("complete config for {}", &link))?;
 
     ics.push(ic);
   }