diff --git a/Cargo.lock b/Cargo.lock index b47ca5b89e4b5290c2df01beec1b78620e63ca9f..eabcf14a84ca727623983621240ff11966700f7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -345,6 +345,7 @@ dependencies = [ "async-trait", "door-sshproto", "futures 0.4.0-alpha.0", + "heapless", "libc", "log", "moro", diff --git a/async/Cargo.toml b/async/Cargo.toml index 12863e490fafc8ba41a94c47b70285a5103e1f32..eb07034dfd65766a7a2797f7330c55394f936430 100644 --- a/async/Cargo.toml +++ b/async/Cargo.toml @@ -29,6 +29,8 @@ moro = "0.4" libc = "0.2" nix = "0.24" +heapless = "0.7.10" + # TODO pretty-hex = "0.3" snafu = { version = "0.7", default-features = true } diff --git a/async/examples/con1.rs b/async/examples/con1.rs index a7116e2e015f4f98d2814db2cd479fcf1fe85e73..3e97b7ab66333f9fe4b07ac70b921ee33064006e 100644 --- a/async/examples/con1.rs +++ b/async/examples/con1.rs @@ -11,7 +11,7 @@ use tokio::net::TcpStream; use std::{net::Ipv6Addr, io::Read}; use door_sshproto::*; -use door_async::SSHClient; +use door_async::{SSHClient, raw_pty}; use simplelog::*; @@ -31,6 +31,10 @@ struct Args { /// a path to id_ed25519 or similar identityfile: Vec<String>, + #[argh(option)] + /// log to a file + tracefile: Option<String>, + #[argh(option, short='l')] /// username username: Option<String>, @@ -88,7 +92,7 @@ fn main() -> Result<()> { }) } -fn setup_log(args: &Args) { +fn setup_log(args: &Args) -> Result<()> { let mut conf = simplelog::ConfigBuilder::new(); let conf = conf .add_filter_allow_str("door") @@ -108,11 +112,17 @@ fn setup_log(args: &Args) { LevelFilter::Warn }; - CombinedLogger::init( - vec![ - TermLogger::new(level, conf, TerminalMode::Mixed, ColorChoice::Auto), - ] - ).unwrap(); + let mut logs: Vec<Box<dyn SharedLogger>> = vec![ + TermLogger::new(level, conf.clone(), TerminalMode::Mixed, ColorChoice::Auto), + ]; + + if let Some(tf) = args.tracefile.as_ref() { + let w = std::fs::File::create(tf).with_context(|| format!("Error opening {tf}"))?; + logs.push(WriteLogger::new(LevelFilter::Trace, conf, w)); + } + + CombinedLogger::init(logs).unwrap(); + Ok(()) } fn read_key(p: &str) -> Result<SignKey> { @@ -133,18 +143,16 @@ async fn run(args: &Args) -> Result<()> { // TODO: better lifetime rather than leaking let work = Box::leak(Box::new(work)); - let mut sess = door_async::CmdlineClient::new(args.username.as_ref().unwrap()); + let mut cli = door_async::CmdlineClient::new(args.username.as_ref().unwrap()); for i in &args.identityfile { - sess.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?); + cli.add_authkey(read_key(&i).with_context(|| format!("loading key {i}"))?); } - let mut door = SSHClient::new(work.as_mut_slice(), Box::new(sess))?; - + let mut door = SSHClient::new(work.as_mut_slice(), Box::new(cli))?; let mut s = door.socket(); - let netloop = tokio::io::copy_bidirectional(&mut stream, &mut s); moro::async_scope!(|scope| { - scope.spawn(netloop); + scope.spawn(tokio::io::copy_bidirectional(&mut stream, &mut s)); scope.spawn(async { loop { @@ -159,25 +167,40 @@ async fn run(args: &Args) -> Result<()> { match ev { Some(Event::Authenticated) => { + let mut raw_pty_guard = None; info!("Opening a new session channel"); - let cmd = if args.cmd.is_empty() { - None + let (cmd, pty) = if args.cmd.is_empty() { + (None, true) + } else { + (Some(args.cmd.join(" ")), false) + }; + let (mut io, mut err) = if pty { + raw_pty_guard = Some(raw_pty()?); + let io = door.open_client_session_pty(cmd.as_deref()).await + .context("Opening session")?; + (io, None) } else { - Some(args.cmd.join(" ")) + let (io, err) = door.open_client_session_nopty(cmd.as_deref()).await + .context("Opening session")?; + (io, Some(err)) + }; + let mut i = door_async::stdin()?; + let mut o = door_async::stdout()?; + let mut e = if err.is_some() { + Some(door_async::stderr()?) + } else { + None }; - let r = door.open_client_session_nopty(cmd.as_deref()).await - .context("Opening session")?; - let (mut io, mut err) = r; + let mut io2 = io.clone(); scope.spawn(async move { - let mut i = door_async::stdin()?; - let mut o = door_async::stdout()?; - let mut e = door_async::stderr()?; - let mut io2 = io.clone(); moro::async_scope!(|scope| { scope.spawn(tokio::io::copy(&mut io, &mut o)); scope.spawn(tokio::io::copy(&mut i, &mut io2)); - scope.spawn(tokio::io::copy(&mut err, &mut e)); + if let Some(ref mut err) = err { + scope.spawn(tokio::io::copy(err, e.as_mut().unwrap())); + } }).await; + drop(raw_pty_guard); Ok::<_, anyhow::Error>(()) }); // TODO: handle channel completion diff --git a/async/src/async_door.rs b/async/src/async_door.rs index 4432fe117042a6ddb039e9c3ee97b812243b4c40..13ae34204fe448e5105ac26c38b03260defe09ca 100644 --- a/async/src/async_door.rs +++ b/async/src/async_door.rs @@ -253,7 +253,9 @@ impl<'a> AsyncWrite for AsyncDoorSocket<'a> { self: Pin<&mut Self>, _cx: &mut Context<'_>, ) -> Poll<Result<(), IoError>> { - todo!("poll_close") + // TODO + error!("connection closed"); + Poll::Ready(Ok(())) } } diff --git a/async/src/client.rs b/async/src/client.rs index 247dd355dc7dd984ded31def10374e88daacee88..acc2f8c94bbecde0500a120ede8986d4a2ee01cf 100644 --- a/async/src/client.rs +++ b/async/src/client.rs @@ -22,6 +22,7 @@ use crate::async_door::*; use door_sshproto as door; use door::{Behaviour, AsyncCliBehaviour, Runner, Result}; use door::sshnames::SSH_EXTENDED_DATA_STDERR; +use door::config::*; pub struct SSHClient<'a> { door: AsyncDoor<'a>, @@ -52,7 +53,7 @@ impl<'a> SSHClient<'a> { pub async fn open_client_session_nopty(&mut self, exec: Option<&str>) -> Result<(ChanInOut<'a>, ChanExtIn<'a>)> { let chan = self.door.with_runner(|runner| { - runner.open_client_session(exec, false) + runner.open_client_session(exec, None) }).await?; let cstd = ChanInOut::new(chan, &self.door); @@ -62,8 +63,12 @@ impl<'a> SSHClient<'a> { pub async fn open_client_session_pty(&mut self, exec: Option<&str>) -> Result<ChanInOut<'a>> { + + // XXX error handling + let pty = pty::current_pty().expect("pty fetch"); + let chan = self.door.with_runner(|runner| { - runner.open_client_session(exec, false) + runner.open_client_session(exec, Some(pty)) }).await?; let cstd = ChanInOut::new(chan, &self.door); diff --git a/async/src/lib.rs b/async/src/lib.rs index ee0f84b24eced466b753b69ee4806789f3b86791..8c167c6258a62035fc39a803050f287a3765dfea 100644 --- a/async/src/lib.rs +++ b/async/src/lib.rs @@ -1,9 +1,9 @@ -#![forbid(unsafe_code)] #![allow(unused_imports)] mod client; mod async_door; mod cmdline_client; +mod pty; pub use async_door::AsyncDoor; pub use client::SSHClient; @@ -13,3 +13,5 @@ pub use cmdline_client::CmdlineClient; mod fdio; #[cfg(unix)] pub use fdio::{stdin, stdout, stderr}; + +pub use pty::raw_pty; diff --git a/sshproto/src/channel.rs b/sshproto/src/channel.rs index 1144b8e65c2695cfa8549b51535fc3ef6895f73c..8e50793776a6b143ee337f44f82a8ed3ac6a8fbf 100644 --- a/sshproto/src/channel.rs +++ b/sshproto/src/channel.rs @@ -11,7 +11,7 @@ use heapless::{Deque, String, Vec}; use crate::{conn::RespPackets, *}; use config::*; use packets::{ChannelReqType, ChannelRequest, Packet, ChannelOpenType, ChannelData, ChannelDataExt}; -use sshwire::BinString; +use sshwire::{BinString, TextString}; pub(crate) struct Channels { ch: [Option<Channel>; config::MAX_CHANNELS], @@ -257,21 +257,21 @@ impl From<&ChannelOpenType<'_>> for ChanType { } #[derive(Debug)] -struct ModePair { - opcode: u8, - arg: u32, +pub struct ModePair { + pub opcode: u8, + pub arg: u32, } #[derive(Debug)] pub struct Pty { // or could we put String into packets::Pty and serialize modes there... - term: String<MAX_TERM>, - cols: u32, - rows: u32, - width: u32, - height: u32, + pub term: String<MAX_TERM>, + pub cols: u32, + pub rows: u32, + pub width: u32, + pub height: u32, // TODO: perhaps we need something serializable here - modes: Vec<ModePair, { termmodes::NUM_MODES }>, + pub modes: Vec<ModePair, { termmodes::NUM_MODES }>, } pub(crate) type ExecString = heapless::String<MAX_EXEC>; @@ -310,8 +310,15 @@ impl Req { let want_reply = self.details.want_reply(); let ty = match &self.details { ReqDetails::Shell => ChannelReqType::Shell, - ReqDetails::Pty(_pty) => { - todo!("serialize modes") + ReqDetails::Pty(pty) => { + ChannelReqType::Pty(packets::Pty { + term: TextString(pty.term.as_bytes()), + cols: pty.cols, + rows: pty.rows, + width: pty.width, + height: pty.height, + modes: BinString(&[]), + }) } ReqDetails::Exec(cmd) => { ChannelReqType::Exec(packets::Exec { command: cmd.as_str().into() }) diff --git a/sshproto/src/config.rs b/sshproto/src/config.rs index 2b783c013a2eb68d01aad805c7b23b9dd3b3637d..03ff3bbc56c4475a88e70a7c096821fad03aa684 100644 --- a/sshproto/src/config.rs +++ b/sshproto/src/config.rs @@ -13,3 +13,5 @@ pub const MAX_EXEC: usize = 200; // Unsure if this is specified somewhere pub const MAX_TERM: usize = 32; +pub const DEFAULT_TERM: &str = "vt220"; + diff --git a/sshproto/src/lib.rs b/sshproto/src/lib.rs index 06ce9d46228c933c02641b6bf5b12c23e812763f..c5ff43e07cd1dcb5ee2681bc6ac34be31e8fef77 100644 --- a/sshproto/src/lib.rs +++ b/sshproto/src/lib.rs @@ -9,7 +9,6 @@ // XXX unused_imports only during dev churn #![allow(unused_imports)] -pub mod packets; // XXX decide what is public pub mod conn; pub mod encrypt; @@ -35,14 +34,16 @@ mod servauth; pub mod doorlog; mod auth; mod channel; -mod config; mod runner; mod behaviour; mod termmodes; mod async_behaviour; mod block_behaviour; mod ssh_chapoly; + +pub mod packets; pub mod sshwire; +pub mod config; // Application API pub use behaviour::{Behaviour, BhError, BhResult, ResponseString}; @@ -56,5 +57,5 @@ pub use conn::RespPackets; pub use sign::SignKey; pub use packets::PubKey; pub use error::{Error,Result}; -pub use channel::{ChanMsg, ChanMsgDetails, ChanEvent}; +pub use channel::{ChanMsg, ChanMsgDetails, ChanEvent, Pty}; pub use conn::Event; diff --git a/sshproto/src/runner.rs b/sshproto/src/runner.rs index ae96403c14d5f92e8f399064d600e80d7afc5f53..cff34740881d4a380078e31650375aaa61e73ea2 100644 --- a/sshproto/src/runner.rs +++ b/sshproto/src/runner.rs @@ -152,11 +152,12 @@ impl<'a> Runner<'a> { } } - pub fn open_client_session(&mut self, exec: Option<&str>, pty: bool) -> Result<u32> { + // TODO: move somewhere client specific? + pub fn open_client_session(&mut self, exec: Option<&str>, pty: Option<channel::Pty>) -> Result<u32> { trace!("open_client_session"); let mut init_req = channel::InitReqs::new(); - if pty { - todo!("pty needs modes and that"); + if let Some(pty) = pty { + init_req.push(channel::ReqDetails::Pty(pty)).trap()?; } if let Some(cmd) = exec { let mut s = channel::ExecString::new();