Compare commits

..

134 Commits

Author SHA1 Message Date
Ilia Ross
7507433bf1 Fix to drop dependency from Term::ANSIColor 2024-06-30 11:41:41 +03:00
Ilia Ross
248cb719c0 Fix restart command depend on config dir 2024-06-29 23:32:15 +03:00
Ilia Ross
94b7fdf0ec Fix to escape params 2024-06-29 23:29:55 +03:00
Ilia Ross
d89f6411b6 Fix to use has_command API 2024-06-29 20:34:29 +03:00
Ilia Ross
01d08a3605 Fix to make patch API work directly from URL 2024-06-28 19:48:51 +03:00
Ilia Ross
3d9497ff45 Fix command example 2024-06-28 01:51:02 +03:00
Ilia Ross
a6832450d1 Fix to restart Webmin when done 2024-06-28 01:38:28 +03:00
Ilia Ross
e41037388c Apply a patch to Webmin core or its modules from GitHub or a local file 2024-06-28 01:35:10 +03:00
Ilia Ross
a7b3af534b Fix to drop Threads module from recommended packages 2024-06-26 18:12:47 +03:00
Ilia Ross
87e006ceeb Add new API to verify session id 2024-06-26 14:39:23 +03:00
Ilia Ross
fbee8f0588 Add logging for active FTP conns 2024-06-20 20:04:19 +03:00
Jamie Cameron
e077b4da94 Start of work on support for requesting only a subset of hostnames 2024-06-19 22:05:22 -07:00
Jamie Cameron
38efad8265 Escape some inputs 2024-06-19 20:37:25 -07:00
Jamie Cameron
4d2a1fa084 Add spam folder name 2024-06-19 17:19:51 -07:00
Jamie Cameron
13b2eca3b1 Follow webmin code standards 2024-06-19 16:48:41 -07:00
Ilia Ross
aa4c3b1de6 Fix embedded styles margin 2024-06-19 16:55:25 +03:00
Ilia Ross
edaab4fd6f Fix embedded styles 2024-06-19 16:52:04 +03:00
Jamie Cameron
6fded0862c Merge pull request #2201 from webmin/dev/fix-last-command-match-freebsd
Fix listing last logins in FreeBSD
2024-06-18 16:16:37 -07:00
Ilia Ross
4006b0454e Fix to escape and not to truncate username 2024-06-18 23:51:19 +03:00
Ilia Ross
5d4ab58baa Fix the regex for last command on FreeBSD 2024-06-18 23:50:12 +03:00
Jamie Cameron
e6c7a60fe6 Merge pull request #2200 from webmin/dev/fix-last-command-regexes
Fix the regex to correctly match the output of the `last` command on contemporary systems
2024-06-18 13:38:21 -07:00
Ilia Ross
a75781d61a Fix the regex to correctly match the output of the last command on contemporary systems 2024-06-18 23:36:31 +03:00
Jamie Cameron
45348f5b02 Merge pull request #2199 from webmin/dev/last-dont-truncate-username
Fix not to truncate usernames
2024-06-18 11:33:01 -07:00
Ilia Ross
548c078813 Fix not to truncate usernames 2024-06-18 20:23:22 +03:00
Ilia Ross
76ccb2b7ed Fix support for Chrony in Debian systems 2024-06-17 15:09:42 +03:00
Ilia Ross
61d2081371 Add to support more passible options to systemd service file 2024-06-16 01:55:03 +03:00
Ilia Ross
458916b4d8 Fix to simplify make_date calls 2024-06-16 01:48:14 +03:00
Jamie Cameron
37451ad905 Merge pull request #2197 from vsc55/fix-2196
Fix metod detect Linux Gentoo.
2024-06-15 09:50:06 -07:00
Javier Pastor
9eec58a1f1 Fix metod detect Linux Gentoo. 2024-06-15 18:41:54 +02:00
Ilia Ross
178f527afa Fix to return an actual value 2024-06-15 16:50:04 +03:00
Jamie Cameron
cd1555b3e1 Merge branch 'master' of github.com:webmin/webmin 2024-06-12 16:53:02 -07:00
Jamie Cameron
2d7cfca67f Prevent duplicate also-notify and allow-transfer IPs 2024-06-12 16:52:50 -07:00
Jamie Cameron
e92a35b3ed Merge pull request #2195 from webmin/dev/xterm-acls-allow-not-enforce-sudo
Fix to allow disabling the enforcement of sudo-capable logins
2024-06-12 15:47:08 -07:00
Ilia Ross
e35efd0f00 Fix to always show the field 2024-06-13 01:36:41 +03:00
Ilia Ross
a12f385a5b Fix to consider user with the same name 2024-06-13 01:07:21 +03:00
Ilia Ross
5dc7cfafd7 Fix to allow disabling the enforcement of sudo-capable logins 2024-06-12 20:14:06 +03:00
Jamie Cameron
35aee74311 Merge pull request #2194 from webmin/dev/xterm-start-with-sudocapable
Fix to start with a sudo-capable user if possible
2024-06-11 18:19:05 -07:00
Ilia Ross
d04dfdf157 Fix to simplify the test 2024-06-12 01:20:29 +03:00
Ilia Ross
41f1adf0df Fix to start with a sudo-capable user if possible 2024-06-11 15:31:00 +03:00
Ilia Ross
1ae01bed8d Fix to support theme styling for embedded calendar 2024-06-10 16:34:29 +03:00
Ilia Ross
4a6f5d9a6a Fix not to load images by default and ask 2024-06-10 14:23:18 +03:00
Ilia Ross
bb3a4bc1e8 Fix font size consistency 2024-06-10 13:48:14 +03:00
Jamie Cameron
084c117547 Merge pull request #2193 from webmin/dev/embed-calendar
Add support for embedding calendar events inline in email messages
2024-06-09 18:48:18 -07:00
Ilia Ross
0221a092b9 Drop duplicate code
https://github.com/webmin/webmin/pull/2193#discussion_r1632362334
2024-06-09 21:16:52 +03:00
Ilia Ross
535d4173b3 Fix to factor out code to separate functions to be available in Usermin 2024-06-09 19:59:50 +03:00
Ilia Ross
20d481e96b Add further margin fixes 2024-06-09 19:13:52 +03:00
Ilia Ross
0256ee47f2 Fix block margin for perfect alignment 2024-06-09 16:16:52 +03:00
Ilia Ross
6af5ed6e2f Fix margin for inner content 2024-06-09 16:09:44 +03:00
Ilia Ross
abeff44b1a Add further improvements to TZs 2024-06-09 04:39:53 +03:00
Ilia Ross
35298efd8a Fix timezones 2024-06-09 04:14:32 +03:00
Ilia Ross
3fa687e716 Fix bugs 2024-06-09 03:49:43 +03:00
Ilia Ross
43fc057484 Add further indent improvements 2024-06-09 03:24:23 +03:00
Ilia Ross
70e9a1c00b Fix indentation 2024-06-09 03:14:50 +03:00
Ilia Ross
d21188c2ad Add improvements to mail frame interactions 2024-06-09 03:06:53 +03:00
Ilia Ross
a780103e2f Fix to improve calendar styles 2024-06-09 02:45:51 +03:00
Ilia Ross
4014293760 Fix to resize embedding iframe for content to fit on view details 2024-06-09 01:55:56 +03:00
Ilia Ross
ba3be31335 Fix to resize iframe if called by some elements from inside 2024-06-09 01:53:36 +03:00
Ilia Ross
adb3d0bb67 Revert "Fix to expose mail iframe resize function"
This reverts commit cd9bc8b7c4.
2024-06-09 01:42:12 +03:00
Ilia Ross
cd9bc8b7c4 Fix to expose mail iframe resize function 2024-06-09 01:16:21 +03:00
Ilia Ross
e1ebcf0506 Fix code to fit within an 80-character width 2024-06-09 01:06:27 +03:00
Ilia Ross
17a27dbe00 Fix to drop showing organizer time unless TZ is explicitly given 2024-06-09 00:48:36 +03:00
Ilia Ross
e36e943251 Fix to keep calendar cell always in right size 2024-06-09 00:31:39 +03:00
Ilia Ross
95ee1e2f2d Add support to embed iCalendar to email message 2024-06-08 23:52:59 +03:00
Ilia Ross
37cde80bbe Fix standard description to replace new lines to HTML break 2024-06-08 23:36:59 +03:00
Ilia Ross
45852664fe Add further fixes and improvements to the processor 2024-06-08 23:19:46 +03:00
Ilia Ross
00885b1f76 Fix location detection 2024-06-08 18:40:35 +03:00
Ilia Ross
cce8911f0f Add additional details to the object 2024-06-08 16:53:25 +03:00
Ilia Ross
3a151469c7 Add proper date parsing and storing extensive details about event 2024-06-08 16:47:08 +03:00
Ilia Ross
e3b94dc458 Fix summary match for strings like SUMMARY;LANGUAGE=fr-CA 2024-06-08 02:18:20 +03:00
Jamie Cameron
cde548ada3 Merge pull request #2192 from webmin/dev/websocket-url-proxy-rely
Set websocket URL depending on the proxy unless defined in config
2024-06-06 20:39:03 -07:00
Ilia Ross
00d0a89630 Set socket URL depending on the proxy unless defined in config 2024-06-07 01:15:27 +03:00
Ilia Ross
9c971202b3 Remove debug 2024-06-07 00:47:47 +03:00
Ilia Ross
2aa3bce4da Fix explicitly set module name from the caller 2024-06-07 00:35:41 +03:00
Ilia Ross
4103177d1e Fix dependencies [build] 2024-06-06 22:55:39 +03:00
Jamie Cameron
31b6cdc82b Merge branch 'master' of github.com:webmin/webmin 2024-06-06 08:34:03 -07:00
Jamie Cameron
fd8c634277 Zone key record might not exist https://forum.virtualmin.com/t/security-updates/127159 2024-06-06 08:33:34 -07:00
Ilia Ross
596ba13b1e Add logic to store iCalendars 2024-06-06 01:59:58 +03:00
Ilia Ross
5e684bf41b Add improvements to iCalendar parser 2024-06-05 03:34:33 +03:00
Jamie Cameron
356c8f7f53 Merge pull request #2191 from webmin/dev/websockets-funcs-are-global
Improve WebSockets API
2024-06-04 16:27:40 -07:00
Ilia Ross
185465351a Fix to use named loop variable 2024-06-05 00:07:44 +03:00
Ilia Ross
8d84e7313a Fix to call function properly 2024-06-04 23:54:13 +03:00
Ilia Ross
71e37adfed Add ability to clean all modules with websockets=1 on the .info 2024-06-04 19:55:25 +03:00
Ilia Ross
af912d9539 Add API to get WebSocket URL 2024-06-04 19:07:34 +03:00
Ilia Ross
5b31c7df84 Factor out WebSockets port and host options to global config 2024-06-04 18:43:34 +03:00
Ilia Ross
55b5939194 Move websocket functions to be always available 2024-06-04 15:48:06 +03:00
Jamie Cameron
00ddfd4d05 Also cleanup websockets 2024-06-03 18:42:31 -07:00
Jamie Cameron
2d23a3503e Fix spacing 2024-06-03 18:34:59 -07:00
Jamie Cameron
a838d11a26 No need for a loop to process a 1-element array 2024-06-03 18:29:56 -07:00
Jamie Cameron
5f28a28d8d Merge pull request #2189 from webmin/dev/icalendar-event-parser
Add support to parse calendar events files #2160
2024-06-03 16:05:49 -07:00
Ilia Ross
e13df24539 Fix to assign argument array before anything else 2024-06-04 00:54:55 +03:00
Ilia Ross
4f7924338d Add missing websockets-lib-funcs.pl file to the build #2190
[build]
2024-06-04 00:48:35 +03:00
Ilia Ross
3a1d609579 Add support to parse calendar events files 2024-06-03 20:58:38 +03:00
Jamie Cameron
e441427031 Merge pull request #2170 from webmin/dev/logviewer-custom-units
Add support for additional units in systemd log viewer
2024-06-01 09:57:19 -07:00
Ilia Ross
469857a41e Fix to use links as is
https://github.com/webmin/webmin/pull/2170#discussion_r1623004037
2024-06-01 18:37:28 +03:00
Ilia Ross
e47c82e7e8 Fix cron id format [build] 2024-06-01 15:32:43 +03:00
Ilia Ross
a0f6dd935c Fix to favour lexically scoped variable over global 2024-06-01 15:31:50 +03:00
Jamie Cameron
e302b706ec Add a default option for mynetworks_style https://github.com/webmin/webmin/issues/2174 2024-05-31 16:26:56 -07:00
Jamie Cameron
8c7fc88d51 Use more accurate wording https://github.com/webmin/webmin/issues/2174 2024-05-31 16:25:11 -07:00
Jamie Cameron
7b4d905eb6 Merge branch 'master' of github.com:webmin/webmin 2024-05-31 16:02:57 -07:00
Jamie Cameron
a1a6f669b2 Use a unique ID for webmin crons created in the same process at the same time https://forum.virtualmin.com/t/webmin-server-stauts/126983 2024-05-31 16:02:41 -07:00
Jamie Cameron
0298d884ef Merge pull request #2182 from webmin/dev/take-out-ws-lib-and-make-it-work-with-themes
Dev/take-out-ws-lib-and-make-it-work-with-themes
2024-05-31 13:40:31 -07:00
Ilia Ross
5a8b3467a1 Fix to consider themes using websockets too 2024-05-31 23:04:19 +03:00
Ilia Ross
17fb8304c3 Fix to take out WebSockets library 2024-05-31 22:58:27 +03:00
Jamie Cameron
5cd88dad43 Merge pull request #2181 from webmin/dev/fix-proftpd-mods-load
Fix how modules are loaded in ProFTPd
2024-05-31 12:41:50 -07:00
Ilia Ross
c15e7a5e5e Fix how modules are loaded in ProFTPd 2024-05-31 19:55:37 +03:00
Jamie Cameron
fad464be47 Merge pull request #2180 from webmin/dev/better-xterm-logging
Dev/better-xterm-logging
2024-05-31 08:57:39 -07:00
Ilia Ross
489db4c769 Fix to store logs in var directory 2024-05-31 16:54:36 +03:00
Ilia Ross
cc663af3df Fix to log username 2024-05-31 16:51:08 +03:00
Ilia Ross
0b58cd5197 Fix to print log nicely 2024-05-31 16:31:56 +03:00
Ilia Ross
dbd16c21cc Fix to drop extra new line [build] 2024-05-31 15:52:50 +03:00
Ilia Ross
8ddabb35b6 Fix test for ports below zero and put port number to error message 2024-05-31 15:50:34 +03:00
Ilia Ross
8476206da8 Merge pull request #2179 from webmin/dev/impove-status-module
Dev/impove-status-module
2024-05-31 13:37:18 +03:00
Ilia Ross
ca3362ee84 Fix to properly test fetched filtered content 2024-05-29 19:20:15 +03:00
Ilia Ross
e88ba87eae Add a message for progressive logs with no data 2024-05-29 17:38:28 +03:00
Ilia Ross
a420c7142f Fix to use hash for mapping since select names 2024-05-29 15:35:41 +03:00
Ilia Ross
6f37dc94bf Revert the change to hide logs from other modules yet
https://github.com/webmin/webmin/pull/2170#discussion_r1618145017
2024-05-29 11:30:53 +03:00
Ilia Ross
c59a200725 Fix functions name 2024-05-29 03:11:28 +03:00
Ilia Ross
e56aa7711c Add status handler function 2024-05-29 03:11:18 +03:00
Ilia Ross
b480b4caa3 Fix SPA themes have own control over onbeforeunload event 2024-05-29 01:37:43 +03:00
Ilia Ross
db456ad458 Add crucial calls abortion control 2024-05-29 01:11:53 +03:00
Ilia Ross
9513d85157 Fix to just always call it Filter 2024-05-28 22:59:45 +03:00
Ilia Ross
dccc3fb10e Fix to call check right away
https://github.com/webmin/webmin/pull/2170#discussion_r1616387257
2024-05-28 00:08:23 +03:00
Ilia Ross
bb7938a0f5 Add support for tailing logs in real time 2024-05-27 21:52:24 +03:00
Ilia Ross
8164480b48 Fix lines bug in journalctl 2024-05-27 18:08:57 +03:00
Ilia Ross
4155fdb4c5 Fix not to use bare words 2024-05-27 15:05:15 +03:00
Ilia Ross
19efd89c28 Fix bug when hiding controls 2024-05-27 14:53:01 +03:00
Ilia Ross
f911137624 Add module config and other buttons in case index page is bypassed 2024-05-27 14:40:06 +03:00
Ilia Ross
d4ac34e4b5 Fix to show right title when viewing journal 2024-05-27 14:39:23 +03:00
Ilia Ross
5323bda372 Fix to limit select width not to break the page on long systemd entries 2024-05-27 14:37:40 +03:00
Ilia Ross
1b1ac686e3 Fix to redirect straight to log view or show error 2024-05-27 13:57:27 +03:00
Ilia Ross
75e9323429 Rename the old logging system to "System Logs RS" to free up the name for actual systemd-journald 2024-05-27 13:47:10 +03:00
Ilia Ross
554b439bf8 Fix to drop redundant support for extra units 2024-05-27 00:56:42 +03:00
Ilia Ross
2f9a0b3f21 Add support for showing messages all units and filter by since
Fix numerous of other bugs:

  1. No `tac` for `journalctl` as there is a special `-r` flag
  2. No using tail for `journalctl`
