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';
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;
+ $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
+
+
+
+
+
+

+
+
SMArT Login
+
MARS_NWE web administration
+
+
+
+
+
+
+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.
-
+