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 = "";
+ }
+
+ 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