]> git.proxmox.com Git - pve-xtermjs.git/blame - src/main.rs
style: use Interest | Interest instead of .add
[pve-xtermjs.git] / src / main.rs
CommitLineData
3e4311fe
DC
1use std::cmp::min;
2use std::collections::HashMap;
3use std::ffi::{OsStr, OsString};
9c147346 4use std::io::{ErrorKind, Result, Write};
3e4311fe
DC
5use std::os::unix::io::{AsRawFd, FromRawFd};
6use std::os::unix::process::CommandExt;
7use std::process::Command;
8use std::time::{Duration, Instant};
9
10use clap::{App, AppSettings, Arg};
11use curl::easy::Easy;
c2d63828
DC
12use mio::net::{TcpListener, TcpStream};
13use mio::{Events, Poll, Token, Interest};
14use mio::unix::SourceFd;
3e4311fe
DC
15
16use proxmox::sys::error::io_err_other;
17use proxmox::sys::linux::pty::{make_controlling_terminal, PTY};
18use proxmox::tools::byte_buffer::ByteBuffer;
19use proxmox::{io_bail, io_format_err};
20
21const MSG_TYPE_DATA: u8 = 0;
22const MSG_TYPE_RESIZE: u8 = 1;
23//const MSG_TYPE_PING: u8 = 2;
24
25fn remove_number(buf: &mut ByteBuffer) -> Option<usize> {
26 loop {
27 if let Some(pos) = &buf.iter().position(|&x| x == b':') {
28 let data = buf.remove_data(*pos);
29 buf.consume(1); // the ':'
30 let len = match std::str::from_utf8(&data) {
31 Ok(lenstring) => match lenstring.parse() {
32 Ok(len) => len,
33 Err(err) => {
34 eprintln!("error parsing number: '{}'", err);
35 break;
36 }
37 },
38 Err(err) => {
39 eprintln!("error parsing number: '{}'", err);
40 break;
41 }
42 };
43 return Some(len);
44 } else if buf.len() > 20 {
45 buf.consume(20);
46 } else {
47 break;
48 }
49 }
50 None
51}
52
53fn process_queue(buf: &mut ByteBuffer, pty: &mut PTY) -> Option<usize> {
54 if buf.is_empty() {
55 return None;
56 }
57
58 loop {
59 if buf.len() < 2 {
60 break;
61 }
62
63 let msgtype = buf[0] - b'0';
64
65 if msgtype == MSG_TYPE_DATA {
66 buf.consume(2);
67 if let Some(len) = remove_number(buf) {
68 return Some(len);
69 }
70 } else if msgtype == MSG_TYPE_RESIZE {
71 buf.consume(2);
72 if let Some(cols) = remove_number(buf) {
73 if let Some(rows) = remove_number(buf) {
74 pty.set_size(cols as u16, rows as u16).ok()?;
75 }
76 }
77 // ignore incomplete messages
78 } else {
79 buf.consume(1);
80 // ignore invalid or ping (msgtype 2)
81 }
82 }
83
84 None
85}
86
87type TicketResult = Result<(Box<[u8]>, Box<[u8]>)>;
88
89/// Reads from the stream and returns the first line and the rest
90fn read_ticket_line(
91 stream: &mut TcpStream,
92 buf: &mut ByteBuffer,
93 timeout: Duration,
94) -> TicketResult {
7739b7e8
DC
95
96 let mut poll = Poll::new()?;
97 poll.registry().register(stream, Token(0), Interest::READABLE)?;
98 let mut events = Events::with_capacity(1);
41f32a28
FG
99
100 let now = Instant::now();
101 let mut elapsed = Duration::new(0, 0);
7739b7e8
DC
102
103 loop {
41f32a28 104 poll.poll(&mut events, Some(timeout - elapsed))?;
7739b7e8
DC
105 if !events.is_empty() {
106 match buf.read_from(stream) {
107 Ok(n) => {
108 if n == 0 {
109 io_bail!("connection closed before authentication");
110 }
3e4311fe 111 }
7739b7e8
DC
112 Err(err) if err.kind() == ErrorKind::WouldBlock => {}
113 Err(err) => return Err(err),
114 }
115
116 if buf[..].contains(&b'\n') {
117 break;
118 }
119
120 if buf.is_full() {
121 io_bail!("authentication data is incomplete: {:?}", &buf[..]);
3e4311fe 122 }
7739b7e8
DC
123 }
124
41f32a28
FG
125 elapsed = now.elapsed();
126 if elapsed > timeout {
7739b7e8 127 io_bail!("timed out");
3e4311fe
DC
128 }
129 }
130
3e4311fe
DC
131 let newline_idx = &buf[..].iter().position(|&x| x == b'\n').unwrap();
132
133 let line = buf.remove_data(*newline_idx);
134 buf.consume(1); // discard newline
135
136 match line.iter().position(|&b| b == b':') {
137 Some(pos) => {
138 let (username, ticket) = line.split_at(pos);
139 Ok((username.into(), ticket[1..].into()))
140 }
141 None => io_bail!("authentication data is invalid"),
142 }
143}
144
145fn authenticate(
146 username: &[u8],
147 ticket: &[u8],
148 path: &str,
149 perm: Option<&str>,
150 authport: u16,
151 port: Option<u16>,
152) -> Result<()> {
153 let mut curl = Easy::new();
154 curl.url(&format!(
155 "http://localhost:{}/api2/json/access/ticket",
156 authport
157 ))?;
158
159 let username = curl.url_encode(username);
160 let ticket = curl.url_encode(ticket);
161 let path = curl.url_encode(path.as_bytes());
162
163 let mut post_fields = Vec::with_capacity(5);
164 post_fields.push(format!("username={}", username));
165 post_fields.push(format!("password={}", ticket));
166 post_fields.push(format!("path={}", path));
167
168 if let Some(perm) = perm {
169 let perm = curl.url_encode(perm.as_bytes());
170 post_fields.push(format!("privs={}", perm));
171 }
172
173 if let Some(port) = port {
174 post_fields.push(format!("port={}", port));
175 }
176
177 curl.post_fields_copy(post_fields.join("&").as_bytes())?;
178 curl.post(true)?;
179 curl.perform()?;
180
181 let response_code = curl.response_code()?;
182
183 if response_code != 200 {
184 io_bail!("invalid authentication, code {}", response_code);
185 }
186
187 Ok(())
188}
189
190fn listen_and_accept(
191 hostname: &str,
192 port: u64,
193 port_as_fd: bool,
194 timeout: Duration,
195) -> Result<(TcpStream, u16)> {
196 let listener = if port_as_fd {
197 unsafe { std::net::TcpListener::from_raw_fd(port as i32) }
198 } else {
199 std::net::TcpListener::bind((hostname, port as u16))?
200 };
201 let port = listener.local_addr()?.port();
c2d63828
DC
202 let mut listener = TcpListener::from_std(listener);
203 let mut poll = Poll::new()?;
3e4311fe 204
c2d63828 205 poll.registry().register(&mut listener, Token(0), Interest::READABLE)?;
3e4311fe
DC
206
207 let mut events = Events::with_capacity(1);
208
41f32a28
FG
209 let now = Instant::now();
210 let mut elapsed = Duration::new(0, 0);
211
3e4311fe 212 loop {
41f32a28 213 poll.poll(&mut events, Some(timeout - elapsed))?;
3e4311fe 214 if !events.is_empty() {
c2d63828 215 let (stream, client) = listener.accept()?;
3e4311fe
DC
216 println!("client connection: {:?}", client);
217 return Ok((stream, port));
218 }
41f32a28
FG
219
220 elapsed = now.elapsed();
221 if elapsed > timeout {
3e4311fe
DC
222 io_bail!("timed out");
223 }
224 }
225}
226
227fn run_pty(cmd: &OsStr, params: clap::OsValues) -> Result<PTY> {
228 let (mut pty, secondary_name) = PTY::new().map_err(io_err_other)?;
229
230 let mut filtered_env: HashMap<OsString, OsString> = std::env::vars_os()
231 .filter(|&(ref k, _)| {
232 k == "PATH"
233 || k == "USER"
234 || k == "HOME"
235 || k == "LANG"
236 || k == "LANGUAGE"
237 || k.to_string_lossy().starts_with("LC_")
238 })
239 .collect();
240 filtered_env.insert("TERM".into(), "xterm-256color".into());
241
242 let mut command = Command::new(cmd);
243
244 command.args(params).env_clear().envs(&filtered_env);
245
246 unsafe {
247 command.pre_exec(move || {
248 make_controlling_terminal(&secondary_name).map_err(io_err_other)?;
249 Ok(())
250 });
251 }
252
253 command.spawn()?;
254
255 pty.set_size(80, 20).map_err(|x| x.as_errno().unwrap())?;
256 Ok(pty)
257}
258
259const TCP: Token = Token(0);
260const PTY: Token = Token(1);
261
262fn do_main() -> Result<()> {
263 let matches = App::new("termproxy")
264 .setting(AppSettings::TrailingVarArg)
265 .arg(Arg::with_name("port").takes_value(true).required(true))
266 .arg(
267 Arg::with_name("authport")
268 .takes_value(true)
269 .long("authport"),
270 )
271 .arg(Arg::with_name("use-port-as-fd").long("port-as-fd"))
272 .arg(
273 Arg::with_name("path")
274 .takes_value(true)
275 .long("path")
276 .required(true),
277 )
278 .arg(Arg::with_name("perm").takes_value(true).long("perm"))
279 .arg(Arg::with_name("cmd").multiple(true).required(true))
280 .get_matches();
281
282 let port: u64 = matches
283 .value_of("port")
284 .unwrap()
285 .parse()
286 .map_err(io_err_other)?;
287 let path = matches.value_of("path").unwrap();
288 let perm: Option<&str> = matches.value_of("perm");
289 let mut cmdparams = matches.values_of_os("cmd").unwrap();
290 let cmd = cmdparams.next().unwrap();
291 let authport: u16 = matches
292 .value_of("authport")
293 .unwrap_or("85")
294 .parse()
295 .map_err(io_err_other)?;
296 let mut pty_buf = ByteBuffer::new();
297 let mut tcp_buf = ByteBuffer::new();
298
299 let use_port_as_fd = matches.is_present("use-port-as-fd");
300
301 if use_port_as_fd && port > u16::MAX as u64 {
302 return Err(io_format_err!("port too big"));
303 } else if port > i32::MAX as u64 {
304 return Err(io_format_err!("Invalid FD number"));
305 }
306
c2d63828 307 let (mut tcp_handle, port) =
3e4311fe
DC
308 listen_and_accept("localhost", port, use_port_as_fd, Duration::new(10, 0))
309 .map_err(|err| io_format_err!("failed waiting for client: {}", err))?;
310
c2d63828 311 let (username, ticket) = read_ticket_line(&mut tcp_handle, &mut pty_buf, Duration::new(10, 0))
3e4311fe
DC
312 .map_err(|err| io_format_err!("failed reading ticket: {}", err))?;
313 let port = if use_port_as_fd { Some(port) } else { None };
314 authenticate(&username, &ticket, path, perm, authport, port)?;
c2d63828 315 tcp_handle.write_all(b"OK").expect("error writing response");
3e4311fe 316
c2d63828 317 let mut poll = Poll::new()?;
3e4311fe
DC
318 let mut events = Events::with_capacity(128);
319
320 let mut pty = run_pty(cmd, cmdparams)?;
321
c2d63828
DC
322 poll.registry().register(
323 &mut tcp_handle,
3e4311fe 324 TCP,
bb32b0c6 325 Interest::READABLE | Interest::WRITABLE,
3e4311fe 326 )?;
c2d63828
DC
327 poll.registry().register(
328 &mut SourceFd(&pty.as_raw_fd()),
3e4311fe 329 PTY,
bb32b0c6 330 Interest::READABLE | Interest::WRITABLE,
3e4311fe
DC
331 )?;
332
333 let mut tcp_writable = true;
334 let mut pty_writable = true;
335 let mut tcp_readable = true;
336 let mut pty_readable = true;
337 let mut remaining = 0;
338 let mut finished = false;
339
340 while !finished {
341 if tcp_readable && !pty_buf.is_full() || pty_readable && !tcp_buf.is_full() {
342 poll.poll(&mut events, Some(Duration::new(0, 0)))?;
343 } else {
344 poll.poll(&mut events, None)?;
345 }
346
347 for event in &events {
c2d63828
DC
348 let writable = event.is_writable();
349 let readable = event.is_readable();
350 if event.is_read_closed() {
3e4311fe
DC
351 finished = true;
352 }
353 match event.token() {
354 TCP => {
355 if readable {
356 tcp_readable = true;
357 }
358 if writable {
359 tcp_writable = true;
360 }
361 }
362 PTY => {
363 if readable {
364 pty_readable = true;
365 }
366 if writable {
367 pty_writable = true;
368 }
369 }
370 _ => unreachable!(),
371 }
372 }
373
374 while tcp_readable && !pty_buf.is_full() {
375 let bytes = match pty_buf.read_from(&mut tcp_handle) {
376 Ok(bytes) => bytes,
377 Err(err) if err.kind() == ErrorKind::WouldBlock => {
378 tcp_readable = false;
379 break;
380 }
381 Err(err) => {
382 if !finished {
383 return Err(io_format_err!("error reading from tcp: {}", err));
384 }
385 break;
386 }
387 };
388 if bytes == 0 {
389 finished = true;
390 break;
391 }
392 }
393
394 while pty_readable && !tcp_buf.is_full() {
395 let bytes = match tcp_buf.read_from(&mut pty) {
396 Ok(bytes) => bytes,
397 Err(err) if err.kind() == ErrorKind::WouldBlock => {
398 pty_readable = false;
399 break;
400 }
401 Err(err) => {
402 if !finished {
403 return Err(io_format_err!("error reading from pty: {}", err));
404 }
405 break;
406 }
407 };
408 if bytes == 0 {
409 finished = true;
410 break;
411 }
412 }
413
414 while !tcp_buf.is_empty() && tcp_writable {
415 let bytes = match tcp_handle.write(&tcp_buf[..]) {
416 Ok(bytes) => bytes,
417 Err(err) if err.kind() == ErrorKind::WouldBlock => {
418 tcp_writable = false;
419 break;
420 }
421 Err(err) => {
422 if !finished {
423 return Err(io_format_err!("error writing to tcp : {}", err));
424 }
425 break;
426 }
427 };
428 tcp_buf.consume(bytes);
429 }
430
431 while !pty_buf.is_empty() && pty_writable {
432 if remaining == 0 {
433 remaining = match process_queue(&mut pty_buf, &mut pty) {
434 Some(val) => val,
435 None => break,
436 };
437 }
438 let len = min(remaining, pty_buf.len());
439 let bytes = match pty.write(&pty_buf[..len]) {
440 Ok(bytes) => bytes,
441 Err(err) if err.kind() == ErrorKind::WouldBlock => {
442 pty_writable = false;
443 break;
444 }
445 Err(err) => {
446 if !finished {
447 return Err(io_format_err!("error writing to pty : {}", err));
448 }
449 break;
450 }
451 };
452 remaining -= bytes;
453 pty_buf.consume(bytes);
454 }
455 }
456
457 Ok(())
458}
459
460fn main() {
461 std::process::exit(match do_main() {
462 Ok(_) => 0,
463 Err(err) => {
464 eprintln!("{}", err);
465 1
466 }
467 });
468}