From d92ab7888e3c48ac721868d0e5de15d0030b1b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0pa=C4=8Dek?= Date: Sun, 17 Jul 2022 15:30:52 +0200 Subject: [PATCH] Handle pseudo-terminal in the example client --- Cargo.toml | 2 +- examples/client.rs | 305 +++++++++++++++++++++++++++++++++--------- src/client/channel.rs | 2 +- src/client/client.rs | 2 +- src/client/mod.rs | 5 +- src/client/session.rs | 114 ++++++++++++++++ src/codes.rs | 129 ++++++++++++++++++ src/lib.rs | 5 +- 8 files changed, 496 insertions(+), 68 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index da4660d..e6c1b1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,7 +66,7 @@ enclose = "1.1" env_logger = "0.9" futures = "0.3" regex = "1.5" -termios = "0.3" +rustix = {version = "0.35", features = ["termios"]} tokio = {version = "1", features = ["full"]} [features] diff --git a/examples/client.rs b/examples/client.rs index 8b513f6..7e589b5 100644 --- a/examples/client.rs +++ b/examples/client.rs @@ -3,8 +3,9 @@ use bytes::BytesMut; use enclose::enclose; use futures::future::{FutureExt as _, FusedFuture as _}; use regex::Regex; +use rustix::termios; use std::collections::HashSet; -use std::fs; +use std::{env, fs}; use std::future::Future; use std::os::unix::io::AsRawFd as _; use std::path::{Path, PathBuf}; @@ -46,6 +47,8 @@ fn run_main() -> Result { .arg(clap::Arg::new("command") .takes_value(true) .value_name("command")) + .arg(clap::Arg::new("want-tty").short('t') + .action(clap::ArgAction::SetTrue)) .get_matches(); let mut destination = Destination::default(); @@ -61,10 +64,11 @@ fn run_main() -> Result { .collect::>>()?; let command = matches.get_one::("command").cloned(); + let want_tty = *matches.get_one::("want-tty").unwrap() || command.is_none(); let runtime = tokio::runtime::Builder::new_current_thread() .enable_all().build()?; - let exit_code = runtime.block_on(run_client(destination, keys, command))?; + let exit_code = runtime.block_on(run_client(destination, keys, command, want_tty))?; runtime.shutdown_background(); Ok(exit_code) } @@ -109,7 +113,12 @@ fn read_key(path: &Path) -> Result { Ok(Key { path: path.into(), data, decoded }) } -async fn run_client(destination: Destination, keys: Vec, command: Option) -> Result { +async fn run_client( + destination: Destination, + keys: Vec, + command: Option, + want_tty: bool, +) -> Result { let host = destination.host .context("please specify the host to connect to")?; let username = destination.username @@ -141,7 +150,7 @@ async fn run_client(destination: Destination, keys: Vec, command: Option::Ok(exit_code) }}); @@ -308,73 +317,33 @@ async fn authenticate(client: &makiko::Client, username: String, keys: Vec) bail!("no authentication method succeeded") } -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 orig_termios = termios::Termios::from_fd(stdin.as_raw_fd())?; - - let mut termios = orig_termios; - termios.c_lflag &= !termios::ECHO; - termios::tcsetattr(stdin.as_raw_fd(), termios::TCSADRAIN, &termios)?; - - 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.as_raw_fd(), termios::TCSADRAIN, &orig_termios)?; - Ok(std::str::from_utf8(&password)?.into()) -} - async fn interact( session: makiko::Session, mut session_rx: makiko::SessionReceiver, command: Option, + want_tty: bool, ) -> Result { + 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?, - makiko::SessionEvent::StderrData(data) => - stderr.write_all(&data).await?, + 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)) @@ -393,6 +362,10 @@ async fn interact( }); let send_task = tokio::task::spawn(enclose!{(session) async move { + if let Some(pty_req) = pty_req.as_ref() { + session.request_pty(&pty_req)?.want_reply().await?; + } + if let Some(command) = command { session.exec(command.as_bytes())?.want_reply().await?; } else { @@ -401,9 +374,22 @@ async fn interact( let mut stdin = tokio::io::stdin(); let mut stdin_buf = BytesMut::new(); - while stdin.read_buf(&mut stdin_buf).await? != 0 { - session.send_stdin(stdin_buf.split().freeze()).await?; + 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(()) }}); @@ -412,7 +398,12 @@ async fn interact( let mut send_fut = AbortOnDrop(send_task).map(|res| res.expect("sending task panicked")).fuse(); loop { tokio::select!{ - recv_res = &mut recv_fut => return recv_res, + 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?, }; } @@ -434,3 +425,191 @@ impl Drop for AbortOnDrop { } } +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; + tio.c_lflag &= !termios::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; + + tio.c_iflag |= termios::IGNPAR; + tio.c_iflag &= !(termios::ISTRIP | termios::INLCR | termios::IGNCR | termios::ICRNL + | termios::IXON | termios::IXANY | termios::IXOFF | termios::IUCLC); + tio.c_lflag &= !(termios::ISIG | termios::ICANON | termios::ECHO | termios::ECHOE + | termios::ECHOK | termios::ECHONL | termios::IEXTEN); + tio.c_oflag &= !termios::OPOST; + tio.c_cc[termios::VMIN] = 1; + tio.c_cc[termios::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.c_cc[termios::$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 { + ($name:ident, $field:ident, $op:ident) => { + let value = (tio.$field & termios::$name) != 0; + let value = value as u32; + req.modes.add(makiko::codes::terminal_mode::$op, value); + }; + ($name:ident, $field:ident) => { + tty_mode!($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!(IGNPAR, c_iflag); + tty_mode!(PARMRK, c_iflag); + tty_mode!(INPCK, c_iflag); + tty_mode!(ISTRIP, c_iflag); + tty_mode!(INLCR, c_iflag); + tty_mode!(IGNCR, c_iflag); + tty_mode!(ICRNL, c_iflag); + tty_mode!(IUCLC, c_iflag); + tty_mode!(IXON, c_iflag); + tty_mode!(IXANY, c_iflag); + tty_mode!(IXOFF, c_iflag); + tty_mode!(IMAXBEL, c_iflag); + tty_mode!(IUTF8, c_iflag); + + tty_mode!(ISIG, c_lflag); + tty_mode!(ICANON, c_lflag); + tty_mode!(XCASE, c_lflag); + tty_mode!(ECHO, c_lflag); + tty_mode!(ECHOE, c_lflag); + tty_mode!(ECHOK, c_lflag); + tty_mode!(ECHONL, c_lflag); + tty_mode!(NOFLSH, c_lflag); + tty_mode!(TOSTOP, c_lflag); + tty_mode!(IEXTEN, c_lflag); + tty_mode!(ECHOCTL, c_lflag); + tty_mode!(ECHOKE, c_lflag); + tty_mode!(PENDIN, c_lflag); + + tty_mode!(OPOST, c_oflag); + tty_mode!(OLCUC, c_oflag); + tty_mode!(ONLCR, c_oflag); + tty_mode!(OCRNL, c_oflag); + tty_mode!(ONOCR, c_oflag); + tty_mode!(ONLRET, c_oflag); + + tty_mode!(CS7, c_cflag); + tty_mode!(CS8, c_cflag); + tty_mode!(PARENB, c_cflag); + tty_mode!(PARODD, c_cflag); + + Ok(req) +} diff --git a/src/client/channel.rs b/src/client/channel.rs index f8bd4db..5e3b48d 100644 --- a/src/client/channel.rs +++ b/src/client/channel.rs @@ -317,7 +317,7 @@ impl Default for ChannelConfig { } impl ChannelConfig { - /// Mutate `self` in a closure. + /// Update the configuration in pseudo-builder pattern style. /// /// This method applies your closure to `self` and returns the mutated configuration. pub fn with(mut self, f: F) -> Self { diff --git a/src/client/client.rs b/src/client/client.rs index 681b6d7..70cc06f 100644 --- a/src/client/client.rs +++ b/src/client/client.rs @@ -447,7 +447,7 @@ impl ClientConfig { }) } - /// Mutate `self` in a closure. + /// Update the configuration in pseudo-builder pattern style. /// /// This method applies your closure to `self` and returns the mutated configuration. pub fn with(mut self, f: F) -> Self { diff --git a/src/client/mod.rs b/src/client/mod.rs index ba8c088..4aa3748 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -8,7 +8,10 @@ pub use self::channel::{ }; pub use self::client::{Client, ClientReceiver, ClientFuture, ClientConfig}; pub use self::client_event::{ClientEvent, AcceptPubkeySender}; -pub use self::session::{Session, SessionReceiver, SessionEvent, SessionReply, ExitSignal}; +pub use self::session::{ + Session, SessionReceiver, SessionEvent, SessionReply, ExitSignal, + PtyRequest, PtyTerminalModes, WindowChange, +}; #[macro_use] mod pump; mod auth; diff --git a/src/client/session.rs b/src/client/session.rs index 1a0d773..eb713f0 100644 --- a/src/client/session.rs +++ b/src/client/session.rs @@ -81,6 +81,37 @@ impl Session { })?; Ok(SessionReply { reply_rx }) } + + /// Request a pseudo-terminal (pty) for the future process. + /// + /// This will allocate a pseudo-terminal according to the `request`. + /// + /// This method returns immediately without any blocking, but you may use the returned + /// [`SessionReply`] to wait for the server response. + pub fn request_pty(&self, request: &PtyRequest) -> Result { + let (reply_tx, reply_rx) = oneshot::channel(); + let mut payload = PacketEncode::new(); + payload.put_str(&request.term); + payload.put_u32(request.width); + payload.put_u32(request.height); + payload.put_u32(request.width_px); + payload.put_u32(request.height_px); + + let mut encoded_modes = PacketEncode::new(); + for &(op, arg) in request.modes.opcodes.iter() { + encoded_modes.put_u8(op); + encoded_modes.put_u32(arg); + } + encoded_modes.put_u8(0); + payload.put_bytes(&encoded_modes.finish()); + + self.channel.send_request(ChannelReq { + request_type: "pty-req".into(), + payload: payload.finish(), + reply_tx: Some(reply_tx), + })?; + Ok(SessionReply { reply_rx }) + } } /// # Starting the process @@ -177,6 +208,24 @@ impl Session { })?; Ok(()) } + + /// Notify the process that the terminal window size has changed. + /// + /// This method returns immediately without any blocking, it is not possible to get a reply + /// from the server. + pub fn window_change(&self, change: &WindowChange) -> Result<()> { + let mut payload = PacketEncode::new(); + payload.put_u32(change.width); + payload.put_u32(change.height); + payload.put_u32(change.width_px); + payload.put_u32(change.height_px); + self.channel.send_request(ChannelReq { + request_type: "window-change".into(), + payload: payload.finish(), + reply_tx: None, + })?; + Ok(()) + } } @@ -344,3 +393,68 @@ fn translate_request(request: ChannelReq) -> Result> { } Ok(Some(event)) } + + + +/// Pseudo-terminal request. +/// +/// Request for allocation of a pseudo-terminal (pty), used with [`Session::request_pty()`], as +/// described in RFC 4254, section 6.2. +#[derive(Debug, Clone, Default)] +pub struct PtyRequest { + /// Value of the `TERM` environment variable (e.g. `"vt100"`). + pub term: String, + /// Terminal width in characters (e.g. 80). + pub width: u32, + /// Terminal height in rows (e.g. 24). + pub height: u32, + /// Terminal width in pixels (e.g. 640). + pub width_px: u32, + /// Terminal height in pixels (e.g. 480). + pub height_px: u32, + /// Terminal modes. + pub modes: PtyTerminalModes, +} + +/// Terminal modes for a [`PtyRequest`]. +/// +/// The terminal modes are encoded as a sequence of opcodes that take a single `u32` argument. For +/// more details, please see RFC 4254, section 8. +#[derive(Debug, Clone, Default)] +pub struct PtyTerminalModes { + opcodes: Vec<(u8, u32)>, +} + +impl PtyTerminalModes { + /// Create an empty instance. + pub fn new() -> Self { + Default::default() + } + + /// Adds an opcode with a single `u32` argument. + /// + /// Only opcodes between 1 and 159 are supported. This method will panic if you try to use it + /// with other opcodes. In particular, do not use the `TTY_OP_END` (0) opcode. + /// + /// The opcodes are defined in [`codes::terminal_mode`][crate::codes::terminal_mode]. + pub fn add(&mut self, opcode: u8, arg: u32) { + assert!(opcode >= 1 && opcode < 160); + self.opcodes.push((opcode, arg)); + } +} + +/// Change of terminal window dimensions. +/// +/// Notification that window dimensions have changed, used with [`Session::window_change()`], as +/// described in RFC 4254, section 6.7. +#[derive(Debug, Copy, Clone)] +pub struct WindowChange { + /// Terminal width in characters (e.g. 80). + pub width: u32, + /// Terminal height in rows (e.g. 24). + pub height: u32, + /// Terminal width in pixels (e.g. 640). + pub width_px: u32, + /// Terminal height in pixels (e.g. 480). + pub height_px: u32, +} diff --git a/src/codes.rs b/src/codes.rs index 41fa855..9857668 100644 --- a/src/codes.rs +++ b/src/codes.rs @@ -121,3 +121,132 @@ pub mod signal { pub static USR1: &str = "USR1"; pub static USR2: &str = "USR2"; } + +/// Terminal mode opcodes for [`PtyTerminalModes`][crate::PtyTerminalModes]. +/// +/// For more information, please consult RFC 4254, section 8. +pub mod terminal_mode { + /// Indicates end of options. + pub const TTY_OP_END: u8 = 0; + + /// Interrupt character; 255 if none. Similarly for the other characters. Not all of these + /// characters are supported on all systems. + pub const VINTR: u8 = 1; + /// The quit character (sends SIGQUIT signal on POSIX systems). + pub const VQUIT: u8 = 2; + /// Erase the character to left of the cursor. + pub const VERASE: u8 = 3; + /// Kill the current input line. + pub const VKILL: u8 = 4; + /// End-of-file character (sends EOF from the terminal). + pub const VEOF: u8 = 5; + /// End-of-line character in addition to carriage return and/or linefeed. + pub const VEOL: u8 = 6; + /// Additional end-of-line character. + pub const VEOL2: u8 = 7; + /// Continues paused output (normally control-Q). + pub const VSTART: u8 = 8; + /// Pauses output (normally control-S). + pub const VSTOP: u8 = 9; + /// Suspends the current program. + pub const VSUSP: u8 = 10; + /// Another suspend character. + pub const VDSUSP: u8 = 11; + /// Reprints the current input line. + pub const VREPRINT: u8 = 12; + /// Erases a word left of cursor. + pub const VWERASE: u8 = 13; + /// Enter the next character typed literally, even if it is a special character + pub const VLNEXT: u8 = 14; + /// Character to flush output. + pub const VFLUSH: u8 = 15; + /// Switch to a different shell layer. + pub const VSWTCH: u8 = 16; + /// Prints system status line (load, command, pid, etc). + pub const VSTATUS: u8 = 17; + /// Toggles the flushing of terminal output. + pub const VDISCARD: u8 = 18; + + /// The ignore parity flag. The parameter SHOULD be 0 if this flag is FALSE, and 1 if it is + /// TRUE. + pub const IGNPAR: u8 = 30; + /// Mark parity and framing errors. + pub const PARMRK: u8 = 31; + /// Enable checking of parity errors. + pub const INPCK: u8 = 32; + /// Strip 8th bit off characters. + pub const ISTRIP: u8 = 33; + /// Map NL into CR on input. + pub const INLCR: u8 = 34; + /// Ignore CR on input. + pub const IGNCR: u8 = 35; + /// Map CR to NL on input. + pub const ICRNL: u8 = 36; + /// Translate uppercase characters to lowercase. + pub const IUCLC: u8 = 37; + /// Enable output flow control. + pub const IXON: u8 = 38; + /// Any char will restart after stop. + pub const IXANY: u8 = 39; + /// Enable input flow control. + pub const IXOFF: u8 = 40; + /// Ring bell on input queue full. + pub const IMAXBEL: u8 = 41; + /// Terminal input and output is assumed to be encoded in UTF-8. + pub const IUTF8: u8 = 42; + + /// Enable signals INTR, QUIT, [D]SUSP. + pub const ISIG: u8 = 50; + /// Canonicalize input lines. + pub const ICANON: u8 = 51; + /// Enable input and output of uppercase characters by preceding their lowercase equivalents + /// with "\". + pub const XCASE: u8 = 52; + /// Enable echoing. + pub const ECHO: u8 = 53; + /// Visually erase chars. + pub const ECHOE: u8 = 54; + /// Kill character discards current line. + pub const ECHOK: u8 = 55; + /// Echo NL even if ECHO is off. + pub const ECHONL: u8 = 56; + /// Don't flush after interrupt. + pub const NOFLSH: u8 = 57; + /// Stop background jobs from output. + pub const TOSTOP: u8 = 58; + /// Enable extensions. + pub const IEXTEN: u8 = 59; + /// Echo control characters as ^(Char). + pub const ECHOCTL: u8 = 60; + /// Visual erase for line kill. + pub const ECHOKE: u8 = 61; + /// Retype pending input. + pub const PENDIN: u8 = 62; + + /// Enable output processing. + pub const OPOST: u8 = 70; + /// Convert lowercase to uppercase. + pub const OLCUC: u8 = 71; + /// Map NL to CR-NL. + pub const ONLCR: u8 = 72; + /// Translate carriage return to newline (output). + pub const OCRNL: u8 = 73; + /// Translate newline to carriage return-newline (output). + pub const ONOCR: u8 = 74; + /// Newline performs a carriage return (output). + pub const ONLRET: u8 = 75; + + /// 7 bit mode. + pub const CS7: u8 = 90; + /// 8 bit mode. + pub const CS8: u8 = 91; + /// Parity enable. + pub const PARENB: u8 = 92; + /// Odd parity, else even. + pub const PARODD: u8 = 93; + + /// Specifies the input baud rate in bits per second. + pub const TTY_OP_ISPEED: u8 = 128; + /// Specifies the output baud rate in bits per second. + pub const TTY_OP_OSPEED: u8 = 129; +} diff --git a/src/lib.rs b/src/lib.rs index ab071d3..567c97f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,10 @@ pub use crate::client::{ }; pub use crate::client::{Client, ClientReceiver, ClientFuture, ClientConfig}; pub use crate::client::{ClientEvent, AcceptPubkeySender}; -pub use crate::client::{Session, SessionReceiver, SessionEvent, SessionReply, ExitSignal}; +pub use crate::client::{ + Session, SessionReceiver, SessionEvent, SessionReply, ExitSignal, + PtyRequest, PtyTerminalModes, WindowChange, +}; pub use crate::codec::{PacketEncode, PacketDecode}; pub use crate::error::{Result, Error, AlgoNegotiateError, DisconnectError, ChannelOpenError};