mirror of
git://git.proxmox.com/git/pve-xtermjs.git
synced 2025-01-10 05:18:33 +03:00
termproxy: rewrite in rust
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@proxmox.com>
This commit is contained in:
parent
f64e1bfac7
commit
3e4311feec
5
.cargo/config
Normal file
5
.cargo/config
Normal file
@ -0,0 +1,5 @@
|
||||
[source]
|
||||
[source.debian-packages]
|
||||
directory = "/usr/share/cargo/registry"
|
||||
[source.crates-io]
|
||||
replace-with = "debian-packages"
|
14
Cargo.toml
Normal file
14
Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "termproxy"
|
||||
version = "4.3.0"
|
||||
authors = ["Dominik Csapak <d.csapak@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 }
|
52
Makefile
52
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
|
||||
|
||||
${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
|
||||
all: cargo-build $(SRCIDR)
|
||||
|
||||
.PHONY: $(SUBDIRS)
|
||||
$(SUBDIRS):
|
||||
make -C $@
|
||||
|
||||
.PHONY: cargo-build
|
||||
cargo-build:
|
||||
cargo build $(CARGO_BUILD_ARGS)
|
||||
|
||||
.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
|
||||
|
1
debian/compat
vendored
1
debian/compat
vendored
@ -1 +0,0 @@
|
||||
10
|
16
debian/control
vendored
16
debian/control
vendored
@ -1,16 +0,0 @@
|
||||
Source: pve-xtermjs
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Proxmox Support Team <support@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
|
14
debian/debcargo.toml
vendored
Normal file
14
debian/debcargo.toml
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
overlay = "."
|
||||
crate_src_path = ".."
|
||||
bin_name = "pve-xtermjs"
|
||||
|
||||
[source]
|
||||
maintainer = "Proxmox Support Team <support@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"
|
1
debian/install
vendored
Normal file
1
debian/install
vendored
Normal file
@ -0,0 +1 @@
|
||||
src/www/* /usr/share/pve-xtermjs/
|
7
debian/rules
vendored
7
debian/rules
vendored
@ -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
|
||||
|
1
debian/source/format
vendored
1
debian/source/format
vendored
@ -1 +0,0 @@
|
||||
1.0
|
4
debian/source/lintian-overrides
vendored
4
debian/source/lintian-overrides
vendored
@ -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 *
|
||||
|
@ -1,7 +0,0 @@
|
||||
all:
|
||||
|
||||
.PHONY: install
|
||||
install:
|
||||
make -C bin install
|
||||
make -C PVE install
|
||||
make -C www install
|
@ -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/
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
.PHONY: install
|
||||
install:
|
||||
make -C CLI install
|
@ -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}
|
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/perl
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use PVE::CLI::termproxy;
|
||||
|
||||
PVE::CLI::termproxy->run_cli_handler();
|
456
src/main.rs
Normal file
456
src/main.rs
Normal file
@ -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
|
||||
}
|
||||
});
|
||||
}
|
@ -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
|
Loading…
Reference in New Issue
Block a user