From 1394b3ba6c36e4a638d96df10ef2054f3f34ec63 Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Thu, 21 May 2026 21:56:28 +0200 Subject: [PATCH] feat: add web login, logout and runtime logging improvements --- apply.pl | 3 + mars-nwe-webui.service.cmake | 2 + settings.pl | 47 ++-- smart.cmake | 450 ++++++++++++++++++++++++++++++++--- smart.conf.cmake | 4 + static/menu.html | 29 ++- 6 files changed, 478 insertions(+), 57 deletions(-) diff --git a/apply.pl b/apply.pl index 8590a55..4eeb4d7 100644 --- a/apply.pl +++ b/apply.pl @@ -271,6 +271,7 @@ EOF } redirect( '/settings/users' ); + return( 1 ); } elsif( $c[1] eq 'groups' ) { @@ -324,6 +325,7 @@ EOF } redirect( '/settings/groups' ); + return( 1 ); } elsif( $c[1] eq 'queues' ) { @@ -393,6 +395,7 @@ EOF } redirect( '/settings/queues' ); + return( 1 ); } elsif( $c[1] eq 'advanced' ) diff --git a/mars-nwe-webui.service.cmake b/mars-nwe-webui.service.cmake index e86d211..7bcedf6 100644 --- a/mars-nwe-webui.service.cmake +++ b/mars-nwe-webui.service.cmake @@ -8,6 +8,8 @@ Documentation=man:systemd.service(5) Type=simple User=root Group=root +RuntimeDirectory=mars-nwe-webui +RuntimeDirectoryMode=0700 WorkingDirectory=/ ExecStartPre=/bin/mkdir -p @MARS_NWE_LOG_DIR@ diff --git a/settings.pl b/settings.pl index 4cc511c..303badd 100644 --- a/settings.pl +++ b/settings.pl @@ -40,7 +40,8 @@ sub settings_nav_bar() return <<'EOF_NAV';
Back - Main menu + Main menu + Logout
EOF_NAV } @@ -708,40 +709,44 @@ sub unix_user_defaults_from_query() my $want = ''; - # Preferred import route: - # /settings/users_import/ - if( defined( $c[1] ) && $c[1] eq 'users_import' && defined( $c[2] ) && $c[2] ne '' ) + # The router puts imported users here after /settings/users_import/ + # was normalized to /settings/users/add_new. + if( defined( $p{unix_user} ) && $p{unix_user} ne '' ) + { + $want = $p{unix_user}; + } + # Compatibility for direct /settings/users_import/, before normalization. + elsif( defined( $c[1] ) && $c[1] eq 'users_import' && defined( $c[2] ) && $c[2] ne '' ) { $want = $c[2]; } - # Compatibility route: - # /settings/users/add_new/ + # Compatibility for /settings/users/add_new/. elsif( defined( $c[3] ) && $c[3] ne '' ) { $want = $c[3]; } - elsif( defined( $p{unix_user} ) && $p{unix_user} ne '' ) - { - $want = $p{unix_user}; - } if( $want ne '' ) { $want =~ s/[^-_\.A-Za-z0-9]//g; - foreach my $u ( unix_userlist() ) - { - next if ! defined( $u->{name} ) || $u->{name} ne $want; + # Minimal import: use the Unix user parameter that reaches settings.pl. + $defaults{unix_user} = $want; - my $gecos = defined( $u->{gecos} ) ? $u->{gecos} : ''; - $gecos =~ s/,.*$//; + $defaults{name} = uc( $want ); + $defaults{name} =~ s/[^-_\.A-Za-z0-9]//g; - $defaults{name} = uc( $u->{name} ); - $defaults{name} =~ s/[^-_\.A-Za-z0-9]//g; - $defaults{unix_user} = $u->{name}; - $defaults{fullname} = $gecos; - last; - } + # Step 2: if no GECOS/fullname is available yet, use a friendly + # fallback from the Unix account name. "mario" -> "Mario", + # "mario_fetka" -> "Mario Fetka". + my $full = $want; + $full =~ s/[_\.\-]+/ /g; + $full =~ s/\s+/ /g; + $full =~ s/^\s+//; + $full =~ s/\s+$//; + $full = join( ' ', map { ucfirst( lc( $_ ) ) } split( / /, $full ) ); + + $defaults{fullname} = $full; } return %defaults; diff --git a/smart.cmake b/smart.cmake index 8846ffe..f821722 100644 --- a/smart.cmake +++ b/smart.cmake @@ -31,7 +31,21 @@ do( '@MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf' ) or die "Could not load @MARS_NWE_INSTALL_FULL_CONFDIR@/smart.conf: $@ $!"; close( STDERR ); -open( STDERR, '>>' . $smart_log_path ) + +# Prefix all raw STDERR from helper tools with timestamp/component before it +# reaches smart.log. This also catches output from nwbols/nwbpset/nwpasswd +# and systemctl warnings. +my $smart_stderr_filter = "perl -MPOSIX=strftime -ne 'chomp; " . + "my \\$v=\\$ENV{SMART_VERSION}||q{0.99.pl28}; " . + "my \\$f=\\$ENV{SMART_LOG_FILE}||q{stderr}; " . + "print strftime(q{[%Y-%m-%d %H:%M:%S]}, localtime), qq{ [ERROR] [SMArT \\$v] [\\$f] \\$_\\n};' >> " . + quotemeta( $smart_log_path ); + +$ENV{SMART_VERSION} = defined( $smart_version ) && $smart_version ne '' ? $smart_version : '0.99.pl28'; +$ENV{SMART_LOG_FILE} = 'stderr'; + +open( STDERR, '|-', $smart_stderr_filter ) + or open( STDERR, '>>' . $smart_log_path ) or die "Could not open $smart_log_path: $!"; $ENV{HOME} = '@MARS_NWE_INSTALL_FULL_CONFDIR@'; @@ -45,11 +59,14 @@ $smart_systemctl_path = '@SYSTEMCTL_EXECUTABLE@' unless defined $smart_systemctl $l = ; $l =~ s/[\n\r]//g; $request_uri = ""; +$post_body = ""; +%hl = (); + @c = split( ' ', $l ); if( scalar( @c ) > 2 ) { $request_uri = $c[1]; - while( keys( %h ) < 15 ) # Who would ever want to send more headers??? + while( keys( %h ) < 50 ) { $l = ; $l =~ s/[\n\r]//g; @@ -59,36 +76,32 @@ if( scalar( @c ) > 2 ) $n =~ s/:[^:]*$//g; $v = $l; $v =~ s/^[^:]*://g; + $v =~ s/^\s+//; + $v =~ s/\s+$//; $h{$n} = $v; + $hl{lc( $n )} = $v; } } $c[0] = uc( $c[0] ); +$request_method = $c[0]; -if( $h{Authorization} eq '' ) - { error( 401 ); } -else +if( $request_method eq 'POST' ) { - @s = split( ' ', $h{Authorization} ); - if( $s[0] ne 'Basic' or length( $h{Authorization} ) > 80 ) # We can't be too careful, can we... - { error( 401 ); } - else + my $content_length = 0; + + if( defined( $hl{'content-length'} ) && $hl{'content-length'} =~ /^[0-9]+$/ ) { - $s[1] =~ tr#A-Za-z0-9+/##cd; - $s[1] =~ tr#A-Za-z0-9+/# -_#; - $s[1] = pack( 'c', 32 + 0.75 * length( $s[1] ) ) . $s[1]; - $s[1] = unpack( 'u', $s[1] ); - $s[1] =~ s/[\r\n]//g; - @l = split( ':', $s[1] ); - if( $l[0] ne 'root' ) - { error( 401 ); } - else - { if( $x = system( $smart_check_login, @l ) ) - { error( 401 ); } } + $content_length = int( $hl{'content-length'} ); + } + + if( $content_length > 0 && $content_length < 8192 ) + { + read( STDIN, $post_body, $content_length ); } } -if( $c[0] ne 'GET' ) +if( $request_method ne 'GET' && $request_method ne 'POST' ) { error( 501 ); } @@ -97,19 +110,39 @@ if( $c[0] ne 'GET' ) $cc = $c[1]; $cc =~ s/[^\?]*\?//; $c = substr( shift( @p ), 1 ); -@p = split( '&', $p[0] ); -foreach $p ( @p ) -{ - $n = $p; - $n =~ s/=.*//; - $v = $p; - $v =~ s/.*=//; - $v =~ s/\+/ /g; - $v =~ s/%([0-9A-F][0-9A-F])/pack('c',hex($1))/gie; - $p{$n} = $v; -} + +parse_params( $p[0] ); +parse_params( $post_body ) if $request_method eq 'POST'; + @c = split( '/', $c ); +if( $c[0] eq 'login' ) +{ + handle_login_route(); + exit; +} + +if( $c[0] eq 'logout' ) +{ + handle_logout_route(); + exit; +} + +# Static assets must be available before login, otherwise the login page +# cannot load the SMArT logo and icons. +if( $c[0] eq 'static' ) +{ + do( $smart_libexec_dir . '/static.pl' ); + handle_request(); + exit; +} + +if( ! valid_session() ) +{ + redirect( '/login' ); + exit; +} + if( ( $c[0] eq 'service' && $c[1] eq 'control' ) || ( $c[0] eq 'cgi-bin' && $c[1] eq 'control' ) ) { @@ -176,6 +209,359 @@ exit; ##### END OF MAIN PROCEDURES FOLLOW ##### ########################################## +sub smart_log_line( $$$ ) +{ + my( $level, $file, $msg ) = @_; + + $level = 'INFO' unless defined( $level ) && $level ne ''; + $file = 'smart' unless defined( $file ) && $file ne ''; + $msg = '' unless defined( $msg ); + + my( $sec, $min, $hour, $mday, $mon, $year ) = localtime( time() ); + my $ts = sprintf( "%04d-%02d-%02d %02d:%02d:%02d", + $year + 1900, $mon + 1, $mday, $hour, $min, $sec ); + + my $version = defined( $smart_version ) && $smart_version ne '' ? $smart_version : '0.99.pl28'; + + if( open( my $fh, '>>', $smart_log_path ) ) + { + print( $fh '[' . $ts . '] [' . $level . '] [SMArT ' . $version . '] [' . $file . '] ' . $msg . "\n" ); + close( $fh ); + } +} + + +sub smart_auth_log( $ ) +{ + my $msg = $_[0]; + $msg = '' unless defined $msg; + + my( $sec, $min, $hour, $mday, $mon, $year ) = localtime( time() ); + my $ts = sprintf( "%04d-%02d-%02d %02d:%02d:%02d", + $year + 1900, $mon + 1, $mday, $hour, $min, $sec ); + + my $version = defined( $smart_version ) && $smart_version ne '' ? $smart_version : '0.99.pl28'; + + if( open( my $fh, '>>', $smart_log_path ) ) + { + print( $fh '[' . $ts . '] [INFO] [SMArT ' . $version . '] [smart] ' . $msg . "\n" ); + close( $fh ); + } +} + + +sub parse_params( $ ) +{ + my $qs = $_[0]; + + return if ! defined( $qs ) || $qs eq ''; + + my @items = split( '&', $qs ); + + foreach my $item ( @items ) + { + my $n = $item; + my $v = $item; + + $n =~ s/=.*//; + $v =~ s/^[^=]*=?//; + + $n =~ s/\+/ /g; + $v =~ s/\+/ /g; + + $n =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('c',hex($1))/gie; + $v =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/pack('c',hex($1))/gie; + + $p{$n} = $v; + } +} + +sub smart_html_escape( $ ) +{ + my $s = $_[0]; + $s = '' unless defined $s; + $s =~ s/&/&/g; + $s =~ s//>/g; + $s =~ s/"/"/g; + return $s; +} + +sub session_timeout() +{ + return $smart_session_timeout if defined( $smart_session_timeout ) && $smart_session_timeout =~ /^[0-9]+$/ && $smart_session_timeout > 0; + return 3600; +} + +sub session_dir() +{ + my $dir = defined( $smart_session_dir ) && $smart_session_dir ne '' ? $smart_session_dir : '/run/mars-nwe-webui'; + + if( ! -d $dir ) + { + if( ! mkdir( $dir, 0700 ) ) + { + smart_auth_log( 'could not create session dir ' . $dir . ': ' . $! ); + } + } + + if( -d $dir ) + { + chmod( 0700, $dir ); + } + else + { + smart_auth_log( 'session dir is not available: ' . $dir ); + } + + return $dir; +} + +sub session_token() +{ + my $token = ''; + + if( open( my $fh, '<', '/dev/urandom' ) ) + { + my $buf = ''; + read( $fh, $buf, 24 ); + close( $fh ); + $token = unpack( 'H*', $buf ); + } + + if( $token eq '' ) + { + $token = sprintf( "%08x%08x%08x%08x", time(), $$, int( rand( 0xffffffff ) ), int( rand( 0xffffffff ) ) ); + } + + $token =~ s/[^A-Fa-f0-9]//g; + return $token; +} + +sub session_file( $ ) +{ + my $token = $_[0]; + $token = '' unless defined $token; + $token =~ s/[^A-Fa-f0-9]//g; + + return '' if $token eq ''; + + return session_dir() . '/' . $token; +} + +sub cookie_session_id() +{ + my $cookie = defined( $hl{'cookie'} ) ? $hl{'cookie'} : ''; + + foreach my $part ( split( /;/, $cookie ) ) + { + $part =~ s/^\s+//; + $part =~ s/\s+$//; + + if( $part =~ /^SMArT_SID=([A-Fa-f0-9]+)$/ ) + { + return $1; + } + } + + return ''; +} + +sub valid_session() +{ + my $token = cookie_session_id(); + my $file = session_file( $token ); + + return 0 if $token eq ''; + return 0 if $file eq ''; + + if( ! -f $file ) + { + smart_auth_log( 'session cookie exists but file is missing: ' . $file ); + return 0; + } + + my @st = stat( $file ); + if( scalar( @st ) == 0 ) + { + smart_auth_log( 'could not stat session file: ' . $file ); + return 0; + } + + if( time() - $st[9] > session_timeout() ) + { + unlink( $file ); + smart_auth_log( 'session expired: ' . $file ); + return 0; + } + + utime( time(), time(), $file ); + return 1; +} + +sub create_session( $ ) +{ + my $user = $_[0]; + my $token = session_token(); + my $file = session_file( $token ); + + if( $file eq '' ) + { + smart_auth_log( 'could not build session file path' ); + return ''; + } + + if( open( my $fh, '>', $file ) ) + { + print( $fh $user . "\n" . time() . "\n" ); + close( $fh ); + chmod( 0600, $file ); + smart_auth_log( 'created session for ' . $user . ' at ' . $file ); + return $token; + } + + smart_auth_log( 'could not create session file ' . $file . ': ' . $! ); + return ''; +} + +sub destroy_session() +{ + my $token = cookie_session_id(); + my $file = session_file( $token ); + + unlink( $file ) if $file ne '' && -f $file; +} + +sub check_login_password( $$ ) +{ + my( $user, $pass ) = @_; + + return 0 if ! defined( $user ) || ! defined( $pass ); + return 0 if $user ne 'root'; + + if( ! defined( $smart_check_login ) || $smart_check_login eq '' || ! -x $smart_check_login ) + { + return -1; + } + + return system( $smart_check_login, $user, $pass ) == 0 ? 1 : 0; +} + +sub print_login_page( $ ) +{ + my $msg = smart_html_escape( $_[0] ); + + print < + + + +SMArT Login + + + + + + +EOF +} + +sub handle_login_route() +{ + if( $request_method ne 'POST' ) + { + print_login_page( '' ); + return; + } + + my $rv = check_login_password( $p{user}, $p{pass} ); + + if( $rv == -1 ) + { + print_login_page( 'Login helper check_login is missing or not executable.' ); + return; + } + + if( $rv != 1 ) + { + print_login_page( 'Login failed.' ); + return; + } + + my $token = create_session( $p{user} ); + + if( $token eq '' ) + { + print_login_page( 'Could not create login session.' ); + return; + } + + print <Main menu

Choose a section from the icon list. The explanation opens here on the left, and the editor opens on the right.

- SMArT logo +