chiark / gitweb /
Move ssh key stuff out of otter.rs
authorIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 2 Jun 2021 23:09:39 +0000 (00:09 +0100)
committerIan Jackson <ijackson@chiark.greenend.org.uk>
Wed, 2 Jun 2021 23:10:01 +0000 (00:10 +0100)
Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
cli/forssh.rs [new file with mode: 0644]
cli/otter.rs

diff --git a/cli/forssh.rs b/cli/forssh.rs
new file mode 100644 (file)
index 0000000..e19266b
--- /dev/null
@@ -0,0 +1,308 @@
+// Copyright 2020-2021 Ian Jackson and contributors to Otter
+// SPDX-License-Identifier: AGPL-3.0-or-later
+// There is NO WARRANTY.
+
+use super::*;
+
+//---------- mgmtchannel-proxy ----------
+
+mod mgmtchannel_proxy {
+  use super::*;
+
+  #[derive(Default,Debug)]
+  struct Args {
+    restrict: Option<sshkeys::KeySpec>,
+  }
+
+  fn subargs(sa: &mut Args) -> ArgumentParser {
+    use argparse::*;
+    let mut ap = ArgumentParser::new();
+    ap.refer(&mut sa.restrict).metavar("ID:NONCE")
+      .add_option(&["--restrict-ssh"],StoreOption,
+                  "restrict to access available to registred OpenSSH key \
+                   (used in runes generated by server for authorized_keys)");
+    ap
+  }
+
+  #[throws(AE)]
+  fn call(SCCA{ out, ma, args,.. }:SCCA) {
+    set_program_name("otter (remote)".into());
+
+    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
+    let mut chan = connect_chan(&ma)?;
+
+    if let Some(ref restrict) = args.restrict {
+      chan.cmd(&MC::SetRestrictedSshScope { key: restrict.clone() })
+        .context("specify authorisation")?;
+    }
+
+    let MgmtChannel { read, write } = chan;
+    let mut read = read.into_stream()?;
+    let mut write = write.into_stream()?;
+
+    drop(out); // do our own stdout
+
+    let tcmds = thread::spawn(move || {
+      io_copy_interactive(&mut BufReader::new(io::stdin()), &mut write)
+        .map_err(|e| match e {
+          Left(re)  => AE::from(re).context("read cmds from stdin"),
+          Right(we) => AE::from(we).context("forward cmds to servvr"),
+        })
+        .unwrap_or_else(|e| e.end_process(8));
+      exit(0);
+    });
+    let tresps = thread::spawn(move || {
+      io_copy_interactive(&mut read, &mut RawStdout::new())
+        .map_err(|e| match e {
+          Left(re)  => AE::from(re).context("read resps from server"),
+          Right(we) => AE::from(we).context("forward cmds to stdout"),
+        })
+        .context("copy responses")
+        .unwrap_or_else(|e| e.end_process(8));
+      exit(0);
+    });
+    tcmds.join().expect("collect commands copy"); 
+    tresps.join().expect("collect responses copy");
+  }
+
+  inventory_subcmd!{
+    SSH_PROXY_SUBCMD,
+    "connect to management channel and copy raw message data back and forth",
+    suppress_selectaccount: true,
+  }
+}
+
+//---------- set-ssh-keys ----------
+
+mod set_ssh_keys {
+  use super::*;
+
+  #[derive(Default,Debug)]
+  struct Args {
+    add: bool,
+    allow_non_ssh: bool,
+    remove_current: bool,
+    keys: String,
+  }
+
+  fn subargs(sa: &mut Args) -> ArgumentParser {
+    use argparse::*;
+    let mut ap = ArgumentParser::new();
+    ap.refer(&mut sa.add)
+      .add_option(&["--add"],StoreTrue,
+                  "add keys, only (ie, leave all existing keys)");
+    ap.refer(&mut sa.allow_non_ssh)
+      .add_option(&["--allow-non-ssh-account"],StoreTrue,
+                  "allow settings ssh key access for a non-ssh: account");
+    ap.refer(&mut sa.remove_current)
+      .add_option(&["--allow-remove-current"],StoreTrue,
+                  "allow removing the key currently being used for access");
+    ap.refer(&mut sa.keys).required()
+      .add_argument("KEYS-FILE", Store,
+                  "file of keys, in authorized_keys formaat \
+                   (`-` means stdin)");
+    ap
+  }
+
+  #[throws(AE)]
+  fn call(SCCA{ ma, args,.. }:SCCA) {
+    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
+    let mut conn = connect(&ma)?;
+
+    if ! ma.account.subaccount.is_empty() {
+      throw!(ME::NoSshKeysForSubaccount);
+    }
+    let is_ssh_account = matches!(ma.account.scope, AS::Ssh{..});
+    if ! (args.allow_non_ssh || is_ssh_account) {
+      throw!(anyhow!("not setting ssh keys for non-ssh: account; \
+                      use --allow-non-ssh-account to override"));
+    }
+
+    conn.prep_access_account(&ma, false)?;
+
+    use sshkeys::*;
+
+    #[derive(Debug)]
+    struct Currently {
+      index: usize,
+      mkr: MgmtKeyReport,
+      thisconn_retain: bool,
+    }
+
+    #[derive(Debug,Default)]
+    struct St {
+      wanted: Option<(usize, AuthkeysLine)>,
+      currently: Vec<Currently>,
+    }
+    type PubDataString = String;
+    let mut states: HashMap<PubDataString, St> = default();
+
+    // read wanted set
+
+    let akf: Box<dyn Read> = if args.keys == "-" { Box::new(io::stdin()) }
+    else {
+      Box::new(File::open(&args.keys)
+        .with_context(|| args.keys.clone())
+        .context("KEYS-FILE")?)
+    };
+    let akf = BufReader::new(akf);
+
+    for (l, lno) in akf.lines().zip(1..) {
+      let l = l.context("read KEYS-FILE")?;
+      let l = l.trim();
+      if l.starts_with("#") || l == "" { continue }
+      let l = AuthkeysLine(l.to_owned());
+      let (pubdata, _comment) = l.parse()
+        .with_context(|| format!("parse KEYS-FILE line {}", lno))?;
+      let st = states.entry(pubdata.to_string()).or_insert_with(default);
+      if let Some((lno0,_)) = st.wanted { throw!(
+        anyhow!("KEYS-FILE has duplicate key, lines {} {}", lno0, lno)
+      )}
+      st.wanted = Some((lno, l));
+    }
+
+    // find the one we're using now
+
+    let using = if args.remove_current { None } else {
+      use MgmtResponse::ThisConnAuthBy as TCAB;
+      use MgmtThisConnAuthBy as MTCAB;
+      match conn.cmd(&MC::ThisConnAuthBy).context("find current auth")? {
+        TCAB(MTCAB::Ssh { key }) => Some(key),
+        TCAB(MTCAB::Local) => None,
+        #[allow(unreachable_patterns)] TCAB(q) => throw!(anyhow!(
+          "unexpected ThisConnAuthBy {:?}, \
+           cannot check if we are removing the current authentication, \
+           (and --allow-remove-current not specified)", q)),
+        _ => throw!(anyhow!("unexpected response to ThisConnAuthBy")),
+      }
+    };
+
+    // obtain current set
+
+    for (index, mkr) in match conn.cmd(&MC::SshListKeys)
+      .context("list existing keys")?
+    {
+      MR::SshKeys(report) => report,
+      _ => throw!(anyhow!("unexpected response to SshListKeys")),
+    }
+      .into_iter().enumerate()
+    {
+      let pubdata_s = mkr.data.to_string();
+      let st = states.entry(pubdata_s).or_insert_with(default);
+      let thisconn_retain = Some(&mkr.key) == using.as_ref();
+      st.currently.push(Currently { index, mkr, thisconn_retain });
+    }
+
+    // check we don't want to bail
+
+    for st in states.values() {
+      if st.wanted.is_none() {
+        for c in &st.currently {
+          if c.thisconn_retain {
+            throw!(anyhow!(
+              "refusing to remove currently-being-used key #{} {}",
+              c.index, &c.mkr));
+          }
+        }
+      }
+    }
+
+    // delete any with problems
+    // doing this even for wanted keys prevents constant buildup
+    // of problem keys
+
+    for st in states.values() {
+      for c in &st.currently {
+        if c.mkr.problem.is_some() {
+          conn.cmd(&MC::SshDeleteKey { index: c.index, id: c.mkr.key.id })
+            .with_context(|| format!("delete broken key #{}", c.index))?;
+        }
+      }
+    }
+
+    // add new keys
+
+    for st in states.values() {
+      if_let!{ Some((lno, akl)) = &st.wanted; else continue };
+      if st.currently.iter().any(|c| c.mkr.problem.is_none()) { continue }
+      conn.cmd(&MC::SshAddKey { akl: akl.clone() })
+        .with_context(|| format!("add key from KEYS-FILE line {}", lno))?;
+    }
+
+    // delete old keys
+
+    for st in states.values() {
+      if st.wanted.is_some() { continue }
+      for c in &st.currently {
+        if c.mkr.problem.is_some() { continue /* we deleted it already */ }
+        conn.cmd(&MC::SshDeleteKey { index: c.index, id: c.mkr.key.id })
+          .with_context(|| format!("delete old key #{}", c.index))?;
+      }
+    }
+  }
+
+  inventory_subcmd!{
+    "set-ssh-keys",
+    "set SSH keys for remote management access authentication",
+  }
+}
+
+//---------- list-ssh-keys ----------
+
+mod list_ssh_keys {
+  use super::*;
+
+  type Args = NoArgs;
+
+  #[throws(AE)]
+  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
+    let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
+    let mut conn = connect(&ma)?;
+    conn.prep_access_account(&ma, false)?;
+
+    use sshkeys::*;
+
+    // find the one we're using now
+
+    let using = {
+      use MgmtResponse::ThisConnAuthBy as TCAB;
+      use MgmtThisConnAuthBy as MTCAB;
+      match conn.cmd(&MC::ThisConnAuthBy).context("find current auth")? {
+        TCAB(MTCAB::Ssh { key }) => Some(key),
+        TCAB(_) => None,
+        _ => throw!(anyhow!("unexpected response to ThisConnAuthBy")),
+      }
+    };
+
+    // obtain current set
+
+    for (_index, mkr) in match conn.cmd(&MC::SshListKeys)
+      .context("list existing keys")?
+    {
+      MR::SshKeys(report) => report,
+      _ => throw!(anyhow!("unexpected response to SshListKeys")),
+    }
+      .into_iter().enumerate()
+    {
+      let s = mkr.to_string();
+      use unicode_width::UnicodeWidthChar;
+      if s.chars().any(|c| c.width() == None /* control char */) {
+        write!(&mut out, "# FUNKY! # {:?}", &s)?;
+      } else {
+        write!(&mut out, "{}", &s)?;
+      }
+
+      if Some(&mkr.key) == using.as_ref() {
+        write!(&mut out, "# <- this connection!")?;
+      }
+      writeln!(&mut out, "")?;
+    }
+
+    out.flush()?;
+  }
+
+  inventory_subcmd!{
+    "list-ssh-keys",
+    "set SSH keys for remote management access authentication",
+  }
+}
index 417265723487830a7c0b9e6711d332186384b6d1..864e704efb6f49c634adbeb8516f8439241faf83 100644 (file)
@@ -30,6 +30,7 @@ pub mod clisupport;
 use clisupport::*;
 
 mod adhoc;
