[pve-devel] [PATCH xtermjs] termproxy: rewrite in rust
Wolfgang Bumiller
w.bumiller at proxmox.com
Tue Jul 7 11:34:48 CEST 2020
Some comments.
On Mon, Jul 06, 2020 at 12:57:21PM +0200, Dominik Csapak wrote:
> termproxy is now completely written in rust (instead of perl) but
> it is a drop-in replacement
(...)
> diff --git a/src/main.rs b/src/main.rs
> new file mode 100644
> index 0000000..459a3c0
> --- /dev/null
> +++ b/src/main.rs
> @@ -0,0 +1,378 @@
(...)
> +use std::io::{Error, ErrorKind, Result, Write};
None of our code currently imports a fixed `Result` type, please prefer
just typing `io::Result` (it's not that much longer).
(...)
> +
> +fn remove_number(buf: &mut ByteBuffer) -> Option<usize> {
> + loop {
> + let data = buf.get_data_slice();
> +
> + if let Some(pos) = data.iter().position(|&x| x == ':' as u8) {
use b':' (see `cargo +nightly clippy`)
> + let len = buf.consume(pos);
please don't use the name `len` for a `Box<[u8]>`, that's confusing
> + buf.consume(1); // the ':'
reminder: this `consume` currently returns a heap-allocated copy of the
consumed data (this should be changed in the ByteBuffer API)
> + let len = match String::from_utf8_lossy(&len).parse() {
`from_utf8_lossy` potentially allocates, but whenever it does, `parse()`
will always fail, so consider using `std::str::from_utf8` instead...
> + Ok(len) => len,
> + Err(err) => {
> + eprintln!("error parsing number: '{}'", err);
> + break;
> + }
> + };
> + return Some(len);
> + } else if data.len() > 20 {
> + buf.consume(20);
> + } else {
> + break;
> + }
> + }
> + None
> +}
> +
> +fn process_queue(buf: &mut ByteBuffer, pty: &mut PTY) -> Option<usize> {
> + if buf.is_empty() {
> + return None;
> + }
> +
> + loop {
> + let data = buf.get_data_slice();
> + let len = buf.data_size();
`len` is used only once here so maybe just drop it
> +
> + if len < 2 {
and then `data.len()` will be shorter and easier to follow than `buf.data_size()`
> + break;
> + }
> +
> + let msgtype = data[0] - '0' as u8;
b'0' again
> +
> + if msgtype == 0 {
please introduce some `const`s with meaningful names for message types
> + buf.consume(2);
> + if let Some(len) = remove_number(buf) {
> + return Some(len);
> + }
> + } else if msgtype == 1 {
> + buf.consume(2);
> + if let Some(cols) = remove_number(buf) {
> + if let Some(rows) = remove_number(buf) {
> + pty.set_size(cols as u16, rows as u16).ok()?;
> + }
Shouldn't the inner `if` have an `else`? `cols` was parsed successfully
here, so if there's no `rows` we'll re-loop back to grabbing the message
type, simply discarding the `cols` value?
> + }
> + // ignore incomplete messages
> + } else if msgtype == 2 {
> + buf.consume(1);
> + // ignore ping
> + } else {
> + buf.consume(1);
> + // ignore invalid
> + }
> + }
> +
> + None
> +}
> +
> +/// Reads from the stream and returns the first line and the rest
> +fn read_ticket_line(
> + stream: &mut TcpStream,
> + buf: &mut ByteBuffer,
> + timeout: u64,
`u64` is a really bad type for a timeout, it doesn't say anything about
the unit, also, the implementation isn't necessarily using time to
measure this (see below).
> +) -> Result<(Box<[u8]>, Box<[u8]>)> {
Clippy complains, but won't complaina bout a pair of `Vec<u8>` which you
already have anyway (but not really, see below...)
> + stream.set_read_timeout(Some(Duration::new(1, 0)))?;
This makes the reader believe that `timeout` will be in seconds but ...
> + let mut count = 0;
> + while !buf.get_data_slice().contains(&('\n' as u8)) {
> + if buf.is_full() || count >= timeout {
> + return Err(Error::new(
> + ErrorKind::InvalidData,
> + "authentication data is invalid",
> + ));
> + }
> + buf.read_from(stream)?;
> + count += 1;
... consider a short burst of 1-byte packets arriving, as many as the
number in `timeout`, then we also reach the count.
> + }
> +
> + stream.set_read_timeout(None)?;
> + let newline_idx = buf
> + .get_data_slice()
> + .iter()
> + .position(|&x| x == '\n' as u8)
> + .unwrap();
> +
> + let line = buf.consume(newline_idx);
> + buf.consume(1); // discard newline
> +
> + let mut iter = line.splitn(2, |&c| c == ':' as u8);
> + let username = iter.next().unwrap();
> + let ticket = iter.next().unwrap();
These `unwrap()`s are invalid. Consider using `.position()` to find the
b':' to have only 1 error case, then use the slice's `split_at()` method
to get a &[u8] 2-tuple:
match line.iter().position(|&b| b == b':') {
Some(pos) => {
let (username, ticket) = line.split_at(pos);
Ok((username.into(), ticket.into()))
}
None => bail!("with a nice error message"),
}
> + Ok((
> + username.to_vec().into_boxed_slice(),
> + ticket.to_vec().into_boxed_slice(),
> + ))
> +}
(...)
> +fn listen_and_accept(hostname: &str, port: u16, timeout: u64) -> Result<(TcpStream, SocketAddr)> {
> + let listener = TcpListener::bind((hostname, port))?;
> + listener.set_nonblocking(true)?;
This turns the loop below into a busy-wait loop. Please use `poll` on
the listen socket for the timeout.
> +
> + let now = Instant::now();
> + loop {
> + if now.elapsed().as_secs() > timeout {
> + return Err(Error::new(
> + ErrorKind::TimedOut,
> + "timed out waiting for client",
> + ));
> + }
> +
> + match listener.accept() {
> + Err(err) => {
> + if err.kind() == ErrorKind::WouldBlock {
> + continue;
> + } else {
> + return Err(err);
> + }
> + }
> + Ok(other) => return Ok(other),
> + };
> + }
> +}
> +
> +fn run_pty(cmd: &OsStr, params: clap::OsValues) -> Result<PTY> {
> + let (mut pty, secondary_name) = PTY::new().map_err(io_err_other)?;
> +
> + let mut filtered_env: HashMap<OsString, OsString> = std::env::vars_os()
> + .filter(|&(ref k, _)| {
> + k == "PATH" || k == "USER" || k == "HOME" || k == "LANG" || k == "LANGUAGE" ||
> + k.to_string_lossy().starts_with("LC_")
Please use `OsStrExt`'s .as_bytes() instead of allocating a new `String`
here.
> + }).collect();
> + filtered_env.insert("TERM".into(), "xterm-256color".into());
> +
> + let mut command = Command::new(cmd);
> +
> + command
> + .args(params)
> + .env_clear()
> + .envs(&filtered_env);
> +
> + unsafe {
> + command.pre_exec(move || {
> + make_controlling_terminal(&secondary_name).map_err(io_err_other)?;
> + Ok(())
> + });
> + }
> +
> + command.spawn()?;
> +
> + pty.set_size(80, 20).map_err(|x| x.as_errno().unwrap())?;
Another `unwrap` I don't find very convincing.
Consider using `into_io_error` from `proxmox::sys::error::SysError` or
`SysResult`.
> + Ok(pty)
> +}
> +
> +const TCP: Token = Token(0);
> +const PTY: Token = Token(1);
> +
> +fn main() -> Result<()> {
> + let matches = App::new("termproxy")
> + .setting(AppSettings::TrailingVarArg)
> + .arg(Arg::with_name("port").takes_value(true).required(true))
> + .arg(
> + Arg::with_name("authport")
> + .takes_value(true)
> + .long("authport"),
> + )
> + .arg(
> + Arg::with_name("path")
> + .takes_value(true)
> + .long("path")
> + .required(true),
> + )
> + .arg(Arg::with_name("perm").takes_value(true).long("perm"))
> + .arg(Arg::with_name("cmd").multiple(true).required(true))
> + .get_matches();
> +
> + let port: u16 = matches .value_of("port") .unwrap() .parse() .map_err(io_err_other)?;
> + let path = matches.value_of("path").unwrap();
> + let perm: Option<&str> = matches.value_of("perm");
> + let mut cmdparams = matches.values_of_os("cmd").unwrap();
> + let cmd = cmdparams.next().unwrap();
> + let authport: u16 = matches .value_of("authport") .unwrap_or("85") .parse() .map_err(io_err_other)?;
> + let mut pty_buf = ByteBuffer::new();
> + let mut tcp_buf = ByteBuffer::new();
> +
> + let (mut stream, client) = listen_and_accept("localhost", port, 10)?;
> +
> + let (username, ticket) = read_ticket_line(&mut stream, &mut pty_buf, 30)?;
> + authenticate(&username, &ticket, path, perm, authport)?;
> + stream.write_all("OK".as_bytes())?;
Use b"OK".
(...)
> + while tcp_readable && !pty_buf.is_full() {
> + let bytes = match pty_buf.read_from(&mut tcp_handle) {
You can handle `0` here:
Ok(0) => return Ok(()),
Skip the `let bytes =` binding and the `if bytes == 0` below.
Makes it a bit shorter.
> + Ok(bytes) => bytes,
> + Err(err) if err.kind() == ErrorKind::WouldBlock => {
> + tcp_readable = false;
> + break;
> + }
> + Err(err) => return Err(err),
> + };
> + if bytes == 0 {
> + return Ok(());
> + }
> + }
> +
> + while pty_readable && !tcp_buf.is_full() {
> + let bytes = match tcp_buf.read_from(&mut pty) {
> + Ok(bytes) => bytes,
> + Err(err) if err.kind() == ErrorKind::WouldBlock => {
> + pty_readable = false;
> + break;
> + }
> + Err(err) => return Err(err),
> + };
> + if bytes == 0 {
> + return Ok(());
> + }
> + }
> +
> + while !tcp_buf.is_empty() && tcp_writable {
> + let bytes = match tcp_handle.write(tcp_buf.get_data_slice()) {
> + Ok(bytes) => bytes,
Similarly the let binding can go and the consume call put in the Ok case
here.
It's a bit more concise and this part here is quite long repetitive
code...
> + Err(err) if err.kind() == ErrorKind::WouldBlock => {
> + tcp_writable = false;
> + break;
> + }
> + Err(err) => return Err(err),
> + };
> + tcp_buf.consume(bytes);
> + }
> +
> + while !pty_buf.is_empty() && pty_writable {
> + if remaining == 0 {
> + remaining = match process_queue(&mut pty_buf, &mut pty) {
> + Some(val) => val,
> + None => break,
> + };
> + }
> + let len = min(remaining, pty_buf.data_size());
> + let bytes = match pty.write(&pty_buf.get_data_slice()[0..len]) {
> + Ok(bytes) => bytes,
> + Err(err) if err.kind() == ErrorKind::WouldBlock => {
> + pty_writable = false;
> + break;
> + }
> + Err(err) => return Err(err),
> + };
> + remaining -= bytes;
> + pty_buf.consume(bytes);
> + }
> + }
> +}
More information about the pve-devel
mailing list