Imported existing sources
This commit is contained in:
15
.gitmodules
vendored
Normal file
15
.gitmodules
vendored
Normal file
@@ -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
|
8
DbWebserver.pro
Normal file
8
DbWebserver.pro
Normal file
@@ -0,0 +1,8 @@
|
||||
TEMPLATE = subdirs
|
||||
|
||||
SUBDIRS += webserver \
|
||||
webserverlib \
|
||||
plugins
|
||||
|
||||
webserver.depends += webserverlib
|
||||
plugins.depends += webserverlib
|
1
plugins/fileserverplugin
Submodule
1
plugins/fileserverplugin
Submodule
Submodule plugins/fileserverplugin added at 98070165b2
1
plugins/helloworldplugin
Submodule
1
plugins/helloworldplugin
Submodule
Submodule plugins/helloworldplugin added at 6d7c8d789f
5
plugins/plugin.pri
Normal file
5
plugins/plugin.pri
Normal file
@@ -0,0 +1,5 @@
|
||||
PROJECT_ROOT = ../../..
|
||||
TEMPLATE = lib
|
||||
CONFIG += shared
|
||||
DESTDIR = $${OUT_PWD}/$${PROJECT_ROOT}/bin/plugins/webserver
|
||||
include(../../project.pri)
|
9
plugins/plugins.pro
Normal file
9
plugins/plugins.pro
Normal file
@@ -0,0 +1,9 @@
|
||||
TEMPLATE = subdirs
|
||||
|
||||
SUBDIRS += fileserverplugin \
|
||||
helloworldplugin \
|
||||
proxyplugin \
|
||||
seriesplugin \
|
||||
wifilampplugin
|
||||
|
||||
OTHER_FILES += plugin.pri
|
1
plugins/proxyplugin
Submodule
1
plugins/proxyplugin
Submodule
Submodule plugins/proxyplugin added at 795d191f27
1
plugins/seriesplugin
Submodule
1
plugins/seriesplugin
Submodule
Submodule plugins/seriesplugin added at 5bcabbdce0
1
plugins/wifilampplugin
Submodule
1
plugins/wifilampplugin
Submodule
Submodule plugins/wifilampplugin added at d9d5f7ff89
121
webserver/main.cpp
Normal file
121
webserver/main.cpp
Normal file
@@ -0,0 +1,121 @@
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QLibrary>
|
||||
#include <QPluginLoader>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDebug>
|
||||
|
||||
#include <stdexcept>
|
||||
#include <iterator>
|
||||
|
||||
#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<QString, WebPlugin*> 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<WebPlugin*>(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<QString, WebApplication*> applications;
|
||||
|
||||
const auto configPath = QDir(QCoreApplication::applicationDirPath()).absoluteFilePath(QCoreApplication::applicationName() + QStringLiteral(".json"));
|
||||
const auto config = getJson<QJsonObject>(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<WebListener*> 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();
|
||||
}
|
92
webserver/webserver.json
Normal file
92
webserver/webserver.json
Normal file
@@ -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"
|
||||
}
|
||||
}]
|
||||
}
|
22
webserver/webserver.pro
Normal file
22
webserver/webserver.pro
Normal file
@@ -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)
|
193
webserverlib/httpclientconnection.cpp
Normal file
193
webserverlib/httpclientconnection.cpp
Normal file
@@ -0,0 +1,193 @@
|
||||
#include "httpclientconnection.h"
|
||||
|
||||
#include <QTcpSocket>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#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<QIODevice> &&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();
|
||||
}
|
43
webserverlib/httpclientconnection.h
Normal file
43
webserverlib/httpclientconnection.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QIODevice>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#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<QIODevice> &&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<QIODevice> m_sendingDeivce;
|
||||
};
|
75
webserverlib/httpcontainers.cpp
Normal file
75
webserverlib/httpcontainers.cpp
Normal file
@@ -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");
|
||||
}
|
||||
}
|
101
webserverlib/httpcontainers.h
Normal file
101
webserverlib/httpcontainers.h
Normal file
@@ -0,0 +1,101 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QHash>
|
||||
#include <QByteArray>
|
||||
|
||||
struct HttpRequest {
|
||||
QString method;
|
||||
QString path;
|
||||
QString protocol;
|
||||
QHash<QString, QString> 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<QString, QString> headers;
|
||||
|
||||
QString statusString() const;
|
||||
};
|
47
webserverlib/utils.cpp
Normal file
47
webserverlib/utils.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "utils.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
|
||||
QHostAddress parseHostAddress(const QString &hostAddress)
|
||||
{
|
||||
static const QMap<QString, QHostAddress> 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<QJsonDocument>(const QJsonDocument &document)
|
||||
{
|
||||
return document;
|
||||
}
|
||||
|
||||
template<>
|
||||
QJsonObject getJson<QJsonObject>(const QJsonDocument &document)
|
||||
{
|
||||
if(!document.isObject())
|
||||
throw std::runtime_error("JSON document does not contain an object!");
|
||||
|
||||
return document.object();
|
||||
}
|
||||
|
||||
template<>
|
||||
QJsonArray getJson<QJsonArray>(const QJsonDocument &document)
|
||||
{
|
||||
if(!document.isArray())
|
||||
throw std::runtime_error("JSON document does not contain an object!");
|
||||
|
||||
return document.array();
|
||||
}
|
60
webserverlib/utils.h
Normal file
60
webserverlib/utils.h
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <QHostAddress>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonParseError>
|
||||
#include <QFile>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
QHostAddress parseHostAddress(const QString &hostAddress);
|
||||
|
||||
template<typename T>
|
||||
T getJson(const QJsonDocument &document);
|
||||
|
||||
template<>
|
||||
QJsonDocument getJson<QJsonDocument>(const QJsonDocument &document);
|
||||
|
||||
template<>
|
||||
QJsonObject getJson<QJsonObject>(const QJsonDocument &document);
|
||||
|
||||
template<>
|
||||
QJsonArray getJson<QJsonArray>(const QJsonDocument &document);
|
||||
|
||||
template<typename T>
|
||||
T getJson(const QByteArray &byteArray);
|
||||
|
||||
template<typename T>
|
||||
T getJson(QIODevice &device);
|
||||
|
||||
template<typename T>
|
||||
T getJson(const QString &filename);
|
||||
|
||||
|
||||
|
||||
template<typename T>
|
||||
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<T>(document);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T getJson(QIODevice &device)
|
||||
{
|
||||
return getJson<T>(device.readAll());
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
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<T>(file);
|
||||
}
|
7
webserverlib/webapplication.cpp
Normal file
7
webserverlib/webapplication.cpp
Normal file
@@ -0,0 +1,7 @@
|
||||
#include "webapplication.h"
|
||||
|
||||
WebApplication::WebApplication(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
|
||||
}
|
14
webserverlib/webapplication.h
Normal file
14
webserverlib/webapplication.h
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include "webserverlib_global.h"
|
||||
|
||||
class WEBSERVERLIB_EXPORT WebApplication : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
WebApplication(QObject *parent = Q_NULLPTR);
|
||||
|
||||
virtual void start() = 0;
|
||||
};
|
93
webserverlib/weblistener.cpp
Normal file
93
webserverlib/weblistener.cpp
Normal file
@@ -0,0 +1,93 @@
|
||||
#include "weblistener.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QHash>
|
||||
#include <QTcpServer>
|
||||
#include <QDebug>
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "utils.h"
|
||||
#include "httpclientconnection.h"
|
||||
|
||||
WebListener::WebListener(const QJsonObject &config, const QHash<QString, WebApplication*> &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);
|
||||
}
|
35
webserverlib/weblistener.h
Normal file
35
webserverlib/weblistener.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include "webserverlib_global.h"
|
||||
|
||||
#include <QAbstractSocket>
|
||||
#include <QHostAddress>
|
||||
|
||||
class QJsonObject;
|
||||
template <class Key, class T> 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<QString, WebApplication*> &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;
|
||||
};
|
6
webserverlib/webplugin.cpp
Normal file
6
webserverlib/webplugin.cpp
Normal file
@@ -0,0 +1,6 @@
|
||||
#include "webplugin.h"
|
||||
|
||||
WebPlugin::WebPlugin(QObject *parent) :
|
||||
QObject(parent)
|
||||
{
|
||||
}
|
21
webserverlib/webplugin.h
Normal file
21
webserverlib/webplugin.h
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#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")
|
30
webserverlib/webserverlib.pro
Normal file
30
webserverlib/webserverlib.pro
Normal file
@@ -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)
|
9
webserverlib/webserverlib_global.h
Normal file
9
webserverlib/webserverlib_global.h
Normal file
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
#if defined(WEBSERVERLIB_LIBRARY)
|
||||
# define WEBSERVERLIB_EXPORT Q_DECL_EXPORT
|
||||
#else
|
||||
# define WEBSERVERLIB_EXPORT Q_DECL_IMPORT
|
||||
#endif
|
Reference in New Issue
Block a user