+mod forssh;
 mod forgame;
 mod usebundles;
 mod uselibs;
@@ -361,306 +362,3 @@ mod list_accounts {
     "List accounts in your account scope",
   }
 }
-
-//---------- mgmtchannel-proxy ----------
-
-mod mgmtchannel_proxy {
-  use super::*;
-
-  #[derive(Default,Debug)]
-  struct Args {
-    restrict: Option<sshkeys::KeySpec>,
-  }
-
-  fn subargs(sa: &mut Args) -> ArgumentParser {
-    use argparse::*;
-    let mut ap = ArgumentParser::new();
-    ap.refer(&mut sa.restrict).metavar("ID:NONCE")
-      .add_option(&["--restrict-ssh"],StoreOption,
-                  "restrict to access available to registred OpenSSH key \
-                   (used in runes generated by server for authorized_keys)");
-    ap
-  }
-
-  #[throws(AE)]
-  fn call(SCCA{ out, ma, args,.. }:SCCA) {
-    set_program_name("otter (remote)".into());
-
-    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
-    let mut chan = connect_chan(&ma)?;
-
-    if let Some(ref restrict) = args.restrict {
-      chan.cmd(&MC::SetRestrictedSshScope { key: restrict.clone() })
-        .context("specify authorisation")?;
-    }
-
-    let MgmtChannel { read, write } = chan;
-    let mut read = read.into_stream()?;
-    let mut write = write.into_stream()?;
-
-    drop(out); // do our own stdout
-
-    let tcmds = thread::spawn(move || {
-      io_copy_interactive(&mut BufReader::new(io::stdin()), &mut write)
-        .map_err(|e| match e {
-          Left(re)  => AE::from(re).context("read cmds from stdin"),
-          Right(we) => AE::from(we).context("forward cmds to servvr"),
-        })
-        .unwrap_or_else(|e| e.end_process(8));
-      exit(0);
-    });
-    let tresps = thread::spawn(move || {
-      io_copy_interactive(&mut read, &mut RawStdout::new())
-        .map_err(|e| match e {
-          Left(re)  => AE::from(re).context("read resps from server"),
-          Right(we) => AE::from(we).context("forward cmds to stdout"),
-        })
-        .context("copy responses")
-        .unwrap_or_else(|e| e.end_process(8));
-      exit(0);
-    });
-    tcmds.join().expect("collect commands copy"); 
-    tresps.join().expect("collect responses copy");
-  }
-
-  inventory_subcmd!{
-    SSH_PROXY_SUBCMD,
-    "connect to management channel and copy raw message data back and forth",
-    suppress_selectaccount: true,
-  }
-}
-
-//---------- set-ssh-keys ----------
-
-mod set_ssh_keys {
-  use super::*;
-
-  #[derive(Default,Debug)]
-  struct Args {
-    add: bool,
-    allow_non_ssh: bool,
-    remove_current: bool,
-    keys: String,
-  }
-
-  fn subargs(sa: &mut Args) -> ArgumentParser {
-    use argparse::*;
-    let mut ap = ArgumentParser::new();
-    ap.refer(&mut sa.add)
-      .add_option(&["--add"],StoreTrue,
-                  "add keys, only (ie, leave all existing keys)");
-    ap.refer(&mut sa.allow_non_ssh)
-      .add_option(&["--allow-non-ssh-account"],StoreTrue,
-                  "allow settings ssh key access for a non-ssh: account");
-    ap.refer(&mut sa.remove_current)
-      .add_option(&["--allow-remove-current"],StoreTrue,
-                  "allow removing the key currently being used for access");
-    ap.refer(&mut sa.keys).required()
-      .add_argument("KEYS-FILE", Store,
-                  "file of keys, in authorized_keys formaat \
-                   (`-` means stdin)");
-    ap
-  }
-
-  #[throws(AE)]
-  fn call(SCCA{ ma, args,.. }:SCCA) {
-    let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
-    let mut conn = connect(&ma)?;
-
-    if ! ma.account.subaccount.is_empty() {
-      throw!(ME::NoSshKeysForSubaccount);
-    }
-    let is_ssh_account = matches!(ma.account.scope, AS::Ssh{..});
-    if ! (args.allow_non_ssh || is_ssh_account) {
-      throw!(anyhow!("not setting ssh keys for non-ssh: account; \
-                      use --allow-non-ssh-account to override"));
-    }
-
-    conn.prep_access_account(&ma, false)?;
-
-    use sshkeys::*;
-
-    #[derive(Debug)]
-    struct Currently {
-      index: usize,
-      mkr: MgmtKeyReport,
-      thisconn_retain: bool,
-    }
-
-    #[derive(Debug,Default)]
-    struct St {
-      wanted: Option<(usize, AuthkeysLine)>,
-      currently: Vec<Currently>,
-    }
-    type PubDataString = String;
-    let mut states: HashMap<PubDataString, St> = default();
-
-    // read wanted set
-
-    let akf: Box<dyn Read> = if args.keys == "-" { Box::new(io::stdin()) }
-    else {
-      Box::new(File::open(&args.keys)
-        .with_context(|| args.keys.clone())
-        .context("KEYS-FILE")?)
-    };
-    let akf = BufReader::new(akf);
-
-    for (l, lno) in akf.lines().zip(1..) {
-      let l = l.context("read KEYS-FILE")?;
-      let l = l.trim();
-      if l.starts_with("#") || l == "" { continue }
-      let l = AuthkeysLine(l.to_owned());
-      let (pubdata, _comment) = l.parse()
-        .with_context(|| format!("parse KEYS-FILE line {}", lno))?;
-      let st = states.entry(pubdata.to_string()).or_insert_with(default);
-      if let Some((lno0,_)) = st.wanted { throw!(
-        anyhow!("KEYS-FILE has duplicate key, lines {} {}", lno0, lno)
-      )}
-      st.wanted = Some((lno, l));
-    }
-
-    // find the one we're using now
-
-    let using = if args.remove_current { None } else {
-      use MgmtResponse::ThisConnAuthBy as TCAB;
-      use MgmtThisConnAuthBy as MTCAB;
-      match conn.cmd(&MC::ThisConnAuthBy).context("find current auth")? {
-        TCAB(MTCAB::Ssh { key }) => Some(key),
-        TCAB(MTCAB::Local) => None,
-        #[allow(unreachable_patterns)] TCAB(q) => throw!(anyhow!(
-          "unexpected ThisConnAuthBy {:?}, \
-           cannot check if we are removing the current authentication, \
-           (and --allow-remove-current not specified)", q)),
-        _ => throw!(anyhow!("unexpected response to ThisConnAuthBy")),
-      }
-    };
-
-    // obtain current set
-
-    for (index, mkr) in match conn.cmd(&MC::SshListKeys)
-      .context("list existing keys")?
-    {
-      MR::SshKeys(report) => report,
-      _ => throw!(anyhow!("unexpected response to SshListKeys")),
-    }
-      .into_iter().enumerate()
-    {
-      let pubdata_s = mkr.data.to_string();
-      let st = states.entry(pubdata_s).or_insert_with(default);
-      let thisconn_retain = Some(&mkr.key) == using.as_ref();
-      st.currently.push(Currently { index, mkr, thisconn_retain });
-    }
-
-    // check we don't want to bail
-
-    for st in states.values() {
-      if st.wanted.is_none() {
-        for c in &st.currently {
-          if c.thisconn_retain {
-            throw!(anyhow!(
-              "refusing to remove currently-being-used key #{} {}",
-              c.index, &c.mkr));
-          }
-        }
-      }
-    }
-
-    // delete any with problems
-    // doing this even for wanted keys prevents constant buildup
-    // of problem keys
-
-    for st in states.values() {
-      for c in &st.currently {
-        if c.mkr.problem.is_some() {
-          conn.cmd(&MC::SshDeleteKey { index: c.index, id: c.mkr.key.id })
-            .with_context(|| format!("delete broken key #{}", c.index))?;
-        }
-      }
-    }
-
-    // add new keys
-
-    for st in states.values() {
-      if_let!{ Some((lno, akl)) = &st.wanted; else continue };
-      if st.currently.iter().any(|c| c.mkr.problem.is_none()) { continue }
-      conn.cmd(&MC::SshAddKey { akl: akl.clone() })
-        .with_context(|| format!("add key from KEYS-FILE line {}", lno))?;
-    }
-
-    // delete old keys
-
-    for st in states.values() {
-      if st.wanted.is_some() { continue }
-      for c in &st.currently {
-        if c.mkr.problem.is_some() { continue /* we deleted it already */ }
-        conn.cmd(&MC::SshDeleteKey { index: c.index, id: c.mkr.key.id })
-          .with_context(|| format!("delete old key #{}", c.index))?;
-      }
-    }
-  }
-
-  inventory_subcmd!{
-    "set-ssh-keys",
-    "set SSH keys for remote management access authentication",
-  }
-}
-
-//---------- list-ssh-keys ----------
-
-mod list_ssh_keys {
-  use super::*;
-
-  type Args = NoArgs;
-
-  #[throws(AE)]
-  fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
-    let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
-    let mut conn = connect(&ma)?;
-    conn.prep_access_account(&ma, false)?;
-
-    use sshkeys::*;
-
-    // find the one we're using now
-
-    let using = {
-      use MgmtResponse::ThisConnAuthBy as TCAB;
-      use MgmtThisConnAuthBy as MTCAB;
-      match conn.cmd(&MC::ThisConnAuthBy).context("find current auth")? {
-        TCAB(MTCAB::Ssh { key }) => Some(key),
-        TCAB(_) => None,
-        _ => throw!(anyhow!("unexpected response to ThisConnAuthBy")),
-      }
-    };
-
-    // obtain current set
-
-    for (_index, mkr) in match conn.cmd(&MC::SshListKeys)
-      .context("list existing keys")?
-    {
-      MR::SshKeys(report) => report,
-      _ => throw!(anyhow!("unexpected response to SshListKeys")),
-    }
-      .into_iter().enumerate()
-    {
-      let s = mkr.to_string();
-      use unicode_width::UnicodeWidthChar;
-      if s.chars().any(|c| c.width() == None /* control char */) {
-        write!(&mut out, "# FUNKY! # {:?}", &s)?;
-      } else {
-        write!(&mut out, "{}", &s)?;
-      }
-
-      if Some(&mkr.key) == using.as_ref() {
-        write!(&mut out, "# <- this connection!")?;
-      }
-      writeln!(&mut out, "")?;
-    }
-
-    out.flush()?;
-  }
-
-  inventory_subcmd!{
-    "list-ssh-keys",
-    "set SSH keys for remote management access authentication",
-  }
-}