Compare commits

...

76 Commits

Author SHA1 Message Date
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
0256ee47f2 Fix block margin for perfect alignment 2024-06-09 16:16:52 +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
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
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
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
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
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
36 changed files with 1332 additions and 323 deletions

File diff suppressed because one or more lines are too long

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,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

@@ -4240,4 +4240,548 @@ 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) {
# 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;
}
.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: 21px;
color: #1d72ff;
text-align: center;
padding: 2px 8px;
}
.calendar-day {
font-size: 24px;
text-align: center;
padding: 4px 8px;
}
.calendar-week {
font-size: 16px;
border-top: 1px dotted #999999aa;
padding: 6px;
display: inline-block;
}
.calendar-details h2 {
margin: 0;
font-size: 18px;
}
.calendar-details p {
margin: 4px 0;
}
.calendar-details .title {
font-size: 20px;
}
.calendar-details .detail strong {
opacity: 0.66;
white-space: nowrap;
}
.calendar-details .detail + .desc p:first-child {
margin-top: 0;
}
details.calendar-details {
font-size: 90%;
display: inline-block;
margin-left: 9px;
}
details.calendar-details summary {
cursor: help;
}
details.calendar-details tr:has(>.detail+td:empty),
.calendar-details tr:has(>.detail+td:empty) {
display: none;
}
</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.

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

@@ -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

@@ -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

@@ -13522,6 +13522,139 @@ 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);
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'}";
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

@@ -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

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

@@ -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

@@ -194,23 +194,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'});
my $canvasAddon = $termlinks->{'js'}[3];
my $webGLAddon = $termlinks->{'js'}[4];
my $term_script = <<EOF;

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

@@ -87,7 +87,7 @@ if (!$pid) {
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
@@ -103,15 +103,15 @@ $SIG{'ALRM'} = sub {
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,13 +153,13 @@ Net::WebSocket::Server->new(
return;
}
if (!syswrite($shellfh, $msg, length($msg))) {
print STDERR "write to shell failed : $!\n";
&error_stderr("Write to shell failed : $!");
&remove_miniserv_websocket($port);
exit(1);
}
},
disconnect => sub {
print STDERR "websocket connection closed\n";
&error_stderr("WebSocket connection closed");
&remove_miniserv_websocket($port);
kill('KILL', $pid) if ($pid);
exit(0);
@@ -172,7 +172,7 @@ Net::WebSocket::Server->new(
my $buf;
my $ok = sysread($shellfh, $buf, 1024);
if ($ok <= 0) {
print STDERR "end of output from shell\n";
&error_stderr("End of output from shell");
&remove_miniserv_websocket($port);
exit(0);
}
@@ -185,6 +185,6 @@ Net::WebSocket::Server->new(
},
],
)->start;
print STDERR "exited websockets server\n";
&error_stderr("Exited WebSocket server");
&remove_miniserv_websocket($port);
&cleanup_miniserv_websockets([$port]);

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);
}
}