2 use std
::collections
::HashMap
;
3 use std
::ffi
::OsString
;
4 use std
::io
::{ErrorKind, Write}
;
5 use std
::os
::fd
::RawFd
;
6 use std
::os
::unix
::io
::{AsRawFd, FromRawFd}
;
7 use std
::os
::unix
::process
::CommandExt
;
8 use std
::process
::Command
;
9 use std
::time
::{Duration, Instant}
;
11 use anyhow
::{bail, format_err, Result}
;
12 use mio
::net
::{TcpListener, TcpStream}
;
13 use mio
::unix
::SourceFd
;
14 use mio
::{Events, Interest, Poll, Token}
;
16 use proxmox_io
::ByteBuffer
;
17 use proxmox_lang
::error
::io_err_other
;
18 use proxmox_sys
::linux
::pty
::{make_controlling_terminal, PTY}
;
20 const MSG_TYPE_DATA
: u8 = 0;
21 const MSG_TYPE_RESIZE
: u8 = 1;
22 //const MSG_TYPE_PING: u8 = 2;
24 fn remove_number(buf
: &mut ByteBuffer
) -> Option
<usize> {
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(len_str
) => match len_str
.parse() {
33 eprintln
!("error parsing number: '{err}'");
38 eprintln
!("error decoding number: '{err}'");
43 } else if buf
.len() > 20 {
52 fn process_queue(buf
: &mut ByteBuffer
, pty
: &mut PTY
) -> Option
<usize> {
62 let msgtype
= buf
[0] - b'
0'
;
64 if msgtype
== MSG_TYPE_DATA
{
66 if let Some(len
) = remove_number(buf
) {
69 } else if msgtype
== MSG_TYPE_RESIZE
{
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()?
;
76 // ignore incomplete messages
79 // ignore invalid or ping (msgtype 2)
86 type TicketResult
= Result
<(Box
<[u8]>, Box
<[u8]>)>;
88 /// Reads from the stream and returns the first line and the rest
90 stream
: &mut TcpStream
,
94 let mut poll
= Poll
::new()?
;
96 .register(stream
, Token(0), Interest
::READABLE
)?
;
97 let mut events
= Events
::with_capacity(1);
99 let now
= Instant
::now();
100 let mut elapsed
= Duration
::new(0, 0);
103 poll
.poll(&mut events
, Some(timeout
- elapsed
))?
;
104 if !events
.is_empty() {
105 match buf
.read_from(stream
) {
108 bail
!("connection closed before authentication");
111 Err(err
) if err
.kind() == ErrorKind
::WouldBlock
=> {}
112 Err(err
) => return Err(err
.into()),
115 if buf
[..].contains(&b'
\n'
) {
120 bail
!("authentication data is incomplete: {:?}", &buf
[..]);
124 elapsed
= now
.elapsed();
125 if elapsed
> timeout
{
130 let newline_idx
= &buf
[..].iter().position(|&x
| x
== b'
\n'
).unwrap();
132 let line
= buf
.remove_data(*newline_idx
);
133 buf
.consume(1); // discard newline
135 match line
.iter().position(|&b
| b
== b'
:'
) {
137 let (username
, ticket
) = line
.split_at(pos
);
138 Ok((username
.into(), ticket
[1..].into()))
140 None
=> bail
!("authentication data is invalid"),
144 fn authenticate(username
: &[u8], ticket
: &[u8], options
: &Options
, listen_port
: u16) -> Result
<()> {
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
)?
));
148 post_fields
.push(("path", &options
.acl_path
));
149 if let Some(perm
) = options
.acl_permission
.as_ref() {
150 post_fields
.push(("privs", perm
));
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.
156 if options
.listen_port
.is_fd() {
157 port_str
= listen_port
.to_string();
158 post_fields
.push(("port", &port_str
));
162 "http://localhost:{}/api2/json/access/ticket",
163 options
.api_daemon_port
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())
172 Err(err
) => bail
!("authentication request failed - {err}"),
176 fn listen_and_accept(
178 listen_port
: &PortOrFd
,
180 ) -> Result
<(TcpStream
, u16)> {
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))?
,
185 let port
= listener
.local_addr()?
.port();
186 let mut listener
= TcpListener
::from_std(listener
);
187 let mut poll
= Poll
::new()?
;
190 .register(&mut listener
, Token(0), Interest
::READABLE
)?
;
192 let mut events
= Events
::with_capacity(1);
194 let now
= Instant
::now();
195 let mut elapsed
= Duration
::new(0, 0);
198 poll
.poll(&mut events
, Some(timeout
- elapsed
))?
;
199 if !events
.is_empty() {
200 let (stream
, client
) = listener
.accept()?
;
201 println
!("client connection: {client:?}");
202 return Ok((stream
, port
));
205 elapsed
= now
.elapsed();
206 if elapsed
> timeout
{
212 fn 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
216 let (mut pty
, secondary_name
) = PTY
::new().map_err(io_err_other
)?
;
218 let mut filtered_env
: HashMap
<OsString
, OsString
> = std
::env
::vars_os()
219 .filter(|&(ref k
, _
)| {
225 || k
.to_string_lossy().starts_with("LC_")
228 filtered_env
.insert("TERM".into(), "xterm-256color".into());
230 let mut command
= Command
::new(cmd_exe
);
232 command
.args(params
).env_clear().envs(&filtered_env
);
235 command
.pre_exec(move || {
236 make_controlling_terminal(&secondary_name
).map_err(io_err_other
)?
;
243 pty
.set_size(80, 20)?
;
247 const TCP
: Token
= Token(0);
248 const PTY
: Token
= Token(1);
250 const CMD_HELP
: &str = "\
251 Usage: proxmox-termproxy [OPTIONS] --path <path> <listen-port> -- <terminal-cmd>...
254 <listen-port> Port or file descriptor to listen for TCP connections
255 <terminal-cmd>... The command to run connected via a proxied PTY
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
272 fn from_cli(value
: u64, use_as_fd
: bool
) -> Result
<PortOrFd
> {
274 if value
> RawFd
::MAX
as u64 {
275 bail
!("FD value too big");
277 Ok(Self::Fd(value
as RawFd
))
279 if value
> u16::MAX
as u64 {
280 bail
!("invalid port number");
282 Ok(Self::Port(value
as u16))
286 fn is_fd(&self) -> bool
{
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
304 /// The ACL permission that the ticket, read from the stream, is required to have on 'acl_path'
305 acl_permission
: Option
<String
>,
308 fn parse_args() -> Result
<Options
> {
309 let mut args
: Vec
<_
> = std
::env
::args_os().collect();
310 args
.remove(0); // remove the executable path.
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 `--`
321 // Now pass the remaining arguments through to `pico_args`.
322 let mut args
= pico_args
::Arguments
::from_vec(args
);
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");
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")?
,
339 if !args
.finish().is_empty() {
340 bail
!("unexpected extra arguments, use '-h' for usage");
346 fn do_main() -> Result
<()> {
347 let options
= parse_args()?
;
349 let (mut tcp_handle
, listen_port
) =
350 listen_and_accept("localhost", &options
.listen_port
, Duration
::new(10, 0))
351 .map_err(|err
| format_err
!("failed waiting for client: {err}"))?
;
353 let mut pty_buf
= ByteBuffer
::new();
354 let mut tcp_buf
= ByteBuffer
::new();
356 let (username
, ticket
) = read_ticket_line(&mut tcp_handle
, &mut pty_buf
, Duration
::new(10, 0))
357 .map_err(|err
| format_err
!("failed reading ticket: {err}"))?
;
359 authenticate(&username
, &ticket
, &options
, listen_port
)?
;
361 tcp_handle
.write_all(b
"OK").expect("error writing response");
363 let mut poll
= Poll
::new()?
;
364 let mut events
= Events
::with_capacity(128);
366 let mut pty
= run_pty(options
.terminal_command
.iter())?
;
368 poll
.registry().register(
371 Interest
::READABLE
| Interest
::WRITABLE
,
373 poll
.registry().register(
374 &mut SourceFd(&pty
.as_raw_fd()),
376 Interest
::READABLE
| Interest
::WRITABLE
,
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;
387 if tcp_readable
&& !pty_buf
.is_full() || pty_readable
&& !tcp_buf
.is_full() {
388 poll
.poll(&mut events
, Some(Duration
::new(0, 0)))?
;
390 poll
.poll(&mut events
, None
)?
;
393 for event
in &events
{
394 let writable
= event
.is_writable();
395 let readable
= event
.is_readable();
396 if event
.is_read_closed() {
399 match event
.token() {
420 while tcp_readable
&& !pty_buf
.is_full() {
421 let bytes
= match pty_buf
.read_from(&mut tcp_handle
) {
423 Err(err
) if err
.kind() == ErrorKind
::WouldBlock
=> {
424 tcp_readable
= false;
429 return Err(format_err
!("error reading from tcp: {err}"));
440 while pty_readable
&& !tcp_buf
.is_full() {
441 let bytes
= match tcp_buf
.read_from(&mut pty
) {
443 Err(err
) if err
.kind() == ErrorKind
::WouldBlock
=> {
444 pty_readable
= false;
449 return Err(format_err
!("error reading from pty: {err}"));
460 while !tcp_buf
.is_empty() && tcp_writable
{
461 let bytes
= match tcp_handle
.write(&tcp_buf
[..]) {
463 Err(err
) if err
.kind() == ErrorKind
::WouldBlock
=> {
464 tcp_writable
= false;
469 return Err(format_err
!("error writing to tcp : {err}"));
474 tcp_buf
.consume(bytes
);
477 while !pty_buf
.is_empty() && pty_writable
{
479 remaining
= match process_queue(&mut pty_buf
, &mut pty
) {
484 let len
= min(remaining
, pty_buf
.len());
485 let bytes
= match pty
.write(&pty_buf
[..len
]) {
487 Err(err
) if err
.kind() == ErrorKind
::WouldBlock
=> {
488 pty_writable
= false;
493 return Err(format_err
!("error writing to pty : {err}"));
499 pty_buf
.consume(bytes
);
507 std
::process
::exit(match do_main() {