[pbs-devel] [PATCH xtermjs v3 1/4] termproxy: rewrite in rust

Dominik Csapak d.csapak at proxmox.com
Tue Jul 21 11:00:45 CEST 2020


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 <d.csapak at proxmox.com>
---
 .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 <d.csapak at proxmox.com>"]
+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 <support at proxmox.com>
-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 <support at proxmox.com>"
+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<usize> {
+    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<usize> {
+    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<u16>,
+) -> 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<PTY> {
+    let (mut pty, secondary_name) = PTY::new().map_err(io_err_other)?;
+
+    let mut filtered_env: HashMap<OsString, OsString> = 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






More information about the pbs-devel mailing list