2024-05-27 00:52:45 +03:00
Ilia Ross
cc2502737f Add support for additional units in systemd log viewer 2024-05-26 01:12:39 +03:00
57 changed files with 1796 additions and 412 deletions

File diff suppressed because one or more lines are too long

153
bin/patch Executable file
View File

@@ -0,0 +1,153 @@
#!/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 is not installed\n";
exit 1;
}
# Check if git is installed
if (!has_command('git')) {
print "git is not 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{https://(github|gitlab)\.com/[^/]+/([^/]+)/commit/[^/]+}) {
$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;
}
# Download command or cat patch file
my $cmd;
if ($patch =~ /^https?:\/\//) {
$cmd = "curl -s @{[quotemeta($patch)]}";
chdir "$path/$module";
}
else {
$cmd = "cat @{[quotemeta($patch)]}";
}
# Apply patch using Git
my $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 "Patch applied successfully to:\n";
print " $1\n" while $output =~ /^Applied patch\s+(\S+)/mg;
system("$config_dir/restart");
=pod
=head1 NAME
patch
=head1 DESCRIPTION
Apply a patch to Webmin core or its modules from GitHub or a local file.
=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>

View File

@@ -1988,6 +1988,8 @@ if (@transfer) {
}
}
if (@notify) {
my %done;
@notify = grep { !$done{$_->{'name'}}++ } @notify;
my $also = { 'name' => 'also-notify',
'type' => 1,
'members' => \@notify};
@@ -1996,6 +1998,8 @@ if (@notify) {
'values' => [ 'yes' ] });
}
if (@transfer) {
my %done;
@transfer = grep { !$done{$_->{'name'}}++ } @transfer;
my $allow = { 'name' => 'allow-transfer',
'type' => 1,
'members' => \@transfer };

View File

@@ -40,6 +40,7 @@ if (@keyrecs) {
my $kt = $key->{'ksk'} ? 'ksk' : 'zone';
my ($keyrec) = grep { $_->{'values'}->[0] ==
($key->{'ksk'} ? 257 : 256) } @keyrecs;
next if (!$keyrec);
my $keyline = format_dnssec_public_key(
join(" ", $keyrec->{'name'}, $keyrec->{'class'},
$keyrec->{'type'}, @{$keyrec->{'values'}}));

View File

@@ -21,7 +21,7 @@ if ($in{'source'} == 0) {
if (!$in{'local'})
{ &install_error($text{'download_elocal'}); }
if (!-r $in{'local'})
{ &install_error(&text('download_elocal2', $in{'local'})); }
{ &install_error(&text('download_elocal2', &html_escape($in{'local'}))); }
$source = $in{'local'};
@pfile = ( $in{'local'} );
$need_unlink = 0;
@@ -91,8 +91,9 @@ elsif ($in{'source'} == 3) {
$i = 0;
@fallback = ( );
foreach $yum (@cpanyum) {
print &text('download_yum', "<tt>$cpan[$i]</tt>",
"<tt>$yum->{'package'}</tt>"),"<br>\n";
print &text('download_yum',
"<tt>".&html_escape($cpan[$i])."</tt>",
"<tt>".&html_escape($yum->{'package'})."</tt>"),"<br>\n";
print "<ul>\n";
@got = &software::update_system_install(
$yum->{'package'});
@@ -154,7 +155,8 @@ elsif ($in{'source'} == 3) {
# Fail if any modules are missing from CPAN
for($i=0; $i<@cpan; $i++) {
push(@missing, "<tt>$cpan[$i]</tt>") if (!$source[$i]);
push(@missing, "<tt>".&html_escape($cpan[$i])."</tt>")
if (!$source[$i]);
}
if ($in{'missingok'}) {
@@ -167,11 +169,12 @@ elsif ($in{'source'} == 3) {
}
}
@cpan || &install_error(&text('download_ecpan',
join(" ", @missing)));
&html_escape(join(" ", @missing))));
}
elsif (@missing) {
# Fail due to missing modules
&install_error(&text('download_ecpan', join(" ", @missing)));
&install_error(&text('download_ecpan',
&html_escape(join(" ", @missing))));
}
$source = join("<br>", @source);
@@ -192,14 +195,16 @@ elsif ($in{'source'} == 3) {
&ftp_download($host, $file, $pfile, \$error,
\&progress_callback);
}
else { &install_error(&text('download_eurl', $m)); }
else {
&install_error(&text('download_eurl',&html_escape($m)));
}
&install_error($error) if ($error);
push(@pfile, $pfile);
}
$need_unlink = 1;
}
else {
&error("Unknown source mode $in{'source'}");
&error("Unknown source mode ".&html_escape($in{'source'}));
}
# Check if the file looks like a perl module
@@ -287,7 +292,7 @@ foreach $d (@dirs) {
close(MAKEFILE);
push(@allreqs, @prereqs);
}
system("rm -rf $mtemp");
&unlink_file($mtemp);
# Work out which pre-requesites are missing
@allreqs = &unique(@allreqs);

View File

@@ -1567,28 +1567,31 @@ if (!$gconfig{'tempdelete_days'}) {
print STDERR "Temp file clearing is disabled\n";
return;
}
# Cleanup files in /tmp/.webmin
if ($gconfig{'tempdir'} && !$gconfig{'tempdirdelete'}) {
print STDERR "Temp file clearing is not done for the custom directory $gconfig{'tempdir'}\n";
return;
}
local $tempdir = &transname();
$tempdir =~ s/\/([^\/]+)$//;
if (!$tempdir || $tempdir eq "/") {
$tempdir = "/tmp/.webmin";
}
local $cutoff = time() - $gconfig{'tempdelete_days'}*24*60*60;
opendir(DIR, $tempdir);
foreach my $f (readdir(DIR)) {
next if ($f eq "." || $f eq "..");
local @st = lstat("$tempdir/$f");
if ($st[9] < $cutoff) {
&unlink_file("$tempdir/$f");
else {
local $tempdir = &transname();
$tempdir =~ s/\/([^\/]+)$//;
if (!$tempdir || $tempdir eq "/") {
$tempdir = "/tmp/.webmin";
}
}
closedir(DIR);
local $cutoff = time() - $gconfig{'tempdelete_days'}*24*60*60;
opendir(DIR, $tempdir);
foreach my $f (readdir(DIR)) {
next if ($f eq "." || $f eq "..");
local @st = lstat("$tempdir/$f");
if ($st[9] < $cutoff) {
&unlink_file("$tempdir/$f");
}
}
closedir(DIR);
}
# Delete stale lock files
my $lockdir = $var_directory."/locks";
opendir(DIR, $lockdir);
foreach my $f (readdir(DIR)) {
@@ -1600,6 +1603,11 @@ foreach my $f (readdir(DIR)) {
}
}
closedir(DIR);
# Cleanup old websockets
foreach (&get_miniserv_websockets_modules()) {
&cleanup_miniserv_websockets(undef, $_);
}
}
=head2 list_cron_files()

View File

@@ -349,8 +349,8 @@ if ($fh6) {
}
while(1) {
$$port++;
if ($$port >= 65536) {
return "Failed to allocate a free port!";
if ($$port < 0 || $$port > 65535) {
return "Failed to allocate a free port number: $$port";
}
$pack = pack_sockaddr_in($$port, INADDR_ANY);
next if (!bind($fh, $pack));
@@ -366,4 +366,3 @@ if ($fh6) {
}
return undef;
}

View File

@@ -1,7 +1 @@
body blockquote:not([style*="border-left"]) {
border-left: 1px solid #ccc;
margin-left: 6px;
margin-top: 0;
margin-bottom: 0;
padding-left: 12px;
}
body blockquote:not([style*="border-left"]){border-left:1px solid #ccc;margin-left:6px;margin-top:0;margin-bottom:0;padding-left:12px;}pre{white-space:break-spaces;margin:0 0 4px 0}

View File

@@ -29,9 +29,6 @@ table.ui_table thead td {
table.sortable tbody td {
padding: 2px;
}
table.ui_table td * {
line-height:1.5em;
}
table.ui_table td textarea {line-height:normal; font-family:monospace;}
table.ui_table td div.barchart * {
margin: 0;
@@ -55,9 +52,16 @@ div.ui_form_end_buttons {
padding: 6px 3px;
}
.ui_form_end_buttons input {
padding: 2px;
padding: 1px;
}
table.ui_grid_table td { padding: 2px 4px; }
.ui_form_value td > b > input+label+tt,
.ui_form_value td > b > input+label {
font-weight: normal !important;
}
table.ui_grid_table td {
padding: 2px 4px;
}
select,
input {
padding: 1px;
}

View File

@@ -2371,6 +2371,8 @@ if (ref($opts)) {
&print_tempfile(CFILE, "ExecReload=$kill -HUP \$MAINPID\n") if ($opts->{'reload'} eq '0');
&print_tempfile(CFILE, "ExecStop=$opts->{'stop'}\n") if ($opts->{'stop'});
&print_tempfile(CFILE, "ExecReload=$opts->{'reload'}\n") if ($opts->{'reload'});
&print_tempfile(CFILE, "ExecStartPre=$opts->{'startpre'}\n") if ($opts->{'startpre'});
&print_tempfile(CFILE, "ExecStartPost=$opts->{'startpost'}\n") if ($opts->{'startpost'});
&print_tempfile(CFILE, "Type=$opts->{'type'}\n") if ($opts->{'type'});
&print_tempfile(CFILE, "Environment=\"$opts->{'env'}\"\n") if ($opts->{'env'});
&print_tempfile(CFILE, "User=$opts->{'user'}\n") if ($opts->{'user'});
@@ -2381,8 +2383,8 @@ if (ref($opts)) {
&print_tempfile(CFILE, "RestartSec=$opts->{'restartsec'}\n") if ($opts->{'restartsec'});
&print_tempfile(CFILE, "TimeoutSec=$opts->{'timeout'}\n") if ($opts->{'timeout'});
&print_tempfile(CFILE, "TimeoutStopSec=$opts->{'timeoutstopsec'}\n") if ($opts->{'timeoutstopsec'});
&print_tempfile(CFILE, "StandardOutput=file:$opts->{'logstd'}\n") if ($opts->{'logstd'});
&print_tempfile(CFILE, "StandardError=file:$opts->{'logerr'}\n") if ($opts->{'logerr'});
&print_tempfile(CFILE, "StandardOutput=".($opts->{'logstd'} =~ /^\// ? 'file:' : '')."$opts->{'logstd'}\n") if ($opts->{'logstd'});
&print_tempfile(CFILE, "StandardError=".($opts->{'logerr'} =~ /^\// ? 'file:' : '')."$opts->{'logerr'}\n") if ($opts->{'logerr'});
}
&print_tempfile(CFILE, "\n");

View File

@@ -1,3 +1,5 @@
skip_index=1
lines=100
others=1
reverse=1
log_any=0

View File

@@ -1,5 +1,7 @@
skip_index=Open log view on module load,1,1-Yes,0-No
lines=Default number of lines to display,0,6
refresh=Seconds between log view refreshes,3,Never
others=Show logs from other modules?,1,1-Yes,0-No
others=Show logs from other modules,1,1-Yes,0-No
extras=Extra log files to show,9,50,4,\t
reverse=Log display order,1,1-Newest lines at top,0-Newest lines at bottom
log_any=Can view any file as a log,1,1-Yes,0-No

View File

@@ -4,26 +4,24 @@
require './logviewer-lib.pl';
&ui_print_header($text{'index_subtitle'}, $text{'index_title'}, "", undef, 1, 1, 0,
&help_search_link("systemd-journal journalctl", "man", "doc"));
if (!&has_command('journalctl')) {
# Not installed
&ui_print_endpage(&text('index_econf', "<tt>$config{'syslog_conf'}</tt>", "../config.cgi?$module_name"));
}
# Display syslog rules
my @col0;
my @col1;
my @col2;
my @col3;
my @lnks;
if ($access{'syslog'}) {
my @systemctl_cmds = &get_systemctl_cmds();
foreach $o (@systemctl_cmds) {
local @cols;
push(@cols, &text('index_cmd', "<tt>".$o->{'cmd'}."</tt>"));
push(@cols, $o->{'desc'});
push(@cols, &ui_link("view_log.cgi?idx=$o->{'id'}&view=1", $text{'index_view'}) );
push(@col1, \@cols);
push(@cols, &text('index_cmd', "<tt>".
&cleanup_destination($o->{'cmd'})."</tt>"));
my $icon = $o->{'id'} =~ /journal-(a|x)/ ? "&#x25E6;&nbsp; " : "";
push(@cols, $icon.&cleanup_description($o->{'desc'}));
push(@cols, &ui_link("view_log.cgi?idx=$o->{'id'}&view=1",
$text{'index_view'}) );
push(@lnks, "view_log.cgi?idx=$o->{'id'}&view=1");
push(@col0, \@cols);
}
# System logs from other modules
@@ -47,6 +45,8 @@ if ($access{'syslog'}) {
map { &html_escape($_) } @{$c->{'sel'}}));
push(@cols, &ui_link("view_log.cgi?idx=syslog-".
$c->{'index'}."&"."view=1", $text{'index_view'}) );
push(@lnks, "view_log.cgi?idx=syslog-".
$c->{'index'}."&"."view=1");
push(@col1, \@cols);
push(@foreign_syslogs, $c->{'file'});
}
@@ -73,6 +73,9 @@ if ($access{'syslog'}) {
"view_log.cgi?idx=syslog-ng-".
$dest->{'index'}."&"."view=1",
$text{'index_view'}) );
push(@lnks, "view_log.cgi?idx=syslog-ng-".
$dest->{'index'}."&"."view=1");
@cols = sort { $a->[2] cmp $b->[2] } @cols;
push(@col1, \@cols);
}
}
@@ -95,9 +98,12 @@ if ($config{'others'} && $access{'others'}) {
push(@cols, &text('index_cmd',
"<tt>".&html_escape($o->{'cmd'})."</tt>"));
}
push(@cols, &html_escape($o->{'desc'}));
push(@cols, $o->{'desc'} ? &html_escape($o->{'desc'}) : "");
push(@cols, &ui_link("view_log.cgi?oidx=$o->{'mindex'}".
"&omod=$o->{'mod'}&view=1", $text{'index_view'}) );
push(@lnks, "view_log.cgi?oidx=$o->{'mindex'}".
"&omod=$o->{'mod'}&view=1");
@cols = sort { $a->[2] cmp $b->[2] } @cols;
push(@col2, \@cols);
}
}
@@ -114,29 +120,49 @@ foreach $e (&extra_log_files()) {
push(@cols, &text('index_cmd',
"<tt>".&html_escape($e->{'cmd'})."</tt>"));
}
push(@cols, &html_escape($e->{'desc'}));
push(@cols, $e->{'desc'} ? &html_escape($e->{'desc'}) : "");
push(@cols, &ui_link("view_log.cgi?extra=".&urlize($e->{'file'} || $e->{'cmd'})."&view=1", $text{'index_view'}) );
push(@lnks, "view_log.cgi?extra=".&urlize($e->{'file'} || $e->{'cmd'})."&view=1");
@cols = sort { $a->[2] cmp $b->[2] } @cols;
push(@col3, \@cols);
}
# Print sorted table with logs files and commands
my @acols = (@col1, @col2, @col3);
my @acols = (@col0, @col1, @col2, @col3);
my $print_header = sub {
# Print the header
&ui_print_header($text{'index_subtitle'}, $text{'index_title'}, "", undef, 1, 1, 0,
&help_search_link("systemd-journal journalctl", "man", "doc"));
};
# If no logs are available just show the message
if (!@acols) {
$print_header->();
&ui_print_endpage($text{'index_elogs'});
}
# If we jump directly to logs just redirect
if ($config{'skip_index'} == 1) {
if ($lnks[0]) {
&redirect($lnks[0]);
exit;
}
}
# Print the header
$print_header->();
print &ui_columns_start( @acols ? [
$text{'index_to'},
$text{'index_rule'}, "" ] : [ ], 100);
if (@acols) {
@acols = sort { $a->[2] cmp $b->[2] } @acols;
foreach my $col (@acols) {
print &ui_columns_row($col);
}
}
else {
print &ui_columns_row([$text{'index_elogs'}], [" colspan='3' style='text-align: center'"], 3);
foreach my $col (@acols) {
print &ui_columns_row($col);
}
print &ui_columns_end();
print "<p>\n";
if ($access{'any'}) {
if ($access{'any'} && $config{'log_any'} == 1) {
# Can view any log (under allowed dirs)
print &ui_form_start("view_log.cgi");
print &ui_hidden("view", 1),"\n";

View File

@@ -1,10 +1,10 @@
index_title=System Logs Viewer
index_elogs=No logs were found to display
index_to=Log destination
index_rule=Messages selected
index_title=System Logs
index_elogs=The <tt>journalctl</tt> command is not available on your system, and other logs are configured not to be displayed in the module configuration.
index_to=Log
index_rule=Description
index_file=File $1
index_cmd=Output from $1
index_return=system logs viewer
index_return=system logs
index_view=View..
index_viewfile=View log file:
index_viewok=View
@@ -16,16 +16,35 @@ journal_journalctl_debug_info=Debug and info messages
journal_journalctl_notice_warning=Notice and warning messages
journal_journalctl_err_crit=Error and critical messages
journal_journalctl_alert_emerg=Alert and emergency messages
journal_journalctl_unit=Messages for specific unit
journal_since0=Latest available
journal_since1=Real-time follow
journal_since2=Current boot
journal_since3=7 days ago
journal_since4=24 hours ago
journal_since5=8 hours ago
journal_since6=1 hour ago
journal_since7=30 minutes ago
journal_since8=10 minutes ago
journal_since9=3 minutes ago
journal_since10=1 minute ago
journal_sincefollow=in
journal_since=since
view_title=View Logfile
view_titlejournal=View Journal
view_header=Last $1 lines of $2
view_header2=Last $1 lines
view_header3=Lines of $1
view_empty=Log file is empty
view_refresh=Refresh
view_loading=Log file is being watched .. No new lines yet.
view_filter=Filter lines with text $1
view_filter_btn=Filter
save_efile='$1' is not a valid filename : $2
save_ecannot2=You are not allowed to view this log
save_ecannot3=Error: You are not allowed to view this log
save_ecannot4=Error: Could not open '$1'
save_ecannot6=You are not allowed to view arbitrary logs
save_ecannot7=You are not allowed to view this extra log
save_emissing=Missing log file to view

View File

@@ -27,33 +27,139 @@ foreach $f (@files) {
return 0;
}
#
# Returns standard
# get_journal_since
# Returns a list of journalctl since commands
sub get_journal_since
{
return [
{ "" => $text{'journal_since0'} },
{ "-f" => $text{'journal_since1'} },
{ "-b" => $text{'journal_since2'} },
{ "-S '7 days ago'" => $text{'journal_since3'} },
{ "-S '24 hours ago'" => $text{'journal_since4'} },
{ "-S '8 hours ago'" => $text{'journal_since5'} },
{ "-S '1 hour ago'" => $text{'journal_since6'} },
{ "-S '30 minutes ago'" => $text{'journal_since7'} },
{ "-S '10 minutes ago'" => $text{'journal_since8'} },
{ "-S '3 minutes ago'" => $text{'journal_since9'} },
{ "-S '1 minute ago'" => $text{'journal_since10'} },
];
}
# get_systemctl_cmds([force-select])
# Returns logs for journalctl
sub get_systemctl_cmds
{
my $lines = $config{'lines'} || 1000;
return !&has_command('journalctl') ? () : (
{ 'cmd' => "journalctl --lines $lines -p alert..emerg",
'desc' => $text{'journal_journalctl_alert_emerg'},
'id' => "journal-1", },
{ 'cmd' => "journalctl --lines $lines -p err..crit",
'desc' => $text{'journal_journalctl_err_crit'},
'id' => "journal-2", },
{ 'cmd' => "journalctl --lines $lines -p notice..warning",
'desc' => $text{'journal_journalctl_notice_warning'},
'id' => "journal-3", },
{ 'cmd' => "journalctl --lines $lines -p debug..info",
'desc' => $text{'journal_journalctl_debug_info'},
'id' => "journal-4", },
{ 'cmd' => "journalctl --lines $lines -k ",
'desc' => $text{'journal_journalctl_dmesg'},
'id' => "journal-5", },
{ 'cmd' => "journalctl --lines $lines -x ",
'desc' => $text{'journal_expla_journalctl'},
'id' => "journal-6", },
{ 'cmd' => "journalctl --lines $lines",
my $fselect = shift;
my $lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'}) || 1000;
my $journalctl_cmd = &has_command('journalctl');
return () if (!$journalctl_cmd);
my @rs = (
{ 'cmd' => "journalctl -n $lines",
'desc' => $text{'journal_journalctl'},
'id' => "journal-1", },
{ 'cmd' => "journalctl -n $lines -x ",
'desc' => $text{'journal_expla_journalctl'},
'id' => "journal-2", },
{ 'cmd' => "journalctl -n $lines -p alert..emerg",
'desc' => $text{'journal_journalctl_alert_emerg'},
'id' => "journal-3", },
{ 'cmd' => "journalctl -n $lines -p err..crit",
'desc' => $text{'journal_journalctl_err_crit'},
'id' => "journal-4", },
{ 'cmd' => "journalctl -n $lines -p notice..warning",
'desc' => $text{'journal_journalctl_notice_warning'},
'id' => "journal-5", },
{ 'cmd' => "journalctl -n $lines -p debug..info",
'desc' => $text{'journal_journalctl_debug_info'},
'id' => "journal-6", },
{ 'cmd' => "journalctl -n $lines -k ",
'desc' => $text{'journal_journalctl_dmesg'},
'id' => "journal-7", } );
# Add more units from config if exists on the system
my (%ucache, %uread);
my $units_cache = "$module_config_directory/units.cache";
&read_file($units_cache, \%ucache);
if (!%ucache) {
my $out = &backquote_command("systemctl list-units --all --no-legend ".
"--no-pager");
foreach my $line (split(/\r?\n/, $out)) {
$line =~ s/^[^a-z0-9\-\_\.]+//i;
my ($unit, $desc) = (split(/\s+/, $line, 5))[0, 4];
$uread{$unit} = $desc;
}
}
# All units
%ucache = %uread if (%uread);
# If forced to select, return full list
if ($fselect) {
my %units = %uread ? %uread : %ucache;
foreach my $u (sort keys %units) {
my $uname = $u;
$uname =~ s/\\x([0-9A-Fa-f]{2})/pack('H2', $1)/eg;
push(@rs, { 'cmd' => "journalctl -n ".
"$lines -u $u",
'desc' => $uname,
'id' => "journal-a-$u", });
}
}
# Otherwise, return only the pointer
# element for the index page
else {
push(@rs,
{ 'cmd' => "journalctl -n $lines -u",
'desc' => $text{'journal_journalctl_unit'},
'id' => "journal-u" });
}
# Save cache
if (%uread) {
&lock_file($units_cache);
&write_file($units_cache, \%ucache);
&unlock_file($units_cache);
}
return @rs;
}
# clear_systemctl_cache()
# Clear the cache of systemctl units
sub clear_systemctl_cache
{
unlink("$module_config_directory/units.cache");
}
# cleanup_destination(cmd)
# Returns a destination of some command cleaned up for display
sub cleanup_destination
{
my $cmd = shift;
$cmd =~ s/-n\s+\d+\s*//;
$cmd =~ s/\.service$//;
return $cmd;
}
# cleanup_description(desc)
# Returns a description cleaned up for display
sub cleanup_description
{
my $desc = shift;
$desc =~ s/\s+\(Virtualmin\)//;
return $desc;
}
# fix_clashing_description(description, service)
# Returns known clashing descriptions fixed
sub fix_clashing_description
{
my ($desc, $serv) = @_;
# EL systems name for PHP FastCGI Process Manager is repeated
if ($serv =~ /php(\d+)-php-fpm/) {
my $php_version = $1;
$php_version = join(".", split(//, $php_version));
$desc =~ s/PHP/PHP $php_version/;
}
return $desc;
}
# all_log_files(file)
@@ -138,5 +244,12 @@ foreach my $f (@rv) {
return @rv;
}
# config_post_save
# Called after the module's configuration has been saved
sub config_post_save
{
&clear_systemctl_cache();
}
1;

View File

@@ -1,7 +1,7 @@
name=logviewer
category=system
os_support=*-linux
desc=System Logs Viewer
desc=System Logs
depends=proc
longdesc=View and search all logs available on system
readonly=1

View File

@@ -7,7 +7,7 @@ require './logviewer-lib.pl';
&foreign_require("proc", "proc-lib.pl");
# Viewing a log file
@extras = &extra_log_files();
my @extras = &extra_log_files();
if ($in{'idx'} =~ /^\//) {
# The drop-down selector on this page has chosen a file
if (&indexof($in{'idx'}, (map { $_->{'file'} } @extras)) >= 0) {
@@ -21,11 +21,31 @@ if ($in{'idx'} =~ /^\//) {
delete($in{'idx'});
delete($in{'oidx'});
}
my $journal_since = &get_journal_since();
if ($in{'idx'} ne '') {
# From systemctl commands
if ($in{'idx'} =~ /^journal-/) {
my @systemctl_cmds = &get_systemctl_cmds();
my ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds;
my @systemctl_cmds = &get_systemctl_cmds(1);
my ($log);
if ($in{'idx'} eq 'journal-u') {
($log) = grep { $_->{'cmd'} =~ /-u\s+\w+/ }
@systemctl_cmds;
$in{'idx'} = $log->{'id'};
}
else {
($log) = grep { $_->{'id'} eq $in{'idx'} }
@systemctl_cmds;
}
# If reverse is set, add it to the command
if ($reverse) {
$log->{'cmd'} .= " -r";
}
# If since is set and allowed, add it to the command
if ($in{'since'} &&
grep { $_ eq $in{'since'} }
map { keys %$_ } @$journal_since) {
$log->{'cmd'} .= " $in{'since'}";
}
&can_edit_log($log) && $access{'syslog'} ||
&error($text{'save_ecannot2'});
$cmd = $log->{'cmd'};
@@ -94,99 +114,204 @@ else {
}
print "Refresh: $config{'refresh'}\r\n"
if ($config{'refresh'});
&ui_print_header("<tt>".&html_escape($file || $cmd)."</tt>",
$in{'linktitle'} || $text{'view_title'}, "", undef, undef, $in{'nonavlinks'});
$lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'});
$filter = $in{'filter'} ? quotemeta($in{'filter'}) : "";
my $lines = $in{'lines'} ? int($in{'lines'}) : int($config{'lines'});
my $jfilter = $in{'filter'} ? $in{'filter'} : "";
my $filter = $jfilter ? quotemeta($jfilter) : "";
my $reverse = $config{'reverse'} ? 1 : 0;
my $follow = $in{'since'} eq '-f' ? 1 : 0;
my $no_navlinks = $in{'nonavlinks'} == 1 ? 1 : undef;
my $skip_index = $config{'skip_index'} == 1 ? 1 : undef;
my $help_link = (!$no_navlinks && $skip_index) ?
&help_search_link("systemd-journal journalctl", "man", "doc") : undef;
my $no_links = $no_navlinks || $skip_index;
my $cmd_unpacked = $cmd;
$cmd_unpacked =~ s/\\x([0-9A-Fa-f]{2})/pack('H2', $1)/eg;
$cmd_unpacked =~ s/\s+\-r// if ($follow);
$cmd_unpacked =~ s/\s+\-n\s+\d+// if ($follow);
$cmd_unpacked .= " -g \"@{[&html_escape($jfilter)]}\"" if ($filter);
my $view_title = $in{'idx'} =~ /^journal/ ?
$text{'view_titlejournal'} : $text{'view_title'};
&ui_print_header("<tt>".&html_escape($file || $cmd_unpacked)."</tt>",
$in{'linktitle'} || $view_title, "", undef,
!$no_navlinks && $skip_index,
($no_navlinks || $skip_index) ? 1 : undef,
0, $help_link);
&filter_form();
$| = 1;
print "<pre>";
local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
$tailcmd =~ s/LINES/$lines/g;
if ($filter ne "") {
# Are we supposed to filter anything? Then use grep.
local @cats;
if ($cmd) {
# Getting output from a command
push(@cats, $cmd);
}
elsif ($config{'compressed'}) {
# All compressed versions
foreach $l (&all_log_files($file)) {
$c = &catter_command($l);
push(@cats, $c) if ($c);
}
}
else {
# Just the one log
@cats = ( "cat ".quotemeta($file) );
}
$cat = "(".join(" ; ", @cats).")";
if ($config{'reverse'}) {
$tailcmd .= " | tac";
}
$eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
$dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
if (@cats) {
$got = &proc::safe_process_exec(
"$cat | grep -i -a $eflag $dashflag $filter ".
"| $tailcmd",
0, 0, STDOUT, undef, 1, 0, undef, 1);
}
else {
$got = undef;
}
} else {
# Not filtering .. so cat the most recent non-empty file
if ($cmd) {
# Getting output from a command
$fullcmd = $cmd." | ".$tailcmd;
}
elsif ($config{'compressed'}) {
# Cat all compressed files
# Standard output
if (!$follow) {
$| = 1;
print "<pre>";
local $tailcmd = $config{'tail_cmd'} || "tail -n LINES";
$tailcmd =~ s/LINES/$lines/g;
my ($safe_proc_out, $safe_proc_out_got);
if ($filter ne "") {
# Are we supposed to filter anything? Then use grep.
local @cats;
$total = 0;
foreach $l (reverse(&all_log_files($file))) {
next if (!-s $l);
$c = &catter_command($l);
if ($c) {
$len = int(&backquote_command(
"$c | wc -l"));
$total += $len;
push(@cats, $c);
last if ($total > $in{'lines'});
if ($cmd) {
# Getting output from a command
push(@cats, $cmd);
}
elsif ($config{'compressed'}) {
# All compressed versions
foreach $l (&all_log_files($file)) {
$c = &catter_command($l);
push(@cats, $c) if ($c);
}
}
else {
# Just the one log
@cats = ( "cat ".quotemeta($file) );
}
$cat = "(".join(" ; ", @cats).")";
if ($reverse) {
$tailcmd .= " | tac" if ($cmd !~ /journalctl/);
}
$eflag = $gconfig{'os_type'} =~ /-linux/ ? "-E" : "";
$dashflag = $gconfig{'os_type'} =~ /-linux/ ? "--" : "";
if (@cats) {
$cat = "(".join(" ; ", reverse(@cats)).")";
$fullcmd = $cat." | ".$tailcmd;
my $fcmd;
if ($cmd =~ /journalctl/) {
$fcmd = "$cmd -g $filter";
}
else {
$fcmd = "$cat | grep -i -a $eflag $dashflag $filter ".
"| $tailcmd";
}
open(my $output_fh, '>', \$safe_proc_out);
$safe_proc_out_got = &proc::safe_process_exec(
$fcmd, 0, 0, $output_fh, undef, 1, 0, undef, 1);
close($output_fh);
print $safe_proc_out if ($safe_proc_out !~ /-- No entries --/m);
}
else {
$fullcmd = undef;
$safe_proc_out_got = undef;
}
} else {
# Not filtering .. so cat the most recent non-empty file
if ($cmd) {
# Getting output from a command
$fullcmd = $cmd.($cmd =~ /journalctl/ ? "" : (" | ".$tailcmd));
}
elsif ($config{'compressed'}) {
# Cat all compressed files
local @cats;
$total = 0;
foreach $l (reverse(&all_log_files($file))) {
next if (!-s $l);
$c = &catter_command($l);
if ($c) {
$len = int(&backquote_command(
"$c | wc -l"));
$total += $len;
push(@cats, $c);
last if ($total > $in{'lines'});
}
}
if (@cats) {
$cat = "(".join(" ; ", reverse(@cats)).")";
$fullcmd = $cat." | ".$tailcmd;
}
else {
$fullcmd = undef;
}
}
else {
# Just run tail on the file
$fullcmd = $tailcmd." ".quotemeta($file);
}
if ($reverse && $fullcmd) {
$fullcmd .= " | tac" if ($fullcmd !~ /journalctl/);
}
if ($fullcmd) {
open(my $output_fh, '>', \$safe_proc_out);
$safe_proc_out_got = &proc::safe_process_exec(
$fullcmd, 0, 0, $output_fh, undef, 1, 0, undef, 1);
close($output_fh);
print $safe_proc_out if ($safe_proc_out !~ /-- No entries --/m);
}
else {
$safe_proc_out_got = undef;
}
}
else {
# Just run tail on the file
$fullcmd = $tailcmd." ".quotemeta($file);
}
if ($config{'reverse'} && $fullcmd) {
$fullcmd .= " | tac";
}
if ($fullcmd) {
$got = &proc::safe_process_exec(
$fullcmd, 0, 0, STDOUT, undef, 1, 0, undef, 1);
}
else {
$got = undef;
}
print "<i data-empty>$text{'view_empty'}</i>\n"
if (!$safe_proc_out_got || $safe_proc_out =~ /-- No entries --/m);
print "</pre>\n";
}
# Progressive output
else {
print "<pre id='logdata' data-reversed='$reverse'>";
print "<i data-loading>$text{'view_loading'}</i>\n";
print "</pre>\n";
my %tinfo = &get_theme_info($current_theme);
my $spa_theme = $tinfo{'spa'} ? 1 : 0;
print <<EOF;
<script>
// Abort previous log viewer progress fetch
if (typeof fn_logviewer_progress_abort === 'function') {
fn_logviewer_progress_abort();
}
// Update log viewer with new data from the server
(async function () {
const logviewer_progress_abort = new AbortController();
const logDataElement = document.getElementById("logdata"),
response = await fetch("view_log_progress.cgi?idx=$in{'idx'}&filter=$jfilter",
{ signal: logviewer_progress_abort.signal }),
reader = response.body.getReader(),
decoder = new TextDecoder("utf-8"),
processText = async function () {
let { done, value } = await reader.read();
while (!done) {
const chunk = decoder.decode(value, { stream: true }).trim(),
dataReversed = logDataElement.getAttribute("data-reversed");
if (!processText.started) {
processText.started = true;
const loadingElement = logDataElement.querySelector("i[data-loading]");
if (loadingElement) {
loadingElement.remove();
}
}
let lines = chunk.split("\\n");
if (dataReversed === "1") {
lines = lines.reverse();
logDataElement.textContent =
lines.join("\\n") + "\\n" + logDataElement.textContent;
}
else {
logDataElement.textContent += lines.join("\\n") + "\\n";
}
if (typeof fn_logviewer_progress_update === 'function') {
fn_logviewer_progress_update(chunk, dataReversed);
}
({ done, value } = await reader.read());
}
};
if (typeof fn_logviewer_progress_status === 'function') {
fn_logviewer_progress_status(response);
}
fn_logviewer_progress_abort = function () {
logviewer_progress_abort.abort();
fn_logviewer_progress_abort = null;
}
if ($spa_theme !== 1) {
window.onbeforeunload = function() {
if (typeof fn_logviewer_progress_abort === 'function') {
fn_logviewer_progress_abort();
}
};
}
processText().catch((error) => {
if (typeof fn_logviewer_progress_ended === 'function') {
fn_logviewer_progress_ended(error);
}
});
})();
</script>
EOF
}
print "<i>$text{'view_empty'}</i>\n" if (!$got);
print "</pre>\n";
&filter_form();
if ($in{'nonavlinks'}) {
if ($no_links) {
&ui_print_footer();
}
else {
@@ -196,7 +321,9 @@ else {
sub filter_form
{
print &ui_form_start("view_log.cgi");
print &ui_hidden("nonavlinks", $in{'nonavlinks'} ? 1 : 0),"\n";
if ($no_navlinks) {
print &ui_hidden("nonavlinks", $no_navlinks),"\n";
}
print &ui_hidden("linktitle", $in{'linktitle'}),"\n";
print &ui_hidden("oidx", $in{'oidx'}),"\n";
print &ui_hidden("omod", $in{'omod'}),"\n";
@@ -210,10 +337,11 @@ my $found = 0;
my $text_view_header = 'view_header';
if ($access{'syslog'}) {
# Logs from syslog
my @systemctl_cmds = &get_systemctl_cmds();
my @systemctl_cmds = &get_systemctl_cmds(1);
foreach $c (@systemctl_cmds) {
next if (!&can_edit_log($c));
push(@logfiles, [ $c->{'id'}, "$c->{'desc'}" ]);
my $icon = $c->{'id'} =~ /journal-(a|x)/ ? "&#x25E6;&nbsp; " : "";
push(@logfiles, [ $c->{'id'}, $icon.$c->{'desc'} ]);
$found++ if ($c->{'id'} eq $in{'idx'});
}
@@ -267,18 +395,35 @@ foreach $e (&extra_log_files()) {
}
if (@logfiles && $found) {
$sel = &ui_select("idx", $in{'idx'} eq '' ? $file : $in{'idx'},
[ @logfiles ], undef, undef, undef, undef, "onChange='form.submit()'");
[ @logfiles ], undef, undef, undef, undef,
"onChange='form.submit()' style='max-width: 240px'");
if ($in{'idx'} =~ /^journal-/) {
my $since_label = $follow ? $text{'journal_sincefollow'} :
$text{'journal_since'};
$sel .= "$since_label&nbsp; " .
&ui_select("since", $in{'since'},
[ map { my ($key) = keys %$_;
[ $key, $_->{$key} ] }
@$journal_since ],
undef, undef, undef, undef,
"onChange='form.submit()'");
}
}
else {
$text_view_header = 'view_header2';
print &ui_hidden("idx", $in{'idx'}),"\n";
}
print &text($text_view_header, "&nbsp;" . &ui_textbox("lines", $lines, 3), "&nbsp;$sel"),"\n";
if ($follow) {
print &text('view_header3', "&nbsp;$sel"),"\n";
}
else {
print &text($text_view_header, "&nbsp;" . &ui_textbox("lines", $lines, 3), "&nbsp;$sel"),"\n";
}
print "&nbsp;&nbsp;&nbsp;&nbsp;\n";
print &text('view_filter', "&nbsp;" . &ui_textbox("filter", $in{'filter'}, 25)),"\n";
print &text('view_filter', "&nbsp;" . &ui_textbox("filter", $in{'filter'}, 12)),"\n";
print "&nbsp;&nbsp;\n";
print &ui_submit($text{'view_refresh'});
print &ui_submit($text{'view_filter_btn'});
print &ui_form_end(),"<br>\n";
}

View File

@@ -0,0 +1,47 @@
#!/usr/local/bin/perl
# view_log_progress.cgi
# Returns progressive output for some system log
require './logviewer-lib.pl';
&ReadParse();
&foreign_require("proc", "proc-lib.pl");
# Send headers
print "Content-Type: text/plain\n\n";
# System log to follow
my @systemctl_cmds = &get_systemctl_cmds(1);
my ($log) = grep { $_->{'id'} eq $in{'idx'} } @systemctl_cmds;
if (!&can_edit_log($log) ||
!$log->{'cmd'} ||
$log->{'cmd'} !~ /^journalctl/) {
print $text{'save_ecannot3'};
exit;
}
# Disable output buffering
$| = 1;
# No lines for real time logs
$log->{'cmd'} =~ s/\s+\-n\s+\d+//;
# Show real time logs
$log->{'cmd'} .= " -f";
# Add filter to the command if present
my $filter = $in{'filter'} ? quotemeta($in{'filter'}) : "";
if ($filter) {
$log->{'cmd'} .= " -g $filter";
}
# Open a pipe to the journalctl command
my $pid = open(my $fh, '-|', $log->{'cmd'}) ||
print &text('save_ecannot4', $log->{'cmd'}).": $!";
# Read and output the log
while (my $line = <$fh>) {
print $line;
}
# Clean up when done
close($fh);

View File

@@ -16,7 +16,7 @@ fwd_mode=0
delete_warn=y
top_buttons=2
view_html=2
view_images=3
view_images=1
mail_usermin=mail
sync_create=0
sync_modify=1

View File

@@ -2947,7 +2947,8 @@ my $iframe_body = <<EOF;
theme_mail_iframe_onload(iframe);
return;
}
const iframe_spinner = document.querySelector('#mail-iframe-spinner'),
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document,
iframe_spinner = document.querySelector('#mail-iframe-spinner'),
iframe_resize = function() {
const iframeobj = document.querySelector('#mail-iframe'),
iframe_height_bound = iframeobj.contentWindow.document.body.getBoundingClientRect().bottom,
@@ -2955,8 +2956,11 @@ my $iframe_body = <<EOF;
iframe_height =
iframe_height_bound > iframe_scroll_height ?
iframe_height_bound : iframe_scroll_height;
iframeobj.style.height = Math.ceil(iframe_height) + "px";
iframeobj.style.height = Math.ceil(iframe_height - 1) + "px";
};
iframeDoc.body.style.removeProperty('width');
iframeDoc.body.style.margin = '4px';
iframeDoc.body.style.padding = '0';
iframe_spinner && iframe_spinner.remove();
iframe.classList.add("loaded");
setTimeout(iframe_resize);
@@ -2980,6 +2984,15 @@ my $iframe_body = <<EOF;
})();
});
}, 99);
iframeDoc.addEventListener('click', function(event) {
if (event.target.tagName.toLowerCase() === 'summary' &&
event.target.dataset.resize === 'iframe') {
setTimeout(iframe_resize);
}
});
iframe.contentWindow.addEventListener('resize', function() {
setTimeout(iframe_resize);
});
}
</script>
<iframe
@@ -4240,4 +4253,562 @@ foreach my $h (@{$mail->{'headers'}}) {
return $rv;
}
# parse_calendar_file(calendar-file|lines)
# Parses an iCalendar file and returns a list of events
sub parse_calendar_file
{
my ($calendar_file) = @_;
my (@events, %event, $line);
eval "use DateTime; use DateTime::TimeZone;";
return \@events if ($@);
# Timezone map
my %timezone_map = (
'Afghanistan Time' => 'AFT',
'Alaskan Daylight Time' => 'AKDT',
'Alaskan Standard Time' => 'AKST',
'Anadyr Time' => 'ANAT',
'Arabian Standard Time' => 'AST',
'Argentina Time' => 'ART',
'Atlantic Daylight Time' => 'ADT',
'Atlantic Standard Time' => 'AST',
'Australian Central Daylight Time' => 'ACDT',
'Australian Central Standard Time' => 'ACST',
'Australian Eastern Daylight Time' => 'AEDT',
'Australian Eastern Standard Time' => 'AEST',
'Bangladesh Standard Time' => 'BST',
'Brasília Time' => 'BRT',
'British Summer Time' => 'BST',
'Central Africa Time' => 'CAT',
'Central Asia Time' => 'ALMT',
'Central Daylight Time' => 'CDT',
'Central Daylight Time (US)' => 'CDT',
'Central European Summer Time' => 'CEST',
'Central European Time' => 'CET',
'Central Indonesia Time' => 'WITA',
'Central Standard Time (Australia)' => 'CST',
'Central Standard Time (US)' => 'CST',
'Central Standard Time' => 'CST',
'Chamorro Daylight Time' => 'CHDT',
'Chamorro Standard Time' => 'CHST',
'China Standard Time' => 'CST',
'Coordinated Universal Time' => 'UTC',
'East Africa Time' => 'EAT',
'Eastern Africa Time' => 'EAT',
'Eastern Daylight Time' => 'EDT',
'Eastern Daylight Time (US)' => 'EDT',
'Eastern European Summer Time' => 'EEST',
'Eastern European Time' => 'EET',
'Eastern Indonesia Time' => 'WIT',
'Eastern Standard Time (Australia)' => 'EST',
'Eastern Standard Time (US)' => 'EST',
'Eastern Standard Time' => 'EST',
'Fiji Time' => 'FJT',
'Greenwich Mean Time' => 'GMT',
'Hawaii-Aleutian Daylight Time' => 'HADT',
'Hawaii-Aleutian Standard Time' => 'HAST',
'Hawaiian Standard Time' => 'HST',
'Hong Kong Time' => 'HKT',
'Indian Standard Time' => 'IST',
'Iran Standard Time' => 'IRST',
'Irish Standard Time' => 'IST',
'Israel Standard Time' => 'IST',
'Japan Standard Time' => 'JST',
'Korea Standard Time' => 'KST',
'Magadan Time' => 'MAGT',
'Malaysia Time' => 'MYT',
'Moscow Standard Time' => 'MSK',
'Mountain Daylight Time' => 'MDT',
'Mountain Standard Time' => 'MST',
'Myanmar Standard Time' => 'MMT',
'Nepal Time' => 'NPT',
'New Caledonia Time' => 'NCT',
'New Zealand Daylight Time' => 'NZDT',
'New Zealand Standard Time' => 'NZST',
'Newfoundland Daylight Time' => 'NDT',
'Newfoundland Standard Time' => 'NST',
'Pacific Daylight Time' => 'PDT',
'Pacific Standard Time' => 'PST',
'Pakistan Standard Time' => 'PKT',
'Philippine Time' => 'PHT',
'Sakhalin Time' => 'SAKT',
'Samoa Standard Time' => 'SST',
'Singapore Standard Time' => 'SGT',
'South Africa Standard Time' => 'SAST',
'Tahiti Time' => 'TAHT',
'Venezuelan Standard Time' => 'VET',
'West Africa Time' => 'WAT',
'Western European Summer Time' => 'WEST',
'Western European Time' => 'WET',
'Western Indonesia Time' => 'WIB',
'Western Standard Time (Australia)' => 'WST',
);
# Make a date from a special timestamp
my $adjust_time_with_timezone = sub {
my ($time, $tzid) = @_;
my $dt = DateTime->new(
year => substr($time, 0, 4),
month => substr($time, 4, 2),
day => substr($time, 6, 2),
hour => substr($time, 9, 2),
minute => substr($time, 11, 2),
second => substr($time, 13, 2),
time_zone => $tzid);
my $local_dt = $dt->clone->set_time_zone('local');
return {
formatted => $dt->strftime("%Y-%m-%d %H:%M:%S"),
timestamp => $dt->epoch,
formatted_local => $local_dt->strftime('%Y-%m-%d %H:%M:%S'),
timestamp_local => $local_dt->epoch,
};
};
# Lines processor
my $process_line = sub
{
my ($line) = @_;
# Start a new event
if ($line =~ /^BEGIN:VEVENT/) {
%event = ();
$event{'description'} = [ ];
$event{'attendees'} = [ ];
}
# Convert times using the timezone
elsif ($line =~ /^END:VEVENT/) {
# Local timezone
$event{'tzid_local'} = DateTime::TimeZone->new(name => 'local')->name();
$event{'tzid'} = 'UTC', $event{'tzid_missing'} = 1 if (!$event{'tzid'});
# Adjust times with timezone
my ($adjusted_start, $adjusted_end);
$event{'tzid'} = $timezone_map{$event{'tzid'}} || $event{'tzid'};
# Add single start/end time
if ($event{'dtstart'}) {
$adjusted_start =
$adjust_time_with_timezone->($event{'dtstart'},
$event{'tzid'});
$event{'dtstart_timestamp'} = $adjusted_start->{'timestamp'};
my $dtstart_date =
&make_date($event{'dtstart_timestamp'},
{ tz => $event{'tzid'} });
$event{'dtstart_date'} =
"$dtstart_date->{'short'} $dtstart_date->{'timeshort'}";
$event{'dtstart_local_timestamp'} =
$adjusted_start->{'timestamp_local'};
$event{'dtstart_local_date'} =
&make_date($event{'dtstart_local_timestamp'});
}
if ($event{'dtend'}) {
$adjusted_end =
$adjust_time_with_timezone->($event{'dtend'}, $event{'tzid'});
$event{'dtend_timestamp'} = $adjusted_end->{'timestamp'};
my $dtend_date = &make_date($event{'dtend_timestamp'},
{ tz => $event{'tzid'} });
$event{'dtend_date'} =
"$dtend_date->{'short'} $dtend_date->{'timeshort'}";
$event{'dtend_local_timestamp'} =
$adjusted_end->{'timestamp_local'};
$event{'dtend_local_date'} =
&make_date($event{'dtend_local_timestamp'});
}
if ($event{'dtstart'} && $event{'dtend'}) {
# Try to add local 'when (period)'
my $dtstart_local_obj =
$event{'_obj_dtstart_local_time'} =
make_date($event{'dtstart_local_timestamp'}, { });
my $dtend_local_obj =
$event{'_obj_dtend_local_time'} =
make_date($event{'dtend_local_timestamp'}, { });
# Build when local, e.g.:
# Tue Jun 04, 2024 04:30 PM 05:15
# PM (Asia/Nicosia +0300)
# or
# Tue Jun 04, 2024 04:30 PM Wed Jun 05, 2024 01:15
# AM (Asia/Nicosia +0300)
$event{'dtwhen_local'} =
# Start local
$dtstart_local_obj->{'week'}.' '.
$dtstart_local_obj->{'month'}.' '.
$dtstart_local_obj->{'day'}.', '.
$dtstart_local_obj->{'year'}.' '.
$dtstart_local_obj->{'timeshort'}.' ';
# End local
if ($dtstart_local_obj->{'year'} eq
$dtend_local_obj->{'year'} &&
$dtstart_local_obj->{'month'} eq
$dtend_local_obj->{'month'} &&
$dtstart_local_obj->{'day'} eq
$dtend_local_obj->{'day'}) {
$event{'dtwhen_local'} .=
$dtend_local_obj->{'timeshort'};
}
else {
$event{'dtwhen_local'} .=
$dtend_local_obj->{'week'}.' '.
$dtend_local_obj->{'month'}.' '.
$dtend_local_obj->{'day'}.', '.
$dtend_local_obj->{'year'}.' '.
$dtend_local_obj->{'timeshort'};
}
# Timezone local
if ($event{'tzid_local'} ||
$dtstart_local_obj->{'tz'}) {
if ($event{'tzid_local'} &&
$dtstart_local_obj->{'tz'}) {
if ($event{'tzid_local'} eq
$dtstart_local_obj->{'tz'}) {
$event{'dtwhen_local'} .=
" ($event{'tzid_local'})";
}
else {
$event{'dtwhen_local'} .=
" ($event{'tzid_local'} ".
"$dtstart_local_obj->{'tz'})";
}
}
elsif ($event{'tzid_local'}) {
$event{'dtwhen_local'} .=
" ($event{'tzid_local'})";
}
else {
$event{'dtwhen_local'} .=
" ($dtstart_local_obj->{'tz'})";
}
}
# Try to add original 'when (period)'
my $dtstart_obj =
$event{'_obj_dtstart_time'} =
make_date($event{'dtstart_timestamp'},
{ tz => $event{'tzid'} });
my $dtend_obj =
$event{'_obj_dtend_time'} =
make_date($event{'dtend_timestamp'},
{ tz => $event{'tzid'} });
# Build original when
if (!$event{'tzid_missing'}) {
$event{'dtwhen'} =
# Start original
$dtstart_obj->{'week'}.' '.
$dtstart_obj->{'month'}.' '.
$dtstart_obj->{'day'}.', '.
$dtstart_obj->{'year'}.' '.
$dtstart_obj->{'timeshort'}.' ';
# End original
if ($dtstart_obj->{'year'} eq
$dtend_obj->{'year'} &&
$dtstart_obj->{'month'} eq
$dtend_obj->{'month'} &&
$dtstart_obj->{'day'} eq
$dtend_obj->{'day'}) {
$event{'dtwhen'} .=
$dtend_obj->{'timeshort'};
}
else {
$event{'dtwhen'} .=
$dtend_obj->{'week'}.' '.
$dtend_obj->{'month'}.' '.
$dtend_obj->{'day'}.', '.
$dtend_obj->{'year'}.' '.
$dtend_obj->{'timeshort'};
}
# Timezone original
if ($dtstart_obj->{'tz'}) {
$event{'dtwhen'} .=
" ($dtstart_obj->{'tz'})";
}
}
}
# Add the event to the list
push(@events, { %event });
}
# Parse fields
elsif ($line =~ /^SUMMARY.*?:(.*)$/) {
$event{'summary'} = $1;
}
elsif ($line =~ /^DTSTART:(.*)$/) {
$event{'dtstart'} = $1;
}
elsif ($line =~ /^DTSTART;TZID=(.*?):(.*)$/) {
$event{'tzid'} = $1;
$event{'dtstart'} = $2;
}
elsif ($line =~ /^DTEND:(.*)$/) {
$event{'dtend'} = $1;
}
elsif ($line =~ /^DTEND;TZID=(.*?):(.*)$/) {
$event{'tzid'} = $1;
$event{'dtend'} = $2;
}
elsif ($line =~ /^DESCRIPTION:(.*)$/) {
my $description = $1;
$description =~ s/\\n/<br>/g;
$description =~ s/\\//g;
unshift(@{$event{'description'}}, $description);
}
elsif ($line =~ /^DESCRIPTION;LANGUAGE=([a-z]{2}-[A-Z]{2}):(.*)$/) {
my $description = $2;
$description =~ s/\\n/<br>/g;
$description =~ s/\\//g;
unshift(@{$event{'description'}}, $description);
}
elsif ($line =~ /^LOCATION.*?:(.*)$/) {
$event{'location'} = $1;
}
elsif ($line =~ /^ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE;CN=(.*?):mailto:(.*)$/ ||
$line =~ /^ATTENDEE;.*CN=(.*?);.*mailto:(.*)$/ ||
$line =~ /^ATTENDEE:mailto:(.*)$/) {
push(@{$event{'attendees'}}, { 'name' => $1, 'email' => $2 });
}
elsif ($line =~ /^ORGANIZER;CN=(.*?):(?:mailto:)?(.*)$/) {
$event{'organizer_name'} = $1;
$event{'organizer_email'} = $2;
}
};
# Read the ICS file lines or just use the lines
my $ics_file_lines =
-r $calendar_file ?
&read_file_lines($calendar_file, 1) :
[ split(/\r?\n/, $calendar_file) ];
# Process each line of the ICS file
foreach my $ics_file_line (@$ics_file_lines) {
# Check if the line is a continuation of the previous line
if ($ics_file_line =~ /^[ \t](.*)$/) {
$line .= $1; # Concatenate with the previous line
}
else {
# Process the previous line
$process_line->($line) if ($line);
$line = $ics_file_line; # Start a new line
}
}
# Process the last line
$process_line->($line) if ($line);
# Return the list of events
return \@events;
}
# get_calendar_data(&calendars)
# Returns HTML for all parsed calendars
sub get_calendar_data
{
my ($calendars) = @_;
my @calendars = @{$calendars};
$calendars = { };
if (@calendars) {
# Fonts for our HTML
$calendars->{'html'} .= &theme_fonts()
if (defined(&theme_fonts));
my $theme_css_inline;
$theme_css_inline = &theme_css_inline('calendar')
if (defined(&theme_css_inline));
# CSS for HTML version
$calendars->{'html'} .= <<STYLE;
<style>
.calendar-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #99999933;
margin-bottom: 4px;
font-family: 'RobotoLocal',arial,helvetica,clean,sans-serif !important;
}
.calendar-table-inner {
table-layout: fixed;
border-collapse: collapse;
}
.calendar-table td {
padding: 5px;
vertical-align: top;
overflow-wrap: anywhere;
}
.calendar-table .calendar-cell {
background-color: #99999916;
text-align: center;
vertical-align: top;
padding: 2px;
padding-top: 24px;
padding-bottom: 24px;
width: 100px;
min-width: 100px;
font-weight: bold;
}
.calendar-month {
font-size: 19px;
color: #1d72ff;
text-align: center;
padding: 2px 8px;
}
.calendar-day {
font-size: 19px;
text-align: center;
padding: 4px 8px;
}
.calendar-week {
font-size: 13px;
border-top: 1px dotted #999999aa;
padding: 6px;
display: inline-block;
}
.calendar-details h2 {
margin: 0;
font-size: 15px;
}
.calendar-details p {
margin: 4px 0;
}
.calendar-details .title {
font-size: 16px;
}
.calendar-details .detail strong {
opacity: 0.83;
white-space: nowrap;
}
.calendar-details .detail + .desc p:first-child {
margin-top: 0;
}
details.calendar-details {
font-size: 87%;
display: inline-block;
margin-left: 9px;
}
.calendar-details > .calendar-table-inner .detail:has(strong),
.calendar-details > .calendar-table-inner .detail strong,
.calendar-details > .calendar-table-inner .detail + td {
font-size: 13px;
line-height: 1.2;
}
details.calendar-details summary {
cursor: help;
}
details.calendar-details tr:has(>.detail+td:empty),
.calendar-details tr:has(>.detail+td:empty) {
display: none;
}
$theme_css_inline
</style>
STYLE
foreach my $calendar (@calendars) {
my $title = $calendar->{'summary'} || $calendar->{'description'};
my $orginizer = $calendar->{'organizer_name'};
my @attendees;
foreach my $a (@{$calendar->{'attendees'}}) {
push(@attendees, { name => $a->{'name'},
email => $a->{'email'} });
}
my $who = join(", ", map { $_->{'name'} } @attendees);
if ($who && $orginizer) {
$who .= ", ${orginizer}*";
}
elsif ($orginizer) {
$who = "${orginizer}*";
}
# HTML version
$calendars->{'html'} .= <<HTML;
<table class="calendar-table">
<tr>
<td class="calendar-cell">
<div class="calendar-block">
<div class="calendar-month">
$calendar->{'_obj_dtstart_local_time'}->{'month'}
</div>
<div class="calendar-day">
$calendar->{'_obj_dtstart_local_time'}->{'day'}
</div>
<div class="calendar-week">
$calendar->{'_obj_dtstart_local_time'}->{'week'}
</div>
</div>
</td>
<td class="calendar-details">
<table class="calendar-table-inner">
<tr>
<td class="title" colspan="2">
<strong>$title</strong>
</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_when'}</strong>
</td>
<td>$calendar->{'dtwhen_local'}</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_where'}</strong>
</td>
<td>$calendar->{'location'}</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_who'}</strong>
</td>
<td>$who</td>
</tr>
</table>
<details class="calendar-details">
<summary data-resize="iframe"></summary>
<table class="calendar-table-inner">
<tr>
<td class="detail">
<strong>$text{'view_ical_orginizertime'}</strong>
</td>
<td>$calendar->{'dtwhen'}</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_orginizername'}</strong>
</td>
<td>$calendar->{'organizer_name'}</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_orginizeremail'}</strong>
</td>
<td>$calendar->{'organizer_email'}</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_attendees'}</strong>
</td>
<td class="desc">@{[join('', map {
"<p>$_->{'name'}<br>$_->{'email'}</p>"
} @attendees)]}</td>
</tr>
<tr>
<td class="detail">
<strong>$text{'view_ical_desc'}</strong>
</td>
<td class="desc">@{[join('<br>',
@{$calendar->{'description'}})]}</td>
</tr>
</table>
</details>
</td>
</tr>
</table>
HTML
# Text version
my %textical = (
'view_ical' => $title,
'view_ical_when' => $calendar->{'dtwhen_local'},
'view_ical_where' => $calendar->{'location'},
'view_ical_who' => $who
);
my $max_label_length = 0;
foreach my $key (sort keys %textical) {
my $label_length = length($text{$key});
if ($label_length > $max_label_length) {
$max_label_length = $label_length;
}
}
$calendars->{'text'} = "=" x 79 . "\n";
foreach my $key (sort keys %textical) {
my $label = $text{$key};
my $value = $textical{$key};
my $spaces .= " " x ($max_label_length - length($label));
$calendars->{'text'} .= "$label$spaces : $value\n";
}
$calendars->{'text'} .= "=" x 79 . "\n";
}
}
return $calendars;
}
1;

View File

@@ -154,6 +154,15 @@ view_sub=Attached Email
view_sub2=Attached email from $1
view_egone=This message no longer exists
view_eugone=This user does not exist
view_ical=Event
view_ical_when=When
view_ical_where=Where
view_ical_who=Who
view_ical_orginizertime=Organizer time
view_ical_orginizername=Organizer name
view_ical_orginizeremail=Organizer email
view_ical_attendees=Attendees details
view_ical_desc=Event description
view_gnupg=GnuPG signature verification
view_gnupg_0=Signature by $1 is valid.
@@ -323,6 +332,7 @@ folder_inbox=Inbox
folder_sent=Sent
folder_drafts=Drafts
folder_trash=Trash
folder_spam=Spam
detach_err=Failed to detach file
detach_edir=No file or directory to save to entered

View File

@@ -1287,4 +1287,3 @@ return $rv;
}
1;

View File

@@ -89,6 +89,16 @@ foreach $s (@sub) {
@attach = grep { $_ ne $body && $_ ne $dstatus } @attach;
@attach = grep { !$_->{'attach'} } @attach;
# Calendar attachments
my @calendars;
eval {
foreach my $i (grep { $_->{'data'} }
grep { $_->{'type'} =~ /^text\/calendar/ } @attach) {
my $calendars = &parse_calendar_file($i->{'data'});
push(@calendars, @{$calendars});
}};
# Mail buttons
if ($config{'top_buttons'} == 2 && &editable_mail($mail)) {
&show_mail_buttons(1, scalar(@sub));
print "<p class='mail_buttons_divide'></p>\n";
@@ -138,11 +148,15 @@ else {
print &ui_table_end();
# Show body attachment, with properly linked URLs
@bodyright = ( );
my $bodycontents;
my @bodyright = ( );
my $calendars = &get_calendar_data(\@calendars);
if ($body && $body->{'data'} =~ /\S/) {
if ($body eq $textbody) {
# Show plain text
$bodycontents = "<pre>";
$bodycontents .= $calendars->{'text'}
if ($calendars->{'text'});
foreach $l (&wrap_lines(&eucconv($body->{'data'}),
$config{'wrap_width'})) {
$bodycontents .= &link_urls_and_escape($l,
@@ -156,7 +170,9 @@ if ($body && $body->{'data'} =~ /\S/) {
}
elsif ($body eq $htmlbody) {
# Attempt to show HTML
$bodycontents = $body->{'data'};
$bodycontents = $calendars->{'html'}
if ($calendars->{'html'});
$bodycontents .= $body->{'data'};
my @imageurls;
my $image_mode = int(defined($in{'images'}) ? $in{'images'} : $config{'view_images'});
$bodycontents = &disable_html_images($bodycontents, $image_mode, \@imageurls);

View File

@@ -330,7 +330,7 @@ Corvus Latinux 8.0 redhat-linux 7.1 `cat /etc/latinux-release 2>/dev/null` =~
Immunix Linux $1 redhat-linux $1 $etc_issue =~ /Immunix.*\s([0-9\.]+)/i || `cat /etc/immunix-release 2>/dev/null` =~ /([0-9\.]+)/
# All versions of Gentoo (which don't appear to have version numbers)
Gentoo Linux Any version gentoo-linux * -d "/usr/portage"
Gentoo Linux Any version gentoo-linux * -d "/usr/portage" || `cat /etc/os-release 2>/dev/null` =~ /gentoo/
# Secure Linux (now called Trustix?)
Secure Linux 1.0 redhat-linux 7.2 `cat /etc/securelinux-release 2>/dev/null` =~ /SecureLinux.*1\.0/i

View File

@@ -86,10 +86,11 @@ print &ui_table_start($text{'general_title_others'}, "width=100%", 4);
&option_radios_freefield("mydomain", 40, $text{'opts_mydomain_default'});
&option_radios_freefield("mynetworks", 60, $text{'opts_mynetworks_default'});
&option_radios_freefield("mynetworks", 60, $text{'default'});
&option_select("mynetworks_style",
[ [ "subnet", $text{'opts_mynetworks_subnet'} ],
[ [ "", $text{'default'} ],
[ "subnet", $text{'opts_mynetworks_subnet'} ],
[ "class", $text{'opts_mynetworks_class'} ],
[ "host", $text{'opts_mynetworks_host'} ] ]);

View File

@@ -249,7 +249,6 @@ opts_mydomain_default=Default (provided by system)
opts_myhostname=Internet hostname of this mail system
opts_myhostname_default=Default (provided by system)
opts_mynetworks=Local networks
opts_mynetworks_default=Default (all attached networks)
opts_mynetworks_style=Automatic local networks
opts_mynetworks_subnet=Same IP subnet
opts_mynetworks_class=Same network class

View File

@@ -62,11 +62,11 @@ if ($site{'size'} != $st[7] || !$site{'version'} || !$site{'fullversion'}) {
# Get the list of modules
local @mods;
open(MODS, "$config{'proftpd_path'} -l |");
open(MODS, "$config{'proftpd_path'} -vv |");
while(<MODS>) {
s/\r|\n//g;
if (/^\s*(\S+)\.c$/) {
push(@mods, $1);
if (/^\s*(?<mod_built_in>\S+)\.c$|\s*(?<mod_loaded>mod_[a-zA-Z0-9_]+)\//) {
push(@mods, $+{mod_loaded} || $+{mod_built_in});
}
}
close(MODS);

View File

@@ -9,7 +9,10 @@ $ver = &get_syslog_ng_version();
my $index_econf2;
if (&has_command('systemctl')) {
if (&foreign_available('logviewer')) {
$index_econf2 = &text('index_econf2', "System Logs Viewer", "@{[&get_webprefix()]}/logviewer") . "<p><br>";
my %logviewer_text = &load_language('logviewer');
$index_econf2 = &text('index_econf2',
$logviewer_text{'index_title'},
"@{[&get_webprefix()]}/logviewer") . "<p><br>";
}
}
if (!$ver) {

View File

@@ -26,7 +26,10 @@ if (!-r $config{'syslog_conf'}) {
my $index_econf2;
if (&has_command('systemctl')) {
if (&foreign_available('logviewer')) {
$index_econf2 = &text('index_econf2', "System Logs Viewer", "@{[&get_webprefix()]}/logviewer") . "<p><br>";
my %logviewer_text = &load_language('logviewer');
$index_econf2 = &text('index_econf2',
$logviewer_text{'index_title'},
"@{[&get_webprefix()]}/logviewer") . "<p><br>";
}
}
# Not installed (maybe using syslog-ng)

View File

@@ -1,4 +1,4 @@
index_title=System Logs
index_title=System Logs RS
index_m4msg=Your system log configuration file $1 appears to contain <tt>m4</tt> directives. Before it can be edited, Webmin needs to pass the file through <tt>m4</tt> to safely remove these directives.
index_m4=Remove m4 directives from config file
index_econf=The syslog configuration file $1 was not found on your system. Maybe syslog is not installed, or a newer version like syslog-ng is in use, or the <a href='$2'>module configuration</a> is incorrect.
@@ -16,7 +16,7 @@ index_cmd=Output from $1
index_all=All users
index_users=Users $1
index_add=Add a new system log.
index_return=system logs
index_return=module index
index_restart=Apply Changes
index_restartmsg=Click this button to make the current configuration active by killing the running <tt>syslog</tt> process and restarting it.
index_start=Start Syslog Server

View File

@@ -1,7 +1,7 @@
name=Syslog
category=system
os_support=solaris *-linux freebsd openbsd macos hpux irix unixware aix netbsd openserver
desc=System Logs
desc=System Logs RS
depends=proc
longdesc=Configure the syslog server on your system and view its log files.
readonly=1

View File

@@ -8,5 +8,5 @@ no warnings 'redefine';
no warnings 'uninitialized';
package system_status;
require './system-status-lib.pl';
&scheduled_collect_system_info();
&scheduled_collect_system_info();

View File

@@ -56,7 +56,7 @@ if ($in{'action'} eq $text{'action_sync'}) {
if (defined($in{'sync_service_name'}) &&
defined($in{'sync_service_status'})) {
my $service_name = $in{'sync_service_name'};
if ($service_name !~ /^(chronyd|systemd-timesyncd)$/) {
if ($service_name !~ /^(chronyd|chrony|systemd-timesyncd)$/) {
&error(&text('error_serviceunknown', &html_escape($service_name)));
}
my $service_status = int($in{'sync_service_status'});

View File

@@ -5,7 +5,8 @@ use warnings;
no warnings 'redefine';
no warnings 'uninitialized';
require './time-lib.pl';
our (%in, %text, %config, %access, $base_remote_user, $get_hardware_time_error);
our (%in, %text, %config, %access, $base_remote_user,
$get_hardware_time_error, $cronyd_name);
my ($rawdate, $rawhwdate, %system_date, $rawtime, %hw_date, $txt);
$txt = "";
@@ -174,13 +175,14 @@ if ( ( !$access{ 'sysdate' } && &has_command( "date" ) || !$access{ 'hwdate' } &
$config{'timeserver_hardware'}));
}
}
if (&foreign_require('init') && &init::action_status('chronyd') > 0 && &has_command("chronyc")) {
my $chronyd_running = &init::status_action('chronyd');
my $chronyd_run_atboot = &init::action_status('chronyd');
my $ui_hiddens = &ui_hidden("sync_service_name", "chronyd");
if (&foreign_require('init') &&
&init::action_status($cronyd_name) > 0 && &has_command("chronyc")) {
my $chronyd_running = &init::status_action($cronyd_name);
my $chronyd_run_atboot = &init::action_status($cronyd_name);
my $ui_hiddens = &ui_hidden("sync_service_name", $cronyd_name);
$ui_hiddens .= &ui_hidden("timeserver", $config{'timeserver'})
if (!$ntp_support);
print &ui_table_row(&text('index_tabsync2', 'chronyd'),
print &ui_table_row(&text('index_tabsync2', $cronyd_name),
$ui_hiddens.
&ui_radio("sync_service_status",
(($chronyd_run_atboot == 2 && $chronyd_running) ? 2 :

View File

@@ -13,6 +13,7 @@ our ($timezones_file, $currentzone_link, $currentzone_file, $timezones_dir,
$sysclock_file);
our ($get_hardware_time_error);
our $cron_cmd = "$module_config_directory/sync.pl";
our $cronyd_name = $gconfig{'os_type'} eq 'debian-linux' ? 'chrony' : 'chronyd';
our $rawtime;
if ($config{'zone_style'}) {
do "$config{'zone_style'}-lib.pl";
@@ -49,13 +50,13 @@ if (&has_command("ntpdate")) {
elsif (&has_command("sntp")) {
$out = &backquote_logged("sntp -s $servs 2>&1");
}
elsif (&foreign_require('init') && &init::action_status('chronyd') > 0 && &has_command("chronyc")) {
my $chronyd_running = &init::status_action('chronyd');
$out = &backquote_logged("systemctl restart chronyd 2>&1");
elsif (&foreign_require('init') && &init::action_status($cronyd_name) > 0 && &has_command("chronyc")) {
my $chronyd_running = &init::status_action($cronyd_name);
$out = &backquote_logged("systemctl restart $cronyd_name 2>&1");
$out .= &backquote_logged("chronyc makestep 2>&1");
sleep ($chronyd_running ? 5 : 15);
if (!$chronyd_running) {
&backquote_logged("systemctl stop chronyd 2>&1");
&backquote_logged("systemctl stop $cronyd_name 2>&1");
}
}
elsif (&foreign_require('init') && &init::action_status('systemd-timesyncd') > 0) {

View File

@@ -21,7 +21,8 @@ return 0;
sub open_last_command
{
local ($fh, $user) = @_;
open($fh, "last $user |");
local $quser = quotemeta($user);
open($fh, "(last -w $quser || last $quser) |");
}
# read_last_line(handle)
@@ -34,7 +35,9 @@ while(1) {
chop($line = <$fh>);
if (!$line) { return (); }
if ($line =~ /^(reboot|shutdown)/) { next; }
if ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+)\s+\-\s+(\S+)\s+\((\d+:\d+)\)/) {
if ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+)\s+\-\s+(\S+)\s+\((.*?\d+:\d+.*?)\)/) {
# root pts/0 10.211.55.2 Tue Nov 22 21:06 - 23:16 (02:10:00)
# root pts/1 10.211.55.2 Wed Jun 29 13:13 - shutdown (7+00:01:20)
return ($1, $2, $3, $4, $5 eq "shutdown" ? "Shutdown" :
$5 eq "crash" ? "Crash" : $5, $6);
}

View File

@@ -22,7 +22,7 @@ sub open_last_command
{
local ($fh, $user) = @_;
local $quser = quotemeta($user);
open($fh, "(last -F $quser || last $quser) |");
open($fh, "(last -F -w $quser || last -w $quser) |");
}
# read_last_line(handle)
@@ -39,12 +39,16 @@ while(1) {
# jcameron pts/0 fudu Thu Feb 22 09:47 - 10:15
return ($1, $2, $3, $4, $5 eq "down" ? "Shutdown" : $5, $6);
}
elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+\-\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+\((\d+:\d+)\)/) {
elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+\-\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+\((.*?\d+:\d+)\)/) {
# jcameron pts/0 fudu Thu Feb 22 09:47 - 10:15
# jcameron pts/0 fudu Sun Feb 4 02:26:28 2024 - Wed Feb 7 18:25:09 2024 (3+15:58)
return ($1, $2, $3, $4, $5 eq "down" ? "Shutdown" : $5, $6);
}
elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+)\s+still/) {
# root pts/0 fudu Fri Feb 23 18:46 still logged in
elsif ($line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+)\s+(still|gone)/ ||
$line =~ /^(\S+)\s+(\S+)\s+(\S+)?\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+\s+\d+)\s+(still|gone)/) {
# root pts/0 fudu Fri Feb 23 18:46 still logged in
# root ftpd10 fudu Thu Jun 20 11:19 gone - no logout
# root pts/0 fudu Tue Jun 18 23:10:30 2024 still logged in
return ($1, $2, $3, $4);
}
}

View File

@@ -64,6 +64,18 @@ else {
$miniserv{'bind'} = $first->[0];
}
$miniserv{'sockets'} = join(" ", map { "$_->[0]:$_->[1]" } @sockets);
if ($in{'websocket_base_port_def'}) {
delete($miniserv{'websocket_base_port'});
}
else {
$miniserv{'websocket_base_port'} = $in{'websocket_base_port'};
}
if ($in{'websocket_host_def'}) {
delete($miniserv{'websocket_host'});
}
else {
$miniserv{'websocket_host'} = $in{'websocket_host'};
}
$miniserv{'ipv6'} = $in{'ipv6'};
if ($in{'listen_def'}) {
delete($miniserv{'listen'});

View File

@@ -47,6 +47,22 @@ if (&foreign_check("firewall")) {
}
print &ui_table_row($text{'bind_sockets'}, $stable);
# WebSocket based port
print &ui_table_row($text{'bind_websocport'},
&ui_radio("websocket_base_port_def",
$miniserv{"websocket_base_port"} ? 0 : 1,
[ [ 1, $text{'bind_websocport_none'} ],
[ 0, &ui_textbox("websocket_base_port",
$miniserv{"websocket_base_port"}, 6) ] ]));
# Hostname for WebSocket connections
print &ui_table_row($text{'bind_websoc_host'},
&ui_radio("websocket_host_def",
$miniserv{"websocket_host"} ? 0 : 1,
[ [ 1, $text{'bind_websoc_host_auto'} ],
[ 0, &ui_textbox("websocket_host",
$miniserv{"websocket_host"}, 25) ] ]));
# IPv6 enabled?
print &ui_table_row($text{'bind_ipv6'},
&ui_yesno_radio("ipv6", $miniserv{'ipv6'}));

View File

@@ -2098,10 +2098,10 @@ formatted like dd/mmm/yyyy hh:mm:ss. Parameters are :
=item seconds - Unix time is seconds to convert.
=item date-only-or-opts - If set to 1, exclude the time from the returned string.
=item date-only-or-opts - If set to 1 exclude the time from the returned string.
In case this param is a hash reference use it for options in a new DateTime::Locale
code or preserve the original, old logic
In case this param is a hash reference use it for options in a new
DateTime::Locale code or preserve the original, old logic
=item fmt - Optional, one of dd/mon/yyyy, dd/mm/yyyy, mm/dd/yyyy or yyyy/mm/dd
@@ -2116,27 +2116,31 @@ if (!$@ && $] > 5.011) {
my $opts = ref($only) ? $only : {};
my $locale_default = &get_default_system_locale();
my $locale_auto = &parse_accepted_language();
my $locale_name = $opts->{'locale'} || $gconfig{'locale_'.$base_remote_user} ||
my $locale_name = $opts->{'locale'} ||
$gconfig{'locale_'.$base_remote_user} ||
$gconfig{'locale_'.$remote_user} || $locale_auto ||
$gconfig{'locale'} || &get_default_system_locale();
my $tz = $opts->{'tz'};
if (!$tz) {
eval {
$tz =
DateTime::TimeZone->new(name => strftime("%z", localtime()))->name(); # +0200
$tz = DateTime::TimeZone->new(
name => strftime("%z", localtime()))->name(); # +0200
};
if ($@) {
eval {
$tz = DateTime::TimeZone->new(name => 'local')->name(); # Asia/Nicosia
$tz = DateTime::TimeZone->new(
name => 'local')->name(); # Asia/Nicosia
};
if ($@) {
$tz = DateTime::TimeZone->new(name => 'UTC')->name(); # UTC
$tz = DateTime::TimeZone->new(
name => 'UTC')->name(); # UTC
}
}
}
# Pre-process time locale
my $locale_military_status = sub {
return ($locale_military_name && $locale_military_name =~ /[a-z]/i) ? 2 :
return ($locale_military_name &&
$locale_military_name =~ /[a-z]/i) ? 2 :
($locale_military_name == 1) ? 1 : 0;
};
# Allow locales with military time (in 24h format)
@@ -2152,15 +2156,18 @@ if (!$@ && $] > 5.011) {
my $locale = DateTime::Locale->load($locale_name_loaded);
# Create a new locale out of base locale
if (&$locale_military_status() == 1) {
my %locale_data = $locale->locale_data;
$locale_data{'code'} = $locale_name_initial;
my %locale_data = $locale->locale_data;
$locale_data{'code'} = $locale_name_initial;
# Force 24h time
$locale_data{'glibc_date_1_format'} = '%a %b %e %H:%M:%S %Z %Y';
$locale_data{'glibc_date_1_format'} = '%a %b %e %H:%M:%S %Z %Y';
$locale_data{'glibc_datetime_format'} = '%a %d %b %Y %T %Z';
$locale_data{'glibc_time_format'} = '%T';
$locale_data{'glibc_time_format'} = '%T';
DateTime::Locale->register_from_data(%locale_data);
# Load newly cloned locale in 24h time format
$locale_military_name = $locale_name_loaded = $locale_name_initial;
$locale_military_name = $locale_name_loaded =
$locale_name_initial;
$locale = DateTime::Locale->load($locale_name_loaded);
}
my $locale_format_full_tz = $locale->glibc_date_1_format; # Sat 20 Nov 2286 17:46:39 UTC
@@ -2176,7 +2183,7 @@ if (!$@ && $] > 5.011) {
}
# Return fully detailed object
if (%{$opts}) {
if (ref($only)) {
# Can we get ago time
my $ago;
my $ago_secs = time() - $secs;
@@ -2194,18 +2201,31 @@ if (!$@ && $] > 5.011) {
"pretty" => $ago_obj->pretty
};
}
# my $xxxx = $locale->full_date_format;
my $data = {
# Wed Feb 8 05:09:39 PM UTC 2023
'full-tz-utc' => DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs)->strftime($locale_format_full_tz),
'full-tz-utc' => DateTime->from_epoch(
locale => $locale_name_loaded,
epoch => $secs)->strftime($locale_format_full_tz),
# Wed Feb 8 07:10:01 PM EET 2023
'full-tz' => DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime($locale_format_full_tz),
'full-tz' => DateTime->from_epoch(
locale => $locale_name_loaded,
epoch => $secs,
time_zone => $tz)->strftime($locale_format_full_tz),
# Wed 08 Feb 2023 07:11:26 PM EET
'full' => DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime($locale_format_full),
'full' => DateTime->from_epoch(
locale => $locale_name_loaded,
epoch => $secs,
time_zone => $tz)->strftime($locale_format_full),
# 02/08/2023
'short' => DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime($locale_format_short),
'short' => DateTime->from_epoch(
locale => $locale_name_loaded,
epoch => $secs,
time_zone => $tz)->strftime($locale_format_short),
# 07:12:07 PM
'time' => DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime($locale_format_time),
'time' => DateTime->from_epoch(
locale => $locale_name_loaded,
epoch => $secs,
time_zone => $tz)->strftime($locale_format_time),
'ago' => $ago,
'tz' => $tz,
'delimiter' => $locale_format_delimiter,
@@ -2216,10 +2236,16 @@ if (!$@ && $] > 5.011) {
$data->{'timeshort'} = $data->{'time'};
$data->{'timeshort'} =~ s/(\d+):(\d+):(\d+)(.*?)/$1:$2$4/;
# %c alternative with full week and month and no seconds in time (complete)
# Wednesday, February 8, 2023, 8:18 PM or 星期三, 2023年2月8日 20:18 or miércoles, 8 febrero 2023, 20:28
$data->{'monthfull'} = DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime("%B");
foreach (split(/\s+/, DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime("%A, %c"))) {
# %c alternative with full week and month and no seconds
# in time (complete)
# Wednesday, February 8, 2023, 8:18 PM or 星期三,
# 2023年2月8日 20:18 or miércoles, 8 febrero 2023, 20:28
$data->{'monthfull'} = DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%B");
foreach (split(/\s+/, DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%A, %c"))) {
if ($data->{'monthfull'} =~ /^$_/) {
$data->{'complete'} .= "$data->{'monthfull'} "
}
@@ -2227,9 +2253,24 @@ if (!$@ && $] > 5.011) {
$data->{'complete'} .= "$_ "
}
};
$data->{'year'} = DateTime->from_epoch(locale => $locale_name_loaded, epoch => $secs, time_zone => $tz)->strftime("%Y");
$data->{'year'} = DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%Y");
$data->{'day'} = DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%d");
$data->{'week'} = DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%a");
$data->{'weekfull'} = DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%A");
$data->{'month'} = DateTime->from_epoch(
locale => $locale_name_loaded, epoch => $secs,
time_zone => $tz)->strftime("%b");
$data->{'complete'} =~ s/(\d+):(\d+):(\d+)(.*?)/$1:$2$4/;
($data->{'complete_short'} = $data->{'complete'}) =~ s/(.*?)([\s\,]*\Q$data->{'year'}\E.*)/$1/;
($data->{'complete_short'} = $data->{'complete'}) =~
s/(.*?)([\s\,]*\Q$data->{'year'}\E.*)/$1/;
if ($opts->{'get'}) {
return $data->{$opts->{'get'}};
@@ -7969,6 +8010,100 @@ return ref($s) && $s->{'host'} && $s->{'port'} ?
ref($s) ? "" : "$s.$$";
}
=head2 verify_session_id(session-id, [sessiondb], [miniserv])
Returns the username (or an array with user, last login, and IP address) for the
given session (or session hash) ID, or undefined if session ID is invalid.
Args:
$sid: The session ID to verify
$sessiondb: A reference to the session database hash (optional)
$miniserv: A reference to the miniserv configuration hash (optional)
Returns:
In list context: A list containing the user, last activity time, and IP
address associated with the session ID.
In scalar context: The user associated with the session ID.
If the session ID is not found, returns an empty list in list context
or undef in scalar context.
Usage:
Retrieve the username associated with the session ID or undef if
session is invalid.
my $user = verify_session_id($main::session_id);
Example of return:
root
Retrieve array containing the user, last login, and IP address
for given session hash ID or undef if session is invalid.
my (@user) = verify_session_id('BSRxr6wpF25lqeRinQ/sv0');
Example of return:
[
'root',
'1719401071',
'10.211.55.2'
]
Retrieve the username associated with the session ID, using given
session DB or undef if session is invalid.
my %sessiondb;
dbmopen(%sessiondb, "$var_directory/sessiondb", 0400);
my $user = verify_session_id($main::session_id, \%sessiondb);
dbmclose(%sessiondb);
Example of return:
someuser1
=cut
sub verify_session_id
{
my ($sid, $sessiondb, $miniserv) = @_;
my $hashsessionidfunc = \&miniserv::hash_session_id;
my %miniserv;
if ($miniserv) {
# Use provided miniserv configuration
%miniserv = %{$miniserv};
}
else {
# Load miniserv configuration if not provided
&get_miniserv_config(\%miniserv);
}
my %sessiondb_;
if ($sessiondb) {
# Use provided session database
%sessiondb_ = %{$sessiondb};
}
elsif (&foreign_available('acl')) {
# Use session database using ACL module API
&foreign_require("acl");
&acl::open_session_db(\%miniserv);
$hashsessionidfunc = \&acl::hash_session_id;
%sessiondb_ = %acl::sessiondb;
}
else {
return wantarray ? ( ) : undef;
}
# Verify given session (hash) ID against the session database
foreach my $k (grep { $sessiondb_{$_} } keys %sessiondb_) {
if ($k eq $sid ||
(defined($hashsessionidfunc) && $k eq $hashsessionidfunc->($sid))) {
my ($user, $last, $ip) = split(/\s+/, $sessiondb_{$k});
return wantarray ? ($user, $last, $ip) : $user;
}
}
# Return an empty list or undef if session ID is not found
return wantarray ? ( ) : undef;
}
=head2 remote_foreign_require(server, module, file)
Connects to rpc.cgi on a remote webmin server and have it open a session
@@ -13522,6 +13657,149 @@ my $dir = $var_directory."/locks/".$$;
return $dir;
}
# allocate_miniserv_websocket([module])
# Allocate a new websocket and
# stores it miniserv.conf file
sub allocate_miniserv_websocket
{
my ($module) = @_;
$module ||= $module_name;
# Find ports already in use
&lock_file(&get_miniserv_config_file());
my %miniserv;
&get_miniserv_config(\%miniserv);
my %inuse;
foreach my $k (keys %miniserv) {
if ($k =~ /^websockets_/ && $miniserv{$k} =~ /port=(\d+)/) {
$inuse{$1} = 1;
}
}
# Pick a port and configure Webmin to proxy it
my $port = $miniserv{'websocket_base_port'} || 555;
while(1) {
if (!$inuse{$port}) {
&open_socket("127.0.0.1", $port, my $fh, \$err);
last if ($err);
close($fh);
}
$port++;
}
my $wspath = "/$module/ws-".$port;
my $now = time();
$miniserv{'websockets_'.$wspath} = "host=127.0.0.1 port=$port wspath=/ user=$remote_user time=$now";
&put_miniserv_config(\%miniserv);
&unlock_file(&get_miniserv_config_file());
&reload_miniserv();
return $port;
}
# get_miniserv_websocket_url(port, [host], [module])
# Returns the URL for a websocket
sub get_miniserv_websocket_url
{
my ($port, $host, $module) = @_;
$module ||= $module_name;
my $ws_proto = lc($ENV{'HTTPS'}) eq 'on' ? 'wss' : 'ws';
my %miniserv;
&get_miniserv_config(\%miniserv);
my $http_host_conf = &trim($miniserv{'websocket_host'} || $host);
# Pass as defined
if ($http_host_conf) {
if ($http_host_conf !~ /^wss?:\/\//) {
$http_host_conf = "$ws_proto://$http_host_conf";
}
$http_host_conf =~ s/[\/]+$//g;
}
# Try to rely on the proxy
if (!defined($http_host_conf)) {
my $forwarded_host = $ENV{'HTTP_X_FORWARDED_HOST'};
if ($forwarded_host) {
$http_host_conf = "$ws_proto://$forwarded_host".
&get_webprefix();
$http_host_conf =~ s/\/$//;
}
}
my $http_host = $http_host_conf || "$ws_proto://$ENV{'HTTP_HOST'}";
return "$http_host/$module/ws-$port";
}
# remove_miniserv_websocket(port, [module])
# Remove old websocket from miniserv.conf
sub remove_miniserv_websocket
{
my ($port, $module) = @_;
$module ||= $module_name;
my %miniserv;
if ($port) {
&lock_file(&get_miniserv_config_file());
&get_miniserv_config(\%miniserv);
my $wspath = "/$module/ws-".$port;
if ($miniserv{'websockets_'.$wspath}) {
delete($miniserv{'websockets_'.$wspath});
&put_miniserv_config(\%miniserv);
&reload_miniserv();
}
&unlock_file(&get_miniserv_config_file());
}
}
# cleanup_miniserv_websockets([&skip-ports], [module])
# Called by scheduled status collection to remove any
# websockets in miniserv.conf that are no longer used
sub cleanup_miniserv_websockets
{
my ($skip, $module) = @_;
$skip ||= [ ];
$module ||= $module_name;
&lock_file(&get_miniserv_config_file());
my %miniserv;
&get_miniserv_config(\%miniserv);
my $now = time();
my @clean;
foreach my $k (keys %miniserv) {
$k =~ /^websockets_\/$module\/ws-(\d+)$/ || next;
my $port = $1;
next if (&indexof($port, @$skip) >= 0);
my $when = 0;
if ($miniserv{$k} =~ /time=(\d+)/) {
$when = $1;
}
if ($now - $when > 60) {
# Has been open for a while, check if the port is still in use?
my $err;
&open_socket("127.0.0.1", $port, my $fh, \$err);
if ($err) {
# Closed now, can clean up
push(@clean, $k);
}
else {
# Still active
close($fh);
}
}
}
if (@clean) {
foreach my $k (@clean) {
delete($miniserv{$k});
}
&put_miniserv_config(\%miniserv);
&reload_miniserv();
}
&unlock_file(&get_miniserv_config_file());
}
# get_miniserv_websockets_modules()
# Returns a list of modules and themes that use websockets
sub get_miniserv_websockets_modules
{
my @rv;
foreach my $i (&get_all_module_infos(), &list_themes()) {
push(@rv, $i->{'dir'}) if ($i->{'websockets'});
}
return @rv;
}
$done_web_lib_funcs = 1;
1;

View File

@@ -84,6 +84,18 @@ else {
$miniserv{'bind'} = $first->[0];
}
$miniserv{'sockets'} = join(" ", map { "$_->[0]:$_->[1]" } @sockets);
if ($in{'websocket_base_port_def'}) {
delete($miniserv{'websocket_base_port'});
}
else {
$miniserv{'websocket_base_port'} = $in{'websocket_base_port'};
}
if ($in{'websocket_host_def'}) {
delete($miniserv{'websocket_host'});
}
else {
$miniserv{'websocket_host'} = $in{'websocket_host'};
}
$miniserv{'ipv6'} = $in{'ipv6'};
if ($in{'listen_def'}) {
delete($miniserv{'listen'});

View File

@@ -44,6 +44,22 @@ if (&foreign_check("firewall")) {
}
print &ui_table_row($text{'bind_sockets'}, $stable);
# WebSocket based port
print &ui_table_row($text{'bind_websocport'},
&ui_radio("websocket_base_port_def",
$miniserv{"websocket_base_port"} ? 0 : 1,
[ [ 1, $text{'bind_websocport_none'} ],
[ 0, &ui_textbox("websocket_base_port",
$miniserv{"websocket_base_port"}, 6) ] ]));
# Hostname for WebSocket connections
print &ui_table_row($text{'bind_websoc_host'},
&ui_radio("websocket_host_def",
$miniserv{"websocket_host"} ? 0 : 1,
[ [ 1, $text{'bind_websoc_host_auto'} ],
[ 0, &ui_textbox("websocket_host",
$miniserv{"websocket_host"}, 25) ] ]));
# IPv6 enabled?
print &ui_table_row($text{'bind_ipv6'},
&ui_yesno_radio("ipv6", $miniserv{'ipv6'}));

View File

@@ -277,7 +277,9 @@ else {
my @doms = $config{'letsencrypt_doms'} ?
split(/\s+/, $config{'letsencrypt_doms'}) : ( $host );
print &ui_table_row($text{'ssl_letsdoms'},
&ui_textarea("dom", join("\n", @doms), 5, 40));
&ui_textarea("dom", join("\n", @doms), 5, 40)."<br>\n".
&ui_checkbox("subset", 1, $text{'ssl_subset'},
$config{'letsencrypt_subset'}));
# Apache vhost or other path
my @opts;

View File

@@ -46,6 +46,10 @@ bind_sport0=Same as first
bind_sport1=Specific port ..
bind_listen=Listen for broadcasts on UDP port
bind_none=Don't listen
bind_websocport=Base port number for WebSockets connections
bind_websocport_none=Default (555)
bind_websoc_host=Hostname for WebSocket connections
bind_websoc_host_auto=Automatic
bind_hostname=Web server hostname
bind_auto=Work out from browser
bind_err=Failed to change address
@@ -420,6 +424,7 @@ ssl_letserr2=Alternately, check the <a href='$1'>module configuration</a> page t
ssl_letsdesc2=This page can be used to request a new certificate, which will overwrite any other currently have configured in Webmin. However, the Let's Encrypt service requires that your ownership of the certificate domain be validated by checking that this system hosts the website for the domain. This is done by placing a small temporary file in the website's document directory.
ssl_letsheader=Options for new SSL certificate
ssl_letsdoms=Hostnames for certificate
ssl_subset=Skip unverifiable hostnames?
ssl_letsmode=Let's Encrypt validation method
ssl_letsmode0=Apache virtual host matching hostname
ssl_letsmode1=Selected Apache virtual host

View File

@@ -58,14 +58,15 @@ return &software::missing_install_link(
# request_letsencrypt_cert(domain|&domains, webroot, [email], [keysize],
# [request-mode], [use-staging], [account-email],
# [reuse-key], [server-url, server-key, server-hmac])
# [reuse-key], [server-url, server-key, server-hmac],
# [allow-subset])
# Attempt to request a cert using a generated key with the Let's Encrypt client
# command, and write it to the given path. Returns a status flag, and either
# an error message or the paths to cert, key and chain files.
sub request_letsencrypt_cert
{
my ($dom, $webroot, $email, $size, $mode, $staging, $account_email,
$key_type, $reuse_key, $server, $server_key, $server_hmac) = @_;
$key_type, $reuse_key, $server, $server_key, $server_hmac, $subset) = @_;
my @doms = ref($dom) ? @$dom : ($dom);
$email ||= "root\@$doms[0]";
$mode ||= "web";
@@ -179,6 +180,7 @@ if ($letsencrypt_cmd) {
my $new_flags = "";
my $reuse_flags = "";
my $server_flags = "";
my $subset_flags = "";
$key_type ||= $config{'letsencrypt_algo'} || 'rsa';
if (&compare_version_numbers($cmd_ver, 1.11) < 0) {
$old_flags = " --manual-public-ip-logging-ok";
@@ -192,6 +194,9 @@ if ($letsencrypt_cmd) {
else {
$reuse_flags = " --no-reuse-key";
}
if ($subset) {
$subset_flags = " --allow-subset-of-names";
}
$reuse_flags = "" if ($reuse_key && $reuse_key == -1);
if ($server) {
$server_flags = " --server ".quotemeta($server);
@@ -227,6 +232,7 @@ if ($letsencrypt_cmd) {
$old_flags.
$server_flags.
$new_flags.
$subset_flags.
" 2>&1)");
&reset_environment();
}
@@ -245,6 +251,7 @@ if ($letsencrypt_cmd) {
$old_flags.
$server_flags.
$new_flags.
$subset_flags.
" 2>&1)");
&reset_environment();
}
@@ -260,6 +267,7 @@ if ($letsencrypt_cmd) {
$old_flags.
$server_flags.
$new_flags.
$subset_flags.
" 2>&1)");
&reset_environment();
}

View File

@@ -76,7 +76,7 @@ else {
if ($in{'save'}) {
# Just update renewal
&save_renewal_only(\@doms, $webroot, $mode);
&save_renewal_only(\@doms, $webroot, $mode, $size, $in{'subset'});
&redirect("edit_ssl.cgi");
}
else {
@@ -88,7 +88,9 @@ else {
'letsencrypt_doing',
"<tt>".&html_escape(join(", ", @doms))."</tt>",
"<tt>".&html_escape($webroot)."</tt>"),"<p>\n";
my ($ok, $cert, $key, $chain) = &request_letsencrypt_cert(\@doms, $webroot, undef, $size, $mode, $in{'staging'});
my ($ok, $cert, $key, $chain) = &request_letsencrypt_cert(
\@doms, $webroot, undef, $size, $mode, $in{'staging'},
undef, 0, undef, undef, undef, $in{'subset'});
if (!$ok) {
print &text('letsencrypt_failed', $cert),"<p>\n";
}
@@ -148,15 +150,16 @@ else {
&ui_print_footer("", $text{'index_return'});
}
# save_renewal_only(&doms, webroot, mode)
# save_renewal_only(&doms, webroot, mode, size, subset-mode)
# Save for future renewals
sub save_renewal_only
{
my ($doms, $webroot, $mode) = @_;
my ($doms, $webroot, $mode, $size, $subset) = @_;
$config{'letsencrypt_doms'} = join(" ", @$doms);
$config{'letsencrypt_webroot'} = $webroot;
$config{'letsencrypt_mode'} = $mode;
$config{'letsencrypt_size'} = $size;
$config{'letsencrypt_subset'} = $subset;
&save_module_config();
if (&foreign_check("webmincron")) {
my $job = &find_letsencrypt_cron_job();

View File

@@ -128,7 +128,7 @@ if (!$eol_data->{'_eol_timestamp'}) {
&error_stderr("The provided data is not a valid EOL data hash reference");
return undef;
}
my $eol_date = &make_date($eol_data->{'_eol_timestamp'}, { '_' => 1 });
my $eol_date = &make_date($eol_data->{'_eol_timestamp'}, { });
if (ref($eol_date)) {
my $eol_in = sub {
my $eol_date = shift;

View File

@@ -6,6 +6,7 @@ Functions for creating and listing Webmin scheduled functions.
BEGIN { push(@INC, ".."); };
use WebminCore;
use feature 'state';
&init_config();
$webmin_crons_directory = "$module_config_directory/crons";
@@ -72,11 +73,12 @@ Create or update a webmin cron function. Also locks the file being written to.
sub save_webmin_cron
{
my ($cron) = @_;
state $cnt = 0;
if (!-d $webmin_crons_directory) {
&make_dir($webmin_crons_directory, 0700);
}
if (!$cron->{'id'}) {
$cron->{'id'} = time().$$;
$cron->{'id'} = time().$$.($cnt++);
}
my $file = "$webmin_crons_directory/$cron->{'id'}.cron";
my %wcron = %$cron;

View File

@@ -10,6 +10,10 @@ my ($o) = @_;
print &ui_table_row($text{'acl_user'},
&ui_opt_textbox("user", $o->{'user'} eq '*' ? undef : $o->{'user'},
20, $text{'acl_sameuser'}));
print &ui_table_row($text{'acl_sudoenforce'},
&ui_yesno_radio("sudoenforce",
$o->{'sudoenforce'} == 1 ? 1 : 0));
}
sub acl_security_save
@@ -17,4 +21,5 @@ sub acl_security_save
my ($o) = @_;
$o->{'user'} = $in{'user_def'} ? '*' : $in{'user'};
$o->{'sudoenforce'} = $in{'sudoenforce'} ? 1 : 0;
}

View File

@@ -1,6 +1,4 @@
xterm=Set <tt>TERM</tt> environmental variable to,4,xterm+256color-xterm&#45;256color,xterm+16color-xterm&#45;16color,xterm-xterm,vt102-vt102,vt100-vt100,vt52-vt52,rxvt-rxvt,nsterm-nsterm,dtterm-dtterm,ansi-ansi
base_port=Base port number for WebSockets connections,0,5
host=Hostname for WebSocket connections,3,Automatic,32,,,Manual
size=Terminal width and height in characters,3,Automatic,5,,,Static (80x24)
locale=Set shell character encoding,10,0-Shell default,1-<tt>en_US.UTF&#45;8</tt>,Custom
rcfile=Execute initialization commands from file,10,0-Shell default,1-Module default,Custom

View File

@@ -1 +1,2 @@
user=root
sudoenforce=1

View File

@@ -168,13 +168,21 @@ EOF
print "<div data-label=\"$text{'index_connecting'}\" id=\"terminal\"></div>\n";
# Get a free port that can be used for the socket
my $port = &allocate_miniserv_websocket();
my $port = &allocate_miniserv_websocket($module_name);
# Check permissions for user to run as
my $user = $access{'user'};
if ($user eq "*") {
$user = $remote_user;
}
elsif ($user eq "root" && $remote_user ne $user && !$in{'user'} &&
$access{'sudoenforce'} ne '0') {
# If possible, start with a sudo-capable user
my @uinfo = getpwnam($remote_user);
if (@uinfo && $uinfo[7]) {
$user = $remote_user;
}
}
$user = $config{'user'} if ($user eq 'root' && $config{'user'});
# Switch to given user
@@ -194,23 +202,13 @@ my $shellserver_cmd = "$module_config_directory/shellserver.pl";
if (!-r $shellserver_cmd) {
&create_wrapper($shellserver_cmd, $module_name, "shellserver.pl");
}
my $tmpdir = &tempname_dir();
$ENV{'SESSION_ID'} = $main::session_id;
&system_logged($shellserver_cmd." ".quotemeta($port)." ".quotemeta($user).
($dir ? " ".quotemeta($dir) : "").
" >$tmpdir/ws-$port.out 2>&1 </dev/null");
" >$module_var_directory/websocket-connection-$port.out 2>&1 </dev/null");
# Open the terminal
my $ws_proto = lc($ENV{'HTTPS'}) eq 'on' ? 'wss' : 'ws';
my $http_host_conf = &trim($config{'host'});
if ($http_host_conf) {
if ($http_host_conf !~ /^wss?:\/\//) {
$http_host_conf = "$ws_proto://$http_host_conf";
}
$http_host_conf =~ s/[\/]+$//g;
}
my $http_host = $http_host_conf || "$ws_proto://$ENV{'HTTP_HOST'}";
my $url = "$http_host/$module_name/ws-$port";
my $url = &get_miniserv_websocket_url($port, $config{'host'}, $module_name);
my $canvasAddon = $termlinks->{'js'}[3];
my $webGLAddon = $termlinks->{'js'}[4];
my $term_script = <<EOF;

View File

@@ -9,3 +9,4 @@ index_eproxy=The Terminal module cannot be used when accessing Webmin via anothe
acl_user=Run shell as Unix user
acl_sameuser=Same as Webmin login
acl_sudoenforce=Enforce <em>sudo</em>-only privileges

View File

@@ -1,3 +1,4 @@
desc=Terminal
name=xterm
longdesc=Access the shell on your system without the need for a separate SSH client, using Xterm.js over Webmin WebSockets
websockets=1

View File

@@ -14,7 +14,7 @@ my @uinfo = getpwnam($user);
my ($uid, $gid);
if ($user ne "root" && !$<) {
if (!@uinfo) {
&remove_miniserv_websocket($port);
&remove_miniserv_websocket($port, $module_name);
die "User $user does not exist!";
}
$uid = $uinfo[2];
@@ -83,11 +83,11 @@ my ($shellfh, $pid) = &proc::pty_process_exec($shellexec, $uid, $gid, $shelllogi
&reset_environment();
my $shcmd = "'$shellexec".($shelllogin ? " $shelllogin" : "")."'";
if (!$pid) {
&remove_miniserv_websocket($port);
&remove_miniserv_websocket($port, $module_name);
die "Failed to run shell with $shcmd\n";
}
else {
print STDERR "Running shell $shcmd with pid $pid\n";
&error_stderr("Running shell $shcmd for user $user with pid $pid");
}
# Detach from controlling terminal
@@ -99,19 +99,19 @@ close(STDIN);
# Clean up when socket is terminated
$SIG{'ALRM'} = sub {
&remove_miniserv_websocket($port);
&remove_miniserv_websocket($port, $module_name);
die "timeout waiting for connection";
};
alarm(60);
print STDERR "listening on port $port\n";
&error_stderr("Listening on port $port");
my ($wsconn, $shellbuf);
Net::WebSocket::Server->new(
listen => $port,
on_connect => sub {
my ($serv, $conn) = @_;
print STDERR "got websockets connection\n";
&error_stderr("WebSocket connection established");
if ($wsconn) {
print STDERR "Unexpected second connection to the same port\n";
&error_stderr("Unexpected second connection to the same port");
$conn->disconnect();
return;
}
@@ -126,7 +126,7 @@ Net::WebSocket::Server->new(
$key =~ s/\s//g;
$dsess =~ s/\s//g;
if (!$key || !$dsess || $key ne $dsess) {
print STDERR "Key $key does not match session ID $dsess\n";
&error_stderr("Key $key does not match session ID $dsess");
$conn->disconnect();
}
},
@@ -140,7 +140,7 @@ Net::WebSocket::Server->new(
# Check for resize escape sequence explicitly
if ($msg =~ /^\\033\[8;\((\d+)\);\((\d+)\)t$/) {
my ($rows, $cols) = ($1, $2);
print STDERR "got resize to $rows $cols\n";
&error_stderr("Got resize to $rows $cols");
eval {
$shellfh->set_winsize($rows, $cols);
};
@@ -153,14 +153,14 @@ Net::WebSocket::Server->new(
return;
}
if (!syswrite($shellfh, $msg, length($msg))) {
print STDERR "write to shell failed : $!\n";
&remove_miniserv_websocket($port);
&error_stderr("Write to shell failed : $!");
&remove_miniserv_websocket($port, $module_name);
exit(1);
}
},
disconnect => sub {
print STDERR "websocket connection closed\n";
&remove_miniserv_websocket($port);
&error_stderr("WebSocket connection closed");
&remove_miniserv_websocket($port, $module_name);
kill('KILL', $pid) if ($pid);
exit(0);
}
@@ -172,8 +172,8 @@ Net::WebSocket::Server->new(
my $buf;
my $ok = sysread($shellfh, $buf, 1024);
if ($ok <= 0) {
print STDERR "end of output from shell\n";
&remove_miniserv_websocket($port);
&error_stderr("End of output from shell");
&remove_miniserv_websocket($port, $module_name);
exit(0);
}
if ($wsconn) {
@@ -185,6 +185,6 @@ Net::WebSocket::Server->new(
},
],
)->start;
print STDERR "exited websockets server\n";
&remove_miniserv_websocket($port);
&cleanup_miniserv_websockets([$port]);
&error_stderr("Exited WebSocket server");
&remove_miniserv_websocket($port, $module_name);
&cleanup_miniserv_websockets([$port], $module_name);

View File

@@ -1,100 +0,0 @@
# allocate_miniserv_websocket()
# Allocate a new websocket and
# stores it miniserv.conf file
sub allocate_miniserv_websocket
{
# Find ports already in use
&lock_file(&get_miniserv_config_file());
my %miniserv;
&get_miniserv_config(\%miniserv);
my %inuse;
foreach my $k (keys %miniserv) {
if ($k =~ /^websockets_/ && $miniserv{$k} =~ /port=(\d+)/) {
$inuse{$1} = 1;
}
}
# Pick a port and configure Webmin to proxy it
my $port = $config{'base_port'} || 555;
while(1) {
if (!$inuse{$port}) {
&open_socket("127.0.0.1", $port, my $fh, \$err);
last if ($err);
close($fh);
}
$port++;
}
my $wspath = "/$module_name/ws-".$port;
my $now = time();
$miniserv{'websockets_'.$wspath} = "host=127.0.0.1 port=$port wspath=/ user=$remote_user time=$now";
&put_miniserv_config(\%miniserv);
&unlock_file(&get_miniserv_config_file());
&reload_miniserv();
return $port;
}
# remove_miniserv_websocket(port)
# Remove old websocket
# from miniserv.conf
sub remove_miniserv_websocket
{
my ($port) = @_;
my %miniserv;
if ($port) {
&lock_file(&get_miniserv_config_file());
&get_miniserv_config(\%miniserv);
my $wspath = "/$module_name/ws-".$port;
if ($miniserv{'websockets_'.$wspath}) {
delete($miniserv{'websockets_'.$wspath});
&put_miniserv_config(\%miniserv);
&reload_miniserv();
}
&unlock_file(&get_miniserv_config_file());
}
}
# cleanup_miniserv_websockets([&skip-ports])
# Called by scheduled status collection to remove any
# websockets in miniserv.conf that are no longer used
sub cleanup_miniserv_websockets
{
my ($skip) = @_;
$skip ||= [ ];
&lock_file(&get_miniserv_config_file());
my %miniserv;
&get_miniserv_config(\%miniserv);
my $now = time();
my @clean;
foreach my $k (keys %miniserv) {
$k =~ /^websockets_\/$module_name\/ws-(\d+)$/ || next;
my $port = $1;
next if (&indexof($port, @$skip) >= 0);
my $when = 0;
if ($miniserv{$k} =~ /time=(\d+)/) {
$when = $1;
}
if ($now - $when > 60) {
# Has been open for a while, check if the port is still in use?
my $err;
&open_socket("127.0.0.1", $port, my $fh, \$err);
if ($err) {
# Closed now, can clean up
push(@clean, $k);
}
else {
# Still active
close($fh);
}
}
}
if (@clean) {
foreach my $k (@clean) {
delete($miniserv{$k});
}
&put_miniserv_config(\%miniserv);
&reload_miniserv();
}
&unlock_file(&get_miniserv_config_file());
}
1;

View File

@@ -4,7 +4,6 @@ BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
our %access = &get_module_acl();
do "$module_root_directory/websockets-lib-funcs.pl";
# config_pre_load(mod-info-ref, [mod-order-ref])
# Check if some config options are conditional,
@@ -12,22 +11,13 @@ do "$module_root_directory/websockets-lib-funcs.pl";
sub config_pre_load
{
my ($modconf_info, $modconf_order) = @_;
if ($ENV{'HTTP_X_REQUESTED_WITH'} eq "XMLHttpRequest") {
# Process forbidden keys
my @forbidden_keys;
# Size is not supported in Authentic, because resize works flawlessly and
# making it work would just add addition complexity for no good reason
push(@forbidden_keys, 'size');
# Remove forbidden from display
foreach my $fkey (@forbidden_keys) {
delete($modconf_info->{$fkey});
@{$modconf_order} = grep { $_ ne $fkey } @{$modconf_order}
if ($modconf_order);
}
# Size is not supported in Authentic, because resize works flawlessly
# and making it work would just add addition complexity for no good
# reason
delete($modconf_info->{'size'});
@{$modconf_order} = grep { $_ ne 'size' } @{$modconf_order}
if ($modconf_order);
}
}