From d42a6dc725f90c41a4452dd06807eeb57da41618 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 22 Jun 2026 23:01:46 +0200 Subject: [PATCH] Fix parent-prefixed linked websocket rewrites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Correct linked-server WebSocket proxy registration for parent-prefixed URLs, rebuild backend Host/Origin from the child server, and prevent duplicate rewrites from invalidating tokens. --- servers/link.cgi | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/servers/link.cgi b/servers/link.cgi index aed9f5df3..0879f73a2 100755 --- a/servers/link.cgi +++ b/servers/link.cgi @@ -197,13 +197,18 @@ if ($header->{'content-type'} && # (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)) { # 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)#egi; - s#(['"])(wss?:\\/\\/[^'"]+)#"$1".®ister_link_websocket($2, $s, $auth)#egi; + 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; @@ -253,7 +258,7 @@ else { # the linked Webmin server, and returns the local URL for the browser to use. sub register_link_websocket { -my ($wsurl, $s, $auth) = @_; +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 =~ /\\\//; @@ -277,17 +282,28 @@ my $from_link_path = $remote_path =~ s/^\Q$link_prefix\E//; # 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"; +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; -my $hostheader = $url_host; -$hostheader .= ":".(defined($url_port) ? $url_port : $port) - if ((defined($url_port) ? $url_port : $port) != $defport); +# 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; @@ -306,6 +322,7 @@ $miniserv{"websockets_$wspath"} = # 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; }