2023-09-05 19:46:15 +02:00
# irclogger_web
# Copyright (C) 2023 mrkubax10 <mrkubax10@onet.pl>
# 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 <http://www.gnu.org/licenses/>.
2023-09-06 21:51:11 +02:00
package frontend ;
2023-09-05 19:46:15 +02:00
use IO::Socket ;
use File::Spec ;
use Time::Piece ;
2023-09-06 21:51:11 +02:00
use DBI ;
2023-09-05 19:46:15 +02:00
use lib "." ;
use configuration ;
use feature qw( switch ) ;
use strict ;
use warnings ;
2023-09-06 20:09:20 +02:00
sub readFullFile {
my $ aFile = $ _ [ 0 ] ;
my $ content = "" ;
while ( ! eof ( $ aFile ) ) {
$ content . = readline ( $ aFile ) ;
}
return $ content ;
}
2023-09-05 19:46:15 +02:00
use constant {
HTTP_METHOD_UNKNOWN = > 0 ,
HTTP_METHOD_GET = > 1 ,
HTTP_METHOD_POST = > 2
} ;
sub stringToHTTPMethod {
my $ aMethod = $ _ [ 0 ] ;
given ( $ aMethod ) {
when ( "GET" ) { return HTTP_METHOD_GET ; }
when ( "POST" ) { return HTTP_METHOD_POST ; }
default { return HTTP_METHOD_UNKNOWN ; }
}
}
2023-09-06 21:51:11 +02:00
use constant {
PPATH_URL = > 0 ,
PPATH_GET_KEY = > 1 ,
PPATH_GET_VALUE = > 2
} ;
sub parsePath {
my $ aPath = $ _ [ 0 ] ;
my $ pathLength = length ( $ aPath ) ;
my $ state = PPATH_URL ;
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 ;
next ;
}
$ currentString . = $ char ;
}
when ( PPATH_GET_VALUE ) {
if ( $ char eq "&" ) {
$ state = PPATH_GET_KEY ;
$ output { "parameters" } { $ currentString } = $ currentString2 ;
$ currentString = "" ;
$ currentString2 = "" ;
next ;
}
$ currentString2 . = $ char ;
if ( $ i == $ pathLength - 1 ) {
$ output { "parameters" } { $ currentString } = $ currentString2 ;
}
}
}
}
return % output ;
}
2023-09-05 19:46:15 +02:00
use constant {
PHTTP_METHOD = > 0 ,
PHTTP_PATH = > 1 ,
PHTTP_VERSION = > 2 ,
PHTTP_HEADER = > 3 ,
PHTTP_VALUE = > 4
} ;
sub parseHTTPRequest {
my $ aRequest = $ _ [ 0 ] ;
my $ requestLength = length ( $ aRequest ) ;
my $ index = 0 ;
my $ state = PHTTP_METHOD ;
my $ currentString = "" ;
my $ currentString2 = "" ;
my % output ;
while ( $ index < $ requestLength ) {
my $ char = substr ( $ aRequest , $ index + + , 1 ) ;
given ( $ state ) {
when ( PHTTP_METHOD ) {
if ( $ char eq " " ) {
$ output { "method" } = stringToHTTPMethod ( $ currentString ) ;
$ currentString = "" ;
$ state = PHTTP_PATH ;
next ;
}
$ currentString . = $ char ;
}
when ( PHTTP_PATH ) {
if ( $ char eq " " ) {
2023-09-06 21:51:11 +02:00
$ output { "path" } = { parsePath ( $ currentString ) } ;
2023-09-05 19:46:15 +02:00
$ currentString = "" ;
$ state = PHTTP_VERSION ;
next ;
}
$ currentString . = $ char ;
}
when ( PHTTP_VERSION ) {
if ( $ char eq "\r" ) {
$ index + + ;
$ output { "version" } = $ currentString ;
$ currentString = "" ;
$ state = PHTTP_HEADER ;
next ;
}
$ currentString . = $ char ;
}
when ( PHTTP_HEADER ) {
if ( $ char eq ":" ) {
while ( substr ( $ aRequest , + + $ index , 1 ) eq " " ) { }
$ state = PHTTP_VALUE ;
next ;
}
$ currentString . = $ char ;
}
when ( PHTTP_VALUE ) {
if ( $ char eq "\r" ) {
$ index + + ;
$ output { "headers" } { $ currentString } = $ currentString2 ;
$ currentString = "" ;
$ currentString2 = "" ;
$ state = PHTTP_HEADER ;
next ;
}
$ currentString2 . = $ char ;
}
}
}
return % output ;
}
sub getBaseResponse {
my $ aStatusCode = $ _ [ 0 ] ;
my $ aStatusText = $ _ [ 1 ] ;
my $ response = "HTTP/1.1 $aStatusCode $aStatusText\r\n" ;
$ response . = "Date: " . localtime - > strftime ( "%a, %d %b %Y %H:%M:%S %Z" ) . "\r\n" ;
$ response . = "Server: irclogger_web\r\n" ;
return $ response ;
}
sub sendNotImplemented {
my $ aClient = $ _ [ 0 ] ;
my $ content = "<h1>501 Not Implemented</h1><h6>irclogger_web</h6>" ;
my $ response = getBaseResponse ( 501 , "Not Implemented" ) ;
$ response . = "Content-Type: text/html, charset=utf-8\r\n" ;
$ response . = "Content-Length: " . length ( $ content ) . "\r\n\r\n" ;
$ response . = $ content ;
$ aClient - > send ( $ response ) ;
}
2023-09-06 20:09:20 +02:00
sub sendNotFound {
my $ aClient = $ _ [ 0 ] ;
my $ content = "<h1>404 Not Found</h1><h6>irclogger_web</h6>" ;
my $ response = getBaseResponse ( 404 , "Not Found" ) ;
$ response . = "Content-Type: text/html, charset=utf-8\r\n" ;
$ response . = "Content-Length: " . length ( $ content ) . "\r\n\r\n" ;
$ response . = $ content ;
$ aClient - > send ( $ response ) ;
}
2023-09-06 21:51:11 +02:00
sub sendBadRequest {
my $ aClient = $ _ [ 0 ] ;
my $ aMessage = $ _ [ 1 ] ;
my $ content = "<h1>400 Bad Request</h1><h6>irclogger_web</h6>Error: $aMessage" ;
my $ response = getBaseResponse ( 400 , "Bad Request" ) ;
$ response . = "Content-Type: text/html, charset=utf-8\r\n" ;
$ response . = "Content-Length: " . length ( $ content ) . "\r\n\r\n" ;
$ response . = $ content ;
$ aClient - > send ( $ response ) ;
}
2023-09-06 20:09:20 +02:00
use constant {
PREPROCESSOR_STATE_TEXT = > 0 ,
PREPROCESSOR_STATE_VAR = > 1 ,
PREPROCESSOR_STATE_INC = > 2
} ;
sub preprocessHTML {
my $ aContent = $ _ [ 0 ] ;
my $ aVariables = $ _ [ 1 ] ;
my $ contentLength = length ( $ aContent ) ;
my $ state = PREPROCESSOR_STATE_TEXT ;
my $ currentString = "" ;
my $ index = 0 ;
my $ output = "" ;
while ( $ index < $ contentLength ) {
my $ char = substr ( $ aContent , $ index + + , 1 ) ;
given ( $ state ) {
when ( PREPROCESSOR_STATE_TEXT ) {
if ( $ char eq "{" && $ index < $ contentLength ) {
my $ nextChar = substr ( $ aContent , $ index , 1 ) ;
if ( $ nextChar eq "{" ) {
$ index + + ;
$ state = PREPROCESSOR_STATE_VAR ;
next ;
}
}
if ( $ char eq "[" && $ index < $ contentLength ) {
my $ nextChar = substr ( $ aContent , $ index , 1 ) ;
if ( $ nextChar eq "[" ) {
$ index + + ;
$ state = PREPROCESSOR_STATE_INC ;
next ;
}
}
$ output . = $ char ;
}
when ( PREPROCESSOR_STATE_VAR ) {
if ( $ char eq "}" && $ index < $ contentLength ) {
my $ nextChar = substr ( $ aContent , $ index , 1 ) ;
if ( $ nextChar eq "}" ) {
$ index + + ;
$ output . = $ aVariables - > { $ currentString } ;
$ currentString = "" ;
$ state = PREPROCESSOR_STATE_TEXT ;
next ;
}
}
$ currentString . = $ char ;
}
when ( PREPROCESSOR_STATE_INC ) {
if ( $ char eq "]" && $ index < $ contentLength ) {
my $ nextChar = substr ( $ aContent , $ index , 1 ) ;
if ( $ nextChar eq "]" ) {
$ index + + ;
if ( open ( my $ file , "<" , $ currentString ) ) {
2023-09-06 21:51:11 +02:00
$ output . = preprocessHTML ( readFullFile ( $ file ) , $ aVariables ) ;
2023-09-06 20:09:20 +02:00
close ( $ file ) ;
}
else {
$ output . = "[[Include file not found]" ;
}
$ currentString = "" ;
$ state = PREPROCESSOR_STATE_TEXT ;
next ;
}
}
$ currentString . = $ char ;
}
}
}
return $ output ;
}
sub sendTemplate {
my $ aFilePath = $ _ [ 0 ] ;
my $ aClient = $ _ [ 1 ] ;
my $ aVariables = $ _ [ 2 ] ;
my $ result = open ( my $ file , "<" , $ aFilePath ) ;
if ( ! $ result ) {
sendNotFound ( $ aClient ) ;
return 0 ;
}
my $ content = preprocessHTML ( readFullFile ( $ file ) , $ aVariables ) ;
close ( $ file ) ;
my $ length = length ( $ content ) ;
my $ response = getBaseResponse ( 200 , "OK" ) ;
$ response . = "Content-Length: $length\r\n\r\n" ;
$ response . = $ content ;
$ aClient - > send ( $ response ) ;
}
sub handlePath {
my $ aClient = $ _ [ 0 ] ;
my $ aPath = $ _ [ 1 ] ;
my $ aRequest = $ _ [ 2 ] ;
2023-09-06 21:51:11 +02:00
my $ aConnection = $ _ [ 3 ] ;
2023-09-06 20:09:20 +02:00
given ( $ aPath ) {
when ( "/" ) {
2023-09-06 21:51:11 +02:00
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 . = "<tr><td><a href=\"view_logs?channel=$channelID\">$channelName</a></td><td>$serverName</td></tr>" ;
}
sendTemplate ( "templates/index.html" , $ aClient , { "publicChannels" = > $ table } ) ;
return 1 ;
}
when ( "/view_logs" ) {
my $ channelID = $ aRequest - > { "path" } { "parameters" } { "channel" } ;
if ( ! defined ( $ channelID ) ) {
2023-09-07 19:49:00 +02:00
sendBadRequest ( $ aClient , "view_logs requires channel URL parameter" ) ;
2023-09-06 21:51:11 +02:00
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 ) {
2023-09-07 19:49:00 +02:00
$ table . = "<tr><td><a href=\"view_log?channel=$channelID&file=$entry\">$entry</a></td></tr>" ;
2023-09-06 21:51:11 +02:00
}
sendTemplate ( "templates/view_logs.html" , $ aClient , { "channel" = > $ channelName , "server" = > $ serverName , "logs" = > $ table } ) ;
2023-09-06 20:09:20 +02:00
return 1 ;
}
2023-09-07 19:49:00 +02:00
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 ) ;
}
2023-09-06 20:09:20 +02:00
}
return 0 ;
}
2023-09-05 19:46:15 +02:00
sub sendResponse {
my $ aClient = $ _ [ 0 ] ;
my $ aRequest = $ _ [ 1 ] ;
2023-09-06 21:51:11 +02:00
my $ aConnection = $ _ [ 2 ] ;
2023-09-05 19:46:15 +02:00
if ( $ aRequest - > { "version" } ne "HTTP/1.1" ) {
sendNotImplemented ( $ aClient ) ;
return ;
}
given ( $ aRequest - > { "method" } ) {
when ( HTTP_METHOD_GET ) {
2023-09-06 21:51:11 +02:00
my $ path = File::Spec - > canonpath ( $ aRequest - > { "path" } { "url" } ) ;
2023-09-06 20:09:20 +02:00
if ( $ path eq "/index.html" || $ path eq "/index.htm" ) {
$ path = "/" ;
2023-09-05 19:46:15 +02:00
}
2023-09-06 21:51:11 +02:00
if ( handlePath ( $ aClient , $ path , $ aRequest , $ aConnection ) ) {
2023-09-06 20:09:20 +02:00
return ;
}
my $ filePath = "static" . $ path ;
2023-09-05 19:46:15 +02:00
my $ result = open ( my $ file , "<" , $ filePath ) ;
if ( ! $ result ) {
2023-09-06 20:09:20 +02:00
sendNotFound ( $ aClient ) ;
2023-09-05 19:46:15 +02:00
return ;
}
2023-09-06 20:09:20 +02:00
my $ content = readFullFile ( $ file ) ;
2023-09-05 19:46:15 +02:00
close ( $ file ) ;
my $ response = getBaseResponse ( 200 , "OK" ) ;
# TODO
#my $mime = File::Type->new()->mime_type($filePath);
#$response.="Content-Type: $mime\r\n";
$ response . = "Content-Length: " . length ( $ content ) . "\r\n\r\n" ;
$ response . = $ content ;
$ aClient - > send ( $ response ) ;
}
default {
sendNotImplemented ( $ aClient ) ;
}
}
}
sub httpServerWorker {
2023-09-06 21:51:11 +02:00
my $ db = DBI - > connect ( "DBI:SQLite:dbname=$configuration::database" , "" , "" , { RaiseError = > 1 } ) ;
2023-09-05 19:46:15 +02:00
my $ server = new IO::Socket:: INET ( LocalHost = > "localhost" , LocalPort = > $ configuration:: httpServerPort , Proto = > "tcp" , Listen = > 1 , Reuse = > 1 ) ;
if ( ! $ server ) {
print ( "Failed to open HTTP server on port $configuration::httpServerPort\n" ) ;
return ;
}
while ( 1 ) {
my $ client = $ server - > accept ( ) ;
my $ buffer ;
$ client - > recv ( $ buffer , 4096 ) ;
if ( length ( $ buffer ) == 0 ) {
close ( $ client ) ;
next ;
}
my % request = parseHTTPRequest ( $ buffer ) ;
2023-09-06 21:51:11 +02:00
sendResponse ( $ client , \ % request , $ db ) ;
2023-09-05 19:46:15 +02:00
close ( $ client ) ;
}
}
2023-09-06 21:51:11 +02:00
1 ;