#!/usr/bin/perl
# 
# SMArT
# 
# Main program file
# 
# Copyright 2001 Wilmer van der Gaast
# 
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#

$redirected = 0;

$server_id = 'Server: SMArT/Perl/@MARS_NWE_VERSION@';

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 )
	or die "Could not open $smart_log_path: $!";

$ENV{HOME} = '@MARS_NWE_INSTALL_FULL_CONFDIR@';
$smart_libexec_dir = '@MARS_NWE_INSTALL_FULL_LIBEXECDIR@';
$smart_libexec_dir =~ s#/*$##;

$smart_control_path = $smart_libexec_dir . '/control' unless defined $smart_control_path;
$mars_nwe_service = '@MARS_NWE_SYSTEMD_SERVICE@' unless defined $mars_nwe_service;
$smart_systemctl_path = '@SYSTEMCTL_EXECUTABLE@' unless defined $smart_systemctl_path;

$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 ) < 50 )
	{
		$l = <STDIN>;
		$l =~ s/[\n\r]//g;
		if( $l eq '' )
			{ last; }
		$n = $l;
		$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( $request_method eq 'POST' )
{
	my $content_length = 0;

	if( defined( $hl{'content-length'} ) && $hl{'content-length'} =~ /^[0-9]+$/ )
	{
		$content_length = int( $hl{'content-length'} );
	}

	if( $content_length > 0 && $content_length < 8192 )
	{
		read( STDIN, $post_body, $content_length );
	}
}

if( $request_method ne 'GET' && $request_method ne 'POST' )
{
	error( 501 );
}

@p = split( '\?', $c[1] );
$cc = $c[1];
$cc =~ s/[^\?]*\?//;
$c = substr( shift( @p ), 1 );

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

if( ! valid_session() )
{
	redirect( '/login' );
	exit;
}

if( ( $c[0] eq 'service' && $c[1] eq 'control' ) ||
    ( $c[0] eq 'cgi-bin' && $c[1] eq 'control' ) )
{
	# Service control must run before drop_root().
	# /service/control is the preferred path; /cgi-bin/control is kept as a legacy alias.
	my $rv = do( $smart_control_path );
	if( ! defined $rv )
	{
		error( 500 );
	}
	handle_request();
	exit;
}
elsif( $c[0] eq 'apply' )
{
	do( $smart_libexec_dir . '/readconfig.pl' );
	do( $smart_libexec_dir . '/apply.pl' );
	handle_request();
	exit;
}
elsif( $c[0] eq 'settings' )
{
	do( $smart_libexec_dir . '/readconfig.pl' );
}

drop_root();

if( $c[0] eq '' )
{
	print <<EOF;
HTTP/1.0 200 OK
Content-Type: text/html
$server_id

<HTML>
<HEAD>
<TITLE>SMArT</TITLE>
</HEAD>
<FRAMESET COLS="50%,50%">
	<FRAME NAME="MENU" SRC="/static/menu.html">
	<FRAME NAME="OPTS" SRC="/static/start.html">
</FRAMESET>
</HTML>
EOF
	exit;
}
elsif( $c[0] eq 'static' )
{
	do( $smart_libexec_dir . '/static.pl' );
}
elsif( $c[0] eq 'settings' )
{
	do( $smart_libexec_dir . '/settings.pl' );
}
else
{
	error( 500 );
}

handle_request();
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/&/&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{display:flex;align-items:center;gap:16px}.hero img{width:54px;height:auto;background:#fff;border-radius:12px;padding:6px;box-shadow:0 8px 20px rgba(0,0,0,.16)}.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">
    <img src="/static/smart_icon.jpg" alt="SMArT logo">
    <div>
      <h1>SMArT Login</h1>
      <p>MARS_NWE web administration</p>
    </div>
  </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>
  </form>
</div>
</body>
</html>
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 <<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' )
	{
		print <<EOF;
HTTP/1.0 401 Access denied
Content-Type: text/plain
WWW-Authenticate: Basic realm="SMArT"
$server_id

You're not allowed to access this URL.
EOF
	}
	elsif( $_[0] eq '404' )
	{
		print <<EOF;
HTTP/1.0 404 File not found
Content-Type: text/plain
$server_id

The file you requested does not exist.
EOF
	}
	elsif( $_[0] eq '501' )
	{
		print <<EOF;
HTTP/1.0 501 Unknown command
Content-Type: text/plain
$server_id

Unknown command: $c[0]
EOF
	}
	else
	{
		print <<EOF;
HTTP/1.0 500 Internal server error
Content-Type: text/plain
$server_id

Something went wrong...
EOF
	}
	exit;
}

sub unix_userlist()
{
	my( @c, @d, %e );
	
	while( @d = getpwent )
	{
		unshift( @c, { name => $d[0], uid => $d[2] } );
	}
	return( sort( { $a->{"name"} cmp $b->{"name"} } @c ) );
}

sub unix_grouplist()
{
	my( @c, @d, %e );
	
	while( @d = getgrent )
	{
		unshift( @c, { name => $d[0], gid => $d[2] } );
	}
	return( sort( { $a->{"name"} cmp $b->{"name"} } @c ) );
}

sub redirect( $ )
{
	if( $redirected != 0 )
		{ return( 0 ); }
	$redirected = 1;
	print <<EOF;
HTTP/1.0 302 Page relocated
Content-Type: text/plain
Location: $_[0]
$server_id

EOF
}

sub get_server
{
	open( SFILE, '<' . $smart_nwclient_path );
	chomp( $line = <SFILE> );
	close( SFILE );
	
	$line =~ s/\/.*//;
	return( $line );
}

sub get_bindery_password
{
	open( SFILE, '<' . $smart_nwclient_path );
	chomp( $line = <SFILE> );
	close( SFILE );
	
	$line =~ s/.* //;
	return( $line );
}

sub read_property_string
{
	my @x = split( "\n", `nwbpvalues -c -o $_[0] -t $_[1] -p $_[2] -S $server` );
	my( $i, $s );
	
	$i = 5;
	while( hex( $x[$i] ) > 0 )
	{
		$s .= pack( 'c', hex( $x[$i] ) );
		$i ++;
	}
	
	return( $s );
}

sub read_property_list
{
	my @x = split( "\n", `nwbpvalues -c -o $_[0] -t $_[1] -p $_[2] -S $server` );
	my( $i, @l );
	
	$i = 6;
	while( $x[$i] ne '' )
	{
		unshift( @l, $x[$i] );
		$i += 2;
	}
	
	return( @l );
}

sub write_property_string
{
	open( FILE, '|' . 'nwbpset -S ' . $server );
	print FILE <<EOF;
000$_[1]
$_[0]
$_[2]
$_[3]
$_[4]
EOF
	for $i ( 0 .. length( $_[5] ) - 1 )
	{
		print( FILE unpack( 'H2', substr( $_[5], $i, 1 ) ) . "\n" );
	}
	close( FILE );
}

sub drop_root
{
	$< = $> = getpwnam( $nonroot_user );
}
