diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..850f71d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/3rdparty/http-parser"] + path = src/3rdparty/http-parser + url = https://github.com/nodejs/http-parser.git diff --git a/.qmake.conf b/.qmake.conf new file mode 100644 index 0000000..097d8b9 --- /dev/null +++ b/.qmake.conf @@ -0,0 +1,3 @@ +load(qt_build_config) + +MODULE_VERSION = 5.12.0 diff --git a/qthttpserver.pro b/qthttpserver.pro new file mode 100644 index 0000000..58c33f2 --- /dev/null +++ b/qthttpserver.pro @@ -0,0 +1 @@ +load(qt_parts) diff --git a/src/3rdparty/http-parser b/src/3rdparty/http-parser new file mode 160000 index 0000000..edeedb1 --- /dev/null +++ b/src/3rdparty/http-parser @@ -0,0 +1 @@ +Subproject commit edeedb1b4d2f34e4c7d8045ac8b92adbc35e7ed7 diff --git a/src/3rdparty/http-parser.pri b/src/3rdparty/http-parser.pri new file mode 100644 index 0000000..51cd2b8 --- /dev/null +++ b/src/3rdparty/http-parser.pri @@ -0,0 +1,6 @@ +NODEJS_HTTP_PARSER_PATH = $$PWD/http-parser + +INCLUDEPATH += $$NODEJS_HTTP_PARSER_PATH + +HEADERS += $$NODEJS_HTTP_PARSER_PATH/http_parser.h +SOURCES += $$NODEJS_HTTP_PARSER_PATH/http_parser.c diff --git a/src/3rdparty/qt_attribution.json b/src/3rdparty/qt_attribution.json new file mode 100644 index 0000000..0f2d6e6 --- /dev/null +++ b/src/3rdparty/qt_attribution.json @@ -0,0 +1,11 @@ +{ + "Id": "http-parser", + "Name": "HTTP request/response parser for C", + "QDocModule": "qthttpserver", + "QtUsage": "Used in Qt HttpServer.", + + "License": "MIT License", + "LicenseId": "MIT", + "LicenseFile": "http-parser/LICENSE-MIT", + "Copyright": "Copyright Joyent, Inc. and other Node contributors. All rights reserved." +} diff --git a/src/httpserver/httpserver.pro b/src/httpserver/httpserver.pro new file mode 100644 index 0000000..d2139f6 --- /dev/null +++ b/src/httpserver/httpserver.pro @@ -0,0 +1,21 @@ +TARGET = QtHttpServer +INCLUDEPATH += . + +QT += network core-private + +qtHaveModule(websockets): QT += websockets-private + +HEADERS += \ + qthttpserverglobal.h \ + qabstracthttpserver.h \ + qabstracthttpserver_p.h \ + qhttpserverrequest.h \ + qhttpserverrequest_p.h + +SOURCES += \ + qabstracthttpserver.cpp \ + qhttpserverrequest.cpp + +include(../3rdparty/http-parser.pri) + +load(qt_module) diff --git a/src/httpserver/qabstracthttpserver.cpp b/src/httpserver/qabstracthttpserver.cpp new file mode 100644 index 0000000..608cb27 --- /dev/null +++ b/src/httpserver/qabstracthttpserver.cpp @@ -0,0 +1,265 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include + +#include +#include +#include + +#include +#include +#include + +#include "http_parser.h" + +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcHttpServer, "qt.httpserver") + +#if !defined(QT_NO_USERDATA) +QAtomicInt QAbstractHttpServerPrivate::userDataId = -1; +#endif + +QAbstractHttpServerPrivate::QAbstractHttpServerPrivate() +{ +#if !defined(QT_NO_USERDATA) + userDataId.testAndSetRelaxed(-1, int(QObject::registerUserData())); +#endif +} + +void QAbstractHttpServerPrivate::handleNewConnections() +{ + auto tcpServer = qobject_cast(currentSender->sender); + Q_ASSERT(tcpServer); + while (auto *socket = tcpServer->nextPendingConnection()) { + auto request = new QHttpServerRequest; // TODO own tcp server could pre-allocate it + http_parser_init(&request->d->httpParser, HTTP_REQUEST); + connect(socket, &QTcpSocket::readyRead, this, &QAbstractHttpServerPrivate::handleReadyRead); + socket->connect(socket, &QTcpSocket::disconnected, &QObject::deleteLater); +#if !defined(QT_NO_USERDATA) + socket->setUserData(uint(userDataId), request); +#else + QObject::connect(socket, &QTcpSocket::destroyed, [&]() { + requests.remove(socket); + }); + requests.insert(socket, request); +#endif + } +} + +void QAbstractHttpServerPrivate::handleReadyRead() +{ + Q_Q(QAbstractHttpServer); + auto socket = qobject_cast(currentSender->sender); +#if !defined(QT_NO_USERDATA) + auto request = static_cast(socket->userData(uint(userDataId))); +#else + auto request = requests[socket]; +#endif + auto &requestPrivate = request->d; + + if (!socket->isTransactionStarted()) + socket->startTransaction(); + + if (!requestPrivate->parse(socket)) { + socket->disconnect(); + return; + } + if (!requestPrivate->httpParser.upgrade && + requestPrivate->state != QHttpServerRequestPrivate::State::OnMessageComplete) + return; // Partial read + + if (requestPrivate->httpParser.upgrade) { // Upgrade + const auto &headers = requestPrivate->headers; + const auto upgradeHash = requestPrivate->headerHash(QStringLiteral("upgrade")); + const auto it = headers.find(upgradeHash); + if (it != headers.end()) { +#if defined(QT_WEBSOCKETS_LIB) + if (it.value().second.compare(QLatin1String("websocket"), Qt::CaseInsensitive) == 0) { + static const auto signal = QMetaMethod::fromSignal( + &QAbstractHttpServer::newWebSocketConnection); + if (q->isSignalConnected(signal)) { + disconnect(socket, &QTcpSocket::readyRead, + this, &QAbstractHttpServerPrivate::handleReadyRead); + socket->rollbackTransaction(); + websocketServer.handleConnection(socket); + Q_EMIT socket->readyRead(); + } else { + qWarning(lcHttpServer, "WebSocket received but no slots connected to " + "QWebSocketServer::newConnection"); + socket->disconnectFromHost(); + } + return; + } +#endif + } + qCWarning(lcHttpServer, "Upgrade to %s not supported", qPrintable(it.value().second)); + socket->disconnectFromHost(); + } else { + socket->commitTransaction(); + if (!q->handleRequest(*request, socket)) + Q_EMIT q->missingHandler(*request, socket); + } +} + +QAbstractHttpServer::QAbstractHttpServer(QObject *parent) + : QObject(*new QAbstractHttpServerPrivate, parent) +{ +#if defined(QT_WEBSOCKETS_LIB) + Q_D(QAbstractHttpServer); + connect(&d->websocketServer, &QWebSocketServer::newConnection, + this, &QAbstractHttpServer::newWebSocketConnection); +#endif +} + +/*! + Tries to bind a \c QTcpServer to \a address and \a port. + + Returns \c true upon success, false otherwise. +*/ +bool QAbstractHttpServer::listen(const QHostAddress &address, quint16 port) +{ + auto tcpServer = new QTcpServer(this); + const auto listening = tcpServer->listen(address, port); + if (listening) + bind(tcpServer); + else + delete tcpServer; + return listening; +} + +/*! + Bind the HTTP server to given TCP \a server over which + the transmission happens. It is possible to call this function + multiple times with different instances of TCP \a server to + handle multiple connections and ports, for example both SSL and + non-encrypted connections. + + After calling this function, every _new_ connection will be + handled and forwarded by the HTTP server. + + It is the user's responsibility to call QTcpServer::listen() on + the \a server. + + If the \a server is null, then a new, default-constructed TCP + server will be constructed, which will be listening on a random + port and all interfaces. + + The \a server will be parented to this HTTP server. + + \sa QTcpServer, QTcpServer::listen() +*/ +void QAbstractHttpServer::bind(QTcpServer *server) +{ + Q_D(QAbstractHttpServer); + if (!server) { + server = new QTcpServer(this); + if (!server->listen()) { + qCCritical(lcHttpServer, "QTcpServer listen failed (%s)", + qPrintable(server->errorString())); + } + } else { + if (!server->isListening()) + qCWarning(lcHttpServer) << "The TCP server" << server << "is not listening."; + server->setParent(this); + } + QObjectPrivate::connect(server, &QTcpServer::newConnection, + d, &QAbstractHttpServerPrivate::handleNewConnections, + Qt::UniqueConnection); +} + +/*! + Returns list of child TCP servers of this HTTP server. + */ +QVector QAbstractHttpServer::servers() const +{ + auto c = children(); + QVector result; + result.reserve(c.size()); + for (auto i = c.constBegin(); i != c.constEnd(); ++i) { + if ((*i)->inherits("QTcpServer")) + result.append(static_cast(*i)); + } + result.squeeze(); + return result; +} + +#if defined(QT_WEBSOCKETS_LIB) +/*! + \fn QAbstractHttpServer::newConnection + This signal is emitted every time a new WebSocket connection is + available. + + \sa hasPendingWebSocketConnections(), nextPendingWebSocketConnection() +*/ + +/*! + Returns \c true if the server has pending WebSocket connections; + otherwise returns \c false. + + \sa newWebSocketConnection(), nextPendingWebSocketConnection() +*/ +bool QAbstractHttpServer::hasPendingWebSocketConnections() const +{ + Q_D(const QAbstractHttpServer); + return d->websocketServer.hasPendingConnections(); +} + +/*! + Returns the next pending connection as a connected QWebSocket + object. QAbstractHttpServer does not take ownership of the + returned QWebSocket object. It is up to the caller to delete the + object explicitly when it will no longer be used, otherwise a + memory leak will occur. \c nullptr is returned if this function + is called when there are no pending connections. + + \note The returned QWebSocket object cannot be used from another + thread. + + \sa newWebSocketConnection(), hasPendingWebSocketConnections() +*/ +QWebSocket *QAbstractHttpServer::nextPendingWebSocketConnection() +{ + Q_D(QAbstractHttpServer); + return d->websocketServer.nextPendingConnection(); +} +#endif + +QT_END_NAMESPACE diff --git a/src/httpserver/qabstracthttpserver.h b/src/httpserver/qabstracthttpserver.h new file mode 100644 index 0000000..4eda6aa --- /dev/null +++ b/src/httpserver/qabstracthttpserver.h @@ -0,0 +1,89 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QABSTRACTHTTPSERVER_H +#define QABSTRACTHTTPSERVER_H + +#include + +#include + +#include + +QT_BEGIN_NAMESPACE + +class QHttpServerRequest; +class QTcpServer; +class QTcpSocket; +class QWebSocket; + +class QAbstractHttpServerPrivate; +class Q_HTTPSERVER_EXPORT QAbstractHttpServer : public QObject +{ + Q_OBJECT + +public: + QAbstractHttpServer(QObject *parent = nullptr); + + bool listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0); + + void bind(QTcpServer *server = nullptr); + QVector servers() const; + +Q_SIGNALS: + void missingHandler(const QHttpServerRequest &request, QTcpSocket *socket); + +#if defined(QT_WEBSOCKETS_LIB) + void newWebSocketConnection(); + +public: + bool hasPendingWebSocketConnections() const; + QWebSocket *nextPendingWebSocketConnection(); +#endif // defined(QT_WEBSOCKETS_LIB) + +protected: + virtual bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) = 0; + +private: + Q_DECLARE_PRIVATE(QAbstractHttpServer) +}; + +QT_END_NAMESPACE + +#endif // QABSTRACTHTTPSERVER_H diff --git a/src/httpserver/qabstracthttpserver_p.h b/src/httpserver/qabstracthttpserver_p.h new file mode 100644 index 0000000..851fb32 --- /dev/null +++ b/src/httpserver/qabstracthttpserver_p.h @@ -0,0 +1,85 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QABSTRACTHTTPSERVER_P_H +#define QABSTRACTHTTPSERVER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of QHttpServer. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +#include + +#if defined(QT_WEBSOCKETS_LIB) +#include +#endif // defined(QT_WEBSOCKETS_LIB) + +class QHttpServerRequest; + +class QAbstractHttpServerPrivate: public QObjectPrivate +{ + Q_DECLARE_PUBLIC(QAbstractHttpServer) + +public: + QAbstractHttpServerPrivate(); + +#if defined(QT_WEBSOCKETS_LIB) + QWebSocketServer websocketServer { + QLatin1String("QtHttpServer"), + QWebSocketServer::NonSecureMode + }; +#endif // defined(QT_WEBSOCKETS_LIB) + +#if !defined(QT_NO_USERDATA) + static QAtomicInt userDataId; +#else + QHash requests; +#endif + + void handleNewConnections(); + void handleReadyRead(); +}; + +#endif // QABSTRACTHTTPSERVER_P_H diff --git a/src/httpserver/qhttpserverrequest.cpp b/src/httpserver/qhttpserverrequest.cpp new file mode 100644 index 0000000..9e2483d --- /dev/null +++ b/src/httpserver/qhttpserverrequest.cpp @@ -0,0 +1,304 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qhttpserverrequest_p.h" + +#include + +#include +#include +#include +#if defined(QT_FEATURE_ssl) +#include +#endif + +Q_LOGGING_CATEGORY(lc, "qt.httpserver.request") + +QT_BEGIN_NAMESPACE + +#if !defined(QT_NO_DEBUG_STREAM) +Q_HTTPSERVER_EXPORT QDebug operator<<(QDebug debug, const QHttpServerRequest &request) +{ + const auto oldSetting = debug.autoInsertSpaces(); + debug.nospace() << "QHttpServerRequest("; + debug << "(Url: " << request.url() << ")"; + debug << "(Headers: " << request.headers() << ")"; + debug << ')'; + debug.setAutoInsertSpaces(oldSetting); + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const http_parser *const httpParser) +{ + const auto oldSetting = debug.autoInsertSpaces(); + debug.nospace() << "http_parser(" << static_cast(httpParser) << ": "; + debug << "HTTP " << httpParser->http_major << "." << httpParser->http_minor << " " + << http_method_str(http_method(httpParser->method)) << ')'; + debug.setAutoInsertSpaces(oldSetting); + return debug.maybeSpace(); +} +#endif + +http_parser_settings QHttpServerRequestPrivate::httpParserSettings { + &QHttpServerRequestPrivate::onMessageBegin, + &QHttpServerRequestPrivate::onUrl, + &QHttpServerRequestPrivate::onStatus, + &QHttpServerRequestPrivate::onHeaderField, + &QHttpServerRequestPrivate::onHeaderValue, + &QHttpServerRequestPrivate::onHeadersComplete, + &QHttpServerRequestPrivate::onBody, + &QHttpServerRequestPrivate::onMessageComplete, + &QHttpServerRequestPrivate::onChunkHeader, + &QHttpServerRequestPrivate::onChunkComplete +}; + +QHttpServerRequestPrivate::QHttpServerRequestPrivate() +{ + httpParser.data = this; +} + +QString QHttpServerRequestPrivate::header(const QString &key) const +{ + return headers.value(headerHash(key)).second; +} + +bool QHttpServerRequestPrivate::parse(QIODevice *socket) +{ + const auto fragment = socket->readAll(); + if (fragment.size()) { +#if defined(QT_FEATURE_ssl) + auto sslSocket = qobject_cast(socket); + url.setScheme(sslSocket && sslSocket->isEncrypted() ? QStringLiteral("https") + : QStringLiteral("http")); +#else + url.setScheme(QStringLiteral("http")); +#endif + const auto parsed = http_parser_execute(&httpParser, + &httpParserSettings, + fragment.constData(), + size_t(fragment.size())); + if (int(parsed) < fragment.size()) { + qCDebug(lc, "Parse error: %d", httpParser.http_errno); + return false; + } + } + return true; +} + +uint QHttpServerRequestPrivate::headerHash(const QString &key) const +{ + return qHash(key.toLower(), headersSeed); +} + +bool QHttpServerRequestPrivate::parseUrl(const char *at, size_t length, bool connect, QUrl *url) +{ + static const std::map> functions { + { UF_SCHEMA, [](const QString &string, QUrl *url) { url->setScheme(string); } }, + { UF_HOST, [](const QString &string, QUrl *url) { url->setHost(string); } }, + { UF_PORT, [](const QString &string, QUrl *url) { url->setPort(string.toInt()); } }, + { UF_PATH, [](const QString &string, QUrl *url) { url->setPath(string); } }, + { UF_QUERY, [](const QString &string, QUrl *url) { url->setQuery(string); } }, + { UF_FRAGMENT, [](const QString &string, QUrl *url) { url->setFragment(string); } }, + { UF_USERINFO, [](const QString &string, QUrl *url) { url->setUserInfo(string); } }, + }; + struct http_parser_url u; + if (http_parser_parse_url(at, length, connect ? 1 : 0, &u) == 0) { + for (auto i = 0u; i < UF_MAX; i++) { + if (u.field_set & (1 << i)) { + functions.find(i)->second(QString::fromUtf8(at + u.field_data[i].off, + u.field_data[i].len), + url); + } + } + return true; + } + return false; +} + +QHttpServerRequestPrivate *QHttpServerRequestPrivate::instance(http_parser *httpParser) +{ + return static_cast(httpParser->data); +} + +int QHttpServerRequestPrivate::onMessageBegin(http_parser *httpParser) +{ + qCDebug(lc) << static_cast(httpParser); + instance(httpParser)->state = State::OnMessageBegin; + return 0; +} + +int QHttpServerRequestPrivate::onUrl(http_parser *httpParser, const char *at, size_t length) +{ + qCDebug(lc) << httpParser << QString::fromUtf8(at, int(length)); + auto instance = static_cast(httpParser->data); + instance->state = State::OnUrl; + parseUrl(at, length, false, &instance->url); + return 0; +} + +int QHttpServerRequestPrivate::onStatus(http_parser *httpParser, const char *at, size_t length) +{ + qCDebug(lc) << httpParser << QString::fromUtf8(at, int(length)); + instance(httpParser)->state = State::OnStatus; + return 0; +} + +int QHttpServerRequestPrivate::onHeaderField(http_parser *httpParser, const char *at, size_t length) +{ + qCDebug(lc) << httpParser << QString::fromUtf8(at, int(length)); + auto i = instance(httpParser); + i->state = State::OnHeaders; + const auto key = QString::fromUtf8(at, int(length)); + i->headers.insert(i->headerHash(key), qMakePair(key, QString())); + i->lastHeader = key; + return 0; +} + +int QHttpServerRequestPrivate::onHeaderValue(http_parser *httpParser, const char *at, size_t length) +{ + qCDebug(lc) << httpParser << QString::fromUtf8(at, int(length)); + auto i = instance(httpParser); + i->state = State::OnHeaders; + Q_ASSERT(!i->lastHeader.isEmpty()); + const auto value = QString::fromUtf8(at, int(length)); + i->headers[i->headerHash(i->lastHeader)] = qMakePair(i->lastHeader, value); + if (i->lastHeader.compare(QStringLiteral("host"), Qt::CaseInsensitive) == 0) + parseUrl(at, length, true, &i->url); +#if defined(QT_DEBUG) + i->lastHeader.clear(); +#endif + return 0; +} + +int QHttpServerRequestPrivate::onHeadersComplete(http_parser *httpParser) +{ + qCDebug(lc) << httpParser; + instance(httpParser)->state = State::OnHeadersComplete; + return 0; +} + +int QHttpServerRequestPrivate::onBody(http_parser *httpParser, const char *at, size_t length) +{ + qCDebug(lc) << httpParser << QString::fromUtf8(at, int(length)); + auto i = instance(httpParser); + i->state = State::OnBody; + i->body = QByteArray(at, int(length)); + return 0; +} + +int QHttpServerRequestPrivate::onMessageComplete(http_parser *httpParser) +{ + qCDebug(lc) << httpParser; + instance(httpParser)->state = State::OnMessageComplete; + return 0; +} + +int QHttpServerRequestPrivate::onChunkHeader(http_parser *httpParser) +{ + qCDebug(lc) << httpParser; + instance(httpParser)->state = State::OnChunkHeader; + return 0; +} + +int QHttpServerRequestPrivate::onChunkComplete(http_parser *httpParser) +{ + qCDebug(lc) << httpParser; + instance(httpParser)->state = State::OnChunkComplete; + return 0; +} + +QHttpServerRequest::QHttpServerRequest() : + QObjectUserData(), + d(new QHttpServerRequestPrivate) +{} + +QHttpServerRequest::QHttpServerRequest(const QHttpServerRequest &other) : + QObjectUserData(), + d(other.d) +{} + +QHttpServerRequest::~QHttpServerRequest() +{} + +QString QHttpServerRequest::value(const QString &key) const +{ + return d->headers.value(d->headerHash(key)).second; +} + +QUrl QHttpServerRequest::url() const +{ + return d->url; +} + +QHttpServerRequest::Method QHttpServerRequest::method() const +{ + switch (d->httpParser.method) { + case HTTP_GET: + return QHttpServerRequest::Method::Get; + case HTTP_PUT: + return QHttpServerRequest::Method::Put; + case HTTP_DELETE: + return QHttpServerRequest::Method::Delete; + case HTTP_POST: + return QHttpServerRequest::Method::Post; + case HTTP_HEAD: + return QHttpServerRequest::Method::Head; + case HTTP_OPTIONS: + return QHttpServerRequest::Method::Options; + case HTTP_PATCH: + return QHttpServerRequest::Method::Patch; + default: + return QHttpServerRequest::Method::Unknown; + } +} + +QVariantMap QHttpServerRequest::headers() const +{ + QVariantMap ret; + for (auto it = d->headers.cbegin(), end = d->headers.cend(); it != end; ++it) + ret.insert(it.value().first, it.value().second); + return ret; +} + +QByteArray QHttpServerRequest::body() const +{ + return d->body; +} + +QT_END_NAMESPACE diff --git a/src/httpserver/qhttpserverrequest.h b/src/httpserver/qhttpserverrequest.h new file mode 100644 index 0000000..0e9a893 --- /dev/null +++ b/src/httpserver/qhttpserverrequest.h @@ -0,0 +1,101 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QHTTPSERVERREQUEST_H +#define QHTTPSERVERREQUEST_H + +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QRegularExpression; +class QString; +class QTcpSocket; + +class QHttpServerRequestPrivate; +class Q_HTTPSERVER_EXPORT QHttpServerRequest : public QObjectUserData +{ + friend class QAbstractHttpServerPrivate; + friend class QHttpServerResponse; + +public: + ~QHttpServerRequest() override; + + enum class Method + { + Unknown = -1, + + Get, + Put, + Delete, + Post, + Head, + Options, + Patch + }; + + QString value(const QString &key) const; + QUrl url() const; + Method method() const; + QVariantMap headers() const; + QByteArray body() const; + +protected: + QHttpServerRequest(const QHttpServerRequest &other); + +private: +#if !defined(QT_NO_DEBUG_STREAM) + friend QDebug operator<<(QDebug debug, const QHttpServerRequest &request); +#endif + + QHttpServerRequest(); + + QExplicitlySharedDataPointer d; +}; + +QT_END_NAMESPACE + +Q_DECLARE_METATYPE(QHttpServerRequest::Method) + +#endif // QHTTPSERVERREQUEST_H diff --git a/src/httpserver/qhttpserverrequest_p.h b/src/httpserver/qhttpserverrequest_p.h new file mode 100644 index 0000000..11bfbab --- /dev/null +++ b/src/httpserver/qhttpserverrequest_p.h @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtHttpServer module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QHTTPSERVERREQUEST_P_H +#define QHTTPSERVERREQUEST_P_H + +#include + +#include +#include +#include +#include +#include +#include + +#include "../3rdparty/http-parser/http_parser.h" + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of QHttpServer. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. + +QT_BEGIN_NAMESPACE + +class QHttpServerRequestPrivate : public QSharedData +{ +public: + QHttpServerRequestPrivate(); + + quint16 port = 0; + enum class State { + NotStarted, + OnMessageBegin, + OnUrl, + OnStatus, + OnHeaders, + OnHeadersComplete, + OnBody, + OnMessageComplete, + OnChunkHeader, + OnChunkComplete + } state = State::NotStarted; + QByteArray body; + + QUrl url; + + http_parser httpParser; + + QString header(const QString &key) const; + bool parse(QIODevice *socket); + + QString lastHeader; + QHash> headers; + const uint headersSeed = uint(qGlobalQHashSeed()); + uint headerHash(const QString &key) const; + +private: + static http_parser_settings httpParserSettings; + static bool parseUrl(const char *at, size_t length, bool connect, QUrl *url); + + static QHttpServerRequestPrivate *instance(http_parser *httpParser); + + static int onMessageBegin(http_parser *httpParser); + static int onUrl(http_parser *httpParser, const char *at, size_t length); + static int onStatus(http_parser *httpParser, const char *at, size_t length); + static int onHeaderField(http_parser *httpParser, const char *at, size_t length); + static int onHeaderValue(http_parser *httpParser, const char *at, size_t length); + static int onHeadersComplete(http_parser *httpParser); + static int onBody(http_parser *httpParser, const char *at, size_t length); + static int onMessageComplete(http_parser *httpParser); + static int onChunkHeader(http_parser *httpParser); + static int onChunkComplete(http_parser *httpParser); +}; + +QT_END_NAMESPACE + +#endif // QHTTPSERVERREQUEST_P_H diff --git a/src/httpserver/qthttpserverglobal.h b/src/httpserver/qthttpserverglobal.h new file mode 100644 index 0000000..70d977c --- /dev/null +++ b/src/httpserver/qthttpserverglobal.h @@ -0,0 +1,58 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTHTTPSERVERGLOBAL_H +#define QTHTTPSERVERGLOBAL_H + +#include + +QT_BEGIN_NAMESPACE + +#ifndef QT_STATIC +# if defined(QT_BUILD_HTTPSERVER_LIB) +# define Q_HTTPSERVER_EXPORT Q_DECL_EXPORT +# else +# define Q_HTTPSERVER_EXPORT Q_DECL_IMPORT +# endif +#else +# define Q_HTTPSERVER_EXPORT +#endif + +QT_END_NAMESPACE + +#endif // QTHTTPSERVERGLOBAL_H + diff --git a/src/src.pro b/src/src.pro new file mode 100644 index 0000000..68d9ed3 --- /dev/null +++ b/src/src.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + httpserver diff --git a/sync.profile b/sync.profile new file mode 100644 index 0000000..b4dfb24 --- /dev/null +++ b/sync.profile @@ -0,0 +1,3 @@ +%modules = ( # path to module name map + "QtHttpServer" => "$basedir/src/httpserver", +); diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro new file mode 100644 index 0000000..cfbe887 --- /dev/null +++ b/tests/auto/auto.pro @@ -0,0 +1,6 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + cmake \ + qabstracthttpserver + diff --git a/tests/auto/cmake/CMakeLists.txt b/tests/auto/cmake/CMakeLists.txt new file mode 100644 index 0000000..5a24d26 --- /dev/null +++ b/tests/auto/cmake/CMakeLists.txt @@ -0,0 +1,16 @@ + +cmake_minimum_required(VERSION 2.8) + +project(qmake_cmake_files) + +enable_testing() + +find_package(Qt5Core REQUIRED) + +include("${_Qt5CTestMacros}") + +test_module_includes( + HttpServer QHttpServer +) + +expect_pass(test_plugins) diff --git a/tests/auto/cmake/cmake.pro b/tests/auto/cmake/cmake.pro new file mode 100644 index 0000000..9cef14e --- /dev/null +++ b/tests/auto/cmake/cmake.pro @@ -0,0 +1,4 @@ +# Cause make to do nothing. +TEMPLATE = subdirs +CMAKE_QT_MODULES_UNDER_TEST = httpserver +CONFIG += ctest_testcase diff --git a/tests/auto/qabstracthttpserver/qabstracthttpserver.pro b/tests/auto/qabstracthttpserver/qabstracthttpserver.pro new file mode 100644 index 0000000..a068028 --- /dev/null +++ b/tests/auto/qabstracthttpserver/qabstracthttpserver.pro @@ -0,0 +1,8 @@ +CONFIG += testcase +TARGET = tst_qabstracthttpserver +SOURCES += tst_qabstracthttpserver.cpp + +requires(qtConfig(private_tests)) +QT = core network network-private testlib httpserver + +qtHaveModule(websockets): QT += websockets diff --git a/tests/auto/qabstracthttpserver/tst_qabstracthttpserver.cpp b/tests/auto/qabstracthttpserver/tst_qabstracthttpserver.cpp new file mode 100644 index 0000000..c470a71 --- /dev/null +++ b/tests/auto/qabstracthttpserver/tst_qabstracthttpserver.cpp @@ -0,0 +1,159 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include + +#if defined(QT_WEBSOCKETS_LIB) +#include +#endif + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(Q_OS_UNIX) +# include +# include +#endif + +QT_BEGIN_NAMESPACE + +class tst_QAbstractHttpServer : public QObject +{ + Q_OBJECT + +private slots: + void request_data(); + void request(); + void checkListenWarns(); + void websocket(); +}; + +void tst_QAbstractHttpServer::request_data() +{ + QTest::addColumn("host"); + QTest::addColumn("path"); + QTest::addColumn("query"); + + QTest::addRow("127.0.0.1") << "127.0.0.1" << "/" << QString(); + QTest::addRow("0.0.0.0") << "0.0.0.0" << "/" << QString(); + QTest::addRow("localhost") << "localhost" << "/" << QString(); + QTest::addRow("localhost with query") << "localhost" << "/" << QString("key=value"); +} + +void tst_QAbstractHttpServer::request() +{ + QFETCH(QString, host); + QFETCH(QString, path); + QFETCH(QString, query); + + struct HttpServer : QAbstractHttpServer + { + QUrl url; + QByteArray body; + QHttpServerRequest::Method method; + quint8 padding[4]; + + bool handleRequest(const QHttpServerRequest &request, QTcpSocket *) override + { + method = request.method(); + url = request.url(); + body = request.body(); + return true; + } + } server; + auto tcpServer = new QTcpServer; + tcpServer->listen(); + server.bind(tcpServer); + QNetworkAccessManager networkAccessManager; + QUrl url(QStringLiteral("http://%1:%2%3") + .arg(host) + .arg(tcpServer->serverPort()) + .arg(path)); + if (!query.isEmpty()) + url.setQuery(query); + const QNetworkRequest request(url); + auto reply = networkAccessManager.get(request); + QTRY_COMPARE(server.method, QHttpServerRequest::Method::Get); + QCOMPARE(server.url, url); + QCOMPARE(server.body, QByteArray()); + reply->deleteLater(); +} + +void tst_QAbstractHttpServer::checkListenWarns() +{ + struct HttpServer : QAbstractHttpServer + { + bool handleRequest(const QHttpServerRequest &, QTcpSocket *) override { return true; } + } server; + auto tcpServer = new QTcpServer; + QTest::ignoreMessage(QtWarningMsg, QRegularExpression("The TCP server .* is not listening.")); + server.bind(tcpServer); +} + +void tst_QAbstractHttpServer::websocket() +{ +#if !defined(QT_WEBSOCKETS_LIB) + QSKIP("This test requires WebSocket support"); +#endif // defined(QT_WEBSOCKETS_LIB) + struct HttpServer : QAbstractHttpServer + { + bool handleRequest(const QHttpServerRequest &, QTcpSocket *) override { return false; } + } server; + auto tcpServer = new QTcpServer; + tcpServer->listen(); + server.bind(tcpServer); + QWebSocket socket; + const QUrl url(QString::fromLatin1("ws://localhost:%1").arg(tcpServer->serverPort())); + socket.open(url); + QSignalSpy newConnectionSpy(&server, &HttpServer::newWebSocketConnection); + QTRY_COMPARE(newConnectionSpy.count(), 1); + delete server.nextPendingWebSocketConnection(); +} + +QT_END_NAMESPACE + +QTEST_MAIN(tst_QAbstractHttpServer) + +#include "tst_qabstracthttpserver.moc" diff --git a/tests/tests.pro b/tests/tests.pro new file mode 100644 index 0000000..804b740 --- /dev/null +++ b/tests/tests.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs + +SUBDIRS = \ + auto