use anyhow::{Result, Context as _, bail}; use bytes::BytesMut; use derivative::Derivative; use enclose::enclose; use futures::ready; use futures::future::{FutureExt as _, FusedFuture as _, Fuse}; use futures::stream::{StreamExt as _, TryStreamExt as _, FuturesUnordered}; use regex::Regex; use rustix::termios; use std::collections::{HashMap, HashSet}; use std::{env, fs}; use std::future::Future; use std::os::unix::io::AsRawFd as _; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::process::ExitCode; use std::task::{Context, Poll}; use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; fn main() -> ExitCode { env_logger::init(); match run_main() { Ok(code) => code, Err(err) => { eprintln!("ssh: {:?}", err); ExitCode::FAILURE }, } } fn run_main() -> Result { let matches = clap::Command::new("client") .arg(clap::Arg::new("private-key").short('i') .action(clap::ArgAction::Append) .value_parser(clap::value_parser!(PathBuf)) .value_hint(clap::ValueHint::FilePath) .value_name("key-file")) .arg(clap::Arg::new("port").short('p') .value_parser(clap::value_parser!(u16)) .value_name("port")) .arg(clap::Arg::new("username").short('l') .value_name("login")) .arg(clap::Arg::new("destination") .required(true) .value_name("destination")) .arg(clap::Arg::new("command") .value_name("command")) .arg(clap::Arg::new("want-tty").short('t') .action(clap::ArgAction::SetTrue)) .arg(clap::Arg::new("local-tunnel").short('L') .action(clap::ArgAction::Append) .value_name("[local-host:]local-port:remote-host:remote-port")) .arg(clap::Arg::new("remote-tunnel").short('R') .action(clap::ArgAction::Append) .value_name("[remote-host:]remote-port:local-host:local-port")) .arg(clap::Arg::new("known-hosts").short('k') .value_hint(clap::ValueHint::FilePath) .value_name("host-file")) .get_matches(); let mut destination = Destination::default(); if let Some(dest) = matches.get_one::("destination") { destination = parse_destination(&dest)?; } destination.port = matches.get_one::("port").or(destination.port.as_ref()).cloned(); destination.username = matches.get_one::("username").or(destination.username.as_ref()).cloned(); let keys = matches.get_many::("private-key") .into_iter().flatten() .map(|key| read_key(&key)) .collect::>>()?; let command = matches.get_one::("command").cloned(); let want_tty = *matches.get_one::("want-tty").unwrap() || command.is_none(); let local_tunnels = matches.get_many::("local-tunnel") .into_iter().flatten() .map(|spec| parse_tunnel_spec(&spec)) .collect::>>()?; let remote_tunnels = matches.get_many::("remote-tunnel") .into_iter().flatten() .map(|spec| parse_tunnel_spec(&spec)) .collect::>>()?; let host_file_path = matches.get_one::("known-hosts").cloned(); let host_file = read_host_file(host_file_path)?; let opts = Opts { destination, keys, command, want_tty, local_tunnels, remote_tunnels, host_file, }; let runtime = tokio::runtime::Builder::new_current_thread() .enable_all().build()?; let exit_code = runtime.block_on(run_client(opts))?; runtime.shutdown_background(); Ok(exit_code) } #[derive(Debug)] struct Opts { destination: Destination, keys: Vec, command: Option, want_tty: bool, local_tunnels: Vec, remote_tunnels: Vec, host_file: Option, } #[derive(Debug, Default)] struct Destination { host: Option, port: Option, username: Option, } fn parse_destination(dest: &str) -> Result { let re = Regex::new(r"(?x) ^ (ssh://)? ((?P\w+) @)? (?P[[:alnum:].]+) (: (?P[[:digit:]]+))? $ ").unwrap(); let captures = re.captures(dest) .context("invalid format of `destination`, should be [user@]host[:port]")?; let host = captures.name("host").map(|x| x.as_str().into()); let port = captures.name("port").map(|x| x.as_str().parse()).transpose() .context("invalid port number in `destination`")?; let username = captures.name("username").map(|x| x.as_str().into()); Ok(Destination { host, port, username }) } #[derive(Derivative)] #[derivative(Debug)] struct Key { path: PathBuf, data: Vec, #[derivative(Debug = "ignore")] decoded: makiko::keys::DecodedPrivkeyNopass, } fn read_key(path: &Path) -> Result { let data = fs::read(&path) .context(format!("could not read file {:?} with private key", path))?; let decoded = makiko::keys::decode_pem_privkey_nopass(&data) .context(format!("could not decode keypair from file {:?}", path))?; Ok(Key { path: path.into(), data, decoded }) } #[derive(Debug)] struct TunnelSpec { bind_host: Option, bind_port: u16, connect_host: String, connect_port: u16, } fn parse_tunnel_spec(spec: &str) -> Result { let re = Regex::new(r"(?x) ^ ((?P[[:alnum:].]+) :)? (?P[[:digit:]]+) : (?P[[:alnum:].]+) : (?P[[:digit:]]+) $ ").unwrap(); let captures = re.captures(spec) .context("invalid format of tunnel spec")?; let bind_host = captures.name("bind_host").map(|x| x.as_str().into()); let bind_port = captures.name("bind_port").unwrap().as_str().parse() .context("invalid bind-port in tunnel spec")?; let connect_host = captures.name("connect_host").unwrap().as_str().into(); let connect_port = captures.name("connect_port").unwrap().as_str().parse() .context("invalid connect-port in tunnel spec")?; Ok(TunnelSpec { bind_host, bind_port, connect_host, connect_port }) } #[derive(Debug)] struct HostFile { path: PathBuf, file: makiko::host_file::File, } fn read_host_file(path: Option) -> Result> { let default_path = home::home_dir().map(|dir| dir.join(".ssh/known_hosts")); let Some(path) = path.or(default_path) else { return Ok(None) }; let file_data = fs::read(&path) .context(format!("could not read known_hosts file {}", path.display()))?; let file = makiko::host_file::File::decode(file_data.into()); Ok(Some(HostFile { path, file })) } async fn run_client(opts: Opts) -> Result { let host = opts.destination.host .context("please specify the host to connect to")?; let username = opts.destination.username .context("please specify the username to login with")?; let port = opts.destination.port.unwrap_or(22); let config = makiko::ClientConfig::default_compatible_less_secure(); let hostname = makiko::host_file::host_port_to_hostname(&host, port); log::info!("connecting to host {:?}, port {}", host, port); let socket = tokio::net::TcpStream::connect((host, port)).await .context("could not open TCP connection to the server")?; log::info!("successfully connected"); let remote_tunnel_addrs = opts.remote_tunnels.into_iter() .map(|spec| { let bind_addr = (spec.bind_host.unwrap_or("".into()), spec.bind_port); let connect_addr = (spec.connect_host, spec.connect_port); (bind_addr, connect_addr) }) .collect::>(); let (client, client_rx, client_fut) = makiko::Client::open(socket, config)?; let client_task = TaskHandle(tokio::task::spawn(client_fut)); let event_task = TaskHandle(tokio::task::spawn( run_events(client.clone(), client_rx, remote_tunnel_addrs.clone(), hostname, opts.host_file) )); let interact_task = TaskHandle(tokio::task::spawn(enclose!{(client) async move { authenticate(&client, username, opts.keys).await .context("could not authenticate")?; log::info!("successfully authenticated"); bind_remote_tunnels(&client, &remote_tunnel_addrs).await?; let session_task = TaskHandle(tokio::task::spawn(enclose!{(client) async move { run_session(client, opts.command, opts.want_tty).await }})); let tunnel_tasks = opts.local_tunnels.into_iter().map(enclose!{(client) |spec| { TaskHandle(tokio::task::spawn(run_local_tunnel(client.clone(), spec))) }}).collect::>(); let mut session_fut = session_task.fuse(); let mut tunnels_fut = tunnel_tasks.try_collect().fuse(); let mut exit_code = ExitCode::SUCCESS; while !session_fut.is_terminated() || !tunnels_fut.is_terminated() { tokio::select! { res = &mut session_fut => exit_code = res?, res = &mut tunnels_fut => res?, } } client.disconnect(makiko::DisconnectError::by_app())?; Result::<_>::Ok(exit_code) }})); let mut client_fut = client_task.fuse(); let mut event_fut = event_task.fuse(); let mut interact_fut = interact_task.fuse(); let mut exit_code = None; loop { if client_fut.is_terminated() && exit_code.is_some() { return Ok(exit_code.unwrap()) } tokio::select!{ res = &mut client_fut => res?, res = &mut event_fut => res?, res = &mut interact_fut => exit_code = Some(res?), }; } } async fn run_events( client: makiko::Client, mut client_rx: makiko::ClientReceiver, remote_tunnel_addrs: HashMap<(String, u16), (String, u16)>, hostname: String, mut host_file: Option, ) -> Result<()> { let mut pubkey_task = Fuse::terminated(); let mut tunnel_tasks = FuturesUnordered::new(); loop { tokio::select!{ event = client_rx.recv() => match event? { Some(makiko::ClientEvent::ServerPubkey(pubkey, accept_tx)) => { let client = client.clone(); let hostname = hostname.clone(); let host_file = host_file.take(); pubkey_task = TaskHandle(tokio::task::spawn(async move { if verify_pubkey(pubkey, hostname, host_file).await? { accept_tx.accept(); } else { client.disconnect(makiko::DisconnectError { reason_code: makiko::codes::disconnect::HOST_KEY_NOT_VERIFIABLE, description: "user did not accept the host public key".into(), description_lang: "".into(), })?; } Result::<()>::Ok(()) })).fuse(); }, Some(makiko::ClientEvent::Tunnel(accept)) => { let connect_addr = remote_tunnel_addrs.get(&accept.connected_addr); let Some(connect_addr) = connect_addr else { continue }; tunnel_tasks.push(TaskHandle(tokio::task::spawn( run_remote_tunnel(accept, connect_addr.clone()) ))); }, Some(_) => continue, None => break, }, res = &mut pubkey_task => res?, Some(res) = tunnel_tasks.next() => res?, }; } Ok(()) } async fn verify_pubkey( pubkey: makiko::Pubkey, hostname: String, mut host_file: Option, ) -> Result { log::info!("verifying pubkey for server {:?}: {}", hostname, pubkey); if let Some(host_file) = host_file.as_ref() { use makiko::host_file::KeyMatch; match host_file.file.match_hostname_key(&hostname, &pubkey) { KeyMatch::Accepted(entries) => { log::info!("server pubkey found in {}:", host_file.path.display()); for entry in entries.iter() { log::info!(" at line {}", entry.line()); } return Ok(true) }, KeyMatch::Revoked(entry) => { println!("ssh: server pubkey was revoked in {} at line {}!!!", host_file.path.display(), entry.line()); return Ok(false) }, KeyMatch::OtherKeys(entries) => { println!("ssh: found other pubkeys in {}!!!", host_file.path.display()); for entry in entries.iter() { println!("ssh: at line {}: {}, fingerprint {}", entry.line(), entry.pubkey(), entry.pubkey().fingerprint()); } return Ok(false) }, KeyMatch::NotFound => { log::info!("server was not found in {}", host_file.path.display()); }, } } let prompt = format!( "ssh: server {:?} has pubkey with fingerprint {}\nssh: do you want to connect?", hostname, pubkey.fingerprint(), ); if ask_yes_no(&prompt).await? { if let Some(host_file) = host_file.as_mut() { host_file.file.append_entry(makiko::host_file::File::entry_builder() .hostname(&hostname) .key(pubkey)); fs::write(&host_file.path, &host_file.file.encode()) .context(format!("could not write to {}", host_file.path.display()))?; } Ok(true) } else { Ok(false) } } async fn run_remote_tunnel(accept: makiko::AcceptTunnel, connect_addr: (String, u16)) -> Result<()> { match tokio::net::TcpStream::connect(&connect_addr).await { Ok(socket) => { let config = makiko::ChannelConfig::default(); let (tunnel, tunnel_rx) = accept.accept(config).await?; run_tunnel_socket(tunnel, tunnel_rx, socket).await }, Err(err) => { log::warn!("Could not open tunnel to {:?}: {}", connect_addr, err); accept.reject(makiko::ChannelOpenError { reason_code: makiko::codes::open::CONNECT_FAILED, description: format!("Connect attempt failed: {}", err), description_lang: "".into(), }); Ok(()) }, } } async fn bind_remote_tunnels( client: &makiko::Client, remote_tunnel_addrs: &HashMap<(String, u16), (String, u16)>, ) -> Result<()> { for bind_addr in remote_tunnel_addrs.keys() { client.bind_tunnel(bind_addr.clone())?.wait().await?; } Ok(()) } async fn authenticate(client: &makiko::Client, username: String, mut keys: Vec) -> Result<()> { struct AuthCtx<'c> { client: &'c makiko::Client, username: String, methods: HashSet, pubkey_algo_names: Option>, } fn update_methods(ctx: &mut AuthCtx, failure: makiko::AuthFailure) { log::info!("authentication methods that can continue: {:?}", failure.methods_can_continue); ctx.methods = failure.methods_can_continue.into_iter().collect(); } fn update_pubkey_algo_names(ctx: &mut AuthCtx) -> Result<()> { ctx.pubkey_algo_names = ctx.client.auth_pubkey_algo_names()? .map(|names| names.into_iter().collect::>()); if let Some(names) = ctx.pubkey_algo_names.as_ref() { log::info!("server supports these public key algorithms: {:?}", names); } Ok(()) } async fn try_auth_none(ctx: &mut AuthCtx<'_>) -> Result { log::info!("trying 'none' authentication"); match ctx.client.auth_none(ctx.username.clone()).await? { makiko::AuthNoneResult::Success => return Ok(true), makiko::AuthNoneResult::Failure(failure) => update_methods(ctx, failure), } Ok(false) } async fn try_auth_key(ctx: &mut AuthCtx<'_>, key: &mut Key) -> Result { if !ctx.methods.contains("publickey") { return Ok(false) } let pubkey = decode_pubkey(key).await?; for algo in pubkey.algos().iter() { if let Some(names) = ctx.pubkey_algo_names.as_ref() { if !names.contains(algo.name) { continue } } if try_auth_key_algo(ctx, key, &pubkey, algo).await? { return Ok(true) } } Ok(false) } async fn try_auth_key_algo( ctx: &mut AuthCtx<'_>, key: &mut Key, pubkey: &makiko::Pubkey, algo: &'static makiko::PubkeyAlgo, ) -> Result { log::info!("checking 'publickey' authentication with key {}, algorithm {:?}", key.path.display(), algo.name); if !ctx.client.check_pubkey(ctx.username.clone(), &pubkey, algo).await? { return Ok(false) } let privkey = decode_privkey(key).await?; log::info!("trying 'publickey' authentication with key {}, algorithm {:?}", key.path.display(), algo.name); match ctx.client.auth_pubkey(ctx.username.clone(), privkey, algo).await? { makiko::AuthPubkeyResult::Success => return Ok(true), makiko::AuthPubkeyResult::Failure(failure) => update_methods(ctx, failure), } Ok(false) } async fn decode_pubkey(key: &mut Key) -> Result { match &key.decoded { makiko::keys::DecodedPrivkeyNopass::Privkey(privkey) => return Ok(privkey.pubkey()), makiko::keys::DecodedPrivkeyNopass::Pubkey(pubkey) => return Ok(pubkey.clone()), _ => {}, } decode_privkey(key).await.map(|privkey| privkey.pubkey()) } async fn decode_privkey(key: &mut Key) -> Result { loop { if let makiko::keys::DecodedPrivkeyNopass::Privkey(ref privkey) = key.decoded { return Ok(privkey.clone()) } let prompt = format!("ssh: password for key {}", key.path.display()); let password = ask_for_password(&prompt).await?; match makiko::keys::decode_pem_privkey(&key.data, password.as_bytes()) { Ok(privkey) => { key.decoded = makiko::keys::DecodedPrivkeyNopass::Privkey(privkey.clone()); return Ok(privkey) }, Err(makiko::Error::BadKeyPassphrase) => continue, Err(err) => return Err(err.into()), } } } async fn try_auth_password(ctx: &mut AuthCtx<'_>) -> Result { if !ctx.methods.contains("password") { return Ok(false) } log::info!("trying 'password' authentication"); let prompt = format!("ssh: password for user {:?}", ctx.username); let password = ask_for_password(&prompt).await?; match ctx.client.auth_password(ctx.username.clone(), password).await? { makiko::AuthPasswordResult::Success => return Ok(true), makiko::AuthPasswordResult::ChangePassword(prompt) => bail!("server wants you to change your password: {:?}", prompt.prompt), makiko::AuthPasswordResult::Failure(failure) => update_methods(ctx, failure), } Ok(false) } let mut ctx = AuthCtx { client, username, methods: HashSet::new(), pubkey_algo_names: None, }; if try_auth_none(&mut ctx).await? { return Ok(()) } update_pubkey_algo_names(&mut ctx)?; for key in keys.iter_mut() { if try_auth_key(&mut ctx, key).await? { return Ok(()) } } if try_auth_password(&mut ctx).await? { return Ok(()) } bail!("no authentication method succeeded") } async fn run_session(client: makiko::Client, command: Option, want_tty: bool) -> Result { let config = makiko::ChannelConfig::default(); let (session, mut session_rx) = client.open_session(config).await?; let mut pty_req = None; let mut orig_tio = None; if want_tty && termios::isatty(std::io::stdin()) { pty_req = Some(get_pty_request()?); orig_tio = Some(enter_raw_mode()?); } let recv_task = tokio::task::spawn(async move { let mut stdout = tokio::io::stdout(); let mut stderr = tokio::io::stderr(); while let Some(event) = session_rx.recv().await? { match event { makiko::SessionEvent::StdoutData(data) => { stdout.write_all(&data).await?; stdout.flush().await?; }, makiko::SessionEvent::StderrData(data) => { stderr.write_all(&data).await?; stderr.flush().await?; }, makiko::SessionEvent::ExitStatus(status) => { log::info!("received exit status {}", status); return Ok(ExitCode::from(status as u8)) }, makiko::SessionEvent::ExitSignal(signal) => { log::info!("received exit signal {:?}", signal.signal_name); let msg = format!("ssh: remote process exited with signal {:?}: {:?}\n", signal.signal_name, signal.message); stderr.write_all(msg.as_bytes()).await?; return Ok(ExitCode::from(255)) }, _ => {}, } } bail!("session terminated before remote process exited") }); let send_task = tokio::task::spawn(enclose!{(session) async move { if let Some(pty_req) = pty_req.as_ref() { session.request_pty(&pty_req)?.wait().await?; } if let Some(command) = command { session.exec(command.as_bytes())?.wait().await?; } else { session.shell()?.wait().await?; } let mut stdin = tokio::io::stdin(); let mut stdin_buf = BytesMut::new(); let mut winch_stream = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::window_change())?; loop { tokio::select!{ res = stdin.read_buf(&mut stdin_buf) => { if res? > 0 { session.send_stdin(stdin_buf.split().freeze()).await? } else { break } }, Some(()) = winch_stream.recv() => { session.window_change(&get_window_change()?)?; }, } } session.send_eof().await?; Result::<()>::Ok(()) }}); let mut recv_fut = TaskHandle(recv_task); let mut send_fut = TaskHandle(send_task).fuse(); loop { tokio::select!{ recv_res = &mut recv_fut => { if let Some(tio) = orig_tio { leave_raw_mode(tio); } return recv_res }, send_res = &mut send_fut => send_res?, }; } } async fn run_local_tunnel(client: makiko::Client, spec: TunnelSpec) -> Result<()> { let bind_addr = (spec.bind_host.unwrap_or("localhost".into()), spec.bind_port); let listener = tokio::net::TcpListener::bind(bind_addr).await?; let mut socket_tasks = FuturesUnordered::new(); loop { tokio::select!{ res = listener.accept() => { let (socket, peer_addr) = res?; let config = makiko::ChannelConfig::default(); let connect_addr = (spec.connect_host.clone(), spec.connect_port); let originator_addr = (peer_addr.ip().to_string(), peer_addr.port()); let (tunnel, tunnel_rx) = client.connect_tunnel( config, connect_addr, originator_addr).await?; let task = TaskHandle(tokio::task::spawn(run_tunnel_socket(tunnel, tunnel_rx, socket))); socket_tasks.push(task); }, Some(res) = socket_tasks.next() => res?, } } } async fn run_tunnel_socket( tunnel: makiko::Tunnel, mut tunnel_rx: makiko::TunnelReceiver, socket: tokio::net::TcpStream, ) -> Result<()> { let (mut socket_read, mut socket_write) = socket.into_split(); let socket_to_tunnel = TaskHandle(tokio::task::spawn(async move { let mut buffer = BytesMut::new(); while socket_read.read_buf(&mut buffer).await? != 0 { tunnel.send_data(buffer.split().freeze()).await?; } tunnel.send_eof().await?; Result::<_>::Ok(()) })); let tunnel_to_socket = TaskHandle(tokio::task::spawn(async move { while let Some(event) = tunnel_rx.recv().await? { match event { makiko::TunnelEvent::Data(mut data) => socket_write.write_all_buf(&mut data).await?, makiko::TunnelEvent::Eof => break, _ => {}, } } Result::<_>::Ok(()) })); tokio::try_join!(socket_to_tunnel, tunnel_to_socket)?; Ok(()) } #[derive(Debug)] pub struct TaskHandle(pub tokio::task::JoinHandle); impl Future for TaskHandle { type Output = T; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match ready!(Pin::new(&mut self.get_mut().0).poll(cx)) { Ok(res) => Poll::Ready(res), Err(err) if err.is_panic() => std::panic::resume_unwind(err.into_panic()), Err(err) => panic!("Task failed: {}", err), } } } impl Drop for TaskHandle { fn drop(&mut self) { self.0.abort(); } } async fn ask_yes_no(prompt: &str) -> Result { let mut stdout = tokio::io::stdout(); stdout.write_all(format!("{} [y/N]: ", prompt).as_bytes()).await?; stdout.flush().await?; let mut stdin = tokio::io::stdin(); let mut yes = false; loop { let c = stdin.read_u8().await?; if c == b'\r' || c == b'\n' { break } else if c.is_ascii_whitespace() { continue } else if c == b'y' || c == b'Y' { yes = true; } else { yes = false; } } Ok(yes) } async fn ask_for_password(prompt: &str) -> Result { let mut stdout = tokio::io::stdout(); stdout.write_all(format!("{}: ", prompt).as_bytes()).await?; stdout.flush().await?; let mut stdin = tokio::io::stdin(); let stdin_raw = unsafe { rustix::fd::BorrowedFd::borrow_raw(stdin.as_raw_fd()) }; let orig_tio = termios::tcgetattr(stdin_raw)?; let mut tio = orig_tio.clone(); tio.local_modes.remove(termios::LocalModes::ECHO); termios::tcsetattr(stdin_raw, termios::OptionalActions::Drain, &tio)?; let mut password = Vec::new(); loop { let c = stdin.read_u8().await?; if password.is_empty() && c.is_ascii_whitespace() { continue } else if c == b'\r' || c == b'\n' { break } else { password.push(c); } } stdout.write_u8(b'\n').await?; termios::tcsetattr(stdin_raw, termios::OptionalActions::Drain, &orig_tio)?; Ok(std::str::from_utf8(&password)?.into()) } fn enter_raw_mode() -> Result { // this code is shamelessly copied from OpenSSH let stdin = tokio::io::stdin(); let stdin_raw = unsafe { rustix::fd::BorrowedFd::borrow_raw(stdin.as_raw_fd()) }; let orig_tio = termios::tcgetattr(stdin_raw)?; let mut tio = orig_tio.clone(); tio.input_modes.insert(termios::InputModes::IGNPAR); tio.input_modes.remove(termios::InputModes::ISTRIP | termios::InputModes::INLCR | termios::InputModes::IGNCR | termios::InputModes::ICRNL | termios::InputModes::IXON | termios::InputModes::IXANY | termios::InputModes::IXOFF | termios::InputModes::IUCLC, ); tio.local_modes.remove(termios::LocalModes::ISIG | termios::LocalModes::ICANON | termios::LocalModes::ECHO | termios::LocalModes::ECHOE | termios::LocalModes::ECHOK | termios::LocalModes::ECHONL | termios::LocalModes::IEXTEN, ); tio.output_modes.remove(termios::OutputModes::OPOST); tio.special_codes[termios::SpecialCodeIndex::VMIN] = 1; tio.special_codes[termios::SpecialCodeIndex::VTIME] = 0; log::debug!("entering terminal raw mode"); termios::tcsetattr(stdin_raw, termios::OptionalActions::Drain, &tio)?; Ok(orig_tio) } fn leave_raw_mode(tio: termios::Termios) { let stdin = tokio::io::stdin(); let stdin_raw = unsafe { rustix::fd::BorrowedFd::borrow_raw(stdin.as_raw_fd()) }; let _ = termios::tcsetattr(stdin_raw, termios::OptionalActions::Drain, &tio); log::debug!("left terminal raw mode"); } fn get_window_change() -> Result { let winsize = termios::tcgetwinsize(std::io::stdin())?; Ok(makiko::WindowChange { width: winsize.ws_col as u32, height: winsize.ws_row as u32, width_px: winsize.ws_xpixel as u32, height_px: winsize.ws_ypixel as u32, }) } fn get_pty_request() -> Result { // this code is shamelessly copied from OpenSSH let mut req = makiko::PtyRequest::default(); req.term = env::var("TERM").unwrap_or(String::new()); let stdin = tokio::io::stdin(); let stdin_raw = unsafe { rustix::fd::BorrowedFd::borrow_raw(stdin.as_raw_fd()) }; let winsize = termios::tcgetwinsize(stdin_raw)?; req.width = winsize.ws_col as u32; req.height = winsize.ws_row as u32; req.width_px = winsize.ws_xpixel as u32; req.height_px = winsize.ws_ypixel as u32; let tio = termios::tcgetattr(stdin_raw)?; macro_rules! tty_char { ($name:ident, $op:ident) => { let value = tio.special_codes[termios::SpecialCodeIndex::$name]; let value = if value == 0 { 255 } else { value as u32 }; req.modes.add(makiko::codes::terminal_mode::$op, value); }; ($name:ident) => { tty_char!($name, $name) }; } macro_rules! tty_mode { ($mode:expr, $field:ident, $op:ident) => { let value = tio.$field.contains($mode); let value = value as u32; req.modes.add(makiko::codes::terminal_mode::$op, value); }; ($enum:ident :: $name:ident, $field:ident) => { tty_mode!(termios::$enum::$name, $field, $name) }; } tty_char!(VINTR); tty_char!(VQUIT); tty_char!(VERASE); tty_char!(VKILL); tty_char!(VEOF); tty_char!(VEOL); tty_char!(VEOL2); tty_char!(VSTART); tty_char!(VSTOP); tty_char!(VSUSP); tty_char!(VREPRINT); tty_char!(VWERASE); tty_char!(VLNEXT); tty_char!(VDISCARD); tty_mode!(InputModes::IGNPAR, input_modes); tty_mode!(InputModes::PARMRK, input_modes); tty_mode!(InputModes::INPCK, input_modes); tty_mode!(InputModes::ISTRIP, input_modes); tty_mode!(InputModes::INLCR, input_modes); tty_mode!(InputModes::IGNCR, input_modes); tty_mode!(InputModes::ICRNL, input_modes); tty_mode!(InputModes::IUCLC, input_modes); tty_mode!(InputModes::IXON, input_modes); tty_mode!(InputModes::IXANY, input_modes); tty_mode!(InputModes::IXOFF, input_modes); tty_mode!(InputModes::IMAXBEL, input_modes); tty_mode!(InputModes::IUTF8, input_modes); tty_mode!(LocalModes::ISIG, local_modes); tty_mode!(LocalModes::ICANON, local_modes); tty_mode!(LocalModes::XCASE, local_modes); tty_mode!(LocalModes::ECHO, local_modes); tty_mode!(LocalModes::ECHOE, local_modes); tty_mode!(LocalModes::ECHOK, local_modes); tty_mode!(LocalModes::ECHONL, local_modes); tty_mode!(LocalModes::NOFLSH, local_modes); tty_mode!(LocalModes::TOSTOP, local_modes); tty_mode!(LocalModes::IEXTEN, local_modes); tty_mode!(LocalModes::ECHOCTL, local_modes); tty_mode!(LocalModes::ECHOKE, local_modes); tty_mode!(LocalModes::PENDIN, local_modes); tty_mode!(OutputModes::OPOST, output_modes); tty_mode!(OutputModes::OLCUC, output_modes); tty_mode!(OutputModes::ONLCR, output_modes); tty_mode!(OutputModes::OCRNL, output_modes); tty_mode!(OutputModes::ONOCR, output_modes); tty_mode!(OutputModes::ONLRET, output_modes); tty_mode!(ControlModes::CS7, control_modes); tty_mode!(ControlModes::CS8, control_modes); tty_mode!(ControlModes::PARENB, control_modes); tty_mode!(ControlModes::PARODD, control_modes); Ok(req) }