diff --git a/frontend.pm b/frontend.pm index 4b18946..f992519 100644 --- a/frontend.pm +++ b/frontend.pm @@ -23,6 +23,7 @@ use DBI; use lib "."; use configuration; +use frontend_routes; use feature qw(switch); use strict; @@ -58,29 +59,17 @@ use constant { PPATH_GET_KEY => 1, PPATH_GET_VALUE => 2 }; -sub parsePath { +sub parsePathParameters { my $aPath = $_[0]; my $pathLength = length($aPath); - my $state = PPATH_URL; + my $state = PPATH_GET_KEY; my $currentString = ""; my $currentString2 = ""; my %output; foreach my $i (0..$pathLength-1) { my $char = substr($aPath, $i, 1); given($state) { - when(PPATH_URL) { - if($char eq "?") { - $output{"url"} = $currentString; - $currentString = ""; - $state = PPATH_GET_KEY; - next; - } - $currentString.=$char; - if($i==$pathLength-1) { - $output{"url"} = $currentString; - } - } when(PPATH_GET_KEY) { if($char eq "=") { $state = PPATH_GET_VALUE; @@ -91,27 +80,96 @@ sub parsePath { when(PPATH_GET_VALUE) { if($char eq "&") { $state = PPATH_GET_KEY; - $output{"parameters"}{$currentString} = $currentString2; + $output{$currentString} = $currentString2; $currentString = ""; $currentString2 = ""; next; } $currentString2.=$char; if($i==$pathLength-1) { - $output{"parameters"}{$currentString} = $currentString2; + $output{$currentString} = $currentString2; } } } } return %output; } +sub parsePath { + my $aPath = $_[0]; + + my $pathLength = length($aPath); + my $state = PPATH_URL; + my $currentString = ""; + my %output; + my $index = 0; + while($index<$pathLength) { + my $char = substr($aPath, $index++, 1); + if($char eq "?") { + $output{"url"} = $currentString; + $state = PPATH_GET_KEY; + last; + } + $currentString.=$char; + } + $output{"url"} = $currentString; + if($state==PPATH_GET_KEY) { + $output{"parameters"} = { parsePathParameters(substr($aPath, $index, $pathLength-$index)) }; + } + return %output; +} + +use constant { + PCOOKIE_NAME => 0, + PCOOKIE_VALUE => 1 +}; +sub parseCookies { + my $aCookies = $_[0]; + + my $cookiesLength = length($aCookies); + my $state = PCOOKIE_NAME; + my $currentString = ""; + my $currentString2 = ""; + my %output; + foreach my $i (0..$cookiesLength-1) { + my $char = substr($aCookies, $i, 1); + given($state) { + when(PCOOKIE_NAME) { + if($char eq " ") { + next; + } + if($char eq "=") { + $state = PCOOKIE_VALUE; + next; + } + $currentString.=$char; + } + when(PCOOKIE_VALUE) { + if($char eq ";" || $i==$cookiesLength-1) { + if(length($currentString)>0 && length($currentString2)>0) { + if($i==$cookiesLength-1) { + $currentString2.=$char; + } + $output{$currentString} = $currentString2; + } + $currentString = ""; + $currentString2 = ""; + $state = PCOOKIE_NAME; + next; + } + $currentString2.=$char; + } + } + } + return %output; +} use constant { PHTTP_METHOD => 0, PHTTP_PATH => 1, PHTTP_VERSION => 2, PHTTP_HEADER => 3, - PHTTP_VALUE => 4 + PHTTP_VALUE => 4, + PHTTP_CONTENT => 5 }; sub parseHTTPRequest { my $aRequest = $_[0]; @@ -164,14 +222,31 @@ sub parseHTTPRequest { when(PHTTP_VALUE) { if($char eq "\r") { $index++; - $output{"headers"}{$currentString} = $currentString2; - $currentString = ""; - $currentString2 = ""; - $state = PHTTP_HEADER; + if($currentString eq "Cookie") { + $output{"cookies"} = { parseCookies($currentString2) }; + } + else { + $output{"headers"}{$currentString} = $currentString2; + } + if($index+1<$requestLength && substr($aRequest, $index, 1) eq "\r") { + if(defined($output{"headers"}{"Content-Length"})) { + $index+=2; + $state = PHTTP_CONTENT; + $output{"content"} = ""; + } + } + else { + $currentString = ""; + $currentString2 = ""; + $state = PHTTP_HEADER; + } next; } $currentString2.=$char; } + when(PHTTP_CONTENT) { + $output{"content"}.=$char; + } } } return %output; @@ -221,6 +296,16 @@ sub sendBadRequest { $aClient->send($response); } +sub redirect { + my $aClient = $_[0]; + my $aLocation = $_[1]; + + my $response = getBaseResponse(301, "Moved Permanently"); + $response.="Content-Length: 0\r\n"; + $response.="Location: $aLocation\r\n"; + $aClient->send($response); +} + use constant { PREPROCESSOR_STATE_TEXT => 0, PREPROCESSOR_STATE_VAR => 1, @@ -313,101 +398,6 @@ sub sendTemplate { $aClient->send($response); } -sub handlePath { - my $aClient = $_[0]; - my $aPath = $_[1]; - my $aRequest = $_[2]; - my $aConnection = $_[3]; - - given($aPath) { - when("/") { - my $query = $aConnection->prepare(qq(select channels.id, channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.public=1;)); - $query->execute(); - my $table = ""; - while(my @row = $query->fetchrow_array()) { - my $channelID = $row[0]; - my $channelName = $row[1]; - my $serverName = $row[2]; - - $table.="$channelName$serverName"; - } - sendTemplate("templates/index.html", $aClient, {"publicChannels"=>$table}); - return 1; - } - when("/view_logs") { - my $channelID = $aRequest->{"path"}{"parameters"}{"channel"}; - if(!defined($channelID)) { - sendBadRequest($aClient, "view_logs requires channel URL parameter"); - return 1; - } - - my $query = $aConnection->prepare(qq(select channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.id=?;)); - $query->execute($channelID); - my @row = $query->fetchrow_array(); - if(scalar(@row)==0) { - sendBadRequest($aClient, "Unknown channel with ID $channelID"); - return 1; - } - my $channelName = $row[0]; - my $serverName = $row[1]; - my $logsPath = "logs/".$serverName."/".$channelName; - - my $result = opendir(my $folder, $logsPath); - if(!$result) { - sendBadRequest($aClient, "Channel $channelName on $serverName doesn't have any logs"); - return 1; - } - my @entries = grep(!/^\.\.?$/, readdir($folder)); - - my $table = ""; - foreach my $entry (@entries) { - $table.="$entry"; - } - - sendTemplate("templates/view_logs.html", $aClient, {"channel"=>$channelName, "server"=>$serverName, "logs"=>$table}); - return 1; - } - when("/view_log") { - my $channelID = $aRequest->{"path"}{"parameters"}{"channel"}; - if(!defined($channelID)) { - sendBadRequest($aClient, "view_log requires channel URL parameter"); - return 1; - } - my $logFile = $aRequest->{"path"}{"parameters"}{"file"}; - if(!defined($channelID)) { - sendBadRequest($aClient, "view_log requires file URL parameter"); - return 1; - } - - my $query = $aConnection->prepare(qq(select channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.id=?;)); - $query->execute($channelID); - my @row = $query->fetchrow_array(); - if(scalar(@row)==0) { - sendBadRequest($aClient, "Unknown channel with ID $channelID"); - return 1; - } - my $channelName = $row[0]; - my $serverName = $row[1]; - my $logFilePath = "logs/".$serverName."/".$channelName."/".$logFile; - - my $result = open(my $file, "<", $logFilePath); - if(!$result) { - sendBadRequest($aClient, "No log file $logFile for channel $channelName at $serverName"); - return 1; - } - my $content = readFullFile($file); - close($file); - - my $response = getBaseResponse(200, "OK"); - $response.="Content-Type: text/plain;charset=utf-8\r\n"; - $response.="Content-Length: ".length($content)."\r\n\r\n"; - $response.=$content; - $aClient->send($response); - } - } - return 0; -} - sub sendResponse { my $aClient = $_[0]; my $aRequest = $_[1]; @@ -424,7 +414,7 @@ sub sendResponse { if($path eq "/index.html" || $path eq "/index.htm") { $path = "/"; } - if(handlePath($aClient, $path, $aRequest, $aConnection)) { + if(frontend_routes::handlePath($aClient, $path, $aRequest, $aConnection)) { return; } my $filePath = "static".$path; @@ -443,6 +433,15 @@ sub sendResponse { $response.=$content; $aClient->send($response); } + when(HTTP_METHOD_POST) { + my $path = File::Spec->canonpath($aRequest->{"path"}{"url"}); + if($path eq "/index.html" || $path eq "/index.htm") { + $path = "/"; + } + if(!frontend_routes::handlePath($aClient, $path, $aRequest, $aConnection)) { + sendNotFound($aClient); + } + } default { sendNotImplemented($aClient); } diff --git a/frontend_routes.pm b/frontend_routes.pm new file mode 100644 index 0000000..9315837 --- /dev/null +++ b/frontend_routes.pm @@ -0,0 +1,194 @@ +# irclogger_web +# Copyright (C) 2023 mrkubax10 + +# 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 3 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, see . + +package frontend_routes; + +use lib "."; +use frontend_session; + +use Digest::SHA; +use Data::Dumper; + +use feature qw(switch); +use strict; +use warnings; + +sub handlePath { + my $aClient = $_[0]; + my $aPath = $_[1]; + my $aRequest = $_[2]; + my $aConnection = $_[3]; + + given($aPath) { + when("/") { + my $userbar; + my $logged = 0; + if(defined($aRequest->{"cookies"}) && defined($aRequest->{"cookies"}{"session"})) { + my $session = $aRequest->{"cookies"}{"session"}; + if(frontend_session::isValidSession($session) && defined($frontend_session::sessions{$session}{"username"}) && $frontend_session::sessions{$session}{"logged"}) { + my $username = $frontend_session::sessions{$session}{"username"}; + $userbar = "$username"; + $userbar.="Log out"; + $logged = 1; + } + } + if(!$logged) { + $userbar = "
"; + $userbar.=" "; + $userbar.=" "; + $userbar.=""; + $userbar.="
"; + } + + my $query = $aConnection->prepare(qq(select channels.id, channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.public=1;)); + $query->execute(); + my $table = ""; + while(my @row = $query->fetchrow_array()) { + my $channelID = $row[0]; + my $channelName = $row[1]; + my $serverName = $row[2]; + + $table.="$channelName$serverName"; + } + + frontend::sendTemplate("templates/index.html", $aClient, {"userbar"=>$userbar, "publicChannels"=>$table}); + return 1; + } + when("/login_action") { + if(defined($aRequest->{"cookies"}{"session"}) && frontend_session::isValidSession($aRequest->{"cookies"}{"session"})) { + frontend::redirect($aClient, "/"); + return 1; + } + if(defined($aRequest->{"headers"}{"Content-Type"}) && $aRequest->{"headers"}{"Content-Type"} ne "application/x-www-form-urlencoded") { + frontend::sendBadRequest($aClient, "Unsupported form Content-Type (application/x-www-form-urlencoded required)"); + return 1; + } + if(!defined($aRequest->{"content"})) { + frontend::sendBadRequest($aClient, "Request content required"); + return 1; + } + + my %parameters = frontend::parsePathParameters($aRequest->{"content"}); + if(!defined($parameters{"username"})) { + frontend::sendBadRequest($aClient, "Username parameter required"); + return 1; + } + if(!defined($parameters{"password"})) { + frontend::sendBadRequest($aClient, "Password parameter required"); + return 1; + } + + my $username = $parameters{'username'}; + #my $hashedPassword = Digest::SHA::sha256_hex($parameters{"password"}); + my $hashedPassword = $parameters{"password"}; + my $query = $aConnection->prepare(qq(select name, password from users where name=?;)); + $query->execute($username); + my @row = $query->fetchrow_array(); + if(scalar(@row)==0) { + frontend::sendBadRequest($aClient, "Unknown user $username"); + return 1; + } + if($row[1] ne $hashedPassword) { + frontend::sendBadRequest($aClient, "Wrong password"); + return 1; + } + + my $token = Digest::SHA::sha256_hex(sprintf("%x", rand(0xFFFFFFFF)%0xFF)); + $frontend_session::sessions{$token}{"username"} = $username; + $frontend_session::sessions{$token}{"logged"} = 1; + + my $response = frontend::getBaseResponse(301, "OK"); + $response.="Location: /\r\n"; + $response.="Content-Length: 0\r\n"; + $response.="Set-Cookie: session=$token\r\n\r\n"; + $aClient->send($response); + return 1; + } + when("/view_logs") { + my $channelID = $aRequest->{"path"}{"parameters"}{"channel"}; + if(!defined($channelID)) { + frontend::sendBadRequest($aClient, "view_logs requires channel URL parameter"); + return 1; + } + + my $query = $aConnection->prepare(qq(select channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.id=?;)); + $query->execute($channelID); + my @row = $query->fetchrow_array(); + if(scalar(@row)==0) { + frontend::sendBadRequest($aClient, "Unknown channel with ID $channelID"); + return 1; + } + my $channelName = $row[0]; + my $serverName = $row[1]; + my $logsPath = "logs/".$serverName."/".$channelName; + + my $result = opendir(my $folder, $logsPath); + if(!$result) { + frontend::sendBadRequest($aClient, "Channel $channelName on $serverName doesn't have any logs"); + return 1; + } + my @entries = grep(!/^\.\.?$/, readdir($folder)); + + my $table = ""; + foreach my $entry (@entries) { + $table.="$entry"; + } + + frontend::sendTemplate("templates/view_logs.html", $aClient, {"channel"=>$channelName, "server"=>$serverName, "logs"=>$table}); + return 1; + } + when("/view_log") { + my $channelID = $aRequest->{"path"}{"parameters"}{"channel"}; + if(!defined($channelID)) { + frontend::sendBadRequest($aClient, "view_log requires channel URL parameter"); + return 1; + } + my $logFile = $aRequest->{"path"}{"parameters"}{"file"}; + if(!defined($channelID)) { + frontend::sendBadRequest($aClient, "view_log requires file URL parameter"); + return 1; + } + + my $query = $aConnection->prepare(qq(select channels.name, servers.name from channels inner join servers on channels.server_id=servers.id where channels.id=?;)); + $query->execute($channelID); + my @row = $query->fetchrow_array(); + if(scalar(@row)==0) { + frontend::sendBadRequest($aClient, "Unknown channel with ID $channelID"); + return 1; + } + my $channelName = $row[0]; + my $serverName = $row[1]; + my $logFilePath = "logs/".$serverName."/".$channelName."/".$logFile; + + my $result = open(my $file, "<", $logFilePath); + if(!$result) { + frontend::sendBadRequest($aClient, "No log file $logFile for channel $channelName at $serverName"); + return 1; + } + my $content = frontend::readFullFile($file); + close($file); + + my $response = frontend::getBaseResponse(200, "OK"); + $response.="Content-Type: text/plain;charset=utf-8\r\n"; + $response.="Content-Length: ".length($content)."\r\n\r\n"; + $response.=$content; + $aClient->send($response); + } + } + return 0; +} + +1; diff --git a/frontend_session.pm b/frontend_session.pm new file mode 100644 index 0000000..25dd4ad --- /dev/null +++ b/frontend_session.pm @@ -0,0 +1,30 @@ +# irclogger_web +# Copyright (C) 2023 mrkubax10 + +# 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 3 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, see . + +package frontend_session; + +use strict; +use warnings; + +our %sessions; + +sub isValidSession { + my $aSession = $_[0]; + + return defined($sessions{$aSession}); +} + +1; diff --git a/templates/index.html b/templates/index.html index 54b535b..0c3fc4d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,6 +4,7 @@ irclogger_web + {{userbar}}

Channel list

ChannelNetwork