From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id CCE7E64CF8 for ; Tue, 21 Jul 2020 11:01:24 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id C41A6163C0 for ; Tue, 21 Jul 2020 11:00:54 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [212.186.127.180]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS id 53F7F16399 for ; Tue, 21 Jul 2020 11:00:51 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 1ECFA40777 for ; Tue, 21 Jul 2020 11:00:51 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Tue, 21 Jul 2020 11:00:45 +0200 Message-Id: <20200721090048.28632-2-d.csapak@proxmox.com> X-Mailer: git-send-email 2.20.1 In-Reply-To: <20200721090048.28632-1-d.csapak@proxmox.com> References: <20200721090048.28632-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -1.009 Adjusted score from AWL reputation of From: address KAM_BADIPHTTP 2 Due to the Storm Bot Network, IPs in emails is bad KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods KAM_SHORT 0.001 Use of a URL Shortener for very short URL NO_DNS_FOR_FROM 0.379 Envelope sender has no MX or A DNS records NUMERIC_HTTP_ADDR 0.001 Uses a numeric IP address in URL RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record URIBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to URIBL was blocked. See http://wiki.apache.org/spamassassin/DnsBlocklists#dnsbl-block for more information. [architecture.mk, main.rs, tpl.in, termproxy.pm, pkg-info.mk, js.map, proxmox.com] WEIRD_PORT 0.001 Uses non-standard port number for HTTP Subject: [pbs-devel] [PATCH xtermjs v3 1/4] termproxy: rewrite in rust X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Tue, 21 Jul 2020 09:01:24 -0000 termproxy is now completely written in rust (instead of perl) but it is a drop-in replacement this contains all other necessary changes to the build-system for it to successfully build Signed-off-by: Dominik Csapak --- .cargo/config | 5 + Cargo.toml | 14 + Makefile | 52 +++- debian/compat | 1 - debian/control | 16 -- debian/debcargo.toml | 14 + debian/install | 1 + debian/rules | 7 +- debian/source/format | 1 - debian/source/lintian-overrides | 4 +- src/Makefile | 7 - src/PVE/CLI/Makefile | 8 - src/PVE/CLI/termproxy.pm | 250 ----------------- src/PVE/Makefile | 3 - src/bin/Makefile | 7 - src/bin/termproxy | 8 - src/main.rs | 456 ++++++++++++++++++++++++++++++++ src/www/Makefile | 21 -- 18 files changed, 536 insertions(+), 339 deletions(-) create mode 100644 .cargo/config create mode 100644 Cargo.toml delete mode 100644 debian/compat delete mode 100644 debian/control create mode 100644 debian/debcargo.toml create mode 100644 debian/install delete mode 100644 debian/source/format delete mode 100644 src/Makefile delete mode 100644 src/PVE/CLI/Makefile delete mode 100644 src/PVE/CLI/termproxy.pm delete mode 100644 src/PVE/Makefile delete mode 100644 src/bin/Makefile delete mode 100755 src/bin/termproxy create mode 100644 src/main.rs delete mode 100644 src/www/Makefile diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..3b5b6e4 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,5 @@ +[source] +[source.debian-packages] +directory = "/usr/share/cargo/registry" +[source.crates-io] +replace-with = "debian-packages" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..55869ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "termproxy" +version = "4.3.0" +authors = ["Dominik Csapak "] +edition = "2018" +license = "AGPL-3" + +exclude = [ "build", "debian" ] + +[dependencies] +mio = "0.6" +curl = "0.4" +clap = "2.33" +proxmox = { version = "0.2.0", default-features = false } diff --git a/Makefile b/Makefile index d4aeee4..7a73fe7 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,8 @@ include /usr/share/dpkg/pkg-info.mk +include /usr/share/dpkg/architecture.mk PACKAGE=pve-xtermjs +CRATENAME=termproxy export VERSION=${DEB_VERSION_UPSTREAM_REVISION} @@ -11,31 +13,53 @@ FITADDONVER=0.4.0 FITADDONTGZ=xterm-addon-fit-${FITADDONVER}.tgz SRCDIR=src -BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM} GITVERSION:=$(shell git rev-parse HEAD) -DEB=${PACKAGE}_${VERSION}_all.deb -DSC=${PACKAGE}_${VERSION}.dsc +DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_${DEB_BUILD_ARCH}.deb +DSC=rust-${CRATENAME}_${DEB_VERSION_UPSTREAM_REVISION}.dsc -all: ${DEB} - @echo ${DEB} +ifeq ($(BUILD_MODE), release) +CARGO_BUILD_ARGS += --release +COMPILEDIR := target/release +else +COMPILEDIR := target/debug +endif + +all: cargo-build $(SRCIDR) + +.PHONY: $(SUBDIRS) +$(SUBDIRS): + make -C $@ + +.PHONY: cargo-build +cargo-build: + cargo build $(CARGO_BUILD_ARGS) -${BUILDDIR}: ${SRCDIR} debian - rm -rf ${BUILDDIR} - rsync -a ${SRCDIR}/ debian ${BUILDDIR} - echo "git clone git://git.proxmox.com/git/pve-xtermjs.git\\ngit checkout ${GITVERSION}" > ${BUILDDIR}/debian/SOURCE +.PHONY: build +build: + rm -rf build + debcargo package \ + --config debian/debcargo.toml \ + --changelog-ready \ + --no-overlay-write-back \ + --directory build \ + $(CRATENAME) \ + $(shell dpkg-parsechangelog -l debian/changelog -SVersion | sed -e 's/-.*//') + rm build/Cargo.lock + find build/debian -name "*.hint" -delete + echo "git clone git://git.proxmox.com/git/pve-xtermjs.git\\ngit checkout ${GITVERSION}" > build/debian/SOURCE .PHONY: deb deb: ${DEB} -${DEB}: ${BUILDDIR} - cd ${BUILDDIR}; dpkg-buildpackage -b -uc -us +$(DEB): build + cd build; dpkg-buildpackage -b -uc -us --no-pre-clean lintian ${DEB} @echo ${DEB} .PHONY: dsc dsc: ${DSC} -${DSC}: ${BUILDDIR} - cd ${BUILDDIR}; dpkg-buildpackage -S -us -uc -d +$(DSC): build + cd build; dpkg-buildpackage -S -us -uc -d -nc lintian ${DSC} X_EXCLUSIONS=--exclude=addons/attach --exclude=addons/fullscreen --exclude=addons/search \ @@ -59,7 +83,7 @@ distclean: clean .PHONY: clean clean: - rm -rf *~ debian/*~ ${PACKAGE}-*/ *.deb *.changes *.dsc *.tar.gz *.buildinfo + rm -rf *~ debian/*~ ${PACKAGE}-*/ build/ *.deb *.changes *.dsc *.tar.?z *.buildinfo .PHONY: dinstall dinstall: deb diff --git a/debian/compat b/debian/compat deleted file mode 100644 index f599e28..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -10 diff --git a/debian/control b/debian/control deleted file mode 100644 index 216b672..0000000 --- a/debian/control +++ /dev/null @@ -1,16 +0,0 @@ -Source: pve-xtermjs -Section: web -Priority: optional -Maintainer: Proxmox Support Team -Build-Depends: debhelper (>= 10~), - libpve-common-perl, -Standards-Version: 4.1.3 - -Package: pve-xtermjs -Architecture: all -Depends: libpve-common-perl (>= 5.0-23), - libwww-perl, - ${misc:Depends}, - ${perl:Depends} -Description: HTML/JS Shell client - This is an xterm.js client for PVE Host, Container and Qemu Serial Terminal diff --git a/debian/debcargo.toml b/debian/debcargo.toml new file mode 100644 index 0000000..cf78dba --- /dev/null +++ b/debian/debcargo.toml @@ -0,0 +1,14 @@ +overlay = "." +crate_src_path = ".." +bin_name = "pve-xtermjs" + +[source] +maintainer = "Proxmox Support Team " +section = "admin" +homepage = "http://www.proxmox.com" +vcs_git = "git://git.proxmox.com/git/pve-xtermjs.git" +vcs_browser = "https://git.proxmox.com/?p=pve-xtermjs.git;a=summary" + +[package] +summary = "HTML/JS Shell client" +description = "This is an xterm.js client/proxy for Proxmox Hosts, PVE containers or QEMU Serial Terminals" diff --git a/debian/install b/debian/install new file mode 100644 index 0000000..04be689 --- /dev/null +++ b/debian/install @@ -0,0 +1 @@ +src/www/* /usr/share/pve-xtermjs/ diff --git a/debian/rules b/debian/rules index 2d33f6a..6263218 100755 --- a/debian/rules +++ b/debian/rules @@ -1,4 +1,9 @@ #!/usr/bin/make -f %: - dh $@ + dh $@ --buildsystem cargo + +override_dh_auto_build: + dh_auto_build + sed -e 's/@VERSION@/${VERSION}/' src/www/index.html.tpl.in > src/www/index.html.tpl + rm src/www/index.html.tpl.in diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index d3827e7..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -1.0 diff --git a/debian/source/lintian-overrides b/debian/source/lintian-overrides index a991da2..100187c 100644 --- a/debian/source/lintian-overrides +++ b/debian/source/lintian-overrides @@ -1,2 +1,2 @@ -pve-xtermjs source: source-is-missing www/xterm.js line length is * -pve-xtermjs source: source-is-missing www/addons/fit/fit.js line length is * +rust-termproxy source: source-is-missing src/www/xterm.js line length is * +rust-termproxy source: source-is-missing src/www/xterm-addon-fit.js line length is * diff --git a/src/Makefile b/src/Makefile deleted file mode 100644 index 4164587..0000000 --- a/src/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -all: - -.PHONY: install -install: - make -C bin install - make -C PVE install - make -C www install diff --git a/src/PVE/CLI/Makefile b/src/PVE/CLI/Makefile deleted file mode 100644 index 8ea5eff..0000000 --- a/src/PVE/CLI/Makefile +++ /dev/null @@ -1,8 +0,0 @@ -PERLLIBDIR=${DESTDIR}/usr/share/perl5 - -all: - -.PHONY: install -install: - install -d ${PERLLIBDIR}/PVE/CLI - install -m 0644 termproxy.pm ${PERLLIBDIR}/PVE/CLI/ diff --git a/src/PVE/CLI/termproxy.pm b/src/PVE/CLI/termproxy.pm deleted file mode 100644 index 089d9b7..0000000 --- a/src/PVE/CLI/termproxy.pm +++ /dev/null @@ -1,250 +0,0 @@ -package PVE::CLI::termproxy; - -use strict; -use warnings; - -use PVE::CLIHandler; -use PVE::JSONSchema qw(get_standard_option); -use PVE::PTY; -use LWP::UserAgent; -use IO::Select; -use IO::Socket::IP; - -use base qw(PVE::CLIHandler); - -use constant MAX_QUEUE_LEN => 16*1024; - -sub verify_ticket { - my ($ticket, $user, $path, $perm) = @_; - - # get all loopback addresses even if no IPv4 or IPv6 address is setup on - # the host, IO::Socket::IP sets AI_ADDRCONFIG (man getaddrinfo) per default - local @LWP::Protocol::http::EXTRA_SOCK_OPTS = ( - GetAddrInfoFlags => 0, - ); - - my $ua = LWP::UserAgent->new(); - - my $params = { - username => $user, - password => $ticket, - path => $path, - }; - - $params->{privs} = $perm if $perm; - - my $res = $ua->post ('http://127.0.0.1:85/api2/json/access/ticket', Content => $params); - - if (!$res->is_success) { - my $err = $res->status_line; - die "Authentication failed: '$err'\n"; - } -} - -sub listen_and_authenticate { - my ($port, $timeout, $path, $perm) = @_; - - my $params = { - Listen => 1, - ReuseAddr => 1, - Proto => &Socket::IPPROTO_TCP, - GetAddrInfoFlags => 0, - LocalAddr => 'localhost', - LocalPort => $port, - }; - - my $socket = IO::Socket::IP->new(%$params) or die "failed to open socket: $!\n"; - - alarm 0; - local $SIG{ALRM} = sub { die "timed out waiting for client\n" }; - alarm $timeout; - my $client = $socket->accept; # Wait for a client - alarm 0; - close($socket); - - my $queue; - my $n = sysread($client, $queue, 4096); - if ($n && $queue =~ s/^([^:]+):(.+)\n//) { - my $user = $1; - my $ticket = $2; - - verify_ticket($ticket, $user, $path, $perm); - - die "aknowledge failed\n" - if !syswrite($client, "OK"); - - } else { - die "malformed authentication string\n"; - } - - return ($queue, $client); -} - -sub run_pty { - my ($cmd, $webhandle, $queue) = @_; - - foreach my $k (keys %ENV) { - next if $k eq 'PATH' || $k eq 'USER' || $k eq 'HOME' || $k eq 'LANG' || $k eq 'LANGUAGE'; - next if $k =~ m/^LC_/; - delete $ENV{$k}; - } - - $ENV{TERM} = 'xterm-256color'; - - my $pty = PVE::PTY->new(); - - my $pid = fork(); - die "fork: $!\n" if !defined($pid); - if (!$pid) { - $pty->make_controlling_terminal(); - exec {$cmd->[0]} @$cmd - or POSIX::_exit(1); - } - - $pty->set_size(80,20); - - read_write_loop($webhandle, $pty->master, $queue, $pty); - - $pty->close(); - waitpid($pid,0); - exit(0); -} - -sub read_write_loop { - my ($webhandle, $cmdhandle, $queue, $pty) = @_; - - my $select = new IO::Select; - - $select->add($webhandle); - $select->add($cmdhandle); - - my @handles; - - # we may have already messages from the first read - $queue = process_queue($queue, $cmdhandle, $pty); - - my $timeout = 5*60; - - while($select->count && scalar(@handles = $select->can_read($timeout))) { - foreach my $h (@handles) { - my $buf; - my $n = $h->sysread($buf, 4096); - - if ($h == $webhandle) { - if ($n && (length($queue) + $n) < MAX_QUEUE_LEN) { - $queue = process_queue($queue.$buf, $cmdhandle, $pty); - } else { - return; - } - } elsif ($h == $cmdhandle) { - if ($n) { - syswrite($webhandle, $buf); - } else { - return; - } - } - } - } -} - -sub process_queue { - my ($queue, $handle, $pty) = @_; - - my $msg; - while(length($queue)) { - ($queue, $msg) = remove_message($queue, $pty); - last if !defined($msg); - syswrite($handle, $msg); - } - return $queue; -} - - -# we try to remove a whole message -# if we succeed, we return the remaining queue and the msg -# if we fail, the message is undef and the queue is not changed -sub remove_message { - my ($queue, $pty) = @_; - - my $msg; - my $type = substr $queue, 0, 1; - - if ($type eq '0') { - # normal message - my ($length) = $queue =~ m/^0:(\d+):/; - my $begin = 3 + length($length); - if (defined($length) && length($queue) >= ($length + $begin)) { - $msg = substr $queue, $begin, $length; - if (defined($msg)) { - # msg contains now $length chars after 0:$length: - $queue = substr $queue, $begin + $length; - } - } - } elsif ($type eq '1') { - # resize message - my ($cols, $rows) = $queue =~ m/^1:(\d+):(\d+):/; - if (defined($cols) && defined($rows)) { - $queue = substr $queue, (length($cols) + length ($rows) + 4); - eval { $pty->set_size($cols, $rows) if defined($pty) }; - warn $@ if $@; - $msg = ""; - } - } elsif ($type eq '2') { - # ping - $queue = substr $queue, 1; - $msg = ""; - } else { - # ignore other input - $queue = substr $queue, 1; - $msg = ""; - } - - return ($queue, $msg); -} - -__PACKAGE__->register_method ({ - name => 'exec', - path => 'exec', - method => 'POST', - description => "Connects a TCP Socket with a commandline", - parameters => { - additionalProperties => 0, - properties => { - port => { - type => 'integer', - description => "The port to listen on." - }, - path => { - type => 'string', - description => "The Authentication path.", - }, - perm => { - type => 'string', - description => "The Authentication Permission.", - optional => 1, - }, - 'extra-args' => get_standard_option('extra-args'), - }, - }, - returns => { type => 'null'}, - code => sub { - my ($param) = @_; - - my $cmd; - if (defined($param->{'extra-args'})) { - $cmd = [@{$param->{'extra-args'}}]; - } else { - die "No command given\n"; - } - - my ($queue, $handle) = listen_and_authenticate($param->{port}, 10, - $param->{path}, $param->{perm}); - - run_pty($cmd, $handle, $queue); - - return undef; - }}); - -our $cmddef = [ __PACKAGE__, 'exec', ['port', 'extra-args' ]]; - -1; diff --git a/src/PVE/Makefile b/src/PVE/Makefile deleted file mode 100644 index b0321d7..0000000 --- a/src/PVE/Makefile +++ /dev/null @@ -1,3 +0,0 @@ -.PHONY: install -install: - make -C CLI install diff --git a/src/bin/Makefile b/src/bin/Makefile deleted file mode 100644 index a8c2842..0000000 --- a/src/bin/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -BINDIR=${DESTDIR}/usr/bin - -.PHONY: install -install: termproxy - perl -I.. -T -e "use PVE::CLI::termproxy; PVE::CLI::termproxy->verify_api();" - install -d ${BINDIR} - install -m 0755 termproxy ${BINDIR} diff --git a/src/bin/termproxy b/src/bin/termproxy deleted file mode 100755 index a28bcd9..0000000 --- a/src/bin/termproxy +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/perl - -use strict; -use warnings; - -use PVE::CLI::termproxy; - -PVE::CLI::termproxy->run_cli_handler(); diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c7bd32e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,456 @@ +use std::cmp::min; +use std::collections::HashMap; +use std::ffi::{OsStr, OsString}; +use std::io::{ErrorKind, Read, Result, Write}; +use std::net::TcpStream; +use std::os::unix::io::{AsRawFd, FromRawFd}; +use std::os::unix::process::CommandExt; +use std::process::Command; +use std::time::{Duration, Instant}; + +use clap::{App, AppSettings, Arg}; +use curl::easy::Easy; +use mio::net::TcpListener; +use mio::unix::{EventedFd, UnixReady}; +use mio::{Events, Poll, PollOpt, Ready, Token}; + +use proxmox::sys::error::io_err_other; +use proxmox::sys::linux::pty::{make_controlling_terminal, PTY}; +use proxmox::tools::byte_buffer::ByteBuffer; +use proxmox::{io_bail, io_format_err}; + +const MSG_TYPE_DATA: u8 = 0; +const MSG_TYPE_RESIZE: u8 = 1; +//const MSG_TYPE_PING: u8 = 2; + +fn remove_number(buf: &mut ByteBuffer) -> Option { + loop { + if let Some(pos) = &buf.iter().position(|&x| x == b':') { + let data = buf.remove_data(*pos); + buf.consume(1); // the ':' + let len = match std::str::from_utf8(&data) { + Ok(lenstring) => match lenstring.parse() { + Ok(len) => len, + Err(err) => { + eprintln!("error parsing number: '{}'", err); + break; + } + }, + Err(err) => { + eprintln!("error parsing number: '{}'", err); + break; + } + }; + return Some(len); + } else if buf.len() > 20 { + buf.consume(20); + } else { + break; + } + } + None +} + +fn process_queue(buf: &mut ByteBuffer, pty: &mut PTY) -> Option { + if buf.is_empty() { + return None; + } + + loop { + if buf.len() < 2 { + break; + } + + let msgtype = buf[0] - b'0'; + + if msgtype == MSG_TYPE_DATA { + buf.consume(2); + if let Some(len) = remove_number(buf) { + return Some(len); + } + } else if msgtype == MSG_TYPE_RESIZE { + 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()?; + } + } + // ignore incomplete messages + } else { + buf.consume(1); + // ignore invalid or ping (msgtype 2) + } + } + + None +} + +type TicketResult = Result<(Box<[u8]>, Box<[u8]>)>; + +/// Reads from the stream and returns the first line and the rest +fn read_ticket_line( + stream: &mut TcpStream, + buf: &mut ByteBuffer, + timeout: Duration, +) -> TicketResult { + let now = Instant::now(); + while !&buf[..].contains(&b'\n') { + if buf.is_full() || now.elapsed() >= timeout { + io_bail!("authentication data is incomplete: {:?}", &buf[..]); + } + stream.set_read_timeout(Some(Duration::new(1, 0)))?; + match buf.read_from(stream) { + Ok(n) => { + if n == 0 { + io_bail!("connection closed before authentication"); + } + } + Err(err) if err.kind() == ErrorKind::WouldBlock => {} + Err(err) => return Err(err), + } + } + + stream.set_read_timeout(None)?; + let newline_idx = &buf[..].iter().position(|&x| x == b'\n').unwrap(); + + let line = buf.remove_data(*newline_idx); + buf.consume(1); // discard newline + + match line.iter().position(|&b| b == b':') { + Some(pos) => { + let (username, ticket) = line.split_at(pos); + Ok((username.into(), ticket[1..].into())) + } + None => io_bail!("authentication data is invalid"), + } +} + +fn authenticate( + username: &[u8], + ticket: &[u8], + path: &str, + perm: Option<&str>, + authport: u16, + port: Option, +) -> Result<()> { + let mut curl = Easy::new(); + curl.url(&format!( + "http://localhost:{}/api2/json/access/ticket", + authport + ))?; + + let username = curl.url_encode(username); + let ticket = curl.url_encode(ticket); + let path = curl.url_encode(path.as_bytes()); + + let mut post_fields = Vec::with_capacity(5); + post_fields.push(format!("username={}", username)); + post_fields.push(format!("password={}", ticket)); + post_fields.push(format!("path={}", path)); + + if let Some(perm) = perm { + let perm = curl.url_encode(perm.as_bytes()); + post_fields.push(format!("privs={}", perm)); + } + + if let Some(port) = port { + post_fields.push(format!("port={}", port)); + } + + curl.post_fields_copy(post_fields.join("&").as_bytes())?; + curl.post(true)?; + curl.perform()?; + + let response_code = curl.response_code()?; + + if response_code != 200 { + io_bail!("invalid authentication, code {}", response_code); + } + + Ok(()) +} + +fn listen_and_accept( + hostname: &str, + port: u64, + port_as_fd: bool, + 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 port = listener.local_addr()?.port(); + let listener = TcpListener::from_std(listener)?; + let poll = Poll::new()?; + + poll.register(&listener, Token(0), Ready::readable(), PollOpt::edge())?; + + let mut events = Events::with_capacity(1); + + let mut timeout = timeout; + loop { + let now = Instant::now(); + poll.poll(&mut events, Some(timeout))?; + let elapsed = now.elapsed(); + if !events.is_empty() { + let (stream, client) = listener.accept_std()?; + println!("client connection: {:?}", client); + return Ok((stream, port)); + } + if timeout >= elapsed { + timeout -= elapsed; + } else { + io_bail!("timed out"); + } + } +} + +fn run_pty(cmd: &OsStr, params: clap::OsValues) -> Result { + let (mut pty, secondary_name) = PTY::new().map_err(io_err_other)?; + + let mut filtered_env: HashMap = std::env::vars_os() + .filter(|&(ref k, _)| { + k == "PATH" + || k == "USER" + || k == "HOME" + || k == "LANG" + || k == "LANGUAGE" + || k.to_string_lossy().starts_with("LC_") + }) + .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())?; + Ok(pty) +} + +const TCP: Token = Token(0); +const PTY: Token = Token(1); + +fn do_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("use-port-as-fd").long("port-as-fd")) + .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: u64 = 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 use_port_as_fd = matches.is_present("use-port-as-fd"); + + if use_port_as_fd && port > u16::MAX as u64 { + return Err(io_format_err!("port too big")); + } else if port > i32::MAX as u64 { + return Err(io_format_err!("Invalid FD number")); + } + + let (mut stream, port) = + listen_and_accept("localhost", port, use_port_as_fd, Duration::new(10, 0)) + .map_err(|err| io_format_err!("failed waiting for client: {}", err))?; + + let (username, ticket) = read_ticket_line(&mut stream, &mut pty_buf, Duration::new(10, 0)) + .map_err(|err| io_format_err!("failed reading ticket: {}", err))?; + let port = if use_port_as_fd { Some(port) } else { None }; + authenticate(&username, &ticket, path, perm, authport, port)?; + stream.write_all(b"OK").expect("error writing response"); + + let mut tcp_handle = mio::net::TcpStream::from_stream(stream)?; + + let poll = Poll::new()?; + let mut events = Events::with_capacity(128); + + let mut pty = run_pty(cmd, cmdparams)?; + + poll.register( + &tcp_handle, + TCP, + Ready::readable() | Ready::writable() | UnixReady::hup(), + PollOpt::edge(), + )?; + poll.register( + &EventedFd(&pty.as_raw_fd()), + PTY, + Ready::readable() | Ready::writable() | UnixReady::hup(), + PollOpt::edge(), + )?; + + let mut tcp_writable = true; + let mut pty_writable = true; + let mut tcp_readable = true; + let mut pty_readable = true; + let mut remaining = 0; + let mut finished = false; + + while !finished { + if tcp_readable && !pty_buf.is_full() || pty_readable && !tcp_buf.is_full() { + poll.poll(&mut events, Some(Duration::new(0, 0)))?; + } else { + poll.poll(&mut events, None)?; + } + + for event in &events { + let readiness = event.readiness(); + let writable = readiness.is_writable(); + let readable = readiness.is_readable(); + if UnixReady::from(readiness).is_hup() { + finished = true; + } + match event.token() { + TCP => { + if readable { + tcp_readable = true; + } + if writable { + tcp_writable = true; + } + } + PTY => { + if readable { + pty_readable = true; + } + if writable { + pty_writable = true; + } + } + _ => unreachable!(), + } + } + + while tcp_readable && !pty_buf.is_full() { + let bytes = match pty_buf.read_from(&mut tcp_handle) { + Ok(bytes) => bytes, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + tcp_readable = false; + break; + } + Err(err) => { + if !finished { + return Err(io_format_err!("error reading from tcp: {}", err)); + } + break; + } + }; + if bytes == 0 { + finished = true; + break; + } + } + + 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) => { + if !finished { + return Err(io_format_err!("error reading from pty: {}", err)); + } + break; + } + }; + if bytes == 0 { + finished = true; + break; + } + } + + while !tcp_buf.is_empty() && tcp_writable { + let bytes = match tcp_handle.write(&tcp_buf[..]) { + Ok(bytes) => bytes, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + tcp_writable = false; + break; + } + Err(err) => { + if !finished { + return Err(io_format_err!("error writing to tcp : {}", err)); + } + break; + } + }; + 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.len()); + let bytes = match pty.write(&pty_buf[..len]) { + Ok(bytes) => bytes, + Err(err) if err.kind() == ErrorKind::WouldBlock => { + pty_writable = false; + break; + } + Err(err) => { + if !finished { + return Err(io_format_err!("error writing to pty : {}", err)); + } + break; + } + }; + remaining -= bytes; + pty_buf.consume(bytes); + } + } + + Ok(()) +} + +fn main() { + std::process::exit(match do_main() { + Ok(_) => 0, + Err(err) => { + eprintln!("{}", err); + 1 + } + }); +} diff --git a/src/www/Makefile b/src/www/Makefile deleted file mode 100644 index 5e51258..0000000 --- a/src/www/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -WWWBASEDIR=${DESTDIR}/usr/share/pve-xtermjs - -SOURCE = \ - xterm-addon-fit.js \ - xterm-addon-fit.js.map \ - index.html.tpl \ - main.js \ - style.css \ - util.js \ - xterm.css \ - xterm.js \ - xterm.js.map - -index.html.tpl: index.html.tpl.in - sed -e 's/@VERSION@/${VERSION}/' $< >$@.tmp - mv $@.tmp $@ - -.PHONY: install -install: ${SOURCE} - install -d ${WWWBASEDIR} - set -e && for i in ${SOURCE}; do install -m 0644 $$i ${WWWBASEDIR}; done -- 2.20.1