mirror of
https://github.com/webmin/webmin.git
synced 2026-06-04 20:30:22 +01:00
Merge branch 'master' of github.com:webmin/webmin
This commit is contained in:
12
.github/workflows/code-review.yml
vendored
Normal file
12
.github/workflows/code-review.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Code Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
code-review:
|
||||
uses: webmin/webmin-ci-cd/.github/workflows/code-review.yml@main
|
||||
secrets:
|
||||
CODE_REVIEW_API_KEY: ${{ secrets.CODE_REVIEW_API_KEY }}
|
||||
19
.github/workflows/tests.yml
vendored
Normal file
19
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
prove:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Perl::Critic
|
||||
run: sudo apt-get update && sudo apt-get install -y libperl-critic-perl
|
||||
- name: prove -lr
|
||||
run: prove -lr
|
||||
1
.github/workflows/webmin.dev+webmin.yml
vendored
1
.github/workflows/webmin.dev+webmin.yml
vendored
@@ -26,3 +26,4 @@ jobs:
|
||||
DEV_SSH_PRV_KEY: ${{ secrets.DEV_SSH_PRV_KEY }}
|
||||
ALL_GPG_PH2: ${{ secrets.ALL_GPG_PH2 }}
|
||||
CODE_REVIEW_API_KEY: ${{ secrets.CODE_REVIEW_API_KEY }}
|
||||
CODE_REVIEW_SMTP_PASSWORD: ${{ secrets.CODE_REVIEW_SMTP_PASSWORD }}
|
||||
|
||||
@@ -1809,6 +1809,7 @@ foreach my $g (&list_groups()) {
|
||||
return $g;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
=head2 check_password_restrictions(username, password)
|
||||
|
||||
@@ -16,8 +16,9 @@ my ($user, $script, $action, $type, $object, $p) = @_;
|
||||
my $g = $type eq 'group' ? "_g" : "";
|
||||
if ($action eq 'modify') {
|
||||
if ($p->{'old'} ne $p->{'name'}) {
|
||||
return &text('log_rename'.$g, "<tt>$p->{'old'}</tt>",
|
||||
"<tt>$p->{'name'}</tt>");
|
||||
return &text('log_rename'.$g,
|
||||
"<tt>".&html_escape($p->{'old'})."</tt>",
|
||||
"<tt>".&html_escape($p->{'name'})."</tt>");
|
||||
}
|
||||
else {
|
||||
return &text('log_modify'.$g,
|
||||
@@ -26,7 +27,8 @@ if ($action eq 'modify') {
|
||||
}
|
||||
elsif ($action eq 'create') {
|
||||
if ($p->{'clone'}) {
|
||||
return &text('log_clone'.$g, "<tt>$p->{'clone'}</tt>",
|
||||
return &text('log_clone'.$g,
|
||||
"<tt>".&html_escape($p->{'clone'})."</tt>",
|
||||
"<tt>".&html_escape($object)."</tt>");
|
||||
}
|
||||
else {
|
||||
@@ -36,21 +38,23 @@ elsif ($action eq 'create') {
|
||||
}
|
||||
elsif ($action eq 'delete') {
|
||||
if ($type eq "users" || $type eq "groups") {
|
||||
return &text('log_delete_'.$type, $object);
|
||||
return &text('log_delete_'.$type, &html_escape($object));
|
||||
}
|
||||
else {
|
||||
return &text('log_delete'.$g, "<tt>$object</tt>");
|
||||
return &text('log_delete'.$g,
|
||||
"<tt>".&html_escape($object)."</tt>");
|
||||
}
|
||||
}
|
||||
elsif ($action eq 'joingroup') {
|
||||
return &text('log_joingroup', $object, $p->{'group'});
|
||||
return &text('log_joingroup', &html_escape($object),
|
||||
&html_escape($p->{'group'}));
|
||||
}
|
||||
elsif ($action eq 'acl') {
|
||||
return &text('log_acl', "<tt>$object</tt>",
|
||||
return &text('log_acl', "<tt>".&html_escape($object)."</tt>",
|
||||
"<i>".&html_escape($p->{'moddesc'})."</i>");
|
||||
}
|
||||
elsif ($action eq 'reset') {
|
||||
return &text('log_reset', "<tt>$object</tt>",
|
||||
return &text('log_reset', "<tt>".&html_escape($object)."</tt>",
|
||||
"<i>".&html_escape($p->{'moddesc'})."</i>");
|
||||
}
|
||||
elsif ($action eq 'cert') {
|
||||
@@ -60,7 +64,9 @@ elsif ($action eq 'switch') {
|
||||
return &text('log_switch', "<tt>".&html_escape($object)."</tt>");
|
||||
}
|
||||
elsif ($action eq 'twofactor') {
|
||||
return &text('log_twofactor', $object, $p->{'provider'}, $p->{'id'});
|
||||
return &text('log_twofactor', &html_escape($object),
|
||||
&html_escape($p->{'provider'}),
|
||||
&html_escape($p->{'id'}));
|
||||
}
|
||||
elsif ($action eq 'forgot') {
|
||||
return &text('log_forgot_'.$type, &html_escape($p->{'user'}),
|
||||
|
||||
1381
acl/t/run-tests.t
Normal file
1381
acl/t/run-tests.t
Normal file
File diff suppressed because it is too large
Load Diff
@@ -436,6 +436,13 @@ foreach $v (@virt) {
|
||||
return \@get_config_cache;
|
||||
}
|
||||
|
||||
# flush_config_cache()
|
||||
# Delete all in-memory config caches
|
||||
sub flush_config_cache
|
||||
{
|
||||
undef(@get_config_cache);
|
||||
}
|
||||
|
||||
# get_config_file(filename, [&seen-files])
|
||||
# Returns a list of config hash refs from some file
|
||||
sub get_config_file
|
||||
@@ -788,6 +795,428 @@ unlink($file);
|
||||
&delete_webfile_link($file);
|
||||
}
|
||||
|
||||
# can_manage_vhost_files()
|
||||
# Returns 1 if this system uses Debian-style available/enabled site dirs
|
||||
sub can_manage_vhost_files
|
||||
{
|
||||
return 0 if ($gconfig{'os_type'} ne 'debian-linux');
|
||||
my $avail = &vhost_available_dir();
|
||||
my $enabled = &vhost_enabled_dir();
|
||||
return $avail && -d $avail && $enabled && -d $enabled &&
|
||||
&simplify_path(&resolve_links($avail)) ne
|
||||
&simplify_path(&resolve_links($enabled));
|
||||
}
|
||||
|
||||
# vhost_available_dir()
|
||||
# Returns the configured directory of available Apache virtual host files
|
||||
sub vhost_available_dir
|
||||
{
|
||||
return $config{'virt_file'} ? &server_root($config{'virt_file'}) : undef;
|
||||
}
|
||||
|
||||
# vhost_enabled_dir()
|
||||
# Returns the configured directory of enabled Apache virtual host symlinks
|
||||
sub vhost_enabled_dir
|
||||
{
|
||||
return $config{'link_dir'} ? &server_root($config{'link_dir'}) : undef;
|
||||
}
|
||||
|
||||
# get_vhost_available_files()
|
||||
# Returns real config files from the directory used for new virtual hosts
|
||||
sub get_vhost_available_files
|
||||
{
|
||||
my @rv;
|
||||
return @rv if (!&can_manage_vhost_files());
|
||||
my $avail = &vhost_available_dir();
|
||||
opendir(AVAIL, $avail) || return @rv;
|
||||
foreach my $f (sort { lc($a) cmp lc($b) } readdir(AVAIL)) {
|
||||
next if ($f eq "." || $f eq "..");
|
||||
my $file = $avail."/".$f;
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
next if (!$rfile || !-f $rfile || !-r $rfile);
|
||||
push(@rv, $rfile);
|
||||
}
|
||||
closedir(AVAIL);
|
||||
return &unique(@rv);
|
||||
}
|
||||
|
||||
# find_virtuals_in_file(file)
|
||||
# Returns VirtualHost blocks parsed from one config file
|
||||
sub find_virtuals_in_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
return ( ) if (!-r $rfile);
|
||||
my @conf = &get_config_file($rfile);
|
||||
return grep { $_->{'file'} eq $rfile }
|
||||
&find_directive_struct("VirtualHost", \@conf);
|
||||
}
|
||||
|
||||
# is_default_vhost(&virt)
|
||||
# Returns 1 if a VirtualHost looks like a default/catch-all host
|
||||
sub is_default_vhost
|
||||
{
|
||||
my ($virt) = @_;
|
||||
return 1 if (!$virt);
|
||||
return 1 if ($virt->{'value'} =~ /_default_/i);
|
||||
return 1 if (!&find_directive("ServerName", $virt->{'members'}));
|
||||
return 0;
|
||||
}
|
||||
|
||||
# can_manage_vhost_file(file)
|
||||
# Returns 1 if all virtual hosts in a file are manageable by this user
|
||||
sub can_manage_vhost_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
return 0 if (!$rfile || !-f $rfile || !-r $rfile);
|
||||
my @virts = &find_virtuals_in_file($rfile);
|
||||
return 0 if (!@virts);
|
||||
foreach my $virt (@virts) {
|
||||
return 0 if (&is_default_vhost($virt));
|
||||
return 0 if (!&can_edit_virt($virt));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
# can_manage_vhost_state_file(file)
|
||||
# Returns 1 if a virtual host file can have its enabled state managed here
|
||||
sub can_manage_vhost_state_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
return 0 if (!$rfile || !-f $rfile);
|
||||
my %available = map { $_, 1 } &get_vhost_available_files();
|
||||
return 0 if (!$available{$rfile});
|
||||
return &can_manage_vhost_file($rfile);
|
||||
}
|
||||
|
||||
# get_virtual_list_rows(&config)
|
||||
# Returns row hashes for the virtual-host list, preserving sites-available order
|
||||
sub get_virtual_list_rows
|
||||
{
|
||||
my ($conf) = @_;
|
||||
my @active = grep { &can_edit_virt($_) }
|
||||
&find_directive_struct("VirtualHost", $conf);
|
||||
if (&can_manage_vhost_files()) {
|
||||
my @rows;
|
||||
my %active_by_file;
|
||||
foreach my $v (@active) {
|
||||
my $file = &simplify_path(&resolve_links($v->{'file'}));
|
||||
$file ||= $v->{'file'};
|
||||
push(@{$active_by_file{$file}}, $v);
|
||||
}
|
||||
my %done_virt;
|
||||
foreach my $file (&get_vhost_available_files()) {
|
||||
my @filevirts = @{$active_by_file{$file} || [ ]};
|
||||
my $active = @filevirts ? 1 : 0;
|
||||
if (!@filevirts) {
|
||||
@filevirts = grep { &can_edit_virt($_) &&
|
||||
!&is_default_vhost($_) }
|
||||
&find_virtuals_in_file($file);
|
||||
}
|
||||
foreach my $v (@filevirts) {
|
||||
push(@rows, { 'virt' => $v,
|
||||
'active' => $active,
|
||||
'file' => $file });
|
||||
$done_virt{$v}++;
|
||||
}
|
||||
}
|
||||
foreach my $v (@active) {
|
||||
next if ($done_virt{$v});
|
||||
push(@rows, { 'virt' => $v,
|
||||
'active' => 1,
|
||||
'file' => $v->{'file'} });
|
||||
}
|
||||
return @rows;
|
||||
}
|
||||
return map { { 'virt' => $_, 'active' => 1, 'file' => $_->{'file'} } }
|
||||
@active;
|
||||
}
|
||||
|
||||
# vhost_file_link(file)
|
||||
# Returns the enabled symlink path for a virtual host file
|
||||
sub vhost_file_link
|
||||
{
|
||||
my ($file) = @_;
|
||||
return undef if (!&can_manage_vhost_files());
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
my $avail = &vhost_available_dir();
|
||||
my $short;
|
||||
if (opendir(AVAIL, $avail)) {
|
||||
foreach my $f (sort { lc($a) cmp lc($b) } readdir(AVAIL)) {
|
||||
next if ($f eq "." || $f eq "..");
|
||||
my $afile = $avail."/".$f;
|
||||
my $rafile = &simplify_path(&resolve_links($afile));
|
||||
if ($rafile && $rafile eq $rfile) {
|
||||
$short = $f;
|
||||
last;
|
||||
}
|
||||
}
|
||||
closedir(AVAIL);
|
||||
}
|
||||
$short ||= $rfile;
|
||||
$short =~ s/^.*\///;
|
||||
return &vhost_enabled_dir()."/".$short;
|
||||
}
|
||||
|
||||
# vhost_file_links(file)
|
||||
# Returns enabled symlinks for a virtual host file
|
||||
sub vhost_file_links
|
||||
{
|
||||
my ($file) = @_;
|
||||
my @rv;
|
||||
return @rv if (!&can_manage_vhost_files());
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
my $enabled = &vhost_enabled_dir();
|
||||
opendir(LINKDIR, $enabled) || return @rv;
|
||||
foreach my $f (readdir(LINKDIR)) {
|
||||
next if ($f eq "." || $f eq "..");
|
||||
my $link = $enabled."/".$f;
|
||||
next if (!-l $link);
|
||||
my $rlink = &simplify_path(&resolve_links($link));
|
||||
if ($rlink && $rlink eq $rfile) {
|
||||
push(@rv, $link);
|
||||
}
|
||||
}
|
||||
closedir(LINKDIR);
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# vhost_file_enabled(file)
|
||||
# Returns 1 if a virtual host file has an enabled symlink
|
||||
sub vhost_file_enabled
|
||||
{
|
||||
my ($file) = @_;
|
||||
return scalar(&vhost_file_links($file)) ? 1 : 0;
|
||||
}
|
||||
|
||||
# enable_vhost_file(file)
|
||||
# Enables a virtual host file and rolls back if apache configtest fails
|
||||
sub enable_vhost_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
return $text{'enable_efile'} if (!&can_manage_vhost_state_file($rfile));
|
||||
my $verr = &virtualmin_vhost_file_state_error($rfile, "enable");
|
||||
return $verr if ($verr);
|
||||
my $link = &vhost_file_link($rfile);
|
||||
$link || return $text{'enable_elinkdir'};
|
||||
return undef if (&vhost_file_enabled($rfile));
|
||||
if (-e $link || -l $link) {
|
||||
return &text('enable_elinkexists', "<tt>".&html_escape($link)."</tt>");
|
||||
}
|
||||
&symlink_logged($rfile, $link) ||
|
||||
return &text('enable_elink', "<tt>".&html_escape($link)."</tt>",
|
||||
"<tt>".&html_escape($!)."</tt>");
|
||||
my $err = &test_config();
|
||||
if ($err) {
|
||||
&unlink_logged($link);
|
||||
return &text('enable_etest', "<tt>".&html_escape($err)."</tt>");
|
||||
}
|
||||
&flush_config_cache();
|
||||
&update_last_config_change();
|
||||
return undef;
|
||||
}
|
||||
|
||||
# disable_vhost_file(file)
|
||||
# Disables a virtual host file and rolls back if apache configtest fails
|
||||
sub disable_vhost_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
return $text{'enable_efile'} if (!&can_manage_vhost_state_file($rfile));
|
||||
my $verr = &virtualmin_vhost_file_state_error($rfile, "disable");
|
||||
return $verr if ($verr);
|
||||
my @links = &vhost_file_links($file);
|
||||
return undef if (!@links);
|
||||
my @restore = map { [ $_, readlink($_) ] } @links;
|
||||
my @removed;
|
||||
foreach my $link (@links) {
|
||||
if (!&unlink_logged($link)) {
|
||||
foreach my $r (@removed) {
|
||||
&symlink_logged($r->[1], $r->[0])
|
||||
if (defined($r->[1]) && !-e $r->[0] && !-l $r->[0]);
|
||||
}
|
||||
return &text('enable_eunlink',
|
||||
"<tt>".&html_escape($link)."</tt>",
|
||||
"<tt>".&html_escape($!)."</tt>");
|
||||
}
|
||||
my ($restore) = grep { $_->[0] eq $link } @restore;
|
||||
push(@removed, $restore) if ($restore);
|
||||
}
|
||||
my $err = &test_config();
|
||||
if ($err) {
|
||||
foreach my $r (@restore) {
|
||||
&symlink_logged($r->[1], $r->[0])
|
||||
if (defined($r->[1]) && !-e $r->[0] && !-l $r->[0]);
|
||||
}
|
||||
return &text('enable_etest', "<tt>".&html_escape($err)."</tt>");
|
||||
}
|
||||
&flush_config_cache();
|
||||
&update_last_config_change();
|
||||
return undef;
|
||||
}
|
||||
|
||||
# virtualmin_available()
|
||||
# Returns 1 if Virtualmin is installed and supported on this system
|
||||
sub virtualmin_available
|
||||
{
|
||||
return $main::apache_virtualmin_available
|
||||
if (defined($main::apache_virtualmin_available));
|
||||
$main::apache_virtualmin_available = &foreign_check("virtual-server");
|
||||
return $main::apache_virtualmin_available;
|
||||
}
|
||||
|
||||
# virtualmin_domain_by_name(name)
|
||||
# Returns a Virtualmin domain object by domain name, if one exists
|
||||
sub virtualmin_domain_by_name
|
||||
{
|
||||
my ($name) = @_;
|
||||
return undef if (!&virtualmin_available());
|
||||
return $main::apache_virtualmin_domain_by_name_cache{$name}
|
||||
if (exists($main::apache_virtualmin_domain_by_name_cache{$name}));
|
||||
&foreign_require("virtual-server");
|
||||
my $d = &virtual_server::get_domain_by("dom", $name);
|
||||
$main::apache_virtualmin_domain_by_name_cache{$name} = $d;
|
||||
return $d;
|
||||
}
|
||||
|
||||
# virtual_names(&virt)
|
||||
# Returns all hostnames from ServerName and ServerAlias directives
|
||||
sub virtual_names
|
||||
{
|
||||
my ($virt) = @_;
|
||||
my @rv;
|
||||
my $sn = &find_directive("ServerName", $virt->{'members'});
|
||||
push(@rv, $sn) if ($sn);
|
||||
foreach my $sa (&find_directive_struct("ServerAlias", $virt->{'members'})) {
|
||||
push(@rv, @{$sa->{'words'} || [ ]});
|
||||
if (!@{$sa->{'words'} || [ ]} && $sa->{'value'}) {
|
||||
push(@rv, $sa->{'value'});
|
||||
}
|
||||
}
|
||||
return grep { $_ && $_ ne "*" } &unique(@rv);
|
||||
}
|
||||
|
||||
# virtualmin_domain_for_vhost_file(file)
|
||||
# Returns the Virtualmin domain object for a virtual host file, if any
|
||||
sub virtualmin_domain_for_vhost_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
return undef if (!&virtualmin_available());
|
||||
my $rfile = &simplify_path(&resolve_links($file));
|
||||
$rfile ||= $file;
|
||||
return $main::apache_virtualmin_domain_for_file_cache{$rfile}
|
||||
if (exists($main::apache_virtualmin_domain_for_file_cache{$rfile}));
|
||||
foreach my $virt (&find_virtuals_in_file($file)) {
|
||||
next if (!&can_edit_virt($virt));
|
||||
foreach my $name (&virtual_names($virt)) {
|
||||
my $d = &virtualmin_domain_by_name($name);
|
||||
if (!$d && $name =~ /^www\.(\S+)/i) {
|
||||
$d = &virtualmin_domain_by_name($1);
|
||||
}
|
||||
if ($d) {
|
||||
$main::apache_virtualmin_domain_for_file_cache{$rfile} = $d;
|
||||
return $d;
|
||||
}
|
||||
}
|
||||
}
|
||||
$main::apache_virtualmin_domain_for_file_cache{$rfile} = undef;
|
||||
return undef;
|
||||
}
|
||||
|
||||
# vhost_file_state(file)
|
||||
# Returns the effective enabled state for a virtual host file
|
||||
sub vhost_file_state
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $d = &virtualmin_domain_for_vhost_file($file);
|
||||
if ($d) {
|
||||
return { 'enabled' => $d->{'disabled'} ? 0 : 1,
|
||||
'source' => 'virtualmin',
|
||||
'domain' => $d };
|
||||
}
|
||||
return { 'enabled' => &vhost_file_enabled($file) ? 1 : 0,
|
||||
'source' => 'apache' };
|
||||
}
|
||||
|
||||
# vhost_file_toggle_action(file)
|
||||
# Returns the action needed to toggle a virtual host file's effective state
|
||||
sub vhost_file_toggle_action
|
||||
{
|
||||
my ($file) = @_;
|
||||
return &vhost_file_state($file)->{'enabled'} ? "disable" : "enable";
|
||||
}
|
||||
|
||||
# virtualmin_domain_state_link(&domain, enabled?)
|
||||
# Returns a link to the Virtualmin state change form for some domain
|
||||
sub virtualmin_domain_state_link
|
||||
{
|
||||
my ($d, $enabled) = @_;
|
||||
my $page = $enabled ? "disable_domain.cgi" : "enable_domain.cgi";
|
||||
my $label = $enabled ? $text{'enable_virtualmin_disable_label'} :
|
||||
$text{'enable_virtualmin_enable_label'};
|
||||
my $url = "../virtual-server/".$page."?dom=".&urlize($d->{'id'});
|
||||
return &ui_link("e_escape($url), "\"".$label."\"");
|
||||
}
|
||||
|
||||
# virtualmin_vhost_file_state_error(file, action)
|
||||
# Returns an error if a Virtualmin-owned site is being enabled or disabled here
|
||||
sub virtualmin_vhost_file_state_error
|
||||
{
|
||||
my ($file, $action) = @_;
|
||||
return undef if ($action ne "enable" && $action ne "disable");
|
||||
my $state_info = &vhost_file_state($file);
|
||||
return undef if ($state_info->{'source'} ne "virtualmin");
|
||||
my $d = $state_info->{'domain'};
|
||||
return undef if (!$d);
|
||||
my $state = lc($state_info->{'enabled'} ? $text{'index_enabled'} :
|
||||
$text{'index_disabled'});
|
||||
my $dom = "<tt>".&html_escape($d->{'dom'})."</tt>";
|
||||
my $link = &virtualmin_domain_state_link($d, $state_info->{'enabled'});
|
||||
return $state_info->{'enabled'} ?
|
||||
&text('enable_evirtualmin_disable', $dom, $state, $link) :
|
||||
&text('enable_evirtualmin_enable', $dom, $state, $link);
|
||||
}
|
||||
|
||||
# delete_virtuals_from_file(file, &virtualhosts...)
|
||||
# Deletes VirtualHost blocks from one file and removes the file if empty
|
||||
sub delete_virtuals_from_file
|
||||
{
|
||||
my ($file, @virts) = @_;
|
||||
return 0 if (!@virts);
|
||||
my $lref = &read_file_lines($file);
|
||||
foreach my $virt (sort { $b->{'line'} <=> $a->{'line'} } @virts) {
|
||||
my $len = $virt->{'eline'} - $virt->{'line'} + 1;
|
||||
splice(@$lref, $virt->{'line'}, $len);
|
||||
}
|
||||
my $empty = 1;
|
||||
foreach my $line (@$lref) {
|
||||
if ($line =~ /\S/) {
|
||||
$empty = 0;
|
||||
last;
|
||||
}
|
||||
}
|
||||
&flush_file_lines($file);
|
||||
if ($empty) {
|
||||
foreach my $link (&vhost_file_links($file)) {
|
||||
&unlink_logged($link);
|
||||
}
|
||||
&unlink_logged($file);
|
||||
}
|
||||
&flush_config_cache();
|
||||
&update_last_config_change();
|
||||
return scalar(@virts);
|
||||
}
|
||||
|
||||
# renumber(&config, line, file, offset)
|
||||
# Recursively changes the line number of all directives from some file
|
||||
# beyond the given line.
|
||||
@@ -1544,9 +1973,10 @@ return undef;
|
||||
# if necessary.
|
||||
sub before_changing
|
||||
{
|
||||
my @extra = grep { $_ } @_;
|
||||
if ($config{'test_always'} || $access{'test_always'}) {
|
||||
local $conf = &get_config();
|
||||
local @files = &unique(map { $_->{'file'} } @$conf);
|
||||
local @files = &unique((map { $_->{'file'} } @$conf), @extra);
|
||||
local $/ = undef;
|
||||
local $f;
|
||||
foreach $f (@files) {
|
||||
@@ -1969,10 +2399,11 @@ return @rv;
|
||||
sub create_webfile_link
|
||||
{
|
||||
local ($file) = @_;
|
||||
if ($config{'link_dir'}) {
|
||||
my $linkdir = &vhost_enabled_dir();
|
||||
if ($linkdir) {
|
||||
local $short = $file;
|
||||
$short =~ s/^.*\///;
|
||||
local $linksrc = "$config{'link_dir'}/$short";
|
||||
local $linksrc = "$linkdir/$short";
|
||||
&lock_file($linksrc);
|
||||
symlink($file, $linksrc);
|
||||
&unlock_file($linksrc);
|
||||
@@ -1985,16 +2416,16 @@ if ($config{'link_dir'}) {
|
||||
sub delete_webfile_link
|
||||
{
|
||||
local ($file) = @_;
|
||||
if ($config{'link_dir'}) {
|
||||
local $short = $file;
|
||||
$short =~ s/^.*\///;
|
||||
opendir(LINKDIR, $config{'link_dir'});
|
||||
$file = &simplify_path(&resolve_links($file));
|
||||
my $linkdir = &vhost_enabled_dir();
|
||||
if ($linkdir && opendir(LINKDIR, $linkdir)) {
|
||||
foreach my $f (readdir(LINKDIR)) {
|
||||
if ($f ne "." && $f ne ".." &&
|
||||
(&simplify_path(
|
||||
&resolve_links($config{'link_dir'}."/".$f)) eq $file ||
|
||||
$short eq $f)) {
|
||||
&unlink_logged($config{'link_dir'}."/".$f);
|
||||
if ($f ne "." && $f ne "..") {
|
||||
my $link = $linkdir."/".$f;
|
||||
next if (!-l $link);
|
||||
if (&simplify_path(&resolve_links($link)) eq $file) {
|
||||
&unlink_logged($link);
|
||||
}
|
||||
}
|
||||
}
|
||||
closedir(LINKDIR);
|
||||
|
||||
@@ -3,31 +3,107 @@
|
||||
|
||||
require './apache-lib.pl';
|
||||
&ReadParse();
|
||||
&error_setup($text{'delete_err'});
|
||||
@d = split(/\0/, $in{'d'});
|
||||
$file_action = $in{'toggle'} ? "toggle" : undef;
|
||||
&error_setup($file_action ? $text{'enable_err'} : $text{'delete_err'});
|
||||
$access{'vaddr'} || &error($text{'delete_ecannot'});
|
||||
$conf = &get_config();
|
||||
@d = split(/\0/, $in{'d'});
|
||||
$can_vhost_files = &can_manage_vhost_files();
|
||||
@d || &error($text{'delete_enone'});
|
||||
|
||||
if ($file_action) {
|
||||
&can_manage_vhost_files() || &error($text{'enable_elinkdir'});
|
||||
foreach $d (@d) {
|
||||
if ($d =~ /^file\t([^\t]+)/) {
|
||||
$file = $1;
|
||||
}
|
||||
elsif ($d !~ /^file\t/) {
|
||||
($vmembers, $vconf) = &get_virtual_config($d);
|
||||
next if (!$vconf || !&can_edit_virt($vconf));
|
||||
$file = $vconf->{'file'};
|
||||
}
|
||||
else {
|
||||
next;
|
||||
}
|
||||
$rfile = $file ? &simplify_path(&resolve_links($file)) : undef;
|
||||
$files{$rfile}++ if ($rfile && -f $rfile &&
|
||||
&can_manage_vhost_state_file($rfile));
|
||||
}
|
||||
@files = keys %files;
|
||||
@files || &error($text{'enable_enone'});
|
||||
foreach $file (@files) {
|
||||
$action = &vhost_file_toggle_action($file);
|
||||
$err = &virtualmin_vhost_file_state_error($file, $action);
|
||||
$err && &error($err);
|
||||
$file_actions{$file} = $action;
|
||||
}
|
||||
foreach $file (@files) {
|
||||
$err = $file_actions{$file} eq "enable" ?
|
||||
&enable_vhost_file($file) :
|
||||
&disable_vhost_file($file);
|
||||
$err && &error($err);
|
||||
}
|
||||
&webmin_log($file_action, "vhostfile", scalar(@files));
|
||||
&redirect("");
|
||||
exit;
|
||||
}
|
||||
if (!$in{'delete'}) {
|
||||
&error($text{'delete_eaction'});
|
||||
}
|
||||
|
||||
# Get them all
|
||||
foreach $d (@d) {
|
||||
if ($d =~ /^file\t([^\t]+)\t(\d+)$/) {
|
||||
push(@{$file_lines{$1}}, $2);
|
||||
next;
|
||||
}
|
||||
elsif ($d =~ /^file\t/) {
|
||||
next;
|
||||
}
|
||||
($vmembers, $vconf) = &get_virtual_config($d);
|
||||
$vconf || &error($text{'delete_egone'});
|
||||
&can_edit_virt($vconf) || &error(&text('delete_ecannot2',
|
||||
&virtual_name($vconf)));
|
||||
$can_vhost_files && &is_default_vhost($vconf) &&
|
||||
&error($text{'delete_edefault'});
|
||||
push(@virts, $vconf);
|
||||
}
|
||||
if (%file_lines) {
|
||||
foreach $file (keys %file_lines) {
|
||||
$rfile = &simplify_path(&resolve_links($file));
|
||||
next if (!$rfile || !-f $rfile ||
|
||||
!&can_manage_vhost_state_file($rfile));
|
||||
@fvirts = &find_virtuals_in_file($rfile);
|
||||
foreach $line (@{$file_lines{$file}}) {
|
||||
($vconf) = grep { $_->{'line'} == $line } @fvirts;
|
||||
$vconf || &error($text{'delete_egone'});
|
||||
&can_edit_virt($vconf) ||
|
||||
&error(&text('delete_ecannot2',
|
||||
&virtual_name($vconf)));
|
||||
&is_default_vhost($vconf) &&
|
||||
&error($text{'delete_edefault'});
|
||||
push(@{$file_virts{$rfile}}, $vconf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@virts || %file_virts || &error($text{'delete_enone'});
|
||||
|
||||
# Delete their structures
|
||||
&before_changing();
|
||||
&before_changing(keys %file_virts);
|
||||
foreach $vconf (@virts) {
|
||||
&lock_file($vconf->{'file'});
|
||||
&save_directive_struct($vconf, undef, $conf, $conf);
|
||||
&delete_file_if_empty($vconf->{'file'});
|
||||
}
|
||||
foreach $file (keys %file_virts) {
|
||||
&lock_file($file);
|
||||
$deleted += &delete_virtuals_from_file($file, @{$file_virts{$file}});
|
||||
&unlock_file($file);
|
||||
}
|
||||
&flush_file_lines();
|
||||
&unlock_all_files();
|
||||
&update_last_config_change();
|
||||
&after_changing();
|
||||
&webmin_log("virts", "delete", scalar(@virts));
|
||||
$deleted += scalar(@virts);
|
||||
&webmin_log("virts", "delete", $deleted);
|
||||
&redirect("");
|
||||
|
||||
|
||||
103
apache/index.cgi
103
apache/index.cgi
@@ -102,6 +102,10 @@ if (&can_edit_virt()) {
|
||||
push(@vproxy, undef);
|
||||
$sn ||= &get_system_hostname();
|
||||
push(@vurl, $defport ? "http://$sn:$defport/" : "http://$sn/");
|
||||
push(@vfile, undef);
|
||||
push(@vstatus, "");
|
||||
push(@vsel, undef);
|
||||
push(@vfilemanage, 0);
|
||||
$showing_default++;
|
||||
}
|
||||
|
||||
@@ -128,16 +132,23 @@ elsif ($httpd_modules{'core'} >= 1.2) {
|
||||
$ba = &find_directive("ServerName", $conf);
|
||||
$nv{&to_ipaddress($ba ? $ba : &get_system_hostname())}++;
|
||||
}
|
||||
@virt = grep { &can_edit_virt($_) } @virt;
|
||||
$can_vhost_files = &can_manage_vhost_files();
|
||||
@vrows = &get_virtual_list_rows($conf);
|
||||
if ($config{'show_order'} == 1) {
|
||||
# sort by server name
|
||||
@virt = sort { &server_name_sort($a) cmp &server_name_sort($b) } @virt;
|
||||
@vrows = sort { &server_name_sort($a->{'virt'}) cmp
|
||||
&server_name_sort($b->{'virt'}) } @vrows;
|
||||
}
|
||||
elsif ($config{'show_order'} == 2) {
|
||||
# sort by IP address
|
||||
@virt = sort { &server_ip_sort($a) cmp &server_ip_sort($b) } @virt;
|
||||
@vrows = sort { &server_ip_sort($a->{'virt'}) cmp
|
||||
&server_ip_sort($b->{'virt'}) } @vrows;
|
||||
}
|
||||
foreach $v (@virt) {
|
||||
@virt = map { $_->{'virt'} } grep { $_->{'active'} } @vrows;
|
||||
%available_vhost_file = map { $_, 1 } &get_vhost_available_files()
|
||||
if ($can_vhost_files);
|
||||
foreach $r (@vrows) {
|
||||
$v = $r->{'virt'};
|
||||
$vm = $v->{'members'};
|
||||
if ($v->{'words'}->[0] =~ /^\[(\S+)\]:(\d+)$/) {
|
||||
# IPv6 address and port
|
||||
@@ -163,7 +174,7 @@ foreach $v (@virt) {
|
||||
$idx = &indexof($v, @$conf);
|
||||
push(@vidx, $idx);
|
||||
push(@vname, $text{'index_virt'});
|
||||
push(@vlink, "virt_index.cgi?virt=$idx");
|
||||
push(@vlink, $r->{'active'} ? "virt_index.cgi?virt=$idx" : undef);
|
||||
$sname = &find_directive("ServerName", $vm);
|
||||
local $daddr = $addr eq "_default_" ||
|
||||
($addr eq "*" && $httpd_modules{'core'} < 1.2);
|
||||
@@ -225,10 +236,34 @@ foreach $v (@virt) {
|
||||
}
|
||||
$sp = undef if ($sp == 80 && $prot eq "http" ||
|
||||
$sp == 443 && $prot eq "https");
|
||||
push(@vurl, $sp ? "$prot://$sn:$sp/" : "$prot://$sn/");
|
||||
push(@vurl, $r->{'active'} ?
|
||||
($sp ? "$prot://$sn:$sp/" : "$prot://$sn/") : undef);
|
||||
local $rfile = $r->{'file'} ? &simplify_path(&resolve_links($r->{'file'}))
|
||||
: undef;
|
||||
push(@vfile, $rfile);
|
||||
local $status = "";
|
||||
if ($can_vhost_files && $rfile && $available_vhost_file{$rfile}) {
|
||||
local $enabled = &vhost_file_state($rfile)->{'enabled'};
|
||||
$status = $enabled ? $text{'index_enabled'} :
|
||||
$text{'index_disabled'};
|
||||
}
|
||||
push(@vstatus, $status);
|
||||
local $file_manage = $can_vhost_files && $rfile &&
|
||||
$available_vhost_file{$rfile} &&
|
||||
&can_manage_vhost_state_file($rfile);
|
||||
push(@vfilemanage, $file_manage ? 1 : 0);
|
||||
local $sel;
|
||||
if ($r->{'active'} && (!$can_vhost_files || !&is_default_vhost($v))) {
|
||||
$sel = $idx;
|
||||
}
|
||||
elsif (!$r->{'active'} && $can_vhost_files && $rfile &&
|
||||
$available_vhost_file{$rfile} && $file_manage) {
|
||||
$sel = "file\t".$rfile."\t".$v->{'line'};
|
||||
}
|
||||
push(@vsel, $sel);
|
||||
}
|
||||
|
||||
if (@vlink == 1 && !$access{'global'} && $access{'virts'} ne "*" &&
|
||||
if (@vlink == 1 && $vlink[0] && !$access{'global'} && $access{'virts'} ne "*" &&
|
||||
!$access{'create'} && $access{'noconfig'}) {
|
||||
# Can only manage one vhost, so go direct to it
|
||||
&redirect($vlink[0]);
|
||||
@@ -297,7 +332,9 @@ if ($access{'global'}) {
|
||||
# work out select links
|
||||
print &ui_tabs_start_tab("mode", "list");
|
||||
#print $text{'index_desclist'},"<p>\n";
|
||||
$showdel = $access{'vaddr'} && ($vidx[0] || $vidx[1]);
|
||||
$showdel = $access{'vaddr'} &&
|
||||
grep { defined($_) && $_ ne "" } @vsel;
|
||||
$showtoggle = $can_vhost_files && grep { $_ } @vfilemanage;
|
||||
@links = ( );
|
||||
if ($showdel) {
|
||||
push(@links, &select_all_link("d"),
|
||||
@@ -326,8 +363,10 @@ if ($config{'max_servers'} && @vname > $config{'max_servers'}) {
|
||||
}
|
||||
elsif ($config{'show_list'} && scalar(@vname)) {
|
||||
# as list for people with lots of servers
|
||||
$list_form = "vhosts_form";
|
||||
if ($showdel) {
|
||||
print &ui_form_start("delete_vservs.cgi", "post");
|
||||
print &ui_form_start("delete_vservs.cgi", "post", undef,
|
||||
"id='$list_form'");
|
||||
}
|
||||
print &ui_links_row(\@links);
|
||||
print &ui_columns_start([
|
||||
@@ -337,19 +376,23 @@ elsif ($config{'show_list'} && scalar(@vname)) {
|
||||
$text{'index_port'},
|
||||
$text{'index_name'},
|
||||
$text{'index_root'},
|
||||
$can_vhost_files ? ( $text{'index_status'} ) : ( ),
|
||||
$text{'index_url'} ], 100);
|
||||
for($i=0; $i<@vname; $i++) {
|
||||
local @cols;
|
||||
push(@cols, &ui_link($vlink[$i], $vname[$i]) );
|
||||
push(@cols, $vlink[$i] ? &ui_link($vlink[$i], $vname[$i]) :
|
||||
$vname[$i] );
|
||||
push(@cols, &html_escape($vaddr[$i]));
|
||||
push(@cols, &html_escape($vport[$i]));
|
||||
push(@cols, $vserv[$i] || $text{'index_auto'});
|
||||
push(@cols, &html_escape($vproxy[$i]) ||
|
||||
&html_escape($vroot[$i]));
|
||||
push(@cols, &ui_link($vurl[$i], $text{'index_view'}) );
|
||||
if ($showdel && $vidx[$i]) {
|
||||
push(@cols, $vstatus[$i]) if ($can_vhost_files);
|
||||
push(@cols, $vurl[$i] ? &ui_link($vurl[$i], $text{'index_view'}) :
|
||||
"" );
|
||||
if ($showdel && defined($vsel[$i]) && $vsel[$i] ne "") {
|
||||
print &ui_checked_columns_row(\@cols, undef,
|
||||
"d", $vidx[$i]);
|
||||
"d", $vsel[$i]);
|
||||
}
|
||||
elsif ($showdel) {
|
||||
print &ui_columns_row([ "", @cols ]);
|
||||
@@ -361,13 +404,23 @@ elsif ($config{'show_list'} && scalar(@vname)) {
|
||||
print &ui_columns_end();
|
||||
print &ui_links_row(\@links);
|
||||
if ($showdel) {
|
||||
print &ui_form_end([ [ "delete", $text{'index_delete'} ] ]);
|
||||
if ($showtoggle) {
|
||||
print &ui_form_end_side_by_side($list_form,
|
||||
[ [ "delete", $text{'index_delete'} ] ],
|
||||
[ [ "toggle", $text{'index_toggle'}, undef,
|
||||
undef, "form=\"$list_form\"" ] ]);
|
||||
}
|
||||
else {
|
||||
print &ui_form_end([ [ "delete", $text{'index_delete'} ] ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
# as icons for niceness
|
||||
$list_form = "vhosts_form";
|
||||
if ($showdel) {
|
||||
print &ui_form_start("delete_vservs.cgi", "post");
|
||||
print &ui_form_start("delete_vservs.cgi", "post", undef,
|
||||
"id='$list_form'");
|
||||
}
|
||||
print &ui_links_row(\@links);
|
||||
print "<table width=100% cellpadding=5>\n";
|
||||
@@ -376,8 +429,9 @@ else {
|
||||
print '<div class="row icons-row inline-row">';
|
||||
&generate_icon("images/virt.gif", $vname[$i], $vlink[$i],
|
||||
undef, undef, undef,
|
||||
$vidx[$i] && $access{'vaddr'} ?
|
||||
&ui_checkbox("d", $vidx[$i]) : "");
|
||||
defined($vsel[$i]) && $vsel[$i] ne "" &&
|
||||
$access{'vaddr'} ?
|
||||
&ui_checkbox("d", $vsel[$i]) : "");
|
||||
print "</div>\n";
|
||||
print "</td> <td valign=top>\n";
|
||||
print "$vdesc[$i]<br>\n";
|
||||
@@ -397,12 +451,24 @@ else {
|
||||
print "<b>$text{'index_root'}</b> ",
|
||||
&html_escape($vroot[$i]),"</td> </tr>\n";
|
||||
}
|
||||
if ($can_vhost_files && $vstatus[$i]) {
|
||||
print "<tr><td colspan=2><b>$text{'index_status'}</b> ",
|
||||
$vstatus[$i],"</td></tr>\n";
|
||||
}
|
||||
print "</table></td> </tr>\n";
|
||||
}
|
||||
print "</table>\n";
|
||||
print &ui_links_row(\@links);
|
||||
if ($showdel) {
|
||||
print &ui_form_end([ [ "delete", $text{'index_delete'} ] ]);
|
||||
if ($showtoggle) {
|
||||
print &ui_form_end_side_by_side($list_form,
|
||||
[ [ "delete", $text{'index_delete'} ] ],
|
||||
[ [ "toggle", $text{'index_toggle'}, undef,
|
||||
undef, "form=\"$list_form\"" ] ]);
|
||||
}
|
||||
else {
|
||||
print &ui_form_end([ [ "delete", $text{'index_delete'} ] ]);
|
||||
}
|
||||
}
|
||||
}
|
||||
print &ui_tabs_end_tab();
|
||||
@@ -492,4 +558,3 @@ return $addr eq '_default_' || $addr eq '*' ? undef :
|
||||
$addr =~ /^\[(\S+)\]$/ && &check_ip6address($1) ? $1 :
|
||||
&to_ipaddress($addr);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ index_listen=Listen on address (if needed)
|
||||
index_port=Port
|
||||
index_name=Server Name
|
||||
index_root=Document Root
|
||||
index_status=State
|
||||
index_enabled=Enabled
|
||||
index_disabled=Disabled
|
||||
index_url=URL
|
||||
index_view=Open..
|
||||
index_adddir=Allow access to this directory
|
||||
@@ -57,6 +60,7 @@ index_fmode1=Virtual servers file $1
|
||||
index_fmode1d=New file under virtual servers directory $1
|
||||
index_fmode2=Selected file..
|
||||
index_delete=Delete Selected Servers
|
||||
index_toggle=Toggle State
|
||||
|
||||
cvirt_ecannot=You are not allowed to create a virtual server
|
||||
cvirt_err=Failed to create virtual server
|
||||
@@ -1032,6 +1036,7 @@ log_stop=Stopped webserver
|
||||
log_apply=Applied changes
|
||||
log_manual=Manually edited configuration file $1
|
||||
log_virts_delete=Deleted $1 virtual servers
|
||||
log_toggle_vhostfile=Toggled state of $1 virtual host files
|
||||
|
||||
search_title=Find Servers
|
||||
search_notfound=No matching virtual servers found
|
||||
@@ -1148,6 +1153,22 @@ delete_err=Failed to delete virtual servers
|
||||
delete_enone=None selected
|
||||
delete_ecannot=You are not allowed to delete servers
|
||||
delete_ecannot2=You are not allowed to edit the server $1
|
||||
delete_eaction=No action was selected
|
||||
delete_egone=The selected virtual server no longer exists
|
||||
delete_edefault=The default virtual server cannot be deleted
|
||||
|
||||
enable_err=Failed to change virtual host file state
|
||||
enable_enone=No manageable virtual host files were selected
|
||||
enable_efile=Virtual host file does not exist or cannot be managed
|
||||
enable_elinkdir=No enabled virtual host links directory is configured
|
||||
enable_elink=Failed to create symbolic link $1 : $2
|
||||
enable_eunlink=Failed to remove symbolic link $1 : $2
|
||||
enable_elinkexists=The symbolic link $1 already exists
|
||||
enable_etest=Apache configuration test failed after changing the virtual host file state : $1
|
||||
enable_evirtualmin_disable=This Apache virtual host is managed by Virtualmin virtual server $1, which is currently $2. Site disabling should be done in Virtualmin using $3.
|
||||
enable_evirtualmin_enable=This Apache virtual host is managed by Virtualmin virtual server $1, which is currently $2. Site enabling should be done in Virtualmin using $3.
|
||||
enable_virtualmin_disable_label=Disable and Delete ⇾ Disable Virtual Server
|
||||
enable_virtualmin_enable_label=Disable and Delete ⇾ Enable Virtual Server
|
||||
|
||||
syslog_desc=Apache error log
|
||||
|
||||
|
||||
409
apache/t/vhost-files.t
Normal file
409
apache/t/vhost-files.t
Normal file
@@ -0,0 +1,409 @@
|
||||
#!/usr/bin/perl
|
||||
# Tests for Debian-style Apache sites-available/sites-enabled handling.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use File::Basename qw(dirname);
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use Cwd qw(abs_path);
|
||||
|
||||
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..', '..'));
|
||||
my $tmp = abs_path(tempdir(CLEANUP => 1));
|
||||
my $webmin_config = File::Spec->catdir($tmp, 'webmin-config');
|
||||
my $webmin_var = File::Spec->catdir($tmp, 'webmin-var');
|
||||
my $apache_root = File::Spec->catdir($tmp, 'apache2');
|
||||
my $available = File::Spec->catdir($apache_root, 'sites-available');
|
||||
my $enabled = File::Spec->catdir($apache_root, 'sites-enabled');
|
||||
my $apache_conf = File::Spec->catfile($apache_root, 'apache2.conf');
|
||||
|
||||
make_path($webmin_config, $webmin_var, "$webmin_config/apache",
|
||||
"$webmin_var/apache", $apache_root, $available, $enabled);
|
||||
|
||||
sub write_text
|
||||
{
|
||||
my ($file, $text) = @_;
|
||||
open(my $fh, '>', $file) || die "Failed to write $file: $!";
|
||||
print $fh $text;
|
||||
close($fh) || die "Failed to close $file: $!";
|
||||
}
|
||||
|
||||
sub read_text
|
||||
{
|
||||
my ($file) = @_;
|
||||
open(my $fh, '<', $file) || die "Failed to read $file: $!";
|
||||
local $/ = undef;
|
||||
my $text = <$fh>;
|
||||
close($fh) || die "Failed to close $file: $!";
|
||||
return $text;
|
||||
}
|
||||
|
||||
sub vhost_conf
|
||||
{
|
||||
my ($name, $rootdir) = @_;
|
||||
my $name_line = defined($name) ? " ServerName $name\n" : "";
|
||||
return "<VirtualHost *:80>\n".
|
||||
$name_line.
|
||||
" DocumentRoot $rootdir\n".
|
||||
"</VirtualHost>\n";
|
||||
}
|
||||
|
||||
my $default = File::Spec->catfile($available, '000-default.conf');
|
||||
my $alpha = File::Spec->catfile($available, 'alpha.conf');
|
||||
my $beta = File::Spec->catfile($available, 'beta.conf');
|
||||
my $charlie = File::Spec->catfile($available, 'charlie.conf');
|
||||
|
||||
write_text($default, vhost_conf(undef, '/srv/default'));
|
||||
write_text($alpha, vhost_conf('alpha.example', '/srv/alpha'));
|
||||
write_text($beta, vhost_conf('beta.example', '/srv/beta'));
|
||||
write_text($charlie, vhost_conf('charlie.example', '/srv/charlie'));
|
||||
write_text($apache_conf,
|
||||
"ServerRoot \"$apache_root\"\n".
|
||||
"Listen 80\n".
|
||||
"IncludeOptional $enabled/*.conf\n");
|
||||
|
||||
symlink($default, File::Spec->catfile($enabled, '000-default.conf')) ||
|
||||
die "Failed to symlink default: $!";
|
||||
symlink($alpha, File::Spec->catfile($enabled, 'alpha.conf')) ||
|
||||
die "Failed to symlink alpha: $!";
|
||||
symlink($charlie, File::Spec->catfile($enabled, 'charlie.conf')) ||
|
||||
die "Failed to symlink charlie: $!";
|
||||
|
||||
write_text(File::Spec->catfile($webmin_config, 'config'),
|
||||
"os_type=debian-linux\n".
|
||||
"os_version=12\n".
|
||||
"real_os_type=Debian Linux\n".
|
||||
"real_os_version=12\n");
|
||||
write_text(File::Spec->catfile($webmin_config, 'miniserv.conf'),
|
||||
"root=$root\n");
|
||||
write_text(File::Spec->catfile($webmin_config, 'apache', 'config'),
|
||||
"httpd_dir=$apache_root\n".
|
||||
"httpd_path=/bin/true\n".
|
||||
"httpd_conf=$apache_conf\n".
|
||||
"apachectl_path=/bin/true\n".
|
||||
"httpd_version=2.4.57\n".
|
||||
"test_apachectl=0\n".
|
||||
"test_config=1\n".
|
||||
"virt_file=$available\n".
|
||||
"link_dir=$enabled\n");
|
||||
|
||||
$ENV{'WEBMIN_CONFIG'} = $webmin_config;
|
||||
$ENV{'WEBMIN_VAR'} = $webmin_var;
|
||||
$ENV{'FOREIGN_MODULE_NAME'} = 'apache';
|
||||
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $root;
|
||||
$ENV{'REMOTE_USER'} = 'root';
|
||||
|
||||
unshift(@INC, $root);
|
||||
require File::Spec->catfile($root, 'apache', 'apache-lib.pl');
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
$main::text{'enable_elinkdir'} = 'No enabled virtual host links directory is configured';
|
||||
$main::text{'enable_efile'} = 'Virtual host file does not exist or cannot be managed';
|
||||
$main::text{'enable_elink'} = 'Failed to create symbolic link $1 : $2';
|
||||
$main::text{'enable_eunlink'} = 'Failed to remove symbolic link $1 : $2';
|
||||
$main::text{'enable_elinkexists'} = 'The symbolic link $1 already exists';
|
||||
$main::text{'enable_etest'} = 'Apache configuration test failed after changing the virtual host file state : $1';
|
||||
$main::text{'enable_evirtualmin_disable'} = 'This Apache virtual host is managed by Virtualmin virtual server $1, which is currently $2. Site disabling should be done in Virtualmin using $3.';
|
||||
$main::text{'enable_evirtualmin_enable'} = 'This Apache virtual host is managed by Virtualmin virtual server $1, which is currently $2. Site enabling should be done in Virtualmin using $3.';
|
||||
$main::text{'enable_virtualmin_disable_label'} = 'Disable and Delete ⇾ Disable Virtual Server';
|
||||
$main::text{'enable_virtualmin_enable_label'} = 'Disable and Delete ⇾ Enable Virtual Server';
|
||||
$main::text{'index_enabled'} = 'Enabled';
|
||||
$main::text{'index_disabled'} = 'Disabled';
|
||||
$main::text{'eafter'} = 'Apache configuration test failed : $1';
|
||||
}
|
||||
|
||||
sub apache_config
|
||||
{
|
||||
main::flush_config_cache();
|
||||
my $conf = main::get_config();
|
||||
ok($conf, 'test apache config can be parsed');
|
||||
return $conf;
|
||||
}
|
||||
|
||||
sub row_names
|
||||
{
|
||||
return [ map {
|
||||
scalar(main::find_directive('ServerName', $_->{'virt'}->{'members'})) || ''
|
||||
} @_ ];
|
||||
}
|
||||
|
||||
sub row_states
|
||||
{
|
||||
return [ map { $_->{'active'} ? 'enabled' : 'disabled' } @_ ];
|
||||
}
|
||||
|
||||
subtest 'sites-available files are manageable and ordered' => sub {
|
||||
ok(main::can_manage_vhost_files(),
|
||||
'sites-available/enabled dirs are manageable');
|
||||
is_deeply(
|
||||
[ main::get_vhost_available_files() ],
|
||||
[ $default, $alpha, $beta, $charlie ],
|
||||
'available files are listed in stable filename order',
|
||||
);
|
||||
|
||||
my @rows = main::get_virtual_list_rows(apache_config());
|
||||
is_deeply(row_names(@rows),
|
||||
[ '', 'alpha.example', 'beta.example', 'charlie.example' ],
|
||||
'disabled rows stay in sites-available order');
|
||||
is_deeply(row_states(@rows),
|
||||
[ 'enabled', 'enabled', 'disabled', 'enabled' ],
|
||||
'row active state follows sites-enabled symlinks');
|
||||
ok(!main::can_manage_vhost_file($default),
|
||||
'default virtual host file is not file-state manageable');
|
||||
};
|
||||
|
||||
subtest 'disable removes only the enabled symlink' => sub {
|
||||
no warnings 'once';
|
||||
unlink($main::last_config_change_flag);
|
||||
unlink($main::last_restart_time_flag);
|
||||
main::restart_last_restart_time();
|
||||
my $old = time() - 10;
|
||||
utime($old, $old, $main::last_restart_time_flag);
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return undef; };
|
||||
is(main::disable_vhost_file($alpha), undef, 'disable succeeds');
|
||||
}
|
||||
ok(main::needs_config_restart(),
|
||||
'disable marks config as needing apply');
|
||||
|
||||
ok(-f $alpha, 'disable leaves the sites-available file in place');
|
||||
ok(!-e File::Spec->catfile($enabled, 'alpha.conf'),
|
||||
'disable removes the sites-enabled symlink');
|
||||
|
||||
my @rows = main::get_virtual_list_rows(apache_config());
|
||||
is_deeply(row_names(@rows),
|
||||
[ '', 'alpha.example', 'beta.example', 'charlie.example' ],
|
||||
'disabled row remains in the same list position');
|
||||
is_deeply(row_states(@rows),
|
||||
[ 'enabled', 'disabled', 'disabled', 'enabled' ],
|
||||
'disabled row status is updated');
|
||||
};
|
||||
|
||||
subtest 'enable creates a symlink without touching the source file' => sub {
|
||||
no warnings 'once';
|
||||
unlink($main::last_config_change_flag);
|
||||
unlink($main::last_restart_time_flag);
|
||||
main::restart_last_restart_time();
|
||||
my $old = time() - 10;
|
||||
utime($old, $old, $main::last_restart_time_flag);
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return undef; };
|
||||
is(main::enable_vhost_file($beta), undef, 'enable succeeds');
|
||||
}
|
||||
ok(main::needs_config_restart(),
|
||||
'enable marks config as needing apply');
|
||||
|
||||
my $link = File::Spec->catfile($enabled, 'beta.conf');
|
||||
ok(-f $beta, 'enable leaves the sites-available file in place');
|
||||
ok(-l $link, 'enable creates the sites-enabled symlink');
|
||||
is(readlink($link), $beta, 'enabled symlink points to the available file');
|
||||
ok(main::vhost_file_enabled($beta), 'vhost_file_enabled sees the symlink');
|
||||
};
|
||||
|
||||
subtest 'same-name symlink to another target is not disabled' => sub {
|
||||
my $otherdir = File::Spec->catdir($tmp, 'other-sites');
|
||||
my $other = File::Spec->catfile($otherdir, 'charlie.conf');
|
||||
my $link = File::Spec->catfile($enabled, 'charlie.conf');
|
||||
make_path($otherdir);
|
||||
write_text($other, vhost_conf('other.example', '/srv/other'));
|
||||
unlink($link) || die "Failed to remove charlie link: $!";
|
||||
symlink($other, $link) || die "Failed to symlink other charlie: $!";
|
||||
|
||||
ok(!main::vhost_file_enabled($charlie),
|
||||
'same-name symlink to another file is not considered enabled');
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return undef; };
|
||||
is(main::disable_vhost_file($charlie), undef, 'disable is a no-op');
|
||||
}
|
||||
ok(-l $link, 'same-name symlink to another target is preserved');
|
||||
is(readlink($link), $other, 'preserved symlink target is unchanged');
|
||||
};
|
||||
|
||||
subtest 'disabled default virtual hosts stay hidden' => sub {
|
||||
my $disabled_default = File::Spec->catfile($available,
|
||||
'zz-disabled-default.conf');
|
||||
write_text($disabled_default, vhost_conf(undef, '/srv/disabled-default'));
|
||||
|
||||
my @rows = main::get_virtual_list_rows(apache_config());
|
||||
ok(!(grep { $_->{'file'} eq $disabled_default } @rows),
|
||||
'disabled catch-all virtual host file is not listed as a normal vhost');
|
||||
};
|
||||
|
||||
subtest 'legacy webfile link helpers resolve relative link_dir' => sub {
|
||||
my $relative = File::Spec->catfile($available, 'relative.conf');
|
||||
my $link = File::Spec->catfile($enabled, 'relative.conf');
|
||||
write_text($relative, vhost_conf('relative.example', '/srv/relative'));
|
||||
unlink($link);
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
local $main::config{'link_dir'} = 'sites-enabled';
|
||||
main::create_webfile_link($relative);
|
||||
ok(-l $link, 'relative link_dir creates link under ServerRoot');
|
||||
is(readlink($link), $relative,
|
||||
'created relative link_dir symlink points to the vhost file');
|
||||
main::delete_webfile_link($relative);
|
||||
ok(!-e $link && !-l $link,
|
||||
'relative link_dir delete removes the enabled symlink');
|
||||
}
|
||||
};
|
||||
|
||||
subtest 'file-level actions require access to every virtual host in the file' => sub {
|
||||
my $mixed = File::Spec->catfile($available, 'mixed.conf');
|
||||
write_text($mixed,
|
||||
vhost_conf('alpha.example', '/srv/mixed-alpha').
|
||||
vhost_conf('hidden.example', '/srv/mixed-hidden'));
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
local $main::access{'virts'} = 'alpha.example:80';
|
||||
ok(!main::can_manage_vhost_file($mixed),
|
||||
'mixed-access file cannot be managed by a restricted user');
|
||||
}
|
||||
ok(main::can_manage_vhost_file($mixed),
|
||||
'shared file can be managed when all contained vhosts are allowed');
|
||||
};
|
||||
|
||||
subtest 'state helpers enforce allowed files and ACLs directly' => sub {
|
||||
my $outside = File::Spec->catfile($tmp, 'outside.conf');
|
||||
write_text($outside, vhost_conf('outside.example', '/srv/outside'));
|
||||
is(main::enable_vhost_file($outside),
|
||||
'Virtual host file does not exist or cannot be managed',
|
||||
'enable rejects files outside sites-available');
|
||||
|
||||
my $mixed = File::Spec->catfile($available, 'state-mixed.conf');
|
||||
write_text($mixed,
|
||||
vhost_conf('alpha.example', '/srv/state-alpha').
|
||||
vhost_conf('hidden.example', '/srv/state-hidden'));
|
||||
{
|
||||
no warnings 'once';
|
||||
local $main::access{'virts'} = 'alpha.example:80';
|
||||
is(main::enable_vhost_file($mixed),
|
||||
'Virtual host file does not exist or cannot be managed',
|
||||
'enable rejects mixed-access files without relying on caller validation');
|
||||
}
|
||||
};
|
||||
|
||||
subtest 'change rollback covers extra disabled vhost files' => sub {
|
||||
my $rollback = File::Spec->catfile($available, 'rollback.conf');
|
||||
my $original = vhost_conf('rollback.example', '/srv/rollback');
|
||||
write_text($rollback, $original);
|
||||
my @virts = main::find_virtuals_in_file($rollback);
|
||||
is(scalar(@virts), 1, 'rollback fixture has one vhost');
|
||||
|
||||
{
|
||||
no warnings qw(redefine once);
|
||||
local %main::before_changing;
|
||||
local $main::config{'test_always'} = 1;
|
||||
local *main::test_config = sub { return 'bad config'; };
|
||||
local *main::error = sub { die $_[0]; };
|
||||
main::before_changing($rollback);
|
||||
is(main::delete_virtuals_from_file($rollback, @virts), 1,
|
||||
'disabled vhost file deletion removes the vhost');
|
||||
ok(!-e $rollback, 'empty disabled vhost file is deleted');
|
||||
like(eval { main::after_changing(); 1 } ? '' : $@,
|
||||
qr/bad config/, 'failed post-change test reports an error');
|
||||
}
|
||||
ok(-f $rollback, 'rollback recreates the disabled vhost file');
|
||||
is(read_text($rollback), $original,
|
||||
'rollback restores the disabled vhost file contents');
|
||||
};
|
||||
|
||||
subtest 'apache configtest failure rolls back link changes' => sub {
|
||||
my $delta = File::Spec->catfile($available, 'delta.conf');
|
||||
my $delta_link = File::Spec->catfile($enabled, 'delta.conf');
|
||||
write_text($delta, vhost_conf('delta.example', '/srv/delta'));
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return 'bad config'; };
|
||||
like(main::enable_vhost_file($delta), qr/bad config/,
|
||||
'failed enable reports apache configtest output');
|
||||
}
|
||||
ok(!-e $delta_link, 'failed enable removes the new symlink');
|
||||
|
||||
symlink($delta, $delta_link) || die "Failed to symlink delta: $!";
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return 'bad config'; };
|
||||
like(main::disable_vhost_file($delta), qr/bad config/,
|
||||
'failed disable reports apache configtest output');
|
||||
}
|
||||
ok(-l $delta_link, 'failed disable restores the removed symlink');
|
||||
is(readlink($delta_link), $delta, 'restored symlink target is unchanged');
|
||||
};
|
||||
|
||||
subtest 'Virtualmin-managed virtual host files cannot be toggled directly' => sub {
|
||||
my $enabled_domain = File::Spec->catfile($available, 'vm-enabled.conf');
|
||||
my $disabled_domain = File::Spec->catfile($available, 'vm-disabled.conf');
|
||||
write_text($enabled_domain,
|
||||
vhost_conf('www.vm-enabled.example', '/srv/vm-enabled'));
|
||||
write_text($disabled_domain,
|
||||
vhost_conf('vm-disabled.example', '/srv/vm-disabled'));
|
||||
|
||||
{
|
||||
no warnings qw(redefine once);
|
||||
local %main::apache_virtualmin_domain_for_file_cache;
|
||||
local %main::apache_virtualmin_domain_by_name_cache;
|
||||
local *main::virtualmin_available = sub { return 1; };
|
||||
local *main::virtualmin_domain_by_name = sub {
|
||||
my ($name) = @_;
|
||||
return $name eq 'vm-enabled.example' ?
|
||||
{ 'dom' => $name, 'id' => '12345',
|
||||
'disabled' => '' } :
|
||||
$name eq 'vm-disabled.example' ?
|
||||
{ 'dom' => $name, 'id' => '67890',
|
||||
'disabled' => 'web' } :
|
||||
undef;
|
||||
};
|
||||
|
||||
my $disable_err =
|
||||
main::virtualmin_vhost_file_state_error($enabled_domain,
|
||||
'disable');
|
||||
my $enabled_state = main::vhost_file_state($enabled_domain);
|
||||
is($enabled_state->{'source'}, 'virtualmin',
|
||||
'Virtualmin is the effective state source for managed files');
|
||||
ok($enabled_state->{'enabled'},
|
||||
'Virtualmin enabled domain is reported as enabled');
|
||||
is(main::vhost_file_toggle_action($enabled_domain), 'disable',
|
||||
'toggle action follows the Virtualmin enabled state');
|
||||
like($disable_err, qr/currently enabled/,
|
||||
'Virtualmin state is included for enabled domains');
|
||||
like($disable_err, qr/Disable Virtual Server/,
|
||||
'disabling directs users to Virtualmin disable action');
|
||||
like($disable_err,
|
||||
qr{virtual-server/disable_domain\.cgi\?dom=12345},
|
||||
'disabling links to the Virtualmin disable form');
|
||||
|
||||
my $enable_err =
|
||||
main::virtualmin_vhost_file_state_error($disabled_domain,
|
||||
'enable');
|
||||
my $disabled_state = main::vhost_file_state($disabled_domain);
|
||||
is($disabled_state->{'source'}, 'virtualmin',
|
||||
'Virtualmin remains the state source for disabled domains');
|
||||
ok(!$disabled_state->{'enabled'},
|
||||
'Virtualmin disabled domain is reported as disabled');
|
||||
is(main::vhost_file_toggle_action($disabled_domain), 'enable',
|
||||
'toggle action follows the Virtualmin disabled state');
|
||||
like($enable_err, qr/currently disabled/,
|
||||
'Virtualmin state is included for disabled domains');
|
||||
like($enable_err, qr/Enable Virtual Server/,
|
||||
'enabling directs users to Virtualmin enable action');
|
||||
like($enable_err,
|
||||
qr{virtual-server/enable_domain\.cgi\?dom=67890},
|
||||
'enabling links to the Virtualmin enable form');
|
||||
|
||||
is(main::virtualmin_vhost_file_state_error($alpha, 'disable'),
|
||||
undef, 'non-Virtualmin virtual host files can still be toggled');
|
||||
}
|
||||
};
|
||||
|
||||
done_testing();
|
||||
@@ -4,11 +4,12 @@
|
||||
# client. From then on, direct TCP connections can be made to this port
|
||||
# to send requests and get replies.
|
||||
|
||||
$main::allow_rpc_only = 1;
|
||||
BEGIN { push(@INC, "."); };
|
||||
use WebminCore;
|
||||
use POSIX;
|
||||
use Socket;
|
||||
|
||||
$main::allow_rpc_only = 1;
|
||||
$force_lang = $default_lang;
|
||||
&init_config();
|
||||
|
||||
|
||||
@@ -111,8 +111,9 @@ if (!$release || !-d "$tardir/$dir") {
|
||||
next if ($f =~ /^\./ || $f =~ /\.git$/ ||
|
||||
$f =~ /\.(tar|wbm|wbt)\.gz$/ ||
|
||||
$f eq "README.md" || $f =~ /^makemodule.*\.pl$/ ||
|
||||
$f eq "linux.sh" || $f eq "freebsd.sh" ||
|
||||
$f eq "LICENCE" || $f eq "version");
|
||||
$f eq "linux.sh" || $f eq "freebsd.sh" ||
|
||||
$f eq "LICENCE" || $f eq "version" ||
|
||||
(-d "$m/$f" && ($f eq "t" || $f eq "xt")));
|
||||
$flist .= " $m/$f";
|
||||
}
|
||||
closedir(DIR);
|
||||
|
||||
@@ -244,6 +244,7 @@ system("find $usr_dir -name .git | xargs rm -rf");
|
||||
system("find $usr_dir -name .github | xargs rm -rf");
|
||||
system("find $usr_dir -name RELEASE | xargs rm -rf");
|
||||
system("find $usr_dir -name RELEASE.sh | xargs rm -rf");
|
||||
system("find $usr_dir -type d \\( -name t -o -name xt \\) | xargs rm -rf");
|
||||
if (-r "$usr_dir/$mod/EXCLUDE") {
|
||||
system("cd $usr_dir/$mod && cat EXCLUDE | xargs rm -rf");
|
||||
system("rm -f $usr_dir/$mod/EXCLUDE");
|
||||
@@ -621,10 +622,10 @@ if ($dsc_file) {
|
||||
$diffmd5 =~ s/\s+.*\n//g;
|
||||
my @diffst = stat($diff_file);
|
||||
|
||||
# Create a tar file of the module directory
|
||||
# Create a tar file of the cleaned staged module directory
|
||||
my $tar_file = $dsc_file;
|
||||
$tar_file =~ s/[^\/]+$//; $tar_file .= "$prefix$mod-$ver.tar.gz";
|
||||
system("cd $par ; tar czf $tar_file $source_mod");
|
||||
system("cd $usr_dir ; tar czf $tar_file $mod");
|
||||
my $md5 = `md5sum $tar_file`;
|
||||
$md5 =~ s/\s+.*\n//g;
|
||||
my @st = stat($tar_file);
|
||||
|
||||
@@ -266,7 +266,7 @@ system("/usr/bin/find /tmp/makemodulerpm -name .git | xargs rm -rf");
|
||||
system("/usr/bin/find /tmp/makemodulerpm -name .github | xargs rm -rf");
|
||||
system("/usr/bin/find /tmp/makemodulerpm -name RELEASE | xargs rm -rf");
|
||||
system("/usr/bin/find /tmp/makemodulerpm -name RELEASE.sh | xargs rm -rf");
|
||||
system("/usr/bin/find /tmp/makemodulerpm -name t | xargs rm -rf");
|
||||
system("/usr/bin/find /tmp/makemodulerpm -type d \\( -name t -o -name xt \\) | xargs rm -rf");
|
||||
if (-r "/tmp/makemodulerpm/$mod/EXCLUDE") {
|
||||
system("cd /tmp/makemodulerpm/$mod && cat EXCLUDE | xargs rm -rf");
|
||||
system("rm -f /tmp/makemodulerpm/$mod/EXCLUDE");
|
||||
|
||||
11
miniserv.pl
11
miniserv.pl
@@ -6779,15 +6779,14 @@ return $newhash eq $hash;
|
||||
}
|
||||
|
||||
# encrypt_sha512(password, [salt])
|
||||
# Hashes a password, possibly with the given salt, with SHA512
|
||||
# Hashes a password, possibly with the given salt, with SHA512. The salt
|
||||
# arg may be a full $6$salt$hash form (verification) or a bare $6$salt$
|
||||
# (fresh hashing) — either way it must be passed to crypt() intact so
|
||||
# crypt() selects SHA512. Only synthesise a new salt when none is given.
|
||||
sub encrypt_sha512
|
||||
{
|
||||
my ($passwd, $salt) = @_;
|
||||
if ($salt =~ /^\$6\$([^\$]+)/) {
|
||||
# Extract actual salt from already encrypted password
|
||||
$salt = $1;
|
||||
}
|
||||
$salt ||= '$6$'.substr(time(), -8).'$';
|
||||
$salt = '$6$'.substr(time(), -8).'$' if (!$salt || $salt !~ /^\$6\$/);
|
||||
return crypt($passwd, $salt);
|
||||
}
|
||||
|
||||
|
||||
@@ -1175,7 +1175,8 @@ foreach my $svc (read_etc_service_defs($services_file)) {
|
||||
foreach my $svc (setup_quick_service_defs()) {
|
||||
merge_quick_service(\%defs, $svc);
|
||||
}
|
||||
return sort { lc($a->{'label'}) cmp lc($b->{'label'}) } values %defs;
|
||||
my @sorted = sort { lc($a->{'label'}) cmp lc($b->{'label'}) } values %defs;
|
||||
return @sorted;
|
||||
}
|
||||
|
||||
# service_search_text(&service)
|
||||
@@ -2543,7 +2544,7 @@ foreach my $service (@services) {
|
||||
my $port = $map->{lc($proto || '')}->{lc($service)};
|
||||
return $port if (defined($port));
|
||||
}
|
||||
return undef;
|
||||
return;
|
||||
}
|
||||
|
||||
# read_etc_services([services-file])
|
||||
@@ -2616,12 +2617,12 @@ return 0;
|
||||
sub configured_port_from_address
|
||||
{
|
||||
my ($value, $default) = @_;
|
||||
return undef if (!defined($value) || $value eq '');
|
||||
return if (!defined($value) || $value eq '');
|
||||
return $1 if ($value =~ /^(\d+)$/);
|
||||
return $1 if ($value =~ /^\[[^\]]+\]:(\d+)$/);
|
||||
return $1 if ($value =~ /^[^:]+:(\d+)$/);
|
||||
return $default if (defined($default) && $value =~ /\S/);
|
||||
return undef;
|
||||
return;
|
||||
}
|
||||
|
||||
# address_is_loopback(address)
|
||||
|
||||
@@ -38,6 +38,11 @@ print &ui_table_row($text{'acl_root'},
|
||||
print &ui_table_row($text{'acl_global'},
|
||||
&ui_yesno_radio("global", $o->{'global'}));
|
||||
|
||||
# Can manually edit configuration files?
|
||||
print &ui_table_row($text{'acl_manual'},
|
||||
&ui_yesno_radio("manual",
|
||||
defined($o->{'manual'}) ? $o->{'manual'} : $o->{'global'}));
|
||||
|
||||
# Can edit log files?
|
||||
print &ui_table_row($text{'acl_logs'},
|
||||
&ui_yesno_radio("logs", $o->{'logs'}));
|
||||
@@ -59,6 +64,7 @@ $o->{'edit'} = $in{'edit'};
|
||||
$o->{'create'} = $in{'create'};
|
||||
$o->{'root'} = $in{'root'};
|
||||
$o->{'global'} = $in{'global'};
|
||||
$o->{'manual'} = $in{'manual'};
|
||||
$o->{'logs'} = $in{'logs'};
|
||||
$o->{'user'} = $in{'user'};
|
||||
$o->{'stop'} = $in{'stop'};
|
||||
|
||||
@@ -6,11 +6,65 @@ use warnings;
|
||||
require './nginx-lib.pl';
|
||||
our (%text, %in, %config, %access);
|
||||
&ReadParse();
|
||||
&error_setup($text{'delete_err'});
|
||||
my @items = split(/\0/, $in{'d'} || "");
|
||||
my $file_action = $in{'toggle'} ? "toggle" :
|
||||
$in{'enable'} ? "enable" :
|
||||
$in{'disable'} ? "disable" : undef;
|
||||
&error_setup($file_action ? $text{'enable_err'} : $text{'delete_err'});
|
||||
$access{'edit'} || &error($text{'server_ecannotedit'});
|
||||
|
||||
my @ids = split(/\0/, $in{'d'} || "");
|
||||
@ids || &error($text{'delete_enone'});
|
||||
if ($file_action) {
|
||||
&can_manage_server_files() || &error($text{'enable_elinkdir'});
|
||||
my %add_to = map { $_, 1 } &get_add_to_files();
|
||||
my %files;
|
||||
foreach my $item (@items) {
|
||||
my $file;
|
||||
if ($item =~ /^file\t([^\t]+)/) {
|
||||
$file = $1;
|
||||
}
|
||||
else {
|
||||
my $server = &find_server($item);
|
||||
next if (!$server || !&can_edit_server($server));
|
||||
$file = $server->{'file'};
|
||||
}
|
||||
my $rfile = $file ? &resolve_links($file) : undef;
|
||||
$files{$rfile}++ if ($rfile && -f $rfile && $add_to{$rfile} &&
|
||||
&can_manage_server_file($rfile));
|
||||
}
|
||||
my @files = keys %files;
|
||||
@files || &error($text{'enable_enone'});
|
||||
my %file_actions;
|
||||
foreach my $file (@files) {
|
||||
my $action = $file_action eq "toggle" ?
|
||||
&server_file_toggle_action($file) : $file_action;
|
||||
my $err = &virtualmin_server_file_state_error($file, $action);
|
||||
$err && &error($err);
|
||||
$file_actions{$file} = $action;
|
||||
}
|
||||
foreach my $file (@files) {
|
||||
my $err = $file_actions{$file} eq "enable" ?
|
||||
&enable_server_file($file) :
|
||||
&disable_server_file($file);
|
||||
$err && &error($err);
|
||||
}
|
||||
&webmin_log($file_action, "serverfile", scalar(@files));
|
||||
&redirect("");
|
||||
exit;
|
||||
}
|
||||
if (!$in{'delete'}) {
|
||||
&error($text{'delete_eaction'});
|
||||
}
|
||||
|
||||
my (@ids, %file_lines);
|
||||
foreach my $item (@items) {
|
||||
if ($item =~ /^file\t([^\t]+)\t(\d+)$/) {
|
||||
push(@{$file_lines{$1}}, $2);
|
||||
}
|
||||
elsif ($item !~ /^file\t/) {
|
||||
push(@ids, $item);
|
||||
}
|
||||
}
|
||||
@ids || %file_lines || &error($text{'delete_enone'});
|
||||
|
||||
# Validate the selected server blocks before locking config files.
|
||||
foreach my $id (@ids) {
|
||||
@@ -19,36 +73,54 @@ foreach my $id (@ids) {
|
||||
&can_edit_server($server) || &error($text{'server_ecannot'});
|
||||
&is_default_server_block($server) && &error($text{'delete_edefault'});
|
||||
}
|
||||
my %add_to = map { $_, 1 } &get_add_to_files();
|
||||
my %file_servers;
|
||||
foreach my $file (keys %file_lines) {
|
||||
my $rfile = &resolve_links($file);
|
||||
next if (!$rfile || !-f $rfile || !$add_to{$rfile} ||
|
||||
!&can_manage_server_file($rfile));
|
||||
my @servers = &find_servers_in_file($rfile);
|
||||
foreach my $line (@{$file_lines{$file}}) {
|
||||
my ($server) = grep { $_->{'line'} == $line } @servers;
|
||||
$server || &error($text{'server_egone'});
|
||||
&can_edit_server($server) || &error($text{'server_ecannot'});
|
||||
&is_default_server_block($server) && &error($text{'delete_edefault'});
|
||||
push(@{$file_servers{$rfile}}, $server);
|
||||
}
|
||||
}
|
||||
@ids || %file_servers || &error($text{'delete_enone'});
|
||||
|
||||
&lock_all_config_files();
|
||||
my $conf = &get_config();
|
||||
my $http = &find("http", $conf);
|
||||
if (!$http) {
|
||||
&unlock_all_config_files();
|
||||
&error(&text('index_ehttp', "<tt>$config{'nginx_config'}</tt>"));
|
||||
}
|
||||
my @servers;
|
||||
foreach my $id (@ids) {
|
||||
my $server = &find_server($id);
|
||||
if (!$server) {
|
||||
if (@ids) {
|
||||
&lock_all_config_files();
|
||||
my $conf = &get_config();
|
||||
my $http = &find("http", $conf);
|
||||
if (!$http) {
|
||||
&unlock_all_config_files();
|
||||
&error($text{'server_egone'});
|
||||
&error(&text('index_ehttp', "<tt>$config{'nginx_config'}</tt>"));
|
||||
}
|
||||
if (!&can_edit_server($server)) {
|
||||
&unlock_all_config_files();
|
||||
&error($text{'server_ecannot'});
|
||||
foreach my $id (@ids) {
|
||||
my $server = &find_server($id);
|
||||
if (!$server) {
|
||||
&unlock_all_config_files();
|
||||
&error($text{'server_egone'});
|
||||
}
|
||||
if (!&can_edit_server($server)) {
|
||||
&unlock_all_config_files();
|
||||
&error($text{'server_ecannot'});
|
||||
}
|
||||
if (&is_default_server_block($server)) {
|
||||
&unlock_all_config_files();
|
||||
&error($text{'delete_edefault'});
|
||||
}
|
||||
push(@servers, $server);
|
||||
}
|
||||
if (&is_default_server_block($server)) {
|
||||
&unlock_all_config_files();
|
||||
&error($text{'delete_edefault'});
|
||||
foreach my $server (@servers) {
|
||||
&save_directive($http, [ $server ], [ ]);
|
||||
}
|
||||
push(@servers, $server);
|
||||
&flush_config_file_lines();
|
||||
&unlock_all_config_files();
|
||||
}
|
||||
foreach my $server (@servers) {
|
||||
&save_directive($http, [ $server ], [ ]);
|
||||
}
|
||||
&flush_config_file_lines();
|
||||
&unlock_all_config_files();
|
||||
foreach my $server (@servers) {
|
||||
&delete_server_link($server);
|
||||
}
|
||||
@@ -57,5 +129,12 @@ foreach my $server (@servers) {
|
||||
next if ($done_file{$server->{'file'}}++);
|
||||
&delete_server_file_if_empty($server);
|
||||
}
|
||||
&webmin_log("delete", "servers", scalar(@servers));
|
||||
foreach my $file (keys %file_servers) {
|
||||
&lock_file($file);
|
||||
&delete_servers_from_file($file, @{$file_servers{$file}});
|
||||
&unlock_file($file);
|
||||
}
|
||||
my $count = scalar(@servers) +
|
||||
scalar(map { @$_ } values %file_servers);
|
||||
&webmin_log("delete", "servers", $count);
|
||||
&redirect("");
|
||||
|
||||
@@ -6,13 +6,14 @@ use warnings;
|
||||
require './nginx-lib.pl';
|
||||
&ReadParse();
|
||||
our (%text, %in, %access);
|
||||
$access{'global'} || &error($text{'index_eglobal'});
|
||||
&can_edit_manual_config() || &error($text{'manual_ecannot'});
|
||||
|
||||
&ui_print_header(undef, $text{'manual_title'}, "");
|
||||
|
||||
my @files = &get_all_config_files();
|
||||
my @files = &get_manual_config_files();
|
||||
$in{'file'} ||= $files[0];
|
||||
&indexof($in{'file'}, @files) >= 0 || &error($text{'manual_efile'});
|
||||
$in{'file'} = &resolve_manual_config_file($in{'file'}, @files) ||
|
||||
&error($text{'manual_efile'});
|
||||
|
||||
# Show file selector
|
||||
print &ui_form_start("edit_manual.cgi");
|
||||
@@ -38,4 +39,3 @@ print &ui_table_end();
|
||||
print &ui_form_end([ [ undef, $text{'save'} ] ]);
|
||||
|
||||
&ui_print_footer("", $text{'index_return'});
|
||||
|
||||
|
||||
@@ -28,10 +28,18 @@ if ($in{'id'}) {
|
||||
my @spages = ( $access{'logs'} ? ( "slogs" ) : ( ),
|
||||
"sdocs", "ssl", "fcgi", "sssi", "sgzip", "sproxy",
|
||||
"saccess", "srewrite", );
|
||||
my @slinks = map { "edit_".$_.".cgi?id=".&urlize($in{'id'}) } @spages;
|
||||
my @stitles = map { $text{$_."_title"} } @spages;
|
||||
my @sicons = map { "images/".$_.".gif" } @spages;
|
||||
if (&can_edit_manual_config() && &can_edit_manual_file($server->{'file'})) {
|
||||
push(@slinks, "edit_manual.cgi?file=".&urlize($server->{'file'}));
|
||||
push(@stitles, $text{'manual_server'});
|
||||
push(@sicons, "images/manual.gif");
|
||||
}
|
||||
&icons_table(
|
||||
[ map { "edit_".$_.".cgi?id=".&urlize($in{'id'}) } @spages ],
|
||||
[ map { $text{$_."_title"} } @spages ],
|
||||
[ map { "images/".$_.".gif" } @spages ],
|
||||
\@slinks,
|
||||
\@stitles,
|
||||
\@sicons,
|
||||
);
|
||||
|
||||
# Show table for locations
|
||||
|
||||
101
nginx/index.cgi
101
nginx/index.cgi
@@ -48,7 +48,8 @@ print &ui_tabs_start(\@tabs, "mode", $mode, 1);
|
||||
if ($access{'global'}) {
|
||||
# Show icons for global config types
|
||||
print &ui_tabs_start_tab("mode", "global");
|
||||
my @gpages = ( "net", "mime", "logs", "docs", "ssi", "misc", "manual" );
|
||||
my @gpages = ( "net", "mime", "logs", "docs", "ssi", "misc",
|
||||
&can_edit_manual_config() ? ( "manual" ) : ( ) );
|
||||
&icons_table(
|
||||
[ map { "edit_".$_.".cgi" } @gpages ],
|
||||
[ map { $text{$_."_title"} } @gpages ],
|
||||
@@ -60,21 +61,41 @@ if ($access{'global'}) {
|
||||
# Show list of server blocks
|
||||
print &ui_tabs_start_tab("mode", "list");
|
||||
my @allservers = &find("server", $http);
|
||||
my @servers = grep { &can_edit_server($_) } @allservers;
|
||||
if (@servers) {
|
||||
my $can_files = &can_manage_server_files();
|
||||
my @add_to_files = $can_files ? &get_add_to_files() : ( );
|
||||
my %add_to_file = map { $_, 1 } @add_to_files;
|
||||
my @rows = &get_server_list_rows($http);
|
||||
if (@rows) {
|
||||
my $can_delete = $access{'edit'};
|
||||
my $has_proxy;
|
||||
foreach my $r (@rows) {
|
||||
my (undef, $proxy) = &server_root_proxy_state($r->{'server'});
|
||||
$has_proxy ||= $proxy;
|
||||
}
|
||||
my @heads = ( $can_delete ? ( "" ) : ( ),
|
||||
$text{'index_name'},
|
||||
$text{'index_ip'},
|
||||
$text{'index_port'},
|
||||
$text{'index_root'} );
|
||||
$text{'index_root'},
|
||||
$has_proxy ? ( $text{'index_proxytarget'} ) : ( ),
|
||||
$can_files ? ( $text{'index_status'} ) : ( ),
|
||||
$text{'index_url'} );
|
||||
my @data;
|
||||
foreach my $s (@servers) {
|
||||
foreach my $r (@rows) {
|
||||
my $s = $r->{'server'};
|
||||
my $name = &find_value("server_name", $s);
|
||||
$name ||= "";
|
||||
my $default = &is_default_server_block($s);
|
||||
my $showname = !$default ?
|
||||
&html_escape($name) : $text{'default_server_block'};
|
||||
my $id = &server_id($s);
|
||||
my $name_sort = ($default ? "0 " : "1 ").
|
||||
lc($default ? $text{'default_server_block'} : $name);
|
||||
my $name_sort_attr = "e_escape($name_sort);
|
||||
my $shownamelink = $r->{'active'} ?
|
||||
"<a href='edit_server.cgi?id=".
|
||||
&urlize($id)."'>".$showname."</a>" :
|
||||
$showname;
|
||||
|
||||
# Extract all IPs and ports from listen directives
|
||||
my (@ips, @ports);
|
||||
@@ -86,43 +107,67 @@ if (@servers) {
|
||||
push(@ports, $port);
|
||||
}
|
||||
|
||||
my $rootdir = &find_value("root", $s);
|
||||
my $root = $rootdir;
|
||||
if (!$root) {
|
||||
my @locs = &find("location", $s);
|
||||
my ($rootloc) = grep { $_->{'value'} eq '/' } @locs;
|
||||
if ($rootloc) {
|
||||
$rootdir = &find_value("root", $rootloc);
|
||||
$root = $rootdir ||
|
||||
"<i>$text{'index_noroot'}</i>";
|
||||
my @cols;
|
||||
my $status = "";
|
||||
if ($can_files) {
|
||||
if ($add_to_file{$r->{'file'}}) {
|
||||
my $enabled =
|
||||
&server_file_state($r->{'file'})->{'enabled'};
|
||||
$status = $enabled ? $text{'index_enabled'} :
|
||||
$text{'index_disabled'};
|
||||
}
|
||||
else {
|
||||
$root = "<i>$text{'index_norootloc'}</i>";
|
||||
}
|
||||
$rootdir ||= "";
|
||||
}
|
||||
my $id = $name.";".$rootdir;
|
||||
my @cols = (
|
||||
"<a href='edit_server.cgi?id=".&urlize($id)."'>".
|
||||
$showname."</a>",
|
||||
push(@cols, { 'type' => 'string',
|
||||
'value' => $shownamelink,
|
||||
'td' => "data-sort=\"$name_sort_attr\" ".
|
||||
"data-order=\"$name_sort_attr\"" });
|
||||
push(@cols,
|
||||
join("<br>", @ips),
|
||||
join("<br>", @ports),
|
||||
$root );
|
||||
if ($can_delete && !$default) {
|
||||
&server_root_summary($s) );
|
||||
push(@cols, &server_proxy_summary($s)) if ($has_proxy);
|
||||
push(@cols, $status) if ($can_files);
|
||||
my $url = $r->{'active'} ? &server_url($s) : undef;
|
||||
push(@cols, $url ? &ui_link("e_escape($url),
|
||||
$text{'index_view'}, undef,
|
||||
'target="_blank" rel="noopener noreferrer"') : "");
|
||||
if ($can_delete && $r->{'active'} && !$default) {
|
||||
unshift(@cols, { 'type' => 'checkbox',
|
||||
'name' => 'd',
|
||||
'value' => $id });
|
||||
}
|
||||
elsif ($can_delete && $can_files && !$r->{'active'} &&
|
||||
!$default && $add_to_file{$r->{'file'}}) {
|
||||
unshift(@cols, { 'type' => 'checkbox',
|
||||
'name' => 'd',
|
||||
'value' => "file\t".$r->{'file'}.
|
||||
"\t".$s->{'line'} });
|
||||
}
|
||||
elsif ($can_delete) {
|
||||
unshift(@cols, "");
|
||||
}
|
||||
push(@data, \@cols);
|
||||
}
|
||||
if ($can_delete) {
|
||||
print &ui_form_columns_table(
|
||||
"delete_servers.cgi",
|
||||
[ [ "delete", $text{'index_delete'} ] ],
|
||||
1, [ ], [ ], \@heads, 100, \@data);
|
||||
my $list_form = "server_blocks_form";
|
||||
my $has_checkbox = grep {
|
||||
ref($_->[0]) && $_->[0]->{'type'} eq 'checkbox'
|
||||
} @data;
|
||||
my $links = $has_checkbox ?
|
||||
&ui_links_row([ &select_all_link("d", 0),
|
||||
&select_invert_link("d", 0) ]) : "";
|
||||
my @left_buttons = ( [ "delete", $text{'index_delete'} ] );
|
||||
my @right_buttons = $can_files ?
|
||||
( [ "toggle", $text{'index_toggle'}, undef, undef,
|
||||
"form=\"$list_form\"" ] ) : ( );
|
||||
print &ui_form_start("delete_servers.cgi", "post", undef,
|
||||
"id='$list_form'");
|
||||
print $links;
|
||||
print &ui_columns_table(\@heads, 100, \@data);
|
||||
print $links;
|
||||
print &ui_form_end_side_by_side($list_form,
|
||||
\@left_buttons,
|
||||
\@right_buttons);
|
||||
}
|
||||
else {
|
||||
print &ui_columns_table(\@heads, 100, \@data);
|
||||
|
||||
@@ -2,15 +2,22 @@ index_version=Nginx version $1
|
||||
index_econfig=The Nginx configuration file $1 was not found on your system. Use the <a href='$2'>module configuration</a> page to enter the correct path.
|
||||
index_ecmd=The Nginx command $1 was not found on your system. Use the <a href='$2'>module configuration</a> page to enter the correct path.
|
||||
index_ehttp=No <tt>http</tt> section was found in your Nginx config file $1. Maybe it is not setup as a webserver?
|
||||
index_name=Server block name
|
||||
index_name=Server Block Name
|
||||
default_server_block=Default Server
|
||||
index_ip=IP addresses
|
||||
index_port=Port numbers
|
||||
index_root=Root directory
|
||||
index_status=State
|
||||
index_ip=Address
|
||||
index_port=Port
|
||||
index_root=Root Directory
|
||||
index_proxytarget=Proxy Target
|
||||
index_url=URL
|
||||
index_view=Open..
|
||||
index_toggle=Toggle State
|
||||
index_enabled=Enabled
|
||||
index_disabled=Disabled
|
||||
index_any=Any IPv4 address
|
||||
index_any6=Any IPv6 address
|
||||
index_noroot=No root location
|
||||
index_norootloc=Not a directory
|
||||
index_noroot=No root directory
|
||||
index_noproxy=No proxy target
|
||||
index_none=No server blocks have been created yet.
|
||||
index_noneaccess=No server blocks that you have access to have been created yet.
|
||||
index_add=Add a new Nginx server block.
|
||||
@@ -25,6 +32,7 @@ index_stopdesc=Shut down all running Nginx webserver processes.
|
||||
index_start=Start Nginx Webserver
|
||||
index_startdesc=Start up the Nginx webserver using the current configuration.
|
||||
index_restart=Apply Nginx Configuration
|
||||
index_apply_changes=Apply Changes
|
||||
index_restartdesc=Apply the current configuration by stopping and re-starting the Nginx webserver.
|
||||
index_delete=Delete Selected Server Blocks
|
||||
index_eglobal=You are not allowed to edit global settings
|
||||
@@ -143,12 +151,14 @@ opt_essi_types=$1 is not a valid MIME type
|
||||
opt_essi_value_length=Maximum parameter length must be a number
|
||||
|
||||
manual_title=Edit Configuration Files
|
||||
manual_server=Edit Configuration File
|
||||
manual_file=Editing configuration file:
|
||||
manual_ok=Switch
|
||||
manual_efile=Selected file is not part of the Nginx configuration!
|
||||
manual_test=Test new configuration before saving?
|
||||
manual_elink=Dangling symbolic link!
|
||||
manual_err=Failed to save configuration file
|
||||
manual_ecannot=You are not allowed to manually edit configuration files!
|
||||
|
||||
server_create=Create a New Server Block
|
||||
server_edit=Edit Server Block
|
||||
@@ -200,8 +210,21 @@ server_eclash=A server block with the same name already exists
|
||||
server_pp=Proxy to $1
|
||||
server_eexist=No Nginx server found
|
||||
delete_err=Failed to delete server blocks
|
||||
delete_eaction=No action was selected
|
||||
delete_enone=No server blocks were selected to delete
|
||||
delete_edefault=The default server block cannot be deleted
|
||||
enable_err=Failed to change server file state
|
||||
enable_enone=No manageable server files were selected
|
||||
enable_efile=Server file does not exist or cannot be managed
|
||||
enable_elinkdir=No enabled server-block links directory is configured
|
||||
enable_elink=Failed to create symbolic link $1 : $2
|
||||
enable_eunlink=Failed to remove symbolic link $1 : $2
|
||||
enable_elinkexists=The symbolic link $1 already exists
|
||||
enable_etest=Nginx configuration test failed after changing the server file state : $1
|
||||
enable_evirtualmin_disable=This Nginx server block is managed by Virtualmin virtual server $1, which is currently $2. Site disabling should be done in Virtualmin using $3.
|
||||
enable_evirtualmin_enable=This Nginx server block is managed by Virtualmin virtual server $1, which is currently $2. Site enabling should be done in Virtualmin using $3.
|
||||
enable_virtualmin_disable_label=Disable and Delete ⇾ Disable Virtual Server
|
||||
enable_virtualmin_enable_label=Disable and Delete ⇾ Enable Virtual Server
|
||||
|
||||
slogs_title=Server Block Logging
|
||||
slogs_header=Log file options
|
||||
@@ -334,6 +357,10 @@ log_manual=Manually edited config file $1
|
||||
log_create_server=Created server block $1
|
||||
log_modify_server=Modified server block $1
|
||||
log_delete_server=Deleted server block $1
|
||||
log_delete_servers=Deleted $1 server blocks
|
||||
log_toggle_serverfile=Toggled state of $1 server block files
|
||||
log_enable_serverfile=Enabled $1 server block files
|
||||
log_disable_serverfile=Disabled $1 server block files
|
||||
log_slogs_server=Changed logging options for $1
|
||||
log_ssl_server=Change SSL configuration for $1
|
||||
log_sdocs_server=Changed document options for $1
|
||||
@@ -398,6 +425,7 @@ acl_create=Can create server blocks?
|
||||
acl_stop=Can stop and start Nginx?
|
||||
acl_root=Allowed directories for locations
|
||||
acl_global=Can edit global settings?
|
||||
acl_manual=Can manually edit raw configuration files?
|
||||
acl_logs=Can configure log files?
|
||||
acl_user=Write password files as user
|
||||
|
||||
|
||||
@@ -26,6 +26,12 @@ elsif ($type eq 'server') {
|
||||
return &text('log_'.$action.'_server',
|
||||
"<tt>".&html_escape($object)."</tt>");
|
||||
}
|
||||
elsif ($type eq 'servers') {
|
||||
return &text('log_'.$action.'_servers', $object);
|
||||
}
|
||||
elsif ($type eq 'serverfile') {
|
||||
return &text('log_'.$action.'_serverfile', $object);
|
||||
}
|
||||
elsif ($type eq 'location') {
|
||||
return &text('log_'.$action.'_location',
|
||||
"<tt>".&html_escape($object)."</tt>",
|
||||
@@ -41,4 +47,3 @@ else {
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,11 @@ eval "use WebminCore;";
|
||||
our %access = &get_module_acl();
|
||||
our ($get_config_cache, $get_config_parent_cache, %list_directives_cache,
|
||||
@list_modules_cache, @open_config_files);
|
||||
our (%config, %text, %in, $module_root_directory);
|
||||
our ($last_config_change_flag, $last_restart_time_flag);
|
||||
our (%config, %text, %in, $module_root_directory, $module_var_directory);
|
||||
&set_nginx_config_defaults();
|
||||
$last_config_change_flag = $module_var_directory."/config-flag";
|
||||
$last_restart_time_flag = $module_var_directory."/restart-flag";
|
||||
|
||||
my @lock_all_config_files_cache;
|
||||
|
||||
@@ -522,9 +525,11 @@ if ($parent->{'type'}) {
|
||||
sub flush_config_file_lines
|
||||
{
|
||||
my ($parent) = @_;
|
||||
foreach my $f (&unique(@open_config_files)) {
|
||||
my @files = &unique(@open_config_files);
|
||||
foreach my $f (@files) {
|
||||
&flush_file_lines($f);
|
||||
}
|
||||
&update_last_config_change() if (@files);
|
||||
@open_config_files = ( );
|
||||
}
|
||||
|
||||
@@ -565,6 +570,29 @@ if ($parent->{'type'}) {
|
||||
return &unique(@rv);
|
||||
}
|
||||
|
||||
# get_manual_config_files([&parent])
|
||||
# Returns all config files this user can manually edit
|
||||
sub get_manual_config_files
|
||||
{
|
||||
my ($parent) = @_;
|
||||
my @files = map { &resolve_links($_) || $_ } &get_all_config_files($parent);
|
||||
@files = &unique(@files);
|
||||
return @files if (!$access{'vhosts'});
|
||||
return grep { &can_edit_manual_file($_) } @files;
|
||||
}
|
||||
|
||||
# resolve_manual_config_file(file, [files...])
|
||||
# Resolves a submitted manual config file path, if it is allowed
|
||||
sub resolve_manual_config_file
|
||||
{
|
||||
my ($file, @files) = @_;
|
||||
return undef if (!$file);
|
||||
@files = &get_manual_config_files() if (!@files);
|
||||
my $rfile = &resolve_links($file);
|
||||
$rfile ||= $file;
|
||||
return &indexof($rfile, @files) >= 0 ? $rfile : undef;
|
||||
}
|
||||
|
||||
# directive_indent(&directive, &parent, &file-lines)
|
||||
# Returns the exact whitespace prefix to use when writing a directive
|
||||
sub directive_indent
|
||||
@@ -1480,7 +1508,11 @@ return $? ? $out : undef;
|
||||
sub start_nginx
|
||||
{
|
||||
my $out = &backquote_logged("$config{'start_cmd'} 2>&1 </dev/null");
|
||||
return $? ? $out : undef;
|
||||
if ($?) {
|
||||
return $out;
|
||||
}
|
||||
&update_last_restart_time();
|
||||
return undef;
|
||||
}
|
||||
|
||||
# apply_nginx()
|
||||
@@ -1489,7 +1521,11 @@ return $? ? $out : undef;
|
||||
sub apply_nginx
|
||||
{
|
||||
my $out = &backquote_logged("$config{'apply_cmd'} 2>&1 </dev/null");
|
||||
return $? ? $out : undef;
|
||||
if ($?) {
|
||||
return $out;
|
||||
}
|
||||
&update_last_restart_time();
|
||||
return undef;
|
||||
}
|
||||
|
||||
# nginx_action_links()
|
||||
@@ -1502,7 +1538,11 @@ if (&is_nginx_running()) {
|
||||
if ($access{'stop'}) {
|
||||
push(@rv, &ui_link("stop.cgi?$args", $text{'index_stop'}));
|
||||
}
|
||||
push(@rv, &ui_link("restart.cgi?$args", $text{'index_restart'}));
|
||||
my $needs = &needs_config_restart();
|
||||
my $apply = $text{'index_apply_changes'} || $text{'index_restart'};
|
||||
my $label = $needs ? "<b>$apply</b>" : $apply;
|
||||
my $url = "restart.cgi?$args";
|
||||
push(@rv, &ui_link($url, $label));
|
||||
}
|
||||
elsif ($access{'stop'}) {
|
||||
push(@rv, &ui_link("start.cgi?$args", $text{'index_start'}));
|
||||
@@ -1510,6 +1550,33 @@ elsif ($access{'stop'}) {
|
||||
return join("<br>\n", @rv);
|
||||
}
|
||||
|
||||
# update_last_config_change()
|
||||
# Updates the flag file indicating when the config was changed
|
||||
sub update_last_config_change
|
||||
{
|
||||
&open_lock_tempfile(my $fh, ">$last_config_change_flag", 0, 1);
|
||||
&close_tempfile($fh);
|
||||
}
|
||||
|
||||
# update_last_restart_time()
|
||||
# Updates the flag file indicating when the config was applied
|
||||
sub update_last_restart_time
|
||||
{
|
||||
&open_lock_tempfile(my $fh, ">$last_restart_time_flag", 0, 1);
|
||||
&close_tempfile($fh);
|
||||
}
|
||||
|
||||
# needs_config_restart()
|
||||
# Returns 1 if a restart is needed after a config change
|
||||
sub needs_config_restart
|
||||
{
|
||||
my @cst = stat($last_config_change_flag);
|
||||
my @rst = stat($last_restart_time_flag);
|
||||
return 0 if (!@cst);
|
||||
return 1 if (!@rst);
|
||||
return $cst[9] > $rst[9] ? 1 : 0;
|
||||
}
|
||||
|
||||
# this_url()
|
||||
# Returns the current module URL
|
||||
sub this_url
|
||||
@@ -1530,6 +1597,485 @@ my $out = &backquote_logged("$config{'nginx_cmd'} -t 2>&1 </dev/null");
|
||||
return $? || $out !~ /syntax\s+is\s+ok/ ? $out : undef;
|
||||
}
|
||||
|
||||
# can_manage_server_files()
|
||||
# Returns 1 if this system uses Debian-style available/enabled site dirs
|
||||
sub can_manage_server_files
|
||||
{
|
||||
return $config{'add_to'} && -d $config{'add_to'} &&
|
||||
$config{'add_link'} && -d $config{'add_link'};
|
||||
}
|
||||
|
||||
# get_add_to_files()
|
||||
# Returns config files from the directory used for new server blocks
|
||||
sub get_add_to_files
|
||||
{
|
||||
my @rv;
|
||||
if ($config{'add_to'} && -d $config{'add_to'}) {
|
||||
opendir(ADDTO, $config{'add_to'}) || return @rv;
|
||||
foreach my $f (sort { lc($a) cmp lc($b) } readdir(ADDTO)) {
|
||||
next if ($f eq "." || $f eq "..");
|
||||
my $file = $config{'add_to'}."/".$f;
|
||||
my $rfile = &resolve_links($file);
|
||||
next if (!$rfile || !-f $rfile || !-r $rfile);
|
||||
push(@rv, $rfile);
|
||||
}
|
||||
closedir(ADDTO);
|
||||
}
|
||||
return &unique(@rv);
|
||||
}
|
||||
|
||||
# find_servers_in_file(file)
|
||||
# Returns server blocks parsed from one config file
|
||||
sub find_servers_in_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &resolve_links($file);
|
||||
$rfile ||= $file;
|
||||
return ( ) if (!-r $rfile);
|
||||
my $conf = &read_config_file($rfile);
|
||||
return grep { $_->{'file'} eq $rfile } &find_recursive("server", $conf);
|
||||
}
|
||||
|
||||
# can_manage_server_file(file)
|
||||
# Returns 1 if all server blocks in a file are manageable by this user
|
||||
sub can_manage_server_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &resolve_links($file);
|
||||
$rfile ||= $file;
|
||||
return 0 if (!$rfile || !-f $rfile || !-r $rfile);
|
||||
my @servers = &find_servers_in_file($rfile);
|
||||
return 0 if (!@servers);
|
||||
foreach my $server (@servers) {
|
||||
return 0 if (!&can_edit_server($server));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
# delete_servers_from_file(file, &servers...)
|
||||
# Deletes server blocks from one config file and removes the file if empty
|
||||
sub delete_servers_from_file
|
||||
{
|
||||
my ($file, @servers) = @_;
|
||||
return 0 if (!@servers);
|
||||
my $lref = &read_file_lines($file);
|
||||
foreach my $server (sort { $b->{'line'} <=> $a->{'line'} } @servers) {
|
||||
my $len = $server->{'eline'} - $server->{'line'} + 1;
|
||||
splice(@$lref, $server->{'line'}, $len);
|
||||
}
|
||||
my $empty = 1;
|
||||
foreach my $line (@$lref) {
|
||||
if ($line =~ /\S/) {
|
||||
$empty = 0;
|
||||
last;
|
||||
}
|
||||
}
|
||||
&flush_file_lines($file);
|
||||
if ($empty) {
|
||||
foreach my $link (&server_file_links($file)) {
|
||||
&unlink_logged($link);
|
||||
}
|
||||
&unlink_logged($file);
|
||||
}
|
||||
&update_last_config_change();
|
||||
return scalar(@servers);
|
||||
}
|
||||
|
||||
# get_server_list_rows(&http)
|
||||
# Returns row hashes for the server blocks list, preserving sites-available order
|
||||
sub get_server_list_rows
|
||||
{
|
||||
my ($http) = @_;
|
||||
my @allservers = &find("server", $http);
|
||||
my @servers = grep { &can_edit_server($_) } @allservers;
|
||||
my $default_first = sub {
|
||||
return ( grep { &is_default_server_block($_->{'server'}) } @_ ),
|
||||
( grep { !&is_default_server_block($_->{'server'}) } @_ );
|
||||
};
|
||||
if (&can_manage_server_files()) {
|
||||
my @rows;
|
||||
my %active_by_file;
|
||||
foreach my $s (@servers) {
|
||||
my $file = &resolve_links($s->{'file'});
|
||||
$file ||= $s->{'file'};
|
||||
push(@{$active_by_file{$file}}, $s);
|
||||
}
|
||||
my %done_server;
|
||||
foreach my $file (&get_add_to_files()) {
|
||||
my @fileservers = @{$active_by_file{$file} || [ ]};
|
||||
my $active = @fileservers ? 1 : 0;
|
||||
if (!@fileservers) {
|
||||
@fileservers = grep { &can_edit_server($_) }
|
||||
&find_servers_in_file($file);
|
||||
}
|
||||
foreach my $s (@fileservers) {
|
||||
push(@rows, { 'server' => $s,
|
||||
'active' => $active,
|
||||
'file' => $file });
|
||||
$done_server{$s}++;
|
||||
}
|
||||
}
|
||||
foreach my $s (@servers) {
|
||||
next if ($done_server{$s});
|
||||
push(@rows, { 'server' => $s,
|
||||
'active' => 1,
|
||||
'file' => $s->{'file'} });
|
||||
}
|
||||
return &$default_first(@rows);
|
||||
}
|
||||
return &$default_first(
|
||||
map { { 'server' => $_, 'active' => 1, 'file' => $_->{'file'} } }
|
||||
@servers);
|
||||
}
|
||||
|
||||
# server_file_link(file)
|
||||
# Returns the enabled symlink path for a server file
|
||||
sub server_file_link
|
||||
{
|
||||
my ($file) = @_;
|
||||
return undef if (!&can_manage_server_files());
|
||||
my $short = $file;
|
||||
$short =~ s/^.*\///;
|
||||
return $config{'add_link'}."/".$short;
|
||||
}
|
||||
|
||||
# server_file_links(file)
|
||||
# Returns enabled symlinks for a server file
|
||||
sub server_file_links
|
||||
{
|
||||
my ($file) = @_;
|
||||
my @rv;
|
||||
return @rv if (!&can_manage_server_files());
|
||||
my $rfile = &resolve_links($file);
|
||||
$rfile ||= $file;
|
||||
opendir(LINKDIR, $config{'add_link'}) || return @rv;
|
||||
foreach my $f (readdir(LINKDIR)) {
|
||||
next if ($f eq "." || $f eq "..");
|
||||
my $link = $config{'add_link'}."/".$f;
|
||||
next if (!-l $link);
|
||||
my $rlink = &resolve_links($link);
|
||||
if ($rlink && $rlink eq $rfile) {
|
||||
push(@rv, $link);
|
||||
}
|
||||
}
|
||||
closedir(LINKDIR);
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# server_file_enabled(file)
|
||||
# Returns 1 if a server file has an enabled symlink
|
||||
sub server_file_enabled
|
||||
{
|
||||
my ($file) = @_;
|
||||
return scalar(&server_file_links($file)) ? 1 : 0;
|
||||
}
|
||||
|
||||
# enable_server_file(file)
|
||||
# Enables a server file and rolls back if nginx -t fails
|
||||
sub enable_server_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $rfile = &resolve_links($file);
|
||||
$rfile ||= $file;
|
||||
my $link = &server_file_link($rfile);
|
||||
$link || return $text{'enable_elinkdir'};
|
||||
return undef if (&server_file_enabled($rfile));
|
||||
if (-e $link || -l $link) {
|
||||
return &text('enable_elinkexists', "<tt>".&html_escape($link)."</tt>");
|
||||
}
|
||||
&symlink_logged($rfile, $link) ||
|
||||
return &text('enable_elink', "<tt>".&html_escape($link)."</tt>",
|
||||
"<tt>".&html_escape($!)."</tt>");
|
||||
my $err = &test_config();
|
||||
if ($err) {
|
||||
&unlink_logged($link);
|
||||
return &text('enable_etest', "<tt>".&html_escape($err)."</tt>");
|
||||
}
|
||||
&update_last_config_change();
|
||||
return undef;
|
||||
}
|
||||
|
||||
# disable_server_file(file)
|
||||
# Disables a server file and rolls back if nginx -t fails
|
||||
sub disable_server_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
my @links = &server_file_links($file);
|
||||
return undef if (!@links);
|
||||
my @restore = map { [ $_, readlink($_) ] } @links;
|
||||
my @removed;
|
||||
foreach my $link (@links) {
|
||||
if (!&unlink_logged($link)) {
|
||||
foreach my $r (@removed) {
|
||||
&symlink_logged($r->[1], $r->[0])
|
||||
if (defined($r->[1]) && !-e $r->[0] && !-l $r->[0]);
|
||||
}
|
||||
return &text('enable_eunlink',
|
||||
"<tt>".&html_escape($link)."</tt>",
|
||||
"<tt>".&html_escape($!)."</tt>");
|
||||
}
|
||||
my ($restore) = grep { $_->[0] eq $link } @restore;
|
||||
push(@removed, $restore) if ($restore);
|
||||
}
|
||||
my $err = &test_config();
|
||||
if ($err) {
|
||||
foreach my $r (@restore) {
|
||||
&symlink_logged($r->[1], $r->[0])
|
||||
if (defined($r->[1]) && !-e $r->[0] && !-l $r->[0]);
|
||||
}
|
||||
return &text('enable_etest', "<tt>".&html_escape($err)."</tt>");
|
||||
}
|
||||
&update_last_config_change();
|
||||
return undef;
|
||||
}
|
||||
|
||||
# virtualmin_available()
|
||||
# Returns 1 if Virtualmin is installed and supported on this system
|
||||
sub virtualmin_available
|
||||
{
|
||||
return $main::nginx_virtualmin_available
|
||||
if (defined($main::nginx_virtualmin_available));
|
||||
$main::nginx_virtualmin_available = &foreign_check("virtual-server");
|
||||
return $main::nginx_virtualmin_available;
|
||||
}
|
||||
|
||||
# virtualmin_domain_by_name(name)
|
||||
# Returns a Virtualmin domain object by domain name, if one exists
|
||||
sub virtualmin_domain_by_name
|
||||
{
|
||||
my ($name) = @_;
|
||||
return undef if (!&virtualmin_available());
|
||||
return $main::nginx_virtualmin_domain_by_name_cache{$name}
|
||||
if (exists($main::nginx_virtualmin_domain_by_name_cache{$name}));
|
||||
&foreign_require("virtual-server");
|
||||
my $d = &virtual_server::get_domain_by("dom", $name);
|
||||
$main::nginx_virtualmin_domain_by_name_cache{$name} = $d;
|
||||
return $d;
|
||||
}
|
||||
|
||||
# server_names(&server)
|
||||
# Returns all names from server_name directives in a server block
|
||||
sub server_names
|
||||
{
|
||||
my ($server) = @_;
|
||||
my @rv;
|
||||
foreach my $sn (&find("server_name", $server)) {
|
||||
push(@rv, @{$sn->{'words'} || [ ]});
|
||||
if (!@{$sn->{'words'} || [ ]} && $sn->{'value'}) {
|
||||
push(@rv, $sn->{'value'});
|
||||
}
|
||||
}
|
||||
return grep { $_ && $_ ne "_" && $_ ne "-" } &unique(@rv);
|
||||
}
|
||||
|
||||
# virtualmin_domain_for_server_file(file)
|
||||
# Returns the Virtualmin domain object for a server file, if any
|
||||
sub virtualmin_domain_for_server_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
return undef if (!&virtualmin_available());
|
||||
my $rfile = &resolve_links($file);
|
||||
$rfile ||= $file;
|
||||
return $main::nginx_virtualmin_domain_for_file_cache{$rfile}
|
||||
if (exists($main::nginx_virtualmin_domain_for_file_cache{$rfile}));
|
||||
foreach my $server (&find_servers_in_file($file)) {
|
||||
foreach my $name (&server_names($server)) {
|
||||
my $d = &virtualmin_domain_by_name($name);
|
||||
if ($d) {
|
||||
$main::nginx_virtualmin_domain_for_file_cache{$rfile} = $d;
|
||||
return $d;
|
||||
}
|
||||
}
|
||||
}
|
||||
$main::nginx_virtualmin_domain_for_file_cache{$rfile} = undef;
|
||||
return undef;
|
||||
}
|
||||
|
||||
# server_file_state(file)
|
||||
# Returns the effective enabled state for a server file
|
||||
sub server_file_state
|
||||
{
|
||||
my ($file) = @_;
|
||||
my $d = &virtualmin_domain_for_server_file($file);
|
||||
if ($d) {
|
||||
return { 'enabled' => $d->{'disabled'} ? 0 : 1,
|
||||
'source' => 'virtualmin',
|
||||
'domain' => $d };
|
||||
}
|
||||
return { 'enabled' => &server_file_enabled($file) ? 1 : 0,
|
||||
'source' => 'nginx' };
|
||||
}
|
||||
|
||||
# server_file_toggle_action(file)
|
||||
# Returns the action needed to toggle a server file's effective state
|
||||
sub server_file_toggle_action
|
||||
{
|
||||
my ($file) = @_;
|
||||
return &server_file_state($file)->{'enabled'} ? "disable" : "enable";
|
||||
}
|
||||
|
||||
# virtualmin_domain_state_link(&domain, enabled?)
|
||||
# Returns a link to the Virtualmin state change form for some domain
|
||||
sub virtualmin_domain_state_link
|
||||
{
|
||||
my ($d, $enabled) = @_;
|
||||
my $page = $enabled ? "disable_domain.cgi" : "enable_domain.cgi";
|
||||
my $label = $enabled ? $text{'enable_virtualmin_disable_label'} :
|
||||
$text{'enable_virtualmin_enable_label'};
|
||||
my $url = "../virtual-server/".$page."?dom=".&urlize($d->{'id'});
|
||||
return &ui_link("e_escape($url), "\"".$label."\"");
|
||||
}
|
||||
|
||||
# virtualmin_server_file_state_error(file, action)
|
||||
# Returns an error if a Virtualmin-owned site is being enabled or disabled here
|
||||
sub virtualmin_server_file_state_error
|
||||
{
|
||||
my ($file, $action) = @_;
|
||||
return undef if ($action ne "enable" && $action ne "disable");
|
||||
my $state_info = &server_file_state($file);
|
||||
return undef if ($state_info->{'source'} ne "virtualmin");
|
||||
my $d = $state_info->{'domain'};
|
||||
return undef if (!$d);
|
||||
my $state = lc($state_info->{'enabled'} ? $text{'index_enabled'} :
|
||||
$text{'index_disabled'});
|
||||
my $dom = "<tt>".&html_escape($d->{'dom'})."</tt>";
|
||||
my $link = &virtualmin_domain_state_link($d, $state_info->{'enabled'});
|
||||
return $state_info->{'enabled'} ?
|
||||
&text('enable_evirtualmin_disable', $dom, $state, $link) :
|
||||
&text('enable_evirtualmin_enable', $dom, $state, $link);
|
||||
}
|
||||
|
||||
# proxy_pass_value(&proxy_pass)
|
||||
# Returns the target URL from a proxy_pass directive
|
||||
sub proxy_pass_value
|
||||
{
|
||||
my ($pp) = @_;
|
||||
my @w = @{$pp->{'words'}};
|
||||
return (@w ? $w[0] : undef) || $pp->{'value'};
|
||||
}
|
||||
|
||||
# server_proxy_target(&server|&location)
|
||||
# Returns the first proxy_pass target under a server or location block
|
||||
sub server_proxy_target
|
||||
{
|
||||
my ($conf) = @_;
|
||||
my ($pp) = &find_recursive("proxy_pass", $conf);
|
||||
return undef if (!$pp);
|
||||
return &proxy_pass_value($pp);
|
||||
}
|
||||
|
||||
# server_proxy_pairs(&server)
|
||||
# Returns location path and proxy_pass target pairs for a server block
|
||||
sub server_proxy_pairs
|
||||
{
|
||||
my ($server) = @_;
|
||||
my @rv;
|
||||
foreach my $loc (&find("location", $server)) {
|
||||
my $path = &location_path($loc) || "/";
|
||||
foreach my $pp (&find_recursive("proxy_pass", $loc)) {
|
||||
my $target = &proxy_pass_value($pp);
|
||||
push(@rv, [ $path, $target ])
|
||||
if (defined($target) && $target ne "");
|
||||
}
|
||||
}
|
||||
foreach my $pp (&find("proxy_pass", $server)) {
|
||||
my $target = &proxy_pass_value($pp);
|
||||
push(@rv, [ "/", $target ])
|
||||
if (defined($target) && $target ne "");
|
||||
}
|
||||
return @rv;
|
||||
}
|
||||
|
||||
# server_root_summary(&server)
|
||||
# Returns the root directory for a server block, or a missing-root message
|
||||
sub server_root_summary
|
||||
{
|
||||
my ($server) = @_;
|
||||
my $root = &server_root_value($server);
|
||||
return defined($root) && $root ne "" ? &html_escape($root) :
|
||||
"<i>$text{'index_noroot'}</i>";
|
||||
}
|
||||
|
||||
# server_root_value(&server)
|
||||
# Returns the configured root directory for a server block
|
||||
sub server_root_value
|
||||
{
|
||||
my ($server) = @_;
|
||||
my $root = &find_value("root", $server);
|
||||
return $root if ($root);
|
||||
|
||||
my @locs = &find("location", $server);
|
||||
my ($rootloc) = grep { &location_path($_) eq '/' } @locs;
|
||||
if ($rootloc) {
|
||||
$root = &find_value("root", $rootloc);
|
||||
return $root if ($root);
|
||||
}
|
||||
return undef;
|
||||
}
|
||||
|
||||
# server_proxy_summary(&server)
|
||||
# Returns the most relevant proxy target for a server block
|
||||
sub server_proxy_summary
|
||||
{
|
||||
my ($server) = @_;
|
||||
my @pairs = &server_proxy_pairs($server);
|
||||
return "<i>$text{'index_noproxy'}</i>" if (!@pairs);
|
||||
return join("<br>", map {
|
||||
&html_escape($_->[0])." ⇾ ".&html_escape($_->[1])
|
||||
} @pairs);
|
||||
}
|
||||
|
||||
# server_root_proxy_summary(&server)
|
||||
# Returns the root directory or most relevant proxy target for a server block
|
||||
sub server_root_proxy_summary
|
||||
{
|
||||
my ($server) = @_;
|
||||
my $root = &server_root_value($server);
|
||||
return &html_escape($root) if (defined($root) && $root ne "");
|
||||
my $pp = &server_proxy_target($server);
|
||||
return &text('server_pp', "<tt>".&html_escape($pp)."</tt>")
|
||||
if ($pp);
|
||||
return &server_root_summary($server);
|
||||
}
|
||||
|
||||
# server_root_proxy_state(&server)
|
||||
# Returns booleans for whether a server has root and proxy_pass directives
|
||||
sub server_root_proxy_state
|
||||
{
|
||||
my ($server) = @_;
|
||||
my $has_root = &find_recursive("root", $server) ? 1 : 0;
|
||||
my $has_proxy = &server_proxy_target($server) ? 1 : 0;
|
||||
return ($has_root, $has_proxy);
|
||||
}
|
||||
|
||||
# server_url(&server)
|
||||
# Returns the browser URL for a server block
|
||||
sub server_url
|
||||
{
|
||||
my ($server) = @_;
|
||||
my $name = &find_value("server_name", $server);
|
||||
return undef if (&is_default_server_block($server));
|
||||
return undef if (!$name || $name !~ /^[A-Za-z0-9.-]+$/);
|
||||
|
||||
my ($best_scheme, $best_port);
|
||||
foreach my $l (&find("listen", $server)) {
|
||||
my @w = @{$l->{'words'}};
|
||||
my $addr = shift(@w);
|
||||
next if (!$addr);
|
||||
my (undef, $port) = &split_ip_port($addr);
|
||||
my $ssl = grep { $_ eq "ssl" } @w;
|
||||
my $scheme = $ssl || $port == 443 ? "https" : "http";
|
||||
if (!$best_scheme || $scheme eq "https") {
|
||||
($best_scheme, $best_port) = ($scheme, $port);
|
||||
}
|
||||
}
|
||||
$best_scheme ||= "http";
|
||||
$best_port ||= $best_scheme eq "https" ? 443 : 80;
|
||||
$best_port = undef if ($best_scheme eq "http" && $best_port == 80 ||
|
||||
$best_scheme eq "https" && $best_port == 443);
|
||||
return $best_scheme."://".$name.($best_port ? ":".$best_port : "")."/";
|
||||
}
|
||||
|
||||
# find_server(id)
|
||||
# Convenience function to find an HTTP server object with some ID
|
||||
sub find_server
|
||||
@@ -1677,7 +2223,8 @@ if ($config{'add_link'}) {
|
||||
my $link = $server->{'file'};
|
||||
$link =~ s/^.*\///;
|
||||
$link = $config{'add_link'}."/".$link;
|
||||
&symlink_logged($server->{'file'}, $link);
|
||||
&update_last_config_change()
|
||||
if (&symlink_logged($server->{'file'}, $link));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1689,6 +2236,7 @@ sub delete_server_link
|
||||
my ($server) = @_;
|
||||
if ($config{'add_link'}) {
|
||||
my $file = $server->{'file'};
|
||||
my $changed;
|
||||
my $short = $file;
|
||||
$short =~ s/^.*\///;
|
||||
opendir(LINKDIR, $config{'add_link'});
|
||||
@@ -1696,10 +2244,11 @@ if ($config{'add_link'}) {
|
||||
if ($f ne "." && $f ne ".." &&
|
||||
(&resolve_links($config{'add_link'}."/".$f) eq $file ||
|
||||
$short eq $f)) {
|
||||
&unlink_logged($config{'add_link'}."/".$f);
|
||||
$changed++ if (&unlink_logged($config{'add_link'}."/".$f));
|
||||
}
|
||||
}
|
||||
closedir(LINKDIR);
|
||||
&update_last_config_change() if ($changed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1714,7 +2263,8 @@ foreach my $l (@$lref) {
|
||||
$count++ if ($l =~ /\S/);
|
||||
}
|
||||
if (!$count) {
|
||||
&unlink_logged($server->{'file'});
|
||||
&update_last_config_change()
|
||||
if (&unlink_logged($server->{'file'}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1775,6 +2325,22 @@ return 0 if (!$name);
|
||||
return &indexoflc($name, split(/\s+/, $access{'vhosts'})) >= 0;
|
||||
}
|
||||
|
||||
# can_edit_manual_config()
|
||||
# Returns 1 if the user can manually edit raw configuration files
|
||||
sub can_edit_manual_config
|
||||
{
|
||||
return defined($access{'manual'}) ? $access{'manual'} : $access{'global'};
|
||||
}
|
||||
|
||||
# can_edit_manual_file(file)
|
||||
# Returns 1 if the user can manually edit some raw configuration file
|
||||
sub can_edit_manual_file
|
||||
{
|
||||
my ($file) = @_;
|
||||
return 1 if (!$access{'vhosts'});
|
||||
return &can_manage_server_file($file);
|
||||
}
|
||||
|
||||
# can_directory(dir)
|
||||
# Check if some directory is under one of the allowed roots
|
||||
sub can_directory
|
||||
|
||||
@@ -7,16 +7,11 @@ require './nginx-lib.pl';
|
||||
&ReadParseMime();
|
||||
our (%text, %in, %access);
|
||||
&error_setup($text{'manual_err'});
|
||||
$access{'global'} || &error($text{'index_eglobal'});
|
||||
&can_edit_manual_config() || &error($text{'manual_ecannot'});
|
||||
|
||||
my @files = &get_all_config_files();
|
||||
&indexof($in{'file'}, @files) >= 0 || &error($text{'manual_efile'});
|
||||
|
||||
# Follow links to get the real file
|
||||
while(-l $in{'file'}) {
|
||||
$in{'file'} = readlink($in{'file'});
|
||||
}
|
||||
$in{'file'} || &error($text{'manual_elink'});
|
||||
my @files = &get_manual_config_files();
|
||||
$in{'file'} = &resolve_manual_config_file($in{'file'}, @files) ||
|
||||
&error($text{'manual_efile'});
|
||||
|
||||
$in{'data'} =~ s/\r//g;
|
||||
my $fh = "CONF";
|
||||
@@ -43,6 +38,6 @@ else {
|
||||
&print_tempfile($fh, $in{'data'});
|
||||
&close_tempfile($fh);
|
||||
}
|
||||
&update_last_config_change();
|
||||
&webmin_log("manual", undef, $in{'file'});
|
||||
&redirect("");
|
||||
|
||||
|
||||
521
nginx/t/server-files.t
Normal file
521
nginx/t/server-files.t
Normal file
@@ -0,0 +1,521 @@
|
||||
#!/usr/bin/perl
|
||||
# Tests for Debian-style Nginx sites-available/sites-enabled handling.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use File::Basename qw(dirname);
|
||||
use File::Path qw(make_path);
|
||||
use File::Spec;
|
||||
use File::Temp qw(tempdir);
|
||||
use Cwd qw(abs_path);
|
||||
|
||||
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..', '..'));
|
||||
my $tmp = abs_path(tempdir(CLEANUP => 1));
|
||||
my $webmin_config = File::Spec->catdir($tmp, 'webmin-config');
|
||||
my $webmin_var = File::Spec->catdir($tmp, 'webmin-var');
|
||||
my $available = File::Spec->catdir($tmp, 'sites-available');
|
||||
my $enabled = File::Spec->catdir($tmp, 'sites-enabled');
|
||||
my $nginx_conf = File::Spec->catfile($tmp, 'nginx.conf');
|
||||
|
||||
make_path($webmin_config, $webmin_var, "$webmin_config/nginx",
|
||||
$available, $enabled);
|
||||
|
||||
sub write_text
|
||||
{
|
||||
my ($file, $text) = @_;
|
||||
open(my $fh, '>', $file) || die "Failed to write $file: $!";
|
||||
print $fh $text;
|
||||
close($fh) || die "Failed to close $file: $!";
|
||||
}
|
||||
|
||||
sub server_conf
|
||||
{
|
||||
my ($name, $body) = @_;
|
||||
return "server {\n".
|
||||
"\tserver_name $name;\n".
|
||||
"\tlisten 80;\n".
|
||||
$body.
|
||||
"}\n";
|
||||
}
|
||||
|
||||
my $alpha = File::Spec->catfile($available, 'alpha.conf');
|
||||
my $beta = File::Spec->catfile($available, 'beta.conf');
|
||||
my $charlie = File::Spec->catfile($available, 'charlie.conf');
|
||||
my $default = File::Spec->catfile($available, 'default');
|
||||
|
||||
write_text($alpha, server_conf('alpha.example', "\troot /srv/alpha;\n"));
|
||||
write_text($beta, server_conf('beta.example',
|
||||
"\tlocation / {\n".
|
||||
"\t\tproxy_pass http://127.0.0.1:8080;\n".
|
||||
"\t}\n"));
|
||||
write_text($charlie, server_conf('charlie.example', "\troot /srv/charlie;\n"));
|
||||
write_text($default, server_conf('_', "\troot /srv/default;\n"));
|
||||
write_text($nginx_conf,
|
||||
"events {\n".
|
||||
"}\n".
|
||||
"http {\n".
|
||||
"\tinclude $enabled/*;\n".
|
||||
"}\n");
|
||||
|
||||
symlink($alpha, File::Spec->catfile($enabled, 'alpha.conf')) ||
|
||||
die "Failed to symlink alpha: $!";
|
||||
symlink($charlie, File::Spec->catfile($enabled, 'charlie.conf')) ||
|
||||
die "Failed to symlink charlie: $!";
|
||||
symlink($default, File::Spec->catfile($enabled, 'default')) ||
|
||||
die "Failed to symlink default: $!";
|
||||
|
||||
write_text(File::Spec->catfile($webmin_config, 'config'),
|
||||
"os_type=unix\n".
|
||||
"os_version=1\n".
|
||||
"real_os_type=Unix\n".
|
||||
"real_os_version=1\n");
|
||||
write_text(File::Spec->catfile($webmin_config, 'miniserv.conf'),
|
||||
"root=$root\n");
|
||||
write_text(File::Spec->catfile($webmin_config, 'nginx', 'config'),
|
||||
"nginx_config=$nginx_conf\n".
|
||||
"nginx_cmd=/bin/true\n".
|
||||
"add_to=$available\n".
|
||||
"add_link=$enabled\n");
|
||||
|
||||
$ENV{'WEBMIN_CONFIG'} = $webmin_config;
|
||||
$ENV{'WEBMIN_VAR'} = $webmin_var;
|
||||
$ENV{'FOREIGN_MODULE_NAME'} = 'nginx';
|
||||
$ENV{'FOREIGN_ROOT_DIRECTORY'} = $root;
|
||||
$ENV{'REMOTE_USER'} = 'root';
|
||||
|
||||
unshift(@INC, $root);
|
||||
require File::Spec->catfile($root, 'nginx', 'nginx-lib.pl');
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
$main::text{'server_pp'} = 'Proxy to $1';
|
||||
$main::text{'index_noroot'} = 'No root directory';
|
||||
$main::text{'index_noproxy'} = 'No proxy target';
|
||||
}
|
||||
|
||||
sub http_config
|
||||
{
|
||||
main::flush_config_cache();
|
||||
my $http = main::find('http', main::get_config());
|
||||
ok($http, 'test nginx config has an http block');
|
||||
return $http;
|
||||
}
|
||||
|
||||
sub row_names
|
||||
{
|
||||
return [ map { scalar main::find_value('server_name', $_->{'server'}) } @_ ];
|
||||
}
|
||||
|
||||
sub row_states
|
||||
{
|
||||
return [ map { $_->{'active'} ? 'enabled' : 'disabled' } @_ ];
|
||||
}
|
||||
|
||||
subtest 'sites-available files are manageable and ordered' => sub {
|
||||
ok(main::can_manage_server_files(), 'sites-available/enabled dirs are manageable');
|
||||
is_deeply(
|
||||
[ main::get_add_to_files() ],
|
||||
[ $alpha, $beta, $charlie, $default ],
|
||||
'available files are listed in stable filename order',
|
||||
);
|
||||
|
||||
my @rows = main::get_server_list_rows(http_config());
|
||||
is_deeply(row_names(@rows),
|
||||
[ '_', 'alpha.example', 'beta.example', 'charlie.example' ],
|
||||
'default site is first and other sites stay in sites-available order');
|
||||
is_deeply(row_states(@rows),
|
||||
[ 'enabled', 'enabled', 'disabled', 'enabled' ],
|
||||
'row active state follows sites-enabled symlinks');
|
||||
};
|
||||
|
||||
subtest 'disable removes only the enabled symlink' => sub {
|
||||
no warnings 'once';
|
||||
unlink($main::last_config_change_flag);
|
||||
unlink($main::last_restart_time_flag);
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return undef; };
|
||||
is(main::disable_server_file($alpha), undef, 'disable succeeds');
|
||||
}
|
||||
ok(main::needs_config_restart(),
|
||||
'disable marks config as needing apply');
|
||||
|
||||
ok(-f $alpha, 'disable leaves the sites-available file in place');
|
||||
ok(!-e File::Spec->catfile($enabled, 'alpha.conf'),
|
||||
'disable removes the sites-enabled symlink');
|
||||
|
||||
my @rows = main::get_server_list_rows(http_config());
|
||||
is_deeply(row_names(@rows),
|
||||
[ '_', 'alpha.example', 'beta.example', 'charlie.example' ],
|
||||
'disabled row remains in the same list position');
|
||||
is_deeply(row_states(@rows),
|
||||
[ 'enabled', 'disabled', 'disabled', 'enabled' ],
|
||||
'disabled row status is updated');
|
||||
};
|
||||
|
||||
subtest 'enable creates a symlink without touching the source file' => sub {
|
||||
no warnings 'once';
|
||||
unlink($main::last_config_change_flag);
|
||||
unlink($main::last_restart_time_flag);
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return undef; };
|
||||
is(main::enable_server_file($beta), undef, 'enable succeeds');
|
||||
}
|
||||
ok(main::needs_config_restart(),
|
||||
'enable marks config as needing apply');
|
||||
|
||||
my $link = File::Spec->catfile($enabled, 'beta.conf');
|
||||
ok(-f $beta, 'enable leaves the sites-available file in place');
|
||||
ok(-l $link, 'enable creates the sites-enabled symlink');
|
||||
is(readlink($link), $beta, 'enabled symlink points to the available file');
|
||||
ok(main::server_file_enabled($beta), 'server_file_enabled sees the symlink');
|
||||
};
|
||||
|
||||
subtest 'legacy create/delete link helpers still manage symlinks' => sub {
|
||||
no warnings 'once';
|
||||
my $echo = File::Spec->catfile($available, 'echo.conf');
|
||||
my $echo_link = File::Spec->catfile($enabled, 'echo.conf');
|
||||
write_text($echo, server_conf('echo.example', "\troot /srv/echo;\n"));
|
||||
my $server = { 'file' => $echo };
|
||||
|
||||
unlink($main::last_config_change_flag);
|
||||
unlink($main::last_restart_time_flag);
|
||||
main::create_server_link($server);
|
||||
ok(-l $echo_link, 'create_server_link creates expected symlink');
|
||||
is(readlink($echo_link), $echo, 'created symlink points to server file');
|
||||
ok(main::needs_config_restart(),
|
||||
'create_server_link marks config as needing apply');
|
||||
|
||||
main::update_last_restart_time();
|
||||
my $old = time() - 10;
|
||||
utime($old, $old, $main::last_restart_time_flag);
|
||||
main::delete_server_link($server);
|
||||
ok(!-e $echo_link, 'delete_server_link removes expected symlink');
|
||||
ok(-f $echo, 'delete_server_link leaves server file in place');
|
||||
ok(main::needs_config_restart(),
|
||||
'delete_server_link marks config as needing apply');
|
||||
};
|
||||
|
||||
subtest 'disabled server blocks can be deleted from available files' => sub {
|
||||
my $multi = File::Spec->catfile($available, 'multi.conf');
|
||||
write_text($multi,
|
||||
server_conf('one.example', "\troot /srv/one;\n").
|
||||
server_conf('two.example', "\troot /srv/two;\n"));
|
||||
my ($one_server) = grep {
|
||||
main::find_value('server_name', $_) eq 'one.example'
|
||||
} main::find_servers_in_file($multi);
|
||||
|
||||
is(main::delete_servers_from_file($multi, $one_server), 1,
|
||||
'delete_servers_from_file removes one disabled server block');
|
||||
ok(-f $multi, 'file remains when another server block is present');
|
||||
is_deeply(
|
||||
[ map { scalar main::find_value('server_name', $_) }
|
||||
main::find_servers_in_file($multi) ],
|
||||
[ 'two.example' ],
|
||||
'only the unselected disabled server block remains');
|
||||
|
||||
my ($two_server) = main::find_servers_in_file($multi);
|
||||
is(main::delete_servers_from_file($multi, $two_server), 1,
|
||||
'delete_servers_from_file removes the last disabled server block');
|
||||
ok(!-e $multi, 'empty available file is removed after last block delete');
|
||||
};
|
||||
|
||||
subtest 'same-name symlink to another target is not disabled' => sub {
|
||||
my $otherdir = File::Spec->catdir($tmp, 'other-sites');
|
||||
my $other = File::Spec->catfile($otherdir, 'charlie.conf');
|
||||
my $link = File::Spec->catfile($enabled, 'charlie.conf');
|
||||
make_path($otherdir);
|
||||
write_text($other, server_conf('other.example', "\troot /srv/other;\n"));
|
||||
unlink($link) || die "Failed to remove charlie link: $!";
|
||||
symlink($other, $link) || die "Failed to symlink other charlie: $!";
|
||||
|
||||
ok(!main::server_file_enabled($charlie),
|
||||
'same-name symlink to another file is not considered enabled');
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return undef; };
|
||||
is(main::disable_server_file($charlie), undef, 'disable is a no-op');
|
||||
}
|
||||
ok(-l $link, 'same-name symlink to another target is preserved');
|
||||
is(readlink($link), $other, 'preserved symlink target is unchanged');
|
||||
};
|
||||
|
||||
subtest 'file-level actions require access to every server in the file' => sub {
|
||||
my $mixed = File::Spec->catfile($available, 'mixed.conf');
|
||||
write_text($mixed,
|
||||
server_conf('alpha.example', "\troot /srv/mixed-alpha;\n").
|
||||
server_conf('hidden.example', "\troot /srv/mixed-hidden;\n"));
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
local $main::access{'vhosts'} = 'alpha.example';
|
||||
ok(!main::can_manage_server_file($mixed),
|
||||
'mixed-access file cannot be managed by a restricted user');
|
||||
}
|
||||
ok(main::can_manage_server_file($mixed),
|
||||
'mixed-access file can be managed when vhost access is unrestricted');
|
||||
};
|
||||
|
||||
subtest 'nginx -t failure rolls back link changes' => sub {
|
||||
my $delta = File::Spec->catfile($available, 'delta.conf');
|
||||
my $delta_link = File::Spec->catfile($enabled, 'delta.conf');
|
||||
write_text($delta, server_conf('delta.example', "\troot /srv/delta;\n"));
|
||||
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return 'bad config'; };
|
||||
like(main::enable_server_file($delta), qr/bad config/,
|
||||
'failed enable reports nginx -t output');
|
||||
}
|
||||
ok(!-e $delta_link, 'failed enable removes the new symlink');
|
||||
|
||||
symlink($delta, $delta_link) || die "Failed to symlink delta: $!";
|
||||
{
|
||||
no warnings 'redefine';
|
||||
local *main::test_config = sub { return 'bad config'; };
|
||||
like(main::disable_server_file($delta), qr/bad config/,
|
||||
'failed disable reports nginx -t output');
|
||||
}
|
||||
ok(-l $delta_link, 'failed disable restores the removed symlink');
|
||||
is(readlink($delta_link), $delta, 'restored symlink target is unchanged');
|
||||
};
|
||||
|
||||
subtest 'root and proxy summaries are detected' => sub {
|
||||
my ($alpha_server) = main::find_servers_in_file($alpha);
|
||||
my ($beta_server) = main::find_servers_in_file($beta);
|
||||
my ($default_server) = main::find_servers_in_file($default);
|
||||
my $path_proxy = File::Spec->catfile($available, 'path-proxy.conf');
|
||||
write_text($path_proxy,
|
||||
"server {\n".
|
||||
"\tserver_name path.example;\n".
|
||||
"\tlisten 443 ssl http2;\n".
|
||||
"\tlocation /webmin {\n".
|
||||
"\t\tproxy_pass https://127.0.0.1:10000/;\n".
|
||||
"\t\tproxy_http_version 1.1;\n".
|
||||
"\t}\n".
|
||||
"}\n");
|
||||
my $named = File::Spec->catfile($available, 'named-proxy.conf');
|
||||
write_text($named,
|
||||
"server {\n".
|
||||
"\tserver_name named.example;\n".
|
||||
"\tlisten 80;\n".
|
||||
"\tlocation / {\n".
|
||||
"\t\ttry_files \$uri \@backend;\n".
|
||||
"\t}\n".
|
||||
"\tlocation \@backend {\n".
|
||||
"\t\tproxy_pass http://127.0.0.1:8081;\n".
|
||||
"\t}\n".
|
||||
"}\n");
|
||||
my ($path_proxy_server) = main::find_servers_in_file($path_proxy);
|
||||
my ($named_server) = main::find_servers_in_file($named);
|
||||
|
||||
is_deeply([ main::server_root_proxy_state($alpha_server) ], [ 1, 0 ],
|
||||
'root-only server state is detected');
|
||||
is(main::server_root_summary($alpha_server), '/srv/alpha',
|
||||
'root-only server root column shows the root directory');
|
||||
is(main::server_proxy_summary($alpha_server), '<i>No proxy target</i>',
|
||||
'root-only server proxy column shows a missing-proxy message');
|
||||
is(main::server_root_proxy_summary($alpha_server), '/srv/alpha',
|
||||
'root-only summary shows the root directory');
|
||||
is(main::server_url($alpha_server), 'http://alpha.example/',
|
||||
'root-only server URL uses HTTP default port');
|
||||
is(main::server_url($default_server), undef,
|
||||
'default server has no URL link target');
|
||||
|
||||
is_deeply([ main::server_root_proxy_state($beta_server) ], [ 0, 1 ],
|
||||
'proxy-only server state is detected');
|
||||
is(main::server_root_summary($beta_server), '<i>No root directory</i>',
|
||||
'proxy-only server root column shows a missing-root message');
|
||||
like(main::server_proxy_summary($beta_server),
|
||||
qr{/ ⇾ http://127\.0\.0\.1:8080},
|
||||
'proxy-only server proxy column shows the path and proxy target');
|
||||
like(main::server_root_proxy_summary($beta_server),
|
||||
qr{http://127\.0\.0\.1:8080},
|
||||
'proxy-only summary shows the proxy target');
|
||||
is(main::server_url($beta_server), 'http://beta.example/',
|
||||
'proxy-only server URL uses HTTP default port');
|
||||
|
||||
is_deeply([ main::server_root_proxy_state($path_proxy_server) ], [ 0, 1 ],
|
||||
'non-root-location proxy state is detected');
|
||||
is(main::server_root_summary($path_proxy_server), '<i>No root directory</i>',
|
||||
'non-root-location proxy root column shows a missing-root message');
|
||||
like(main::server_proxy_summary($path_proxy_server),
|
||||
qr{/webmin ⇾ https://127\.0\.0\.1:10000/},
|
||||
'non-root-location proxy column shows the path and proxy target');
|
||||
like(main::server_root_proxy_summary($path_proxy_server),
|
||||
qr{https://127\.0\.0\.1:10000/},
|
||||
'non-root-location proxy summary shows the proxy target');
|
||||
is(main::server_url($path_proxy_server), 'https://path.example/',
|
||||
'SSL listener URL uses HTTPS default port');
|
||||
|
||||
is_deeply([ main::server_root_proxy_state($named_server) ], [ 0, 1 ],
|
||||
'named-location proxy state is detected');
|
||||
like(main::server_proxy_summary($named_server),
|
||||
qr{\@backend ⇾ http://127\.0\.0\.1:8081},
|
||||
'named-location proxy column shows the path and proxy target');
|
||||
like(main::server_root_proxy_summary($named_server),
|
||||
qr{http://127\.0\.0\.1:8081},
|
||||
'named-location proxy summary shows the proxy target');
|
||||
is(main::server_url($named_server), 'http://named.example/',
|
||||
'named-location proxy URL uses HTTP default port');
|
||||
};
|
||||
|
||||
subtest 'config change apply flag tracks pending changes' => sub {
|
||||
no warnings 'once';
|
||||
unlink($main::last_config_change_flag);
|
||||
unlink($main::last_restart_time_flag);
|
||||
|
||||
ok(!main::needs_config_restart(),
|
||||
'no apply needed when no change flag exists');
|
||||
main::update_last_config_change();
|
||||
ok(main::needs_config_restart(),
|
||||
'apply needed after config change');
|
||||
main::update_last_restart_time();
|
||||
ok(!main::needs_config_restart(),
|
||||
'apply not needed after config has been applied');
|
||||
|
||||
my $old = time() - 10;
|
||||
utime($old, $old, $main::last_restart_time_flag);
|
||||
main::update_last_config_change();
|
||||
ok(main::needs_config_restart(),
|
||||
'apply needed when config change is newer than last apply');
|
||||
};
|
||||
|
||||
subtest 'manual edit ACL is separately configurable' => sub {
|
||||
no warnings 'once';
|
||||
{
|
||||
local %main::access = ( 'global' => 1 );
|
||||
ok(main::can_edit_manual_config(),
|
||||
'manual edit defaults to global ACL for existing users');
|
||||
}
|
||||
{
|
||||
local %main::access = ( 'global' => 0 );
|
||||
ok(!main::can_edit_manual_config(),
|
||||
'manual edit is denied when global ACL default is denied');
|
||||
}
|
||||
{
|
||||
local %main::access = ( 'global' => 1, 'manual' => 0 );
|
||||
ok(!main::can_edit_manual_config(),
|
||||
'manual edit can be disabled for global users');
|
||||
}
|
||||
{
|
||||
local %main::access = ( 'global' => 0, 'manual' => 1 );
|
||||
ok(main::can_edit_manual_config(),
|
||||
'manual edit can be explicitly enabled');
|
||||
}
|
||||
};
|
||||
|
||||
subtest 'manual edit files respect vhost ACL' => sub {
|
||||
my $single = File::Spec->catfile($available, 'manual-single.conf');
|
||||
my $shared = File::Spec->catfile($available, 'manual-shared.conf');
|
||||
my $single_link = File::Spec->catfile($tmp, 'manual-single-link.conf');
|
||||
my $shared_link = File::Spec->catfile($tmp, 'manual-shared-link.conf');
|
||||
write_text($single,
|
||||
server_conf('single.example', "\troot /srv/single;\n"));
|
||||
write_text($shared,
|
||||
server_conf('single.example', "\troot /srv/shared-single;\n").
|
||||
server_conf('other.example', "\troot /srv/shared-other;\n"));
|
||||
symlink($single, File::Spec->catfile($enabled, 'manual-single.conf')) ||
|
||||
die "Failed to symlink manual-single: $!";
|
||||
symlink($shared, File::Spec->catfile($enabled, 'manual-shared.conf')) ||
|
||||
die "Failed to symlink manual-shared: $!";
|
||||
symlink($single, $single_link) ||
|
||||
die "Failed to symlink manual-single-link: $!";
|
||||
symlink($shared, $shared_link) ||
|
||||
die "Failed to symlink manual-shared-link: $!";
|
||||
main::flush_config_cache();
|
||||
|
||||
{
|
||||
local %main::access = ( 'manual' => 1,
|
||||
'vhosts' => 'single.example' );
|
||||
my @files = main::get_manual_config_files();
|
||||
ok(main::can_edit_manual_file($single),
|
||||
'restricted user can manually edit their own single-server file');
|
||||
ok(!main::can_edit_manual_file($shared),
|
||||
'restricted user cannot manually edit a shared server file');
|
||||
is_deeply(
|
||||
[ grep { $_ eq $single || $_ eq $shared } @files ],
|
||||
[ $single ],
|
||||
'manual file list excludes shared files for restricted users');
|
||||
is(main::resolve_manual_config_file($single_link, @files), $single,
|
||||
'submitted symlink resolves to an authorized config file');
|
||||
is(main::resolve_manual_config_file($shared_link, @files), undef,
|
||||
'submitted symlink to unauthorized file is rejected');
|
||||
}
|
||||
{
|
||||
local %main::access = ( 'manual' => 1 );
|
||||
ok(main::can_edit_manual_file($shared),
|
||||
'unrestricted user can manually edit shared server files');
|
||||
}
|
||||
};
|
||||
|
||||
subtest 'Virtualmin-managed server files cannot be toggled directly' => sub {
|
||||
my $enabled_domain = File::Spec->catfile($available, 'vm-enabled.conf');
|
||||
my $disabled_domain = File::Spec->catfile($available, 'vm-disabled.conf');
|
||||
write_text($enabled_domain,
|
||||
server_conf('vm-enabled.example www.vm-enabled.example',
|
||||
"\troot /srv/vm-enabled;\n"));
|
||||
write_text($disabled_domain,
|
||||
server_conf('vm-disabled.example',
|
||||
"\troot /srv/vm-disabled;\n"));
|
||||
|
||||
{
|
||||
no warnings qw(redefine once);
|
||||
local *main::virtualmin_available = sub { return 1; };
|
||||
local *main::virtualmin_domain_by_name = sub {
|
||||
my ($name) = @_;
|
||||
return $name eq 'vm-enabled.example' ?
|
||||
{ 'dom' => $name, 'id' => '12345',
|
||||
'disabled' => '' } :
|
||||
$name eq 'vm-disabled.example' ?
|
||||
{ 'dom' => $name, 'id' => '67890',
|
||||
'disabled' => 'web' } :
|
||||
undef;
|
||||
};
|
||||
|
||||
my $disable_err =
|
||||
main::virtualmin_server_file_state_error($enabled_domain,
|
||||
'disable');
|
||||
my $enabled_state = main::server_file_state($enabled_domain);
|
||||
is($enabled_state->{'source'}, 'virtualmin',
|
||||
'Virtualmin is the effective state source for managed files');
|
||||
ok($enabled_state->{'enabled'},
|
||||
'Virtualmin enabled domain is reported as enabled');
|
||||
is(main::server_file_toggle_action($enabled_domain), 'disable',
|
||||
'toggle action follows the Virtualmin enabled state');
|
||||
like($disable_err, qr/currently enabled/,
|
||||
'Virtualmin state is included for enabled domains');
|
||||
like($disable_err, qr/Disable Virtual Server/,
|
||||
'disabling directs users to Virtualmin disable action');
|
||||
like($disable_err,
|
||||
qr{virtual-server/disable_domain\.cgi\?dom=12345},
|
||||
'disabling links to the Virtualmin disable form');
|
||||
|
||||
my $enable_err =
|
||||
main::virtualmin_server_file_state_error($disabled_domain,
|
||||
'enable');
|
||||
my $disabled_state = main::server_file_state($disabled_domain);
|
||||
is($disabled_state->{'source'}, 'virtualmin',
|
||||
'Virtualmin remains the state source for disabled domains');
|
||||
ok(!$disabled_state->{'enabled'},
|
||||
'Virtualmin disabled domain is reported as disabled');
|
||||
is(main::server_file_toggle_action($disabled_domain), 'enable',
|
||||
'toggle action follows the Virtualmin disabled state');
|
||||
like($enable_err, qr/currently disabled/,
|
||||
'Virtualmin state is included for disabled domains');
|
||||
like($enable_err, qr/Enable Virtual Server/,
|
||||
'enabling directs users to Virtualmin enable action');
|
||||
like($enable_err,
|
||||
qr{virtual-server/enable_domain\.cgi\?dom=67890},
|
||||
'enabling links to the Virtualmin enable form');
|
||||
|
||||
is(main::virtualmin_server_file_state_error($alpha, 'disable'),
|
||||
undef, 'non-Virtualmin server files can still be toggled');
|
||||
}
|
||||
};
|
||||
|
||||
done_testing();
|
||||
5
rpc.cgi
5
rpc.cgi
@@ -4,13 +4,13 @@
|
||||
# other webmin servers. State is preserved by starting a process for each
|
||||
# session that listens for requests on a named pipe (and dies after a few
|
||||
# seconds of inactivity)
|
||||
# access{'rpc'} 0=not allowed 1=allowed 2=allowed if root or admin, 3=allowed
|
||||
# access{'rpc'} 0=not allowed 1=allowed 2=allowed if root or admin, 3=RPC only
|
||||
|
||||
$main::allow_rpc_only = 1;
|
||||
BEGIN { push(@INC, "."); };
|
||||
use WebminCore;
|
||||
use POSIX;
|
||||
|
||||
$main::allow_rpc_only = 1;
|
||||
&init_config();
|
||||
if ($ENV{'REQUEST_METHOD'} eq 'POST') {
|
||||
local $got;
|
||||
@@ -169,4 +169,3 @@ unlink($fifo1);
|
||||
unlink($fifo2);
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
148
t/README.md
Normal file
148
t/README.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# Webmin test suite
|
||||
|
||||
Two kinds of tests live under this tree:
|
||||
|
||||
- Repo-root `t/` for core code (`miniserv.pl`, `web-lib-funcs.pl`, top-level
|
||||
scripts, `bin/`).
|
||||
- Per-module `t/` for module-internal coverage (see `nftables/t/` for the
|
||||
established pattern: `perlcritic.t` plus a `run-tests.t` smoke harness).
|
||||
|
||||
## Running tests
|
||||
|
||||
```sh
|
||||
prove -lr # everything, including modules t/
|
||||
prove -lr t # everything under repo-root t/
|
||||
prove t/compile.t # one test file
|
||||
WEBMIN_COMPILE_T_FILTER='^\./acl/' prove t/compile.t # one module
|
||||
```
|
||||
|
||||
`prove` and Test::More are core, though on RPM-based distros, you need
|
||||
`perl-Test-Harness`.
|
||||
|
||||
## Coverage reports
|
||||
|
||||
```sh
|
||||
HARNESS_PERL_SWITCHES=-MDevel::Cover prove -lr
|
||||
cover
|
||||
```
|
||||
|
||||
You need Devel::Cover installed.
|
||||
|
||||
## What's here
|
||||
|
||||
| File | What it checks |
|
||||
| --- | --- |
|
||||
| `compile.t` | Every `.pl`, `.cgi`, and shebang-perl script in `bin/` parses cleanly (`perl -c`). Catches breakage from bulk refactors without browsing every page. ~12s for the full tree. |
|
||||
| `miniserv.t` | Contract test for `miniserv.pl` functions — status codes, headers, body rendering, log behaviour. Demonstrates the require-and-stub pattern below. |
|
||||
|
||||
## The require-and-stub pattern
|
||||
|
||||
Many Webmin scripts mix sub definitions with a main body that opens sockets,
|
||||
reads `/etc/webmin/*`, or spawns CGIs. To test individual subs in isolation
|
||||
we need to `require` the script as a library without running the main body.
|
||||
|
||||
Two complementary idioms can be used. Both work; they look different because
|
||||
the underlying script does.
|
||||
|
||||
### Block wrap (main body precedes sub definitions)
|
||||
|
||||
Used by `miniserv.pl`. The executable preamble runs at file scope (so any
|
||||
`my` vars stay file-scoped for the subs below); the main body wraps in
|
||||
`unless (caller)`:
|
||||
|
||||
```perl
|
||||
#!/usr/bin/perl
|
||||
use Foo; # use lines and pragmas stay outside the guard
|
||||
|
||||
unless (caller) {
|
||||
|
||||
# main body: arg parsing, setup, the actual work
|
||||
...
|
||||
|
||||
} # end of unless (caller)
|
||||
|
||||
sub helper { ... }
|
||||
sub other_helper { ... }
|
||||
```
|
||||
|
||||
### One-liner (sub definitions precede the main call)
|
||||
|
||||
Used by most `bin/` CLI tools, which already define `sub main` and dispatch
|
||||
to it at the bottom:
|
||||
|
||||
```perl
|
||||
#!/usr/bin/env perl
|
||||
use strict; use warnings;
|
||||
|
||||
sub main {
|
||||
...
|
||||
return 0;
|
||||
}
|
||||
exit main(\@ARGV) if !caller(0);
|
||||
|
||||
sub helper { ... }
|
||||
```
|
||||
|
||||
`!caller(0)` is true at script invocation (depth-0 frame absent) and false
|
||||
under `require`.
|
||||
|
||||
## Sub-stubbing in tests
|
||||
|
||||
`miniserv.t` is the canonical example. The pattern:
|
||||
|
||||
1. `require` the script. The guard skips its main body.
|
||||
2. Replace side-effecting subs (socket I/O, logging, disk reads) with
|
||||
capture-buffer overrides under `no warnings 'redefine'`.
|
||||
3. Populate package globals (`%miniserv::config`, `@miniserv::roots`, etc.)
|
||||
yourself instead of running the config loader.
|
||||
4. Call the sub under test. Assert on contract (status code, presence of
|
||||
required headers, structural balance of emitted markup) — not on
|
||||
cosmetics like exact wording or class names.
|
||||
|
||||
Tying tests to the contract rather than the rendering lets the UI evolve
|
||||
without breaking the test, while still catching real regressions.
|
||||
|
||||
## Tiered coverage policy
|
||||
|
||||
Not all 268 modules deserve the same investment.
|
||||
|
||||
- **Tier 1 — security-critical, network-facing.** `miniserv.pl`,
|
||||
`web-lib-funcs.pl`, `acl/`, auth, file upload paths. Comprehensive
|
||||
contract tests; mock filesystem and `backquote_command` for parser
|
||||
coverage of external-binary output.
|
||||
- **Tier 2 — active refactor surface.** Currently `nftables/`, `firewalld/`,
|
||||
and whichever module is being reviewed. Mandatory tests for new code;
|
||||
`perlcritic.t` at severity 5 as a gate.
|
||||
- **Tier 3 — stable OS-config wrappers.** Covered by `compile.t` plus
|
||||
optional per-module `perlcritic.t`. Don't chase line coverage on parsers
|
||||
for config files that haven't changed in a decade.
|
||||
|
||||
The goal is not coverage-as-a-number. It's:
|
||||
|
||||
- Every parser round-trips its serializer.
|
||||
- Every privilege boundary has a test.
|
||||
- Every external-binary call has a mock-driven test for its output parser.
|
||||
|
||||
## Adding a per-module test directory
|
||||
|
||||
```
|
||||
yourmodule/
|
||||
t/
|
||||
perlcritic.t # see nftables/t/perlcritic.t for the template
|
||||
run-tests.t # see nftables/t/run-tests.t for the WEBMIN_CONFIG / tmpdir setup
|
||||
```
|
||||
|
||||
A module's tests are reachable from `prove -lr` at the repo root (no
|
||||
path arg, so the recursive walk starts at the cwd). `prove -lr t` only
|
||||
walks within `t/` and will miss `<module>/t/`.
|
||||
|
||||
## Caveats
|
||||
|
||||
- `WEBMIN_COMPILE_T_STRICT=1` turns missing-CPAN-module skips into failures.
|
||||
Use this in CI on a fully-provisioned image; leave it off on dev boxes
|
||||
where optional deps (`Pod::Simple::Wiki`, `Encode::Detect::Detector`) may
|
||||
not be installed.
|
||||
- `.pl` is also the Polish translation suffix in Webmin. `compile.t` skips
|
||||
`<file>.pl` when a sibling `<file>` (no extension) exists; this catches
|
||||
`config.info.pl` and `module.info.pl` data files without a hardcoded list.
|
||||
|
||||
90
t/compile.t
Normal file
90
t/compile.t
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/usr/local/bin/perl
|
||||
# Verify every .pl and .cgi in the tree parses (perl -c).
|
||||
#
|
||||
# Catches syntax and `use` breakage from bulk refactors without having
|
||||
# to load every page in a browser. The test is the first line of defence
|
||||
# for the "we changed thousands of files mechanically, did anything
|
||||
# break" question.
|
||||
#
|
||||
# Skipped:
|
||||
# - $file.pl when a sibling $file (no .pl) exists. Webmin uses .pl as
|
||||
# the Polish translation suffix, so config.info.pl, module.info.pl,
|
||||
# etc. are data files, not Perl.
|
||||
# - Files that fail only because of a missing CPAN module. The file
|
||||
# itself parses, but `use Foo::Bar` can't resolve at compile time.
|
||||
# Treated as a skip so missing optional deps don't gate the suite.
|
||||
# Set WEBMIN_COMPILE_T_STRICT=1 to turn these into failures.
|
||||
#
|
||||
# Speed: ~12 seconds for the full tree (~3.4k files). Narrow with
|
||||
# WEBMIN_COMPILE_T_FILTER=<regex> when iterating on one module.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use File::Find;
|
||||
use File::Basename qw(dirname);
|
||||
use File::Spec;
|
||||
use Cwd qw(abs_path);
|
||||
|
||||
my $root = abs_path(File::Spec->catdir(dirname(__FILE__), '..'));
|
||||
chdir($root) or die "chdir($root): $!";
|
||||
|
||||
my $filter = $ENV{WEBMIN_COMPILE_T_FILTER};
|
||||
my $strict = $ENV{WEBMIN_COMPILE_T_STRICT};
|
||||
|
||||
my @files;
|
||||
find({
|
||||
no_chdir => 1,
|
||||
wanted => sub {
|
||||
return if -d;
|
||||
# .pl and .cgi files, plus extensionless files in bin/ with a
|
||||
# perl shebang. The shebang check keeps us from compile-checking
|
||||
# arbitrary non-perl files just because they share a directory.
|
||||
my $name = $File::Find::name;
|
||||
my $is_pl_or_cgi = $name =~ /\.(pl|cgi)\z/;
|
||||
my $is_bin_dotless = $name =~ m{^\./bin/([^/]+)\z} && $1 !~ /\./;
|
||||
return unless $is_pl_or_cgi || $is_bin_dotless;
|
||||
# Skip the Polish translations that share the .pl suffix.
|
||||
if ($is_pl_or_cgi && $name =~ m{(.+)\.pl\z}) {
|
||||
my $base = $1;
|
||||
return if -f "$base";
|
||||
}
|
||||
# For extensionless bin/ scripts, require a perl shebang.
|
||||
if ($is_bin_dotless) {
|
||||
open(my $fh, '<', $name) or return;
|
||||
my $shebang = <$fh>;
|
||||
close($fh);
|
||||
return unless defined $shebang && $shebang =~ /^#!.*\bperl\b/;
|
||||
}
|
||||
push(@files, $name);
|
||||
},
|
||||
}, '.');
|
||||
|
||||
@files = sort @files;
|
||||
@files or BAIL_OUT("found no .pl/.cgi/bin scripts under $root");
|
||||
|
||||
if ($filter) {
|
||||
@files = grep { /$filter/ } @files;
|
||||
@files or do { diag("filter '$filter' matched zero files"); plan skip_all => "no files match filter"; };
|
||||
}
|
||||
|
||||
diag("compile-checking " . scalar(@files) . " files");
|
||||
|
||||
for my $f (@files) {
|
||||
my $rel = $f;
|
||||
$rel =~ s{^\./}{};
|
||||
my $out = qx{perl -I. -c -- "$rel" 2>&1};
|
||||
if ($out =~ /\bsyntax OK\b/) {
|
||||
pass("$rel compiles");
|
||||
}
|
||||
elsif (!$strict && $out =~ /Can't locate (\S+\.pm) in \@INC/) {
|
||||
SKIP: { skip("$rel: missing optional CPAN module $1", 1); }
|
||||
}
|
||||
else {
|
||||
fail("$rel compiles");
|
||||
diag($out);
|
||||
}
|
||||
}
|
||||
|
||||
done_testing();
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/perl
|
||||
# Tests for miniserv::http_error.
|
||||
#
|
||||
# miniserv.pl is loaded as a module; its top-level script body is skipped
|
||||
# by the `unless (caller) { ... }` guard, so we only get the sub
|
||||
# definitions plus a handful of pure-constant globals (@itoa64, @weekday,
|
||||
# @month, @miniserv_argv). Everything else (%config, @roots, $datestr,
|
||||
# etc.) we populate ourselves.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use File::Basename qw(dirname);
|
||||
use File::Spec;
|
||||
|
||||
my $script = File::Spec->rel2abs(
|
||||
File::Spec->catfile(dirname(__FILE__), '..', 'miniserv.pl'));
|
||||
require $script;
|
||||
|
||||
# Capture buffers populated by the overridden I/O subs.
|
||||
our @written;
|
||||
our @errlog;
|
||||
our @reqlog;
|
||||
|
||||
# Replace the subs that would otherwise touch SOCK, STDERR, the log file,
|
||||
# or read disk. Each capturing override is the minimum needed to keep
|
||||
# http_error's control flow intact. `once` is suppressed because these
|
||||
# package globals are only ever written from this file.
|
||||
{
|
||||
no warnings qw(redefine once);
|
||||
*miniserv::write_data = sub { push @written, join('', @_); };
|
||||
*miniserv::write_keep_alive = sub { };
|
||||
*miniserv::log_error = sub { push @errlog, join('', @_); };
|
||||
*miniserv::log_request = sub { push @reqlog, [@_]; };
|
||||
*miniserv::embed_error_styles = sub { return ''; };
|
||||
*miniserv::server_info = sub { return 'MiniServ/test'; };
|
||||
*miniserv::reset_byte_count = sub { };
|
||||
*miniserv::byte_count = sub { return 0; };
|
||||
}
|
||||
|
||||
{
|
||||
no warnings 'once';
|
||||
%miniserv::config = ();
|
||||
@miniserv::roots = ('/tmp');
|
||||
@miniserv::preroots = ();
|
||||
$miniserv::datestr = 'Sun, 01 Jan 2026 00:00:00 GMT';
|
||||
}
|
||||
|
||||
# Call http_error with capture buffers reset. noexit=1 is REQUIRED — the
|
||||
# real sub calls exit() otherwise. shutdown(SOCK,1)/close(SOCK) at the end
|
||||
# of http_error warn because SOCK is not a real socket here; the localized
|
||||
# warn handler swallows that one specific noise.
|
||||
sub run_http_error {
|
||||
my (%args) = @_;
|
||||
@written = ();
|
||||
@errlog = ();
|
||||
@reqlog = ();
|
||||
no warnings 'once';
|
||||
local $miniserv::reqline = $args{reqline};
|
||||
local $miniserv::loghost = $args{loghost};
|
||||
local $miniserv::authuser = $args{authuser};
|
||||
local $SIG{__WARN__} = sub { };
|
||||
miniserv::http_error(
|
||||
$args{code}, $args{msg}, $args{body},
|
||||
1, # noexit
|
||||
$args{noerr},
|
||||
);
|
||||
return join('', @written);
|
||||
}
|
||||
|
||||
# minimal call: code + message, no body, no reqline
|
||||
# Assert on the contract, not the cosmetics: the status code, the
|
||||
# presence of required headers, and that the caller's code + message
|
||||
# reach the rendered page. Specific wording, header values, decoration
|
||||
# (—), and class names are presentation and may change.
|
||||
{
|
||||
my $out = run_http_error(code => 404, msg => 'Not Found');
|
||||
|
||||
like($out, qr{^HTTP/1\.0 404\b}, 'status line carries 404');
|
||||
like($out, qr{\r\nServer:\s+\S}, 'Server header present and non-empty');
|
||||
like($out, qr{\r\nDate:\s+\S}, 'Date header present and non-empty');
|
||||
like($out, qr{\r\nContent-type:\s*text/html\b.*\butf-?8\b}i, 'Content-type is HTML with utf-8 charset');
|
||||
like($out, qr{<title>[^<]*404[^<]*</title>}, 'title surfaces the code');
|
||||
like($out, qr{<title>[^<]*Not Found[^<]*</title>}, 'title surfaces the message');
|
||||
like($out, qr{<h2\b[^>]*>[^<]*Not Found[^<]*</h2>}, 'message appears in a heading');
|
||||
unlike($out, qr{<p\b}, 'no paragraph emitted when body arg absent');
|
||||
|
||||
is(scalar @errlog, 1, 'log_error called once');
|
||||
like($errlog[0], qr/Not Found/, 'log_error received the caller message');
|
||||
is(scalar @reqlog, 0, 'log_request skipped when reqline empty');
|
||||
}
|
||||
|
||||
# body argument renders as a paragraph
|
||||
{
|
||||
my $out = run_http_error(code => 500, msg => 'Server Error', body => 'something broke');
|
||||
like($out, qr{<p\b[^>]*>[^<]*\Qsomething broke\E[^<]*</p>}, 'body argument rendered in a paragraph');
|
||||
}
|
||||
|
||||
# reqline triggers log_request
|
||||
{
|
||||
run_http_error(
|
||||
code => 403, msg => 'Forbidden', body => 'no access',
|
||||
reqline => 'GET /secret HTTP/1.0',
|
||||
loghost => '127.0.0.1', authuser => 'bob',
|
||||
);
|
||||
is(scalar @reqlog, 1, 'log_request called when reqline is set');
|
||||
is($reqlog[0][0], '127.0.0.1', 'log_request host arg');
|
||||
is($reqlog[0][1], 'bob', 'log_request user arg');
|
||||
is($reqlog[0][2], 'GET /secret HTTP/1.0', 'log_request request arg');
|
||||
is($reqlog[0][3], 403, 'log_request code arg');
|
||||
}
|
||||
|
||||
# noerr suppresses log_error
|
||||
{
|
||||
run_http_error(code => 401, msg => 'Unauthorized', noerr => 1);
|
||||
is(scalar @errlog, 0, 'log_error suppressed when noerr is true');
|
||||
}
|
||||
|
||||
# error_handler config that points to a missing file falls through
|
||||
# This exercises the early branch without triggering `goto rerun` (which
|
||||
# only resolves inside handle_request).
|
||||
{
|
||||
local $miniserv::config{'error_handler'} = 'definitely-not-a-real-file.cgi';
|
||||
my $out = run_http_error(code => 500, msg => 'Server Error');
|
||||
like($out, qr{^HTTP/1\.0 500\b}, 'falls through to standard path when handler file missing');
|
||||
}
|
||||
|
||||
# HTML scaffolding is balanced
|
||||
{
|
||||
my $out = run_http_error(code => 400, msg => 'Bad Request', body => 'oops');
|
||||
for my $tag (qw(html head title body h2 p)) {
|
||||
my $open = () = $out =~ /<$tag\b[^>]*>/g;
|
||||
my $close = () = $out =~ /<\/$tag>/g;
|
||||
is($open, $close, "<$tag> tags balanced (open=$open, close=$close)");
|
||||
cmp_ok($open, '>=', 1, "<$tag> appears at least once");
|
||||
}
|
||||
# Sanity-check element ordering: head before body, title inside head.
|
||||
like($out, qr{<html>.*<head>.*<title>.*</title>.*</head>.*<body[^>]*>.*</body>\s*</html>}s,
|
||||
'top-level elements appear in the expected order');
|
||||
}
|
||||
|
||||
done_testing();
|
||||
1439
t/miniserv.t
Normal file
1439
t/miniserv.t
Normal file
File diff suppressed because it is too large
Load Diff
39
t/web-lib-funcs-filter_javascript.t
Normal file
39
t/web-lib-funcs-filter_javascript.t
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/perl
|
||||
# Tests for web-lib-funcs.pl filter_javascript.
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
use Test::More;
|
||||
use File::Basename qw(dirname);
|
||||
use File::Spec;
|
||||
|
||||
my $script = File::Spec->rel2abs(
|
||||
File::Spec->catfile(dirname(__FILE__), '..', 'web-lib-funcs.pl'));
|
||||
require $script;
|
||||
|
||||
is(
|
||||
main::filter_javascript('<video/onloadstart=alert(1) src=1>'),
|
||||
'<video/xonloadstart=alert(1) src=1>',
|
||||
'slash-separated HTML5 event handler is disabled',
|
||||
);
|
||||
|
||||
is(
|
||||
main::filter_javascript('<img src=x onload=alert(1)>'),
|
||||
'<img src=x xonload=alert(1)>',
|
||||
'classic event handler is disabled',
|
||||
);
|
||||
|
||||
is(
|
||||
main::filter_javascript('<div onwheel = alert(1) onpointerdown=alert(2)>'),
|
||||
'<div xonwheel = alert(1) xonpointerdown=alert(2)>',
|
||||
'multiple modern event handlers are disabled',
|
||||
);
|
||||
|
||||
is(
|
||||
main::filter_javascript(
|
||||
'<a data-onload="safe" href="javascript:alert(1)">link</a>'),
|
||||
'<a data-onload="safe" href="xjavascript:alert(1)">link</a>',
|
||||
'non-handler attributes are preserved while script URIs are disabled',
|
||||
);
|
||||
|
||||
done_testing();
|
||||
@@ -5741,6 +5741,16 @@ if ($module_name) {
|
||||
$module_root_directory = &module_root_directory($module_name);
|
||||
}
|
||||
|
||||
if (!$main::allow_rpc_only &&
|
||||
$main::webmin_script_type eq 'web' &&
|
||||
!$main::no_acl_check &&
|
||||
!defined($ENV{'FOREIGN_MODULE_NAME'})) {
|
||||
# Check if this user is RPC-only
|
||||
if (&webmin_user_can_rpc() == 2) {
|
||||
&error($text{'erpconly'});
|
||||
}
|
||||
}
|
||||
|
||||
if ($module_name && !$main::no_acl_check &&
|
||||
(!defined($ENV{'FOREIGN_MODULE_NAME'}) ||
|
||||
defined($ENV{'FOREIGN_MODULE_SEC_CHECK'})) &&
|
||||
@@ -5759,16 +5769,6 @@ if ($module_name && !$main::no_acl_check &&
|
||||
$main::no_acl_check++;
|
||||
}
|
||||
|
||||
if (!$main::allow_rpc_only &&
|
||||
$main::webmin_script_type eq 'web' &&
|
||||
!$main::no_acl_check &&
|
||||
!defined($ENV{'FOREIGN_MODULE_NAME'})) {
|
||||
# Check if this user is RPC-only
|
||||
if (&webmin_user_can_rpc() == 2) {
|
||||
&error($text{'erpconly'});
|
||||
}
|
||||
}
|
||||
|
||||
# Check the Referer: header for nasty redirects
|
||||
my @referers = split(/\s+/, $gconfig{'referers'});
|
||||
my $referer_site;
|
||||
@@ -10179,10 +10179,15 @@ sub filter_javascript
|
||||
my ($rv, $type) = @_;
|
||||
if (!$type || $type eq 'html') {
|
||||
$rv =~ s/<\s*script[^>]*>([\000-\377]*?)<\s*\/script\s*>//gi;
|
||||
$rv =~ s/(on(Abort|BeforeUnload|Blur|Change|Click|ContextMenu|Copy|Cut|DblClick|Drag|DragEnd|DragEnter|DragLeave|DragOver|DragStart|DragDrop|Drop|Error|Focus|FocusIn|FocusOut|HashChange|Input|Invalid|KeyDown|KeyPress|KeyUp|Load|MouseDown|MouseEnter|MouseLeave|MouseMove|MouseOut|MouseOver|MouseUp|Move|Paste|PageShow|PageHide|Reset|Resize|Scroll|Search|Select|Submit|Toggle|Unload)=)/x$1/gi;
|
||||
$rv =~ s/(javascript(:|:|:|:))/x$1/gi;
|
||||
$rv =~ s/(vbscript(:|:|:|:))/x$1/gi;
|
||||
$rv =~ s/<([^>]*\s|)(on\S+=)(.*)>/<$1x$2$3>/gi;
|
||||
my $event_attr = qr/on[a-z][a-z0-9_:-]*\s*=/i;
|
||||
my $event_attrs;
|
||||
do {
|
||||
$event_attrs = 0;
|
||||
$event_attrs += $rv =~ s{(<[^>]*?)([\s/]+)($event_attr)}{$1$2x$3}g;
|
||||
$event_attrs += $rv =~ s{(<)($event_attr)}{$1x$2}g;
|
||||
} while ($event_attrs);
|
||||
}
|
||||
if ($type eq 'pdf') {
|
||||
$rv =~ s/([\n]*)<<[\n((?:.*?|\n)*?)][\w\s\/]+[\n((?:.*?|\n)*?)][\w\s\/]+JavaScript[\w\s\/]*[\n((?:.*?|\n)*?)][\w\s\/]+\s.*?>>[\n]*/$1/gmsi;
|
||||
|
||||
@@ -83,7 +83,7 @@ for(my $i=0; $i<@wlinks; $i++) {
|
||||
}
|
||||
}
|
||||
|
||||
print &ui_alert_box(&filter_javascript($in{'message'}), 'success', undef, 1,
|
||||
print &ui_alert_box(&html_escape($in{'message'}), 'success', undef, 1,
|
||||
&html_escape($in{'title'})) if ($in{'message'});
|
||||
|
||||
&icons_table(\@wlinks, \@wtitles, \@wicons);
|
||||
|
||||
@@ -1962,7 +1962,15 @@ my ($title, $msg) = @_;
|
||||
if (!$gconfig{'restart_async'}) {
|
||||
&restart_miniserv();
|
||||
my $msg_redir = "";
|
||||
$msg_redir = "?title=".&urlize($title)."&message=".&urlize($msg) if $msg;
|
||||
if ($msg) {
|
||||
$title = defined($title) ? &html_strip($title, " ") : "";
|
||||
$msg = &html_strip($msg, " ");
|
||||
$title =~ s/\s+/ /g;
|
||||
$title =~ s/^\s+|\s+$//g;
|
||||
$msg =~ s/\s+/ /g;
|
||||
$msg =~ s/^\s+|\s+$//g;
|
||||
$msg_redir = "?title=".&urlize($title)."&message=".&urlize($msg);
|
||||
}
|
||||
&redirect($msg_redir);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ BEGIN { push(@INC, "."); };
|
||||
use WebminCore;
|
||||
use POSIX;
|
||||
use Socket;
|
||||
|
||||
$main::allow_rpc_only = 1;
|
||||
$force_lang = $default_lang;
|
||||
$trust_unknown_referers = 2; # Only trust if referer was not set
|
||||
&init_config();
|
||||
@@ -317,4 +319,3 @@ $xmlerr .= "</methodResponse>\n";
|
||||
return $xmlerr;
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user