X-Git-Url: https://git.proxmox.com/?p=pve-xtermjs.git;a=blobdiff_plain;f=termproxy%2Fsrc%2Fmain.rs;fp=termproxy%2Fsrc%2Fmain.rs;h=a97396a8a997b53554476d07a89c3855d9aceafe;hp=e9181fbce173b0fb5c5e024f6df77c91216d0f95;hb=24d707d0506b120a085b06b5f2b6000696879a1e;hpb=749ebb0907293a9f1cf0f5074e0a240f39f94f6f diff --git a/termproxy/src/main.rs b/termproxy/src/main.rs index e9181fb..a97396a 100644 --- a/termproxy/src/main.rs +++ b/termproxy/src/main.rs @@ -2,13 +2,13 @@ use std::cmp::min; use std::collections::HashMap; use std::ffi::OsString; use std::io::{ErrorKind, Write}; +use std::os::fd::RawFd; use std::os::unix::io::{AsRawFd, FromRawFd}; use std::os::unix::process::CommandExt; use std::process::Command; use std::time::{Duration, Instant}; use anyhow::{bail, format_err, Result}; -use clap::Arg; use mio::net::{TcpListener, TcpStream}; use mio::unix::SourceFd; use mio::{Events, Interest, Poll, Token}; @@ -141,28 +141,27 @@ fn read_ticket_line( } } -fn authenticate( - username: &[u8], - ticket: &[u8], - path: &str, - perm: Option<&str>, - authport: u16, - port: Option, -) -> Result<()> { +fn authenticate(username: &[u8], ticket: &[u8], options: &Options, listen_port: u16) -> Result<()> { let mut post_fields: Vec<(&str, &str)> = Vec::with_capacity(5); post_fields.push(("username", std::str::from_utf8(username)?)); post_fields.push(("password", std::str::from_utf8(ticket)?)); - post_fields.push(("path", path)); - if let Some(perm) = perm { + post_fields.push(("path", &options.acl_path)); + if let Some(perm) = options.acl_permission.as_ref() { post_fields.push(("privs", perm)); } + + // if the listen-port was passed indirectly via an FD, it's encoded also in the ticket so that + // the access system can enforce that the users actually can access that port. let port_str; - if let Some(port) = port { - port_str = port.to_string(); + if options.listen_port.is_fd() { + port_str = listen_port.to_string(); post_fields.push(("port", &port_str)); } - let url = format!("http://localhost:{}/api2/json/access/ticket", authport); + let url = format!( + "http://localhost:{}/api2/json/access/ticket", + options.api_daemon_port + ); match ureq::post(&url).send_form(&post_fields[..]) { Ok(res) if res.status() == 200 => Ok(()), @@ -176,14 +175,12 @@ fn authenticate( fn listen_and_accept( hostname: &str, - port: u64, - port_as_fd: bool, + listen_port: &PortOrFd, timeout: Duration, ) -> Result<(TcpStream, u16)> { - let listener = if port_as_fd { - unsafe { std::net::TcpListener::from_raw_fd(port as i32) } - } else { - std::net::TcpListener::bind((hostname, port as u16))? + let listener = match listen_port { + PortOrFd::Fd(fd) => unsafe { std::net::TcpListener::from_raw_fd(*fd) }, + PortOrFd::Port(port) => std::net::TcpListener::bind((hostname, *port as u16))?, }; let port = listener.local_addr()?.port(); let mut listener = TcpListener::from_std(listener); @@ -201,7 +198,7 @@ fn listen_and_accept( poll.poll(&mut events, Some(timeout - elapsed))?; if !events.is_empty() { let (stream, client) = listener.accept()?; - println!("client connection: {:?}", client); + println!("client connection: {client:?}"); return Ok((stream, port)); } @@ -250,42 +247,107 @@ fn run_pty<'a>(mut full_cmd: impl Iterator) -> Result const TCP: Token = Token(0); const PTY: Token = Token(1); -fn do_main() -> Result<()> { - let matches = clap::builder::Command::new("termproxy") - .trailing_var_arg(true) - .arg( - Arg::new("port") - .num_args(1) - .required(true) - .value_parser(clap::value_parser!(u64)), - ) - .arg(Arg::new("authport").num_args(1).long("authport")) - .arg(Arg::new("use-port-as-fd").long("port-as-fd")) - .arg(Arg::new("path").num_args(1).long("path").required(true)) - .arg(Arg::new("perm").num_args(1).long("perm")) - .arg( - Arg::new("cmd") - .value_parser(clap::value_parser!(OsString)) - .num_args(1..) - .required(true), - ) - .get_matches(); - - let port: u64 = *matches.get_one("port").unwrap(); - let path = matches.get_one::("path").unwrap(); - let perm = matches.get_one::("perm").map(|x| x.as_str()); - let full_cmd: clap::parser::ValuesRef = matches.get_many("cmd").unwrap(); - let authport: u16 = *matches.get_one("authport").unwrap_or(&85); - let use_port_as_fd = matches.contains_id("use-port-as-fd"); - - if use_port_as_fd && port > std::os::fd::RawFd::MAX as u64 { - return Err(format_err!("FD too big")); - } else if !use_port_as_fd && port > u16::MAX as u64 { - return Err(format_err!("invalid port number")); +const CMD_HELP: &str = "\ +Usage: proxmox-termproxy [OPTIONS] --path -- ... + +Arguments: + Port or file descriptor to listen for TCP connections + ... The command to run connected via a proxied PTY + +Options: + --authport Port to relay auth-request, default 85 + --port-as-fd Use as file descriptor. + --path ACL object path to test on. + --perm Permission to test. + -h, --help Print help +"; + +#[derive(Debug)] +enum PortOrFd { + Port(u16), + Fd(RawFd), +} + +impl PortOrFd { + fn from_cli(value: u64, use_as_fd: bool) -> Result { + if use_as_fd { + if value > RawFd::MAX as u64 { + bail!("FD value too big"); + } + Ok(Self::Fd(value as RawFd)) + } else { + if value > u16::MAX as u64 { + bail!("invalid port number"); + } + Ok(Self::Port(value as u16)) + } + } + + fn is_fd(&self) -> bool { + match self { + Self::Fd(_) => true, + _ => false, + } + } +} + +#[derive(Debug)] +struct Options { + /// The actual command to run proxied in a pseudo terminal. + terminal_command: Vec, + /// The port or FD that termproxy will listen on for an incoming conection + listen_port: PortOrFd, + /// The port of the local privileged daemon that authentication is relayed to. Defaults to `85` + api_daemon_port: u16, + /// The ACL object path the 'acl_permission' is checked on + acl_path: String, + /// The ACL permission that the ticket, read from the stream, is required to have on 'acl_path' + acl_permission: Option, +} + +fn parse_args() -> Result { + let mut args: Vec<_> = std::env::args_os().collect(); + args.remove(0); // remove the executable path. + + // handle finding command after `--` first so that we only parse our options later + let terminal_command = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") { + let later_args = args.drain(dash_dash + 1..).collect(); + args.pop(); // .. then remove the `--` + Some(later_args) + } else { + None + }; + + // Now pass the remaining arguments through to `pico_args`. + let mut args = pico_args::Arguments::from_vec(args); + + if args.contains(["-h", "--help"]) { + print!("{CMD_HELP}"); + std::process::exit(0); + } else if terminal_command.is_none() { + bail!("missing terminal command or -- option-end marker, see '-h' for usage"); + } + + let options = Options { + terminal_command: terminal_command.unwrap(), // checked above + listen_port: PortOrFd::from_cli(args.free_from_str()?, args.contains("--port-as-fd"))?, + api_daemon_port: args.opt_value_from_str("--authport")?.unwrap_or(85), + acl_path: args.value_from_str("--path")?, + acl_permission: args.opt_value_from_str("--perm")?, + }; + + if !args.finish().is_empty() { + bail!("unexpected extra arguments, use '-h' for usage"); } - let (mut tcp_handle, port) = - listen_and_accept("localhost", port, use_port_as_fd, Duration::new(10, 0)) + Ok(options) +} + +fn do_main() -> Result<()> { + let options = parse_args()?; + + let (mut tcp_handle, listen_port) = + listen_and_accept("localhost", &options.listen_port, Duration::new(10, 0)) .map_err(|err| format_err!("failed waiting for client: {}", err))?; let mut pty_buf = ByteBuffer::new(); @@ -293,14 +355,15 @@ fn do_main() -> Result<()> { let (username, ticket) = read_ticket_line(&mut tcp_handle, &mut pty_buf, Duration::new(10, 0)) .map_err(|err| format_err!("failed reading ticket: {}", err))?; - let port = if use_port_as_fd { Some(port) } else { None }; - authenticate(&username, &ticket, &path, perm.as_deref(), authport, port)?; + + authenticate(&username, &ticket, &options, listen_port)?; + tcp_handle.write_all(b"OK").expect("error writing response"); let mut poll = Poll::new()?; let mut events = Events::with_capacity(128); - let mut pty = run_pty(full_cmd)?; + let mut pty = run_pty(options.terminal_command.iter())?; poll.registry().register( &mut tcp_handle,