mirror of
https://github.com/webmin/webmin.git
synced 2026-06-28 15:00:25 +01:00
Merge pull request #2772 from webmin/dev/miniserv-proxy-websockets
Add support to proxy linked-server WebSockets
This commit is contained in:
File diff suppressed because one or more lines are too long
430
miniserv.pl
430
miniserv.pl
@@ -1509,6 +1509,24 @@ while(1) {
|
||||
}
|
||||
}
|
||||
alarm(0);
|
||||
my $websocket_upgrade_request = lc($header{'connection'}) =~ /upgrade/ &&
|
||||
lc($header{'upgrade'}) eq 'websocket';
|
||||
my $websocket_configured_request;
|
||||
my $websocket_basic_auth_ok;
|
||||
if ($websocket_upgrade_request) {
|
||||
# Check the configured websocket paths before auth, so Basic auth can
|
||||
# remain disabled for normal session-mode requests.
|
||||
my $wsbogus;
|
||||
my $ws_simple = &simplify_path($page, $wsbogus);
|
||||
$websocket_configured_request = !$wsbogus &&
|
||||
&find_websocket_config($ws_simple);
|
||||
if ($websocket_configured_request) {
|
||||
# Session-mode Basic auth stays disabled unless the websocket
|
||||
# route explicitly allows a no-cookie Basic-auth hop.
|
||||
$websocket_basic_auth_ok =
|
||||
$websocket_configured_request->{'allow_basic_ws'};
|
||||
}
|
||||
}
|
||||
|
||||
# If a remote IP is given in a header (such as via a proxy), only use it
|
||||
# for logging unless trust_real_ip is set
|
||||
@@ -1829,8 +1847,11 @@ if (!$validated && !$deny_authentication) {
|
||||
}
|
||||
}
|
||||
|
||||
# Check for normal HTTP authentication
|
||||
if (!$validated && !$deny_authentication && !$config{'session'} &&
|
||||
# Keep Basic auth disabled in session mode except for websocket routes that
|
||||
# explicitly allow no-cookie Basic-auth hops. Token/user checks still run
|
||||
# before the backend connection is opened.
|
||||
if (!$validated && !$deny_authentication &&
|
||||
(!$config{'session'} || $websocket_basic_auth_ok) &&
|
||||
$header{authorization} =~ /^basic\s+(\S+)$/i) {
|
||||
# authorization given..
|
||||
($authuser, $authpass) = split(/:/, &b64decode($1), 2);
|
||||
@@ -2335,21 +2356,9 @@ if ($davpath) {
|
||||
}
|
||||
|
||||
# Check for a websockets request
|
||||
if (lc($header{'connection'}) =~ /upgrade/ &&
|
||||
lc($header{'upgrade'}) eq 'websocket' &&
|
||||
$baseauthuser) {
|
||||
if ($websocket_upgrade_request && $baseauthuser) {
|
||||
print DEBUG "websockets request to $simple\n";
|
||||
my $ws_simple = $simple;
|
||||
my ($ws) = grep { $_->{'path'} eq $ws_simple } @websocket_paths;
|
||||
if (!$ws && $config{'redirect_prefix'}) {
|
||||
my $prefix = $config{'redirect_prefix'};
|
||||
$prefix =~ s/[\/]+$//g;
|
||||
if ($prefix && $ws_simple =~ s/^\Q$prefix\E(?=\/|$)//) {
|
||||
$ws_simple ||= "/";
|
||||
print DEBUG "websockets retry without prefix $prefix as $ws_simple\n";
|
||||
($ws) = grep { $_->{'path'} eq $ws_simple } @websocket_paths;
|
||||
}
|
||||
}
|
||||
my ($ws, $ws_simple) = &find_websocket_config($simple);
|
||||
if (!$ws) {
|
||||
&http_error(400, "Unknown websocket path");
|
||||
return 0;
|
||||
@@ -5033,6 +5042,27 @@ foreach my $c (keys %config) {
|
||||
}
|
||||
}
|
||||
|
||||
# find_websocket_config(path)
|
||||
# Returns the websocket config and path, after dropping any query string and
|
||||
# redirect prefix from the request path.
|
||||
sub find_websocket_config
|
||||
{
|
||||
my ($simple) = @_;
|
||||
my $ws_simple = $simple;
|
||||
$ws_simple =~ s/\?.*$//;
|
||||
my ($ws) = grep { $_->{'path'} eq $ws_simple } @websocket_paths;
|
||||
if (!$ws && $config{'redirect_prefix'}) {
|
||||
my $prefix = $config{'redirect_prefix'};
|
||||
$prefix =~ s/[\/]+$//g;
|
||||
if ($prefix && $ws_simple =~ s/^\Q$prefix\E(?=\/|$)//) {
|
||||
$ws_simple ||= "/";
|
||||
print DEBUG "websockets retry without prefix $prefix as $ws_simple\n";
|
||||
($ws) = grep { $_->{'path'} eq $ws_simple } @websocket_paths;
|
||||
}
|
||||
}
|
||||
return wantarray ? ($ws, $ws_simple) : $ws;
|
||||
}
|
||||
|
||||
# reload_config_file()
|
||||
# Re-read %config, and call post-config actions
|
||||
sub reload_config_file
|
||||
@@ -5074,6 +5104,71 @@ close(CONF);
|
||||
return %rv;
|
||||
}
|
||||
|
||||
# lock_config_file(file)
|
||||
# Lock a config file using Webmin's .lock sidecar convention. miniserv.pl must
|
||||
# stay standalone, but link.cgi also edits miniserv.conf through this lock.
|
||||
sub lock_config_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
if ($file =~ /\r|\n|\0/) {
|
||||
die "Lock filename contains invalid characters";
|
||||
}
|
||||
my $lockfile = $file.".lock";
|
||||
my $tries = 0;
|
||||
my $last_lock_err;
|
||||
while(1) {
|
||||
my $pid;
|
||||
# Webmin's lock file contains the owner PID. A dead owner means the lock
|
||||
# can be safely reclaimed.
|
||||
if (open(my $locking, "<", $lockfile)) {
|
||||
$pid = int(<$locking>);
|
||||
close($locking);
|
||||
}
|
||||
if (!$pid || !kill(0, $pid) || $pid == $$) {
|
||||
# The sidecar lock is the shared convention with web-lib-funcs.pl;
|
||||
# the non-blocking flock just narrows races between lock claimers.
|
||||
unlink($lockfile);
|
||||
if (open(my $locking, ">", $lockfile)) {
|
||||
if (!flock($locking, 2+4)) {
|
||||
$last_lock_err = "Flock failed : ".$!;
|
||||
close($locking);
|
||||
unlink($lockfile);
|
||||
}
|
||||
elsif (!print $locking $$,"\n") {
|
||||
$last_lock_err = "Lock write failed : ".$!;
|
||||
close($locking);
|
||||
unlink($lockfile);
|
||||
}
|
||||
elsif (!close($locking)) {
|
||||
$last_lock_err = "Lock close failed : ".$!;
|
||||
unlink($lockfile);
|
||||
}
|
||||
else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$last_lock_err = "Lock open failed : ".$!;
|
||||
}
|
||||
}
|
||||
elsif ($pid) {
|
||||
$last_lock_err = "Locked by PID $pid";
|
||||
}
|
||||
sleep(1);
|
||||
if ($tries++ > 5*60) {
|
||||
die "Failed to lock config file $file : $last_lock_err";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# unlock_config_file(file)
|
||||
# Release a config lock taken by lock_config_file.
|
||||
sub unlock_config_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
unlink($file.".lock");
|
||||
}
|
||||
|
||||
# read_any_file(file)
|
||||
# Reads any given file and returns its content
|
||||
sub read_any_file
|
||||
@@ -6078,79 +6173,178 @@ if (!grep { $_ eq $parsed_origin } @allowed_origins) {
|
||||
}
|
||||
my @protos = split(/\s*,\s*/, $header{'sec-websocket-protocol'});
|
||||
print DEBUG "websockets protos ",join(" ", @protos),"\n";
|
||||
# Once token and origin checks have passed, a ws-link route is a live
|
||||
# single-use credential. If the backend cannot complete its handshake, remove
|
||||
# that route before http_error exits this child.
|
||||
my $backend_fail = sub {
|
||||
&cleanup_websocket_route($ws);
|
||||
&http_error(@_);
|
||||
return 0;
|
||||
};
|
||||
|
||||
# Connect to the configured backend
|
||||
my $fh = "WEBSOCKET";
|
||||
my ($backend_ssl, $backend_ssl_ctx);
|
||||
if ($ws->{'host'}) {
|
||||
# Backend is a TCP port
|
||||
my $err = &open_socket($ws->{'host'}, $ws->{'port'}, $fh);
|
||||
&http_error(500, "Websockets connection failed : $err") if ($err);
|
||||
return &$backend_fail(500, "Websockets connection failed : $err")
|
||||
if ($err);
|
||||
if ($ws->{'ssl'}) {
|
||||
eval "use Net::SSLeay";
|
||||
if ($@) {
|
||||
return &$backend_fail(500,
|
||||
"Missing Net::SSLeay perl module");
|
||||
}
|
||||
$backend_ssl_ctx = Net::SSLeay::CTX_new();
|
||||
if (!$backend_ssl_ctx) {
|
||||
return &$backend_fail(500,
|
||||
"Failed to create SSL context");
|
||||
}
|
||||
$backend_ssl = Net::SSLeay::new($backend_ssl_ctx);
|
||||
if (!$backend_ssl) {
|
||||
return &$backend_fail(500,
|
||||
"Failed to create SSL connection");
|
||||
}
|
||||
Net::SSLeay::set_fd($backend_ssl, fileno($fh));
|
||||
my $sslhost = $ws->{'hostheader'} || $ws->{'host'};
|
||||
if (defined(&Net::SSLeay::set_tlsext_host_name)) {
|
||||
# Linked websocket routes may connect to an IP while the
|
||||
# certificate belongs to the configured linked-server
|
||||
# host.
|
||||
my $snihost = $sslhost;
|
||||
if ($snihost =~ /^\[([^\]]+)\](?::\d+)?$/) {
|
||||
$snihost = $1;
|
||||
}
|
||||
elsif ($snihost =~ /^([^:]+):\d+$/) {
|
||||
$snihost = $1;
|
||||
}
|
||||
Net::SSLeay::set_tlsext_host_name($backend_ssl, $snihost);
|
||||
}
|
||||
if (!Net::SSLeay::connect($backend_ssl)) {
|
||||
return &$backend_fail(500,
|
||||
"SSL connect to websockets backend failed");
|
||||
}
|
||||
if ($ws->{'checkssl'}) {
|
||||
my $err = &check_websocket_backend_ssl(
|
||||
$backend_ssl, $sslhost);
|
||||
if ($err) {
|
||||
return &$backend_fail(500,
|
||||
"Invalid SSL certificate from websockets ".
|
||||
"backend : $err");
|
||||
}
|
||||
}
|
||||
}
|
||||
print DEBUG "websockets host $ws->{'host'}:$ws->{'port'}\n";
|
||||
}
|
||||
elsif ($ws->{'pipe'}) {
|
||||
# Backend is a Unix pipe
|
||||
open($fh, $ws->{'pipe'}) ||
|
||||
&http_error(500, "Websockets pipe failed : $?");
|
||||
return &$backend_fail(500, "Websockets pipe failed : $?");
|
||||
print DEBUG "websockets pipe $ws->{'pipe'}\n";
|
||||
}
|
||||
else {
|
||||
&http_error(500, "Invalid Webmin websockets config");
|
||||
return &$backend_fail(500, "Invalid Webmin websockets config");
|
||||
}
|
||||
# Keep the rest of the websocket proxy code independent of whether the
|
||||
# backend hop is plain TCP or wrapped in TLS.
|
||||
my $backend_write = sub {
|
||||
my ($buf) = @_;
|
||||
return $backend_ssl ? Net::SSLeay::write($backend_ssl, $buf)
|
||||
: syswrite($fh, $buf, length($buf));
|
||||
};
|
||||
my $backend_read = sub {
|
||||
my ($size) = @_;
|
||||
if ($backend_ssl) {
|
||||
return Net::SSLeay::read($backend_ssl, $size);
|
||||
}
|
||||
my $buf;
|
||||
my $rv = sysread($fh, $buf, $size);
|
||||
return $rv ? $buf : undef;
|
||||
};
|
||||
my $backend_readline = sub {
|
||||
my $line = "";
|
||||
while(1) {
|
||||
my $c = $backend_read->(1);
|
||||
return undef if (!defined($c) || $c eq "");
|
||||
$line .= $c;
|
||||
last if ($c eq "\n");
|
||||
}
|
||||
return $line;
|
||||
};
|
||||
|
||||
# Send successful connection headers
|
||||
# OpenSSL can keep decrypted bytes in its own buffer after the socket fd stops
|
||||
# being readable, so the forwarding loop must check this before select().
|
||||
my $backend_pending = sub {
|
||||
return $backend_ssl && defined(&Net::SSLeay::pending) &&
|
||||
Net::SSLeay::pending($backend_ssl) > 0;
|
||||
};
|
||||
|
||||
# Prepare successful connection headers, but don't send them to the browser
|
||||
# until the backend websocket handshake has succeeded.
|
||||
eval "use Digest::SHA";
|
||||
if ($@) {
|
||||
&http_error(500, "Missing Digest::SHA perl module");
|
||||
return &$backend_fail(500, "Missing Digest::SHA perl module");
|
||||
}
|
||||
my $rkey = $key."258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
my $sha1 = Digest::SHA->new;
|
||||
$sha1->add($rkey);
|
||||
my $digest = $sha1->digest;
|
||||
$digest = &b64encode($digest);
|
||||
&write_data("HTTP/1.1 101 Switching Protocols\r\n");
|
||||
&write_data("Upgrade: websocket\r\n");
|
||||
&write_data("Connection: Upgrade\r\n");
|
||||
&write_data("Sec-Websocket-Accept: $digest\r\n");
|
||||
if (@protos) {
|
||||
&write_data("Sec-Websocket-Protocol: $protos[0]\r\n");
|
||||
}
|
||||
&write_data("\r\n");
|
||||
|
||||
# Send a websockets request to the backend
|
||||
my $path = $ws->{'wspath'} || $simple;
|
||||
my $bsession_id = &b64encode($session_id);
|
||||
# Normal websocket proxies use the browser session id in the backend
|
||||
# Sec-WebSocket-Key. Linked xterm routes can instead provide a one-time
|
||||
# backend session key because the backend request is authenticated with Basic.
|
||||
my $backend_session = $ws->{'backend_session'} || $session_id;
|
||||
my $bsession_id = &b64encode($backend_session);
|
||||
print DEBUG "send request to $path to websockets backend\n";
|
||||
print $fh "GET $path HTTP/1.1\r\n";
|
||||
if ($ws->{'host'}) {
|
||||
print $fh "Host: $ws->{'host'}\r\n";
|
||||
$backend_write->("GET $path HTTP/1.1\r\n");
|
||||
if ($ws->{'host'} || $ws->{'hostheader'}) {
|
||||
$backend_write->("Host: ".($ws->{'hostheader'} || $ws->{'host'})."\r\n");
|
||||
}
|
||||
print $fh "Upgrade: websocket\r\n";
|
||||
print $fh "Connection: Upgrade\r\n";
|
||||
$backend_write->("Upgrade: websocket\r\n");
|
||||
$backend_write->("Connection: Upgrade\r\n");
|
||||
if ($ws->{'nokey'}) {
|
||||
print $fh "Sec-WebSocket-Key: $key\r\n";
|
||||
$backend_write->("Sec-WebSocket-Key: $key\r\n");
|
||||
}
|
||||
else {
|
||||
print DEBUG "Sending key $bsession_id\n";
|
||||
print $fh "Sec-WebSocket-Key: $bsession_id\r\n";
|
||||
$backend_write->("Sec-WebSocket-Key: $bsession_id\r\n");
|
||||
}
|
||||
if (@protos) {
|
||||
print $fh "Sec-WebSocket-Protocol: ",join(" ", @protos),"\r\n";
|
||||
$backend_write->("Sec-WebSocket-Protocol: ".join(" ", @protos)."\r\n");
|
||||
}
|
||||
print $fh "Sec-WebSocket-Version: $header{'sec-websocket-version'}\r\n";
|
||||
print $fh "\r\n";
|
||||
if ($ws->{'origin'}) {
|
||||
$backend_write->("Origin: $ws->{'origin'}\r\n");
|
||||
}
|
||||
if ($ws->{'auth'} && $ws->{'auth'} =~ /^basic:(\S+)$/) {
|
||||
$backend_write->("Authorization: Basic $1\r\n");
|
||||
}
|
||||
$backend_write->("Sec-WebSocket-Version: $header{'sec-websocket-version'}\r\n");
|
||||
$backend_write->("\r\n");
|
||||
|
||||
# Read back the reply
|
||||
my $rh = <$fh>;
|
||||
my $rh = $backend_readline->();
|
||||
if (!defined($rh)) {
|
||||
return &$backend_fail(500, "No response from websockets backend");
|
||||
}
|
||||
$rh =~ s/\r|\n//g;
|
||||
print DEBUG "got $rh from websockets backend\n";
|
||||
$rh =~ /^HTTP\/1\.1\s+(\d+)/ ||
|
||||
&http_error(500, "Bad response from websockets backend : ".
|
||||
&html_strip($rh));
|
||||
if ($rh !~ /^HTTP\/1\.1\s+(\d+)/) {
|
||||
return &$backend_fail(500, "Bad response from websockets backend : ".
|
||||
&html_strip($rh));
|
||||
}
|
||||
my $code = $1;
|
||||
my %rheader;
|
||||
my $lastheader;
|
||||
while(1) {
|
||||
$rh = <$fh>;
|
||||
$rh = $backend_readline->();
|
||||
if (!defined($rh)) {
|
||||
return &$backend_fail(500,
|
||||
"Unexpected EOF from websockets backend");
|
||||
}
|
||||
$rh =~ s/\r|\n//g;
|
||||
last if ($rh eq "");
|
||||
if ($rh =~ /^(\S+):\s*(.*)$/) {
|
||||
@@ -6161,18 +6355,24 @@ while(1) {
|
||||
$rheader{$lastheader} .= $headline;
|
||||
}
|
||||
else {
|
||||
&http_error(500, "Bad header from websockets backend ".
|
||||
&html_strip($rh));
|
||||
return &$backend_fail(500,
|
||||
"Bad header from websockets backend ".
|
||||
&html_strip($rh));
|
||||
}
|
||||
}
|
||||
if ($code != 101) {
|
||||
&http_error(500, "Bad response code $code from websockets backend : ".
|
||||
&html_strip($rh));
|
||||
return &$backend_fail(500,
|
||||
"Bad response code $code from websockets backend : ".
|
||||
&html_strip($rh));
|
||||
}
|
||||
if (lc($rheader{'upgrade'}) ne 'websocket') {
|
||||
return &$backend_fail(500,
|
||||
"Missing Upgrade header from websockets backend");
|
||||
}
|
||||
if (lc($rheader{'connection'}) !~ /upgrade/) {
|
||||
return &$backend_fail(500,
|
||||
"Missing Connection header from websockets backend");
|
||||
}
|
||||
lc($rheader{'upgrade'}) eq 'websocket' ||
|
||||
&http_error(500, "Missing Upgrade header from websockets backend");
|
||||
lc($rheader{'connection'}) =~ /upgrade/ ||
|
||||
&http_error(500, "Missing Connection header from websockets backend");
|
||||
|
||||
# Check the reply key
|
||||
my $bdigest;
|
||||
@@ -6187,8 +6387,24 @@ else {
|
||||
$bdigest = &b64encode($bdigest);
|
||||
}
|
||||
print DEBUG "expecting digest $bdigest\n";
|
||||
lc($rheader{'sec-websocket-accept'}) eq lc($bdigest) ||
|
||||
&http_error(500, "Incorrect digest header from websockets backend");
|
||||
if (lc($rheader{'sec-websocket-accept'}) ne lc($bdigest)) {
|
||||
return &$backend_fail(500,
|
||||
"Incorrect digest header from websockets backend");
|
||||
}
|
||||
# The route has been consumed by this child process. The active tunnel now owns
|
||||
# the open backend socket, so the temporary miniserv.conf route can be removed
|
||||
# before the tunnel goes long-lived. Final cleanup remains a harmless fallback.
|
||||
&cleanup_websocket_route($ws);
|
||||
|
||||
# Send successful connection headers
|
||||
&write_data("HTTP/1.1 101 Switching Protocols\r\n");
|
||||
&write_data("Upgrade: websocket\r\n");
|
||||
&write_data("Connection: Upgrade\r\n");
|
||||
&write_data("Sec-Websocket-Accept: $digest\r\n");
|
||||
if (@protos) {
|
||||
&write_data("Sec-Websocket-Protocol: $protos[0]\r\n");
|
||||
}
|
||||
&write_data("\r\n");
|
||||
|
||||
# Log now
|
||||
&log_request($loghost, $authuser, $reqline, "101", 0);
|
||||
@@ -6197,29 +6413,45 @@ lc($rheader{'sec-websocket-accept'}) eq lc($bdigest) ||
|
||||
seek(DEBUG, 0, 2);
|
||||
print DEBUG "in websockets loop\n";
|
||||
my $last_session_check_time = time();
|
||||
# Frontend browser sockets have a session id and should be revalidated while
|
||||
# open. Backend hops authenticated without a browser session must not be closed
|
||||
# by the periodic session check.
|
||||
my $verify_session = defined($session_id) && $session_id ne "";
|
||||
while(1) {
|
||||
my $rmask = undef;
|
||||
vec($rmask, fileno($fh), 1) = 1;
|
||||
vec($rmask, fileno(SOCK), 1) = 1;
|
||||
my $sel = select($rmask, undef, undef, 10);
|
||||
my ($buf, $ok);
|
||||
my $uptime = 0;
|
||||
if (vec($rmask, fileno($fh), 1)) {
|
||||
my ($backend_ready, $browser_ready);
|
||||
if (&$backend_pending()) {
|
||||
$backend_ready = 1;
|
||||
my $bm = undef;
|
||||
vec($bm, fileno(SOCK), 1) = 1;
|
||||
select($bm, undef, undef, 0);
|
||||
$browser_ready = vec($bm, fileno(SOCK), 1);
|
||||
}
|
||||
else {
|
||||
my $rmask = undef;
|
||||
vec($rmask, fileno($fh), 1) = 1;
|
||||
vec($rmask, fileno(SOCK), 1) = 1;
|
||||
my $sel = select($rmask, undef, undef, 10);
|
||||
$backend_ready = vec($rmask, fileno($fh), 1);
|
||||
$browser_ready = vec($rmask, fileno(SOCK), 1);
|
||||
}
|
||||
if ($backend_ready) {
|
||||
# Got something from the websockets backend
|
||||
$ok = sysread($fh, $buf, 1024);
|
||||
last if ($ok <= 0); # Backend has closed
|
||||
$buf = $backend_read->(1024);
|
||||
last if (!defined($buf) || length($buf) == 0);
|
||||
&write_data($buf);
|
||||
$uptime = 1;
|
||||
}
|
||||
if (vec($rmask, fileno(SOCK), 1)) {
|
||||
if ($browser_ready) {
|
||||
# Got something from the browser
|
||||
$buf = &read_data(1024);
|
||||
last if (!defined($buf) || length($buf) == 0);
|
||||
syswrite($fh, $buf, length($buf)) || last;
|
||||
$backend_write->($buf) || last;
|
||||
$uptime = 1;
|
||||
}
|
||||
my $now = time();
|
||||
if ($now - $last_session_check_time > 10) {
|
||||
if ($verify_session && $now - $last_session_check_time > 10) {
|
||||
# Re-validate the browser session every 10 seconds
|
||||
print DEBUG "verifying websockets session $session_id\n";
|
||||
print $PASSINw "verify $session_id $acptip $uptime\n";
|
||||
@@ -6231,13 +6463,47 @@ while(1) {
|
||||
$last_session_check_time = $now;
|
||||
}
|
||||
}
|
||||
Net::SSLeay::free($backend_ssl) if ($backend_ssl);
|
||||
Net::SSLeay::CTX_free($backend_ssl_ctx) if ($backend_ssl_ctx);
|
||||
close($fh);
|
||||
close(SOCK);
|
||||
&cleanup_websocket_route($ws);
|
||||
print DEBUG "done websockets loop\n";
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
# cleanup_websocket_route(&wsconfig)
|
||||
# Removes a single-use linked-server websocket route from miniserv.conf.
|
||||
sub cleanup_websocket_route
|
||||
{
|
||||
my ($ws) = @_;
|
||||
# Only link.cgi-created ws-link routes are temporary. Ordinary module
|
||||
# websocket routes are managed by their owning code and must be left alone.
|
||||
return 0 if (!$ws || !$ws->{'path'} || $ws->{'path'} !~ /\/ws-link-/);
|
||||
my $deleted;
|
||||
my $locked;
|
||||
my $cleanup_ok = eval {
|
||||
&lock_config_file($config_file);
|
||||
$locked = 1;
|
||||
my %miniserv = &read_config_file($config_file);
|
||||
if (delete($miniserv{"websockets_$ws->{'path'}"})) {
|
||||
&write_file($config_file, \%miniserv);
|
||||
$deleted = 1;
|
||||
}
|
||||
1;
|
||||
};
|
||||
my $cleanup_err = $@;
|
||||
eval { &unlock_config_file($config_file) } if ($locked);
|
||||
if (!$cleanup_ok) {
|
||||
&log_error("Failed to cleanup linked websocket route", $cleanup_err);
|
||||
return 0;
|
||||
}
|
||||
# The master keeps websocket routes parsed in memory.
|
||||
kill('USR1', $miniserv_main_pid || getppid()) if ($deleted);
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
# get_system_hostname()
|
||||
# Returns the hostname of this system, for reporting to listeners
|
||||
sub get_system_hostname
|
||||
@@ -7527,3 +7793,37 @@ foreach my $p (@$hosts) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
# check_websocket_backend_ssl(ssl-handle, host)
|
||||
# Returns an error if the websocket backend certificate is not for host.
|
||||
sub check_websocket_backend_ssl
|
||||
{
|
||||
my ($ssl, $host) = @_;
|
||||
if ($host =~ /^\[([^\]]+)\](?::\d+)?$/) {
|
||||
$host = $1;
|
||||
}
|
||||
elsif ($host =~ /^([^:]+):\d+$/) {
|
||||
$host = $1;
|
||||
}
|
||||
my $cert = Net::SSLeay::get_peer_certificate($ssl);
|
||||
return "Could not fetch peer certificate" if (!$cert);
|
||||
my @hosts;
|
||||
my $subject = Net::SSLeay::X509_get_subject_name($cert);
|
||||
if ($subject) {
|
||||
my $cn = Net::SSLeay::X509_NAME_get_text_by_NID($subject, 13);
|
||||
push(@hosts, $cn) if (defined($cn) && $cn ne "" && $cn ne "-1");
|
||||
}
|
||||
my @alts = Net::SSLeay::X509_get_subjectAltNames($cert);
|
||||
while(my ($type, $val) = splice(@alts, 0, 2)) {
|
||||
push(@hosts, $val) if ($type == 2 || $type == 7);
|
||||
}
|
||||
Net::SSLeay::X509_free($cert);
|
||||
if (&check_ipaddress($host) || &check_ip6address($host)) {
|
||||
return undef if (grep { lc($_) eq lc($host) } @hosts);
|
||||
return @hosts ? "Certificate is for ".join(", ", @hosts).", not $host"
|
||||
: "No certificate names found";
|
||||
}
|
||||
return undef if (@hosts && &ssl_hostname_match($host, \@hosts));
|
||||
return @hosts ? "Certificate is for ".join(", ", @hosts).", not $host"
|
||||
: "No certificate names found";
|
||||
}
|
||||
|
||||
155
servers/link.cgi
155
servers/link.cgi
@@ -191,29 +191,45 @@ else {
|
||||
|
||||
# read back the rest of the page
|
||||
if ($header->{'content-type'} &&
|
||||
$header->{'content-type'} =~ /text\/html/ &&
|
||||
!$header->{'x-no-links'}) {
|
||||
# Fix up HTML
|
||||
$header->{'content-type'} =~ /text\/html/) {
|
||||
# Fix up HTML. Websocket URLs must always be proxied via a local
|
||||
# miniserv ws-link route, even when the linked server sets x-no-links
|
||||
# (as themes do for AJAX page loads), because a websocket connection
|
||||
# cannot be tunnelled through link.cgi itself.
|
||||
my $dolinks = !$header->{'x-no-links'};
|
||||
my %websocket_links;
|
||||
&cleanup_link_websockets();
|
||||
while($_ = &read_http_connection($con)) {
|
||||
s/src='(\/[^']*)'/src='$url$1'/gi;
|
||||
s/src="(\/[^"]*)"/src="$url$1"/gi;
|
||||
s/src=(\/[^ "'>]*)/src=$url$1/gi;
|
||||
s/href='(\/[^']*)'/href='$url$1'/gi;
|
||||
s/href="(\/[^"]*)"/href="$url$1"/gi;
|
||||
s/href=(\/[^ >"']*)/href=$url$1/gi;
|
||||
s/action='(\/[^']*)'/action='$url$1'/gi;
|
||||
s/action="(\/[^"]*)"/action="$url$1"/gi;
|
||||
s/action=(\/[^ "'>]*)/action=$url$1/gi;
|
||||
s/\.location\s*=\s*'(\/[^']*)'/.location='$url$1'/gi;
|
||||
s/\.location\s*=\s*"(\/[^']*)"/.location="$url$1"/gi;
|
||||
s/window.open\("(\/[^"]*)"/window.open\("$url$1"/gi;
|
||||
s/name=return\s+value="(\/[^"]*)"/name=return value="$url$1"/gi;
|
||||
s/param\s+name=config\s+value='(\/[^']*)'/param name=config value='$url$1'/gi;
|
||||
s/param\s+name=config\s+value="(\/[^']*)"/param name=config value="$url$1"/gi;
|
||||
s/param\s+name=config\s+value=(\/[^']*)/param name=config value=$url$1/gi;
|
||||
# Websocket URLs can appear in JavaScript strings or JSON values,
|
||||
# where slashes are escaped. Ordinary HTML rewrites remain gated
|
||||
# below by x-no-links, but websocket URLs always need local routes.
|
||||
s#(['"])(wss?://[^'"]+)#
|
||||
"$1".®ister_link_websocket(
|
||||
$2, $s, $auth, \%websocket_links)#egi;
|
||||
s#(['"])(wss?:\\/\\/[^'"]+)#
|
||||
"$1".®ister_link_websocket(
|
||||
$2, $s, $auth, \%websocket_links)#egi;
|
||||
if ($dolinks) {
|
||||
s/src='(\/[^']*)'/src='$url$1'/gi;
|
||||
s/src="(\/[^"]*)"/src="$url$1"/gi;
|
||||
s/src=(\/[^ "'>]*)/src=$url$1/gi;
|
||||
s/href='(\/[^']*)'/href='$url$1'/gi;
|
||||
s/href="(\/[^"]*)"/href="$url$1"/gi;
|
||||
s/href=(\/[^ >"']*)/href=$url$1/gi;
|
||||
s/action='(\/[^']*)'/action='$url$1'/gi;
|
||||
s/action="(\/[^"]*)"/action="$url$1"/gi;
|
||||
s/action=(\/[^ "'>]*)/action=$url$1/gi;
|
||||
s/\.location\s*=\s*'(\/[^']*)'/.location='$url$1'/gi;
|
||||
s/\.location\s*=\s*"(\/[^']*)"/.location="$url$1"/gi;
|
||||
s/window.open\("(\/[^"]*)"/window.open\("$url$1"/gi;
|
||||
s/name=return\s+value="(\/[^"]*)"/name=return value="$url$1"/gi;
|
||||
s/param\s+name=config\s+value='(\/[^']*)'/param name=config value='$url$1'/gi;
|
||||
s/param\s+name=config\s+value="(\/[^']*)"/param name=config value="$url$1"/gi;
|
||||
s/param\s+name=config\s+value=(\/[^']*)/param name=config value=$url$1/gi;
|
||||
}
|
||||
print;
|
||||
if (/<applet.*archive=file.jar.*>/) {
|
||||
# Remote webmin file manager applet - give it the
|
||||
if ($dolinks && /<applet.*archive=file.jar.*>/) {
|
||||
# Remote webmin file manager applet - give it the
|
||||
# session ID on *this* system
|
||||
print "<param name=session value=\"$main::session_id\">\n";
|
||||
}
|
||||
@@ -237,3 +253,100 @@ else {
|
||||
}
|
||||
&close_http_connection($con);
|
||||
|
||||
# register_link_websocket(url, &server, auth, \%cache)
|
||||
# Registers a local miniserv websocket proxy for a websocket URL generated by
|
||||
# the linked Webmin server, and returns the local URL for the browser to use.
|
||||
sub register_link_websocket
|
||||
{
|
||||
my ($wsurl, $s, $auth, $cache) = @_;
|
||||
# JSON script responses can encode wss:// as wss:\/\/. Normalize before
|
||||
# matching, and restore the escaping style for the returned URL below.
|
||||
my $escaped_slashes = $wsurl =~ /\\\//;
|
||||
$wsurl =~ s#\\/#/#g;
|
||||
my $host = $s->{'host'};
|
||||
my $port = $s->{'port'};
|
||||
my $ssl = $s->{'ssl'};
|
||||
my $proto = $ssl ? "wss" : "ws";
|
||||
my ($url_proto, $url_host, $url_port, $remote_path) =
|
||||
$wsurl =~ /^(wss?):\/\/(\[[^\]]+\]|[^:\/]+)(?::(\d+))?(\/[^'"]*)$/i;
|
||||
return $wsurl if (!$remote_path || lc($url_proto) ne $proto);
|
||||
return $wsurl if ($url_host =~ /[\s\x00-\x1f\x7f]/ ||
|
||||
$remote_path =~ /[\s\x00-\x1f\x7f]/);
|
||||
my $url_host_cmp = lc($url_host);
|
||||
$url_host_cmp =~ s/^\[|\]$//g;
|
||||
my @valid_hosts = grep { defined($_) && $_ ne "" } ($host, $s->{'ip'});
|
||||
my $link_prefix = &get_webprefix()."/$module_name/link.cgi/$s->{'id'}";
|
||||
my $from_link_path = $remote_path =~ s/^\Q$link_prefix\E//;
|
||||
# Only websocket URLs owned by the linked server are proxied. Some themes
|
||||
# build them with the link.cgi prefix already present; strip that prefix
|
||||
# before registering the backend path.
|
||||
return $wsurl if (!$from_link_path &&
|
||||
!grep { lc($_) eq $url_host_cmp } @valid_hosts);
|
||||
return $wsurl if (!$from_link_path && defined($url_port) &&
|
||||
$url_port != $port);
|
||||
my ($remote_port) = $remote_path =~ /\/ws-(\d+)(?:\?|$)/;
|
||||
return $wsurl if (!$remote_port);
|
||||
# Reuse the same local URL when a response repeats the same backend websocket.
|
||||
# Otherwise the later rewrite would replace the config token for the earlier URL.
|
||||
my $cache_key = join("\0", $s->{'id'}, $remote_path);
|
||||
if ($cache && $cache->{$cache_key}) {
|
||||
my $rv = $cache->{$cache_key};
|
||||
$rv =~ s#/#\\/#g if ($escaped_slashes);
|
||||
return $rv;
|
||||
}
|
||||
|
||||
my $token = &generate_miniserv_websocket_token();
|
||||
my $wspath = "/$module_name/ws-link-$s->{'id'}-$remote_port-$token";
|
||||
my $now = time();
|
||||
my $backend_host = $s->{'ip'} || $host;
|
||||
my $defport = $ssl ? 443 : 80;
|
||||
# If the URL was already routed through this parent link.cgi, its host is the
|
||||
# parent server. Backend Host, Origin and TLS checks must use the child server.
|
||||
my $hostheader = $from_link_path ? $host : $url_host;
|
||||
$hostheader .= ":".$port if ($port != $defport);
|
||||
my $origin = ($ssl ? "https" : "http")."://".$hostheader;
|
||||
my $checkssl = $s->{'checkssl'} ? 1 : 0;
|
||||
my %miniserv;
|
||||
&lock_file(&get_miniserv_config_file());
|
||||
&get_miniserv_config(\%miniserv);
|
||||
$miniserv{"websockets_$wspath"} =
|
||||
"host=$backend_host port=$port ssl=$ssl wspath=$remote_path ".
|
||||
"hostheader=$hostheader origin=$origin auth=basic:$auth ".
|
||||
"checkssl=$checkssl nokey=1 allow_basic_ws=1 user=$main::remote_user ".
|
||||
"token=$token time=$now";
|
||||
&put_miniserv_config(\%miniserv);
|
||||
&unlock_file(&get_miniserv_config_file());
|
||||
&reload_miniserv();
|
||||
|
||||
# Pass the fresh token directly. Re-reading miniserv.conf here can race the
|
||||
# config cache and return a stale token for the same ws-link path.
|
||||
my $rv = &get_miniserv_websocket_url(
|
||||
undef, undef, $module_name, $wspath, $token);
|
||||
$cache->{$cache_key} = $rv if ($cache);
|
||||
$rv =~ s#/#\\/#g if ($escaped_slashes);
|
||||
return $rv;
|
||||
}
|
||||
|
||||
# cleanup_link_websockets()
|
||||
# Removes abandoned websocket proxy routes created for linked Webmin servers.
|
||||
# Routes opened by the browser are removed by miniserv when consumed.
|
||||
sub cleanup_link_websockets
|
||||
{
|
||||
my %miniserv;
|
||||
my $now = time();
|
||||
my $link_ttl = 5*60;
|
||||
my $changed = 0;
|
||||
&lock_file(&get_miniserv_config_file());
|
||||
&get_miniserv_config(\%miniserv);
|
||||
foreach my $k (keys %miniserv) {
|
||||
next if ($k !~ /^websockets_\/\Q$module_name\E\/ws-link-/);
|
||||
my ($time) = $miniserv{$k} =~ /\btime=(\d+)/;
|
||||
if (!$time || $now - $time > $link_ttl) {
|
||||
delete($miniserv{$k});
|
||||
$changed++;
|
||||
}
|
||||
}
|
||||
&put_miniserv_config(\%miniserv) if ($changed);
|
||||
&unlock_file(&get_miniserv_config_file());
|
||||
&reload_miniserv() if ($changed);
|
||||
}
|
||||
|
||||
@@ -4,3 +4,4 @@ desc=Webmin Servers Index
|
||||
longdesc=Displays an index of other Webmin servers for easy linking.
|
||||
readonly=1
|
||||
depends=mailboxes cron
|
||||
websockets=1
|
||||
|
||||
47
t/miniserv.t
47
t/miniserv.t
@@ -1245,6 +1245,30 @@ EOF
|
||||
ok(!exists $got{'# this is a comment'}, 'comment lines skipped');
|
||||
};
|
||||
|
||||
# lock_config_file — matches Webmin's .lock convention without loading web-lib
|
||||
subtest 'lock_config_file' => sub {
|
||||
require File::Temp;
|
||||
my ($fh, $path) = File::Temp::tempfile(UNLINK => 1);
|
||||
close($fh);
|
||||
|
||||
ok(miniserv::lock_config_file($path), 'lock succeeds');
|
||||
ok(-e "$path.lock", 'sidecar lock file created');
|
||||
|
||||
open(my $locking, '<', "$path.lock") or die "open lock: $!";
|
||||
chomp(my $pid = <$locking>);
|
||||
close($locking);
|
||||
is($pid, $$, 'lock records the current PID');
|
||||
|
||||
miniserv::unlock_config_file($path);
|
||||
ok(!-e "$path.lock", 'unlock removes sidecar lock file');
|
||||
|
||||
open($locking, '>', "$path.lock") or die "create stale lock: $!";
|
||||
print $locking "999999999\n";
|
||||
close($locking);
|
||||
ok(miniserv::lock_config_file($path), 'stale lock is reclaimed');
|
||||
miniserv::unlock_config_file($path);
|
||||
};
|
||||
|
||||
# read_any_file — basic file reader; returns undef on open failure
|
||||
subtest 'read_any_file' => sub {
|
||||
require File::Temp;
|
||||
@@ -1287,7 +1311,7 @@ EOF
|
||||
subtest 'parse_websockets_config' => sub {
|
||||
no warnings 'once';
|
||||
local %miniserv::config = (
|
||||
'websockets_/chat' => 'host=back.example.com port=9000 proto=ws',
|
||||
'websockets_/chat' => 'host=back.example.com port=9000 proto=ws ssl=1 checkssl=1 backend_session=abc123 allow_basic_ws=1',
|
||||
'unrelated_key' => 'ignored',
|
||||
);
|
||||
local @miniserv::websocket_paths = ();
|
||||
@@ -1299,6 +1323,27 @@ subtest 'parse_websockets_config' => sub {
|
||||
is($ws->{host}, 'back.example.com', 'host kv parsed');
|
||||
is($ws->{port}, '9000', 'port kv parsed');
|
||||
is($ws->{proto}, 'ws', 'proto kv parsed');
|
||||
is($ws->{ssl}, '1', 'ssl kv parsed');
|
||||
is($ws->{checkssl}, '1', 'SSL check kv parsed');
|
||||
is($ws->{backend_session}, 'abc123', 'backend session kv parsed');
|
||||
is($ws->{allow_basic_ws}, '1', 'Basic auth websocket flag parsed');
|
||||
};
|
||||
|
||||
# find_websocket_config
|
||||
subtest 'find_websocket_config' => sub {
|
||||
no warnings 'once';
|
||||
local %miniserv::config = ( 'redirect_prefix' => '/prefix' );
|
||||
local @miniserv::websocket_paths = (
|
||||
{ 'path' => '/chat' },
|
||||
);
|
||||
|
||||
my ($ws, $simple) = miniserv::find_websocket_config('/prefix/chat');
|
||||
is($ws->{path}, '/chat', 'matches after redirect prefix stripping');
|
||||
is($simple, '/chat', 'returns canonical websocket path');
|
||||
($ws, $simple) = miniserv::find_websocket_config('/prefix/chat?token=abc');
|
||||
is($ws->{path}, '/chat', 'matches request path with query string');
|
||||
is($simple, '/chat', 'drops query string from canonical path');
|
||||
ok(!miniserv::find_websocket_config('/missing'), 'unknown path rejected');
|
||||
};
|
||||
|
||||
# get_user_details — local-files branch (skips the userdb code path entirely)
|
||||
|
||||
@@ -14294,11 +14294,11 @@ if (!$token) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
# allocate_miniserv_websocket([module], [base-remote-user])
|
||||
# allocate_miniserv_websocket([module], [base-remote-user], [backend-session])
|
||||
# Allocate a new websocket and stores it miniserv.conf file
|
||||
sub allocate_miniserv_websocket
|
||||
{
|
||||
my ($module, $buser) = @_;
|
||||
my ($module, $buser, $backend_session) = @_;
|
||||
$module ||= $module_name;
|
||||
# Find ports already in use
|
||||
&lock_file(&get_miniserv_config_file());
|
||||
@@ -14326,40 +14326,67 @@ my $now = time();
|
||||
my $token = &generate_miniserv_websocket_token();
|
||||
my $opt_buser = "";
|
||||
$opt_buser = " buser=$buser" if (defined($buser) && $buser eq $base_remote_user);
|
||||
my $opt_backend = "";
|
||||
# Some websocket backends are reached with Basic auth and no browser session
|
||||
# cookie. Store the one-time backend session key that the child server expects.
|
||||
$opt_backend = " backend_session=$backend_session"
|
||||
if (defined($backend_session) && $backend_session =~ /^\S+$/);
|
||||
my $opt_basic = "";
|
||||
$opt_basic = " allow_basic_ws=1" if ($opt_backend);
|
||||
$miniserv{"websockets_$wspath"} = "host=127.0.0.1 port=$port wspath=/ ".
|
||||
"user=$remote_user$opt_buser token=$token time=$now";
|
||||
"user=$remote_user$opt_buser$opt_backend$opt_basic token=$token time=$now";
|
||||
&put_miniserv_config(\%miniserv);
|
||||
&unlock_file(&get_miniserv_config_file());
|
||||
&reload_miniserv();
|
||||
return $port;
|
||||
}
|
||||
|
||||
# get_miniserv_websocket_url(port, [host], [module])
|
||||
# Returns the URL for a websocket
|
||||
# get_miniserv_websocket_url(port, [host], [module], [path], [token])
|
||||
# Returns the browser-visible URL for a websocket. The optional path/token
|
||||
# arguments are used by linked-server websocket proxy routes, whose path does
|
||||
# not follow the normal /module/ws-port form.
|
||||
sub get_miniserv_websocket_url
|
||||
{
|
||||
my ($port, $host, $module) = @_;
|
||||
my ($port, $host, $module, $path, $wstoken) = @_;
|
||||
$module ||= $module_name;
|
||||
my $ws_proto = lc($ENV{'HTTPS'}) eq 'on' ? 'wss' : 'ws';
|
||||
my %miniserv;
|
||||
my $webprefix = &get_webprefix();
|
||||
&get_miniserv_config(\%miniserv);
|
||||
my $trust_proxy = $miniserv{'trust_real_ip'};
|
||||
my $wspath = "/$module/ws-".$port;
|
||||
my $wstoken;
|
||||
if ($miniserv{'websockets_'.$wspath} &&
|
||||
my $default_ws_proto = lc($ENV{'HTTPS'}) eq 'on' ? 'wss' : 'ws';
|
||||
my $ws_proto;
|
||||
# Match the public browser scheme when Webmin is behind a trusted reverse
|
||||
# proxy, but only allow websocket schemes into the returned URL.
|
||||
if ($trust_proxy && $ENV{'HTTP_X_FORWARDED_PROTO'}) {
|
||||
$ws_proto = (split(/\s*,\s*/, $ENV{'HTTP_X_FORWARDED_PROTO'}))[0];
|
||||
$ws_proto =~ s/^\s+|\s+$//g;
|
||||
}
|
||||
if (!$ws_proto && $trust_proxy && $ENV{'HTTP_FORWARDED'} &&
|
||||
(split(/\s*,\s*/, $ENV{'HTTP_FORWARDED'}))[0] =~
|
||||
/(?:^|;)\s*proto="?([^";]+)"?/i) {
|
||||
$ws_proto = $1;
|
||||
}
|
||||
$ws_proto ||= $default_ws_proto;
|
||||
$ws_proto = lc($ws_proto);
|
||||
$ws_proto = 'wss' if ($ws_proto eq 'https');
|
||||
$ws_proto = 'ws' if ($ws_proto eq 'http');
|
||||
$ws_proto = $default_ws_proto if ($ws_proto ne 'wss' && $ws_proto ne 'ws');
|
||||
my $wspath = $path || "/$module/ws-".$port;
|
||||
# If the caller already generated the token, use it directly; otherwise fall
|
||||
# back to reading the token stored for the normal allocated websocket route.
|
||||
if (!defined($wstoken) && $miniserv{'websockets_'.$wspath} &&
|
||||
$miniserv{'websockets_'.$wspath} =~ /\btoken=(\S+)/) {
|
||||
$wstoken = $1;
|
||||
}
|
||||
my $http_host_conf = &trim($miniserv{'websocket_host'} || $host);
|
||||
# Pass as defined
|
||||
# Prefer the explicit websocket host when configured
|
||||
if ($http_host_conf) {
|
||||
if ($http_host_conf !~ /^wss?:\/\//) {
|
||||
$http_host_conf = "$ws_proto://$http_host_conf";
|
||||
}
|
||||
$http_host_conf =~ s/[\/]+$//g;
|
||||
}
|
||||
# Try to rely on the proxy
|
||||
# Otherwise use trusted proxy headers when available
|
||||
if ($trust_proxy && !defined($http_host_conf)) {
|
||||
my $forwarded_host = $ENV{'HTTP_X_FORWARDED_HOST'};
|
||||
if ($forwarded_host) {
|
||||
@@ -14402,19 +14429,26 @@ sub cleanup_miniserv_websockets
|
||||
my ($skip, $module) = @_;
|
||||
$skip ||= [ ];
|
||||
$module ||= $module_name;
|
||||
my $link_ttl = 5*60;
|
||||
&lock_file(&get_miniserv_config_file());
|
||||
my %miniserv;
|
||||
&get_miniserv_config(\%miniserv);
|
||||
my $now = time();
|
||||
my @clean;
|
||||
foreach my $k (keys %miniserv) {
|
||||
$k =~ /^websockets_\/$module\/ws-(\d+)$/ || next;
|
||||
my $port = $1;
|
||||
next if (&indexof($port, @$skip) >= 0);
|
||||
my $when = 0;
|
||||
if ($miniserv{$k} =~ /time=(\d+)/) {
|
||||
$when = $1;
|
||||
}
|
||||
if ($k =~ /^websockets_\/\Q$module\E\/ws-link-/) {
|
||||
# Linked-server websocket routes carry a backend credential and are
|
||||
# single-use. If the browser never opens them, expire them by age.
|
||||
push(@clean, $k) if (!$when || $now - $when > $link_ttl);
|
||||
next;
|
||||
}
|
||||
$k =~ /^websockets_\/\Q$module\E\/ws-(\d+)$/ || next;
|
||||
my $port = $1;
|
||||
next if (&indexof($port, @$skip) >= 0);
|
||||
if ($now - $when > 60) {
|
||||
# Has been open for a while, check if the port is still in use?
|
||||
my $err;
|
||||
|
||||
@@ -8,11 +8,9 @@ our $unsafe_index_cgi = 1;
|
||||
require './xterm-lib.pl'; ## no critic
|
||||
our (%in, %text, %config, %gconfig, %access, %module_info,
|
||||
$module_name, $module_config_directory, $module_var_directory,
|
||||
$remote_user);
|
||||
$remote_user, $session_id);
|
||||
ReadParse();
|
||||
|
||||
$ENV{'HTTP_WEBMIN_PATH'} && error($text{'index_eproxy'});
|
||||
|
||||
# Check for needed modules
|
||||
my @modload = (
|
||||
['Digest::SHA', sub { eval { require Digest::SHA; Digest::SHA->import; 1 } }],
|
||||
@@ -181,8 +179,18 @@ ui_print_header(undef, $text{'index_title'}, "", undef, 1, 1, 0, undef,
|
||||
# Print main container
|
||||
print "<div data-label=\"$text{'index_connecting'}\" id=\"terminal\"></div>\n";
|
||||
|
||||
# Get a free port that can be used for the socket
|
||||
my $port = allocate_miniserv_websocket($module_name);
|
||||
# Get a free port that can be used for the socket. Normal browser sessions
|
||||
# are revalidated by miniserv while the websocket stays open. Proxied or
|
||||
# Basic-auth requests may not have a session cookie, so only those routes get
|
||||
# a one-time backend handshake secret.
|
||||
my $websocket_session_id = $session_id;
|
||||
my $backend_session;
|
||||
if (!$websocket_session_id) {
|
||||
$websocket_session_id = generate_miniserv_websocket_token();
|
||||
$backend_session = $websocket_session_id;
|
||||
}
|
||||
my $port = allocate_miniserv_websocket(
|
||||
$module_name, undef, $backend_session);
|
||||
|
||||
# Decide which Unix account the terminal will run as
|
||||
my $user = resolve_shell_user(\%access, $remote_user, \%in, \%config);
|
||||
@@ -197,7 +205,10 @@ my $shellserver_cmd = "$module_config_directory/shellserver.pl";
|
||||
if (!-r $shellserver_cmd) {
|
||||
create_wrapper($shellserver_cmd, $module_name, "shellserver.pl");
|
||||
}
|
||||
$ENV{'SESSION_ID'} = $main::session_id;
|
||||
# shellserver.pl validates the backend websocket key against SESSION_ID. For
|
||||
# normal sessions miniserv forwards the browser session; no-cookie routes use
|
||||
# the one-time backend_session stored in miniserv.conf above.
|
||||
$ENV{'SESSION_ID'} = $websocket_session_id;
|
||||
system_logged($shellserver_cmd." ".quotemeta($port)." ".quotemeta($user).
|
||||
($dir ? " ".quotemeta($dir) : "").
|
||||
" >$module_var_directory/websocket-connection-$port.out 2>&1 </dev/null");
|
||||
@@ -210,8 +221,8 @@ my $term_script = <<EOF;
|
||||
(function() {
|
||||
const socket = new WebSocket('$url', 'binary'),
|
||||
termcont = document.getElementById('terminal'),
|
||||
err_conn_cannot = 'Cannot connect to the socket $url',
|
||||
err_conn_lost = 'Connection to the socket $url lost',
|
||||
err_conn_cannot = 'Cannot connect to the socket ' + socket.url,
|
||||
err_conn_lost = 'Connection to the socket ' + socket.url + ' lost',
|
||||
webGLAddonLink = '$webGLAddon',
|
||||
detectWebGLContext = (function() {
|
||||
const canvas = document.createElement("canvas"),
|
||||
|
||||
Reference in New Issue
Block a user