This commit is contained in:
Mario Fetka
2026-05-21 21:56:28 +02:00
parent 4b56266392
commit c7c746d264
3 changed files with 390 additions and 51 deletions

View File

@@ -708,40 +708,44 @@ sub unix_user_defaults_from_query()
my $want = '';
# Preferred import route:
# /settings/users_import/<unixuser>
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/<user>
# 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/<user>, 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/<unixuser>
# Compatibility for /settings/users/add_new/<user>.
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;

View File

@@ -45,11 +45,14 @@ $smart_systemctl_path = '@SYSTEMCTL_EXECUTABLE@' unless defined $smart_systemctl
$l = <STDIN>;
$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 = <STDIN>;
$l =~ s/[\n\r]//g;
@@ -59,36 +62,31 @@ 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] );
if( $h{Authorization} eq '' )
{ error( 401 ); }
else
if( $c[0] 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( $c[0] ne 'GET' && $c[0] ne 'POST' )
{
error( 501 );
}
@@ -97,19 +95,30 @@ 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 $c[0] eq 'POST';
@c = split( '/', $c );
if( $c[0] eq 'login' )
{
handle_login_route();
exit;
}
if( $c[0] eq 'logout' )
{
handle_logout_route();
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 +185,328 @@ exit;
##### END OF MAIN PROCEDURES FOLLOW #####
##########################################
sub smart_auth_log( $ )
{
my $msg = $_[0];
$msg = '' unless defined $msg;
if( open( my $fh, '>>', $smart_log_path ) )
{
print( $fh scalar( localtime() ) . " [AUTH] " . $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/&/&amp;/g;
$s =~ s/</&lt;/g;
$s =~ s/>/&gt;/g;
$s =~ s/"/&quot;/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 <<EOF;
HTTP/1.0 200 OK
Content-Type: text/html
$server_id
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SMArT Login</title>
<style>
:root{--bg:#f4f1ea;--panel:#faf8f4;--line:#dfd2bf;--text:#3d342c;--muted:#6f6257;--accent:#ad1d1c;--gold:#b9813d}
*{box-sizing:border-box}
html,body{margin:0;padding:0;min-height:100%;background:var(--bg);color:var(--text);font:15px/1.5 Arial,Helvetica,sans-serif}
body{display:flex;align-items:center;justify-content:center;padding:24px}
.login{width:min(440px,100%);background:var(--panel);border:1px solid var(--line);border-radius:20px;box-shadow:0 18px 45px rgba(64,36,12,.12);overflow:hidden}
.hero{padding:26px 28px;background:linear-gradient(135deg,#a80f18,#c44731 60%,#d79a54);color:white}
.hero h1{margin:0;font-size:28px}
.hero p{margin:6px 0 0;opacity:.95}
form{padding:24px 28px 28px}
label{display:block;font-weight:bold;margin:0 0 7px}
input{width:100%;border:1px solid #cdbb9f;border-radius:12px;padding:10px 12px;background:#fffdf9;color:var(--text);font-size:15px;margin:0 0 16px}
button{width:100%;border:1px solid #a33d2f;border-radius:12px;padding:11px 14px;background:#b84434;color:#fff;font-weight:bold;font-size:15px;cursor:pointer}
.msg{margin:0 0 16px;padding:10px 12px;border-radius:12px;background:#fff3e0;border:1px solid #ead0a4;color:#7a3d18}
.note{margin-top:14px;color:var(--muted);font-size:13px;text-align:center}
</style>
</head>
<body>
<div class="login">
<div class="hero">
<h1>SMArT Login</h1>
<p>MARS_NWE web administration</p>
</div>
<form method="POST" action="/login">
EOF
if( $msg ne '' )
{
print '<div class="msg">' . $msg . "</div>\n";
}
print <<EOF;
<label for="user">User</label>
<input id="user" name="user" value="root" autocomplete="username">
<label for="pass">Password</label>
<input id="pass" name="pass" type="password" autocomplete="current-password" autofocus>
<button type="submit">Login</button>
<div class="note">Authentication is checked through PAM via <tt>check_login</tt>.</div>
</form>
</div>
</body>
</html>
EOF
}
sub handle_login_route()
{
if( $c[0] 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 <<EOF;
HTTP/1.0 302 Found
Location: /
Set-Cookie: SMArT_SID=$token; Path=/; HttpOnly; Max-Age=3600
$server_id
EOF
}
sub handle_logout_route()
{
destroy_session();
print <<EOF;
HTTP/1.0 302 Found
Location: /login
Set-Cookie: SMArT_SID=deleted; Path=/; HttpOnly; Max-Age=0
$server_id
EOF
}
sub error( $ )
{
if( $_[0] eq '401' )

View File

@@ -146,3 +146,7 @@ $nw_cert_file = '@MARS_NWE_INSTALL_FULL_CONFDIR@/server.crt';
# TLS private key file in PEM format.
# Required only when HTTPS is enabled.
$nw_key_file = '@MARS_NWE_INSTALL_FULL_CONFDIR@/server.key';
# Directory for HTML login cookie sessions. Created by systemd RuntimeDirectory.
$smart_session_dir = '/run/mars-nwe-webui';
$smart_session_timeout = 3600;