diff --git a/async/examples/sshclient.rs b/async/examples/sshclient.rs index c6c65e973d61dc2ae7362d68132da97c04ad2212..295f4d875bd9d05e94b60d31c61026d3bd6d8faf 100644 --- a/async/examples/sshclient.rs +++ b/async/examples/sshclient.rs @@ -3,7 +3,7 @@ use { // crate::error::Error, log::{debug, error, info, log, trace, warn}, }; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; use embassy_sync::{mutex::Mutex, blocking_mutex::raw::NoopRawMutex}; use tokio::net::TcpStream; @@ -58,6 +58,10 @@ struct Args { /// force no pty force_no_pty: bool, + #[argh(option, short='s')] + /// ssh subsystem (eg "sftp") + subsystem: Option<String>, + #[argh(positional)] /// command cmd: Vec<String>, @@ -117,8 +121,8 @@ fn setup_log(args: &Args) -> Result<()> { .add_filter_allow_str("sshclient") // not debugging these bits of the stack at present // .add_filter_ignore_str("sunset::traffic") - .add_filter_ignore_str("sunset::runner") - .add_filter_ignore_str("sunset_embassy") + // .add_filter_ignore_str("sunset::runner") + // .add_filter_ignore_str("sunset_embassy") .set_time_offset_to_local().expect("Couldn't get local timezone") .build(); @@ -181,13 +185,25 @@ async fn run(args: Args) -> Result<()> { trace!("tracing main"); debug!("verbose main"); - let (cmd, wantpty) = if args.cmd.is_empty() { - (None, true) + if !args.cmd.is_empty() && args.subsystem.is_some() { + bail!("can't have '-s subsystem' with a command") + } + + let mut want_pty = true; + let cmd = if args.cmd.is_empty() { + None } else { - (Some(args.cmd.join(" ")), false) + want_pty = false; + Some(args.cmd.join(" ")) }; - let wantpty = wantpty && !args.force_no_pty; + if args.subsystem.is_some() { + want_pty = false; + } + + if args.force_no_pty { + want_pty = false + } let ssh_task = spawn_local(async move { let mut rxbuf = Zeroizing::new(vec![0; 3000]); @@ -197,17 +213,26 @@ async fn run(args: Args) -> Result<()> { let mut app = CmdlineClient::new( args.username.as_ref().unwrap(), &args.host, - args.port, - cmd, - wantpty, - ); + ); + + app.port(args.port); + + if want_pty { + app.pty(); + } + if let Some(c) = cmd { + app.exec(&c); + } + if let Some(c) = args.subsystem { + app.subsystem(&c); + } for i in &args.identityfile { app.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?); } let agent = load_agent_keys(&mut app).await; if let Some(agent) = agent { - app.set_agent(agent) + app.agent(agent); } // Connect to a peer diff --git a/async/src/cmdline_client.rs b/async/src/cmdline_client.rs index 198d1a3aecbff61594b4afd5cd2bd9deb747970b..ecc54d9ce0b928c8569fd9e42dda62cae038df84 100644 --- a/async/src/cmdline_client.rs +++ b/async/src/cmdline_client.rs @@ -5,9 +5,9 @@ use log::{debug, error, info, log, trace, warn}; use core::str::FromStr; use core::fmt::Debug; -use sunset::{AuthSigMsg, SignKey, OwnedSig}; +use sunset::{AuthSigMsg, SignKey, OwnedSig, Pty, sshnames}; use sunset::{BhError, BhResult}; -use sunset::{ChanMsg, ChanMsgDetails, Error, Result, Runner}; +use sunset::{Error, Result, Runner, SessionCommand}; use sunset::behaviour::{UnusedCli, UnusedServ}; use sunset_embassy::*; @@ -32,6 +32,10 @@ use crate::pty::win_size; enum CmdlineState<'a> { PreAuth, Authed, + Opening { + io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>, + extin: Option<ChanIn<'a, CmdlineHooks<'a>, UnusedServ>>, + }, Ready { io: ChanInOut<'a, CmdlineHooks<'a>, UnusedServ>, extin: Option<ChanIn<'a, CmdlineHooks<'a>, UnusedServ>>, @@ -40,12 +44,13 @@ enum CmdlineState<'a> { enum Msg { Authed, + Opened, /// The SSH session exited Exited, } pub struct CmdlineClient { - cmd: Option<String>, + cmd: SessionCommand<String>, want_pty: bool, // to be passed to hooks @@ -56,13 +61,12 @@ pub struct CmdlineClient { agent: Option<AgentClient>, notify: Channel<SunsetRawMutex, Msg, 1>, + pty_guard: Option<RawPtyGuard>, } pub struct CmdlineRunner<'a> { state: CmdlineState<'a>, - pty_guard: Option<RawPtyGuard>, - cmd: &'a Option<String>, want_pty: bool, notify: Receiver<'a, SunsetRawMutex, Msg, 1>, @@ -74,22 +78,109 @@ pub struct CmdlineHooks<'a> { host: &'a str, port: u16, agent: Option<AgentClient>, + cmd: &'a SessionCommand<String>, + pty: Option<Pty>, notify: Sender<'a, SunsetRawMutex, Msg, 1>, } -impl<'a> Debug for CmdlineHooks<'a> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str("CmdlineHooks") +impl CmdlineClient { + pub fn new(username: impl AsRef<str>, host: impl AsRef<str>) -> Self { + Self { + cmd: SessionCommand::Shell, + want_pty: false, + agent: None, + + notify: Channel::new(), + pty_guard: None, + + username: username.as_ref().into(), + host: host.as_ref().into(), + port: sshnames::SSH_PORT, + authkeys: Default::default(), + } } + + pub fn split(&mut self) -> (CmdlineHooks, CmdlineRunner) { + + let pty = self.make_pty(); + + let authkeys = core::mem::replace(&mut self.authkeys, Default::default()); + + let runner = CmdlineRunner::new(pty.is_some(), self.notify.receiver()); + + let hooks = CmdlineHooks { + username: &self.username, + host: &self.host, + port: self.port, + authkeys, + agent: self.agent.take(), + cmd: &self.cmd, + pty, + notify: self.notify.sender(), + }; + + (hooks, runner) + } + + pub fn port(&mut self, port: u16) -> &mut Self { + self.port = port; + self + } + + pub fn pty(&mut self) -> &mut Self { + self.want_pty = true; + self + } + + pub fn exec(&mut self, cmd: &str) -> &mut Self { + self.cmd = SessionCommand::Exec(cmd.into()); + self + } + + pub fn subsystem(&mut self, subsystem: &str) -> &mut Self { + self.cmd = SessionCommand::Subsystem(subsystem.into()); + self + } + + pub fn add_authkey(&mut self, k: SignKey) { + self.authkeys.push_back(k) + } + + pub fn agent(&mut self, agent: AgentClient) { + self.agent = Some(agent) + } + + fn make_pty(&mut self) -> Option<Pty> { + let mut pty = None; + if self.want_pty { + match pty::current_pty() { + Ok(p) => pty = Some(p), + Err(e) => warn!("Failed getting current pty: {e:?}"), + } + + if pty.is_some() { + // switch to raw pty mode + match raw_pty() { + Ok(p) => self.pty_guard = Some(p), + Err(e) => { + warn!("Failed getting raw pty: {e:?}"); + pty = None + } + } + + } + } + pty + } + } + impl<'a> CmdlineRunner<'a> { - fn new(cmd: &'a Option<String>, want_pty: bool, notify: Receiver<'a, SunsetRawMutex, Msg, 1>) -> Self { + fn new(want_pty: bool, notify: Receiver<'a, SunsetRawMutex, Msg, 1>) -> Self { Self { state: CmdlineState::PreAuth, - pty_guard: None, - cmd, want_pty, notify, } @@ -209,8 +300,12 @@ impl<'a> CmdlineRunner<'a> { self.state = CmdlineState::Authed; debug!("Opening a new session channel"); self.open_session(cli).await?; - if let CmdlineState::Ready { io, extin } = &self.state { - chanio.set(Self::chan_run(io.clone(), extin.clone()).fuse()) + } + Msg::Opened => { + let st = core::mem::replace(&mut self.state, CmdlineState::Authed); + if let CmdlineState::Opening { io, extin } = st { + chanio.set(Self::chan_run(io.clone(), extin.clone()).fuse()); + self.state = CmdlineState::Ready { io, extin }; } } Msg::Exited => { @@ -241,23 +336,20 @@ impl<'a> CmdlineRunner<'a> { async fn open_session(&mut self, cli: &'a SSHClient<'a, CmdlineHooks<'a>>) -> Result<()> { debug_assert!(matches!(self.state, CmdlineState::Authed)); - let cmd = self.cmd.as_ref().map(|s| s.as_str()); let (io, extin) = if self.want_pty { - // TODO expect - let pty = pty::current_pty().expect("pty fetch"); - self.pty_guard = Some(raw_pty().expect("raw pty")); - let io = cli.open_session_pty(cmd, pty).await?; + let io = cli.open_session_pty().await?; (io, None) } else { - let (io, extin) = cli.open_session_nopty(cmd).await?; + let (io, extin) = cli.open_session_nopty().await?; (io, Some(extin)) }; - self.state = CmdlineState::Ready { io, extin }; + self.state = CmdlineState::Opening { io, extin }; Ok(()) } async fn window_change_signal(&mut self) { let io = match &self.state { + CmdlineState::Opening { io, ..} => io, CmdlineState::Ready { io, ..} => io, _ => return, }; @@ -276,54 +368,7 @@ impl<'a> CmdlineRunner<'a> { } } -impl CmdlineClient { - pub fn new(username: impl AsRef<str>, host: impl AsRef<str>, port: u16, - cmd: Option<impl AsRef<str>>, want_pty: bool, ) -> Self { - Self { - - // TODO: shorthand for this? - cmd: cmd.map(|c| c.as_ref().into()), - want_pty, - agent: None, - - notify: Channel::new(), - - username: username.as_ref().into(), - host: host.as_ref().into(), - port, - authkeys: Default::default(), - } - } - - pub fn set_agent(&mut self, agent: AgentClient) { - self.agent = Some(agent) - } - - pub fn split(&mut self) -> (CmdlineHooks, CmdlineRunner) { - let ak = core::mem::replace(&mut self.authkeys, Default::default()); - let hooks = CmdlineHooks::new(&self.username, &self.host, self.port, ak, self.agent.take(), self.notify.sender()); - let runner = CmdlineRunner::new(&self.cmd, self.want_pty, self.notify.receiver()); - (hooks, runner) - } - - pub fn add_authkey(&mut self, k: SignKey) { - self.authkeys.push_back(k) - } -} - impl<'a> CmdlineHooks<'a> { - fn new(username: &'a str, host: &'a str, port: u16, authkeys: VecDeque<SignKey>, - agent: Option<AgentClient>, notify: Sender<'a, SunsetRawMutex, Msg, 1>) -> Self { - Self { - authkeys, - username, - host, - port, - agent, - notify, - } - } - /// Notify the `CmdlineClient` that the main SSH session has exited. /// /// This will cause the `CmdlineRunner` to finish flushing output and terminate. @@ -389,4 +434,20 @@ impl sunset::CliBehaviour for CmdlineHooks<'_> { warn!("Full notification queue"); } } + + async fn session_opened(&mut self, chan: sunset::ChanNum, opener: &mut sunset::SessionOpener<'_, '_, '_>) -> BhResult<()> { + if let Some(p) = self.pty.take() { + opener.pty(p) + } + opener.cmd(self.cmd); + self.notify.send(Msg::Opened).await; + Ok(()) + } +} + +impl<'a> Debug for CmdlineHooks<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CmdlineHooks") + } } + diff --git a/embassy/src/client.rs b/embassy/src/client.rs index 64a1cdee260174ae3bc2fbaa31ee562f4978cffe..0369a124acf2520a6a2cc9e09dc324e81c341dc9 100644 --- a/embassy/src/client.rs +++ b/embassy/src/client.rs @@ -38,10 +38,10 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> { self.sunset.exit().await } - pub async fn open_session_nopty(&'a self, exec: Option<&str>) + pub async fn open_session_nopty(&'a self) -> Result<(ChanInOut<'a, C, S>, ChanIn<'a, C, S>)> { let chan = self.sunset.with_runner(|runner| { - runner.open_client_session(exec, None) + runner.open_client_session() }).await?; let num = chan.num(); @@ -52,11 +52,9 @@ impl<'a, C: CliBehaviour> SSHClient<'a, C> { Ok((cstd, cerr)) } - pub async fn open_session_pty(&'a self, exec: Option<&str>, pty: Pty) - -> Result<ChanInOut<'a, C, S>> { - + pub async fn open_session_pty(&'a self) -> Result<ChanInOut<'a, C, S>> { let chan = self.sunset.with_runner(|runner| { - runner.open_client_session(exec, Some(pty)) + runner.open_client_session() }).await?; let num = chan.num(); diff --git a/src/behaviour.rs b/src/behaviour.rs index 996705d7678780f5677f35e76a47ff3d0aec50b1..3e17703f50400c2cad71f7e4d9effc5fc76bc6cc 100644 --- a/src/behaviour.rs +++ b/src/behaviour.rs @@ -176,10 +176,15 @@ pub trait CliBehaviour { // functions. `Events` may be handled asynchronously so wouldn't // guarantee that. #[allow(unused)] - fn show_banner(&self, banner: TextString, language: TextString) { + fn show_banner(&mut self, banner: TextString, language: TextString) { } // TODO: postauth channel callbacks + #[allow(unused)] + async fn session_opened(&mut self, chan: ChanNum, opener: &mut SessionOpener<'_, '_, '_>) -> BhResult<()> { + Err(BhError::Fail) + } + #[allow(unused)] fn open_tcp_forwarded(&mut self, chan: ChanHandle, t: &ForwardedTcpip) -> ChanOpened { ChanOpened::Failure((ChanFail::SSH_OPEN_UNKNOWN_CHANNEL_TYPE, chan)) diff --git a/src/channel.rs b/src/channel.rs index cadeb6174c755f3d806d56a1dc5c6719759cf608..ac05b25ddf3cc24f5677e17b3defdb6709f18769 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -33,11 +33,10 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> { pub fn open<'b>( &mut self, ty: packets::ChannelOpenType<'b>, - init_req: InitReqs, ) -> Result<(ChanNum, Packet<'b>)> { let num = self.unused_chan()?; - let chan = Channel::new(num, (&ty).into(), init_req); + let chan = Channel::new(num, (&ty).into()); let p = packets::ChannelOpen { num: num.0, initial_window: chan.recv.window as u32, @@ -135,7 +134,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> { /// Creates a new channel in InOpen state. fn reserve_chan(&mut self, co: &ChannelOpen<'_>) -> Result<&mut Channel> { let num = self.unused_chan()?; - let mut chan = Channel::new(num, (&co.ty).into(), Vec::new()); + let mut chan = Channel::new(num, (&co.ty).into()); chan.send = Some(ChanDir { num: co.num, max_packet: co.max_packet as usize, @@ -207,7 +206,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> { s: &mut TrafSend) -> Result<()> { let ch = self.get(num)?; match ch.ty { - ChanType::Session => ch.request(ReqDetails::WinChange(winch), s), + ChanType::Session => Req::WinChange(winch).send(ch, s), _ => error::BadChannelData.fail(), } } @@ -313,21 +312,24 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> { Packet::ChannelOpenConfirmation(p) => { let ch = self.get_any_mut(ChanNum(p.num))?; match ch.state { - ChanState::Opening { .. } => { - let init_state = - mem::replace(&mut ch.state, ChanState::Normal); - if let ChanState::Opening { init_req } = init_state { - debug_assert!(ch.send.is_none()); - ch.send = Some(ChanDir { - num: p.sender_num, - max_packet: p.max_packet as usize, - window: p.initial_window as usize, - }); - for r in init_req { - ch.request(r, s)? + ChanState::Opening => { + debug_assert!(ch.send.is_none()); + ch.send = Some(ChanDir { + num: p.sender_num, + max_packet: p.max_packet as usize, + window: p.initial_window as usize, + }); + + if matches!(ch.ty, ChanType::Session) { + // let the CliBehaviour open a shell etc + let mut opener = SessionOpener::new(&ch, s); + let r = b.client()?.session_opened(ch.num(), &mut opener).await; + if let Err(e) = r { + trace!("Error from session_opened"); } - ch.state = ChanState::Normal; } + + ch.state = ChanState::Normal; } _ => { trace!("Bad channel state"); @@ -395,7 +397,7 @@ impl<C: CliBehaviour, S: ServBehaviour> Channels<C, S> { trace!("channel success, TODO"); } Packet::ChannelFailure(_p) => { - todo!("ChannelFailure"); + trace!("channel failure, TODO"); } _ => Error::bug_msg("unreachable")?, }; @@ -453,13 +455,11 @@ pub struct ModePair { #[derive(Debug)] pub struct Pty { - // or could we put String into packets::PtyReq and serialize modes there... pub term: String<MAX_TERM>, pub cols: u32, pub rows: u32, pub width: u32, pub height: u32, - // TODO: perhaps we need something serializable here pub modes: Vec<ModePair, { termmodes::NUM_MODES }>, } @@ -483,42 +483,29 @@ pub(crate) type ExecString = heapless::String<MAX_EXEC>; /// Like a `packets::ChannelReqType` but with storage. /// Lifetime-free variants have the packet part directly. #[derive(Debug)] -pub enum ReqDetails { +pub enum Req<'a> { // TODO let hook impls provide a string type? Shell, - Exec(ExecString), + Exec(&'a str), + Subsystem(&'a str), Pty(Pty), - // Subsytem { subsystem: heapless::String<MAX_EXEC> }, WinChange(packets::WinChange), Break(packets::Break), + // Signal, + // ExitStatus, + // ExitSignal, } -#[derive(Debug)] -pub struct Req { - // recipient's channel number - num: u32, - details: ReqDetails, -} - -impl ReqDetails { - fn want_reply(&self) -> bool { - match self { - Self::WinChange(_) => false, - _ => true, - } - } -} - -impl Req { - pub(crate) fn packet<'a>(&'a self) -> Result<Packet<'a>> { - let num = self.num; - let want_reply = self.details.want_reply(); - let ty = match &self.details { - ReqDetails::Shell => ChannelReqType::Shell, - ReqDetails::Pty(pty) => { +impl Req<'_> { + pub(crate) fn send(self, ch: &Channel, s: &mut TrafSend) -> Result<()> { + let t; + let req = match self { + Req::Shell => ChannelReqType::Shell, + Req::Pty(pty) => { debug!("TODO implement pty modes"); + t = pty.term; ChannelReqType::Pty(packets::PtyReq { - term: TextString(pty.term.as_bytes()), + term: TextString(t.as_bytes()), cols: pty.cols, rows: pty.rows, width: pty.width, @@ -526,17 +513,45 @@ impl Req { modes: BinString(&[]), }) } - ReqDetails::Exec(cmd) => { - ChannelReqType::Exec(packets::Exec { command: cmd.as_str().into() }) + Req::Exec(cmd) => { + ChannelReqType::Exec(packets::Exec { command: cmd.into() }) } - ReqDetails::WinChange(rt) => ChannelReqType::WinChange(rt.clone()), - ReqDetails::Break(rt) => ChannelReqType::Break(rt.clone()), + Req::Subsystem(cmd) => { + ChannelReqType::Subsystem(packets::Subsystem { subsystem: cmd.into() }) + } + Req::WinChange(rt) => ChannelReqType::WinChange(rt), + Req::Break(rt) => ChannelReqType::Break(rt), }; - let p = ChannelRequest { num, want_reply, req: ty }.into(); - Ok(p) + + let p = ChannelRequest { + num: ch.send_num()?, + // we aren't handling responses for anything + want_reply: false, + req, + }; + let p: Packet = p.into(); + s.send(p) } } +/// Convenience for the types of session channels that can be opened +pub enum SessionCommand<S: AsRef<str>> { + Shell, + Exec(S), + Subsystem(S), +} + +impl<'a, S: AsRef<str> + 'a> Into<Req<'a>> for &'a SessionCommand<S> { + fn into(self) -> Req<'a> { + match self { + SessionCommand::Shell => Req::Shell, + SessionCommand::Exec(s) => Req::Exec(s.as_ref()), + SessionCommand::Subsystem(s) => Req::Subsystem(s.as_ref()), + } + } +} + + // // Variants match packets::ChannelReqType, without data // enum ReqKind { // Shell, @@ -550,11 +565,6 @@ impl Req { // Break, // } -// shell+pty. or perhaps this should match the hook queue size and then -// we can stop servicing the hook queue if this limit is reached. -// const MAX_OUTSTANDING_REQS: usize = 2; -const MAX_INIT_REQS: usize = 2; - /// Per-direction channel variables #[derive(Debug)] struct ChanDir { @@ -572,17 +582,9 @@ enum ChanState { /// Not to be used for normal channel messages InOpen, - /// `init_req` are the request messages to be sent once the ChannelOpenConfirmation - /// is received - - // TODO: this is wasting half a kB. where else could we store it? could - // the Behaviour own it? Or we don't store them here, just callback to the Behaviour. - // TODO: perhaps .get() and .get_mut() should ignore Opening state channels? - Opening { - init_req: InitReqs, - }, + Opening, Normal, RecvEof, // TODO: recvclose state probably shouldn't be possible, we remove it straight away? @@ -615,10 +617,10 @@ pub(crate) struct Channel { } impl Channel { - fn new(num: ChanNum, ty: ChanType, init_req: InitReqs) -> Self { + fn new(num: ChanNum, ty: ChanType) -> Self { Channel { ty, - state: ChanState::Opening { init_req }, + state: ChanState::Opening, sent_close: false, sent_eof: false, // last_req: Deque::new(), @@ -647,12 +649,6 @@ impl Channel { Ok(self.send.as_ref().trap()?.num) } - fn request(&self, req: ReqDetails, s: &mut TrafSend) -> Result<()> { - let num = self.send_num()?; - let r = Req { num, details: req }; - s.send(r.packet()?) - } - /// Returns an open confirmation reply packet to send. /// Must be called with state of `InOpen`. fn open_done<'p>(&mut self) -> Result<Packet<'p>> { @@ -838,20 +834,6 @@ impl Channel { } } -pub struct ChanMsg { - pub num: ChanNum, - pub msg: ChanMsgDetails, -} - -pub enum ChanMsgDetails { - Data, - ExtData { ext: u32 }, - // TODO: perhaps we don't need the storaged ReqDetails, just have the reqtype packet? - Req(ReqDetails), - // TODO closein/closeout/eof, etc. Should also return the exit status etc - Close, -} - #[derive(Debug)] pub(crate) struct DataIn { pub num: ChanNum, @@ -927,8 +909,6 @@ impl ChanData { } } -pub(crate) type InitReqs = Vec<ReqDetails, MAX_INIT_REQS>; - // for dispatch_open_inner() enum DispatchOpenError { /// A program error @@ -952,3 +932,44 @@ impl From<ChanFail> for DispatchOpenError { } } +pub struct SessionOpener<'a, 's, 't> { + ch: &'a Channel, + s: &'a mut TrafSend<'s, 't>, +} + +impl<'a, 's, 't> SessionOpener<'a, 's, 't> { + fn new(ch: &'a Channel, s: &'a mut TrafSend<'s, 't>) -> Self { + Self { + ch, + s, + } + } + + pub fn cmd<S: AsRef<str>>(&mut self, cmd: &SessionCommand<S>) { + self.send(cmd.into()) + } + + pub fn shell(&mut self) { + self.send(Req::Shell) + } + + pub fn exec(&mut self, cmd: &str) { + self.send(Req::Exec(cmd)) + } + + pub fn subsystem(&mut self, cmd: &str) { + self.send(Req::Subsystem(cmd)) + } + + pub fn pty(&mut self, pty: channel::Pty) { + self.send(Req::Pty(pty)) + } + + fn send(&mut self, req: Req) { + let r = req.send(self.ch, self.s); + if let Err(e) = r { + warn!("Error sending request: {e:?}") + } + } +} + diff --git a/src/conn.rs b/src/conn.rs index f628bb0baabd2094085b31bc706c2c1efc6b3d77..a2502a3c027a2216f8a162daef2bd20c4a51a044 100644 --- a/src/conn.rs +++ b/src/conn.rs @@ -325,7 +325,6 @@ impl<C: CliBehaviour, S: ServBehaviour> Conn<C, S> { } } Packet::UserauthBanner(p) => { - // TODO: client only if let ClientServer::Client(cli) = &mut self.cliserv { cli.banner(&p, b.client()?); } else { diff --git a/src/ident.rs b/src/ident.rs index 2863e19ee5db4a5994cdac01acdf002c085c2bb0..256904143250068b2a6b904343512822211eb9ff 100644 --- a/src/ident.rs +++ b/src/ident.rs @@ -1,6 +1,6 @@ use crate::error::{Error,TrapBug, Result}; -pub(crate) const OUR_VERSION: &[u8] = b"SSH-2.0-Sunset-0.1"; +pub(crate) const OUR_VERSION: &[u8] = b"SSH-2.0-Sunset-1"; pub(crate) const SSH_PREFIX: &[u8] = b"SSH-2.0-"; diff --git a/src/lib.rs b/src/lib.rs index 7b0ba60a7251850f2c1048407f97f3dee8fe576e..a8b053683acd12ed3216cb63560b78130f74fafb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,7 +36,7 @@ pub mod sunsetlog; mod auth; mod channel; mod runner; -// TODO only public for UnusedCli etc. +// TODO only public for UnusedCli etc. pub mod behaviour; mod termmodes; mod ssh_chapoly; @@ -55,7 +55,7 @@ pub use runner::Runner; pub use sign::{SignKey, KeyType, OwnedSig}; pub use packets::{PubKey, Signature}; pub use error::{Error,Result}; -pub use channel::{ChanMsg, ChanMsgDetails, Pty, ChanOpened}; +pub use channel::{Pty, ChanOpened, SessionOpener, SessionCommand}; pub use sshnames::ChanFail; pub use channel::{ChanData, ChanNum}; pub use runner::ChanHandle; diff --git a/src/namelist.rs b/src/namelist.rs index 05644e1d2999f344943e12147ff5d9c710a65d33..49460c44a1ece2c6b6d5f2a0c028b2be4fa21a7c 100644 --- a/src/namelist.rs +++ b/src/namelist.rs @@ -233,7 +233,7 @@ mod tests { let n = LocalNames::try_from(*t).unwrap(); let n = NameList::Local(&n); let mut buf = vec![99; 30]; - let l = sshwire::write_ssh(&mut buf, &n, None).unwrap(); + let l = sshwire::write_ssh(&mut buf, &n).unwrap(); buf.truncate(l); let out1 = core::str::from_utf8(&buf).unwrap(); // check that a join with std gives the same result. diff --git a/src/packets.rs b/src/packets.rs index 7bbb69b2bdc4de7eaf1153ca621e3afef7972450..c4f58b26b2cf6e3870718827853a13d2f8609f94 100644 --- a/src/packets.rs +++ b/src/packets.rs @@ -590,6 +590,11 @@ pub struct Exec<'a> { pub command: TextString<'a>, } +#[derive(Debug, SSHEncode, SSHDecode)] +pub struct Subsystem<'a> { + pub subsystem: TextString<'a>, +} + /// The contents of a `"pty-req"` request. /// /// Note that most function arguments use [`channel::Pty`] rather than this struct. @@ -603,11 +608,6 @@ pub struct PtyReq<'a> { pub modes: BinString<'a>, } -#[derive(Debug, SSHEncode, SSHDecode)] -pub struct Subsystem<'a> { - pub subsystem: &'a str, -} - #[derive(Debug, Clone, SSHEncode, SSHDecode)] pub struct WinChange { pub cols: u32, diff --git a/src/runner.rs b/src/runner.rs index b875364ef7259b0e603e0f0d50fbb28496b203c5..04c29bcc5be2fbc5f9ebc8e7ccfbd28697474d58 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -8,7 +8,7 @@ use core::task::{Poll, Waker}; use pretty_hex::PrettyHex; -use crate::*; +use crate::{*, packets::Subsystem}; use packets::{ChannelDataExt, ChannelData}; use crate::channel::{ChanNum, ChanData}; use encrypt::KeyState; @@ -208,20 +208,26 @@ impl<'a, C: CliBehaviour, S: ServBehaviour> Runner<'a, C, S> { } // TODO: move somewhere client specific? - pub fn open_client_session(&mut self, exec: Option<&str>, pty: Option<channel::Pty>) -> Result<ChanHandle> { + pub fn open_client_session(&mut self) -> Result<ChanHandle> { trace!("open_client_session"); - let mut init_req = channel::InitReqs::new(); - if let Some(pty) = pty { - init_req.push(channel::ReqDetails::Pty(pty)).trap()?; - } - if let Some(cmd) = exec { - let mut s = channel::ExecString::new(); - s.push_str(cmd).trap()?; - init_req.push(channel::ReqDetails::Exec(s)).trap()?; - } else { - init_req.push(channel::ReqDetails::Shell).trap()?; - } - let (chan, p) = self.conn.channels.open(packets::ChannelOpenType::Session, init_req)?; + // let mut init_req = channel::InitReqs::new(); + // if let Some(pty) = pty { + // init_req.push(channel::ReqDetails::Pty(pty)).trap()?; + // } + + // match cmd { + // SessionCommand::Shell => { + // init_req.push(channel::ReqDetails::Shell).trap()?; + // } + // } + // if let Some(cmd) = exec { + // let mut s = channel::ExecString::new(); + // s.push_str(cmd).trap()?; + // init_req.push(channel::ReqDetails::Exec(s)).trap()?; + // } else { + // } + + let (chan, p) = self.conn.channels.open(packets::ChannelOpenType::Session)?; self.traf_out.send_packet(p, &mut self.keys)?; self.wake(); Ok(ChanHandle(chan)) diff --git a/src/sshwire.rs b/src/sshwire.rs index 8509a1b8907d2377bcceb17c62b42d1838ad3400..f62c5d775c62690f81df91fe0034a041f999dec9 100644 --- a/src/sshwire.rs +++ b/src/sshwire.rs @@ -13,7 +13,7 @@ use { use core::str; use core::convert::AsRef; -use core::fmt::{self,Debug}; +use core::fmt::{self,Debug,Display}; use digest::Output; use pretty_hex::PrettyHex; use snafu::{prelude::*, Location}; @@ -362,6 +362,17 @@ impl Debug for TextString<'_> { } } +impl Display for TextString<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let s = core::str::from_utf8(self.0); + if let Ok(s) = s { + write!(f, "\"{}\"", s.escape_default()) + } else { + write!(f, "{:?}", self) + } + } +} + impl SSHEncode for TextString<'_> { fn enc<S>(&self, s: &mut S) -> WireResult<()> where S: sshwire::SSHSink { diff --git a/sshwire-derive/src/lib.rs b/sshwire-derive/src/lib.rs index 8d843c3e2e9834e52df1f792c9f9916d102222a2..a871a43e3795b27c41c2f60f9a8ee7b44f649f46 100644 --- a/sshwire-derive/src/lib.rs +++ b/sshwire-derive/src/lib.rs @@ -77,10 +77,12 @@ enum FieldAtt { /// A variant method name will be encoded/decoded before the next field. /// eg `#[sshwire(variant_name = ch)]` for `ChannelRequest` VariantName(Ident), + /// Any unknown variant name should be recorded here. /// This variant can't be written out. /// `#[sshwire(unknown))]` CaptureUnknown, + /// The name of a variant, used by the parent struct /// `#[sshwire(variant = "exit-signal"))]` /// or @@ -310,6 +312,8 @@ fn encode_enum( } Ok(()) })?; + // an enum with only an Unknown variant will always return an earlier error + fn_body.push_parsed("#[allow(unreachable_code)]")?; fn_body.push_parsed("Ok(())")?; Ok(()) })?; @@ -380,7 +384,10 @@ fn encode_enum_names( } Ok(()) })?; - fn_body.push_parsed("; Ok(r)")?; + fn_body.push_parsed(";")?; + // an enum with only an Unknown variant will always return an earlier error + fn_body.push_parsed("#[allow(unreachable_code)]")?; + fn_body.push_parsed("Ok(r)")?; Ok(()) })?;