chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:17 +08:00
commit cf8edb9f21
44281 changed files with 1655134 additions and 0 deletions
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env perl
# disable-proxy - Reverse/remove the configuration options set by enable-proxy.
use strict;
use warnings;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; }
use 5.010; # Version in CentOS 6
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use Fcntl qw( :flock );
use Sys::Hostname;
sub main {
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
);
pod2usage(0) if ( $opt{'help'} );
$opt{'config'} ||= "/etc/webmin";
enable_proxy( \%opt );
return 0;
}
exit main( \@ARGV ) if !caller(0);
sub enable_proxy {
my ($optref) = @_;
my @config_lines;
my $file = "$optref->{'config'}/config";
my $referer;
if ($optref->{'referer'}) {
$referer = $optref->{'referer'};
} else {
$referer = hostname;
}
# Setup Webmin
if ($optref->{'prefix'}) {
# Set'em up for proxying on https://domain.tld/prefix
set_config('webprefix', '', $file);
set_config('webprefix_noredir', '', $file);
set_config('ssl_redirect', '1', "$optref->{'config'}/miniserv.conf");
set_config('ssl', '1', "$optref->{'config'}/miniserv.conf");
} else {
# No prefix, just proxying at the root level: https://domain.tld/
set_config('ssl_redirect', '1', "$optref->{'config'}/miniserv.conf");
set_config('ssl', '1', "$optref->{'config'}/miniserv.conf");
}
# Setup the local web server?
# Restart Webmin
say "Restarting Webmin to apply changes...";
system("$optref->{'config'}/restart");
exit 0;
}
sub set_config {
my ($key, $value, $file, $module, $force) = @_;
$key or die RED, "An --option must be specified", RESET;
my @config_lines;
open my $fh, '+<', $file
or die RED, "Unable to open $file", RESET;
flock($fh, LOCK_EX) or die RED, "Unable to lock $file", RESET;
chomp(@config_lines = <$fh>);
# Change'em
my $found = 0;
my $exit_code = 0;
# Validate it against the config.info if this is a module and
if ($module && !$force) {
validate_config_option($key, $value, $module);
}
for (@config_lines) {
if (/^${key}=(.*)/) {
s/^${key}=(.*)/${key}=${value}/;
$found++;
}
}
unless ($found > 0) {
push(@config_lines, "$key=$value");
$exit_code++;
}
# Write'em
seek($fh, 0, 0);
print $fh qq|$_\n| for @config_lines;
close $fh;
}
sub root {
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") || die RED,
"Failed to open $config/miniserv.conf", RESET;
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ( $root ) {
die "$root is not a directory. Is --config correct?" unless (-d $root);
} else {
# Try to guess where Webmin lives, since config file didn't know.
die "Unable to determine Webmin installation directory from $ENV{'WEBMIN_CONFIG'}";
}
return $root;
}
1;
=pod
=head1 NAME
disable-proxy
=head1 DESCRIPTION
Disable proxy-related features in Webmin.
=head1 SYNOPSIS
webmin disable-proxy [options]
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env perl
# disable-twofactor - Disable two-factor authentication for a user.
use strict;
use warnings;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; }
use 5.010; # Version in CentOS 6
use Getopt::Long;
use Pod::Usage;
use Term::ANSIColor qw(:constants);
sub main {
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
'user|u=s' => \$opt{'user'}
);
pod2usage(0) if ( $opt{'help'} );
$opt{'config'} ||= "/etc/webmin";
# Boilerplate, boilerplate, boilerplate...
$ENV{'WEBMIN_CONFIG'} = $opt{'config'};
$ENV{'WEBMIN_VAR'} ||= "/var/webmin";
$ENV{'MINISERV_CONFIG'} = $ENV{'WEBMIN_CONFIG'} . "/miniserv.conf";
my $root = root($opt{'config'});
chdir($root);
$0 = "$root/bin/webmin";
push(@INC, $root);
eval 'use WebminCore'; ## no critic
init_config();
foreign_require('acl', 'acl-lib.pl');
our (%config);
# Get the user
my @users = acl::list_users();
my $user;
($user) = grep { $_->{'name'} eq $opt{'user'} } @users;
# Cancel twofactor authentication
$user->{'twofactor_provider'} = undef;
$user->{'twofactor_id'} = undef;
$user->{'twofactor_apikey'} = undef;
acl::modify_user($user->{'name'}, $user);
reload_miniserv();
webmin_log("onefactor", "user", $user->{'name'});
exit 0;
}
exit main( \@ARGV ) if !caller(0);
sub root {
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") || die RED,
"Failed to open $config/miniserv.conf", RESET;
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ( $root ) {
die "$root is not a directory. Is --config correct?" unless (-d $root);
} else {
die "Unable to determine Webmin installation directory from $ENV{'WEBMIN_CONFIG'}";
}
return $root;
}
1;
=pod
=head1 NAME
disable-twofactor
=head1 DESCRIPTION
Disable two factor authentication for a given user. Useful in cases where the
second factor (e.g. phone or USB key) has been lost.
=head1 SYNOPSIS
webmin disable-twofactor --user username
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>
=item --user, -u
Name of the user to disable two-factor authentication for.
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
Ilia Ross <ilia@virtualmin.com>
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env perl
# enable-proxy - Set Webmin configuration options to support being served
# through a proxy.
use strict;
use warnings;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; }
use 5.010; # Version in CentOS 6
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use Fcntl qw( :flock );
use Sys::Hostname;
sub main {
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
'prefix|p=s' => \$opt{'prefix'},
'referer|r=s' => \$opt{'referer'}
);
pod2usage(0) if ( $opt{'help'} );
$opt{'config'} ||= "/etc/webmin";
enable_proxy( \%opt );
return 0;
}
exit main( \@ARGV ) if !caller(0);
sub enable_proxy {
my ($optref) = @_;
my @config_lines;
my $file = "$optref->{'config'}/config";
my $referer;
if ($optref->{'referer'}) {
$referer = $optref->{'referer'};
} else {
$referer = hostname;
}
# Setup Webmin
if ($optref->{'prefix'}) {
# Set'em up for proxying on https://domain.tld/prefix
set_config('webprefix', $optref->{'prefix'}, $file);
set_config('webprefix_noredir', '1', $file);
set_config('referer', $referer, $file);
set_config('ssl_redirect', '0', "$optref->{'config'}/miniserv.conf");
set_config('ssl', '0', "$optref->{'config'}/miniserv.conf");
} else {
# No prefix, just proxying at the root level: https://domain.tld/
set_config('referer', $referer, $file);
set_config('ssl_redirect', '0', "$optref->{'config'}/miniserv.conf");
set_config('ssl', '0', "$optref->{'config'}/miniserv.conf");
}
# Setup the local web server?
# Restart Webmin
say "Restarting Webmin to apply changes...";
system("$optref->{'config'}/restart");
exit 0;
}
sub set_config {
my ($key, $value, $file, $module, $force) = @_;
$key or die RED, "An --option must be specified", RESET;
my @config_lines;
open my $fh, '+<', $file
or die RED, "Unable to open $file", RESET;
flock($fh, LOCK_EX) or die RED, "Unable to lock $file", RESET;
chomp(@config_lines = <$fh>);
# Change'em
my $found = 0;
my $exit_code = 0;
# Validate it against the config.info if this is a module and
if ($module && !$force) {
validate_config_option($key, $value, $module);
}
for (@config_lines) {
if (/^${key}=(.*)/) {
s/^${key}=(.*)/${key}=${value}/;
$found++;
}
}
unless ($found > 0) {
push(@config_lines, "$key=$value");
$exit_code++;
}
# Write'em
seek($fh, 0, 0);
print $fh qq|$_\n| for @config_lines;
close $fh;
}
sub root {
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") || die RED,
"Failed to open $config/miniserv.conf", RESET;
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ( $root ) {
die "$root is not a directory. Is --config correct?" unless (-d $root);
} else {
# Try to guess where Webmin lives, since config file didn't know.
die "Unable to determine Webmin installation directory from $ENV{'WEBMIN_CONFIG'}";
}
return $root;
}
1;
=pod
=head1 NAME
enable-proxy
=head1 DESCRIPTION
Configure the Webmin web server to be proxied through another web server, like Apache or nginx. This is not usually recommended, and disables some security features, but can help traverse a firewall.
=head1 SYNOPSIS
webmin enable-proxy [options]
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>
=item --prefix, -p
To use a directory prefix for URLs (e.g. https://domain.tld/webmin) provide
it using this option.
=item --referer, -r
The hostname you'll be using in your browser to contact the server. (e.g.
domain.tld)
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
+2282
View File
File diff suppressed because it is too large Load Diff
+203
View File
@@ -0,0 +1,203 @@
#!/usr/bin/env perl
# list-config - List one or all config directives for Webmin or a specific
# module.
use strict;
use warnings;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; }
use 5.010; # Version in CentOS 6
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
use Term::ANSIColor qw(:constants);
sub main {
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
'module|m=s' => \$opt{'module'},
'option|o=s' => \$opt{'option'},
'describe|d' => \$opt{'describe'}
);
pod2usage(0) if ( $opt{'help'} );
$opt{'config'} ||= "/etc/webmin";
list_config( \%opt );
return 0;
}
exit main( \@ARGV ) if !caller(0);
sub list_config {
my ($optref) = @_;
my @config_lines;
# Module or root-level config?
if ($optref->{'module'}) {
# Read the module config file
@config_lines = load_config($optref->{'config'}, "$optref->{'module'}/config");
} else {
@config_lines = load_config($optref->{'config'}, "miniserv.conf");
}
# Display either a single item or the whole thing
if ($optref->{'option'}) {
# Show one item
my $value;
if ($optref->{'module'} and $optref->{'describe'}) {
$value = get_description($optref);
} else {
$value = get_config_value($optref->{'option'}, \@config_lines)
|| die RED, "Unable to retrieve value of $optref->{'option'}", RESET;
}
say $value;
} else {
# Show all
if ($optref->{'module'} and $optref->{'describe'}) {
@config_lines = get_description($optref);
} elsif ($optref->{'describe'}) {
die RED, "--describe only available for modules", RESET;
}
say for @config_lines;
}
}
sub load_config {
my ($config_dir, $file_path) = @_;
my @config_lines;
if (-e "$config_dir/$file_path") {
open my $fh, '<', "$config_dir/$file_path"
|| die RED, "Unable to open $config_dir/$file_path", RESET;
chomp(@config_lines = <$fh>);
close $fh;
} else {
die RED, "Unable to open $config_dir/$file_path.", RESET;
}
return @config_lines;
}
# get_config_var
# Read the file at $config and return the value of key
sub get_config_value {
my ($key, $config_lines_ref) = @_;
my $value;
foreach my $line (@$config_lines_ref) {
if ($line =~ /^${key}=(.*)/) {
$value = $1;
}
}
return $value;
}
# get_description
# Return a description of one or more options from config.info
sub get_description {
my ($optref) = @_;
my $root = root($optref->{'config'});
my $config_info = "$root/$optref->{'module'}/config.info";
my $key = $optref->{'option'};
open my $fh, '<', $config_info
or die RED, "Unable to open $config_info", RESET;
if ($optref->{'option'}) {
my $found = 0;
# return one description
while (<$fh>) {
if (/^${key}=([^,]*)/) {
$found++;
return "$key - $1";
}
}
$found or die RED, "Unrecognized option $key", RESET;
} else {
my $found;
# return all descriptions
my @lines;
while (<$fh>) {
if (/^(.*)=([^,]*)/) {
push (@lines, "$1 - $2");
$found++;
}
}
$found or die RED, "No options found for module $optref->{'module'}";
return @lines;
}
}
sub root {
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") || die RED,
"Failed to open $config/miniserv.conf", RESET;
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ( $root ) {
die "$root is not a directory. Is --config correct?" unless (-d $root);
} else {
# Try to guess where Webmin lives, since config file didn't know.
die "Unable to determine Webmin installation directory from $ENV{'WEBMIN_CONFIG'}";
}
return $root;
}
1;
=pod
=head1 NAME
list-config
=head1 DESCRIPTION
List one or all configuration directives for C<miniserv.conf> or a module C<config> file.
=head1 SYNOPSIS
webmin list-config [options]
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>
=item --module, -m
Specify which module configuration to display. If none given, configuration will be assumed to be the Webmin core configuration (/etc/webmin/miniserv.conf).
=item --option, -o
Specify a single option to display. By default, the entire configuration file will be displayed. If this option is given, only the option specified will be shown.
=item --describe, -d
Display the description of the option from the module C<config.info> file, instead of it's current value. This option is only available for modules, as miniserv.conf does not have a config.info.
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
Executable
+264
View File
@@ -0,0 +1,264 @@
#!/usr/bin/env perl
# passwd - change Webmin users password
use strict;
use warnings;
use 5.010;
use File::Basename;
use Getopt::Long;
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use lib (dirname(dirname($0)));
use WebminCore;
sub main
{
my %opt;
GetOptions('help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
'user|u=s' => \$opt{'user'},
'password|p=s' => \$opt{'password'},
'stdout|o!' => \$opt{'stdout'});
# If username passed as regular param
my $user = scalar(@ARGV) == 1 && $ARGV[0];
# Show usage
pod2usage(0) if ($opt{'help'} || (!$opt{'user'} && !$user));
# Assign defaults
$opt{'config'} ||= "/etc/webmin";
$opt{'user'} = $user if ($user && !$opt{'user'});
# Catch kill signal
my $sigkill = sub {
system("stty echo");
print "\n^C";
print "\n";
exit 1;
};
$SIG{INT} = \&$sigkill;
# Run change password command
change_password(\%opt);
return 0;
}
exit main(\@ARGV) if !caller(0);
sub change_password
{
my ($optref) = @_;
my ($minserv_uconf_file, %lusers, @users, %uinfos, %ulines);
my $user = $optref->{'user'};
my $pass = $optref->{'password'};
my $confdif = $optref->{'config'};
my $conf = "$confdif/config";
my $mconf = "$confdif/miniserv.conf";
my $conf_check = sub {
my ($configs) = @_;
foreach my $config (@{$configs}) {
if (!-r $config) {
say BRIGHT_RED, "Error: ", RESET, "Failed to read Webmin essential config file: ", BRIGHT_YELLOW, $config,
RESET, " doesn't exist";
exit 1;
}
}
};
my $root = root($confdif, \&$conf_check);
my $encrypt_password = sub {
my ($pass, $gconfig, $config) = @_;
my $root = root($confdif, \&$conf_check);
# Use pre-defined encryption (forced by Webmin config)
if (!$optref->{'stdout'} &&
($gconfig->{'md5pass'} == 1 ||
$gconfig->{'md5pass'} == 2))
{
do "$root/acl/md5-lib.pl";
# Use MD5 encryption
return &encrypt_md5($pass) if ($gconfig->{'md5pass'}) == 1;
# Use SHA512 encryption
return &encrypt_sha512($pass) if ($gconfig->{'md5pass'}) == 2;
} else {
# Try detecting system default first
my $module = 'useradmin';
if (-d "$root/$module") {
$ENV{'PERLLIB'} = "$root";
$ENV{'WEBMIN_CONFIG'} = "$confdif";
$ENV{'FOREIGN_ROOT_DIRECTORY'} = "$root/$module";
$ENV{'FOREIGN_MODULE_NAME'} = "$module";
chdir("$root/$module");
require "$root/useradmin/user-lib.pl";
# We need to set third parameter to make sure useradmin's config
# won't be used for hashing format, as we need to auto detect it
return &encrypt_password($pass, undef, 'force_system_detection');
} else {
# Use old Unix DES
srand(time() ^ $$);
return crypt($pass, chr(int(rand(26)) + 65) . chr(int(rand(26)) + 65));
}
}
};
# Check for main config and miniserv config files
&$conf_check([$conf, $mconf]);
# Read and parse configs
my (%config, %gconfig, %uconfig);
read_file($mconf, \%config);
read_file($conf, \%gconfig);
$minserv_uconf_file = $config{'userfile'};
# Check for main user file
&$conf_check([$minserv_uconf_file]);
# Read and parse `miniserv.users` config file
read_file($minserv_uconf_file, \%lusers, undef, undef, ":");
@users = keys %lusers;
map {my @uinfo = split(':', "$lusers{$_}"); $uinfos{$_} = \@uinfo} @users;
# Check if user exists
if (!defined($uinfos{$user})) {
my $user_str = scalar(@users) > 1 ? 'users' : 'user';
my $user_str2 = scalar(@users) > 1 ? 'are' : 'is';
die(BRIGHT_RED, "Error: ", RESET . "Webmin user ",
BRIGHT_YELLOW, $user, RESET, " doesn't exist. Existing Webmin $user_str on your system $user_str2 — ",
BRIGHT_YELLOW, join(", ", sort(@users)),
RESET, "\n");
}
# Ask for password on stdin
my $suc_pre_msg = "";
my $suc_msg = 'updated successfully';
if (!$pass) {
print "Enter password for user ", BRIGHT_YELLOW, $user, RESET, ":";
system("stty -echo");
$pass = <STDIN>;
system("stty echo");
print "\nRetype new password:";
system("stty -echo");
my $pass2 = <STDIN>;
system("stty echo");
print "\n";
if ($pass ne $pass2) {
say BRIGHT_RED, "Error: ", RESET, "Passwords do not match";
exit 1;
}
chomp $pass;
if (!$pass) {
$suc_pre_msg = BOLD BRIGHT_RED ON_WHITE . 'Warning:' . RESET . " ";
$suc_msg = "has been removed, enabling anyone to login without authentication";
}
}
# Update with new password and store timestamp
$uinfos{$user}->[0] = &$encrypt_password($pass, \%gconfig, \%config);
# Print the hash and exit
if ($optref->{'stdout'}) {
say $uinfos{$user}->[0];
exit 0;
}
$uinfos{$user}->[5] = time() if ($uinfos{$user}->[5]);
map {$ulines{$_} = join(":", @{ $uinfos{$_} })} keys %uinfos;
# Store original file first
copy_source_dest($minserv_uconf_file, "$minserv_uconf_file-");
# Restart Webmin and write new user config file
system("$confdif/stop >/dev/null 2>&1");
write_file($minserv_uconf_file, \%ulines, ":");
system("$confdif/start >/dev/null 2>&1");
# Print user message
say "${suc_pre_msg}Password for Webmin user ", BRIGHT_YELLOW, $user, RESET, " $suc_msg";
exit 0;
}
sub root
{
my ($config, $conf_check) = @_;
my $mconf = "$config/miniserv.conf";
$conf_check->([$mconf]);
open(my $CONF, "<", $mconf);
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ($root) {
die BRIGHT_RED, "Error: ", BRIGHT_YELLOW, $root, RESET, " is not a directory\n" unless (-d $root);
} else {
# Try to guess where Webmin lives, since config file didn't know.
die BRIGHT_RED, "Error: ", RESET, "Unable to determine Webmin installation directory\n";
}
return $root;
}
1;
=pod
=head1 NAME
passwd
=head1 DESCRIPTION
This program allows you to change the password of a user in the Webmin password file
=head1 SYNOPSIS
webmin passwd [options]
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
Examples of usage:
- webmin passwd root
- webmin passwd --user root
- webmin passwd --user root --password ycwyMQRVAZY
- webmin passwd --config /usr/local/etc/webmin --user root --password ycwyMQRVAZY
- webmin passwd --config /usr/local/etc/webmin --user root --password ycwyMQRVAZY --stdout
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to C</etc/webmin>
=item --user, -u
Existing Webmin user to change password for
=item --password, -p
Set new user password. Using this option may be unsecure.
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
Ilia Ross <ilia@virtualmin.com>
Executable
+236
View File
@@ -0,0 +1,236 @@
#!/usr/bin/env perl
# patch - Apply a patch to Webmin core or its modules from GitHub or a local file
use strict;
use warnings;
use 5.010;
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
use File::Basename;
use Cwd qw(cwd);
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
);
pod2usage(0) if ($opt{'help'});
# Get Webmin path
my $path = cwd;
my $lib = "web-lib-funcs.pl";
if (!-r "$path/$lib") {
$path = dirname(dirname($0));
if (!-r "$path/$lib") {
$path = $path = Cwd::realpath('..');
}
}
# Init core
my $config_dir = $opt{'config'} || '/etc/webmin';
$ENV{'WEBMIN_CONFIG'} = $config_dir;
push(@INC, $path);
eval 'use WebminCore';
init_config();
# Check if curl is installed
if (!has_command('curl')) {
print "\"curl\" command is not installed\n";
exit 1;
}
# Check if git is installed
if (!has_command('patch')) {
if (!has_command('git')) {
print "Neither \"patch\" nor \"git\" commands are installed\n";
exit 1;
}
}
# Get patch URL or file
my $patch = $ARGV[0];
# Params check
if (!$patch) {
pod2usage(0);
exit 1;
}
# Patch check
if ($patch !~ /^https?:\/\//) {
if (!-r $patch) {
print "Patch file $patch doesn't exist\n";
exit 1;
}
}
elsif ($patch =~ /^https?:\/\/(github|gitlab)\.com/ &&
$patch !~ /\.patch$/ && $patch !~ /\.diff$/) {
$patch .= '.patch';
}
# Parse module name from URL
my $module = "";
if ($patch =~ m{
(?|
# GitHub/GitLab commit URL
https://(?:github|gitlab)\.com/([^/]+)/([^/]+)/commit/([^/]+)
|
# GitHub pull request commit URL
https://github\.com/([^/]+)/([^/]+)/pull/\d+/commits/([^/]+)
|
# GitLab merge request URL with commit ID
https://gitlab\.com/([^/]+)/([^/]+)/-/merge_requests/\d+/diffs\?commit_id=([^&]+)
|
# GitHub raw URL
https://raw\.githubusercontent\.com/([^/]+)/([^/]+)/([^/]+)/(.+)
|
# GitLab raw URL
https://gitlab\.com/([^/]+)/([^/]+)/-/raw/([^/]+)/(.+)
)
}x) {
$module = $2;
$module = "" if ($2 eq 'webmin');
# Special handling for some modules
$module = $module =~ /^virtualmin-pro$/ ?
'virtual-server/pro' :
'virtual-server'
if $module =~ /^virtualmin-(gpl|pro)$/;
}
# Check if module exists
if (!-d "$path/$module") {
print "Module '$module' doesn't exist\n";
exit 1;
}
# Prepare patch command
my $cmd;
my $direct;
my $filename;
my $dir;
my $output;
# If raw URL given, try to download and just replace the whole file
if ($patch =~ m{^https?://raw\.githubusercontent\.com/} ||
$patch =~ m{^https?://gitlab\.com/.*?/-/raw/}) {
if ($patch =~ m{
.*? # Non-greedy match of everything up to the branch name
(?:master|main)/ # Branches name
(.*/)? # Capture all directories after the branch (group 1)
([^/]+)$ # Filename (group 2)
}x)
{
$direct = 1;
$dir = $1 // "";
$filename = $2;
}
else {
print "Patch failed: Can't parse file name from URL\n";
exit 1;
}
my $cd = "$path/$module/$dir";
$cd =~ s|/+|/|g;
chdir "$cd";
$cmd = "curl -w \"%{http_code}\\n\" -s -o $filename @{[quotemeta($patch)]}";
}
# Download command
elsif ($patch =~ /^https?:\/\//) {
$cmd = "curl -L -s @{[quotemeta($patch)]}";
chdir "$path/$module";
}
# Local file
else {
$cmd = "cat @{[quotemeta($patch)]}";
}
# Download file directly
if ($direct) {
$output = `$cmd 2>&1`;
if ($output != 200) {
print "Patch failed: Cannot download '$filename'. HTTP status code: $output\n";
exit 1;
}
}
# Apply patch using patch command
elsif (has_command('patch')) {
$output = `$cmd 2>&1 | patch -p1 --verbose 2>&1`;
if ($output !~ /succeeded/i) {
print "Patch failed: $output\n";
exit 1;
}
}
# Apply patch using git command
else {
$output = `$cmd 2>&1 | git apply --reject --verbose --whitespace=fix 2>&1`;
if ($output !~ /applied patch.*?cleanly/i) {
print "Patch failed: $output\n";
exit 1;
}
}
# Print results
if ($direct) {
print "File replaced successfully:\n";
if ($dir) {
$dir = $dir ? "/$dir" : "";
$dir =~ s|^/||;
}
print " $dir$filename\n";
system("sed -i '1s|^#!/usr/local/bin/perl|#!/usr/bin/perl|' \"$filename\"");
}
else {
print "Patch applied successfully to:\n";
print " $1\n" while $output =~ /^(?|Applied patch\s+(\S+)|patching file\s+(\S+))/mg;
}
# Reload Webmin
reload_miniserv();
=pod
=head1 NAME
patch
=head1 DESCRIPTION
Apply a patch to Webmin core or its modules from GitHub/GitLab, a local
file, or by downloading and replacing the entire file from a raw URL.
=head1 SYNOPSIS
webmin patch patch-url/file
=head1 OPTIONS
=over
=item --help, -h
Give this help list.
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>
Examples of usage:
Apply a patch from a URL.
- webmin patch https://github.com/webmin/webmin/commit/e6a2bb15b0.patch
- webmin patch https://github.com/virtualmin/virtualmin-gpl/commit/f4433153d
Apply a patch from local file.
- cd /usr/libexec/webmin/virtual-server/pro &&
webmin patch /root/virtualmin-pro/patches/patch-1.patch
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2024 Ilia Ross <ilia@virtualmin.com>
Executable
+567
View File
@@ -0,0 +1,567 @@
#!/usr/bin/env perl
# server - control Webmin web-server
use strict;
use warnings;
use 5.010;
use File::Basename;
use Getopt::Long;
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use lib (dirname(dirname($0)));
use WebminCore;
sub main
{
my %opt;
GetOptions('help|h' => \$opt{'help'},
'command|x=s' => \$opt{'command'},
'config|c=s' => \$opt{'config'});
# If username passed as regular param
my $cmd = scalar(@ARGV) == 1 && $ARGV[0];
$cmd = $opt{'command'} if ($opt{'command'});
if ($cmd !~ /^(stats|status|start|stop|restart|reload|force-restart|kill)$/) {
$cmd = undef;
}
# Show usage
pod2usage(0) if ($opt{'help'} || !$cmd);
# Assign defaults
$opt{'config'} ||= "/etc/webmin";
$opt{'cmd'} = $cmd;
# Catch kill signal
my $sigkill = sub {
system("stty echo");
print "\n^C";
print "\n";
exit 1;
};
$SIG{INT} = \&$sigkill;
# Run change password command
run(\%opt);
return 0;
}
exit main(\@ARGV) if !caller(0);
sub run
{
my ($o) = @_;
my $conf_check = sub {
my ($configs) = @_;
foreach my $config (@{$configs}) {
if (!-r $config) {
say BRIGHT_RED, "Error: ", RESET, "Failed to read Webmin essential config file: ", BRIGHT_YELLOW, $config,
RESET, " doesn't exist";
exit 1;
}
}
};
root($o->{'config'}, \&$conf_check);
my $service = ($o->{'config'} =~ /usermin/ ? 'usermin' : 'webmin');
my $systemctlcmd = &has_command('systemctl');
$systemctlcmd =~ s/\s+$//;
if ($o->{'cmd'} =~ /^(start|stop|restart|reload)$/) {
my $rs = system("$o->{'config'}/$o->{'cmd'} $service");
exit $rs;
}
if ($o->{'cmd'} =~ /^(kill)$/) {
my $rs;
if (-x $systemctlcmd) {
$rs = system("$systemctlcmd stop $service");
$rs = system("$systemctlcmd kill -s SIGTERM $service");
}
$rs = system("$o->{'config'}/.stop-init --kill >/dev/null 2>&1 $service");
exit $rs;
}
if ($o->{'cmd'} =~ /^(force-restart)$/) {
my $rs = system("$o->{'config'}/restart-by-force-kill $service");
exit $rs;
}
if ($o->{'cmd'} =~ /^(status)$/) {
my $rs;
if (-x $systemctlcmd) {
$rs = system("$systemctlcmd status $service");
} else {
$rs = system("service $service status");
}
exit $rs;
}
if ($o->{'cmd'} =~ /^(stats)$/) {
my $rs = 0;
if (-x $systemctlcmd) {
my $format_bytes = sub {
my $bytes = shift;
return "0" unless defined $bytes && $bytes =~ /^\d+$/;
my $mb = $bytes / 1048576;
my $gb = $mb / 1024;
if ($gb >= 1) {
return sprintf("%.2f GB", $gb);
} elsif ($mb >= 1) {
return sprintf("%.2f MB", $mb);
} else {
return sprintf("%.2f KB", $bytes / 1024);
}
};
# Check if service is running first
my $is_active_cmd = qq{systemctl is-active "$service" 2>/dev/null};
my $is_active = `$is_active_cmd`;
$rs = $? >> 8;
chomp($is_active);
if ($rs != 0 || $is_active ne 'active') {
print "Service '$service' is not running (status: $is_active)\n";
return 2;
}
# Get main pid
my $main_pid_cmd = qq{systemctl show -p MainPID --value "$service"};
my $main_pid = `$main_pid_cmd`;
$rs = $? >> 8;
return $rs if $rs != 0;
chomp($main_pid);
if (!$main_pid || $main_pid eq '0') {
print "Service '$service' has no main PID\n";
return;
}
# Get process list
my $cmd = qq{
CG=\$(systemctl show -p ControlGroup --value "$service");
P=\$({ cat /sys/fs/cgroup"\$CG"/cgroup.procs; systemctl show -p MainPID --value "$service"; } | sort -u);
COLUMNS=10000 ps --cols 10000 -ww --no-headers -o pid=,ppid=,rss=,pmem=,pcpu=,args= --sort=-rss -p \$P |
awk 'function h(k){m=k/1024;g=m/1024;return g>=1?sprintf("%.2fG",g):sprintf("%.1fM",m)} BEGIN{printf "%6s %6s %9s %6s %6s %-s\\n","PID","PPID","RSS_KiB","%MEM","%CPU","CMD (RSS_human)"} {cmd=substr(\$0,index(\$0,\$6)); printf "%6s %6s %9s %6s %6s %s (%s)\\n",\$1,\$2,\$3,\$4,\$5,cmd,h(\$3)}'
};
my $out = `$cmd`;
$rs = $? >> 8;
return $rs if $rs != 0;
# Extract pids from the output
my @all_pids;
foreach my $line (split(/\n/, $out)) {
if ($line =~ /^\s*(\d+)\s+/) {
push @all_pids, $1;
}
}
if (!@all_pids) {
print "No processes found for service '$service'\n";
return 3;
}
# Reorder with main pid first, then rest sorted by size
my @pids;
if ($main_pid && $main_pid ne '' && grep { $_ eq $main_pid } @all_pids) {
push @pids, $main_pid;
push @pids, grep { $_ ne $main_pid } @all_pids;
} else {
@pids = @all_pids;
}
# Print the table with main pid marked
foreach my $line (split(/\n/, $out)) {
if ($line =~ /^\s*$main_pid\s+/ && $main_pid) {
chomp($line);
print "$line [MAIN]\n";
} else {
print "$line\n";
}
}
# Check if lsof is available
my $has_lsof = has_command('lsof');
# Get detailed info for each pid
foreach my $pid (@pids) {
my $is_main = ($pid eq $main_pid) ? " [MAIN PROCESS]" : "";
# Check if process still exists
unless (-d "/proc/$pid") {
print "\n\nProcess $pid no longer exists, skipping...\n";
next;
}
print "\n";
print "╔" . "═"x78 . "╗\n";
print "║" . sprintf("%-78s", " DETAILED ANALYSIS FOR PID $pid$is_main") . "║\n";
print "╚" . "═"x78 . "╝\n";
# Working directory and binary
print "\n┌─ WORKING DIRECTORY & BINARY " . "─"x49 . "\n";
my $cwd = `readlink /proc/$pid/cwd 2>/dev/null`;
chomp($cwd);
print "CWD: $cwd\n" if $cwd;
my $exe = `readlink /proc/$pid/exe 2>/dev/null`;
chomp($exe);
print "EXE: $exe\n" if $exe;
my $root = `readlink /proc/$pid/root 2>/dev/null`;
chomp($root);
print "ROOT: $root\n" if $root && $root ne '/';
# Environment variables
print "\n┌─ ENVIRONMENT VARIABLES " . "─"x54 . "\n";
my $env = `cat /proc/$pid/environ 2>/dev/null | tr '\\0' '\\n' | grep -E '^(PATH|HOME|USER|LANG|TZ|LD_|PYTHON|JAVA|NODE|PORT|HOST|DB_|API_)' | sort`;
if ($env) {
print $env;
} else {
print "Unable to read environment\n";
}
# Basic process info
print "\n┌─ PROCESS INFO " . "─"x63 . "\n";
my $ps_info = `ps -p $pid -o user=,pid=,ppid=,pri=,ni=,vsz=,rss=,stat=,start=,time=,cmd= 2>/dev/null`;
if ($ps_info) {
print "USER PID PPID PRI NI VSZ RSS STAT START TIME CMD\n";
print $ps_info;
} else {
print "Process no longer exists\n";
next;
}
# Process tree
print "\n┌─ PROCESS TREE " . "─"x63 . "\n";
my $pstree = `pstree -p -a $pid 2>/dev/null`;
if ($pstree) {
print $pstree;
} else {
print "pstree not available\n";
}
# Memory and status
print "\n┌─ MEMORY & STATUS " . "─"x60 . "\n";
my $status = `grep -E 'VmPeak|VmSize|VmRSS|VmSwap|RssAnon|RssFile|Threads|voluntary_ctxt|nonvoluntary_ctxt' /proc/$pid/status 2>/dev/null`;
print $status || "N/A\n";
# Open file descriptors
print "\n┌─ FILE DESCRIPTORS " . "─"x59 . "\n";
my $fd_count = `ls -1 /proc/$pid/fd 2>/dev/null | wc -l`;
chomp($fd_count);
print "Total Open FDs: $fd_count\n";
if ($has_lsof) {
print "\nFile Descriptor Types:\n";
my $fd_types = `lsof +c 0 -p $pid 2>/dev/null | awk 'NR>1 {print \$5}' | sort | uniq -c | sort -rn`;
print $fd_types || "Unable to get FD types\n";
print "\nDetailed File Descriptors:\n";
my $all_fds = `lsof +c 0 -p $pid 2>/dev/null`;
$all_fds =~ s/^/ /mg;
print $all_fds || "No files open\n";
} else {
print "\n(Install lsof for detailed file descriptor analysis)\n";
print "\nOpen FD Sample:\n";
my $fd_sample = `ls -la /proc/$pid/fd 2>/dev/null | head -15`;
print $fd_sample;
}
# Network Connections
print "\n┌─ NETWORK CONNECTIONS " . "─"x56 . "\n";
# tcp connections with details
my $tcp_detailed = `ss -tnp -o 2>/dev/null | grep 'pid=$pid'`;
my $tcp_count = `echo "$tcp_detailed" | grep -c 'pid=$pid'` || 0;
chomp($tcp_count);
print "Active TCP Connections: $tcp_count\n";
if ($tcp_count > 0) {
print "\nTCP Connections (with timers and queues):\n";
print $tcp_detailed;
print "\nConnection State Summary:\n";
my $state_summary = `ss -tnp 2>/dev/null | grep 'pid=$pid' | awk '{print \$1}' | sort | uniq -c | sort -rn`;
print $state_summary;
print "\nLocal Ports in Use:\n";
my $local_ports = `ss -tnp 2>/dev/null | grep 'pid=$pid' | awk '{split(\$4,a,":"); print a[length(a)]}' | sort -n | uniq -c`;
print $local_ports || "None\n";
print "\nRemote Endpoints:\n";
my $remote_ips = `ss -tnp 2>/dev/null | grep 'pid=$pid' | awk '{print \$5}' | cut -d: -f1 | sort | uniq -c | sort -rn`;
print $remote_ips || "None\n";
}
# tcp listening
my $tcp_listen = `ss -tlnp 2>/dev/null | grep 'pid=$pid'`;
if ($tcp_listen) {
print "\nTCP Listening Sockets:\n";
print $tcp_listen;
}
# udp connections
my $udp_count = `ss -unp 2>/dev/null | grep -c 'pid=$pid'`;
chomp($udp_count);
if ($udp_count > 0) {
print "\nUDP Connections: $udp_count\n";
my $udp_conns = `ss -unp 2>/dev/null | grep 'pid=$pid'`;
print $udp_conns;
}
# udp listening
my $udp_listen = `ss -ulnp 2>/dev/null | grep 'pid=$pid'`;
if ($udp_listen) {
print "\nUDP Listening Sockets:\n";
print $udp_listen;
}
# unix sockets
my $unix_sockets = `ss -xp 2>/dev/null | grep 'pid=$pid' | wc -l`;
chomp($unix_sockets);
if ($unix_sockets > 0) {
print "\nUnix Domain Sockets: $unix_sockets\n";
}
# I/O Statistics
print "\n┌─ I/O STATISTICS " . "─"x61 . "\n";
my $io = `cat /proc/$pid/io 2>/dev/null`;
if ($io) {
print $io;
# Parse and show human-readable
my ($read_bytes, $write_bytes);
if ($io =~ /read_bytes:\s*(\d+)/) {
$read_bytes = $1;
}
if ($io =~ /write_bytes:\s*(\d+)/) {
$write_bytes = $1;
}
if (defined $read_bytes && defined $write_bytes) {
print "\nRead: " . $format_bytes->($read_bytes) .
", Write: " . $format_bytes->($write_bytes) . "\n";
}
} else {
print "N/A\n";
}
# Resource Limits
print "\n┌─ RESOURCE LIMITS " . "─"x60 . "\n";
my $limits = `grep -E 'Max open files|Max processes|Max locked memory|Max address space|Max cpu time' /proc/$pid/limits 2>/dev/null`;
print $limits || "N/A\n";
# Cgroup limits
my $cg_path = `cat /proc/$pid/cgroup 2>/dev/null | grep '^0::' | cut -d: -f3`;
chomp($cg_path);
my $cgroup_output = "";
if ($cg_path) {
my $mem_limit = `cat /sys/fs/cgroup$cg_path/memory.max 2>/dev/null`;
my $mem_current = `cat /sys/fs/cgroup$cg_path/memory.current 2>/dev/null`;
my $cpu_max = `cat /sys/fs/cgroup$cg_path/cpu.max 2>/dev/null`;
chomp($mem_limit, $mem_current, $cpu_max);
if ($mem_limit && $mem_limit ne 'max') {
$cgroup_output .= "Memory Limit: " . $format_bytes->(int($mem_limit)) . "\n";
$cgroup_output .= "Memory Current: " . $format_bytes->(int($mem_current)) . "\n" if $mem_current;
if ($mem_current) {
my $pct = sprintf("%.1f", ($mem_current / $mem_limit) * 100);
$cgroup_output .= "Memory Usage: $pct%\n";
}
}
if ($cpu_max && $cpu_max ne 'max') {
$cgroup_output .= "CPU Quota: $cpu_max\n";
}
}
if ($cgroup_output) {
print "\n┌─ CGROUP LIMITS " . "─"x62 . "\n";
print $cgroup_output;
}
# CPU & Scheduling
print "\n┌─ CPU & SCHEDULING " . "─"x59 . "\n";
my $sched = `grep -E 'se.sum_exec_runtime|nr_switches|nr_voluntary_switches|nr_involuntary_switches' /proc/$pid/sched 2>/dev/null | head -4`;
if ($sched) {
print $sched;
}
my $cpuset = `cat /proc/$pid/cpuset 2>/dev/null`;
chomp($cpuset);
print "CPUset: $cpuset\n" if $cpuset;
# Signal handlers
print "\n┌─ SIGNAL HANDLERS " . "─"x60 . "\n";
my $signals = `cat /proc/$pid/status 2>/dev/null | grep -E '^Sig(Cgt|Ign|Blk):'`;
if ($signals) {
print $signals;
# Decode signal masks
my %signal_names = (
1 => 'SIGHUP', 2 => 'SIGINT', 3 => 'SIGQUIT',
4 => 'SIGILL', 5 => 'SIGTRAP', 6 => 'SIGABRT',
7 => 'SIGBUS', 8 => 'SIGFPE', 9 => 'SIGKILL',
10 => 'SIGUSR1', 11 => 'SIGSEGV', 12 => 'SIGUSR2',
13 => 'SIGPIPE', 14 => 'SIGALRM', 15 => 'SIGTERM',
16 => 'SIGSTKFLT', 17 => 'SIGCHLD', 18 => 'SIGCONT',
19 => 'SIGSTOP', 20 => 'SIGTSTP', 21 => 'SIGTTIN',
22 => 'SIGTTOU', 23 => 'SIGURG', 24 => 'SIGXCPU',
25 => 'SIGXFSZ', 26 => 'SIGVTALRM', 27 => 'SIGPROF',
28 => 'SIGWINCH', 29 => 'SIGIO', 30 => 'SIGPWR',
31 => 'SIGSYS'
);
my $decode_sigmask = sub {
my ($hex_mask, $names_ref) = @_;
return "none" if $hex_mask eq '0000000000000000';
# Convert hex to decimal
my $mask = hex($hex_mask);
my @signals;
# Check each bit
for (my $i = 1; $i <= 31; $i++) {
if ($mask & (1 << ($i - 1))) {
push @signals, "$names_ref->{$i}($i)";
}
}
return @signals ? join(", ", @signals) : "none";
};
print "\nDecoded:\n";
if ($signals =~ /SigBlk:\s*([0-9a-f]+)/i) {
print " Blocked: " .
$decode_sigmask->($1, \%signal_names) . "\n";
}
if ($signals =~ /SigIgn:\s*([0-9a-f]+)/i) {
print " Ignored: " .
$decode_sigmask->($1, \%signal_names) . "\n";
}
if ($signals =~ /SigCgt:\s*([0-9a-f]+)/i) {
print " Caught: " .
$decode_sigmask->($1, \%signal_names) . "\n";
}
} else {
print "N/A\n";
}
# Memory maps sum
print "\n┌─ MEMORY MAPS (top 20 by size) " . "─"x47 . "\n";
my $maps = `awk '
/^[0-9a-f]+-[0-9a-f]+/ {hdr=\$0}
/^Size:/ {size=\$2}
/^Rss:/ {rss=\$2}
/^VmFlags:/ { if (rss>0) {print rss"\\t"size"\\t"hdr} rss=0; size=0 }
' /proc/$pid/smaps 2>/dev/null | sort -rn | head -20`;
if ($maps) {
print "RSS(MB)\tSize(MB)\tMapping\n";
foreach my $map_line (split(/\n/, $maps)) {
if ($map_line =~ /^(\d+)\s+(\d+)\s+(.+)$/) {
my $rss_mb = sprintf("%.2f", $1 / 1024);
my $size_mb = sprintf("%.2f", $2 / 1024);
print "$rss_mb\t$size_mb\t\t$3\n";
}
}
} else {
print "Unable to read memory maps\n";
}
# Recent logs
print "\n┌─ RECENT LOGS (last 20 lines) " . "─"x48 . "\n";
my $logs = `journalctl _PID=$pid -b -n 20 --no-pager -o short-precise 2>/dev/null`;
if ($logs && $logs !~ /^-- No entries --/) {
print $logs;
} else {
print "No recent logs found for this PID in current boot\n";
}
print "\n" . "─"x79 . "\n";
}
} else {
print "Stats command is only available on systemd based systems.\n";
$rs = 1;
}
exit $rs;
}
exit 0;
}
sub root
{
my ($config, $conf_check) = @_;
my $mconf = "$config/miniserv.conf";
$conf_check->([$mconf]);
open(my $CONF, "<", $mconf);
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ($root) {
die BRIGHT_RED, "Error: ", BRIGHT_YELLOW, $root, RESET, " is not a directory\n" unless (-d $root);
} else {
# Try to guess where Webmin lives, since config file didn't know.
die BRIGHT_RED, "Error: ", RESET, "Unable to determine Webmin installation directory\n";
}
return $root;
}
1;
=pod
=head1 NAME
server
=head1 DESCRIPTION
This program allows you to control Webmin web-server
=head1 SYNOPSIS
webmin server [command]
webmin [command]
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
Examples of usage:
- webmin server status
- webmin server restart
- webmin server --config /usr/local/etc/webmin --command start
- webmin status
- webmin restart
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to C</etc/webmin>
=item --command, -x
Available commands:
- status
- start
- stop
- restart
- force-restart
- reload
- kill
=back
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
Ilia Ross <ilia@virtualmin.com>
Executable
+184
View File
@@ -0,0 +1,184 @@
#!/usr/bin/env perl
# set-config - Set a Webmin configuration option to the value provided and
# restart Webmin to apply the change.
use strict;
use warnings;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; }
use 5.010; # Version in CentOS 6
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use Fcntl qw( :flock );
sub main {
my %opt;
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
'module|m=s' => \$opt{'module'},
'option|o=s' => \$opt{'option'},
'value|v=s' => \$opt{'value'},
'force|f' => \$opt{'force'}
);
pod2usage(0) if ( $opt{'help'} );
$opt{'config'} ||= "/etc/webmin";
my $rs = set_config( \%opt );
return !$rs ? 1 : 0;
}
exit main( \@ARGV ) if !caller(0);
sub set_config {
my ($optref) = @_;
my $key = $optref->{'option'};
my $value = $optref->{'value'};
$key or die RED, "An --option must be specified", RESET;
my @config_lines;
# Module or root-level config?
my $config_file;
if ($optref->{'module'}) {
$config_file = "$optref->{'config'}/$optref->{'module'}/config";
} else {
$config_file = "$optref->{'config'}/miniserv.conf";
}
# Read'em
open my $fh, '+<', $config_file
or die RED, "Unable to open $config_file", RESET;
flock($fh, LOCK_EX) or die RED, "Unable to lock $config_file", RESET;
chomp(@config_lines = <$fh>);
# Change'em
my $found = 0;
my $force = $optref->{'force'};
# Validate it against the config.info if this is a module and
if ($optref->{'module'} && !$force) {
validate_config_option($optref);
}
for (@config_lines) {
if (/^${key}=(.*)/) {
s/^${key}=(.*)/${key}=${value}/;
$found++;
}
}
unless ($found > 0) {
push(@config_lines, "$key=$value");
}
# Write'em
seek($fh, 0, 0);
print $fh qq|$_\n| for @config_lines;
close $fh;
# Restart Webmin if editing miniserv.conf
unless ($optref->{'module'}) {
say "Restarting Webmin to apply miniserv.conf changes...";
system("$optref->{'config'}/restart");
}
return $force ? ($found > 0 ? $found : -1) : $found;
}
sub validate_config_option {
my ($optref) = @_;
my $root = root($optref->{'config'});
my $key = $optref->{'option'};
# Load the config.info
open my $fh, '<', "$root/$optref->{'module'}/config.info";
# Does this key exist?
my $found;
while (<$fh>) {
if (/^${key}=(.*)/) {
$found++;
}
}
close $fh;
$found or
die RED, "Option '$key' is unknown in module $optref->{'module'}", RESET;
return 1;
}
sub root {
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") || die RED,
"Failed to open $config/miniserv.conf", RESET;
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ( $root ) {
die "$root is not a directory. Is --config correct?" unless (-d $root);
} else {
die "Unable to determine Webmin installation directory from $ENV{'WEBMIN_CONFIG'}";
}
return $root;
}
1;
=pod
=head1 NAME
set-config
=head1 DESCRIPTION
Set a configuration directive in either C<miniserv.conf> (the core Webmin config) or in the specified module C<config>.
=head1 SYNOPSIS
webmin set-config [options] [--module] --option <option-name> --value <value>
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit.
=item --config, -c
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>
=item --module, -m
Specify which module configuration to modify. If none given, configuration will be assumed to be the Webmin core configuration (/etc/webmin/miniserv.conf).
=item --option, -o
Specify the option to change.
=item --value, -v
The value to change the option to.
=item --force, -f
Skip validation of the option name. Allows modifying hidden options, and adding unknown options.
=back
=head1 EXIT CODES
0 on successfully replacing a config variable
1 on successfully adding a new config variable (the specified option did not
already exist in the file, and was added)
>1 on error
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jcameron@webmin.com>
Joe Cooper <joe@virtualmin.com>
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env perl
# update-devel - Update Webmin/Usermin and/or Authentic Theme from Git, with latest unstable, development version.
use strict;
use warnings;
use 5.010;
use Getopt::Long qw(:config permute pass_through);
use Pod::Usage;
use Term::ANSIColor qw(:constants);
use File::Basename;
use Cwd qw(cwd);
my %opt;
GetOptions('help|h' => \$opt{'help'},
'product|p=s' => \$opt{'product'},
'theme|t:s' => \$opt{'theme'});
pod2usage(0) if ($opt{'help'} || !$opt{'product'});
# Get current path
my $path = cwd;
# Check Webmin lib
my $lib = "web-lib-funcs.pl";
if (!-r "$path/$lib") {
$path = dirname(dirname($0));
if (!-r "$path/$lib") {
$path = $path = Cwd::realpath('..');
}
}
# Run actual update or throw an error
my $p = $opt{'product'};
if ($p =~ /^webmin$|^usermin$/i) {
if ($p =~ /^usermin$/i) {
$path =~ s/webmin/$p/;
}
my $cmd = "cd $path && ./update-from-repo.sh -force";
if (defined($opt{'theme'})) {
my $tver = $opt{'theme'} ? " -release:$opt{'theme'}" : "";
$cmd = "cd $path/authentic-theme && ./theme-update.sh$tver -force";
}
system($cmd);
} else {
say RED, "Unknow product name: $p", RESET;
exit 0;
}
=pod
=head1 NAME
update-devel
=head1 DESCRIPTION
Update Webmin/Usermin and/or Authentic Theme from Git, with latest unstable, development version.
=head1 SYNOPSIS
update-devel [options] --product <webmin|usermin> [--theme]
=head1 OPTIONS
=over
=item --product, -p
Specify product name to update, like "webmin" or "usermin".
=item --theme, -t
If set, only Authentic Theme will be updated for specified product
=item --help, -h
=back
Executable
+567
View File
@@ -0,0 +1,567 @@
#!/usr/bin/env perl
# Webmin CLI - Allows performing a variety of common Webmin-related
# functions on the command line.
use strict;
use warnings;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Color'; }
use Getopt::Long qw(:config no_ignore_case permute pass_through);
use Term::ANSIColor qw(color);
use Pod::Usage;
# %ansi_specs
# Map logical color names to preferred Term::ANSIColor specs and
# compatibility fallbacks for older versions.
my %ansi_specs = (
reset => [ 'reset' ],
red => [ 'red' ],
green => [ 'green' ],
yellow => [ 'yellow' ],
cyan => [ 'cyan' ],
dark => [ 'dark', 'faint' ],
bright_red => [ 'bright_red', 'bold red', 'red' ],
bright_green => [ 'bright_green', 'bold green', 'green' ],
bright_yellow => [ 'bright_yellow', 'bold yellow', 'yellow' ],
);
my %ansi_cache;
# ansi(name)
# Resolve a named color to an ANSI escape sequence, with fallbacks
# for older Term::ANSIColor versions that may not support newer names.
sub ansi
{
my ($name) = @_;
return $ansi_cache{$name} if (exists $ansi_cache{$name});
foreach my $spec (@{ $ansi_specs{$name} || [] }) {
my $value = eval { color($spec) };
return $ansi_cache{$name} = $value if (defined($value));
}
return $ansi_cache{$name} = "";
}
# print_line(line)
# Print a full line without relying on Perl's say syntax,
# which is rejected by some older Perl parsers in this script.
sub print_line
{
print @_;
print "\n";
}
# Check if root
if ($> != 0) {
die join("",
ansi('bright_red'), "Error: ", ansi('reset'),
ansi('bright_yellow'), "webmin", ansi('reset'),
" command must be run as root\n");
exit 1;
}
my $a0 = $ARGV[0];
# main()
# Parse global options and dispatch the requested Webmin CLI action.
sub main
{
my ( %opt, $subcmd );
GetOptions(
'help|h' => \$opt{'help'},
'config|c=s' => \$opt{'config'},
'list-commands|l' => \$opt{'list'},
'describe|d' => \$opt{'describe'},
'man|m' => \$opt{'man'},
'version|v' => \$opt{'version'},
'versions|V' => \$opt{'versions'},
'<>' => sub {
# Handle unrecognized options, inc. subcommands.
my($arg) = @_;
if ($arg =~ m{^-}) {
print_line("Usage error: Unknown option $arg.");
pod2usage(0);
}
else {
# It must be a subcommand.
$subcmd = $arg;
die "!FINISH";
}
}
);
# Set defaults
$opt{'config'} ||= "/etc/webmin";
$opt{'commands'} = $a0;
# Load libs
loadlibs(\%opt);
my @remain = @ARGV;
# List commands?
if ($opt{'list'}) {
list_commands(\%opt);
exit 0;
}
elsif ($opt{'version'} || $opt{'versions'}) {
# Load libs
my $print_mod_vers = sub {
my ($module_type, $modules_list, $prod_root, $prod_ver) = @_;
return if (!ref($modules_list));
# Gather module info
my @mods;
foreach my $mod (@{$modules_list}) {
my %mi;
read_file($mod, \%mi);
my $ver = $mi{'version'};
my ($dir) = $mod =~ m/$prod_root\/(.*?)\//;
next if (!$ver || !$mi{'desc'} || !$dir);
next if ($prod_ver =~ /^\Q$ver\E/);
push(@mods, { desc => $mi{'desc'}, ver => $ver,
dir => $dir });
}
# Print sorted by description
my $head;
foreach my $m (sort { $a->{'desc'} cmp $b->{'desc'} } @mods) {
my $mod_ver = $m->{'ver'};
if (-r "$prod_root/$m->{'dir'}/module.info") {
eval { no warnings 'once';
local $main::error_must_die = 1;
&foreign_require($m->{'dir'}) };
# Get module edition if available
my $ed;
$ed = eval { &foreign_call(
$m->{'dir'},
"get_module_edition")
} if (&foreign_defined($m->{'dir'},
"get_module_edition"));
$mod_ver .= " $ed" if ($ed);
}
print_line(ansi('cyan'), " $module_type: ",
ansi('reset')) if (!$head++);
print_line(" $m->{'desc'}: ", ansi('green'),
$mod_ver, ansi('reset'),
ansi('dark'), " [$m->{'dir'}]",
ansi('reset'));
}
};
my $root = root($opt{'config'});
if ($root && -d $root) {
$ENV{'WEBMIN_CONFIG'} = $opt{'config'};
no warnings 'once';
@main::root_directories = ($root);
$main::root_directory = $root;
*unique = sub { my %seen; grep { !$seen{$_}++ } @_ }
if (!defined(&unique));
use warnings;
require("$root/web-lib-funcs.pl");
# Get Webmin version installed
my $ver1 = "$root/version";
my $ver2 = "$opt{'config'}/version";
my $ver = read_file_contents($ver1) ||
read_file_contents($ver2);
my $verrel_file = "$root/release";
my $verrel = -r $verrel_file
? read_file_contents($verrel_file) : "";
if ($verrel) {
$verrel = ":@{[trim($verrel)]}";
}
$ver = trim($ver);
if ($ver) {
if ($opt{'version'}) {
print_line("$ver$verrel");
exit 0;
}
else {
print_line(ansi('cyan'), "Webmin: ",
ansi('reset'),
ansi('green'),
"$ver$verrel", ansi('reset'),
ansi('dark'), " [$root]",
ansi('reset'));
}
}
else {
print_line(ansi('red'), "Error: ",
ansi('reset'),
"Cannot determine Webmin version");
exit 1;
}
# Get other Webmin themes/modules versions if available
my ($dir, @themes, @mods);
if (opendir($dir, $root)) {
while (my $file = readdir($dir)) {
my $theme_info_file =
"$root/$file/theme.info";
push(@themes, $theme_info_file)
if (-r $theme_info_file);
my $mod_info_file = "$root/$file/module.info";
push(@mods, $mod_info_file)
if (-r $mod_info_file);
}
}
closedir($dir);
&$print_mod_vers('Themes', \@themes, $root, $ver);
&$print_mod_vers('Modules', \@mods, $root, $ver);
# Check for Usermin
my $wmumconfig = "$opt{'config'}/usermin/config";
if (-r $wmumconfig) {
my %wmumconfig;
read_file($wmumconfig, \%wmumconfig);
# Usermin config dir
$wmumconfig = $wmumconfig{'usermin_dir'};
if ($wmumconfig) {
my %uminiserv;
read_file("$wmumconfig/miniserv.conf",
\%uminiserv);
my $uroot = $uminiserv{'root'};
# Get Usermin version installed
if ($uroot && -d $uroot) {
my $uver1 = "$uroot/version";
my $uver2 = "$wmumconfig/version";
my $uver = read_file_contents($uver1) ||
read_file_contents($uver2);
my $uverrel_file = "$uroot/release";
my $uverrel = -r $uverrel_file
? read_file_contents($uverrel_file) : "";
$uverrel = ":@{[trim($uverrel)]}" if ($uverrel);
$uver = trim($uver) . $uverrel;
if ($uver) {
print_line(ansi('cyan'),
"Usermin: ",
ansi('reset'),
ansi('green'),
$uver, ansi('reset'),
ansi('dark'),
" [$uroot]",
ansi('reset'));
my ($udir, @uthemes, @umods);
if (opendir($udir, "$uroot")) {
while (my $file = readdir($udir)) {
my $theme_info_file = "$uroot/$file/theme.info";
push(@uthemes, $theme_info_file)
if (-r $theme_info_file);
my $mod_info_file = "$uroot/$file/module.info";
push(@umods, $mod_info_file)
if (-r $mod_info_file);
}
}
closedir($udir);
&$print_mod_vers('Themes', \@uthemes, $uroot, $uver);
&$print_mod_vers('Modules', \@umods, $uroot, $uver);
}
}
}
}
}
exit 0;
}
elsif ($opt{'man'} || $opt{'help'} || !defined($remain[0])) {
# Show the full manual page
man_command(\%opt, $subcmd);
exit 0;
}
elsif ($subcmd) {
run_command( \%opt, $subcmd, \@remain );
}
exit 0;
}
exit main( \@ARGV ) if !caller(0);
# run_command(optref, subcmd, remainref)
# Run a subcommand with the resolved Webmin config and pass through
# its exit status.
sub run_command
{
my ( $optref, $subcmd, $remainref ) = @_;
# Load libs
loadlibs($optref);
# Figure out the Webmin root directory
my $root = root($optref->{'config'});
my (@commands) = list_commands($optref);
if (! grep( /^$subcmd$/, @commands ) ) {
print_line(ansi('red'), "Error: ", ansi('reset'),
"Command \`$subcmd\` doesn't exist",
ansi('reset'));
exit 1;
}
my $command_path = get_command_path($root, $subcmd, $optref);
# Merge the options
my @allopts = ("--config", "$optref->{'config'}", @$remainref);
# Run
system($command_path, @allopts);
# Try to exit with the passed through exit code (rarely used, but why not?)
if ($? == -1) {
print_line(ansi('red'), "Error: ", ansi('reset'),
"Failed to execute \`$command_path\`: $!");
exit 1;
}
else {
exit $? >> 8;
}
}
# get_command_path(root, subcmd, optref)
# Resolve a CLI command name to an executable path under Webmin's
# global or module-specific bin directories.
sub get_command_path
{
my ($root, $subcmd, $optref) = @_;
# Load libs
loadlibs($optref);
# Check for a root-level command (in "$root/bin")
my $command_path;
if ($subcmd) {
$command_path = File::Spec->catfile($root, 'bin', $subcmd);
}
else {
$command_path = File::Spec->catfile($root, 'bin', 'webmin');
}
my $module_name;
my $command;
if ( -x $command_path) {
$command = $command_path;
}
else {
# Try to extract a module name from the command
# Get list of directories
opendir (my $DIR, $root);
my @module_dirs = grep { -d "$root/$_" } readdir($DIR);
closedir($DIR);
# See if any of them are a substring of $subcmd
for my $dir (@module_dirs) {
if (index($subcmd, $dir) == 0) {
$module_name = $dir;
my $barecmd = substr($subcmd, -(length($subcmd)-length($module_name)-1));
$command = File::Spec->catfile($root, $dir, 'bin', $barecmd);
# Could be .pl or no extension
if ( -x $command ) {
last;
}
elsif ( -x $command . ".pl" ) {
$command = $command . ".pl";
last;
}
}
}
}
if ($optref->{'commands'} &&
$optref->{'commands'} =~ /^(stats|status|start|stop|restart|reload|force-restart|force-reload|kill)$/) {
exit system("$0 server $optref->{'commands'}");
}
elsif ($command) {
return $command;
}
else {
die join("", ansi('red'),
"Unrecognized subcommand: $subcmd",
ansi('reset'), "\n");
}
}
# list_commands(optref)
# Enumerate available global and module CLI commands, optionally
# rendering each command's DESCRIPTION section.
sub list_commands
{
my ($optref) = @_;
my $root = root($optref->{'config'});
my @commands;
# Find and list global commands
for my $command (glob ("$root/bin/*")) {
my ($bin, $path) = fileparse($command);
if ($bin =~ "webmin") {
next;
}
if ($optref->{'describe'}) {
# Display name and description
print_line(ansi('yellow'), "$bin", ansi('reset'));
pod2usage( -verbose => 99,
-sections => [ qw(DESCRIPTION) ],
-input => $command,
-exitval => "NOEXIT");
}
else {
if (wantarray) {
push(@commands, $bin);
}
else {
# Just list the names
print_line("$bin");
}
}
}
my @modules;
# Find all module directories with something in bin
for my $command (glob ("$root/*/bin/*")) {
my ($bin, $path) = fileparse($command);
my $module = (split /\//, $path)[-2];
if ($optref->{'describe'}) {
# Display name and description
print_line(ansi('yellow'), "$module-$bin",
ansi('reset'));
pod2usage( -verbose => 99,
-sections => [ qw(DESCRIPTION) ],
-input => $command,
-exitval => "NOEXIT");
}
else {
if (wantarray) {
push(@modules, "$module-$bin");
}
else {
# Just list the names
print_line("$module-$bin");
}
}
}
if (wantarray) {
return (@commands, @modules);
}
}
# man_command(optref, subcmd)
# Display usage message.
sub man_command
{
my ($optref, $subcmd) = @_;
my $root = root($optref->{'config'});
my $command_path = get_command_path($root, $subcmd, $optref);
$ENV{'PAGER'} ||= "more";
open(my $PAGER, "|-", "$ENV{'PAGER'}");
if ($optref->{'help'}) {
pod2usage( -input => $command_path );
}
else {
pod2usage( -verbose => 99,
-input => $command_path,
-output => $PAGER);
}
close($PAGER);
}
# root(config)
# Read the configured Webmin installation root from miniserv.conf.
sub root
{
my ($config) = @_;
open(my $CONF, "<", "$config/miniserv.conf") ||
die join("", ansi('red'),
"Failed to open $config/miniserv.conf",
ansi('reset'), "\n");
my $root;
while (<$CONF>) {
if (/^root=(.*)/) {
$root = $1;
}
}
close($CONF);
# Does the Webmin root exist?
if ( $root ) {
die "$root is not a directory. Is --config correct?\n" unless (-d $root);
}
else {
die "Unable to determine Webmin installation directory ".
"from $ENV{'WEBMIN_CONFIG'}\n";
}
return $root;
}
# loadlibs(optref)
# Load bundled libraries from the Webmin vendor dir when they are not
# available from system package dependencies.
sub loadlibs
{
my ($optref) = @_;
$optref->{'config'} ||= "/etc/webmin";
my $root = root($optref->{'config'});
my $libroot = "$root/vendor_perl";
eval "use lib '$libroot'";
eval "use File::Basename";
eval "use File::Spec";
}
1;
=pod
=head1 NAME
webmin
=head1 DESCRIPTION
Webmin CLI command to perform many common Webmin tasks from the command line or from scripts.
=head1 SYNOPSIS
webmin [options] subcommand [subcommand options]
=head1 OPTIONS
=over
=item --help, -h
Print this usage summary and exit. Subcommands may also have a usage summary.
=item --config <path>, -c <path>
Specify the full path to the Webmin configuration directory. Defaults to
C</etc/webmin>.
=item --list-commands, -l
List available subcommands.
=item --describe, -d
When listing commands, briefly describe what they do.
=item --man <command>, -m <command>
Display the manual page for the given subcommand.
=item --version, -v
Returns current Webmin version installed
=item --versions
Returns Webmin and other modules and themes versions installed (only those for which version is available)
=back
=head1 EXIT CODES
0 on success ; non-0 on error
=head1 LICENSE AND COPYRIGHT
Copyright 2018 Jamie Cameron <jamie@webmin.com>
Joe Cooper <joe@virtualmin.com>
Ilia Ross <ilia@virtualmin.com>