]> git.proxmox.com Git - pve-xtermjs.git/blame - termproxy/src/main.rs
termproxy: switch from clap to pico-args for CLI argument handling
[pve-xtermjs.git] / termproxy / src / main.rs
CommitLineData
3e4311fe
DC
1use std::cmp::min;
2use std::collections::HashMap;
13e2e502 3use std::ffi::OsString;
cfa75e79 4use std::io::{ErrorKind, Write};
24d707d0 5use std::os::fd::RawFd;
3e4311fe
DC
6use std::os::unix::io::{AsRawFd, FromRawFd};
7use std::os::unix::process::CommandExt;
8use std::process::Command;
9use std::time::{Duration, Instant};
10
cfa75e79 11use anyhow::{bail, format_err, Result};
c2d63828 12use mio::net::{TcpListener, TcpStream};
c2d63828 13use mio::unix::SourceFd;
3c586bd8 14use mio::{Events, Interest, Poll, Token};
3e4311fe 15
cc1dbda3 16use proxmox_io::ByteBuffer;
27f74629 17use proxmox_lang::error::io_err_other;
8c6783e9 18use proxmox_sys::linux::pty::{make_controlling_terminal, PTY};
3e4311fe
DC
19
20const MSG_TYPE_DATA: u8 = 0;
21const MSG_TYPE_RESIZE: u8 = 1;
22//const MSG_TYPE_PING: u8 = 2;
23
24fn remove_number(buf: &mut ByteBuffer) -> Option<usize> {
25 loop {
26 if let Some(pos) = &buf.iter().position(|&x| x == b':') {
27 let data = buf.remove_data(*pos);
28 buf.consume(1); // the ':'
29 let len = match std::str::from_utf8(&data) {
30 Ok(lenstring) => match lenstring.parse() {
31 Ok(len) => len,
32 Err(err) => {
33 eprintln!("error parsing number: '{}'", err);
34 break;
35 }
36 },
37 Err(err) => {
38 eprintln!("error parsing number: '{}'", err);
39 break;
40 }
41 };
42 return Some(len);
43 } else if buf.len() > 20 {
44 buf.consume(20);
45 } else {
46 break;
47 }
48 }
49 None
50}
51
52fn process_queue(buf: &mut ByteBuffer, pty: &mut PTY) -> Option<usize> {
53 if buf.is_empty() {
54 return None;
55 }
56
57 loop {
58 if buf.len() < 2 {
59 break;
60 }
61
62 let msgtype = buf[0] - b'0';
63
64 if msgtype == MSG_TYPE_DATA {
65 buf.consume(2);
66 if let Some(len) = remove_number(buf) {
67 return Some(len);
68 }
69 } else if msgtype == MSG_TYPE_RESIZE {
70 buf.consume(2);
71 if let Some(cols) = remove_number(buf) {
72 if let Some(rows) = remove_number(buf) {
73 pty.set_size(cols as u16, rows as u16).ok()?;
74 }
75 }
76 // ignore incomplete messages
77 } else {
78 buf.consume(1);
79 // ignore invalid or ping (msgtype 2)
80 }
81 }
82
83 None
84}
85
86type TicketResult = Result<(Box<[u8]>, Box<[u8]>)>;
87
88/// Reads from the stream and returns the first line and the rest
89fn read_ticket_line(
90 stream: &mut TcpStream,
91 buf: &mut ByteBuffer,
92 timeout: Duration,
93) -> TicketResult {
7739b7e8 94 let mut poll = Poll::new()?;
3c586bd8
FG
95 poll.registry()
96 .register(stream, Token(0), Interest::READABLE)?;
7739b7e8 97 let mut events = Events::with_capacity(1);
41f32a28
FG
98
99 let now = Instant::now();
100 let mut elapsed = Duration::new(0, 0);
7739b7e8
DC
101
102 loop {
41f32a28 103 poll.poll(&mut events, Some(timeout - elapsed))?;
7739b7e8
DC
104 if !events.is_empty() {
105 match buf.read_from(stream) {
106 Ok(n) => {
107 if n == 0 {
cfa75e79 108 bail!("connection closed before authentication");
7739b7e8 109 }
3e4311fe 110 }
7739b7e8 111 Err(err) if err.kind() == ErrorKind::WouldBlock => {}
cfa75e79 112 Err(err) => return Err(err.into()),
7739b7e8
DC
113 }
114
115 if buf[..].contains(&b'\n') {
116 break;
117 }
118
119 if buf.is_full() {
cfa75e79 120 bail!("authentication data is incomplete: {:?}", &buf[..]);
3e4311fe 121 }
7739b7e8
DC
122 }
123
41f32a28
FG
124 elapsed = now.elapsed();
125 if elapsed > timeout {
cfa75e79 126 bail!("timed out");
3e4311fe
DC
127 }
128 }
129
3e4311fe
DC
130 let newline_idx = &buf[..].iter().position(|&x| x == b'\n').unwrap();
131
132 let line = buf.remove_data(*newline_idx);
133 buf.consume(1); // discard newline
134
135 match line.iter().position(|&b| b == b':') {
136 Some(pos) => {
137 let (username, ticket) = line.split_at(pos);
138 Ok((username.into(), ticket[1..].into()))
139 }
cfa75e79 140 None => bail!("authentication data is invalid"),
3e4311fe
DC
141 }
142}
143
24d707d0 144fn authenticate(username: &[u8], ticket: &[u8], options: &Options, listen_port: u16) -> Result<()> {
81a288b9
TL
145 let mut post_fields: Vec<(&str, &str)> = Vec::with_capacity(5);
146 post_fields.push(("username", std::str::from_utf8(username)?));
147 post_fields.push(("password", std::str::from_utf8(ticket)?));
24d707d0
TL
148 post_fields.push(("path", &options.acl_path));
149 if let Some(perm) = options.acl_permission.as_ref() {
81a288b9 150 post_fields.push(("privs", perm));
3e4311fe 151 }
24d707d0
TL
152
153 // if the listen-port was passed indirectly via an FD, it's encoded also in the ticket so that
154 // the access system can enforce that the users actually can access that port.
81a288b9 155 let port_str;
24d707d0
TL
156 if options.listen_port.is_fd() {
157 port_str = listen_port.to_string();
81a288b9 158 post_fields.push(("port", &port_str));
3e4311fe
DC
159 }
160
24d707d0
TL
161 let url = format!(
162 "http://localhost:{}/api2/json/access/ticket",
163 options.api_daemon_port
164 );
3e4311fe 165
81a288b9
TL
166 match ureq::post(&url).send_form(&post_fields[..]) {
167 Ok(res) if res.status() == 200 => Ok(()),
168 Ok(res) | Err(ureq::Error::Status(_, res)) => {
169 let code = res.status();
170 bail!("invalid authentication - {} {}", code, res.status_text())
171 }
172 Err(err) => bail!("authentication request failed - {}", err),
3e4311fe 173 }
3e4311fe
DC
174}
175
176fn listen_and_accept(
177 hostname: &str,
24d707d0 178 listen_port: &PortOrFd,
3e4311fe
DC
179 timeout: Duration,
180) -> Result<(TcpStream, u16)> {
24d707d0
TL
181 let listener = match listen_port {
182 PortOrFd::Fd(fd) => unsafe { std::net::TcpListener::from_raw_fd(*fd) },
183 PortOrFd::Port(port) => std::net::TcpListener::bind((hostname, *port as u16))?,
3e4311fe
DC
184 };
185 let port = listener.local_addr()?.port();
c2d63828
DC
186 let mut listener = TcpListener::from_std(listener);
187 let mut poll = Poll::new()?;
3e4311fe 188
3c586bd8
FG
189 poll.registry()
190 .register(&mut listener, Token(0), Interest::READABLE)?;
3e4311fe
DC
191
192 let mut events = Events::with_capacity(1);
193
41f32a28
FG
194 let now = Instant::now();
195 let mut elapsed = Duration::new(0, 0);
196
3e4311fe 197 loop {
41f32a28 198 poll.poll(&mut events, Some(timeout - elapsed))?;
3e4311fe 199 if !events.is_empty() {
c2d63828 200 let (stream, client) = listener.accept()?;
24d707d0 201 println!("client connection: {client:?}");
3e4311fe
DC
202 return Ok((stream, port));
203 }
41f32a28
FG
204
205 elapsed = now.elapsed();
206 if elapsed > timeout {
cfa75e79 207 bail!("timed out");
3e4311fe
DC
208 }
209 }
210}
211
13e2e502
TL
212fn run_pty<'a>(mut full_cmd: impl Iterator<Item = &'a OsString>) -> Result<PTY> {
213 let cmd_exe = full_cmd.next().unwrap();
214 let params = full_cmd; // rest
215
3e4311fe
DC
216 let (mut pty, secondary_name) = PTY::new().map_err(io_err_other)?;
217
218 let mut filtered_env: HashMap<OsString, OsString> = std::env::vars_os()
219 .filter(|&(ref k, _)| {
220 k == "PATH"
221 || k == "USER"
222 || k == "HOME"
223 || k == "LANG"
224 || k == "LANGUAGE"
225 || k.to_string_lossy().starts_with("LC_")
226 })
227 .collect();
228 filtered_env.insert("TERM".into(), "xterm-256color".into());
229
13e2e502 230 let mut command = Command::new(cmd_exe);
3e4311fe
DC
231
232 command.args(params).env_clear().envs(&filtered_env);
233
234 unsafe {
235 command.pre_exec(move || {
236 make_controlling_terminal(&secondary_name).map_err(io_err_other)?;
237 Ok(())
238 });
239 }
240
241 command.spawn()?;
242
27f74629 243 pty.set_size(80, 20)?;
3e4311fe
DC
244 Ok(pty)
245}
246
247const TCP: Token = Token(0);
248const PTY: Token = Token(1);
249
24d707d0
TL
250const CMD_HELP: &str = "\
251Usage: proxmox-termproxy [OPTIONS] --path <path> <listen-port> -- <terminal-cmd>...
252
253Arguments:
254 <listen-port> Port or file descriptor to listen for TCP connections
255 <terminal-cmd>... The command to run connected via a proxied PTY
256
257Options:
258 --authport <authport> Port to relay auth-request, default 85
259 --port-as-fd Use <listen-port> as file descriptor.
260 --path <path> ACL object path to test <perm> on.
261 --perm <perm> Permission to test.
262 -h, --help Print help
263";
264
265#[derive(Debug)]
266enum PortOrFd {
267 Port(u16),
268 Fd(RawFd),
269}
270
271impl PortOrFd {
272 fn from_cli(value: u64, use_as_fd: bool) -> Result<PortOrFd> {
273 if use_as_fd {
274 if value > RawFd::MAX as u64 {
275 bail!("FD value too big");
276 }
277 Ok(Self::Fd(value as RawFd))
278 } else {
279 if value > u16::MAX as u64 {
280 bail!("invalid port number");
281 }
282 Ok(Self::Port(value as u16))
283 }
284 }
285
286 fn is_fd(&self) -> bool {
287 match self {
288 Self::Fd(_) => true,
289 _ => false,
290 }
291 }
292}
293
294#[derive(Debug)]
295struct Options {
296 /// The actual command to run proxied in a pseudo terminal.
297 terminal_command: Vec<OsString>,
298 /// The port or FD that termproxy will listen on for an incoming conection
299 listen_port: PortOrFd,
300 /// The port of the local privileged daemon that authentication is relayed to. Defaults to `85`
301 api_daemon_port: u16,
302 /// The ACL object path the 'acl_permission' is checked on
303 acl_path: String,
304 /// The ACL permission that the ticket, read from the stream, is required to have on 'acl_path'
305 acl_permission: Option<String>,
306}
307
308fn parse_args() -> Result<Options> {
309 let mut args: Vec<_> = std::env::args_os().collect();
310 args.remove(0); // remove the executable path.
311
312 // handle finding command after `--` first so that we only parse our options later
313 let terminal_command = if let Some(dash_dash) = args.iter().position(|arg| arg == "--") {
314 let later_args = args.drain(dash_dash + 1..).collect();
315 args.pop(); // .. then remove the `--`
316 Some(later_args)
317 } else {
318 None
319 };
320
321 // Now pass the remaining arguments through to `pico_args`.
322 let mut args = pico_args::Arguments::from_vec(args);
323
324 if args.contains(["-h", "--help"]) {
325 print!("{CMD_HELP}");
326 std::process::exit(0);
327 } else if terminal_command.is_none() {
328 bail!("missing terminal command or -- option-end marker, see '-h' for usage");
329 }
330
331 let options = Options {
332 terminal_command: terminal_command.unwrap(), // checked above
333 listen_port: PortOrFd::from_cli(args.free_from_str()?, args.contains("--port-as-fd"))?,
334 api_daemon_port: args.opt_value_from_str("--authport")?.unwrap_or(85),
335 acl_path: args.value_from_str("--path")?,
336 acl_permission: args.opt_value_from_str("--perm")?,
337 };
338
339 if !args.finish().is_empty() {
340 bail!("unexpected extra arguments, use '-h' for usage");
3e4311fe
DC
341 }
342
24d707d0
TL
343 Ok(options)
344}
345
346fn do_main() -> Result<()> {
347 let options = parse_args()?;
348
349 let (mut tcp_handle, listen_port) =
350 listen_and_accept("localhost", &options.listen_port, Duration::new(10, 0))
cfa75e79 351 .map_err(|err| format_err!("failed waiting for client: {}", err))?;
3e4311fe 352
13e2e502
TL
353 let mut pty_buf = ByteBuffer::new();
354 let mut tcp_buf = ByteBuffer::new();
355
c2d63828 356 let (username, ticket) = read_ticket_line(&mut tcp_handle, &mut pty_buf, Duration::new(10, 0))
cfa75e79 357 .map_err(|err| format_err!("failed reading ticket: {}", err))?;
24d707d0
TL
358
359 authenticate(&username, &ticket, &options, listen_port)?;
360
c2d63828 361 tcp_handle.write_all(b"OK").expect("error writing response");
3e4311fe 362
c2d63828 363 let mut poll = Poll::new()?;
3e4311fe
DC
364 let mut events = Events::with_capacity(128);
365
24d707d0 366 let mut pty = run_pty(options.terminal_command.iter())?;
3e4311fe 367
c2d63828
DC
368 poll.registry().register(
369 &mut tcp_handle,
3e4311fe 370 TCP,
bb32b0c6 371 Interest::READABLE | Interest::WRITABLE,
3e4311fe 372 )?;
c2d63828
DC
373 poll.registry().register(
374 &mut SourceFd(&pty.as_raw_fd()),
3e4311fe 375 PTY,
bb32b0c6 376 Interest::READABLE | Interest::WRITABLE,
3e4311fe
DC
377 )?;
378
379 let mut tcp_writable = true;
380 let mut pty_writable = true;
381 let mut tcp_readable = true;
382 let mut pty_readable = true;
383 let mut remaining = 0;
384 let mut finished = false;
385
386 while !finished {
387 if tcp_readable && !pty_buf.is_full() || pty_readable && !tcp_buf.is_full() {
388 poll.poll(&mut events, Some(Duration::new(0, 0)))?;
389 } else {
390 poll.poll(&mut events, None)?;
391 }
392
393 for event in &events {
c2d63828
DC
394 let writable = event.is_writable();
395 let readable = event.is_readable();
396 if event.is_read_closed() {
3e4311fe
DC
397 finished = true;
398 }
399 match event.token() {
400 TCP => {
401 if readable {
402 tcp_readable = true;
403 }
404 if writable {
405 tcp_writable = true;
406 }
407 }
408 PTY => {
409 if readable {
410 pty_readable = true;
411 }
412 if writable {
413 pty_writable = true;
414 }
415 }
416 _ => unreachable!(),
417 }
418 }
419
420 while tcp_readable && !pty_buf.is_full() {
421 let bytes = match pty_buf.read_from(&mut tcp_handle) {
422 Ok(bytes) => bytes,
423 Err(err) if err.kind() == ErrorKind::WouldBlock => {
424 tcp_readable = false;
425 break;
426 }
427 Err(err) => {
428 if !finished {
cfa75e79 429 return Err(format_err!("error reading from tcp: {}", err));
3e4311fe
DC
430 }
431 break;
432 }
433 };
434 if bytes == 0 {
435 finished = true;
436 break;
437 }
438 }
439
440 while pty_readable && !tcp_buf.is_full() {
441 let bytes = match tcp_buf.read_from(&mut pty) {
442 Ok(bytes) => bytes,
443 Err(err) if err.kind() == ErrorKind::WouldBlock => {
444 pty_readable = false;
445 break;
446 }
447 Err(err) => {
448 if !finished {
cfa75e79 449 return Err(format_err!("error reading from pty: {}", err));
3e4311fe
DC
450 }
451 break;
452 }
453 };
454 if bytes == 0 {
455 finished = true;
456 break;
457 }
458 }
459
460 while !tcp_buf.is_empty() && tcp_writable {
461 let bytes = match tcp_handle.write(&tcp_buf[..]) {
462 Ok(bytes) => bytes,
463 Err(err) if err.kind() == ErrorKind::WouldBlock => {
464 tcp_writable = false;
465 break;
466 }
467 Err(err) => {
468 if !finished {
cfa75e79 469 return Err(format_err!("error writing to tcp : {}", err));
3e4311fe
DC
470 }
471 break;
472 }
473 };
474 tcp_buf.consume(bytes);
475 }
476
477 while !pty_buf.is_empty() && pty_writable {
478 if remaining == 0 {
479 remaining = match process_queue(&mut pty_buf, &mut pty) {
480 Some(val) => val,
481 None => break,
482 };
483 }
484 let len = min(remaining, pty_buf.len());
485 let bytes = match pty.write(&pty_buf[..len]) {
486 Ok(bytes) => bytes,
487 Err(err) if err.kind() == ErrorKind::WouldBlock => {
488 pty_writable = false;
489 break;
490 }
491 Err(err) => {
492 if !finished {
cfa75e79 493 return Err(format_err!("error writing to pty : {}", err));
3e4311fe
DC
494 }
495 break;
496 }
497 };
498 remaining -= bytes;
499 pty_buf.consume(bytes);
500 }
501 }
502
503 Ok(())
504}
505
506fn main() {
507 std::process::exit(match do_main() {
508 Ok(_) => 0,
509 Err(err) => {
510 eprintln!("{}", err);
511 1
512 }
513 });
514}