From 3426433a22b4e7f3bce3acea9075e8796f8131c7 Mon Sep 17 00:00:00 2001 From: 0xFEEDC0DE64 <0xFEEDC0DE64@gmail.com> Date: Mon, 17 Sep 2018 19:29:29 +0200 Subject: [PATCH] Imported existing sources --- .gitmodules | 15 ++ DbWebserver.pro | 8 ++ plugins/fileserverplugin | 1 + plugins/helloworldplugin | 1 + plugins/plugin.pri | 5 + plugins/plugins.pro | 9 ++ plugins/proxyplugin | 1 + plugins/seriesplugin | 1 + plugins/wifilampplugin | 1 + webserver/main.cpp | 121 ++++++++++++++++ webserver/webserver.json | 92 ++++++++++++ webserver/webserver.pro | 22 +++ webserverlib/httpclientconnection.cpp | 193 ++++++++++++++++++++++++++ webserverlib/httpclientconnection.h | 43 ++++++ webserverlib/httpcontainers.cpp | 75 ++++++++++ webserverlib/httpcontainers.h | 101 ++++++++++++++ webserverlib/utils.cpp | 47 +++++++ webserverlib/utils.h | 60 ++++++++ webserverlib/webapplication.cpp | 7 + webserverlib/webapplication.h | 14 ++ webserverlib/weblistener.cpp | 93 +++++++++++++ webserverlib/weblistener.h | 35 +++++ webserverlib/webplugin.cpp | 6 + webserverlib/webplugin.h | 21 +++ webserverlib/webserverlib.pro | 30 ++++ webserverlib/webserverlib_global.h | 9 ++ 26 files changed, 1011 insertions(+) create mode 100644 .gitmodules create mode 100644 DbWebserver.pro create mode 160000 plugins/fileserverplugin create mode 160000 plugins/helloworldplugin create mode 100644 plugins/plugin.pri create mode 100644 plugins/plugins.pro create mode 160000 plugins/proxyplugin create mode 160000 plugins/seriesplugin create mode 160000 plugins/wifilampplugin create mode 100644 webserver/main.cpp create mode 100644 webserver/webserver.json create mode 100644 webserver/webserver.pro create mode 100644 webserverlib/httpclientconnection.cpp create mode 100644 webserverlib/httpclientconnection.h create mode 100644 webserverlib/httpcontainers.cpp create mode 100644 webserverlib/httpcontainers.h create mode 100644 webserverlib/utils.cpp create mode 100644 webserverlib/utils.h create mode 100644 webserverlib/webapplication.cpp create mode 100644 webserverlib/webapplication.h create mode 100644 webserverlib/weblistener.cpp create mode 100644 webserverlib/weblistener.h create mode 100644 webserverlib/webplugin.cpp create mode 100644 webserverlib/webplugin.h create mode 100644 webserverlib/webserverlib.pro create mode 100644 webserverlib/webserverlib_global.h diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..467c506 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,15 @@ +[submodule "plugins/fileserverplugin"] + path = plugins/fileserverplugin + url = https://github.com/0xFEEDC0DE64/DbWebserver-fileserverplugin.git +[submodule "plugins/helloworldplugin"] + path = plugins/helloworldplugin + url = https://github.com/0xFEEDC0DE64/DbWebserver-helloworldplugin.git +[submodule "plugins/proxyplugin"] + path = plugins/proxyplugin + url = https://github.com/0xFEEDC0DE64/DbWebserver-proxyplugin.git +[submodule "plugins/seriesplugin"] + path = plugins/seriesplugin + url = https://github.com/0xFEEDC0DE64/DbWebserver-seriesplugin.git +[submodule "plugins/wifilampplugin"] + path = plugins/wifilampplugin + url = https://github.com/0xFEEDC0DE64/DbWebserver-wifilampplugin.git diff --git a/DbWebserver.pro b/DbWebserver.pro new file mode 100644 index 0000000..5e55abd --- /dev/null +++ b/DbWebserver.pro @@ -0,0 +1,8 @@ +TEMPLATE = subdirs + +SUBDIRS += webserver \ + webserverlib \ + plugins + +webserver.depends += webserverlib +plugins.depends += webserverlib diff --git a/plugins/fileserverplugin b/plugins/fileserverplugin new file mode 160000 index 0000000..9807016 --- /dev/null +++ b/plugins/fileserverplugin @@ -0,0 +1 @@ +Subproject commit 98070165b26f0dc31e27f1e98535a0ce608e1786 diff --git a/plugins/helloworldplugin b/plugins/helloworldplugin new file mode 160000 index 0000000..6d7c8d7 --- /dev/null +++ b/plugins/helloworldplugin @@ -0,0 +1 @@ +Subproject commit 6d7c8d789f85c2e8053229e166bd5b9575fb0193 diff --git a/plugins/plugin.pri b/plugins/plugin.pri new file mode 100644 index 0000000..695fb09 --- /dev/null +++ b/plugins/plugin.pri @@ -0,0 +1,5 @@ +PROJECT_ROOT = ../../.. +TEMPLATE = lib +CONFIG += shared +DESTDIR = $${OUT_PWD}/$${PROJECT_ROOT}/bin/plugins/webserver +include(../../project.pri) diff --git a/plugins/plugins.pro b/plugins/plugins.pro new file mode 100644 index 0000000..d20b583 --- /dev/null +++ b/plugins/plugins.pro @@ -0,0 +1,9 @@ +TEMPLATE = subdirs + +SUBDIRS += fileserverplugin \ + helloworldplugin \ + proxyplugin \ + seriesplugin \ + wifilampplugin + +OTHER_FILES += plugin.pri diff --git a/plugins/proxyplugin b/plugins/proxyplugin new file mode 160000 index 0000000..795d191 --- /dev/null +++ b/plugins/proxyplugin @@ -0,0 +1 @@ +Subproject commit 795d191f27260bf5d8190f3f9ce8c05209565976 diff --git a/plugins/seriesplugin b/plugins/seriesplugin new file mode 160000 index 0000000..5bcabbd --- /dev/null +++ b/plugins/seriesplugin @@ -0,0 +1 @@ +Subproject commit 5bcabbdce058fd803503126708d05f8b04372ca9 diff --git a/plugins/wifilampplugin b/plugins/wifilampplugin new file mode 160000 index 0000000..d9d5f7f --- /dev/null +++ b/plugins/wifilampplugin @@ -0,0 +1 @@ +Subproject commit d9d5f7ff89f89517b28f3140f0508f8b886d4351 diff --git a/webserver/main.cpp b/webserver/main.cpp new file mode 100644 index 0000000..9fc2038 --- /dev/null +++ b/webserver/main.cpp @@ -0,0 +1,121 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "utils.h" +#include "webplugin.h" +#include "weblistener.h" +#include "webapplication.h" + +int main(int argc, char **argv) +{ + QCoreApplication app(argc, argv); + QCoreApplication::setApplicationName("webserver"); + + QHash plugins; + + { + QDir dir(QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(QStringLiteral("plugins/") + QCoreApplication::applicationName())); + for(const auto &fileInfo : dir.entryInfoList(QDir::Files | QDir::NoSymLinks)) + { + if(!QLibrary::isLibrary(fileInfo.filePath())) + { + qWarning() << "skipping" << fileInfo.fileName() << "because no QLibrary"; + continue; // to skip windows junk files + } + + QPluginLoader pluginLoader(fileInfo.filePath()); + if(!pluginLoader.load()) + { + qCritical() << "error loading plugin" << fileInfo.fileName() << "because" << pluginLoader.errorString(); + continue; + } + + if(auto plugin = qobject_cast(pluginLoader.instance())) + { + const auto pluginName = plugin->pluginName(); + if(plugins.contains(pluginName)) + throw std::runtime_error(QString("duplicate plugin %0").arg(pluginName).toStdString()); + plugins.insert(pluginName, plugin); + } + else + qCritical() << "plugin" << fileInfo.fileName() << "could not be casted to WebPlugin"; + } + } + + QHash applications; + + const auto configPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(QCoreApplication::applicationName() + QStringLiteral(".json")); + const auto config = getJson(configPath); + + if(!config.contains(QStringLiteral("applications"))) + throw std::runtime_error("settings does not contain a applications"); + + { + const auto applicationsVal = config.value(QStringLiteral("applications")); + if(!applicationsVal.isObject()) + throw std::runtime_error("applications is not a json object"); + + const auto applicationsList = applicationsVal.toObject(); + for(auto iter = applicationsList.constBegin(); iter != applicationsList.constEnd(); iter++) + { + const auto applicationVal = iter.value(); + if(!applicationVal.isObject()) + throw std::runtime_error(QString("application %0 is not a json object").arg(iter.key()).toStdString()); + + auto application = applicationVal.toObject(); + if(!application.contains(QStringLiteral("_pluginName"))) + throw std::runtime_error(QString("application %0 does not contain a _pluginName").arg(iter.key()).toStdString()); + + const auto pluginNameVal = application.take(QStringLiteral("_pluginName")); + if(!pluginNameVal.isString()) + throw std::runtime_error(QString("application %0 pluginName is not a string").arg(iter.key()).toStdString()); + + auto pluginName = pluginNameVal.toString(); + + const auto pluginsIter = plugins.find(pluginName); + if(pluginsIter == plugins.constEnd()) + throw std::runtime_error(QString("application %0 references not installed plugin %1").arg(iter.key(), pluginName).toStdString()); + + applications.insert(iter.key(), pluginsIter.value()->createApplication(application)); + } + } + + QList listeners; + + if(!config.contains(QStringLiteral("listeners"))) + throw std::runtime_error("settings does not contain a listeners"); + + { + const auto listenersVal = config.value(QStringLiteral("listeners")); + if(!listenersVal.isArray()) + throw std::runtime_error("listeners is not a json array"); + + const auto listenersList = listenersVal.toArray(); + for(auto iter = listenersList.constBegin(); iter != listenersList.constEnd(); iter++) + { + const auto listenerVal = *iter; + if(!listenerVal.isObject()) + throw std::runtime_error(QString("listener %0 is not an object").arg(std::distance(listenersList.constBegin(), iter)).toStdString()); + + const auto listener = listenerVal.toObject(); + + listeners.append(new WebListener(listener, applications)); + } + } + + for(auto iter = applications.constBegin(); iter != applications.constEnd(); iter++) + iter.value()->start(); + + for(auto listener : listeners) + listener->start(); + + return app.exec(); +} diff --git a/webserver/webserver.json b/webserver/webserver.json new file mode 100644 index 0000000..31d3790 --- /dev/null +++ b/webserver/webserver.json @@ -0,0 +1,92 @@ +{ + "applications": { + "Fallback": { + "_pluginName": "fileserver", + "rootPath": "htdocs/__fallback" + }, + "HelloWorld": { + "_pluginName": "helloworld" + }, + "1000serien.com": { + "_pluginName": "series", + "mysql": { + "hostname": "localhost", + "username": "series", + "password": "_stripped_", + "database": "series" + } + }, + "cdn.1000serien.com": { + "_pluginName": "fileserver", + "rootPath": "/komposthaufen/multimedia/Videos" + }, + "brunner.ninja": { + "_pluginName": "fileserver", + "rootPath": "htdocs/brunner.ninja" + }, + "telegram.brunner.ninja": { + "_pluginName": "fileserver", + "rootPath": "htdocs/telegram.brunner.ninja/dist" + }, + "transmission.brunner.ninja": { + "_pluginName": "proxy", + "url": "http://127.0.0.1:9091/" + }, + "findtheinvisiblegspot.com": { + "_pluginName": "fileserver", + "rootPath": "htdocs/findtheinvisiblegspot.com" + }, + "flucky.xyz": { + "_pluginName": "fileserver", + "rootPath": "htdocs/flucky.xyz" + }, + "mail.flucky.xyz": { + "_pluginName": "fileserver", + "rootPath": "/etc/webapps/roundcubemail" + }, + "phpmyadmin.flucky.xyz": { + "_pluginName": "fileserver", + "rootPath": "/usr/share/webapps/phpMyAdmin" + }, + "localhorst.xyz": { + "_pluginName": "fileserver", + "rootPath": "htdocs/localhorst.xyz" + }, + "maik-mahlow.de": { + "_pluginName": "fileserver", + "rootPath": "htdocs/maik-mahlow.de" + }, + "WifiLamp": { + "_pluginName": "wifilamp", + "controlHostAdress": "QHostAddress::Any", + "controlPort": 1234 + } + }, + "listeners": [{ + "hostAddress": "QHostAddress::Any", + "port": 8080, + "vhosts": { + "*": "Fallback", + "1000serien.com": "1000serien.com", + "www.1000serien.com": "1000serien.com", + "cdn.1000serien.com": "cdn.1000serien.com", + "brunner.ninja": "brunner.ninja", + "www.brunner.ninja": "brunner.ninja", + "telegram.brunner.ninja": "telegram.brunner.ninja", + "transmission.brunner.ninja": "transmission.brunner.ninja", + "findtheinvisiblegspot.com": "findtheinvisiblegspot.com", + "www.findtheinvisiblegspot.com": "findtheinvisiblegspot.com", + "flucky.xyz": "flucky.xyz", + "www.flucky.xyz": "flucky.xyz", + "mail.flucky.xyz": "mail.flucky.xyz", + "phpmyadmin.flucky.xyz": "phpmyadmin.flucky.xyz", + "localhorst.xyz": "localhorst.xyz", + "www.localhorst.xyz": "localhorst.xyz", + "maik-mahlow.de": "maik-mahlow.de", + "www.maik-mahlow.de": "maik-mahlow.de", + "flucky-server": "HelloWorld", + "192.168.0.2": "HelloWorld", + "lampen": "WifiLamp" + } + }] +} diff --git a/webserver/webserver.pro b/webserver/webserver.pro new file mode 100644 index 0000000..60dff67 --- /dev/null +++ b/webserver/webserver.pro @@ -0,0 +1,22 @@ +QT += core network +QT -= gui widgets + +DBLIBS += webserverlib + +PROJECT_ROOT = ../.. + +SOURCES += main.cpp + +HEADERS += + +FORMS += + +RESOURCES += + +TRANSLATIONS += + +configinstall.path = $${OUT_PWD}/$${PROJECT_ROOT}/bin +configinstall.files = webserver.json +INSTALLS += configinstall + +include($${PROJECT_ROOT}/app.pri) diff --git a/webserverlib/httpclientconnection.cpp b/webserverlib/httpclientconnection.cpp new file mode 100644 index 0000000..53c74f6 --- /dev/null +++ b/webserverlib/httpclientconnection.cpp @@ -0,0 +1,193 @@ +#include "httpclientconnection.h" + +#include +#include +#include + +#include "weblistener.h" + +HttpClientConnection::HttpClientConnection(QTcpSocket &socket, WebListener &httpServer) : + QObject(&httpServer), + m_socket(socket), + m_webListener(httpServer), + m_state(RequestLine), + m_bodyLength(-1) +{ + m_socket.setParent(this); + + connect(&m_socket, &QIODevice::readyRead, this, &HttpClientConnection::readyRead); + connect(&m_socket, &QTcpSocket::disconnected, this, &QObject::deleteLater); +} + +void HttpClientConnection::sendResponse(const HttpResponse &response) +{ + if(m_state != WaitingForResponse) + { + qCritical() << "sending a response now is not allowed!"; + return; + } + + QTextStream stream(&m_socket); + stream << response.protocol << ' ' << int(response.statusCode) << ' ' << response.statusString() << endl; + + for(auto iter = response.headers.constBegin(); iter != response.headers.constEnd(); iter++) + stream << iter.key() << ": " << iter.value() << endl; + + stream << endl; +} + +void HttpClientConnection::sendResponse(HttpResponse response, const QByteArray &byteArray) +{ + if(m_state != WaitingForResponse) + { + qCritical() << "sending a response now is not allowed!"; + return; + } + + response.headers.insert(QStringLiteral("Content-Length"), QString::number(byteArray.length())); + sendResponse(response); + m_socket.write(byteArray); + m_state = RequestLine; +} + +void HttpClientConnection::sendResponse(HttpResponse response, const QString &string) +{ + if(m_state != WaitingForResponse) + { + qCritical() << "sending a response now is not allowed!"; + return; + } + + sendResponse(response, string.toUtf8()); + m_state = RequestLine; +} + +void HttpClientConnection::sendResponse(HttpResponse response, std::unique_ptr &&device) +{ + if(m_state != WaitingForResponse) + throw std::runtime_error("sending a response now is not allowed!"); + + if(!device->isReadable()) + throw std::runtime_error("device is not readable"); + + if(device->isSequential()) + throw std::runtime_error("sequental device not supported yet"); + + m_sendingDeivce = std::move(device); + + response.headers.insert(QStringLiteral("Content-Length"), QString::number(m_sendingDeivce->size())); + + sendResponse(response); + + connect(&m_socket, &QIODevice::bytesWritten, this, &HttpClientConnection::bytesWritten); + bytesWritten(); + + m_state = SendingResponse; +} + +void HttpClientConnection::readyRead() +{ + m_buffer.append(m_socket.readAll()); + + switch(m_state) + { + case RequestLine: + case Headers: + { + int index; + while((index = m_buffer.indexOf(QByteArrayLiteral("\r\n"))) != -1) + { + QString line(m_buffer.left(index)); + m_buffer.remove(0, index + 2); + + switch(m_state) + { + case RequestLine: + { + auto parts = line.split(' '); + Q_ASSERT(parts.count() == 3); + + m_request.method = parts.at(0); + m_request.path = parts.at(1); + m_request.protocol = parts.at(2); + + m_state = Headers; + continue; + } + case Headers: + { + if(!line.isEmpty()) + { + static const QRegularExpression regex(QStringLiteral("^ *([^ :]+) *: *(.*) *$")); + + auto match = regex.match(line); + if(!match.hasMatch()) + qWarning() << "ignoring invalid" << line; + + if(m_request.headers.contains(match.captured(1))) + qWarning() << "duplicate header" << match.captured(1); + + m_request.headers.insert(match.captured(1), match.captured(2)); + } + else + { + if(m_request.headers.contains(QStringLiteral("Content-Length"))) + { + m_bodyLength = m_request.headers.value(QStringLiteral("Content-Length")).toInt(); + m_state = RequestBody; + goto hatschi; + } + else + { + m_state = WaitingForResponse; + m_webListener.handleRequest(this, m_request); + clearRequest(); + } + } + } + } + } + return; + } + case RequestBody: + hatschi: + { + auto length = qMin(m_bodyLength - m_request.body.count(), m_buffer.count()); + m_request.body.append(m_buffer.left(length)); + m_buffer.remove(0, length); + + if(m_request.body.count() == m_bodyLength) + { + if(!m_buffer.isEmpty()) + qCritical() << "received more than expected!"; + + m_state = WaitingForResponse; + m_webListener.handleRequest(this, m_request); + clearRequest(); + } + } + } +} + +void HttpClientConnection::bytesWritten() +{ + if(m_socket.bytesToWrite() >= 1024*1024*4) + return; + + if(m_socket.bytesToWrite() == 0 && m_sendingDeivce->bytesAvailable() == 0) + { + m_state = RequestLine; + disconnect(&m_socket, &QIODevice::bytesWritten, this, &HttpClientConnection::bytesWritten); + m_sendingDeivce.reset(); + return; + } + + auto buffer = m_sendingDeivce->read(1024*1024*4); + m_socket.write(buffer); +} + +void HttpClientConnection::clearRequest() +{ + m_request.headers.clear(); + m_request.body.clear(); +} diff --git a/webserverlib/httpclientconnection.h b/webserverlib/httpclientconnection.h new file mode 100644 index 0000000..2147003 --- /dev/null +++ b/webserverlib/httpclientconnection.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include + +#include + +#include "httpcontainers.h" + +class QTcpSocket; + +class WebListener; + +class HttpClientConnection : public QObject +{ + Q_OBJECT + +public: + explicit HttpClientConnection(QTcpSocket &socket, WebListener &WebListener); + + void sendResponse(const HttpResponse &response); + void sendResponse(HttpResponse response, const QByteArray &byteArray); + void sendResponse(HttpResponse response, const QString &string); + void sendResponse(HttpResponse response, std::unique_ptr &&device); + +private Q_SLOTS: + void readyRead(); + void bytesWritten(); + +private: + void clearRequest(); + + QTcpSocket &m_socket; + WebListener &m_webListener; + + QByteArray m_buffer; + enum { RequestLine, Headers, RequestBody, WaitingForResponse, SendingResponse } m_state; + int m_bodyLength; + + HttpRequest m_request; + + std::unique_ptr m_sendingDeivce; +}; diff --git a/webserverlib/httpcontainers.cpp b/webserverlib/httpcontainers.cpp new file mode 100644 index 0000000..273513c --- /dev/null +++ b/webserverlib/httpcontainers.cpp @@ -0,0 +1,75 @@ +#include "httpcontainers.h" + +QString HttpResponse::statusString() const +{ + switch(statusCode) { + case HttpResponse::StatusCode::Continue: return QStringLiteral("Continue"); + case HttpResponse::StatusCode::SwitchingProtocols: return QStringLiteral("Switching Protocols"); + case HttpResponse::StatusCode::Processing: return QStringLiteral("Processing"); + case HttpResponse::StatusCode::EarlyHints: return QStringLiteral("Early Hints"); + case HttpResponse::StatusCode::OK: return QStringLiteral("OK"); + case HttpResponse::StatusCode::Created: return QStringLiteral("Created"); + case HttpResponse::StatusCode::Accepted: return QStringLiteral("Accepted"); + case HttpResponse::StatusCode::NonAuthoritativeInformation: return QStringLiteral("Non-Authoritative Information"); + case HttpResponse::StatusCode::NoContent: return QStringLiteral("No Content"); + case HttpResponse::StatusCode::ResetContent: return QStringLiteral("Reset Content"); + case HttpResponse::StatusCode::PartialContent: return QStringLiteral("Partial Content"); + case HttpResponse::StatusCode::MultiStatus: return QStringLiteral("Multi-Status"); + case HttpResponse::StatusCode::AlreadyReported: return QStringLiteral("Already Reported"); + case HttpResponse::StatusCode::IMUsed: return QStringLiteral("IM Used"); + case HttpResponse::StatusCode::MultipleChoices: return QStringLiteral("Multiple Choices"); + case HttpResponse::StatusCode::MovedPermanently: return QStringLiteral("Moved Permanently"); + case HttpResponse::StatusCode::Found: return QStringLiteral("Found (Moved Temporarily)"); + case HttpResponse::StatusCode::SeeOther: return QStringLiteral("See Other"); + case HttpResponse::StatusCode::NotModified: return QStringLiteral("Not Modified"); + case HttpResponse::StatusCode::UseProxy: return QStringLiteral("Use Proxy"); + case HttpResponse::StatusCode::SwitchProxy: return QStringLiteral("(reserviert)"); + case HttpResponse::StatusCode::TemporaryRedirect: return QStringLiteral("Temporary Redirect"); + case HttpResponse::StatusCode::PermanentRedirect: return QStringLiteral("Permanent Redirect"); + case HttpResponse::StatusCode::BadRequest: return QStringLiteral("Bad Request"); + case HttpResponse::StatusCode::Unauthorized: return QStringLiteral("Unauthorized"); + case HttpResponse::StatusCode::PaymentRequired: return QStringLiteral("Payment Required"); + case HttpResponse::StatusCode::Forbidden: return QStringLiteral("Forbidden"); + case HttpResponse::StatusCode::NotFound: return QStringLiteral("Not Found"); + case HttpResponse::StatusCode::MethodNotAllowed: return QStringLiteral("Method Not Allowed"); + case HttpResponse::StatusCode::NotAcceptable: return QStringLiteral("Not Acceptable"); + case HttpResponse::StatusCode::ProxyAuthenticationRequired: return QStringLiteral("Proxy Authentication Required"); + case HttpResponse::StatusCode::RequestTimeout: return QStringLiteral("Request Timeout"); + case HttpResponse::StatusCode::Conflict: return QStringLiteral("Conflict"); + case HttpResponse::StatusCode::Gone: return QStringLiteral("Gone"); + case HttpResponse::StatusCode::LengthRequired: return QStringLiteral("Length Required"); + case HttpResponse::StatusCode::PreconditionFailed: return QStringLiteral("Precondition Failed"); + case HttpResponse::StatusCode::RequestEntityTooLarge: return QStringLiteral("Request Entity Too Large"); + case HttpResponse::StatusCode::URITooLong: return QStringLiteral("URI Too Long"); + case HttpResponse::StatusCode::UnsupportedMediaType: return QStringLiteral("Unsupported Media Type"); + case HttpResponse::StatusCode::Requestedrangenotsatisfiable: return QStringLiteral("Requested range not satisfiable"); + case HttpResponse::StatusCode::ExpectationFailed: return QStringLiteral("Expectation Failed"); + case HttpResponse::StatusCode::Imateapot: return QStringLiteral("I’m a teapot"); + case HttpResponse::StatusCode::PolicyNotFulfilled: return QStringLiteral("Policy Not Fulfilled"); + case HttpResponse::StatusCode::MisdirectedRequest: return QStringLiteral("Misdirected Request"); + case HttpResponse::StatusCode::UnprocessableEntity: return QStringLiteral("Unprocessable Entity"); + case HttpResponse::StatusCode::Locked: return QStringLiteral("Locked"); + case HttpResponse::StatusCode::FailedDependency: return QStringLiteral("Failed Dependency"); + case HttpResponse::StatusCode::UnorderedCollection: return QStringLiteral("Unordered Collection"); + case HttpResponse::StatusCode::UpgradeRequired: return QStringLiteral("Upgrade Required"); + case HttpResponse::StatusCode::PreconditionRequired: return QStringLiteral("Precondition Required"); + case HttpResponse::StatusCode::TooManyRequests: return QStringLiteral("Too Many Requests"); + case HttpResponse::StatusCode::RequestHeaderFieldsTooLarge: return QStringLiteral("Request Header Fields Too Large"); + case HttpResponse::StatusCode::NoResponse: return QStringLiteral("No Response"); + case HttpResponse::StatusCode::Therequestshouldberetriedafterdoingtheappropriateaction: return QStringLiteral("The request should be retried after doing the appropriate action"); + case HttpResponse::StatusCode::UnavailableForLegalReasons: return QStringLiteral("Unavailable For Legal Reasons"); + case HttpResponse::StatusCode::ClientClosedRequest: return QStringLiteral("Client Closed Request"); + case HttpResponse::StatusCode::InternalServerError: return QStringLiteral("Internal Server Error"); + case HttpResponse::StatusCode::NotImplemented: return QStringLiteral("Not Implemented"); + case HttpResponse::StatusCode::BadGateway: return QStringLiteral("Bad Gateway"); + case HttpResponse::StatusCode::ServiceUnavailable: return QStringLiteral("Service Unavailable"); + case HttpResponse::StatusCode::GatewayTimeout: return QStringLiteral("Gateway Timeout"); + case HttpResponse::StatusCode::HTTPVersionnotsupported: return QStringLiteral("HTTP Version not supported"); + case HttpResponse::StatusCode::VariantAlsoNegotiates: return QStringLiteral("Variant Also Negotiates"); + case HttpResponse::StatusCode::InsufficientStorage: return QStringLiteral("Insufficient Storage"); + case HttpResponse::StatusCode::LoopDetected: return QStringLiteral("Loop Detected"); + case HttpResponse::StatusCode::BandwidthLimitExceeded: return QStringLiteral("Bandwidth Limit Exceeded"); + case HttpResponse::StatusCode::NotExtended: return QStringLiteral("Not Extended"); + case HttpResponse::StatusCode::NetworkAuthenticationRequired: return QStringLiteral("Network Authentication Required"); + } +} diff --git a/webserverlib/httpcontainers.h b/webserverlib/httpcontainers.h new file mode 100644 index 0000000..615349f --- /dev/null +++ b/webserverlib/httpcontainers.h @@ -0,0 +1,101 @@ +#pragma once + +#include +#include +#include + +struct HttpRequest { + QString method; + QString path; + QString protocol; + QHash headers; + QByteArray body; +}; + +struct HttpResponse { + enum class StatusCode { + // 1xx Informational responses + Continue = 100, // Continue + SwitchingProtocols = 101, // Switching Protocols + Processing = 102, // Processing + EarlyHints = 103, // Early Hints + + // 2xx Success + OK = 200, // OK + Created = 201, // Created + Accepted = 202, // Accepted + NonAuthoritativeInformation = 203, // Non-Authoritative Information + NoContent = 204, // No Content + ResetContent = 205, // Reset Content + PartialContent = 206, // Partial Content + MultiStatus = 207, // Multi-Status + AlreadyReported = 208, // Already Reported + IMUsed = 226, // IM Used + + // 3xx Redirection + MultipleChoices = 300, // Multiple Choices + MovedPermanently = 301, // Moved Permanently + Found = 302, // Found (Moved Temporarily) + SeeOther = 303, // See Other + NotModified = 304, // Not Modified + UseProxy = 305, // Use Proxy + SwitchProxy = 306, // (reserviert) + TemporaryRedirect = 307, // Temporary Redirect + PermanentRedirect = 308, // Permanent Redirect + + // 4xx Client errors + BadRequest = 400, // Bad Request + Unauthorized = 401, // Unauthorized + PaymentRequired = 402, // Payment Required + Forbidden = 403, // Forbidden + NotFound = 404, // Not Found + MethodNotAllowed = 405, // Method Not Allowed + NotAcceptable = 406, // Not Acceptable + ProxyAuthenticationRequired = 407, // Proxy Authentication Required + RequestTimeout = 408, // Request Timeout + Conflict = 409, // Conflict + Gone = 410, // Gone + LengthRequired = 411, // Length Required + PreconditionFailed = 412, // Precondition Failed + RequestEntityTooLarge = 413, // Request Entity Too Large + URITooLong = 414, // URI Too Long + UnsupportedMediaType = 415, // Unsupported Media Type + Requestedrangenotsatisfiable = 416, // Requested range not satisfiable + ExpectationFailed = 417, // Expectation Failed + Imateapot = 418, // I’m a teapot + PolicyNotFulfilled = 420, // Policy Not Fulfilled + MisdirectedRequest = 421, // Misdirected Request + UnprocessableEntity = 422, // Unprocessable Entity + Locked = 423, // Locked + FailedDependency = 424, // Failed Dependency + UnorderedCollection = 425, // Unordered Collection + UpgradeRequired = 426, // Upgrade Required + PreconditionRequired = 428, // Precondition Required + TooManyRequests = 429, // Too Many Requests + RequestHeaderFieldsTooLarge = 431, // Request Header Fields Too Large + NoResponse = 444, // No Response + Therequestshouldberetriedafterdoingtheappropriateaction = 449, // The request should be retried after doing the appropriate action + UnavailableForLegalReasons = 451, // Unavailable For Legal Reasons + ClientClosedRequest = 499, // Client Closed Request + + // 5xx Server errors + InternalServerError = 500, // Internal Server Error + NotImplemented = 501, // Not Implemented + BadGateway = 502, // Bad Gateway + ServiceUnavailable = 503, // Service Unavailable + GatewayTimeout = 504, // Gateway Timeout + HTTPVersionnotsupported = 505, // HTTP Version not supported + VariantAlsoNegotiates = 506, // Variant Also Negotiates + InsufficientStorage = 507, // Insufficient Storage + LoopDetected = 508, // Loop Detected + BandwidthLimitExceeded = 509, // Bandwidth Limit Exceeded + NotExtended = 510, // Not Extended + NetworkAuthenticationRequired = 511, // Network Authentication Required + }; + + QString protocol; + StatusCode statusCode; + QHash headers; + + QString statusString() const; +}; diff --git a/webserverlib/utils.cpp b/webserverlib/utils.cpp new file mode 100644 index 0000000..d0c685e --- /dev/null +++ b/webserverlib/utils.cpp @@ -0,0 +1,47 @@ +#include "utils.h" + +#include +#include + +QHostAddress parseHostAddress(const QString &hostAddress) +{ + static const QMap specialHostAddresses { + { QStringLiteral("QHostAddress::Null"), QHostAddress::Null }, + { QStringLiteral("QHostAddress::Broadcast"), QHostAddress::Broadcast }, + { QStringLiteral("QHostAddress::LocalHost"), QHostAddress::LocalHost }, + { QStringLiteral("QHostAddress::LocalHostIPv6"), QHostAddress::LocalHostIPv6 }, + { QStringLiteral("QHostAddress::Any"), QHostAddress::Any }, + { QStringLiteral("QHostAddress::AnyIPv6"), QHostAddress::AnyIPv6 }, + { QStringLiteral("QHostAddress::AnyIPv4"), QHostAddress::AnyIPv4 } + }; + + const auto iter = specialHostAddresses.find(hostAddress); + if(iter != specialHostAddresses.constEnd()) + return *iter; + + return QHostAddress(hostAddress); +} + +template<> +QJsonDocument getJson(const QJsonDocument &document) +{ + return document; +} + +template<> +QJsonObject getJson(const QJsonDocument &document) +{ + if(!document.isObject()) + throw std::runtime_error("JSON document does not contain an object!"); + + return document.object(); +} + +template<> +QJsonArray getJson(const QJsonDocument &document) +{ + if(!document.isArray()) + throw std::runtime_error("JSON document does not contain an object!"); + + return document.array(); +} diff --git a/webserverlib/utils.h b/webserverlib/utils.h new file mode 100644 index 0000000..41ff562 --- /dev/null +++ b/webserverlib/utils.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include + +#include + +QHostAddress parseHostAddress(const QString &hostAddress); + +template +T getJson(const QJsonDocument &document); + +template<> +QJsonDocument getJson(const QJsonDocument &document); + +template<> +QJsonObject getJson(const QJsonDocument &document); + +template<> +QJsonArray getJson(const QJsonDocument &document); + +template +T getJson(const QByteArray &byteArray); + +template +T getJson(QIODevice &device); + +template +T getJson(const QString &filename); + + + +template +T getJson(const QByteArray &byteArray) +{ + QJsonParseError error; + auto document = QJsonDocument::fromJson(byteArray, &error); + if(error.error != QJsonParseError::NoError) + throw std::runtime_error(QString("Could not parse json: %0").arg(error.errorString()).toStdString()); + + return getJson(document); +} + +template +T getJson(QIODevice &device) +{ + return getJson(device.readAll()); +} + +template +T getJson(const QString &filename) +{ + QFile file(filename); + if(!file.open(QIODevice::ReadOnly|QIODevice::Text)) + throw std::runtime_error(QString("Could not open json file %0: %1").arg(filename, file.errorString()).toStdString()); + + return getJson(file); +} diff --git a/webserverlib/webapplication.cpp b/webserverlib/webapplication.cpp new file mode 100644 index 0000000..2a17369 --- /dev/null +++ b/webserverlib/webapplication.cpp @@ -0,0 +1,7 @@ +#include "webapplication.h" + +WebApplication::WebApplication(QObject *parent) : + QObject(parent) +{ + +} diff --git a/webserverlib/webapplication.h b/webserverlib/webapplication.h new file mode 100644 index 0000000..0751176 --- /dev/null +++ b/webserverlib/webapplication.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "webserverlib_global.h" + +class WEBSERVERLIB_EXPORT WebApplication : public QObject +{ + Q_OBJECT + +public: + WebApplication(QObject *parent = Q_NULLPTR); + + virtual void start() = 0; +}; diff --git a/webserverlib/weblistener.cpp b/webserverlib/weblistener.cpp new file mode 100644 index 0000000..42b580a --- /dev/null +++ b/webserverlib/weblistener.cpp @@ -0,0 +1,93 @@ +#include "weblistener.h" + +#include +#include +#include +#include + +#include + +#include "utils.h" +#include "httpclientconnection.h" + +WebListener::WebListener(const QJsonObject &config, const QHash &applications, QObject *parent) : + QObject(parent) +{ + if(!config.contains(QStringLiteral("hostAddress"))) + throw std::runtime_error("listener does not contain hostAddress"); + + const auto hostAddressVal = config.value(QStringLiteral("hostAddress")); + if(!hostAddressVal.isString()) + throw std::runtime_error("listener hostAddress is not a string"); + + m_address = parseHostAddress(hostAddressVal.toString()); + + if(!config.contains(QStringLiteral("port"))) + throw std::runtime_error("listener does not contain port"); + + const auto portVal = config.value(QStringLiteral("port")); + if(!portVal.isDouble()) + throw std::runtime_error("listener port is not a number"); + + m_port = portVal.toInt(); + + m_tcpServer = new QTcpServer(this); + + if(!config.contains(QStringLiteral("vhosts"))) + throw std::runtime_error("listener does not contain vhosts"); + + const auto vhostsVal = config.value(QStringLiteral("vhosts")); + if(!vhostsVal.isObject()) + throw std::runtime_error("listener vhosts is not an object"); + + const auto vhosts = vhostsVal.toObject(); + for(auto iter = vhosts.constBegin(); iter != vhosts.constEnd(); iter++) + { + const auto applicationNameVal = iter.value(); + if(!applicationNameVal.isString()) + throw std::runtime_error(QString("listener %0:%1 vhost %2 is not a string") + .arg(m_address.toString()).arg(m_port).arg(iter.key()).toStdString()); + + const auto applicationName = applicationNameVal.toString(); + + const auto applicationsIter = applications.find(applicationName); + if(applicationsIter == applications.constEnd()) + throw std::runtime_error(QString("listener %0:%1 vhost %2 references unknown application %3") + .arg(m_address.toString()).arg(m_port).arg(iter.key(), applicationName).toStdString()); + + qDebug() << iter.key() << applicationName; + } +} + +void WebListener::start() +{ + qDebug() << "starting listening" << m_address << m_port; + if(!m_tcpServer->listen(m_address, m_port)) + throw std::runtime_error(QString("Could not start listening on %0:%1 because %2") + .arg(m_address.toString()).arg(m_port).arg(m_tcpServer->errorString()).toStdString()); + + connect(m_tcpServer, &QTcpServer::acceptError, this, &WebListener::acceptError); + connect(m_tcpServer, &QTcpServer::newConnection, this, &WebListener::newConnection); +} + +void WebListener::handleRequest(HttpClientConnection *connection, const HttpRequest &request) +{ + HttpResponse response; + response.protocol = request.protocol; + response.statusCode = HttpResponse::StatusCode::OK; + connection->sendResponse(response, request.path); +} + +void WebListener::acceptError(QAbstractSocket::SocketError socketError) +{ + qCritical() << socketError; +} + +void WebListener::newConnection() +{ + auto connection = m_tcpServer->nextPendingConnection(); + if(!connection) + return; + + new HttpClientConnection(*connection, *this); +} diff --git a/webserverlib/weblistener.h b/webserverlib/weblistener.h new file mode 100644 index 0000000..223216c --- /dev/null +++ b/webserverlib/weblistener.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "webserverlib_global.h" + +#include +#include + +class QJsonObject; +template class QHash; +class QTcpServer; + +class WebApplication; +class HttpClientConnection; +class HttpRequest; + +class WEBSERVERLIB_EXPORT WebListener : public QObject +{ + Q_OBJECT + +public: + WebListener(const QJsonObject &config, const QHash &applications, QObject *parent = Q_NULLPTR); + + void start(); + void handleRequest(HttpClientConnection *connection, const HttpRequest &request); + +private Q_SLOTS: + void acceptError(QAbstractSocket::SocketError socketError); + void newConnection(); + +private: + QTcpServer *m_tcpServer; + QHostAddress m_address; + quint16 m_port; +}; diff --git a/webserverlib/webplugin.cpp b/webserverlib/webplugin.cpp new file mode 100644 index 0000000..c3a04c6 --- /dev/null +++ b/webserverlib/webplugin.cpp @@ -0,0 +1,6 @@ +#include "webplugin.h" + +WebPlugin::WebPlugin(QObject *parent) : + QObject(parent) +{ +} diff --git a/webserverlib/webplugin.h b/webserverlib/webplugin.h new file mode 100644 index 0000000..958e3ef --- /dev/null +++ b/webserverlib/webplugin.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include "webserverlib_global.h" + +class QJsonObject; + +class WebApplication; + +class WEBSERVERLIB_EXPORT WebPlugin : public QObject +{ + Q_OBJECT + +public: + WebPlugin(QObject *parent = Q_NULLPTR); + + virtual QString pluginName() const = 0; + virtual WebApplication *createApplication(const QJsonObject &config) const = 0; +}; + +Q_DECLARE_INTERFACE(WebPlugin, "dbsoftware.webserver.plugin/1.0") diff --git a/webserverlib/webserverlib.pro b/webserverlib/webserverlib.pro new file mode 100644 index 0000000..f335abd --- /dev/null +++ b/webserverlib/webserverlib.pro @@ -0,0 +1,30 @@ +QT += core network +QT -= gui widgets + +PROJECT_ROOT = ../.. + +DEFINES += WEBSERVERLIB_LIBRARY + +SOURCES += \ + weblistener.cpp \ + webapplication.cpp \ + webplugin.cpp \ + utils.cpp \ + httpclientconnection.cpp \ + httpcontainers.cpp + +HEADERS += webserverlib_global.h \ + weblistener.h \ + webapplication.h \ + webplugin.h \ + utils.h \ + httpclientconnection.h \ + httpcontainers.h + +FORMS += + +RESOURCES += + +TRANSLATIONS += + +include($${PROJECT_ROOT}/lib.pri) diff --git a/webserverlib/webserverlib_global.h b/webserverlib/webserverlib_global.h new file mode 100644 index 0000000..9908673 --- /dev/null +++ b/webserverlib/webserverlib_global.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +#if defined(WEBSERVERLIB_LIBRARY) +# define WEBSERVERLIB_EXPORT Q_DECL_EXPORT +#else +# define WEBSERVERLIB_EXPORT Q_DECL_IMPORT +#endif