ircabot/httpserver.cpp

646 lines
24 KiB
C++

#include "httpserver.h"
#include "ircclient.h"
#include "connectiondata.h"
#include "global.h"
#include "version.h"
#include <QRegularExpression>
#include <QDirIterator>
#include <QHostAddress>
#include <QTcpSocket>
#include <QDebug>
#include <QFile>
#include <QDir>
HttpServer::HttpServer(const QString &address, quint16 port, const QString& logFolder,
const QString& mainChannel, QObject *parent) :
QObject(parent),
m_TcpServer(new QTcpServer),
m_mainChannel(mainChannel),
m_logFolder(logFolder)
{
if (not m_TcpServer->listen(QHostAddress(address), port)) {
throw std::runtime_error("HttpServer not binded at " +
address.toStdString() + " : " + QString::number(port).toStdString());
}
else {
consoleLog(address + " : " + QString::number(port));
}
connect (m_TcpServer, &QTcpServer::newConnection, this, &HttpServer::acceptor);
}
HttpServer::~HttpServer()
{
m_TcpServer->close();
m_TcpServer->deleteLater();
}
void HttpServer::consoleLog(const QString &message)
{
qInfo().noquote() << "[WEBINTERFACE]" << message;
}
void HttpServer::acceptor()
{
QTcpSocket* socket = m_TcpServer->nextPendingConnection();
connect(socket, &QTcpSocket::readyRead, this, &HttpServer::reader);
connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater);
}
void HttpServer::reader()
{
QTcpSocket* socket = static_cast<QTcpSocket*>(sender());
QString request = socket->readAll();
QString urlPath = getRequestPath(request);
// static files
if (urlPath == "/favicon.ico") {
QFile icon("://html/favicon.ico");
if (icon.open(QIODevice::ReadOnly)) {
socket->write(HEADER_ICO.toUtf8());
socket->write(icon.readAll());
icon.close();
}
}
else if (urlPath == "/style.css") {
QFile css("://html/style.css");
if (css.open(QIODevice::ReadOnly)) {
socket->write(HEADER_CSS.toUtf8());
socket->write(css.readAll());
css.close();
}
}
else if (urlPath == "/robots.txt") {
socket->write(HEADER_HTML.toUtf8());
socket->write("Go out, mr.Robot.");
}
else if (urlPath.endsWith(".svg")) {
QFile svg("://html"+urlPath);
if (svg.open(QIODevice::ReadOnly)) {
socket->write(HEADER_SVG.toUtf8());
socket->write(svg.readAll());
svg.close();
}
else {
socket->write(HEADER_404.toUtf8());
socket->write("<center><h1>NOT FOUND</H1></center>");
}
}
// dynamic page
else {
writeMainPage(socket, urlPath);
}
socket->disconnectFromHost();
}
void HttpServer::ircBotFirstInfo(QString server, QStringList channels)
{
for (const auto &c: channels) {
m_backendInfo[server][c] = QStringList();
}
}
void HttpServer::ircClientAction(QString server, QString channel, QStringList users)
{
if (server.isEmpty()) return;
m_backendInfo[server][channel] = users;
}
void HttpServer::ircServerOnline(QString server, quint8 status)
{
if (server.isEmpty()) return;
bool online = status;
m_serversOnline[server] = online;
}
void HttpServer::ircBotNic(QString server, QString nickname)
{
m_botsNick[server] = nickname;
}
QString HttpServer::getRequestPath(const QString &req)
{
if (req.isEmpty()) return QString();
QString result(req);
int begin = result.indexOf(' ');
if (begin == -1) return QString();
result.remove(0, begin+1);
int space = result.indexOf(' ');
int size = result.size();
result.remove(space, size-space);
result = QByteArray::fromPercentEncoding(result.toUtf8());
return result;
}
QString HttpServer::getWordFromPath(const QString &path)
{
QString result {path};
if (result.startsWith('/')) {
result.remove(QRegularExpression("^/"));
}
result.remove(QRegularExpression("/.*$"));
return result;
}
void HttpServer::writeErrorPage(QTcpSocket *socket)
{
socket->write(HEADER_404.toUtf8());
socket->write("<title>404</title><center><h1>NOT FOUND</H1></center>");
}
void HttpServer::replaceTag(QString &page, const QString &tag, const QString &payload)
{
page.replace("{{"+tag+"}}", payload);
}
void HttpServer::writeMainPage(QTcpSocket *socket, QString &urlPath)
{
if (urlPath == "/") {
urlPath += m_mainChannel;
}
QFile main("://html/main.html");
QString page;
if (main.open(QIODevice::ReadOnly)) {
page = main.readAll();
main.close();
}
else {
socket->write(HEADER_404.toUtf8());
socket->write("<title>Critical error</title><center><h1>NOT FOUND</H1><p>Maybe it is compilation error</p></center>");
}
QString server = getWordFromPath(urlPath);
QDir fsPath(m_logFolder+server);
if (not fsPath.exists()) {
writeErrorPage(socket);
return;
}
urlPath.remove(QRegularExpression("^.*/"+server));
QString channel = getWordFromPath(urlPath);
channel.remove(QRegularExpression("\\?.*$"));
if (channel.isEmpty()){
// First channel if not passed directly
QDirIterator it(fsPath.path());
while (it.hasNext()) {
channel = it.next();
if (channel.endsWith(".") or channel.endsWith("..")) continue; // QDir::NoDotAndNoDotDot not works!
while(channel.contains('/')) {
channel.remove(QRegularExpression("^.*/"));
}
break;
}
}
if (not fsPath.cd(channel)) {
writeErrorPage(socket);
return;
}
QString originalServerName;
for (const auto &s: m_backendInfo) {
if (global::toLowerAndNoSpaces(s.first) == server) {
originalServerName = s.first;
}
}
QString originalChannelName;
for (const auto &server: m_backendInfo) {
for (const auto &channel_users: server.second) {
if (global::toLowerAndNoSpaces(channel_users.first) == "#"+channel) {
originalChannelName = global::toLowerAndNoSpaces(channel_users.first);
}
}
}
urlPath.remove(QRegularExpression("^.*/"+channel));
QString year = getWordFromPath(urlPath);
year.remove(QRegularExpression("\\?.*$"));
QString month;
QString day;
if (not year.isEmpty() and fsPath.cd(year)) {
urlPath.remove(QRegularExpression("^.*/"+year));
month = getWordFromPath(urlPath);
month.remove(QRegularExpression("\\?.*$"));
if (not month.isEmpty() and fsPath.cd(month)) {
urlPath.remove(QRegularExpression("^.*/"+month));
day = getWordFromPath(urlPath);
day.remove(QRegularExpression("\\?.*$"));
if (not QFile::exists(fsPath.path()+global::slash+day+".txt")) {
day.clear();
}
}
else { month.clear(); }
}
else { year.clear(); }
//// Left menu compilation
QString htmlServersSectionS;
for (const auto &s: m_backendInfo) {
if (s.first.isEmpty()) continue; // empty server name?
QString htmlServersSection = HTML_SERVER_SECTION;
replaceTag(htmlServersSection, "SERVER_NAME", s.first);
QString htmlChannelLineS;
for (const auto &c: s.second) {
QString htmlChannelLine;
if (originalServerName == s.first and originalChannelName == c.first) {
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL_SELECTED;
} else {
htmlChannelLine = HTML_SERVER_SECTION_CHANNEL;
}
replaceTag(htmlChannelLine, "CHANNEL_NAME", c.first);
QString channelNameForUrl {c.first};
channelNameForUrl.remove('#');
QString channelLink = "/" + global::toLowerAndNoSpaces(s.first) + "/" + channelNameForUrl;
replaceTag(htmlChannelLine, "CHANNEL_LINK", channelLink);
htmlChannelLineS += htmlChannelLine;
}
replaceTag(htmlServersSection, "CHANNELS", htmlChannelLineS);
bool online {false};
for (const auto &srv: m_serversOnline) {
if (srv.first == s.first) {
online = srv.second;
break;
}
}
if (online) {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_ONLINE_MARKER);
} else {
replaceTag(htmlServersSection, "ONLINE_STATUS", HTML_SERVER_OFFLINE_MARKER);
}
htmlServersSectionS += htmlServersSection;
}
replaceTag(page, "SERVERS_SECTION", htmlServersSectionS);
//// Main section header compilation
if (m_backendInfo.size() > 1) {
replaceTag(page, "PAGE_TITLE", originalChannelName + " ("+originalServerName+") | IRCaBot");
} else {
replaceTag(page, "PAGE_TITLE", originalChannelName + " | IRCaBot");
}
replaceTag(page, "MAIN_HEADER", originalChannelName);
QString middlePath = "/";
if (not year.isEmpty()) {
middlePath += "<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"/"+year+"\">" + year + "</a>";
if (not month.isEmpty()) {
middlePath += "/<a style=\"text-decoration: none\" href=\"/"+server+"/"+channel+"/"+year+"/"+month+"\">" + month + "</a>";
if (not day.isEmpty()) {
middlePath += "/" + day;
}
}
}
int currentOnline = m_backendInfo[originalServerName][originalChannelName].size();
if (currentOnline > 0) currentOnline -= 1;
replaceTag(page, "ONLINE", QString::number(currentOnline));
QString onlineUserS;
for (const auto &user: m_backendInfo[originalServerName][originalChannelName]) {
if (QRegularExpression("^.?"+m_botsNick[originalServerName]+"$").match(user).hasMatch()) {
continue;
}
QString onlineUser = HTML_ONLINE_POINT;
replaceTag(onlineUser, "NICKNAME", user);
onlineUserS += onlineUser;
}
replaceTag(page, "ONLINE_LIST", onlineUserS);
if (middlePath == "/") {
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName);
} else if (month.isEmpty()) {
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName + "/" + year);
} else {
replaceTag(page, "SEARCH_PLACEHOLDER", originalChannelName + "/" + year + "/" + month);
}
//// Main section body compilation
QString payloadBlock;
QString searchRequest = global::getValue(urlPath, "toSearch", global::eForWeb);
// Search request
if (not searchRequest.isEmpty()) {
QRegularExpression userRgx(searchRequest);
bool rgxIsValid = false;
if (userRgx.isValid()) rgxIsValid = true;
consoleLog("Search request (" + server + "): " + searchRequest);
if (not day.isEmpty()) {
middlePath.remove(QRegularExpression("/[0-9]{2}$"));
}
middlePath += " ("+searchRequest+")";
QStringList paths;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString currentPath = it.next();
if (currentPath.endsWith(".") or currentPath.endsWith("..")) continue;
QString logFolder = m_logFolder;
#ifdef WIN32
logFolder.replace('\\', '/');
#endif
QString server {currentPath}; // Folder wich is not server folder is ignored
server.remove(QRegularExpression("^"+logFolder));
server.remove(QRegularExpression("/.*$"));
bool serverIsOk = false;
for (const auto &srv: m_backendInfo) {
if (global::toLowerAndNoSpaces(srv.first) == server) {
serverIsOk = true;
break;
}
}
if (not serverIsOk) continue;
QString currentChannel {currentPath}; // Folder wich is not channel folder is ignored
currentChannel.remove(QRegularExpression("^"+logFolder+"[^/]*/"));
currentChannel.remove(QRegularExpression("/.*$"));
bool channelIsOk = false;
for (const auto &ch: m_backendInfo[originalServerName]) {
QString searchChan {ch.first};
searchChan.remove('#');
if (searchChan == currentChannel) {
channelIsOk = true;
break;
}
}
if (not channelIsOk) continue;
paths.push_back(currentPath);
}
if (paths.isEmpty()) {
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Not found");
replaceTag(payloadBlock, "ERROR_TEXT", "");
}
else {
QStringList matchedPaths;
if (not month.isEmpty()) {
for (const auto& path: paths) {
if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(path).hasMatch()) continue;
QFile file(path);
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
continue;
}
QString buffer {file.readAll()};
file.close();
if (rgxIsValid) {
if (QRegularExpression(searchRequest).match(buffer).hasMatch()) {
matchedPaths.push_back(path);
}
} else {
if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
matchedPaths.push_back(path);
}
}
}
}
else if (month.isEmpty() and not year.isEmpty()){
for (const auto &p: paths) {
QStringList slavePaths;
QDirIterator it(p);
while (it.hasNext()) {
QString fileName = it.next();
if (fileName.endsWith(".") or fileName.endsWith("..")) continue;
if (not QRegularExpression("\\.txt$").match(fileName).hasMatch()) continue;
slavePaths.push_back(fileName);
}
for (const auto &path: slavePaths) {
if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(path).hasMatch()) continue;
QFile file(path);
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
continue;
}
QString buffer {file.readAll()};
file.close();
if (rgxIsValid) {
if (QRegularExpression(searchRequest).match(buffer).hasMatch()) {
matchedPaths.push_back(path);
}
} else {
if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
matchedPaths.push_back(path);
}
}
}
}
}
else { // root directory
QStringList yearPaths;
for (const auto& p: paths) {
if (not QRegularExpression("/2[0-9]{3}$").match(p).hasMatch()) continue;
yearPaths.push_back(p);
}
/* If you are reading this code, maybe you are crying now.
* Sorry me, man (woman/forgot/etc). Maybe I'll refactor this hell in the future.
* acetone.
*/
QStringList fileNameS;
for (const auto& p: yearPaths) {
QStringList slavePaths;
QDirIterator it(p);
while (it.hasNext()) {
QString folderName = it.next();
if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
if (not QRegularExpression("/[0-9]{2}$").match(folderName).hasMatch()) continue;
slavePaths.push_back(folderName);
}
for (const auto &path: slavePaths) {
QDirIterator itMonth(path);
while (itMonth.hasNext()) {
QString fileName = itMonth.next();
if (fileName.endsWith(".") or fileName.endsWith("..")) continue;
if (not QRegularExpression("^.*[0-9]{2}\\.txt$").match(fileName).hasMatch()) continue;
fileNameS.push_back(fileName);
}
}
}
for (const auto& path: fileNameS) {
QFile file(path);
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
continue;
}
QString buffer {file.readAll()};
file.close();
if (rgxIsValid) {
if (QRegularExpression(searchRequest).match(buffer).hasMatch()) {
matchedPaths.push_back(path);
}
} else {
if (buffer.contains(searchRequest, Qt::CaseInsensitive)) {
matchedPaths.push_back(path);
}
}
}
}
if (matchedPaths.isEmpty()) {
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Not found");
replaceTag(payloadBlock, "ERROR_TEXT", "");
}
else {
for (auto& point: matchedPaths) {
QString logFolder {m_logFolder};
logFolder.remove(QRegularExpression(".$"));
point.remove(logFolder);
point.remove(QRegularExpression("\\.txt$"));
QString finded = HTML_PAYLOAD_LIST_POINT_MESSAGE;
replaceTag(finded, "POINT_LINK", point);
point.remove(QRegularExpression("^.*"+channel));
replaceTag(finded, "POINT_CONTENT", point);
payloadBlock += finded;
}
}
}
}
// Simple log explorer
else {
if (year.isEmpty()) { // /
QStringList folderNameS;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString folderName = it.next();
if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
while(folderName.contains('/')) {
folderName.remove(QRegularExpression("^.*/"));
}
folderName.remove(QRegularExpression("\\.txt$"));
folderNameS << folderName;
}
if (not folderNameS.isEmpty()) {
for (const auto &f: folderNameS) {
QString onePoint = HTML_PAYLOAD_LIST_POINT_FOLDER;
replaceTag(onePoint, "POINT_CONTENT", f);
replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+f);
payloadBlock += onePoint;
}
}
}
else if (not year.isEmpty() and month.isEmpty()) { // /YYYY
QStringList folderNameS;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString folderName = it.next();
if (folderName.endsWith(".") or folderName.endsWith("..")) continue;
while(folderName.contains('/')) {
folderName.remove(QRegularExpression("^.*/"));
}
folderName.remove(QRegularExpression("\\.txt$"));
folderNameS << folderName;
}
if (not folderNameS.isEmpty()) {
for (const auto &f: folderNameS) {
QString onePoint = HTML_PAYLOAD_LIST_POINT_FOLDER;
replaceTag(onePoint, "POINT_CONTENT", f);
replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+year+"/"+f);
payloadBlock += onePoint;
}
}
}
else if (not month.isEmpty() and day.isEmpty()) { // /YYYY/MM
QStringList fileNameS;
QDirIterator it(fsPath.path());
while (it.hasNext()) {
QString fileName = it.next();
if (fileName.endsWith(".") or fileName.endsWith("..")) continue; // QDir::NoDotAndNoDotDot not works!
while(fileName.contains('/')) {
fileName.remove(QRegularExpression("^.*/"));
}
fileName.remove(QRegularExpression("\\.txt$"));
fileNameS << fileName;
}
if (not fileNameS.isEmpty()) {
for (const auto &a: fileNameS) {
QString onePoint = HTML_PAYLOAD_LIST_POINT_MESSAGE;
replaceTag(onePoint, "POINT_CONTENT", a);
replaceTag(onePoint, "POINT_LINK", "/"+server+"/"+channel+"/"+year+"/"+month+"/"+a);
payloadBlock += onePoint;
}
}
}
else if (not day.isEmpty()) { // /YYYY/MM/dd
QFile file(fsPath.path()+global::slash+day+".txt");
if (not file.open(QIODevice::ReadOnly)) {
consoleLog("Error! I can't open log file " + fsPath.path());
payloadBlock = HTML_PAYLOAD_ERROR;
replaceTag(payloadBlock, "ERROR_TITLE", "Internal error");
replaceTag(payloadBlock, "ERROR_TEXT", "Requested log file openning failed");
}
else {
QString buffer = file.readLine();
while (not buffer.isEmpty()) {
buffer.remove('\n');
buffer.remove('\r'); // perebzdel
buffer.remove('\t'); //
QString nick {buffer};
nick.remove(QRegularExpression("\\]\\s.*$"));
nick.remove(QRegularExpression("^\\["));
if (nick.isEmpty()) continue;
QString text {buffer};
text.remove(QRegularExpression("^\\[.*\\]\\s"));
text.replace(' ', "&nbsp;"); // for long spaces (code examples, etc)
if (text.isEmpty()) continue;
QString message = HTML_PAYLOAD_LIST_CHAT_MESSAGE;
for (const auto &user: m_backendInfo[originalServerName][originalChannelName]) {
if (QRegularExpression("^.?"+nick+"$").match(user).hasMatch()) {
message.replace("<div class=\"main_payload__chat_username\">",
"<div class=\"main_payload__chat_username\" style=\"color: green\">");
break;
}
}
replaceTag(message, "USERNAME", nick);
replaceTag(message, "MESSAGE_TEXT", text);
payloadBlock += message;
buffer = file.readLine();
}
file.close();
}
}
}
replaceTag(page, "MIDDLE_PATH", middlePath);
replaceTag(page, "PAYLOAD_BLOCK", payloadBlock);
//// Footer
replaceTag(page, "VERSION", IRCABOT_VERSION);
replaceTag(page, "COPYRIGHT_YEAR", COPYRIGHT_YEAR);
socket->write(HEADER_HTML.toUtf8());
socket->write(page.toUtf8());
}