| Server IP : 167.99.254.82 / Your IP : 216.73.216.188 Web Server : Apache System : Linux host.techisquad.com 5.15.0-190-generic #200-Ubuntu SMP Fri Aug 7 15:06:04 UTC 2026 x86_64 User : pawluxera ( 1011) PHP Version : 8.1.34 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /usr/share/webmin/virtual-server/ |
Upload File : |
#!/usr/bin/perl
# link.cgi
# Serve a read-only preview of a locally hosted website inside Webmin.
BEGIN { push(@INC, ".."); };
eval "use WebminCore;";
if ($@) {
do '../web-lib.pl';
}
# parse_preview_request(pathinfo)
# Parse PATH_INFO into a normalized preview or proxy request hash.
sub parse_preview_request
{
my ($pathinfo) = @_;
my %req = ( 'mode' => 'preview' );
$pathinfo ||= '';
$req{'mode'} = 'proxy' if ($pathinfo =~ s/^\/proxy(?=\/)//);
if ($pathinfo =~ /^\/([0-9\.]+)\/(http|https):\/+([^:\/]+)(:(\d+))?(.*)$/) {
# Preview a local site by explicit IP and URL.
$req{'ip'} = $1;
$req{'protocol'} = $2;
$req{'ssl'} = $2 eq "https";
$req{'host'} = $3;
$req{'port'} = $5 || ($req{'ssl'} ? 443 : 80);
$req{'path'} = $6;
$req{'path'} =~ s/ /%20/g;
$req{'openurl'} = "$2://$3$4$6";
$req{'baseurl'} = "$2://$3$4";
}
elsif ($pathinfo =~ /^\/(http|https):\/+([^:\/]+)(:(\d+))?(.*)$/) {
# Keep support for older off-site style URLs.
$req{'protocol'} = $1;
$req{'ssl'} = $1 eq "https";
$req{'host'} = $2;
$req{'port'} = $4 || ($req{'ssl'} ? 443 : 80);
$req{'path'} = $5;
$req{'path'} =~ s/ /%20/g;
$req{'openurl'} = "$1://$2$3$5";
$req{'baseurl'} = "$1://$2$3";
$req{'ip'} = &to_ipaddress($2);
}
else {
&error("Bad PATH_INFO : $pathinfo");
}
if ($ENV{'QUERY_STRING'}) {
$req{'path'} .= '?'.$ENV{'QUERY_STRING'};
}
elsif (@ARGV) {
$req{'path'} .= '?'.join('+', @ARGV);
}
if ($req{'host'} =~ /^www\.(.*)$/) {
$req{'althost'} = $1;
}
else {
$req{'althost'} = "www.$req{'host'}";
}
$req{'linkurl'} = "/$module_name/link.cgi/proxy/$req{'ip'}/";
$req{'url'} = $req{'linkurl'}.$req{'openurl'};
$req{'url'} .= '?'.$ENV{'QUERY_STRING'} if ($ENV{'QUERY_STRING'});
$req{'url'} .= '?'.join('+', @ARGV)
if (!$ENV{'QUERY_STRING'} && @ARGV);
return \%req;
}
# require_local_preview_ip(ip)
# Reject preview targets that are not bound to a local system IP.
sub require_local_preview_ip
{
my ($ip) = @_;
open(my $fh, "<$module_config_directory/localips") ||
&error("Failed to read local IP addresses");
chop(my @localips = <$fh>);
close($fh);
&indexof($ip, @localips) >= 0 ||
&error("Connections to IP addresses not on this system are ".
"not allowed : $ip");
}
# open_target_connection(req, method)
# Open the upstream site connection, with a fallback to the alternate host.
sub open_target_connection
{
my ($req, $method) = @_;
my $oldproxy = $gconfig{'http_proxy'};
my $con;
my $httphost = $req->{'host'};
$gconfig{'http_proxy'} = '';
$con = &make_http_connection(
$req->{'ip'}, $req->{'port'}, $req->{'ssl'}, $method,
$req->{'path'}, undef, undef,
{ 'host' => $req->{'host'},
'checkhost' => $req->{'host'} });
if (!ref($con)) {
# Some sites answer only on the www/non-www alternate host.
$httphost = $req->{'althost'};
$con = &make_http_connection(
$req->{'ip'}, $req->{'port'}, $req->{'ssl'}, $method,
$req->{'path'}, undef, undef,
{ 'host' => $req->{'althost'},
'checkhost' => $req->{'althost'} });
}
$gconfig{'http_proxy'} = $oldproxy;
return ($con, $httphost);
}
# send_target_request(con, httphost, body)
# Forward the browser request metadata and optional body upstream.
sub send_target_request
{
my ($con, $httphost, $body) = @_;
my $cl = length($body);
&write_http_connection($con, "Host: $httphost\r\n");
&write_http_connection($con, "User-agent: Webmin\r\n");
&write_http_connection($con, sprintf(
"Webmin-servers: %s://%s:%d/$module_name/\r\n",
$ENV{'HTTPS'} eq "ON" ? "https" : "http",
$ENV{'SERVER_NAME'}, $ENV{'SERVER_PORT'}));
&write_http_connection($con, "Content-length: $cl\r\n") if ($cl);
&write_http_connection($con, "Content-type: $ENV{'CONTENT_TYPE'}\r\n")
if ($ENV{'CONTENT_TYPE'});
&write_http_connection($con, "\r\n");
&write_http_connection($con, $body) if ($cl);
}
# read_target_headers(con)
# Read the upstream status line and response headers.
sub read_target_headers
{
my ($con) = @_;
my $status = &read_http_connection($con);
my (%header, $headers);
while (1) {
(my $line = &read_http_connection($con)) =~ s/\r|\n//g;
last if (!$line);
$line =~ /^(\S+):\s+(.*)$/ || &error("Bad header");
$header{lc($1)} = $2;
$headers .= $line."\n";
}
return ($status, \%header, $headers);
}
# read_browser_request_body()
# Read the incoming browser request body, if one was sent.
sub read_browser_request_body
{
my $cl = $ENV{'CONTENT_LENGTH'} || 0;
my $body = '';
if ($cl) {
if (defined(&read_fully)) {
&read_fully(STDIN, \$body, $cl);
}
else {
read(STDIN, $body, $cl);
}
}
return $body;
}
# is_basic_auth_challenge(status, header)
# Return true when the upstream response requests HTTP Basic Authentication.
sub is_basic_auth_challenge
{
my ($status, $header) = @_;
return $status =~ /\s401(?:\s|$)/ &&
$header->{'www-authenticate'} =~ /basic/i;
}
# preflight_preview_request(req)
# Probe the target before rendering the wrapper so early errors reach the user.
sub preflight_preview_request
{
my ($req) = @_;
my ($con, $httphost) = &open_target_connection($req, "GET");
&error($con) if (!ref($con));
&send_target_request($con, $httphost, '');
my ($status, $header) = &read_target_headers($con);
&close_http_connection($con);
&error(&text('links_preview_basic'))
if (&is_basic_auth_challenge($status, $header));
}
# print_preview_wrapper(req)
# Render the simple preview page with a banner and sandboxed iframe.
sub print_preview_wrapper
{
my ($req) = @_;
my $style = ':root { color-scheme: light dark; --preview-bg: #fff; } '.
'@media (prefers-color-scheme: dark) { '.
':root { --preview-bg: #111; } } '.
'html, body { margin: 0; padding: 0; height: 100%; '.
'background: var(--preview-bg); } '.
'body { font-family: sans-serif; display: grid; '.
'grid-template-rows: 30px 1fr; } '.
'.preview-banner { height: 30px; padding: 0 14px; '.
'background: #961602; font-weight: bold; color: #fff4f5; '.
'font-size: 12px; line-height: 30px; text-align: center; '.
'white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } '.
'iframe { display: block; width: 100%; height: 100%; border: 0; '.
'background: var(--preview-bg); min-height: 0; }';
print "Content-type: text/html; charset=UTF-8\n";
print "X-Content-Type-Options: nosniff\n";
print "Content-Security-Policy: default-src 'none'; style-src ".
"'unsafe-inline'; frame-src 'self'; frame-ancestors 'self'; ".
"base-uri 'none'; form-action 'none'\n\n";
print &ui_tag('html',
&ui_tag('head',
&ui_tag('meta', undef, { 'charset' => 'utf-8' }).
&ui_tag('meta', undef, {
'name' => 'viewport',
'content' => 'width=device-width, initial-scale=1',
}).
&ui_tag('title', &html_escape($text{'links_website'})).
&ui_tag('style', $style)
).
&ui_tag('body',
&ui_tag('div',
&html_escape($text{'links_preview_note'}),
{ 'class' => 'preview-banner' }).
&ui_tag('iframe', undef, {
'src' => $req->{'url'},
'title' => $text{'links_website'},
'sandbox' => 'allow-scripts',
'loading' => 'eager',
})
));
}
# rewrite_proxy_redirect(req, header)
# Translate upstream redirects back into preview proxy URLs when possible.
sub rewrite_proxy_redirect
{
my ($req, $header) = @_;
my $location = $header->{'location'} || return undef;
my $defport = $req->{'ssl'} ? 443 : 80;
if ($location =~ /^(http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E):\Q$req->{'port'}\E(.*)$/) {
return $req->{'linkurl'}.$location;
}
if ($req->{'port'} == $defport &&
$location =~ /^(http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)(\/.*)$/) {
return $req->{'linkurl'}.$location;
}
if ($location =~ /^(\/\S+)$/) {
return $req->{'linkurl'}.$req->{'baseurl'}.$1;
}
return undef;
}
# sanitize_proxy_headers(headers, header)
# Drop or replace upstream headers that are unsafe or no longer accurate.
sub sanitize_proxy_headers
{
my ($headers, $header) = @_;
# Drop headers that do not make sense once Webmin owns the response.
$headers =~ s/^Set-Cookie:.*\n//gmi;
$headers =~ s/^Location:.*\n//gmi;
if ($header->{'content-type'} =~ /text\/html/i) {
$headers =~ s/^Content-Length:.*\n//gmi;
$headers =~ s/^X-Frame-Options:.*\n//gmi;
$headers .= "Content-Security-Policy: sandbox allow-scripts\n";
}
return $headers;
}
# preview_blocker_markup()
# Return the injected CSS and JS that keeps preview content non-interactive.
sub preview_blocker_markup
{
return qq|<style>a, area, button, input[type=submit], input[type=button], input[type=image] { cursor: not-allowed !important; }</style>| .
qq~<script>(function(){function block(event){var target = event.target && event.target.closest ? event.target.closest('a, area, button, input[type="submit"], input[type="button"], input[type="image"]') : null;if(target){event.preventDefault();event.stopPropagation();}}document.addEventListener('click', block, true);document.addEventListener('submit', function(event){event.preventDefault();event.stopPropagation();}, true);document.addEventListener('keydown', function(event){if ((event.key === 'Enter' || event.key === ' ') && event.target && event.target.closest && event.target.closest('a, area, button')) { event.preventDefault(); event.stopPropagation(); }}, true);})();</script>~;
}
# rewrite_html_chunk(req, chunk, injected_ref)
# Rewrite local asset URLs and inject the preview blocker into HTML once.
sub rewrite_html_chunk
{
my ($req, $chunk, $injected_ref) = @_;
if (!$$injected_ref) {
# Inject once into the document body or head, without disturbing doctype.
my $blocker = &preview_blocker_markup();
if ($chunk =~ s/(<head\b[^>]*>)/$1$blocker/i ||
$chunk =~ s/(<body\b[^>]*>)/$1$blocker/i) {
$$injected_ref = 1;
}
}
$chunk =~ s/src='(\/[^']*)'/src='$req->{'linkurl'}$req->{'baseurl'}$1'/gi;
$chunk =~ s/src="(\/[^"]*)"/src="$req->{'linkurl'}$req->{'baseurl'}$1"/gi;
$chunk =~ s/src=(\/[^ "'>]*)/src=$req->{'linkurl'}$req->{'baseurl'}$1/gi;
$chunk =~ s/src='((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^']*)'/src='$req->{'linkurl'}$1'/gi;
$chunk =~ s/src="((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^"]*)"/src="$req->{'linkurl'}$1"/gi;
$chunk =~ s/src=((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^ "'>]*)/src=$req->{'linkurl'}$1/gi;
$chunk =~ s/href='(\/[^']*)'/href='$req->{'linkurl'}$req->{'baseurl'}$1'/gi;
$chunk =~ s/href="(\/[^"]*)"/href="$req->{'linkurl'}$req->{'baseurl'}$1"/gi;
$chunk =~ s/href=(\/[^ "'>]*)/href=$req->{'linkurl'}$req->{'baseurl'}$1/gi;
$chunk =~ s/href='((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^']*)'/href='$req->{'linkurl'}$1'/gi;
$chunk =~ s/href="((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^"]*)"/href="$req->{'linkurl'}$1"/gi;
$chunk =~ s/href=((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^ "'>]*)/href=$req->{'linkurl'}$1/gi;
$chunk =~ s/action='(\/[^']*)'/action='$req->{'linkurl'}$req->{'baseurl'}$1'/gi;
$chunk =~ s/action="(\/[^"]*)"/action="$req->{'linkurl'}$req->{'baseurl'}$1"/gi;
$chunk =~ s/action=(\/[^ "'>]*)/action=$req->{'linkurl'}$req->{'baseurl'}$1/gi;
$chunk =~ s/action='((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^']*)'/action='$req->{'linkurl'}$1'/gi;
$chunk =~ s/action="((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^"]*)"/action="$req->{'linkurl'}$1"/gi;
$chunk =~ s/action=((http|https):\/\/(\Q$req->{'host'}\E|\Q$req->{'althost'}\E)[^ "'>]*)/action=$req->{'linkurl'}$1/gi;
$chunk =~ s/\@import '(\/[^']*)'/\@import '$req->{'linkurl'}$req->{'baseurl'}$1'/gi;
$chunk =~ s/\@import "(\/[^']*)"/\@import "$req->{'linkurl'}$req->{'baseurl'}$1"/gi;
return $chunk;
}
&init_config();
delete($ENV{'HTTP_REFERER'});
$| = 1;
my $req = &parse_preview_request($ENV{'PATH_INFO'});
&require_local_preview_ip($req->{'ip'});
if ($req->{'mode'} eq 'preview') {
# Fail early in the main request before printing any wrapper HTML.
&preflight_preview_request($req);
&print_preview_wrapper($req);
exit;
}
my $body = &read_browser_request_body();
my ($con, $httphost) = &open_target_connection($req, $ENV{'REQUEST_METHOD'});
&error($con) if (!ref($con));
&send_target_request($con, $httphost, $body);
my ($status, $header, $headers) = &read_target_headers($con);
if (&is_basic_auth_challenge($status, $header)) {
&close_http_connection($con);
&error(&text('links_preview_basic'));
}
if (my $redirect = &rewrite_proxy_redirect($req, $header)) {
&close_http_connection($con);
&redirect($redirect);
exit;
}
$headers = &sanitize_proxy_headers($headers, $header);
print $headers;
print "\n";
if ($header->{'content-type'} =~ /text\/html/ && !$header->{'x-no-links'}) {
my $injected = 0;
while (defined(my $chunk = &read_http_connection($con))) {
print &rewrite_html_chunk($req, $chunk, \$injected);
}
}
else {
while (my $chunk = &read_http_connection($con, 1024)) {
print $chunk;
}
}
&close_http_connection($con);