) {
if (/^([^\t]+)\t+([^\t]+)\t+([^\t]+)\t+([^\t]+)\t+(.*)/) {
push(@updates, [ $1, $2, $3, $4, $5 ]);
}
}
close(UPDATES);
unlink($temp);
@updates || &error($text{'update_efile'});
return ( \@updates, $host, $port, $page, $ssl );
}
=head2 check_update_signature(host, port, page, ssl, user, pass, file, sig-mode)
Given a downloaded module update file, fetch the signature from the same URL
with -sig.asc appended, and check that it is valid. Parameters are :
=item host - Module download host
=item port - Module download port
=item page - Module download URL path
=item ssl - Use SSL to download?
=item user - Login for module download
=item pass - Password for module download
=item file - File containing module to check
=item sig-mode - 0=No check, 1=Check if possible, 2=Must check
=cut
sub check_update_signature
{
my ($host, $port, $page, $ssl, $user, $pass, $file, $sigmode) = @_;
my ($ec, $emsg) = &gnupg_setup();
if (!$ec && $sigmode) {
my $err;
my $sig;
&http_download($host, $port, $page."-sig.asc", \$sig,
\$err, undef, $ssl, $user, $pass);
if ($err) {
$sigmode == 2 && return &text('update_enomodsig', $err);
}
else {
my $data = &read_file_contents($file);
my ($vc, $vmsg) = &verify_data($data, $sig);
if ($vc > 1) {
return &text('update_ebadmodsig',
&text('upgrade_everify'.$vc, $vmsg));
}
}
}
return undef;
}
=head2 find_cron_job(\@jobs)
Finds the cron job for Webmin updates, given an array ref of cron jobs
as returned by cron::list_cron_jobs
=cut
sub find_cron_job
{
my ($jobs) = @_;
my ($job) = grep { $_->{'user'} eq 'root' &&
$_->{'command'} eq $cron_cmd } @$jobs;
return $job;
}
=head2 get_ipkeys(&miniserv)
Returns a list of IP address to key file mappings from a miniserv.conf entry.
=cut
sub get_ipkeys
{
my @rv;
foreach my $k (keys %{$_[0]}) {
if ($k =~ /^ipkey_(\S+)/) {
my $ipkey = { 'ips' => [ split(/,/, $1) ],
'key' => $_[0]->{$k},
'index' => scalar(@rv) };
$ipkey->{'cert'} = $_[0]->{'ipcert_'.$1};
$ipkey->{'extracas'} = $_[0]->{'ipextracas_'.$1};
push(@rv, $ipkey);
}
}
return @rv;
}
=head2 save_ipkeys(&miniserv, &keys)
Updates miniserv.conf entries from the given list of keys.
=cut
sub save_ipkeys
{
my $k;
foreach $k (keys %{$_[0]}) {
if ($k =~ /^(ipkey_|ipcert_|ipextracas_)/) {
delete($_[0]->{$k});
}
}
foreach $k (@{$_[1]}) {
my $ips = join(",", @{$k->{'ips'}});
$_[0]->{'ipkey_'.$ips} = $k->{'key'};
if ($k->{'cert'}) {
$_[0]->{'ipcert_'.$ips} = $k->{'cert'};
}
else {
delete($_[0]->{'ipcert_'.$ips});
}
if ($k->{'extracas'}) {
$_[0]->{'ipextracas_'.$ips} = $k->{'extracas'};
}
else {
delete($_[0]->{'ipextracas_'.$ips});
}
}
}
=head2 validate_key_cert(key, [cert])
Call &error if some key and cert file don't look correct, based on the BEGIN
line.
=cut
sub validate_key_cert
{
my ($keyfile, $certfile) = @_;
-r $keyfile || return &error(&text('ssl_ekey', $keyfile));
my $key = &read_file_contents($keyfile);
$key =~ /BEGIN (RSA |EC )?PRIVATE KEY/i ||
&error(&text('ssl_ekey2', $keyfile));
if (!$certfile) {
$key =~ /BEGIN (CERTIFICATE|PUBLIC KEY)/ || &error(&text('ssl_ecert2', $keyfile));
}
else {
-r $certfile || return &error(&text('ssl_ecert', $certfile));
my $cert = &read_file_contents($certfile);
$cert =~ /BEGIN (CERTIFICATE|PUBLIC KEY)/ || &error(&text('ssl_ecert2', $certfile));
}
}
=head2 detect_operating_system([os-list-file], [with-cache])
Returns a hash containing os_type, os_version, real_os_type and
real_os_version, suitable for the current system.
=cut
sub detect_operating_system
{
my $file = $_[0] || "$root_directory/os_list.txt";
my $cache = $_[1];
if ($cache) {
# Check the cache file, and only re-check the OS if older than
# 1 day, or if we have rebooted recently
my %cache;
my $uptime = &get_system_uptime();
my $lastreboot = $uptime ? time()-$uptime : undef;
if (&read_file($detect_operating_system_cache, \%cache) &&
$cache{'os_type'} && $cache{'os_version'} &&
$cache{'real_os_type'} && $cache{'real_os_version'}) {
if ($cache{'time'} > time()-24*60*60 &&
$cache{'time'} > $lastreboot) {
return %cache;
}
}
}
my $temp = &transname();
my $perl = &get_perl_path();
system("$perl $root_directory/oschooser.pl ".
quotemeta($file)." ".quotemeta($temp)." 1");
my %rv;
&read_env_file($temp, \%rv);
$rv{'time'} = time();
&write_file($detect_operating_system_cache, \%rv);
&unlink_file($temp);
return %rv;
}
=head2 show_webmin_notifications([no-updates])
Print various notifications for the current user, if any. These can include
password expiry, Webmin updates and more.
=cut
sub show_webmin_notifications
{
my ($noupdates) = @_;
my @notifs = &get_webmin_notifications($noupdates);
if (@notifs) {
print "\n",join("
\n", @notifs),"\n";
}
}
=head2 get_webmin_notifications([no-updates])
Returns a list of Webmin notification messages, each of which is a string of
HTML. If the no-updates flag is set, Webmin version / module updates are
not included.
=cut
sub get_webmin_notifications
{
my ($noupdates) = @_;
$noupdates = 1 if (&shared_root_directory());
my @notifs;
my %miniserv;
&get_miniserv_config(\%miniserv);
&load_theme_library(); # So that UI functions work
# Need OS upgrade, but only once per day or if the system was rebooted
my $now = time();
my $uptime = &get_system_uptime();
if (&foreign_available("webmin")) {
my %realos;
my @st = stat($realos_cache_file);
if (!@st || $now - $st[9] > 24*60*60 ||
$uptime && $now - $st[9] > $uptime) {
%realos = &detect_operating_system(undef, 1);
&write_file($realos_cache_file, \%realos);
}
else {
&read_file($realos_cache_file, \%realos);
}
my $new_real = $realos{'real_os_version'};
my $old_real = $gconfig{'real_os_version'};
$new_real =~ s/\.\d+$//;
$old_real =~ s/\.\d+$//;
if ($realos{'real_os_type'} eq $gconfig{'real_os_type'} &&
$new_real eq $old_real &&
$realos{'real_os_version'} ne $gconfig{'real_os_version'}) {
# Only the minor OS version has changed, just silently update it
&lock_file("$config_directory/config");
$gconfig{'real_os_version'} = $realos{'real_os_version'};
$gconfig{'os_version'} = $realos{'os_version'};
&write_file("$config_directory/config", \%gconfig);
&unlock_file("$config_directory/config");
}
if (($realos{'os_version'} ne $gconfig{'os_version'} ||
$realos{'real_os_version'} ne $gconfig{'real_os_version'} ||
$realos{'os_type'} ne $gconfig{'os_type'}) &&
$realos{'os_version'} && $realos{'os_type'} &&
&foreign_available("webmin")) {
# Tell the user that OS version was updated
push(@notifs,
&ui_form_start("@{[&get_webprefix()]}/webmin/fix_os.cgi").
&text('os_incorrect',
$realos{'real_os_type'},
$realos{'real_os_version'}).
&show_os_release_notes($realos{'real_os_version'}).
"\n".
&ui_form_end([ [ undef, $text{'os_fix'} ] ])
);
}
}
# Password close to expiry
my $warn_days = $config{'warn_days'};
if (&foreign_check("acl")) {
# Get the Webmin user
&foreign_require("acl", "acl-lib.pl");
my @users = &acl::list_users();
my ($uinfo) = grep { $_->{'name'} eq $base_remote_user } @users;
if ($uinfo && $uinfo->{'pass'} eq 'x' && &foreign_check("useradmin")) {
# Unix auth .. check password in Users and Groups
&foreign_require("useradmin", "user-lib.pl");
($uinfo) = grep { $_->{'user'} eq $remote_user }
&useradmin::list_users();
if ($uinfo && $uinfo->{'warn'} && $uinfo->{'change'} &&
$uinfo->{'max'}) {
my $daysago = int(time()/(24*60*60)) -
$uinfo->{'change'};
my $cdate = &make_date(
$uinfo->{'change'}*24*60*60, 1);
if ($daysago > $uinfo->{'max'}) {
# Passed expiry date
push(@notifs, &text('notif_unixexpired',
$cdate));
}
elsif ($daysago > $uinfo->{'max'}-$uinfo->{'warn'}) {
# Passed warning date
push(@notifs, &text('notif_unixwarn',
$cdate,
$uinfo->{'max'}-$daysago));
}
}
}
elsif ($uinfo && $uinfo->{'lastchange'}) {
# Webmin auth .. check password in Webmin
my $daysold = (time() - $uinfo->{'lastchange'})/(24*60*60);
my $link = &foreign_available("change-user") ?
&text('notif_changenow',
"@{[&get_webprefix()]}/change-user/")."
\n" : "";
if ($miniserv{'pass_maxdays'} &&
$daysold > $miniserv{'pass_maxdays'}) {
# Already expired
push(@notifs, &text('notif_passexpired')."
\n".$link);
}
elsif ($miniserv{'pass_maxdays'} &&
$daysold > $miniserv{'pass_maxdays'} - $warn_days) {
# About to expire
push(@notifs, &text('notif_passchange',
&make_date($uinfo->{'lastchange'}, 1),
int($miniserv{'pass_maxdays'} - $daysold)).
"
\n".$link);
}
elsif ($miniserv{'pass_lockdays'} &&
$daysold > $miniserv{'pass_lockdays'} - $warn_days) {
# About to lock out
push(@notifs, &text('notif_passlock',
&make_date($uinfo->{'lastchange'}, 1),
int($miniserv{'pass_maxdays'} - $daysold)).
"
\n".$link);
}
}
}
# New Webmin version is available, but only once per day
my %access = &get_module_acl();
my %disallow = map { $_, 1 } split(/\s+/, $access{'disallow'} || "");
my %allow = map { $_, 1 } split(/\s+/, $access{'allow'} || "");
if (&foreign_available($module_name) && !$gconfig{'nowebminup'} &&
!$gconfig{'noselfwebminup'} && !$noupdates &&
($allow{'upgrade'} || !$disallow{'upgrade'})) {
if (!$config{'last_version_check'} ||
$now - $config{'last_version_check'} > 24*60*60) {
# Cached last version has expired .. re-fetch
my ($ok, $version, $release) = &get_latest_webmin_version();
if ($ok) {
$config{'last_version_check'} = $now;
$config{'last_version_number'} = $version;
$config{'last_version_release'} = $release;
$config{'last_version_full'} =
$version.($release ? "-".$release : "");
&save_module_config();
}
}
my $minor_release =
$config{'last_version_release'} &&
$config{'last_version_release'} >= 2;
my $full = &get_webmin_full_version();
my $compver = $config{'last_version_full'};
my ($repotype, $repover) = &get_webmin_repo_version();
my $source = 2;
if ($repotype) {
$compver = $repover;
$source = 6;
}
if ($compver &&
&compare_version_numbers($compver, $full) > 0) {
# New version is out there .. offer to upgrade
my $mode = &get_install_type();
my $checksig = 0;
if ((!$mode || $mode eq "rpm") && &foreign_check("proc")) {
my ($ec, $emsg) = &gnupg_setup();
if (!$ec) {
$checksig = 1;
}
}
my $release_notes_link = " ".
&ui_link("https://github.com/webmin/webmin/releases/tag/".
"$config{'last_version_number'}",
$text{'os_release_notes'}, undef,
'target="_blank" data-link-external="after"').".";
# $release_notes_link = "" if ($minor_release);
push(@notifs,
&ui_form_start("@{[&get_webprefix()]}/webmin/upgrade.cgi").
&ui_hidden("source", $source).
&ui_hidden("sig", $checksig).
&ui_hidden("mode", $mode).
&text('notif_upgrade', $config{'last_version_full'}, $full).
"$release_notes_link
\n".
&ui_form_end([ [ undef, $text{'notif_upgradeok'} ] ]));
}
}
# Check for use of the old YUM or APT repos
my $repoerr;
if (-r $webmin_yum_repo_file) {
my $lref = &read_file_lines($webmin_yum_repo_file, 1);
foreach my $l (@$lref) {
if ($l =~ /^\s*baseurl\s*=\s*(\S+)/ &&
$1 ne $webmin_yum_repo_url) {
$repoerr = &text('notify_yumrepo',
$webmin_yum_repo_url);
last;
}
}
}
foreach my $repo ($webmin_apt_repo_file, $global_apt_repo_file) {
next if (!-r $repo);
my $lref = &read_file_lines($repo, 1);
foreach my $l (@$lref) {
if ($l =~ /^\s*deb\s+.*?((http|https):\/\/download.webmin.com\/download\/repository)\s+sarge\s+contrib/) {
$repoerr = &text('notify_aptrepo',
$webmin_apt_repo_url);
last;
}
}
}
if ($repoerr && &foreign_available("webmin")) {
push(@notifs,
&ui_form_start("@{[&get_webprefix()]}/webmin/fix_repo.cgi").
$repoerr."
\n".
&ui_form_end([ [ undef, $text{'notif_fixreponow'} ] ]));
}
# Reboot needed
if (&foreign_check("package-updates") && &foreign_available("init")) {
&foreign_require("package-updates");
my $allow_reboot_required = 1;
if (-r $postpone_reboot_required) {
my $uptime = &get_system_uptime();
my $lastreboot = $uptime ? time()-$uptime : undef;
if ($lastreboot) {
my @prr = stat($postpone_reboot_required);
if ($lastreboot < $prr[9]) {
$allow_reboot_required = 0;
}
}
}
if ($allow_reboot_required &&
&package_updates::check_reboot_required()) {
push(@notifs,
&ui_form_start("@{[&get_webprefix()]}/init/reboot.cgi").
$text{'notif_reboot'}."
\n".
&ui_form_end([ [ undef, $text{'notif_rebootok'} ],
[ 'removenotify', $text{'alert_hide'} ] ]));
}
}
return @notifs;
}
=head2 get_system_uptime
Returns the number of seconds the system has been up, or undef if un-available.
=cut
sub get_system_uptime
{
# Try Linux /proc/uptime first
if (open(UPTIME, ";
close(UPTIME);
my ($uptime, $dummy) = split(/\s+/, $line);
if ($uptime > 0) {
return int($uptime);
}
}
# Try to parse uptime command output
if ($gconfig{'os_type'} ne 'windows') {
my $out = &backquote_command("uptime");
if ($out =~ /up\s+(\d+)\s+day/) {
return $1*24*60*60;
}
elsif ($out =~ /up\s+(\d+)\s+min/) {
return $1*60;
}
elsif ($out =~ /up\s+(\d+)\s+hour/) {
return $1*60*60;
}
elsif ($out =~ /up\s+(\d+):(\d+)/) {
return $1*60*60 + $2*60;
}
}
return undef;
}
=head2 list_operating_systems([os-list-file])
Returns a list of known OSs, each of which is a hash ref with keys :
=item realtype - A human-readable OS name, like Ubuntu Linux.
=item realversion - A human-readable version, like 8.04.
=item type - Webmin's internal OS code, like debian-linux.
=item version - Webmin's internal version number, like 3.1.
=item code - A fragment of Perl that will return true if evaluated on this OS.
=cut
sub list_operating_systems
{
my $file = $_[0] || "$root_directory/os_list.txt";
my @rv;
open(OSLIST, "<".$file);
while() {
if (/^([^\t]+)\t+([^\t]+)\t+([^\t]+)\t+([^\t]+)\t+(.*)/) {
push(@rv, { 'realtype' => $1,
'realversion' => $2,
'type' => $3,
'version' => $4,
'code' => $5 });
}
}
close(OSLIST);
return @rv;
}
=head2 shared_root_directory
Returns 1 if the Webmin root directory is shared with another system, such as
via NFS, or in a Solaris zone. If so, updates and module installs are not
allowed.
=cut
sub shared_root_directory
{
if (exists($gconfig{'shared_root'}) && $gconfig{'shared_root'} eq '1') {
# Always shared
return 1;
}
elsif (exists($gconfig{'shared_root'}) && $gconfig{'shared_root'} eq '0') {
# Definitely not shared
return 0;
}
if (&running_in_zone()) {
# In a Solaris zone .. is the root directory loopback mounted?
if (&foreign_exists("mount")) {
&foreign_require("mount", "mount-lib.pl");
my @rst = stat($root_directory);
my $m;
foreach $m (&mount::list_mounted()) {
my @mst = stat($m->[0]);
if ($mst[0] == $rst[0] &&
&is_under_directory($m->[0], $root_directory)) {
# Found the mount!
if ($m->[2] eq "lofs" || $m->[2] eq "nfs") {
return 1;
}
}
}
}
}
return 0;
}
=head2 submit_os_info(id)
Send via email a message about this system's OS and Perl version. Returns
undef if OK, or an error message.
=cut
sub submit_os_info
{
if (!&foreign_installed("mailboxes", 1)) {
return $text{'submit_emailboxes'};
}
&foreign_require("mailboxes", "mailboxes-lib.pl");
my $mail = { 'headers' => [ [ 'From', &mailboxes::get_from_address() ],
[ 'To', $os_info_address ],
[ 'Subject', 'Webmin OS Information' ] ],
'attach' => [ {
'headers' => [ [ 'Content-type', 'text/plain' ] ],
'data' => "OS: $gconfig{'real_os_type'}\n".
"Version: $gconfig{'real_os_version'}\n".
"OS code: $gconfig{'os_type'}\n".
"Version code: $gconfig{'os_version'}\n".
"Perl: $]\n".
"Webmin: ".&get_webmin_version()."\n".
"ID: ".&get_webmin_id()."\n" } ],
};
eval { &mailboxes::send_mail($mail); };
return $@ ? $@ : undef;
}
=head2 get_webmin_id
Returns a (hopefully) unique ID for this Webmin install.
=cut
sub get_webmin_id
{
if (!$config{'webminid'}) {
my $salt = substr(time(), -2);
$config{'webminid'} = &unix_crypt(&get_system_hostname(), $salt);
&save_module_config();
}
return $config{'webminid'};
}
=head2 ip_match(ip, [match]+)
Checks an IP address against a list of IPs, networks and networks/masks, and
returns 1 if a match is found.
=cut
sub ip_match
{
my @io = &check_ip6address($_[0]) ? split(/:/, $_[0])
: split(/\./, $_[0]);
# Resolve to hostname and check that it forward resolves again
my $hn = &to_hostname($_[0]);
if (&check_ip6address($_[0])) {
$hn = "" if (&to_ip6address($hn) ne $_[0]);
}
else {
$hn = "" if (&to_ipaddress($hn) ne $_[0]);
}
for(my $i=1; $i<@_; $i++) {
my $mismatch = 0;
my $ip = $_[$i];
if ($ip =~ /^([0-9\.]+)\/(\d+)$/) {
# Convert CIDR to netmask format
$ip = $1."/".&prefix_to_mask($2);
}
if ($ip =~ /^([0-9\.]+)\/([0-9\.]+)$/) {
# Compare with IPv4 network/mask
my @mo = split(/\./, $1);
my @ms = split(/\./, $2);
for(my $j=0; $j<4; $j++) {
if ((int($io[$j]) & int($ms[$j])) != (int($mo[$j]) & int($ms[$j]))) {
$mismatch = 1;
}
}
}
elsif ($_[$i] =~ /^([0-9\.]+)-([0-9\.]+)$/) {
# Compare with an IPv4 range (separated by a hyphen -)
my ($remote, $min, $max);
my @low = split(/\./, $1);
my @high = split(/\./, $2);
for(my $j=0; $j<4; $j++) {
$remote += $io[$j] << ((3-$j)*8);
$min += $low[$j] << ((3-$j)*8);
$max += $high[$j] << ((3-$j)*8);
}
if ($remote < $min || $remote > $max) {
$mismatch = 1;
}
}
elsif ($ip =~ /^\*(\.\S+)$/) {
# Compare with hostname regexp
$mismatch = 1 if ($hn !~ /^.*\Q$1\E$/i);
}
elsif ($ip eq 'LOCAL') {
# Just assume OK for now
}
elsif ($_[$i] =~ /^[0-9\.]+$/) {
# Compare with IPv4 address or network
my @mo = split(/\./, $_[$i]);
while(@mo && !$mo[$#mo]) { pop(@mo); }
for(my $j=0; $j<@mo; $j++) {
if ($mo[$j] != $io[$j]) {
$mismatch = 1;
}
}
}
elsif ($_[$i] =~ /^[a-f0-9:]+$/) {
# Compare with a full IPv6 address
if (&canonicalize_ip6($_[$i]) ne canonicalize_ip6($_[0])) {
$mismatch = 1;
}
}
elsif ($_[$i] =~ /^([a-f0-9:]+)\/(\d+)$/) {
# Compare with an IPv6 network
my $v6size = $2;
my $v6addr = &canonicalize_ip6($1);
my $bytes = $v6size / 8;
my @mo = &expand_ipv6_bytes($v6addr);
my @io = &expand_ipv6_bytes(&canonicalize_ip6($_[0]));
for(my $j=0; $j<$bytes; $j++) {
if ($mo[$j] ne $io[$j]) {
$mismatch = 1;
}
}
}
elsif ($_[$i] !~ /^[0-9\.]+$/) {
# Compare with hostname
$mismatch = 1 if ($_[0] ne &to_ipaddress($_[$i]));
}
return 1 if (!$mismatch);
}
return 0;
}
=head2 expand_ipv6_bytes(address)
Given a canonical IPv6 address, split it into an array of bytes
=cut
sub expand_ipv6_bytes
{
my ($addr) = @_;
my @rv;
foreach my $w (split(/:/, $addr)) {
$w =~ /^(..)(..)$/ || return ( );
push(@rv, hex($1), hex($2));
}
return @rv;
}
=head2 prefix_to_mask(prefix)
Converts a number like 24 to a mask like 255.255.255.0.
=cut
sub prefix_to_mask
{
return $_[0] >= 24 ? "255.255.255.".(256-(2 ** (32-$_[0]))) :
$_[0] >= 16 ? "255.255.".(256-(2 ** (24-$_[0]))).".0" :
$_[0] >= 8 ? "255.".(256-(2 ** (16-$_[0]))).".0.0" :
(256-(2 ** (8-$_[0]))).".0.0.0";
}
=head2 valid_allow(text)
Returns undef if some text is a valid IP, hostname or network for use in
allowed IPs, or an error message if not
=cut
sub valid_allow
{
my ($h) = @_;
if ($h =~ /^([0-9\.]+)\/(\d+)$/) {
# IPv4 address/cidr
&check_ipaddress($1) ||
return &text('access_enet', "$1");
$2 >= 0 && $2 <= 32 ||
return &text('access_ecidr', "$2");
}
elsif ($h =~ /^([0-9\.]+)\/([0-9\.]+)$/) {
# IPv4 address/netmask
&check_ipaddress($1) ||
return &text('access_enet', "$1");
&check_ipaddress($2) ||
return &text('access_emask', "$2");
}
elsif ($h =~ /^([0-9\.]+)\-([0-9\.]+)$/) {
# IPv4 address
&check_ipaddress("$1") ||
return &text('access_eip', "$1");
&check_ipaddress("$2") ||
return &text('access_eip', "$2");
}
elsif ($h =~ /^[0-9\.]+$/) {
# IPv4 address
&check_ipaddress($h) ||
return &text('access_eip', $h);
}
elsif ($h =~ /^([a-f0-9:]+)\/(\d+)$/) {
# IPv6 address/prefix
&check_ip6address($1) ||
return &text('access_eip6', $1);
$2 >= 0 && $2 <= 128 ||
return &text('access_ecidr6', "$2");
$2 % 8 == 0 ||
return &text('access_ecidr8', "$2");
}
elsif ($h =~ /^[a-f0-9:]+$/) {
# IPv6 address
&check_ip6address($h) ||
return &text('access_eip6', $h);
}
elsif ($h =~ /^\*\.(\S+)$/) {
# *.domain is OK
}
elsif ($h eq 'LOCAL') {
# Local means any on local nets
}
elsif (&to_ipaddress($h) || &to_ip6address($h)) {
# Resolvable hostname
}
else {
return &text('access_ehost', $h);
}
return undef;
}
=head2 get_preloads(&miniserv)
Returns a list of module names and files to pre-load, based on a Webmin
miniserv configuration hash. Each is a two-element array ref containing
a package name and the relative path of the .pl file to pre-load.
=cut
sub get_preloads
{
my @rv = map { [ split(/=/, $_) ] } split(/\s+/, $_[0]->{'preload'} || "");
return @rv;
}
=head2 save_preloads(&miniserv, &preloads)
Updates a Webmin miniserv configuration hash from a list of preloads, in
the format returned by get_preloads.
=cut
sub save_preloads
{
$_[0]->{'preload'} = join(" ", map { "$_->[0]=$_->[1]" } @{$_[1]});
}
=head2 get_tempdirs(&gconfig)
Returns a list of per-module temp directories, each of which is an array
ref containing a module name and directory.
=cut
sub get_tempdirs
{
my ($gconfig) = @_;
my @rv;
foreach my $k (keys %$gconfig) {
if ($k =~ /^tempdir_(.*)$/) {
push(@rv, [ $1, $gconfig->{$k} ]);
}
}
return sort { $a->[0] cmp $b->[0] } @rv;
}
=head2 save_tempdirs(&gconfig, &tempdirs)
Updates the global config with a list of per-module temp dirs
=cut
sub save_tempdirs
{
my ($gconfig, $dirs) = @_;
foreach my $k (keys %$gconfig) {
if ($k =~ /^tempdir_(.*)$/) {
delete($gconfig->{$k});
}
}
foreach my $d (@$dirs) {
$gconfig->{'tempdir_'.$d->[0]} = $d->[1];
}
}
=head2 get_module_install_type(dir)
Returns the installation method used for some module (such as 'rpm'), or undef
if it was installed from a .wbm.
=cut
sub get_module_install_type
{
my ($mod) = @_;
my $it = &module_root_directory($mod)."/install-type";
open(TYPE, "<".$it) || return undef;
my $type = ;
chop($type);
close(TYPE);
return $type;
}
=head2 get_install_type
Returns the package type Webmin was installed form (rpm, deb, solaris-pkg
or undef for tar.gz).
=cut
sub get_install_type
{
my $mode;
if (open(MODE, "<$root_directory/install-type")) {
chop($mode = );
close(MODE);
}
else {
if ($root_directory eq "/usr/libexec/webmin") {
$mode = "rpm";
}
elsif ($root_directory eq "/usr/share/webmin") {
$mode = "deb";
}
elsif ($root_directory eq "/opt/webmin") {
$mode = "solaris-pkg";
}
elsif (&has_command("eix") &&
&backquote_command("eix webmin 2>/dev/null") =~ /Installed/i) {
$mode = "portage";
}
else {
$mode = undef;
}
}
return $mode;
}
=head2 list_cached_files
Returns a list of cached filenames for downloads made by Webmin, as array refs
containing a full path and url.
=cut
sub list_cached_files
{
my @rv;
opendir(DIR, $main::http_cache_directory);
foreach my $cfile (readdir(DIR)) {
next if ($cfile eq "." || $cfile eq "..");
my $curl = $cfile;
$curl =~ s/_/\//g;
push(@rv, [ $cfile, "$main::http_cache_directory/$cfile", $curl ]);
}
closedir(DIR);
return @rv;
}
=head2 show_restart_page([title, msg])
Output a page with header and footer about Webmin needing to restart.
=cut
sub show_restart_page
{
if (!$gconfig{'restart_async'}) {
&restart_miniserv();
&redirect("");
return;
}
my ($title, $msg) = @_;
$title ||= $text{'restart_title'};
$msg ||= $text{'restart_done'};
&ui_print_header(undef, $title, "");
print "$msg\n";
&ui_print_footer("", $text{'index_return'});
&restart_miniserv(1);
}
=head2 cert_info(file)
Returns a hash of details of a cert in some file.
=cut
sub cert_info
{
my %rv;
local $_;
open(OUT, "openssl x509 -in ".quotemeta($_[0])." -issuer -subject -enddate -text |");
while() {
s/\r|\n//g;
if (/subject=.*CN\s*=\s*([^\/,]+)/) {
$rv{'cn'} = $1;
}
if (/subject=.*O\s*=\s*([^\/,]+)/) {
$rv{'o'} = $1;
}
if (/subject=.*Email\s*=\s*([^\/,]+)/) {
$rv{'email'} = $1;
}
if (/issuer=.*CN\s*=\s*([^\/,]+)/) {
$rv{'issuer_cn'} = $1;
}
if (/issuer=.*O\s*=\s*([^\/,]+)/) {
$rv{'issuer_o'} = $1;
}
if (/issuer=.*Email\s*=\s*([^\/,]+)/) {
$rv{'issuer_email'} = $1;
}
if (/notAfter\s*=\s*(.*)/) {
$rv{'notafter'} = $1;
}
if (/Subject\s+Alternative\s+Name/i) {
my $alts = ;
$alts =~ s/^\s+//;
foreach my $a (split(/[, ]+/, $alts)) {
if ($a =~ /^DNS:(\S+)/) {
push(@{$rv{'alt'}}, $1);
}
}
}
}
close(OUT);
if ($rv{'o'} && $rv{'issuer_o'}) {
$rv{'type'} = $rv{'o'} eq $rv{'issuer_o'} ? $text{'ssl_typeself'}
: $text{'ssl_typereal'};
}
return \%rv;
}
=head2 cert_pem_data(file)
Returns a cert in PEM format, from a file containing the PEM and possibly
other keys.
=cut
sub cert_pem_data
{
my ($d) = @_;
my $data = &read_file_contents($_[0]);
if ($data =~ /(-----BEGIN\s+CERTIFICATE-----\n([A-Za-z0-9\+\/=\n\r]+)-----END\s+CERTIFICATE-----)/) {
return $1;
}
return undef;
}
=head2 cert_pkcs12_data(keyfile, [certfile])
Returns a cert in PKCS12 format.
=cut
sub cert_pkcs12_data
{
my ($keyfile, $certfile) = @_;
if ($certfile) {
open(OUT, "openssl pkcs12 -in ".quotemeta($certfile).
" -inkey ".quotemeta($keyfile).
" -export -passout pass: -nokeys |");
}
else {
open(OUT, "openssl pkcs12 -in ".quotemeta($keyfile).
" -export -passout pass: -nokeys |");
}
my $data;
while() {
$data .= $_;
}
close(OUT);
return $data;
}
=head2 cert_file_split(file)
Returns a list of certs in some file
=cut
sub cert_file_split
{
my ($file) = @_;
my @rv;
my $lref = &read_file_lines($file, 1);
foreach my $l (@$lref) {
my $cl = $l;
$cl =~ s/^#.*//;
if ($cl =~ /^-----BEGIN/) {
push(@rv, $cl."\n");
}
elsif ($cl =~ /\S/ && @rv) {
$rv[$#rv] .= $cl."\n";
}
}
return @rv;
}
=head2 get_blocked_users_hosts(&miniserv)
Returns a list of blocked users and hosts from the file written by Webmin
at run-time.
=cut
sub get_blocked_users_hosts
{
my ($miniserv) = @_;
my $bf = $miniserv->{'blockedfile'};
if (!$bf) {
$miniserv->{'pidfile'} =~ /^(.*)\/[^\/]+$/;
$bf = "$1/blocked";
}
my @rv;
my $fh = "BLOCKED";
&open_readfile($fh, $bf) || return ();
while(<$fh>) {
s/\r|\n//g;
my ($type, $who, $fails, $when) = split(/\s+/, $_);
push(@rv, { 'type' => $type,
$type => $who,
'fails' => $fails,
'when' => $when });
}
close($fh);
return @rv;
}
=head2 show_ssl_key_form([defhost], [defemail], [deforg])
Returns HTML for inputs to generate a new self-signed cert.
=cut
sub show_ssl_key_form
{
my ($defhost, $defemail, $deforg) = @_;
my $rv;
$rv .= &ui_table_row($text{'ssl_cn'},
&ui_opt_textbox("commonName", $defhost, 50,
$text{'ssl_all'}));
$rv .= &ui_table_row($text{'ca_email'},
&ui_textbox("emailAddress", $defemail, 30));
$rv .= &ui_table_row($text{'ca_ou'},
&ui_textbox("organizationalUnitName", undef, 30));
$rv .= &ui_table_row($text{'ca_o'},
&ui_textbox("organizationName", $deforg, 30));
$rv .= &ui_table_row($text{'ca_city'},
&ui_textbox("cityName", undef, 30));
$rv .= &ui_table_row($text{'ca_sp'},
&ui_textbox("stateOrProvinceName", undef, 15));
$rv .= &ui_table_row($text{'ca_c'},
&ui_textbox("countryName", undef, 2));
$rv .= &ui_table_row($text{'ssl_size'},
&ui_opt_textbox("size", undef, 6,
"$text{'default'} ($default_key_size)").
" ".$text{'ssl_bits'});
$rv .= &ui_table_row($text{'ssl_days'},
&ui_textbox("days", 1825, 8));
return $rv;
}
=head2 parse_ssl_key_form(&in, keyfile, [certfile])
Parses the key generation form, and creates new key and cert files.
Returns undef on success or an error message on failure.
=cut
sub parse_ssl_key_form
{
my ($in, $keyfile, $certfile) = @_;
my %in = %$in;
# Validate inputs
my @cns;
if ($in{'commonName_def'}) {
@cns = ( &get_system_hostname(0),
&get_system_hostname(1),
"localhost" );
}
else {
@cns = split(/\s+/, $in{'commonName'});
@cns || return $text{'newkey_ecns'};
foreach my $cn (@cns) {
$cn =~ /^[A-Za-z0-9\.\-\*]+$/ || return $text{'newkey_ecn'};
}
}
@cns = &unique(@cns);
$in{'size_def'} || $in{'size'} =~ /^\d+$/ || return $text{'newkey_esize'};
$in{'days'} =~ /^\d+$/ || return $text{'newkey_edays'};
$in{'countryName'} =~ /^\S\S$/ || return $text{'newkey_ecountry'};
# Work out SSL command
my %aclconfig = &foreign_config('acl');
&foreign_require("acl", "acl-lib.pl");
my $cmd = &acl::get_ssleay();
if (!$cmd) {
return &text('newkey_ecmd', "$aclconfig{'ssleay'}",
"@{[&get_webprefix()]}/config.cgi?acl");
}
# Run openssl and feed it key data
my $ctemp = &transname();
my $ktemp = &transname();
my $size = $in{'size_def'} ? $default_key_size : quotemeta($in{'size'});
my $subject = &build_ssl_subject($in{'countryName'},
$in{'stateOrProvinceName'},
$in{'cityName'},
$in{'organizationName'},
$in{'organizationalUnitName'},
\@cns,
$in{'emailAddress'});
my $conf = &build_ssl_config(\@cns);
my $out = &backquote_logged(
"$cmd req -newkey rsa:$size -x509 -sha256 -nodes -out ".quotemeta($ctemp)." -keyout ".quotemeta($ktemp)." ".
"-days ".quotemeta($in{'days'})." -subj ".quotemeta($subject)." ".
"-config ".quotemeta($conf)." -reqexts v3_req -utf8 2>&1");
if (!-r $ctemp || !-r $ktemp || $?) {
return $text{'newkey_essl'}."
"."".&html_escape($out)."
";
}
# Write to the final files
my $certout = &read_file_contents($ctemp);
my $keyout = &read_file_contents($ktemp);
unlink($ctemp, $ktemp);
my ($kfh, $cfh) = ("KEY", "CERT");
&open_lock_tempfile($kfh, ">$keyfile");
&print_tempfile($kfh, $keyout);
if ($certfile) {
# Separate files
&open_lock_tempfile($cfh, ">$certfile");
&print_tempfile($cfh, $certout);
&close_tempfile($cfh);
&set_ownership_permissions(undef, undef, 0600, $certfile);
}
else {
# Both go in the same file
&print_tempfile($kfh, $certout);
}
&close_tempfile($kfh);
&set_ownership_permissions(undef, undef, 0600, $keyfile);
return undef;
}
=head2 parse_ssl_csr_form(&in, keyfile, csrfile)
Parses the CSR generation form, and creates new key and CSR files.
Returns undef on success or an error message on failure.
=cut
sub parse_ssl_csr_form
{
my ($in, $keyfile, $csrfile) = @_;
my %in = %$in;
# Validate inputs
my @cns;
if (!$in{'commonName_def'}) {
@cns = split(/\s+/, $in{'commonName'});
@cns || return $text{'newkey_ecns'};
foreach my $cn (@cns) {
$cn =~ /^[A-Za-z0-9\.\-\*]+$/ || return $text{'newkey_ecn'};
}
}
else {
@cns = ( "*" );
}
$in{'size_def'} || $in{'size'} =~ /^\d+$/ || return $text{'newkey_esize'};
$in{'days'} =~ /^\d+$/ || return $text{'newkey_edays'};
$in{'countryName'} =~ /^\S\S$/ || return $text{'newkey_ecountry'};
# Work out SSL command
my %aclconfig = &foreign_config('acl');
&foreign_require("acl");
my $cmd = &acl::get_ssleay();
if (!$cmd) {
return &text('newkey_ecmd', "$aclconfig{'ssleay'}",
"@{[&get_webprefix()]}/config.cgi?acl");
}
# Generate the key
my $ktemp = &transname();
my $size = $in{'size_def'} ? $default_key_size : quotemeta($in{'size'});
my $out = &backquote_command("$cmd genrsa -out ".quotemeta($ktemp)." $size 2>&1 "."".&html_escape($out)."
";
}
# Run openssl and feed it key data
my ($ok, $ctemp) = &generate_ssl_csr(
$ktemp,
$in{'countryName'},
$in{'stateOrProvinceName'},
$in{'cityName'},
$in{'organizationName'},
$in{'organizationalUnitName'},
\@cns,
$in{'emailAddress'});
if (!$ok) {
return $text{'newkey_essl'}."
".
"".&html_escape($ctemp)."
";
}
# Write to the final files
my $csrout = &read_file_contents($ctemp);
my $keyout = &read_file_contents($ktemp);
unlink($ctemp, $ktemp);
my ($kfh, $cfh);
&open_lock_tempfile($kfh, ">$keyfile");
&print_tempfile($kfh, $keyout);
&close_tempfile($kfh);
&set_ownership_permissions(undef, undef, 0600, $keyfile);
&open_lock_tempfile($cfh, ">$csrfile");
&print_tempfile($cfh, $csrout);
&close_tempfile($cfh);
&set_ownership_permissions(undef, undef, 0600, $csrfile);
return undef;
}
# build_ssl_subject(country, state, city, org, orgunit, cname|&cnames, email)
# Generate a full subject line suitable for use with the -subj parameter
sub build_ssl_subject
{
my ($country, $state, $city, $org, $orgunit, $cn, $email) = @_;
$org =~ s/[\177-\377]//g if ($org); # Remove non-ascii chars
$orgunit =~ s/[\177-\377]//g if ($orgunit);
my @cns = ref($cn) ? @$cn : ( $cn );
my $subject;
$city = substr($city, 0, 64) if ($city && length($city) > 64);
$org = substr($org, 0, 64) if ($org && length($org) > 64);
$orgunit = substr($orgunit, 0, 64) if ($orgunit && length($orgunit) > 64);
$email = substr($email, 0, 64) if ($email && length($email) > 64);
$subject .= "/C=$country" if ($country);
$subject .= "/ST=$state" if ($state);
$subject .= "/L=$city" if ($city);
$subject .= "/O=$org" if ($org);
$subject .= "/OU=$orgunit" if ($orgunit);
$subject .= "/CN=$cns[0]";
$subject .= "/emailAddress=$email" if ($email);
return $subject;
}
# build_ssl_config(cname|&cnames)
# Create a temporary openssl config file that is setup to include altnames, if needed
sub build_ssl_config
{
my ($cn) = @_;
my @cns = ref($cn) ? @$cn : ( $cn );
my $conf = &find_openssl_config_file();
$conf || &error("No OpenSSL configuration file found on this system!");
my $temp = &transname();
©_source_dest($conf, $temp);
# Make sure subjectAltNames is set in .cnf file, in the right places
my $lref = &read_file_lines($temp);
my $i = 0;
my $found_req = 0;
my $found_ca = 0;
my $found_alt = 0;
my $altline = "subjectAltName=".join(",", map { "DNS:$_" } @cns);
foreach my $l (@$lref) {
if ($l =~ /^\s*subjectAltName\s*=/) {
$lref->[$i] = $altline;
$found_alt++;
}
$i++;
}
if (!$found_alt) {
$i = 0;
foreach my $l (@$lref) {
if ($l =~ /^\s*\[\s*v3_req\s*\]/ && !$found_req) {
splice(@$lref, $i+1, 0, $altline);
$found_req = 1;
}
if ($l =~ /^\s*\[\s*v3_ca\s*\]/ && !$found_ca) {
splice(@$lref, $i+1, 0, $altline);
$found_ca = 1;
}
$i++;
}
# If v3_req or v3_ca sections are missing, add at end
if (!$found_req) {
push(@$lref, "[ v3_req ]", $altline);
}
if (!$found_ca) {
push(@$lref, "[ v3_ca ]", $altline);
}
}
# Add copyall line if needed
$i = 0;
my $found_copy = 0;
my $copyline = "copy_extensions=copyall";
foreach my $l (@$lref) {
if ($l =~ /^\s*\#*\s*copy_extensions\s*=/) {
$l = $copyline;
$found_copy = 1;
last;
}
elsif ($l =~ /^\s*\[\s*CA_default\s*\]/) {
$found_ca = $i;
}
$i++;
}
if (!$found_copy) {
if ($found_ca) {
splice(@$lref, $found_ca+1, 0, $copyline);
}
else {
push(@$lref, "[ CA_default ]", $copyline);
}
}
&flush_file_lines($temp);
return $temp;
}
# generate_ssl_csr(keyfile, country, state, city, org, orgunit, cname|&cnames,
# email, ["sha1"|"sha2"])
# Generates a new CSR, and returns either 1 and the temp file path, or 0 and
# an error message
sub generate_ssl_csr
{
my ($ktemp, $country, $state, $city, $org, $orgunit, $cn, $email, $ctype) = @_;
$ctype ||= "sha2";
&foreign_require("acl");
my $ctemp = &transname();
my $cmd = &acl::get_ssleay();
my $subject = &build_ssl_subject($country, $state, $city, $org, $orgunit, $cn,$email);
my $conf = &build_ssl_config($cn);
my $ctypeflag = $ctype eq "sha2" ? "-sha256" : "";
my $out = &backquote_command(
"$cmd req -new -key ".quotemeta($ktemp)." -out $ctemp $ctypeflag ".
"-subj ".quotemeta($subject)." -config $conf -reqexts v3_req ".
"-utf8 2>&1");
if (!-r $ctemp || $?) {
return (0, $out);
}
else {
return (1, $ctemp);
}
}
=head2 build_installed_modules(force-all, force-mod)
Calls each module's install_check function, and updates the cache of
modules whose underlying servers are installed.
=cut
sub build_installed_modules
{
my ($force, $mod) = @_;
my %installed;
my $changed;
&read_file_cached("$config_directory/installed.cache", \%installed);
my @changed;
foreach my $minfo (&get_all_module_infos()) {
next if ($mod && $minfo->{'dir'} ne $mod);
next if (defined($installed{$minfo->{'dir'}}) && !$force && !$mod);
next if (!&check_os_support($minfo));
$@ = undef;
my $o = $installed{$minfo->{'dir'}} || 0;
my $pid = fork();
if (!$pid) {
# Check in a sub-process
my $rv;
eval {
local $main::error_must_die = 1;
$rv = &foreign_installed($minfo->{'dir'}, 0) ? 1 : 0;
};
if ($@) {
# Install check failed .. but assume the module is OK
$rv = 1;
}
exit($rv);
}
waitpid($pid, 0);
$installed{$minfo->{'dir'}} = $? / 256;
push(@changed, $minfo->{'dir'}) if ($installed{$minfo->{'dir'}} &&
$installed{$minfo->{'dir'}} ne $o);
}
&write_file("$config_directory/installed.cache", \%installed);
return wantarray ? (\%installed, \@changed) : \%installed;
}
=head2 get_latest_webmin_version
Returns 1 and the latest version of Webmin available on www.webmin.com, or
0 and an error message
=cut
sub get_latest_webmin_version
{
my $file = &transname();
my ($error, $version, $release);
&http_download($primary_host, $primary_port, '/', $file, \$error, undef, 0,
undef, undef, 5);
return (0, $error) if ($error);
open(FILE, "<".$file);
while() {
if (/webmin-([0-9\.]+)-(\d+)\.tar\.gz/ ||
/webmin-([0-9\.]+)\.tar\.gz/) {
$version = $1;
$release = $2;
last;
}
}
close(FILE);
unlink($file);
return $version ? (1, $version, $release)
: (0, "No version number found at $primary_host");
}
=head2 filter_updates(&updates, [version], [include-third], [include-missing])
Given a list of updates, filters them to include only those that are
suitable for this system. The parameters are :
=item updates - Array ref of updates, as returned by fetch_updates.
=item version - Webmin version number to use in comparisons.
=item include-third - Set to 1 to include non-core modules in the results.
=item include-missing - Set to 1 to include modules not currently installed.
=cut
sub filter_updates
{
my ($allupdates, $version, $third, $missing) = @_;
$version ||= &get_webmin_version();
my $bversion = &base_version($version);
my $updatestemp = &transname();
my @updates;
foreach my $u (@$allupdates) {
my %minfo = &get_module_info($u->[0]);
my %tinfo = &get_theme_info($u->[0]);
my %info = %minfo ? %minfo : %tinfo;
# Skip if wrong version of Webmin, unless this is non-core module and
# we are handling them too
my $nver = $u->[1];
$nver =~ s/^(\d+\.\d+)\..*$/$1/;
next if (($nver >= $bversion + .01 ||
$nver <= $bversion ||
$nver <= $version) &&
(!%info || $info{'longdesc'} || !$third));
# Skip if not installed, unless installing new
next if (!%info && !$missing);
# Skip if module has a version, and we already have it
next if (%info && $info{'version'} &&
&compare_version_numbers($info{'version'}, $nver) >= 0);
# Skip if not supported on this OS
my $osinfo = { 'os_support' => $u->[3] };
next if (!&check_os_support($osinfo));
# Skip if installed from RPM or Deb and update was not
my $itype = &get_module_install_type($u->[0]);
next if ($itype && $u->[2] !~ /\.$itype$/i);
push(@updates, $u);
}
return \@updates;
}
# get_clone_source(dir)
# Given a module dir, returns the dir of its original
sub get_clone_source
{
my ($dir) = @_;
my $lnk = readlink(&module_root_directory($dir));
return undef if (!$lnk);
if ($lnk =~ /\/([^\/]+)$/) {
return $1;
}
elsif ($lnk =~ /^[^\/ ]+$/) {
return $lnk;
}
return undef;
}
# retry_http_download(host, port, etc..)
# Calls http_download until it succeeds
sub retry_http_download
{
my ($host, $port, $page, $dest, $error, $cbfunc, $ssl, $user, $pass,
$timeout, $osdn, $nocache, $headers) = @_;
my $tries = 5;
my $i = 0;
my $tryerror;
while($i < $tries) {
$tryerror = undef;
&http_download($host, $port, $page, $dest, \$tryerror, $cbfunc, $ssl, $user,
$pass, $timeout, $osdn, $nocache, $headers);
if (!$tryerror) {
last;
}
$i++;
sleep($i);
}
if ($tryerror) {
# Failed every time
if (ref($error)) {
$$error = $tryerror;
}
else {
&error($tryerror);
}
}
}
# canonicalize_ip6(address)
# Converts an address to its full long form. Ie. 2001:db8:0:f101::20 to
# 2001:0db8:0000:f101:0000:0000:0000:0020
sub canonicalize_ip6
{
my ($addr) = @_;
return $addr if (!&check_ip6address($addr));
my @w = split(/:/, $addr);
my $idx = &indexof("", @w);
if ($idx >= 0) {
# Expand ::
my $mis = 8 - scalar(@w);
my @nw = @w[0..$idx];
for(my $i=0; $i<$mis; $i++) {
push(@nw, 0);
}
push(@nw, @w[$idx+1 .. $#w]);
@w = @nw;
}
foreach my $w (@w) {
while(length($w) < 4) {
$w = "0".$w;
}
}
return lc(join(":", @w));
}
# list_visible_themes([current-theme])
# Lists all themes the user should be able to use, possibly including their
# current theme if one is set.
sub list_visible_themes
{
my ($curr) = @_;
my @rv;
my %done;
foreach my $theme (&list_themes()) {
my $iscurr = $curr && $theme->{'dir'} eq $curr;
my $lnk = readlink($root_directory."/".$theme->{'dir'});
next if ($lnk && $lnk !~ /^\// && $lnk !~ /^\.\.\// && !$iscurr);
next if ($done{$theme->{'desc'}}++ && !$iscurr);
push(@rv, $theme);
}
return @rv;
}
# apply_new_os_version(&info)
# Update the Webmin and Usermin detected OS name and version
sub apply_new_os_version
{
my %osinfo = %{$_[0]};
# Do Webmin
&lock_file("$config_directory/config");
$gconfig{'real_os_type'} = $osinfo{'real_os_type'};
$gconfig{'real_os_version'} = $osinfo{'real_os_version'};
$gconfig{'os_type'} = $osinfo{'os_type'};
$gconfig{'os_version'} = $osinfo{'os_version'};
foreach my $key ('os_eol_db', 'os_eol_expired',
'os_eol_expiring') {
delete($gconfig{$key});
}
&write_file("$config_directory/config", \%gconfig);
&unlock_file("$config_directory/config");
# Do Usermin too, if installed and running an equivalent version
if (&foreign_installed("usermin")) {
&foreign_require("usermin");
my %miniserv;
&usermin::get_usermin_miniserv_config(\%miniserv);
my %uconfig;
&lock_file($usermin::usermin_config);
&usermin::get_usermin_config(\%uconfig);
$uconfig{'real_os_type'} = $osinfo{'real_os_type'};
$uconfig{'real_os_version'} = $osinfo{'real_os_version'};
$uconfig{'os_type'} = $osinfo{'os_type'};
$uconfig{'os_version'} = $osinfo{'os_version'};
&usermin::put_usermin_config(\%uconfig);
&unlock_file($usermin::usermin_config);
}
}
sub find_letsencrypt_cron_job
{
if (&foreign_check("webmincron")) {
&foreign_require("webmincron");
return &webmincron::find_webmin_cron($module_name,
'renew_letsencrypt_cert');
}
return undef;
}
# renew_letsencrypt_cert()
# Called by cron to renew the last requested cert
sub renew_letsencrypt_cert
{
my @doms = split(/\s+/, $config{'letsencrypt_doms'});
my $webroot = $config{'letsencrypt_webroot'};
my $mode = $config{'letsencrypt_mode'} || "web";
my $size = $config{'letsencrypt_size'};
my $usewebmin = !$config{'letsencrypt_nouse'};
if (!@doms) {
print "No domains saved to renew cert for!\n";
return;
}
if (!$webroot) {
print "No webroot saved to renew cert for!\n";
return;
}
elsif (!-d $webroot) {
print "Webroot $webroot does not exist!\n";
return;
}
my ($ok, $cert, $key, $chain) = &request_letsencrypt_cert(\@doms, $webroot,
undef, $size, $mode);
if (!$ok) {
print "Failed to renew certificate : $cert\n";
return;
}
# If we don't want to update the Webmin SSL certificate, then just return
return if (!$usewebmin);
# Copy into place
my %miniserv;
&lock_file($ENV{'MINISERV_CONFIG'});
&get_miniserv_config(\%miniserv);
&lock_file($miniserv{'keyfile'});
©_source_dest($key, $miniserv{'keyfile'}, 1);
&unlock_file($miniserv{'keyfile'});
&lock_file($miniserv{'certfile'});
©_source_dest($cert, $miniserv{'certfile'}, 1);
&unlock_file($miniserv{'certfile'});
if ($chain) {
&lock_file($miniserv{'extracas'});
©_source_dest($chain, $miniserv{'extracas'}, 1);
&unlock_file($miniserv{'extracas'});
}
else {
delete($miniserv{'extracas'});
}
&put_miniserv_config(\%miniserv);
&unlock_file($ENV{'MINISERV_CONFIG'});
&restart_miniserv(1);
}
# find_openssl_config_file()
# Returns the full path to the OpenSSL config file, or undef if not found
sub find_openssl_config_file
{
my %vconfig = &foreign_config("virtual-server");
foreach my $p ($vconfig{'openssl_cnf'}, # Virtualmin module config
"/etc/ssl/openssl.cnf", # Debian and FreeBSD
"/etc/openssl.cnf",
"/usr/local/etc/openssl.cnf",
"/etc/pki/tls/openssl.cnf", # Redhat
"/opt/csw/ssl/openssl.cnf", # Solaris CSW
"/opt/csw/etc/ssl/openssl.cnf", # Solaris CSW
"/System/Library/OpenSSL/openssl.cnf", # OSX
) {
return $p if ($p && -r $p);
}
return undef;
}
# show_os_release_notes()
# Returns a link with `Release notes` after OS
# upgrade, within alert displayed on the Dashboard
sub show_os_release_notes
{
my ($ver) = @_;
return if (!$ver);
my $basever = $ver;
($basever) = $basever =~ /(\d+)/;
return if (!$basever);
my $link;
my $os = $gconfig{'real_os_type'};
return if (!$os);
my $link_tag = 'target="_blank" data-link-external="after"';
# Ubuntu release notes
if ($os =~ /ubuntu/i &&
$ver =~ /\d+\.04/ &&
$basever >= 18) {
my $ubuntuver = $ver;
$ubuntuver =~ s/\./-/g;
$link = &ui_link("https://fridge.ubuntu.com/".
"ubuntu-$ubuntuver-lts-released",
$text{'os_release_notes'}, undef, $link_tag);
}
# AlmaLinux release notes
if ($os =~ /alma/i && $basever >= 8) {
$link = &ui_link("https://wiki.almalinux.org/release-notes/$ver.html",
$text{'os_release_notes'}, undef, $link_tag);
}
# Rocky linux release notes
if ($os =~ /rocky/i && $basever >= 8) {
$ver =~ s/\./_/;
$link = &ui_link("https://docs.rockylinux.org/release_notes/$ver",
$text{'os_release_notes'}, undef, $link_tag);
}
return ". $link" if ($link);
}
# get_webmin_repo_version()
# If Webmin was installed from a repository like APT or YUM, return the repo
# type and the new version number
sub get_webmin_repo_version
{
return () if (!&foreign_check("package-updates"));
&foreign_require("package-updates");
return () if (!&package_updates::supports_updates_available());
my @updates = &package_updates::updates_available();
my ($wpkg) = grep { $_->{'name'} eq 'webmin' } @updates;
return () if (!$wpkg);
return ($wpkg->{'system'}, $wpkg->{'version'});
}
# list_active_locks()
# Returns an array of structs containing details of currently open locks
sub list_active_locks
{
my @rv;
my $lockdir = $var_directory."/locks";
&foreign_require("proc");
my %pidmap = map { $_->{'pid'}, $_ } &proc::list_processes();
opendir(DIR, $lockdir);
foreach my $pid (readdir(DIR)) {
next if ($pid eq "." || $pid eq "..");
my $proc = { 'pid' => $pid,
'proc' => $pidmap{$pid},
'locks' => [ ] };
opendir(SUBDIR, "$lockdir/$pid");
foreach my $l (readdir(SUBDIR)) {
next if ($l eq "." || $l eq "..");
my ($t, $n) = split(/\-/, $l);
my $f = readlink("$lockdir/$pid/$l");
$f =~ s/\.lock$//;
if (&test_lock($f) == $pid) {
push(@{$proc->{'locks'}}, { 'time' => $t,
'num' => $n,
'lock' => $f });
}
}
closedir(SUBDIR);
if (@{$proc->{'locks'}}) {
push(@rv, $proc);
}
}
closedir(DIR);
return @rv;
}
1;