├── .gitignore ├── HttpServer.pro ├── LICENSE ├── README.md ├── src ├── httpServer │ ├── const.h │ ├── httpConnection.cpp │ ├── httpConnection.h │ ├── httpCookie.h │ ├── httpData.cpp │ ├── httpData.h │ ├── httpRequest.cpp │ ├── httpRequest.h │ ├── httpRequestHandler.h │ ├── httpRequestRouter.cpp │ ├── httpRequestRouter.h │ ├── httpResponse.cpp │ ├── httpResponse.h │ ├── httpServer.cpp │ ├── httpServer.h │ ├── httpServerConfig.h │ ├── middleware.h │ ├── middleware │ │ ├── CORS.cpp │ │ ├── auth.cpp │ │ ├── getArray.cpp │ │ ├── getObject.cpp │ │ └── verifyJson.cpp │ ├── util.cpp │ └── util.h └── src.pro └── test ├── data ├── 404.html ├── 404_2.html ├── colorPage.png ├── data │ ├── 404.html │ ├── 404_2.html │ ├── colorPage.png │ ├── data │ │ ├── 404.html │ │ ├── 404_2.html │ │ ├── colorPage.png │ │ ├── data │ │ │ ├── 404.html │ │ │ ├── 404_2.html │ │ │ ├── colorPage.png │ │ │ └── presentation.pptx │ │ └── presentation.pptx │ └── presentation.pptx └── presentation.pptx ├── main.cpp ├── requestHandler.cpp ├── requestHandler.h └── test.pro /.gitignore: -------------------------------------------------------------------------------- 1 | # C++ objects and libs 2 | *.slo 3 | *.lo 4 | *.o 5 | *.a 6 | *.la 7 | *.lai 8 | *.so 9 | *.dll 10 | *.dylib 11 | 12 | # Qt-es 13 | object_script.*.Release 14 | object_script.*.Debug 15 | *_plugin_import.cpp 16 | /.qmake.cache 17 | /.qmake.stash 18 | *.pro.user 19 | *.pro.user.* 20 | *.qbs.user 21 | *.qbs.user.* 22 | *.moc 23 | moc_*.cpp 24 | moc_*.h 25 | qrc_*.cpp 26 | ui_*.h 27 | *.qmlc 28 | *.jsc 29 | Makefile* 30 | *build-* 31 | 32 | # Qt unit tests 33 | target_wrapper.* 34 | 35 | # QtCreator 36 | *.autosave 37 | 38 | # QtCreator Qml 39 | *.qmlproject.user 40 | *.qmlproject.user.* 41 | 42 | # QtCreator CMake 43 | CMakeLists.txt.user* 44 | 45 | # File meant for user-specific includes 46 | *.pri 47 | 48 | # Output files 49 | debug/ 50 | release/ 51 | 52 | # Public/private key files for TLS 53 | *.crt 54 | *.csr 55 | *.key 56 | *.p12 57 | 58 | .vscode -------------------------------------------------------------------------------- /HttpServer.pro: -------------------------------------------------------------------------------- 1 | TEMPLATE = subdirs 2 | 3 | SUBDIRS += src test 4 | 5 | test.depends = src 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Addison Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ================= 3 | HttpServer is a C++ library that uses the Qt platform to setup a feature rich, easy-to-use HTTP server. 4 | 5 | Two existing Qt HTTP server libraries exist out there already, but the licenses are more restrictive (GPL & LGPL), so I decided to create my own: 6 | 7 | 1. [QtWebApp](https://github.com/fffaraz/QtWebApp) 8 | 2. [qthttpserver](https://github.com/qt-labs/qthttpserver) 9 | 10 | Features 11 | ================= 12 | * Single-threaded with asynchronous callbacks 13 | * HTTP/1.1 14 | * TLS support 15 | * Compression & decompression (GZIP-only) 16 | * Easy URL router with regex matching 17 | * Form parsing (multi-part and www-form-urlencoded) 18 | * Sending files 19 | * JSON sending or receiving support 20 | * Custom error responses (e.g. HTML page or JSON response) 21 | 22 | Promises Support 23 | ================= 24 | There are two variants of this library, one with and without promise support. Promises allow for easier & cleaner development with asynchronous logic. The two variants are supported via separate branches: 25 | 26 | 1. **master** - No promise support 27 | 2. **promises** - Promise support 28 | 29 | **Note:** The variant without promise support is considered deprecated and will only be supported via bug fixes in the future. For new development, promises are encouraged. The code will remain in two separate branches until sufficient unit testing & documentation is provided for promises support. If you would like to help, please contribute! 30 | 31 | Installing 32 | ================= 33 | Prerequisites 34 | ------------- 35 | * Qt & Qt Creator for IDE 36 | * zlib 37 | * OpenSSL binaries for TLS support (see [here](https://doc.qt.io/qt-5/ssl.html#enabling-and-disabling-ssl-support)) 38 | * QtPromise for promise support (see [here](https://qtpromise.netlify.app/qtpromise/getting-started.html#installation) for installation instructions) 39 | 40 | Building HttpServer 41 | ------------------------- 42 | 1. Open `HttpServer.pro` in Qt Creator. 43 | 2. Create a `common.pri` file in the top-level directory. This will store any specific include & library paths on a per-machine basis. 44 | * Append paths to your `zlib` build with `INCLUDEPATH` and `LIBS` 45 | * Append paths to your `qtpromise` directory with `INCLUDEPATH` (QtPromise is a header-only library) 46 | * Note: You can include the provided `qtpromise.pri` to do this for your. Alternatively, you can install the headers to a system-configured path in which case you don't need to do anything. 47 | * Make sure on Windows that the compiled zlib DLL is in your environment `PATH` variable 48 | 3. Build and run the application 49 | * Building the application will build the shared library as well as the test application. When you press run, it will run the test application in which you can experiment with the library via the provided URLs 50 | 51 | **Note:** Since this is just a normal Qt project with a `pro` file, you can compile the project via the command-line with `qmake` and your platform-specific compiler (i.e. `make` for Linux or `nmake` for Windows). 52 | 53 | Example 54 | ================= 55 | See [here](https://github.com/addisonElliott/HttpServer/blob/master/test/requestHandler.cpp) for example code using HttpServer. 56 | 57 | Roadmap & Bugs 58 | ================= 59 | * TLS session resumption is not supported by Qt currently 60 | 61 | Pull requests are welcome (and encouraged) for any or all issues! 62 | 63 | License 64 | ================= 65 | HttpServer has an MIT-based [license](https://github.com/addisonElliott/HttpServer/blob/master/LICENSE). -------------------------------------------------------------------------------- /src/httpServer/const.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_CONST_H 2 | #define HTTP_SERVER_CONST_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "httpData.h" 9 | 10 | 11 | using QtPromise::QPromiseTimeoutException; 12 | using QtPromise::QPromise; 13 | using HttpDataPtr = std::shared_ptr; 14 | using HttpPromise = QPromise>; 15 | using HttpFunc = std::function data)>; 16 | using HttpResolveFunc = const QtPromise::QPromiseResolve> &; 17 | using HttpRejectFunc = const QtPromise::QPromiseReject> &; 18 | 19 | Q_DECLARE_METATYPE(QRegularExpressionMatch); 20 | 21 | #endif // HTTP_SERVER_CONST_H 22 | -------------------------------------------------------------------------------- /src/httpServer/httpConnection.cpp: -------------------------------------------------------------------------------- 1 | #include "httpConnection.h" 2 | 3 | HttpConnection::HttpConnection(HttpServerConfig *config, HttpRequestHandler *requestHandler, qintptr socketDescriptor, 4 | QSslConfiguration *sslConfig, QObject *parent) : QObject(parent), config(config), currentRequest(nullptr), 5 | currentResponse(nullptr), requestHandler(requestHandler), sslConfig(sslConfig) 6 | { 7 | timeoutTimer = new QTimer(this); 8 | keepAliveMode = false; 9 | 10 | // Create TCP or SSL socket 11 | createSocket(socketDescriptor); 12 | 13 | // Connect signals 14 | connect(socket, &QTcpSocket::readyRead, this, &HttpConnection::read); 15 | connect(socket, &QTcpSocket::bytesWritten, this, &HttpConnection::bytesWritten); 16 | connect(socket, &QTcpSocket::disconnected, this, &HttpConnection::socketDisconnected); 17 | connect(timeoutTimer, &QTimer::timeout, this, &HttpConnection::timeout); 18 | } 19 | 20 | void HttpConnection::createSocket(qintptr socketDescriptor) 21 | { 22 | // If SSL is supported and configured, then create an instance of QSslSocket 23 | if (sslConfig) 24 | { 25 | QSslSocket *sslSocket = new QSslSocket(); 26 | sslSocket->setSslConfiguration(*sslConfig); 27 | socket = sslSocket; 28 | 29 | // Use QOverload because there is another function sslErrors that causes issues 30 | // Any errors in TLS handshake will be notified via this signal 31 | connect(sslSocket, QOverload &>::of(&QSslSocket::sslErrors), this, 32 | &HttpConnection::sslErrors); 33 | } 34 | else 35 | socket = new QTcpSocket(); 36 | 37 | if (!socket->setSocketDescriptor(socketDescriptor)) 38 | { 39 | if (config->verbosity >= HttpServerConfig::Verbose::Critical) 40 | qCritical() << QString("Invalid socket descriptor given (%1)").arg(socket->errorString()); 41 | 42 | return; 43 | } 44 | 45 | if (config->verbosity >= HttpServerConfig::Verbose::Debug) 46 | qDebug().noquote() << QString("New incoming connection from %1").arg(socket->peerAddress().toString()); 47 | 48 | // Begin TLS handshake if SSL is enabled 49 | if (sslConfig) 50 | dynamic_cast(socket)->startServerEncryption(); 51 | 52 | address = socket->peerAddress(); 53 | 54 | // Begin timer for read timeout, occurs in case the client fails to send full message in a specified amount of time 55 | timeoutTimer->start(config->requestTimeout * 1000); 56 | } 57 | 58 | void HttpConnection::read() 59 | { 60 | // Looping adds support for HTTP pipelining 61 | while (socket->bytesAvailable()) 62 | { 63 | // Create new request if necessary 64 | if (!currentRequest) 65 | { 66 | currentRequest = new HttpRequest(config); 67 | currentResponse = new HttpResponse(config); 68 | } 69 | 70 | // If this returns false, that indicates there is no more data left to read 71 | // Otherwise, true means a request was parsed or the request was aborted 72 | if (!currentRequest->parseRequest(socket, currentResponse)) 73 | { 74 | // If we are reading the body, give additional time for large files 75 | if (currentRequest->state() == HttpRequest::State::ReadBody) 76 | timeoutTimer->start(config->requestTimeout * 1000); 77 | 78 | return; 79 | } 80 | 81 | // We are done parsing data, whether it be an error or not 82 | timeoutTimer->stop(); 83 | 84 | // Store request & response in map while it is processed asynchronously 85 | auto httpData = std::make_shared(currentRequest, currentResponse); 86 | data.emplace(currentResponse, httpData); 87 | pendingResponses.push(currentResponse); 88 | 89 | // If a response exists, then just send that, doesn't matter if its an error or not 90 | if (currentResponse->isValid()) 91 | { 92 | currentResponse->setupFromRequest(currentRequest); 93 | currentRequest = nullptr; 94 | currentResponse = nullptr; 95 | return; 96 | } 97 | 98 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 99 | qInfo().noquote() << QString("Received %1 request to %2 from %3").arg(currentRequest->method()) 100 | .arg(currentRequest->uriStr()).arg(address.toString()); 101 | 102 | // Handle request and setup timeout timer if necessary 103 | // Note: Wrap the handler in a promise so exceptions are handled correctly 104 | // Note: Create local copies of current request and response so they are captured by value in the lambda 105 | auto request = currentRequest; 106 | auto response = currentResponse; 107 | auto promise = HttpPromise::resolve(httpData).then([=](HttpDataPtr data) { 108 | return requestHandler->handle(data); 109 | }); 110 | if (config->responseTimeout > 0) 111 | promise = promise.timeout(config->responseTimeout * 1000); 112 | 113 | promise 114 | .fail([=](const QPromiseTimeoutException &error) { 115 | // Request timed out 116 | response->setError(HttpStatus::RequestTimeout, "", false); 117 | return nullptr; 118 | }) 119 | .fail([=](const HttpException &error) { 120 | response->setError(error.status, error.message, false); 121 | return nullptr; 122 | }) 123 | .fail([=](const std::exception &error) { 124 | response->setError(HttpStatus::InternalServerError, error.what(), false); 125 | return nullptr; 126 | }) 127 | .finally([=]() { 128 | // If response is already finished, don't do anything 129 | // This can occur if the socket is closed prematurely 130 | if (httpData->finished) 131 | return; 132 | 133 | // Handle if no response is set 134 | // This should not happen, but handle it and warn the user 135 | if (!response->isValid()) 136 | { 137 | if (config->verbosity >= HttpServerConfig::Verbose::Warning) 138 | { 139 | qWarning().noquote() << QString("No valid response set, defaulting to 500: %1 %2 %3") 140 | .arg(request->method()).arg(request->uriStr()).arg(address.toString()); 141 | } 142 | response->setError(HttpStatus::InternalServerError, "An unknown error occurred", false); 143 | } 144 | 145 | // Send response 146 | httpData->finished = true; 147 | response->prepareToSend(); 148 | 149 | // If we were waiting on this response to be sent, then call bytesWritten to get things rolling 150 | if (response == pendingResponses.front()) 151 | bytesWritten(0); 152 | }); 153 | 154 | currentResponse->setupFromRequest(currentRequest); 155 | 156 | // Clear pointers for next request 157 | currentRequest = nullptr; 158 | currentResponse = nullptr; 159 | } 160 | } 161 | 162 | void HttpConnection::bytesWritten(qint64 bytes) 163 | { 164 | bool closeConnection = false; 165 | 166 | // Keep sending the responses until the buffer fills up 167 | while (!pendingResponses.empty()) 168 | { 169 | // If the response has not been prepared for sending, it means we're still waiting for a response 170 | // from this. Due to the setup of HTTP pipelining, we must send responses in the same order we received them, 171 | // so we can't send anything else) 172 | HttpResponse *response = pendingResponses.front(); 173 | if (!response->isSending()) 174 | break; 175 | 176 | // If writeChunk returns false, means buffer is full 177 | if (!response->writeChunk(socket)) 178 | break; 179 | 180 | // Read connection header, default to keep-alive 181 | QString connection; 182 | if (!response->header("Connection", &connection)) 183 | connection = "keep-alive"; 184 | 185 | // If any of the responses say to close the connection, then do that 186 | closeConnection |= connection.contains("close", Qt::CaseInsensitive); 187 | 188 | // Delete the corresponding request for the response 189 | auto it = data.find(response); 190 | if (it != data.end()) 191 | { 192 | it->second->finished = true; 193 | data.erase(it); 194 | } 195 | 196 | // Delete response and pop from queue 197 | pendingResponses.pop(); 198 | } 199 | 200 | socket->flush(); 201 | 202 | // If we are done sending responses, close the connection or start keep-alive timer 203 | if (pendingResponses.empty()) 204 | { 205 | if (closeConnection) 206 | { 207 | socket->disconnectFromHost(); 208 | } 209 | else 210 | { 211 | keepAliveMode = true; 212 | timeoutTimer->start(config->keepAliveTimeout * 1000); 213 | } 214 | } 215 | } 216 | 217 | void HttpConnection::timeout() 218 | { 219 | // If we are in keep-alive mode (meaning this socket has already had one successful request) and there is no data 220 | // that's been read, we just close the socket peacefully 221 | if (keepAliveMode && (!currentRequest || currentRequest->state() != HttpRequest::State::ReadRequestLine)) 222 | { 223 | socket->disconnectFromHost(); 224 | return; 225 | } 226 | 227 | // Otherwise we send a request timeout response 228 | if (!currentResponse) 229 | currentResponse = new HttpResponse(config); 230 | 231 | currentResponse->setError(HttpStatus::RequestTimeout, "", true); 232 | currentResponse->prepareToSend(); 233 | 234 | // Assume that the entire request will be written in one go, relatively safe assumption 235 | currentResponse->writeChunk(socket); 236 | 237 | // This will disconnect after all bytes have been written 238 | socket->disconnectFromHost(); 239 | } 240 | 241 | void HttpConnection::socketDisconnected() 242 | { 243 | if (config->verbosity >= HttpServerConfig::Verbose::Debug) 244 | qDebug().noquote() << QString("Client %1 disconnected").arg(address.toString()); 245 | 246 | timeoutTimer->stop(); 247 | emit disconnected(); 248 | } 249 | 250 | void HttpConnection::sslErrors(const QList &errors) 251 | { 252 | if (config->verbosity >= HttpServerConfig::Verbose::Warning) 253 | { 254 | // Combine all the SSL error messages into one string delineated by commas 255 | QString errorMessages = std::accumulate(errors.begin(), errors.end(), QString(""), 256 | [](const QString str, const QSslError &error) { 257 | return str.isEmpty() ? error.errorString() : str + ", " + error.errorString(); 258 | }); 259 | 260 | qWarning().noquote() << QString("TLS handshake failed for client %1: %2").arg(address.toString()).arg(errorMessages); 261 | } 262 | 263 | // Connection will automatically disconnect 264 | // A response is not sent back here because it will not be encrypted 265 | } 266 | 267 | HttpConnection::~HttpConnection() 268 | { 269 | socket->abort(); 270 | delete socket; 271 | delete timeoutTimer; 272 | 273 | // Delete pending responses 274 | while (!pendingResponses.empty()) 275 | pendingResponses.pop(); 276 | 277 | // Clear pending requests, will be automatically cleaned up 278 | for (auto it : data) 279 | it.second->finished = true; 280 | data.clear(); 281 | 282 | if (currentRequest) 283 | { 284 | delete currentRequest; 285 | currentRequest = nullptr; 286 | } 287 | 288 | if (currentResponse) 289 | { 290 | delete currentResponse; 291 | currentResponse = nullptr; 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/httpServer/httpConnection.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_CONNECTION_H 2 | #define HTTP_SERVER_HTTP_CONNECTION_H 3 | 4 | #include "httpData.h" 5 | #include "httpServerConfig.h" 6 | #include "httpRequest.h" 7 | #include "httpRequestHandler.h" 8 | #include "httpResponse.h" 9 | #include "util.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | 23 | 24 | class HTTPSERVER_EXPORT HttpConnection : public QObject 25 | { 26 | Q_OBJECT 27 | 28 | private: 29 | HttpServerConfig *config; 30 | QTcpSocket *socket; 31 | QHostAddress address; 32 | QTimer *timeoutTimer; 33 | bool keepAliveMode; 34 | 35 | HttpRequest *currentRequest; 36 | HttpResponse *currentResponse; 37 | 38 | HttpRequestHandler *requestHandler; 39 | // Responses are stored in a queue to support HTTP pipelining and sending multiple responses 40 | std::queue pendingResponses; 41 | // Store data for each request to enable asynchronous logic 42 | std::unordered_map data; 43 | 44 | const QSslConfiguration *sslConfig; 45 | 46 | void createSocket(qintptr socketDescriptor); 47 | 48 | public: 49 | HttpConnection(HttpServerConfig *config, HttpRequestHandler *requestHandler, qintptr socketDescriptor, 50 | QSslConfiguration *sslConfig = nullptr, QObject *parent = nullptr); 51 | ~HttpConnection(); 52 | 53 | private slots: 54 | void read(); 55 | void bytesWritten(qint64 bytes); 56 | void timeout(); 57 | void socketDisconnected(); 58 | void sslErrors(const QList &errors); 59 | 60 | signals: 61 | void disconnected(); 62 | }; 63 | 64 | #endif // HTTP_SERVER_HTTP_CONNECTION_H 65 | -------------------------------------------------------------------------------- /src/httpServer/httpCookie.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_COOKIE_H 2 | #define HTTP_SERVER_HTTP_COOKIE_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "util.h" 10 | 11 | 12 | struct HTTPSERVER_EXPORT HttpCookie 13 | { 14 | QString name; 15 | QString value; 16 | int ageSeconds; 17 | QDateTime expiration; 18 | QString domain; 19 | QString path; 20 | bool secure; 21 | bool httpOnly; 22 | 23 | HttpCookie() {} 24 | HttpCookie(QString name, QString value, int ageSeconds = -1, QDateTime expiration = QDateTime(), QString domain = "", 25 | QString path = "/", bool secure = false, bool httpOnly = false) : name(name), value(value), ageSeconds(ageSeconds), 26 | expiration(expiration), domain(domain), path(path), secure(secure), httpOnly(httpOnly) {} 27 | 28 | QByteArray toByteArray() const 29 | { 30 | QByteArray buf; 31 | 32 | buf += name.toLatin1(); 33 | buf += '='; 34 | buf += QUrl::toPercentEncoding(value); 35 | 36 | if (expiration.isValid()) 37 | buf += "; Expires=" + expiration.toString(Qt::RFC2822Date); 38 | 39 | if (ageSeconds > 0) 40 | buf += "; Max-Age=" + QString::number(ageSeconds); 41 | 42 | if (!domain.isEmpty()) 43 | buf += "; Domain=" + domain; 44 | 45 | if (!path.isEmpty()) 46 | buf += "; Path=" + QUrl::toPercentEncoding(path); 47 | 48 | if (secure) 49 | buf += "; Secure"; 50 | 51 | if (httpOnly) 52 | buf += "; HttpOnly"; 53 | 54 | return buf; 55 | } 56 | }; 57 | 58 | #endif // HTTP_SERVER_HTTP_COOKIE_H 59 | -------------------------------------------------------------------------------- /src/httpServer/httpData.cpp: -------------------------------------------------------------------------------- 1 | #include "httpData.h" 2 | #include "httpRequest.h" 3 | #include "httpResponse.h" 4 | 5 | HttpData::HttpData(HttpRequest *request, HttpResponse *response) : request(request), response(response), state(), finished(false) 6 | { 7 | } 8 | 9 | void HttpData::checkFinished() 10 | { 11 | if (finished) 12 | throw HttpException(HttpStatus::None); 13 | } 14 | 15 | HttpData::~HttpData() 16 | { 17 | delete request; 18 | delete response; 19 | } 20 | -------------------------------------------------------------------------------- /src/httpServer/httpData.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_DATA_H 2 | #define HTTP_SERVER_HTTP_DATA_H 3 | 4 | #include "util.h" 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | 11 | // Forward declarations 12 | class HttpRequest; 13 | class HttpResponse; 14 | 15 | struct HTTPSERVER_EXPORT HttpData 16 | { 17 | HttpRequest *request; 18 | HttpResponse *response; 19 | std::unordered_map state; 20 | bool finished; 21 | 22 | HttpData(HttpRequest *request, HttpResponse *response); 23 | ~HttpData(); 24 | 25 | void checkFinished(); 26 | }; 27 | 28 | #endif // HTTP_SERVER_HTTP_DATA_H 29 | -------------------------------------------------------------------------------- /src/httpServer/httpRequest.cpp: -------------------------------------------------------------------------------- 1 | #include "httpRequest.h" 2 | 3 | HttpRequest::HttpRequest(HttpServerConfig *config) : config(config), buffer(), requestBytesSize(0), 4 | state_(State::ReadRequestLine), method_(), uri_(), version_(), expectedBodySize(0), body_(), mimeType_(), 5 | charset_(), boundary(), tmpFormData(nullptr) 6 | { 7 | } 8 | 9 | bool HttpRequest::parseRequest(QTcpSocket *socket, HttpResponse *response) 10 | { 11 | while (true) 12 | { 13 | switch (state_) 14 | { 15 | case State::ReadRequestLine: 16 | if (!parseRequestLine(socket, response)) 17 | return false; 18 | 19 | break; 20 | 21 | case State::ReadHeader: 22 | if (!parseHeader(socket, response)) 23 | return false; 24 | 25 | break; 26 | 27 | case State::ReadBody: 28 | if (!parseBody(socket, response)) 29 | return false; 30 | 31 | break; 32 | 33 | case State::ReadMultiFormBodyData: 34 | case State::ReadMultiFormBodyHeaders: 35 | if (!parseMultiFormBody(socket, response)) 36 | return false; 37 | 38 | break; 39 | 40 | case State::Complete: 41 | return true; 42 | 43 | case State::Abort: 44 | // This state should only be reached when a fatal error occurs where the server cannot parse the 45 | // request successfully. Any minor errors or issues where the request can still be parsed successfully 46 | // will go to the complete state still 47 | // 48 | // It is unclear how much of the data in the socket buffer is meant for this request (HTTP/1.1 supports 49 | // pipelining so multiple requests could be present). We take a gamble by just clearing all buffers 50 | socket->readAll(); 51 | buffer.clear(); 52 | return true; 53 | } 54 | } 55 | } 56 | 57 | bool HttpRequest::parseRequestLine(QTcpSocket *socket, HttpResponse *response) 58 | { 59 | // Return false if no more data is available 60 | QByteArray chunk = socket->readLine(4096); 61 | if (chunk.isEmpty()) 62 | return false; 63 | 64 | requestBytesSize += chunk.size(); 65 | if (requestBytesSize > config->maxRequestSize) 66 | { 67 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 68 | { 69 | qInfo().noquote() << QString("Maximum size for request was reached for %1 (%2)") 70 | .arg(socket->peerAddress().toString()).arg(config->maxRequestSize); 71 | } 72 | 73 | response->setError(HttpStatus::RequestHeaderFieldsTooLarge, QString("The request line was too large to parse " 74 | "(max size: %1").arg(config->maxRequestSize)); 75 | state_ = State::Abort; 76 | return true; 77 | } 78 | 79 | // Line does not end with newline, we didn't actually read the entire line (line is greater than 4096 bytes, keep reading) 80 | buffer += chunk; 81 | if (!buffer.endsWith('\n')) 82 | return true; 83 | 84 | // Remove whitespace from either ends of buffer 85 | buffer = buffer.trimmed(); 86 | 87 | // RFC2616 section 4.1 states that servers SHOULD ignore all empty lines since some buggy clients send extra 88 | // lines after POST requests 89 | if (buffer.isEmpty()) 90 | return true; 91 | 92 | auto parts = buffer.split(' '); 93 | buffer.clear(); 94 | 95 | // RFC7230 section 2.6 states that version must start with HTTP, case-sensitive 96 | if (parts.length() != 3 || !parts[2].startsWith("HTTP")) 97 | { 98 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 99 | { 100 | qInfo().noquote() << QString("Invalid HTTP request line received from %1: %2").arg(socket->peerAddress().toString()) 101 | .arg(QString(parts.join(' '))); 102 | } 103 | 104 | response->setError(HttpStatus::BadRequest, "Invalid HTTP request, invalid request line"); 105 | state_ = State::Abort; 106 | return true; 107 | } 108 | 109 | // RFC7230 section 3 states that a subset of ASCII character set must be used 110 | // The obsolete RFC2616 required Latin1 character set but that has since been changed. The Latin1 requirement was 111 | // a problem for header values when specifying certain characters. The implementation used here will be to utilize 112 | // Latin1 when it's known that non-ASCII characters won't be used, and to read UTF-8 if non-ASCII characters are 113 | // possible. 114 | // 115 | // In the future, a configuration setting could be added to allow customization of the heading character set 116 | method_ = QString::fromLatin1(parts[0]); 117 | uri_ = QUrl(parts[1]); 118 | uriQuery_ = QUrlQuery(uri_); 119 | version_ = QString::fromLatin1(parts[2]); 120 | address_ = socket->peerAddress(); 121 | state_ = State::ReadHeader; 122 | 123 | // Make sure the method specified is allowed 124 | if (std::find(allowedMethods.begin(), allowedMethods.end(), method_) == allowedMethods.end()) 125 | { 126 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 127 | qInfo().noquote() << QString("Invalid method received from %1: %2").arg(address_.toString()).arg(method_); 128 | 129 | response->setError(HttpStatus::MethodNotAllowed); 130 | return true; 131 | } 132 | 133 | if (!uri_.isValid()) 134 | { 135 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 136 | qInfo().noquote() << QString("Invalid URI received from %1: %2").arg(address_.toString()).arg(QString(parts[1])); 137 | 138 | response->setError(HttpStatus::BadRequest, "Invalid URI"); 139 | return true; 140 | } 141 | 142 | // HTTP versions 0.9 and 1.0 are deprecated and unsafe, do not allow clients that do not support HTTP/1.1 143 | // Any HTTP versions besides these should be supported (any future versions of HTTP should be backwards compatible) 144 | QString versionStr = version_.mid(4); 145 | if (versionStr == "0.9" || versionStr == "1.0") 146 | { 147 | response->setError(HttpStatus::HttpVersionNotSupported, "HTTP version must be at least 1.1"); 148 | return true; 149 | } 150 | 151 | return true; 152 | } 153 | 154 | bool HttpRequest::parseHeader(QTcpSocket *socket, HttpResponse *response) 155 | { 156 | // Return false if no more data is available 157 | QByteArray chunk = socket->readLine(4096); 158 | if (chunk.isEmpty()) 159 | return false; 160 | 161 | requestBytesSize += chunk.size(); 162 | if (requestBytesSize > config->maxRequestSize) 163 | { 164 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 165 | { 166 | qInfo().noquote() << QString("Maximum size for request was reached for %1 (%2)").arg(address_.toString()) 167 | .arg(config->maxRequestSize); 168 | } 169 | 170 | response->setError(HttpStatus::RequestHeaderFieldsTooLarge, QString("The headers were too large to parse " 171 | "(max size: %1").arg(config->maxRequestSize)); 172 | state_ = State::Abort; 173 | return true; 174 | } 175 | 176 | // Line does not end with newline, we didn't actually read the entire line (line is greater than 4096 bytes, keep reading) 177 | buffer += chunk; 178 | if (!buffer.endsWith('\n')) 179 | return true; 180 | 181 | // Remove whitespace from either ends of buffer 182 | buffer = buffer.trimmed(); 183 | 184 | // Empty line signifies end of headers 185 | if (buffer.isEmpty()) 186 | { 187 | // Parse expected body size 188 | expectedBodySize = headerDefault("Content-Length", 0); 189 | 190 | // Parse content-type header 191 | parseContentType(); 192 | 193 | // No body expected, we're done 194 | if (expectedBodySize == 0) 195 | { 196 | state_ = State::Complete; 197 | return true; 198 | } 199 | 200 | // Check if the body size is going to be larger than allowed (we have a different max for multipart data) 201 | if (mimeType_ == "multipart/form-data") 202 | { 203 | if (requestBytesSize + expectedBodySize > config->maxMultipartSize) 204 | { 205 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 206 | { 207 | qInfo().noquote() << QString("Maximum size for request was reached for %1 (%2)") 208 | .arg(address_.toString()).arg(config->maxMultipartSize); 209 | } 210 | 211 | response->setError(HttpStatus::PayloadTooLarge, QString("The body is too large to parse (max size: %1)") 212 | .arg(config->maxMultipartSize)); 213 | state_ = State::Abort; 214 | return true; 215 | } 216 | 217 | // We use this to keep track of how much of the body has been read 218 | requestBytesSize = 0; 219 | state_ = State::ReadMultiFormBodyData; 220 | } 221 | else 222 | { 223 | if (requestBytesSize + expectedBodySize > config->maxRequestSize) 224 | { 225 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 226 | { 227 | qInfo().noquote() << QString("Maximum size for request was reached for %1 (%2)") 228 | .arg(address_.toString()).arg(config->maxRequestSize); 229 | } 230 | 231 | response->setError(HttpStatus::PayloadTooLarge, QString("The body is too large to parse (max size: %1)") 232 | .arg(config->maxRequestSize)); 233 | state_ = State::Abort; 234 | return true; 235 | } 236 | 237 | state_ = State::ReadBody; 238 | } 239 | 240 | buffer.clear(); 241 | return true; 242 | } 243 | 244 | int index = buffer.indexOf(':'); 245 | if (index == -1) 246 | { 247 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 248 | { 249 | qInfo().noquote() << QString("Invalid headers in request for %1 (%2)").arg(address_.toString()) 250 | .arg(QString(buffer)); 251 | } 252 | 253 | response->setError(HttpStatus::BadRequest, "Invalid headers in request, must contain a field name and value"); 254 | buffer.clear(); 255 | return true; 256 | } 257 | 258 | // In RFC7230 multi-line folding headers are deprecated, will assume headers are contained on one line 259 | QString field = QString::fromLatin1(buffer.left(index)); 260 | QString value = buffer.mid(index + 1).trimmed(); 261 | buffer.clear(); 262 | 263 | // Save cookies in separate map 264 | if (field.compare("Cookie", Qt::CaseInsensitive) == 0) 265 | { 266 | // Split cookies by semicolons, get the key/value pair and add to map 267 | auto parts = value.split(';'); 268 | for (QString part : parts) 269 | { 270 | auto kvPart = part.split('='); 271 | if (kvPart.length() != 2) 272 | { 273 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 274 | qInfo().noquote() << QString("Invalid cookie header for %1: %2").arg(address_.toString()).arg(value); 275 | 276 | continue; 277 | } 278 | 279 | QString key = kvPart[0].trimmed(); 280 | QString value = kvPart[1]; 281 | 282 | // This will overwrite any existing cookies 283 | cookies[key] = value; 284 | } 285 | } 286 | else 287 | { 288 | // Insert new field if it doesnt exist, otherwise append to existing content with comma separator 289 | // RFC7230 section 3.2.2 states that multiple headers with the same field MUST be able to be appended via comma 290 | auto it = headers.find(field); 291 | if (it == headers.end()) 292 | headers[field] = value; 293 | else 294 | it->second += ", " + value; 295 | } 296 | 297 | return true; 298 | } 299 | 300 | bool HttpRequest::parseBody(QTcpSocket *socket, HttpResponse *response) 301 | { 302 | // Read as much data as is available 303 | // Return false if no more data is available 304 | QByteArray chunk = socket->read(expectedBodySize - buffer.size()); 305 | if (chunk.isEmpty()) 306 | return false; 307 | 308 | requestBytesSize += chunk.size(); 309 | buffer += chunk; 310 | 311 | // If the buffer size is not equal to expected body size, then we will need to return and wait for more data 312 | if (buffer.size() == expectedBodySize) 313 | { 314 | body_ = buffer; 315 | state_ = State::Complete; 316 | buffer.clear(); 317 | 318 | // Decompress gzip requests 319 | if (expectedBodySize > 0 && headerDefault("Content-Encoding", "") == "gzip") 320 | { 321 | body_ = gzipUncompress(body_); 322 | 323 | if (body_.size() == 0 && config->verbosity >= HttpServerConfig::Verbose::Info) 324 | qInfo() << "Unable to decompress GZIP request"; 325 | } 326 | 327 | // Since multipart/form-data requests are automatically buffered and parsed, we will parse URL encoded ones just 328 | // to be consistent 329 | if (mimeType_ == "application/x-www-form-urlencoded") 330 | parsePostFormBody(); 331 | 332 | return true; 333 | } 334 | 335 | return true; 336 | } 337 | 338 | bool HttpRequest::parseMultiFormBody(QTcpSocket *socket, HttpResponse *response) 339 | { 340 | // Read as much data as is available 341 | QByteArray chunk = socket->read(expectedBodySize - requestBytesSize); 342 | 343 | // Keep track of the number of bytes read in total and append the chunk to the buffer 344 | // Note that the requestBytesSize is defined to be ONLY the bytes read from the body 345 | // This is used to know when we've read the expected amount of bytes 346 | requestBytesSize += chunk.size(); 347 | buffer += chunk; 348 | 349 | if (state_ == State::ReadMultiFormBodyData) 350 | { 351 | // Construct boundary delimiter and search for it in the buffer 352 | // Note that the delimiterSize is two bytes more than the delimiter because the boundary will be suffixed by either 353 | // "\r\n" - Indicates end of current boundary, starting new one 354 | // "--" - Indicates end of current boundary, that was the last boundary 355 | const QString delimiter = "--" + boundary; 356 | const int delimiterSize = delimiter.size() + 2; 357 | const int index = buffer.indexOf(delimiter.toUtf8()); 358 | 359 | // Check if we found a match for the boundary delimiter 360 | if (index != -1) 361 | { 362 | // If there was existing format data, then we finish that up first 363 | // If there was no existing form data, then this is the beginning of the multipart data, and if the index 364 | // is not zero, this signals that the body did not start with a valid boundary 365 | if (tmpFormData) 366 | { 367 | if (tmpFormData->file) 368 | { 369 | // If the form data is a file, write the remainder of the file and set the position back to 370 | // the beginning 371 | QByteArray remainingData = buffer.left(index - 2); 372 | tmpFormData->file->write(remainingData); 373 | tmpFormData->file->seek(0); 374 | 375 | // Store to form files 376 | formFiles_.emplace(tmpFormData->name, FormFile {tmpFormData->file, tmpFormData->filename}); 377 | } 378 | else 379 | { 380 | // For non-file form data, the data is stored in the entire buffer, convert to UTF-8 string and save 381 | // Note: We are assuming that the charset is UTF-8, majority of cases this will be true 382 | QString data = QString::fromUtf8(buffer.left(index - 1)); 383 | 384 | // Store to form fields 385 | formFields_.emplace(tmpFormData->name, data); 386 | } 387 | 388 | // Delete the temporary form data and reset it 389 | delete tmpFormData; 390 | tmpFormData = nullptr; 391 | } 392 | else if (index != 0) 393 | { 394 | // We're at the start of the multipart data but it didn't start with a boundary 395 | response->setError(HttpStatus::BadRequest, "Invalid multipart form data"); 396 | state_ = State::Abort; 397 | buffer.clear(); 398 | } 399 | 400 | // Check two bytes following the delimiter 401 | // If it's CRLF, then that means there's another boundary, start reading its headers 402 | // Otherwise if it's a --, that means we're at the end of the data 403 | QString suffix = buffer.mid(index + delimiter.size(), 2); 404 | if (suffix == "\r\n") 405 | { 406 | state_ = State::ReadMultiFormBodyHeaders; 407 | buffer = buffer.mid(index + delimiterSize); 408 | } 409 | else if (suffix == "--") 410 | { 411 | state_ = State::Complete; 412 | buffer.clear(); 413 | } 414 | else 415 | { 416 | response->setError(HttpStatus::BadRequest, "Invalid multipart form data"); 417 | state_ = State::Abort; 418 | buffer.clear(); 419 | } 420 | } 421 | else if ((!tmpFormData && buffer.size() > delimiterSize) || requestBytesSize == expectedBodySize) 422 | { 423 | // This will occur in two instances: 424 | // 1. Beginning of multipart data did not start with delimiter 425 | // 2. The end of the data did not have an end boundary delimiter 426 | response->setError(HttpStatus::BadRequest, "Invalid multipart form data"); 427 | state_ = State::Abort; 428 | buffer.clear(); 429 | } 430 | else if (tmpFormData && tmpFormData->file && buffer.size() > delimiterSize) 431 | { 432 | // Append this data to the temporary file 433 | // Note: Write entire buffer minus the delimiter size because the delimiter could be split into two packets 434 | // So this guarantees that we will never miss a delimiter 435 | tmpFormData->file->write(buffer.left(buffer.size() - delimiterSize)); 436 | buffer = buffer.right(delimiterSize); 437 | return !chunk.isEmpty(); 438 | } 439 | else 440 | { 441 | return !chunk.isEmpty(); 442 | } 443 | } 444 | else if (state_ == State::ReadMultiFormBodyHeaders) 445 | { 446 | // Search for empty newline to indicate end of headers 447 | int index = buffer.indexOf("\r\n\r\n"); 448 | if (index == -1) 449 | return !chunk.isEmpty(); 450 | 451 | // Grab the header data and convert to a string (UTF-8 encoding assumed) 452 | QString headers = buffer.left(index); 453 | 454 | // Complex looking regex, but it's basically searching for the following options: 455 | // Content-Disposition: form-data; name="" 456 | // Content-Disposition: form-data; name=""; filename="" 457 | QRegularExpression regex("Content-Disposition: form-data; name=\"?([^;\"]*)\"?(?:; filename=\"?([^;\"]*)\"?)?"); 458 | auto match = regex.match(headers); 459 | if (!match.hasMatch()) 460 | { 461 | response->setError(HttpStatus::BadRequest, "Invalid multipart form data"); 462 | state_ = State::Abort; 463 | buffer.clear(); 464 | return true; 465 | } 466 | 467 | // Construct temporary form data 468 | tmpFormData = new TemporaryFormData(); 469 | tmpFormData->name = match.captured(1); 470 | tmpFormData->filename = match.captured(2); 471 | if (!tmpFormData->filename.isEmpty()) 472 | { 473 | tmpFormData->file = new QTemporaryFile(); 474 | tmpFormData->file->open(); 475 | } 476 | 477 | // Remove the headers from the buffer (+4 because of the two CRLF's) 478 | buffer = buffer.mid(index + 4); 479 | state_ = State::ReadMultiFormBodyData; 480 | } 481 | else 482 | { 483 | response->setError(HttpStatus::BadRequest, "Invalid multipart form data"); 484 | state_ = State::Abort; 485 | buffer.clear(); 486 | } 487 | 488 | return true; 489 | } 490 | 491 | void HttpRequest::parseContentType() 492 | { 493 | // No content-type header, use the default content type and charset 494 | QString contentType; 495 | if (!header("Content-Type", &contentType)) 496 | { 497 | mimeType_ = config->defaultContentType; 498 | charset_ = config->defaultCharset; 499 | return; 500 | } 501 | 502 | // Attempt to match syntax for multipart/form-data content type (specifies a boundary instead of a charset) 503 | QRegularExpression formDataRegex("^multipart/form-data;\\s*boundary=\"?([^\"]*)\"?$"); 504 | auto match = formDataRegex.match(contentType); 505 | if (match.hasMatch()) 506 | { 507 | mimeType_ = "multipart/form-data"; 508 | charset_ = config->defaultCharset; 509 | boundary = match.captured(1); 510 | return; 511 | } 512 | 513 | // Attempt to match syntax for content type with charset 514 | // If not valid, then set the default charset and the entire content-type string is the content-type 515 | QRegularExpression regex("^(.*);\\s*[cC]harset=\"?(.*)\"?$"); 516 | auto match2 = regex.match(contentType); 517 | if (!match2.hasMatch()) 518 | { 519 | mimeType_ = contentType; 520 | charset_ = config->defaultCharset; 521 | return; 522 | } 523 | 524 | // Matched syntax, first group is content-type, second is charset 525 | mimeType_ = match2.captured(1); 526 | charset_ = match2.captured(2); 527 | } 528 | 529 | void HttpRequest::parsePostFormBody() 530 | { 531 | // Clear the two maps just in case there is old data 532 | formFields_.clear(); 533 | formFiles_.clear(); 534 | 535 | QString bodyStr = parseBodyStr(); 536 | QUrlQuery query = QUrlQuery(bodyStr); 537 | 538 | // Add each query item to the form data fields 539 | for (auto item : query.queryItems(QUrl::FullyDecoded)) 540 | formFields_.emplace(item.first, item.second); 541 | 542 | // Clear the body since it has been parsed 543 | body_.clear(); 544 | } 545 | 546 | QString HttpRequest::parseBodyStr() const 547 | { 548 | // Manually parse the most common encodings first 549 | // Otherwise, fallback to QTextCodec which has an extensive list of charsets that are supported 550 | if (charset_.compare("US-ASCII", Qt::CaseInsensitive) == 0 || charset_.compare("ISO-8859-1", Qt::CaseInsensitive) == 0) 551 | return QString::fromLatin1(body_); 552 | else if (charset_.compare("UTF-8", Qt::CaseInsensitive) == 0) 553 | return QString::fromUtf8(body_); 554 | else 555 | { 556 | QTextCodec *codec = QTextCodec::codecForName(charset_.toLatin1()); 557 | if (!codec) 558 | { 559 | // Unknown codec, fallback to UTF-8 and log warning 560 | if (config->verbosity >= HttpServerConfig::Verbose::Warning) 561 | qWarning().noquote() << QString("Unknown charset when parsing body: %1. Falling back to UTF-8").arg(charset_); 562 | 563 | return QString::fromUtf8(body_); 564 | } 565 | else 566 | return codec->toUnicode(body_); 567 | } 568 | } 569 | 570 | QJsonDocument HttpRequest::parseJsonBody() const 571 | { 572 | QString bodyStr = parseBodyStr(); 573 | 574 | QJsonParseError error; 575 | QJsonDocument document = QJsonDocument::fromJson(bodyStr.toUtf8(), &error); 576 | 577 | if (config->verbosity >= HttpServerConfig::Verbose::Warning && error.error != QJsonParseError::NoError) 578 | qWarning().noquote() << QString("Unable to parse JSON document: %1").arg(error.errorString()); 579 | 580 | return document; 581 | } 582 | 583 | HttpRequest::State HttpRequest::state() const 584 | { 585 | return state_; 586 | } 587 | 588 | QHostAddress HttpRequest::address() const 589 | { 590 | return address_; 591 | } 592 | 593 | QString HttpRequest::method() const 594 | { 595 | return method_; 596 | } 597 | 598 | QUrl HttpRequest::uri() const 599 | { 600 | return uri_; 601 | } 602 | 603 | QString HttpRequest::uriStr() const 604 | { 605 | return uri_.path(); 606 | } 607 | 608 | QUrlQuery HttpRequest::uriQuery() const 609 | { 610 | return uriQuery_; 611 | } 612 | 613 | QString HttpRequest::version() const 614 | { 615 | return version_; 616 | } 617 | 618 | bool HttpRequest::hasParameter(QString name) const 619 | { 620 | return uriQuery_.hasQueryItem(name); 621 | } 622 | 623 | QString HttpRequest::parameter(QString name) const 624 | { 625 | return uriQuery_.queryItemValue(name); 626 | } 627 | 628 | bool HttpRequest::hasFragment() const 629 | { 630 | return uri_.hasFragment(); 631 | } 632 | 633 | QString HttpRequest::fragment() const 634 | { 635 | return uri_.fragment(); 636 | } 637 | 638 | QString HttpRequest::mimeType() const 639 | { 640 | return mimeType_; 641 | } 642 | 643 | QString HttpRequest::charset() const 644 | { 645 | return charset_; 646 | } 647 | 648 | void HttpRequest::setCharset(QString charset) 649 | { 650 | charset_ = charset; 651 | } 652 | 653 | std::unordered_map HttpRequest::formFields() const 654 | { 655 | return formFields_; 656 | } 657 | 658 | std::unordered_map HttpRequest::formFiles() const 659 | { 660 | return formFiles_; 661 | } 662 | 663 | QString HttpRequest::formFile(QString key) const 664 | { 665 | auto it = formFields_.find(key); 666 | return it != formFields_.end() ? it->second : ""; 667 | } 668 | 669 | FormFile HttpRequest::formField(QString key) const 670 | { 671 | auto it = formFiles_.find(key); 672 | return it != formFiles_.end() ? it->second : FormFile(); 673 | } 674 | 675 | QByteArray HttpRequest::body() const 676 | { 677 | return body_; 678 | } 679 | 680 | QString HttpRequest::cookie(QString name) const 681 | { 682 | auto it = cookies.find(name); 683 | return it == cookies.end() ? "" : it->second; 684 | } 685 | 686 | // Template specializations for header 687 | template <> 688 | short HttpRequest::headerDefault(QString key, short defaultValue, bool *ok) const 689 | { 690 | auto it = headers.find(key); 691 | if (it == headers.end()) 692 | { 693 | if (ok) *ok = false; 694 | return defaultValue; 695 | } 696 | 697 | return it->second.toShort(ok); 698 | } 699 | 700 | template <> 701 | unsigned short HttpRequest::headerDefault(QString key, unsigned short defaultValue, bool *ok) const 702 | { 703 | auto it = headers.find(key); 704 | if (it == headers.end()) 705 | { 706 | if (ok) *ok = false; 707 | return defaultValue; 708 | } 709 | 710 | return it->second.toUShort(ok); 711 | } 712 | 713 | template <> 714 | int HttpRequest::headerDefault(QString key, int defaultValue, bool *ok) const 715 | { 716 | auto it = headers.find(key); 717 | if (it == headers.end()) 718 | { 719 | if (ok) *ok = false; 720 | return defaultValue; 721 | } 722 | 723 | return it->second.toInt(ok); 724 | } 725 | 726 | template <> 727 | unsigned int HttpRequest::headerDefault(QString key, unsigned int defaultValue, bool *ok) const 728 | { 729 | auto it = headers.find(key); 730 | if (it == headers.end()) 731 | { 732 | if (ok) *ok = false; 733 | return defaultValue; 734 | } 735 | 736 | return it->second.toUInt(ok); 737 | } 738 | 739 | template <> 740 | long HttpRequest::headerDefault(QString key, long defaultValue, bool *ok) const 741 | { 742 | auto it = headers.find(key); 743 | if (it == headers.end()) 744 | { 745 | if (ok) *ok = false; 746 | return defaultValue; 747 | } 748 | 749 | return it->second.toLong(ok); 750 | } 751 | 752 | template <> 753 | unsigned long HttpRequest::headerDefault(QString key, unsigned long defaultValue, bool *ok) const 754 | { 755 | auto it = headers.find(key); 756 | if (it == headers.end()) 757 | { 758 | if (ok) *ok = false; 759 | return defaultValue; 760 | } 761 | 762 | return it->second.toULong(ok); 763 | } 764 | 765 | template <> 766 | QString HttpRequest::headerDefault(QString key, QString defaultValue, bool *ok) const 767 | { 768 | auto it = headers.find(key); 769 | if (it == headers.end()) 770 | { 771 | if (ok) *ok = false; 772 | return defaultValue; 773 | } 774 | 775 | if (ok) *ok = true; 776 | return it->second; 777 | } 778 | 779 | QString HttpRequest::headerDefault(QString key, const char *defaultValue, bool *ok) const 780 | { 781 | auto it = headers.find(key); 782 | if (it == headers.end()) 783 | { 784 | if (ok) *ok = false; 785 | return defaultValue; 786 | } 787 | 788 | if (ok) *ok = true; 789 | return it->second; 790 | } 791 | 792 | template <> 793 | QDateTime HttpRequest::headerDefault(QString key, QDateTime defaultValue, bool *ok) const 794 | { 795 | auto it = headers.find(key); 796 | if (it == headers.end()) 797 | { 798 | if (ok) *ok = false; 799 | return defaultValue; 800 | } 801 | 802 | if (ok) *ok = true; 803 | return QDateTime::fromString(it->second, Qt::RFC2822Date); 804 | } 805 | 806 | template <> 807 | float HttpRequest::headerDefault(QString key, float defaultValue, bool *ok) const 808 | { 809 | auto it = headers.find(key); 810 | if (it == headers.end()) 811 | { 812 | if (ok) *ok = false; 813 | return defaultValue; 814 | } 815 | 816 | return it->second.toFloat(ok); 817 | } 818 | 819 | template <> 820 | double HttpRequest::headerDefault(QString key, double defaultValue, bool *ok) const 821 | { 822 | auto it = headers.find(key); 823 | if (it == headers.end()) 824 | { 825 | if (ok) *ok = false; 826 | return defaultValue; 827 | } 828 | 829 | return it->second.toDouble(ok); 830 | } 831 | 832 | template <> 833 | QUrl HttpRequest::headerDefault(QString key, QUrl defaultValue, bool *ok) const 834 | { 835 | auto it = headers.find(key); 836 | if (it == headers.end()) 837 | { 838 | if (ok) *ok = false; 839 | return defaultValue; 840 | } 841 | 842 | QUrl ret = QUrl(it->second); 843 | if (ok) *ok = ret.isValid(); 844 | return ret; 845 | } 846 | 847 | template <> 848 | bool HttpRequest::header(QString key, short *value) const 849 | { 850 | auto it = headers.find(key); 851 | if (it == headers.end()) 852 | return false; 853 | 854 | bool ok; 855 | *value = it->second.toShort(&ok); 856 | return ok; 857 | } 858 | 859 | template <> 860 | bool HttpRequest::header(QString key, unsigned short *value) const 861 | { 862 | auto it = headers.find(key); 863 | if (it == headers.end()) 864 | return false; 865 | 866 | bool ok; 867 | *value = it->second.toUShort(&ok); 868 | return ok; 869 | } 870 | 871 | template <> 872 | bool HttpRequest::header(QString key, int *value) const 873 | { 874 | auto it = headers.find(key); 875 | if (it == headers.end()) 876 | return false; 877 | 878 | bool ok; 879 | *value = it->second.toInt(&ok); 880 | return ok; 881 | } 882 | 883 | template <> 884 | bool HttpRequest::header(QString key, unsigned int *value) const 885 | { 886 | auto it = headers.find(key); 887 | if (it == headers.end()) 888 | return false; 889 | 890 | bool ok; 891 | *value = it->second.toUInt(&ok); 892 | return ok; 893 | } 894 | 895 | template <> 896 | bool HttpRequest::header(QString key, long *value) const 897 | { 898 | auto it = headers.find(key); 899 | if (it == headers.end()) 900 | return false; 901 | 902 | bool ok; 903 | *value = it->second.toLong(&ok); 904 | return ok; 905 | } 906 | 907 | template <> 908 | bool HttpRequest::header(QString key, unsigned long *value) const 909 | { 910 | auto it = headers.find(key); 911 | if (it == headers.end()) 912 | return false; 913 | 914 | bool ok; 915 | *value = it->second.toULong(&ok); 916 | return ok; 917 | } 918 | 919 | template <> 920 | bool HttpRequest::header(QString key, QString *value) const 921 | { 922 | auto it = headers.find(key); 923 | if (it == headers.end()) 924 | return false; 925 | 926 | *value = it->second; 927 | return true; 928 | } 929 | 930 | template <> 931 | bool HttpRequest::header(QString key, float *value) const 932 | { 933 | auto it = headers.find(key); 934 | if (it == headers.end()) 935 | return false; 936 | 937 | bool ok; 938 | *value = it->second.toFloat(&ok); 939 | return ok; 940 | } 941 | 942 | template <> 943 | bool HttpRequest::header(QString key, double *value) const 944 | { 945 | auto it = headers.find(key); 946 | if (it == headers.end()) 947 | return false; 948 | 949 | bool ok; 950 | *value = it->second.toDouble(&ok); 951 | return ok; 952 | } 953 | 954 | template <> 955 | bool HttpRequest::header(QString key, QUrl *value) const 956 | { 957 | auto it = headers.find(key); 958 | if (it == headers.end()) 959 | return false; 960 | 961 | *value = QUrl(it->second); 962 | return value->isValid(); 963 | } 964 | 965 | HttpRequest::~HttpRequest() 966 | { 967 | // Delete each temporary file (will automatically close it) 968 | for (auto kv : formFiles_) 969 | delete kv.second.file; 970 | formFiles_.clear(); 971 | 972 | // Delete temporary form data (should always be deleted while parsing, but this may occur if an error happens) 973 | if (tmpFormData) 974 | { 975 | delete tmpFormData; 976 | tmpFormData = nullptr; 977 | } 978 | } 979 | -------------------------------------------------------------------------------- /src/httpServer/httpRequest.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_REQUEST_H 2 | #define HTTP_SERVER_HTTP_REQUEST_H 3 | 4 | #include "httpCookie.h" 5 | #include "httpResponse.h" 6 | #include "httpServerConfig.h" 7 | #include "util.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | 27 | 28 | struct HTTPSERVER_EXPORT FormFile 29 | { 30 | QTemporaryFile *file; 31 | QString filename; 32 | }; 33 | 34 | struct TemporaryFormData 35 | { 36 | QString name; 37 | QString filename; 38 | QTemporaryFile *file = nullptr; 39 | }; 40 | 41 | class HTTPSERVER_EXPORT HttpRequest 42 | { 43 | friend class HttpResponse; 44 | 45 | public: 46 | enum class State 47 | { 48 | ReadRequestLine, 49 | ReadHeader, 50 | ReadBody, 51 | ReadMultiFormBodyData, 52 | ReadMultiFormBodyHeaders, 53 | Complete, 54 | Abort 55 | }; 56 | 57 | private: 58 | const std::vector allowedMethods = {"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}; 59 | 60 | HttpServerConfig *config; 61 | 62 | QByteArray buffer; 63 | int requestBytesSize; 64 | 65 | State state_; 66 | QHostAddress address_; 67 | QString method_; 68 | QUrl uri_; 69 | QUrlQuery uriQuery_; 70 | QString version_; 71 | 72 | std::unordered_map headers; 73 | // Note: Cookies ARE case sensitive, headers are not 74 | std::unordered_map cookies; 75 | 76 | int expectedBodySize; 77 | QByteArray body_; 78 | 79 | QString mimeType_; 80 | QString charset_; 81 | QString boundary; 82 | 83 | TemporaryFormData *tmpFormData; 84 | 85 | std::unordered_map formFields_; 86 | std::unordered_map formFiles_; 87 | 88 | bool parseRequestLine(QTcpSocket *socket, HttpResponse *response); 89 | bool parseHeader(QTcpSocket *socket, HttpResponse *response); 90 | bool parseBody(QTcpSocket *socket, HttpResponse *response); 91 | bool parseMultiFormBody(QTcpSocket *socket, HttpResponse *response); 92 | 93 | void parseContentType(); 94 | void parsePostFormBody(); 95 | 96 | public: 97 | HttpRequest(HttpServerConfig *config); 98 | ~HttpRequest(); 99 | 100 | bool parseRequest(QTcpSocket *socket, HttpResponse *response); 101 | 102 | QString parseBodyStr() const; 103 | QJsonDocument parseJsonBody() const; 104 | 105 | State state() const; 106 | QHostAddress address() const; 107 | QString method() const; 108 | QUrl uri() const; 109 | QString uriStr() const; 110 | QUrlQuery uriQuery() const; 111 | QString version() const; 112 | 113 | bool hasParameter(QString name) const; 114 | QString parameter(QString name) const; 115 | bool hasFragment() const; 116 | QString fragment() const; 117 | 118 | template 119 | T headerDefault(QString key, T defaultValue, bool *ok = nullptr) const; 120 | 121 | QString headerDefault(QString key, const char *defaultValue, bool *ok = nullptr) const; 122 | 123 | template 124 | bool header(QString key, T *value) const; 125 | 126 | QString mimeType() const; 127 | QString charset() const; 128 | // Note: This function is useful if you want to override the given charset (or say if you know that the request doesn't contain a charset) 129 | void setCharset(QString charset); 130 | 131 | std::unordered_map formFields() const; 132 | std::unordered_map formFiles() const; 133 | QString formFile(QString key) const; 134 | FormFile formField(QString key) const; 135 | 136 | QByteArray body() const; 137 | QString cookie(QString name) const; 138 | }; 139 | 140 | // Declarations for templates 141 | template<> HTTPSERVER_EXPORT short HttpRequest::headerDefault(QString key, short defaultValue, bool *ok) const; 142 | template<> HTTPSERVER_EXPORT unsigned short HttpRequest::headerDefault(QString key, unsigned short defaultValue, bool *ok) const; 143 | template<> HTTPSERVER_EXPORT int HttpRequest::headerDefault(QString key, int defaultValue, bool *ok) const; 144 | template<> HTTPSERVER_EXPORT unsigned int HttpRequest::headerDefault(QString key, unsigned int defaultValue, bool *ok) const; 145 | template<> HTTPSERVER_EXPORT long HttpRequest::headerDefault(QString key, long defaultValue, bool *ok) const; 146 | template<> HTTPSERVER_EXPORT unsigned long HttpRequest::headerDefault(QString key, unsigned long defaultValue, bool *ok) const; 147 | template<> HTTPSERVER_EXPORT QString HttpRequest::headerDefault(QString key, QString defaultValue, bool *ok) const; 148 | template<> HTTPSERVER_EXPORT QDateTime HttpRequest::headerDefault(QString key, QDateTime defaultValue, bool *ok) const; 149 | template<> HTTPSERVER_EXPORT float HttpRequest::headerDefault(QString key, float defaultValue, bool *ok) const; 150 | template<> HTTPSERVER_EXPORT double HttpRequest::headerDefault(QString key, double defaultValue, bool *ok) const; 151 | template<> HTTPSERVER_EXPORT QUrl HttpRequest::headerDefault(QString key, QUrl defaultValue, bool *ok) const; 152 | 153 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, short *value) const; 154 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, unsigned short *value) const; 155 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, int *value) const; 156 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, unsigned int *value) const; 157 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, long *value) const; 158 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, unsigned long *value) const; 159 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, QString *value) const; 160 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, QDateTime *value) const; 161 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, float *value) const; 162 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, double *value) const; 163 | template<> HTTPSERVER_EXPORT bool HttpRequest::header(QString key, QUrl *value) const; 164 | 165 | #endif // HTTP_SERVER_HTTP_REQUEST_H 166 | -------------------------------------------------------------------------------- /src/httpServer/httpRequestHandler.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_REQUEST_HANDLER_H 2 | #define HTTP_SERVER_HTTP_REQUEST_HANDLER_H 3 | 4 | #include 5 | 6 | #include "const.h" 7 | #include "httpData.h" 8 | #include "httpRequest.h" 9 | #include "httpResponse.h" 10 | 11 | 12 | class HTTPSERVER_EXPORT HttpRequestHandler : public QObject 13 | { 14 | Q_OBJECT 15 | 16 | public: 17 | HttpRequestHandler(QObject *parent = nullptr) : QObject(parent) {} 18 | 19 | virtual HttpPromise handle(HttpDataPtr data) = 0; 20 | }; 21 | 22 | #endif // HTTP_SERVER_HTTP_REQUEST_HANDLER_H 23 | -------------------------------------------------------------------------------- /src/httpServer/httpRequestRouter.cpp: -------------------------------------------------------------------------------- 1 | #include "httpRequestRouter.h" 2 | 3 | void HttpRequestRouter::addRoute(QString method, QString regex, HttpFunc handler) 4 | { 5 | HttpRequestRoute route = {{method}, QRegularExpression(regex), handler}; 6 | route.pathRegex.optimize(); 7 | routes.push_back(route); 8 | } 9 | 10 | void HttpRequestRouter::addRoute(std::vector methods, QString regex, HttpFunc handler) 11 | { 12 | HttpRequestRoute route = {methods, QRegularExpression(regex), handler}; 13 | route.pathRegex.optimize(); 14 | routes.push_back(route); 15 | } 16 | 17 | HttpPromise HttpRequestRouter::route(HttpDataPtr data, bool *foundRoute) 18 | { 19 | // Iterate through each route 20 | for (const HttpRequestRoute &route : routes) 21 | { 22 | // Check for matching method and URI match 23 | const bool methodMatch = std::find(route.methods.begin(), route.methods.end(), data->request->method()) != route.methods.end(); 24 | const QRegularExpressionMatch regexMatch = route.pathRegex.match(data->request->uriStr()); 25 | 26 | // Found one, call route handler and return 27 | if (methodMatch && regexMatch.hasMatch()) 28 | { 29 | data->state["matches"] = regexMatch.capturedTexts(); 30 | data->state["match"] = QVariant::fromValue(regexMatch); 31 | 32 | if (foundRoute) *foundRoute = true; 33 | return route.handler(data); 34 | } 35 | } 36 | 37 | // No match found, defer back to handler 38 | if (foundRoute) *foundRoute = false; 39 | return HttpPromise::resolve(data); 40 | } 41 | -------------------------------------------------------------------------------- /src/httpServer/httpRequestRouter.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_REQUEST_ROUTER_H 2 | #define HTTP_SERVER_HTTP_REQUEST_ROUTER_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "const.h" 10 | #include "httpRequest.h" 11 | #include "httpResponse.h" 12 | #include "util.h" 13 | 14 | 15 | struct HttpRequestRoute 16 | { 17 | std::vector methods; 18 | QRegularExpression pathRegex; 19 | 20 | HttpFunc handler; 21 | }; 22 | 23 | class HTTPSERVER_EXPORT HttpRequestRouter 24 | { 25 | private: 26 | std::list routes; 27 | 28 | public: 29 | void addRoute(QString method, QString regex, HttpFunc handler); 30 | void addRoute(std::vector methods, QString regex, HttpFunc handler); 31 | 32 | // Allows registering member functions using addRoute(..., , &Class:memberFunction) 33 | template 34 | void addRoute(QString method, QString regex, T *inst, 35 | HttpPromise (T::*handler)(HttpDataPtr data)) 36 | { 37 | return addRoute(method, regex, std::bind(handler, inst, std::placeholders::_1)); 38 | } 39 | 40 | template 41 | void addRoute(QString method, QString regex, T *inst, 42 | HttpPromise (T::*handler)(HttpDataPtr data) const) 43 | { 44 | return addRoute(method, regex, std::bind(handler, inst, std::placeholders::_1)); 45 | } 46 | 47 | template 48 | void addRoute(std::vector methods, QString regex, T *inst, 49 | HttpPromise (T::*handler)(HttpDataPtr data)) 50 | { 51 | return addRoute(methods, regex, std::bind(handler, inst, std::placeholders::_1)); 52 | } 53 | 54 | template 55 | void addRoute(std::vector methods, QString regex, T *inst, 56 | HttpPromise (T::*handler)(HttpDataPtr data) const) 57 | { 58 | return addRoute(methods, regex, std::bind(handler, inst, std::placeholders::_1)); 59 | } 60 | 61 | HttpPromise route(HttpDataPtr data, bool *foundRoute = nullptr); 62 | }; 63 | 64 | #endif // HTTP_SERVER_HTTP_REQUEST_ROUTER_H 65 | -------------------------------------------------------------------------------- /src/httpServer/httpResponse.cpp: -------------------------------------------------------------------------------- 1 | #include "httpResponse.h" 2 | #include "httpRequest.h" 3 | 4 | HTTPSERVER_EXPORT QMimeDatabase HttpResponse::mimeDatabase; 5 | 6 | HttpResponse::HttpResponse(HttpServerConfig *config, QObject *parent) : QObject(parent), config(config), 7 | status_(HttpStatus::None) 8 | { 9 | } 10 | 11 | bool HttpResponse::isSending() const 12 | { 13 | return !buffer.isEmpty(); 14 | } 15 | 16 | bool HttpResponse::isValid() const 17 | { 18 | return status_ != HttpStatus::None; 19 | } 20 | 21 | QString HttpResponse::version() const 22 | { 23 | return version_; 24 | } 25 | 26 | HttpStatus HttpResponse::status() const 27 | { 28 | return status_; 29 | } 30 | 31 | QByteArray HttpResponse::body() const 32 | { 33 | return body_; 34 | } 35 | 36 | bool HttpResponse::header(QString key, QString *value) const 37 | { 38 | auto it = headers.find(key); 39 | if (it == headers.end()) 40 | return false; 41 | 42 | *value = it->second; 43 | return true; 44 | } 45 | 46 | bool HttpResponse::cookie(QString name, HttpCookie *cookie) const 47 | { 48 | auto it = cookies.find(name); 49 | if (it == cookies.end()) 50 | return false; 51 | 52 | *cookie = it->second; 53 | return true; 54 | } 55 | 56 | void HttpResponse::setStatus(HttpStatus status) 57 | { 58 | status_ = status; 59 | } 60 | 61 | void HttpResponse::setStatus(HttpStatus status, QByteArray body, QString contentType) 62 | { 63 | status_ = status; 64 | body_ = body; 65 | 66 | // Auto-determine content type 67 | if (contentType.isEmpty()) 68 | contentType = mimeDatabase.mimeTypeForData(body).name(); 69 | 70 | // Note that the content type here must contain the charset in addition since it cannot be deduced from the body 71 | setHeader("Content-Type", contentType); 72 | } 73 | 74 | void HttpResponse::setStatus(HttpStatus status, QJsonDocument body) 75 | { 76 | status_ = status; 77 | body_ = body.toJson(QJsonDocument::Compact); 78 | 79 | setHeader("Content-Type", "application/json"); 80 | } 81 | 82 | void HttpResponse::setStatus(HttpStatus status, QString body, QString mimeType) 83 | { 84 | status_ = status; 85 | body_ = body.toUtf8(); 86 | 87 | setHeader("Content-Type", mimeType + "; charset=utf-8"); 88 | } 89 | 90 | void HttpResponse::setBody(QByteArray body) 91 | { 92 | body_ = body; 93 | } 94 | 95 | void HttpResponse::setError(HttpStatus status, QString errorMessage, bool closeConnection) 96 | { 97 | auto it = config->errorDocumentMap.find(status); 98 | if (it != config->errorDocumentMap.end()) 99 | { 100 | QFile file(it->second); 101 | if (file.open(QIODevice::ReadOnly)) 102 | { 103 | QByteArray data = file.readAll(); 104 | data.replace("${message}", errorMessage.toUtf8()); 105 | data.replace("${statusCode}", QByteArray::number(int(status))); 106 | data.replace("${statusStr}", getHttpStatusStr(status).toUtf8()); 107 | 108 | setStatus(status, data, ""); 109 | 110 | if (config->errorDocumentCacheTime > 0) 111 | setHeader("Cache-Control", QString("max-age=%1").arg(config->errorDocumentCacheTime)); 112 | } 113 | else 114 | { 115 | // Default to JSON object if we can't open the filename 116 | QJsonObject object; 117 | object["message"] = errorMessage; 118 | setStatus(status, QJsonDocument(object)); 119 | } 120 | } 121 | else if (!errorMessage.isEmpty()) 122 | { 123 | QJsonObject object; 124 | object["message"] = errorMessage; 125 | setStatus(status, QJsonDocument(object)); 126 | } 127 | else 128 | { 129 | setStatus(status); 130 | } 131 | 132 | // If close connection is false, leave the connection header alone to default to what the client sent (or 133 | // keep-alive if client sends nothing) 134 | if (closeConnection) 135 | headers["Connection"] = "close"; 136 | } 137 | 138 | void HttpResponse::redirect(QUrl url, bool permanent) 139 | { 140 | setStatus(permanent ? HttpStatus::PermanentRedirect : HttpStatus::TemporaryRedirect); 141 | setHeader("Location", url.toString()); 142 | } 143 | 144 | void HttpResponse::redirect(QString url, bool permanent) 145 | { 146 | setStatus(permanent ? HttpStatus::PermanentRedirect : HttpStatus::TemporaryRedirect); 147 | setHeader("Location", url); 148 | } 149 | 150 | void HttpResponse::compressBody(int compressionLevel) 151 | { 152 | // Do nothing if there is no body 153 | if (body_.size() == 0) 154 | return; 155 | 156 | body_ = gzipCompress(body_, compressionLevel); 157 | setHeader("Content-Encoding", "gzip"); 158 | } 159 | 160 | void HttpResponse::sendFile(QString filename, QString mimeType, QString charset, int len, int compressionLevel, 161 | QString attachmentFilename, int cacheTime) 162 | { 163 | QFile file(filename); 164 | if (!file.open(QIODevice::ReadOnly)) 165 | { 166 | if (config->verbosity >= HttpServerConfig::Verbose::Info) 167 | qInfo().noquote() << QString("Unable to open file to be sent (%1): %2").arg(filename).arg(file.errorString()); 168 | 169 | return; 170 | } 171 | 172 | if (mimeType.isEmpty()) 173 | mimeType = mimeDatabase.mimeTypeForFile(filename, QMimeDatabase::MatchExtension).name(); 174 | 175 | sendFile(&file, mimeType, charset, len, compressionLevel, attachmentFilename, cacheTime); 176 | } 177 | 178 | void HttpResponse::sendFile(QIODevice *device, QString mimeType, QString charset, int len, int compressionLevel, 179 | QString attachmentFilename, int cacheTime) 180 | { 181 | body_ = len != -1 ? device->read(len) : device->readAll(); 182 | 183 | if (mimeType.isEmpty()) 184 | mimeType = mimeDatabase.mimeTypeForData(device).name(); 185 | 186 | setHeader("Content-Type", charset.isEmpty() ? mimeType : QString("%1; charset=%2").arg(mimeType).arg(charset)); 187 | 188 | if (!attachmentFilename.isEmpty()) 189 | setHeader("Content-Disposition", QString("attachment; filename=\"%1\"").arg(attachmentFilename)); 190 | 191 | if (cacheTime > 0) 192 | setHeader("Cache-Control", QString("max-age=%1").arg(cacheTime)); 193 | 194 | if (compressionLevel >= -1) 195 | compressBody(compressionLevel); 196 | } 197 | 198 | void HttpResponse::setCookie(HttpCookie &cookie) 199 | { 200 | // Check if the cookie exists first 201 | if (cookies.find(cookie.name) != cookies.end()) 202 | { 203 | if (config->verbosity >= HttpServerConfig::Verbose::Warning) 204 | { 205 | qWarning().noquote() << QString("HTTP response cannot have two cookies with the same name: %1") 206 | .arg(cookie.name); 207 | } 208 | 209 | return; 210 | } 211 | 212 | // Insert the cookie 213 | cookies[cookie.name] = cookie; 214 | } 215 | 216 | void HttpResponse::setHeader(QString name, QString value, bool encode) 217 | { 218 | headers[name] = encode ? QUrl::toPercentEncoding(value) : value; 219 | } 220 | 221 | void HttpResponse::setHeader(QString name, QDateTime value) 222 | { 223 | headers[name] = value.toString(Qt::RFC2822Date); 224 | } 225 | 226 | void HttpResponse::setHeader(QString name, int value) 227 | { 228 | headers[name] = QString::number(value); 229 | } 230 | 231 | void HttpResponse::setupFromRequest(HttpRequest *request) 232 | { 233 | // If no connection is specified in the response, use the value from the request or default to keep-alive 234 | if (headers.find("Connection") == headers.end()) 235 | headers["Connection"] = request ? request->headerDefault("Connection", "keep-alive") : "keep-alive"; 236 | 237 | if (status_ == HttpStatus::MethodNotAllowed && request) 238 | { 239 | // Combine the allowed methods into one string delineated by commas 240 | headers["Allow"] = std::accumulate(request->allowedMethods.begin(), request->allowedMethods.end(), QString(""), 241 | [](const QString str1, const QString str2) { 242 | return str1.isEmpty() ? str2 : str1 + ", " + str2; 243 | }); 244 | } 245 | } 246 | 247 | void HttpResponse::prepareToSend() 248 | { 249 | headers["Content-Length"] = QString::number(body_.size()); 250 | 251 | // If the connection is keep-alive, then attach the keep alive timeout value 252 | if (headers["Connection"].contains("keep-alive", Qt::CaseInsensitive)) 253 | headers["Keep-Alive"] = "timeout=" + QString::number(config->keepAliveTimeout); 254 | 255 | writeIndex = 0; 256 | buffer.clear(); 257 | // Reserve a generally acceptable amount of space 258 | buffer.reserve(2048 + body_.length()); 259 | 260 | // Status line 261 | buffer += version_; 262 | buffer += ' '; 263 | buffer += QString::number((int)status_); 264 | buffer += ' '; 265 | buffer += getHttpStatusStr(status_); 266 | buffer += "\r\n"; 267 | 268 | // Headers 269 | for (auto &keyValue : headers) 270 | { 271 | buffer += keyValue.first; 272 | buffer += ": "; 273 | buffer += keyValue.second; 274 | buffer += "\r\n"; 275 | } 276 | 277 | // Cookies 278 | for (auto &keyValue : cookies) 279 | { 280 | buffer += "Set-Cookie: "; 281 | buffer += keyValue.second.toByteArray(); 282 | buffer += "\r\n"; 283 | } 284 | 285 | // Empty line signifies end of headers 286 | buffer += "\r\n"; 287 | 288 | // Body 289 | buffer += body_; 290 | } 291 | 292 | bool HttpResponse::writeChunk(QTcpSocket *socket) 293 | { 294 | int bytesWritten = socket->write(&buffer.data()[writeIndex], buffer.length() - writeIndex); 295 | if (bytesWritten == -1) 296 | { 297 | // Force close the socket and say we're done 298 | socket->close(); 299 | return true; 300 | } 301 | 302 | // Increment the bytes written, if we wrote the entire buffer, return true, otherwise return false 303 | writeIndex += bytesWritten; 304 | if (writeIndex >= buffer.size() - 1) 305 | return true; 306 | 307 | return false; 308 | } 309 | -------------------------------------------------------------------------------- /src/httpServer/httpResponse.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTP_RESPONSE_H 2 | #define HTTP_SERVER_HTTP_RESPONSE_H 3 | 4 | #include "httpCookie.h" 5 | #include "httpServerConfig.h" 6 | #include "util.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | 18 | // Forward declaration 19 | class HttpRequest; 20 | 21 | class HTTPSERVER_EXPORT HttpResponse : public QObject 22 | { 23 | Q_OBJECT 24 | 25 | private: 26 | // Used for determining MIME type when sending files 27 | static QMimeDatabase mimeDatabase; 28 | 29 | HttpServerConfig *config; 30 | 31 | // The HTTP version is currently fixed at 1.1 since HTTP/2 is not supported 32 | QString version_ = "HTTP/1.1"; 33 | HttpStatus status_; 34 | 35 | std::unordered_map headers; 36 | // Note: Cookies ARE case sensitive, headers are not 37 | std::unordered_map cookies; 38 | 39 | QByteArray body_; 40 | 41 | int writeIndex; 42 | QByteArray buffer; 43 | 44 | public: 45 | HttpResponse(HttpServerConfig *config, QObject *parent = nullptr); 46 | 47 | bool isValid() const; 48 | bool isSending() const; 49 | 50 | QString version() const; 51 | HttpStatus status() const; 52 | QByteArray body() const; 53 | 54 | bool header(QString key, QString *value) const; 55 | bool cookie(QString name, HttpCookie *cookie) const; 56 | 57 | void setStatus(HttpStatus status); 58 | void setStatus(HttpStatus status, QByteArray body, QString contentType = ""); 59 | void setStatus(HttpStatus status, QJsonDocument body); 60 | void setStatus(HttpStatus status, QString body, QString mimeType); 61 | void setBody(QByteArray body); 62 | 63 | void setError(HttpStatus status, QString errorMessage = "", bool closeConnection = false); 64 | 65 | void redirect(QUrl url, bool permanent = false); 66 | void redirect(QString url, bool permanent = false); 67 | void compressBody(int compressionLevel = Z_DEFAULT_COMPRESSION); 68 | 69 | void sendFile(QString filename, QString mimeType = "", QString charset = "", int len = -1, 70 | int compressionLevel = -2, QString attachmentFilename = "", int cacheTime = 0); 71 | void sendFile(QIODevice *device, QString mimeType = "", QString charset = "", int len = -1, 72 | int compressionLevel = -2, QString attachmentFilename = "", int cacheTime = 0); 73 | 74 | void setCookie(HttpCookie &cookie); 75 | 76 | void setHeader(QString name, QString value, bool encode = false); 77 | void setHeader(QString name, QDateTime value); 78 | void setHeader(QString name, int value); 79 | 80 | void setupFromRequest(HttpRequest *request); 81 | void prepareToSend(); 82 | bool writeChunk(QTcpSocket *socket); 83 | }; 84 | 85 | #endif // HTTP_SERVER_HTTP_RESPONSE_H 86 | -------------------------------------------------------------------------------- /src/httpServer/httpServer.cpp: -------------------------------------------------------------------------------- 1 | #include "httpServer.h" 2 | 3 | HttpServer::HttpServer(const HttpServerConfig &config, HttpRequestHandler *requestHandler, QObject *parent) : 4 | QTcpServer(parent), config(config), requestHandler(requestHandler), sslConfig(nullptr) 5 | { 6 | setMaxPendingConnections(config.maxPendingConnections); 7 | loadSslConfig(); 8 | } 9 | 10 | bool HttpServer::listen() 11 | { 12 | if (!QTcpServer::listen(config.host, config.port)) 13 | { 14 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 15 | { 16 | qWarning().noquote() << QString("Unable to listen on %1:%2: %3").arg(config.host.toString()) 17 | .arg(config.port).arg(errorString()); 18 | } 19 | 20 | return false; 21 | } 22 | 23 | if (config.verbosity >= HttpServerConfig::Verbose::Info) 24 | qInfo() << "Listening..."; 25 | 26 | return true; 27 | } 28 | 29 | void HttpServer::close() 30 | { 31 | QTcpServer::close(); 32 | } 33 | 34 | void HttpServer::loadSslConfig() 35 | { 36 | // TODO Want to handle caching SSL sessions here if able too 37 | 38 | if (!config.sslKeyPath.isEmpty() && !config.sslCertPath.isEmpty()) 39 | { 40 | if (!QSslSocket::supportsSsl()) 41 | { 42 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 43 | { 44 | qWarning().noquote() << QString("OpenSSL is not supported for HTTP server (OpenSSL Qt build " 45 | "version: %1). Disabling TLS").arg(QSslSocket::sslLibraryBuildVersionString()); 46 | } 47 | 48 | return; 49 | } 50 | 51 | // Load the SSL certificate 52 | QFile certFile(config.sslCertPath); 53 | if (!certFile.open(QIODevice::ReadOnly)) 54 | { 55 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 56 | { 57 | qWarning().noquote() << QString("Failed to open SSL certificate file for HTTP server: %1 (%2). " 58 | "Disabling TLS").arg(config.sslCertPath).arg(certFile.errorString()); 59 | } 60 | 61 | return; 62 | } 63 | 64 | QSslCertificate certificate(&certFile, QSsl::Pem); 65 | certFile.close(); 66 | 67 | if (certificate.isNull()) 68 | { 69 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 70 | { 71 | qWarning().noquote() << QString("Invalid SSL certificate file for HTTP server: %1. Disabling TLS") 72 | .arg(config.sslCertPath); 73 | } 74 | 75 | return; 76 | } 77 | 78 | // Load the key file 79 | QFile keyFile(config.sslKeyPath); 80 | if (!keyFile.open(QIODevice::ReadOnly)) 81 | { 82 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 83 | { 84 | qWarning().noquote() << QString("Failed to open private SSL key file for HTTP server: %1 (%2). " 85 | "Disabling TLS").arg(config.sslKeyPath).arg(keyFile.errorString()); 86 | } 87 | 88 | return; 89 | } 90 | 91 | QSslKey sslKey(&keyFile, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey, config.sslKeyPassPhrase); 92 | keyFile.close(); 93 | 94 | if (sslKey.isNull()) 95 | { 96 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 97 | { 98 | qWarning().noquote() << QString("Invalid private SSL key for HTTP server: %1. Disabling TLS") 99 | .arg(config.sslKeyPath); 100 | } 101 | 102 | return; 103 | } 104 | 105 | sslConfig = new QSslConfiguration(); 106 | sslConfig->setLocalCertificate(certificate); 107 | sslConfig->setPrivateKey(sslKey); 108 | sslConfig->setPeerVerifyMode(QSslSocket::VerifyNone); 109 | sslConfig->setProtocol(QSsl::SecureProtocols); 110 | 111 | if (config.verbosity >= HttpServerConfig::Verbose::Debug) 112 | qDebug().noquote() << "Successfully setup SSL configuration, HTTPS enabled"; 113 | } 114 | else if (config.verbosity >= HttpServerConfig::Verbose::Debug) 115 | qDebug().noquote() << "No private key or certificate file path given. Disabling TLS"; 116 | } 117 | 118 | void HttpServer::incomingConnection(qintptr socketDescriptor) 119 | { 120 | if ((int)connections.size() >= config.maxConnections) 121 | { 122 | // Create TCP socket 123 | // Delete the socket automatically once a disconnected signal is received 124 | QTcpSocket *socket = new QTcpSocket(this); 125 | connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater); 126 | 127 | if (!socket->setSocketDescriptor(socketDescriptor)) 128 | { 129 | if (config.verbosity >= HttpServerConfig::Verbose::Critical) 130 | qCritical() << QString("Invalid socket descriptor given (%1)").arg(socket->errorString()); 131 | 132 | return; 133 | } 134 | 135 | if (config.verbosity >= HttpServerConfig::Verbose::Warning) 136 | { 137 | qWarning() << QString("Maximum connections reached (%1). Rejecting connection from %2") 138 | .arg(config.maxConnections).arg(socket->peerAddress().toString()); 139 | } 140 | 141 | HttpResponse *response = new HttpResponse(&config); 142 | response->setError(HttpStatus::ServiceUnavailable, "Too many connections", true); 143 | response->prepareToSend(); 144 | 145 | // Assume that the entire request will be written in one go, relatively safe assumption 146 | response->writeChunk(socket); 147 | delete response; 148 | 149 | // This will disconnect after all bytes have been written 150 | socket->disconnectFromHost(); 151 | return; 152 | } 153 | 154 | HttpConnection *connection = new HttpConnection(&config, requestHandler, socketDescriptor, sslConfig); 155 | connect(connection, &HttpConnection::disconnected, this, &HttpServer::connectionDisconnected); 156 | connections.push_back(connection); 157 | } 158 | 159 | void HttpServer::connectionDisconnected() 160 | { 161 | HttpConnection *connection = dynamic_cast(sender()); 162 | if (!connection) 163 | return; 164 | 165 | // Remove connection from connections list 166 | auto it = std::find(connections.begin(), connections.end(), connection); 167 | if (it != connections.end()) 168 | connections.erase(it); 169 | 170 | // We do delete later here because if this signal was emitted while socket is disconnecting, it still needs the 171 | // socket reference for a bit 172 | connection->deleteLater(); 173 | } 174 | 175 | HttpServer::~HttpServer() 176 | { 177 | for (HttpConnection *connection : connections) 178 | delete connection; 179 | 180 | delete sslConfig; 181 | close(); 182 | } 183 | -------------------------------------------------------------------------------- /src/httpServer/httpServer.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_HTTPSERVER_H 2 | #define HTTP_SERVER_HTTPSERVER_H 3 | 4 | #include "httpConnection.h" 5 | #include "httpServerConfig.h" 6 | #include "httpRequestHandler.h" 7 | #include "util.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | 14 | // HTTP server is HTTP/1.1 compliant and is based on RFC7230 series. This specification was created in June 2014 and 15 | // obsoletes RFC2616, RFC2145 16 | class HTTPSERVER_EXPORT HttpServer : public QTcpServer 17 | { 18 | Q_OBJECT 19 | 20 | private: 21 | HttpServerConfig config; 22 | HttpRequestHandler *requestHandler; 23 | 24 | QSslConfiguration *sslConfig; 25 | std::vector connections; 26 | 27 | void loadSslConfig(); 28 | 29 | public: 30 | HttpServer(const HttpServerConfig &config, HttpRequestHandler *requestHandler, QObject *parent = nullptr); 31 | ~HttpServer(); 32 | 33 | bool listen(); 34 | void close(); 35 | 36 | protected: 37 | void incomingConnection(qintptr socketDescriptor); 38 | 39 | private slots: 40 | void connectionDisconnected(); 41 | 42 | signals: 43 | void handleConnection(int socketDescriptor); 44 | 45 | }; 46 | 47 | #endif // HTTP_SERVER_HTTPSERVER_H 48 | -------------------------------------------------------------------------------- /src/httpServer/httpServerConfig.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_CONFIG_H 2 | #define HTTP_SERVER_CONFIG_H 3 | 4 | #include 5 | 6 | #include "util.h" 7 | 8 | 9 | struct HTTPSERVER_EXPORT HttpServerConfig 10 | { 11 | enum class Verbose 12 | { 13 | None = 0, 14 | Critical, 15 | Warning, 16 | Info, 17 | Debug, 18 | All 19 | }; 20 | 21 | QHostAddress host = QHostAddress::AnyIPv4; 22 | unsigned short port = 80; 23 | 24 | int maxConnections = 100; 25 | int maxPendingConnections = 100; 26 | 27 | int maxRequestSize = 16 * 1024; 28 | int maxMultipartSize = 1 * 1024 * 1024; 29 | 30 | // Timeout time in seconds to receive a request 31 | // The request timeout is applied for the first request and will usually be set higher. If a request is not 32 | // received by this time, an error response will be sent back. 33 | // The keep alive timeout is the amount of time to keep the connection alive for future requests. If this timeout 34 | // is met, the socket is closed and the client must open a new connection. 35 | // The response timeout is only started and used if a response is not returned immediately. This is the number of 36 | // seconds that a handler has to finish processing asynchronously before a timeout will occur 37 | int requestTimeout = 10; 38 | int keepAliveTimeout = 5; 39 | int responseTimeout = 10; 40 | 41 | QString defaultContentType = "application/octet-stream"; 42 | QString defaultCharset = "utf-8"; 43 | 44 | Verbose verbosity = Verbose::None; 45 | 46 | QString sslKeyPath = ""; 47 | QByteArray sslKeyPassPhrase; 48 | QString sslCertPath = ""; 49 | 50 | // Format errors as JSON, 51 | // If true, then do that, if no error message is given, then do not put anything 52 | // 53 | // If false, search for document in the document map, if available, then send that filename and set status 54 | // If not available, then fallback to JSON, otherwise fallback to no message at all if message isn't given 55 | 56 | std::map errorDocumentMap; 57 | int errorDocumentCacheTime = 60 * 60 * 24; // 1 day cache time 58 | 59 | HttpServerConfig() {} 60 | }; 61 | 62 | #endif // HTTP_SERVER_CONFIG_H 63 | -------------------------------------------------------------------------------- /src/httpServer/middleware.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_MIDDLEWARE_H 2 | #define HTTP_SERVER_MIDDLEWARE_H 3 | 4 | #include "const.h" 5 | #include "httpData.h" 6 | #include "httpRequest.h" 7 | #include "httpResponse.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | // Synchronous vs Asynchronous Middleware 15 | // ---------------------------------------------------------------------------------------------------- 16 | // Sync middleware can be used exactly as async middleware, e.g. with .then(middleware) clauses 17 | // but it also has the added benefit of being able to just call the function in a .then clause without 18 | // having to return it as the next promise. 19 | // 20 | // In other words, it can take this: 21 | // promise.then(middleware1).then(middleware2).then(middleware3) 22 | // and turn it into: 23 | // promise.then(middleware1).then(middleware2).then(middleware3) 24 | // promise.then([](HttpDataPtr data) { 25 | // middleware1(data); 26 | // middleware2(data); 27 | // middleware3(data); 28 | // }); 29 | // 30 | namespace middleware 31 | { 32 | // Synchronous Middleware 33 | HttpPromise CORS(HttpDataPtr data); 34 | HttpPromise verifyJson(HttpDataPtr data); 35 | HttpPromise getArray(HttpDataPtr data); 36 | HttpPromise getObject(HttpDataPtr data); 37 | HttpPromise checkAuthBasic(HttpDataPtr data, QString validUsername, QString validPassword); 38 | 39 | // Asynchronous Middleware 40 | } 41 | 42 | #endif // HTTP_SERVER_MIDDLEWARE_H 43 | -------------------------------------------------------------------------------- /src/httpServer/middleware/CORS.cpp: -------------------------------------------------------------------------------- 1 | #include "../middleware.h" 2 | 3 | namespace middleware 4 | { 5 | 6 | HttpPromise CORS(HttpDataPtr data) 7 | { 8 | data->response->setHeader("Access-Control-Allow-Origin", data->request->headerDefault("Origin", "*")); 9 | data->response->setHeader("Access-Control-Allow-Credentials", "true"); 10 | 11 | if (data->request->method() == "OPTIONS") 12 | { 13 | // Pre-flight request, send additional headers 14 | data->response->setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE"); 15 | data->response->setHeader("Access-Control-Max-Age", "3600"); 16 | data->response->setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); 17 | data->response->setStatus(HttpStatus::Ok); 18 | } 19 | 20 | return HttpPromise::resolve(data); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/httpServer/middleware/auth.cpp: -------------------------------------------------------------------------------- 1 | #include "../middleware.h" 2 | 3 | namespace middleware 4 | { 5 | 6 | HttpPromise checkAuthBasic(HttpDataPtr data, QString validUsername, QString validPassword) 7 | { 8 | QString auth; 9 | if (data->request->header("Authorization", &auth)) 10 | { 11 | if (auth.startsWith("Basic")) 12 | { 13 | QString credentials = QByteArray::fromBase64(auth.mid(6).toLatin1()); 14 | int colonIndex = credentials.indexOf(':'); 15 | if (colonIndex != -1) 16 | { 17 | QString username = credentials.left(colonIndex); 18 | QString password = credentials.mid(colonIndex + 1); 19 | 20 | // Verify username and password are correct 21 | if (username == validUsername && password == validPassword) 22 | { 23 | data->state["authUsername"] = username; 24 | data->state["authPassword"] = password; 25 | return HttpPromise::resolve(data); 26 | } 27 | } 28 | } 29 | } 30 | 31 | throw HttpException(HttpStatus::Unauthorized, "Access denied"); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/httpServer/middleware/getArray.cpp: -------------------------------------------------------------------------------- 1 | #include "../middleware.h" 2 | 3 | namespace middleware 4 | { 5 | 6 | HttpPromise getArray(HttpDataPtr data) 7 | { 8 | QJsonDocument jsonDocument = data->request->parseJsonBody(); 9 | if (jsonDocument.isNull()) 10 | throw HttpException(HttpStatus::BadRequest, "Invalid JSON"); 11 | 12 | QJsonArray requestJson = jsonDocument.array(); 13 | data->state["requestArray"] = requestJson; 14 | return HttpPromise::resolve(data); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/httpServer/middleware/getObject.cpp: -------------------------------------------------------------------------------- 1 | #include "../middleware.h" 2 | 3 | namespace middleware 4 | { 5 | 6 | HttpPromise getObject(HttpDataPtr data) 7 | { 8 | QJsonDocument jsonDocument = data->request->parseJsonBody(); 9 | if (jsonDocument.isNull()) 10 | throw HttpException(HttpStatus::BadRequest, "Invalid JSON"); 11 | 12 | QJsonObject requestJson = jsonDocument.object(); 13 | data->state["requestObject"] = requestJson; 14 | return HttpPromise::resolve(data); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/httpServer/middleware/verifyJson.cpp: -------------------------------------------------------------------------------- 1 | #include "../middleware.h" 2 | 3 | namespace middleware 4 | { 5 | 6 | HttpPromise verifyJson(HttpDataPtr data) 7 | { 8 | if (data->request->mimeType().compare("application/json", Qt::CaseInsensitive) != 0) 9 | throw HttpException(HttpStatus::BadRequest, "Request body content type must be application/json"); 10 | 11 | return HttpPromise::resolve(data); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/httpServer/util.cpp: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | QString getHttpStatusStr(HttpStatus status) 4 | { 5 | auto it = httpStatusStrs.find(static_cast(status)); 6 | if (it == httpStatusStrs.end()) 7 | return ""; 8 | 9 | return it->second; 10 | } 11 | 12 | QByteArray gzipCompress(QByteArray &data, int compressionLevel) 13 | { 14 | QByteArray ret; 15 | 16 | if (data.size() == 0) 17 | return QByteArray(); 18 | 19 | if (compressionLevel < -1 || compressionLevel > 9) 20 | compressionLevel = -1; 21 | 22 | // Set chunk size based on size of data, but clamp between 1024 bytes to 128kB 23 | // Since we're compressing, output should be smaller than the input 24 | const int chunkSize = std::min(std::max((int)qNextPowerOfTwo(data.size()), 1024), 128 * 1024); 25 | 26 | z_stream stream; 27 | stream.zalloc = Z_NULL; 28 | stream.zfree = Z_NULL; 29 | stream.opaque = Z_NULL; 30 | 31 | // Initialize deflate operation 32 | // Use default memory level (8) 33 | // Use default window bits (15) but add 16 to make output gzip instead of zlib format 34 | int err = deflateInit2(&stream, compressionLevel, Z_DEFLATED, 15 + 16, 8, Z_DEFAULT_STRATEGY); 35 | if (err != Z_OK) 36 | return QByteArray(); 37 | 38 | // Point stream to input data 39 | stream.avail_in = (unsigned int)data.size(); 40 | stream.next_in = (unsigned char *)data.data(); 41 | 42 | // In 99% of cases, this loop should only run once since the output data will be less than the input data 43 | do 44 | { 45 | // Allocate one more chunk size to the byte array 46 | ret.resize(ret.size() + chunkSize); 47 | 48 | // Point to output data 49 | stream.avail_out = (unsigned long)ret.size() - stream.total_out; 50 | stream.next_out = (unsigned char *)&ret.data()[stream.total_out]; 51 | 52 | err = deflate(&stream, Z_FINISH); 53 | } while (err == Z_OK); 54 | 55 | if (err != Z_STREAM_END) 56 | return QByteArray(); 57 | 58 | ret.resize((int)stream.total_out); 59 | deflateEnd(&stream); 60 | return ret; 61 | } 62 | 63 | QByteArray gzipUncompress(QByteArray &data) 64 | { 65 | QByteArray ret; 66 | 67 | if (data.size() == 0) 68 | return QByteArray(); 69 | 70 | // Set chunk size to be twice the size of the data to the nearest power of two 71 | // Clamped between 1024 bytes and 128kB to prevent consuming too little or too much memory 72 | // We use twice the size of the data because the average file will have a compression ratio of around 2:1 73 | // This gives a good starting point for decompressing the file in as few passes as possible 74 | const int chunkSize = std::min(std::max((int)qNextPowerOfTwo(data.size() * 2), 1024), 128 * 1024); 75 | 76 | z_stream stream; 77 | stream.zalloc = Z_NULL; 78 | stream.zfree = Z_NULL; 79 | stream.opaque = Z_NULL; 80 | 81 | // Initialize inflate operation 82 | // Use default window bits (15) but add 16 to make output gzip instead of zlib format 83 | int err = inflateInit2(&stream, 15 + 16); 84 | if (err != Z_OK) 85 | return QByteArray(); 86 | 87 | // Point stream to input data 88 | stream.avail_in = (unsigned int)data.size(); 89 | stream.next_in = (unsigned char *)data.data(); 90 | 91 | // If the compression ratio is less than 2:1, then this loop will only run once 92 | do 93 | { 94 | // Allocate one more chunk size to the byte array 95 | ret.resize(ret.size() + chunkSize); 96 | 97 | // Point to output data 98 | stream.avail_out = (unsigned long)ret.size() - stream.total_out; 99 | stream.next_out = (unsigned char *)&ret.data()[stream.total_out]; 100 | 101 | err = inflate(&stream, Z_NO_FLUSH); 102 | } while (err == Z_OK || err == Z_BUF_ERROR); // Check for Z_BUF_ERROR too because that indicates buffer not big enough 103 | 104 | if (err != Z_STREAM_END) 105 | return QByteArray(); 106 | 107 | ret.resize((int)stream.total_out); 108 | inflateEnd(&stream); 109 | return ret; 110 | } 111 | -------------------------------------------------------------------------------- /src/httpServer/util.h: -------------------------------------------------------------------------------- 1 | #ifndef HTTP_SERVER_UTIL_H 2 | #define HTTP_SERVER_UTIL_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if defined(HTTPSERVER_LIBRARY) 14 | # define HTTPSERVER_EXPORT Q_DECL_EXPORT 15 | #else 16 | # define HTTPSERVER_EXPORT Q_DECL_IMPORT 17 | #endif 18 | 19 | 20 | enum class HttpStatus 21 | { 22 | None = 0, 23 | 24 | // 1xx: Informational 25 | Continue = 100, 26 | SwitchingProtocols, 27 | Processing, 28 | 29 | // 2xx: Success 30 | Ok = 200, 31 | Created, 32 | Accepted, 33 | NonAuthoritativeInformation, 34 | NoContent, 35 | ResetContent, 36 | PartialContent, 37 | MultiStatus, 38 | AlreadyReported, 39 | IMUsed = 226, 40 | 41 | // 3xx: Redirection 42 | MultipleChoices = 300, 43 | MovedPermanently, 44 | Found, 45 | SeeOther, 46 | NotModified, 47 | UseProxy, 48 | // 306: not used, was proposed as "Switch Proxy" but never standardized 49 | TemporaryRedirect = 307, 50 | PermanentRedirect, 51 | 52 | // 4xx: Client Error 53 | BadRequest = 400, 54 | Unauthorized, 55 | PaymentRequired, 56 | Forbidden, 57 | NotFound, 58 | MethodNotAllowed, 59 | NotAcceptable, 60 | ProxyAuthenticationRequired, 61 | RequestTimeout, 62 | Conflict, 63 | Gone, 64 | LengthRequired, 65 | PreconditionFailed, 66 | PayloadTooLarge, 67 | UriTooLong, 68 | UnsupportedMediaType, 69 | RequestRangeNotSatisfiable, 70 | ExpectationFailed, 71 | ImATeapot, 72 | MisdirectedRequest = 421, 73 | UnprocessableEntity, 74 | Locked, 75 | FailedDependency, 76 | UpgradeRequired = 426, 77 | PreconditionRequired = 428, 78 | TooManyRequests, 79 | RequestHeaderFieldsTooLarge = 431, 80 | UnavailableForLegalReasons = 451, 81 | 82 | // 5xx: Server Error 83 | InternalServerError = 500, 84 | NotImplemented, 85 | BadGateway, 86 | ServiceUnavailable, 87 | GatewayTimeout, 88 | HttpVersionNotSupported, 89 | VariantAlsoNegotiates, 90 | InsufficientStorage, 91 | LoopDetected, 92 | NotExtended = 510, 93 | NetworkAuthenticationRequired, 94 | NetworkConnectTimeoutError = 599, 95 | }; 96 | 97 | /* Status Codes */ 98 | 99 | static const std::map httpStatusStrs { 100 | {100, "Continue"}, 101 | {101, "Switching Protocols"}, 102 | {102, "Processing"}, 103 | 104 | {200, "OK"}, 105 | {201, "Created"}, 106 | {202, "Accepted"}, 107 | {203, "Non-Authoritative Information"}, 108 | {204, "No Content"}, 109 | {205, "Reset Content"}, 110 | {206, "Partial Content"}, 111 | {207, "Multi-Status"}, 112 | {208, "Already Reported"}, 113 | {226, "IM Used"}, 114 | 115 | {300, "Multiple Choices"}, 116 | {301, "Moved Permanently"}, 117 | {302, "Found"}, 118 | {303, "See Other"}, 119 | {304, "Not Modified"}, 120 | {305, "Use Proxy"}, 121 | {307, "Temporary Redirect"}, 122 | {308, "Permanent Redirect"}, 123 | 124 | {400, "Bad Request"}, 125 | {401, "Unauthorized"}, 126 | {402, "Payment Required"}, 127 | {403, "Forbidden"}, 128 | {404, "Not Found"}, 129 | {405, "Method Not Allowed"}, 130 | {406, "Not Acceptable"}, 131 | {407, "Proxy Authentication Required"}, 132 | {408, "Request Timeout"}, 133 | {409, "Conflict"}, 134 | {410, "Gone"}, 135 | {411, "Length Required"}, 136 | {412, "Precondition Failed"}, 137 | {413, "Payload Too Large"}, 138 | {414, "URI Too Long"}, 139 | {415, "Unsupported Media Type"}, 140 | {416, "Range Not Satisfiable"}, 141 | {417, "Expectation Failed"}, 142 | {421, "Misdirected Request"}, 143 | {422, "Unprocessable Entity"}, 144 | {423, "Locked"}, 145 | {424, "Failed Dependency"}, 146 | {426, "Upgrade Required"}, 147 | {428, "Precondition Required"}, 148 | {429, "Too Many Requests"}, 149 | {431, "Request Header Fields Too Large"}, 150 | {451, "Unavailable For Legal Reasons"}, 151 | 152 | {500, "Internal Server Error"}, 153 | {501, "Not implemented"}, 154 | {502, "Bad Gateway"}, 155 | {503, "Service Unavailable"}, 156 | {504, "Gateway Timeout"}, 157 | {505, "HTTP Version Not Supported"}, 158 | {506, "Variant Also Negotiates"}, 159 | {507, "Insufficient Storage"}, 160 | {508, "Loop Detected"}, 161 | {510, "Not Extended"}, 162 | {511, "Network Authentication Required"} 163 | }; 164 | 165 | struct HttpException : public std::exception 166 | { 167 | const HttpStatus status; 168 | const QString message; 169 | 170 | HttpException(HttpStatus status) : status(status), message() {} 171 | HttpException(HttpStatus status, QString message) : status(status), message(message) {} 172 | }; 173 | 174 | struct QStringCaseSensitiveHash 175 | { 176 | size_t operator()(const QString str) const 177 | { 178 | static const unsigned int seed = (unsigned int)qGlobalQHashSeed(); 179 | 180 | // Case-sensitive QString hash 181 | return qHash(str, seed); 182 | } 183 | }; 184 | 185 | struct QStringCaseSensitiveEqual 186 | { 187 | bool operator()(const QString str1, const QString str2) const 188 | { 189 | return str1.compare(str2, Qt::CaseSensitive) == 0; 190 | } 191 | }; 192 | 193 | struct QStringCaseInsensitiveHash 194 | { 195 | size_t operator()(const QString str) const 196 | { 197 | static const unsigned int seed = (unsigned int)qGlobalQHashSeed(); 198 | 199 | // Case-insensitive QString hash 200 | return qHash(str.toLower(), seed); 201 | } 202 | }; 203 | 204 | struct QStringCaseInSensitiveEqual 205 | { 206 | bool operator()(const QString str1, const QString str2) const 207 | { 208 | return str1.compare(str2, Qt::CaseInsensitive) == 0; 209 | } 210 | }; 211 | 212 | namespace std 213 | { 214 | #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) 215 | // Default hash and comparator for QString is case-sensitive 216 | template<> struct hash 217 | { 218 | size_t operator()(const QString str) const 219 | { 220 | static const unsigned int seed = (unsigned int)qGlobalQHashSeed(); 221 | 222 | return qHash(str, seed); 223 | } 224 | }; 225 | #endif 226 | template<> struct equal_to 227 | { 228 | bool operator()(const QString str1, const QString str2) const 229 | { 230 | return str1.compare(str2, Qt::CaseSensitive) == 0; 231 | } 232 | }; 233 | } 234 | 235 | HTTPSERVER_EXPORT QString getHttpStatusStr(HttpStatus status); 236 | 237 | QByteArray gzipCompress(QByteArray &data, int compressionLevel = Z_DEFAULT_COMPRESSION); 238 | QByteArray gzipUncompress(QByteArray &data); 239 | 240 | #endif // HTTP_SERVER_UTIL_H 241 | -------------------------------------------------------------------------------- /src/src.pro: -------------------------------------------------------------------------------- 1 | #------------------------------------------------- 2 | # 3 | # Project created by QtCreator 2019-07-31T12:47:36 4 | # 5 | #------------------------------------------------- 6 | 7 | QT -= gui 8 | QT += network 9 | 10 | TARGET = httpServer 11 | TEMPLATE = lib 12 | 13 | DEFINES += HTTPSERVER_LIBRARY 14 | 15 | # The following define makes your compiler emit warnings if you use 16 | # any feature of Qt which has been marked as deprecated (the exact warnings 17 | # depend on your compiler). Please consult the documentation of the 18 | # deprecated API in order to know how to port your code away from it. 19 | DEFINES += QT_DEPRECATED_WARNINGS 20 | 21 | CONFIG += c++11 22 | 23 | # You can also make your code fail to compile if you use deprecated APIs. 24 | # In order to do so, uncomment the following line. 25 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 26 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 27 | 28 | SOURCES += \ 29 | httpServer/httpConnection.cpp \ 30 | httpServer/httpData.cpp \ 31 | httpServer/httpRequest.cpp \ 32 | httpServer/httpRequestRouter.cpp \ 33 | httpServer/httpResponse.cpp \ 34 | httpServer/httpServer.cpp \ 35 | httpServer/middleware/CORS.cpp \ 36 | httpServer/middleware/auth.cpp \ 37 | httpServer/middleware/getArray.cpp \ 38 | httpServer/middleware/getObject.cpp \ 39 | httpServer/middleware/verifyJson.cpp \ 40 | httpServer/util.cpp 41 | 42 | HEADERS += \ 43 | httpServer/const.h \ 44 | httpServer/httpConnection.h \ 45 | httpServer/httpCookie.h \ 46 | httpServer/httpData.h \ 47 | httpServer/httpRequest.h \ 48 | httpServer/httpRequestHandler.h \ 49 | httpServer/httpRequestRouter.h \ 50 | httpServer/httpResponse.h \ 51 | httpServer/httpServer.h \ 52 | httpServer/httpServerConfig.h \ 53 | httpServer/middleware.h \ 54 | httpServer/util.h 55 | 56 | include(../common.pri) 57 | 58 | win32: LIBS += -lzlib 59 | unix: LIBS += -lz 60 | 61 | unix { 62 | QMAKE_STRIP = 63 | 64 | headers.path = /usr/local/include/httpServer 65 | headers.files = $$HEADERS 66 | target.path = /usr/local/lib 67 | strip.path = /usr/local/lib 68 | strip.commands = strip --strip-unneeded /usr/local/lib/$(TARGET) 69 | strip.depends = install_headers install_target 70 | INSTALLS += headers target strip 71 | 72 | CONFIG(debug, debug|release) { 73 | mkpath($$PWD/build/debug) 74 | 75 | DESTDIR = $$PWD/build/debug 76 | OBJECTS_DIR = $$PWD/build/debug 77 | } 78 | 79 | CONFIG(release, debug|release) { 80 | mkpath($$PWD/build/release) 81 | 82 | DESTDIR = $$PWD/build/release 83 | OBJECTS_DIR = $$PWD/build/release 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /test/data/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

404 Error: Page not found

11 |

Go home

12 |

Email Administrator

13 | Page Not Found (404). 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/data/404_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

${statusCode} Error: ${statusStr}

11 |

Description: ${message}

12 |

Go home

13 |

Email Administrator

14 | Page Not Found (404). 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/data/colorPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/colorPage.png -------------------------------------------------------------------------------- /test/data/data/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

404 Error: Page not found

11 |

Go home

12 |

Email Administrator

13 | Page Not Found (404). 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/data/data/404_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

${statusCode} Error: ${statusStr}

11 |

Description: ${message}

12 |

Go home

13 |

Email Administrator

14 | Page Not Found (404). 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/data/data/colorPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/data/colorPage.png -------------------------------------------------------------------------------- /test/data/data/data/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

404 Error: Page not found

11 |

Go home

12 |

Email Administrator

13 | Page Not Found (404). 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/data/data/data/404_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

${statusCode} Error: ${statusStr}

11 |

Description: ${message}

12 |

Go home

13 |

Email Administrator

14 | Page Not Found (404). 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/data/data/data/colorPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/data/data/colorPage.png -------------------------------------------------------------------------------- /test/data/data/data/data/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

404 Error: Page not found

11 |

Go home

12 |

Email Administrator

13 | Page Not Found (404). 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/data/data/data/data/404_2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | You've ripped a hole in the fabric of the internet 7 | 8 | 9 | 10 |

${statusCode} Error: ${statusStr}

11 |

Description: ${message}

12 |

Go home

13 |

Email Administrator

14 | Page Not Found (404). 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/data/data/data/data/colorPage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/data/data/data/colorPage.png -------------------------------------------------------------------------------- /test/data/data/data/data/presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/data/data/data/presentation.pptx -------------------------------------------------------------------------------- /test/data/data/data/presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/data/data/presentation.pptx -------------------------------------------------------------------------------- /test/data/data/presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/data/presentation.pptx -------------------------------------------------------------------------------- /test/data/presentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/addisonElliott/HttpServer/81f1d42378d95f7d40523739bb21b5eb09fc9fd1/test/data/presentation.pptx -------------------------------------------------------------------------------- /test/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "httpServer/httpServer.h" 4 | #include "requestHandler.h" 5 | 6 | int main(int argc, char *argv[]) 7 | { 8 | QCoreApplication a(argc, argv); 9 | 10 | HttpServerConfig config; 11 | config.port = 44387; 12 | config.requestTimeout = 20; 13 | config.responseTimeout = 5; 14 | config.verbosity = HttpServerConfig::Verbose::All; 15 | config.maxMultipartSize = 512 * 1024 * 1024; 16 | config.errorDocumentMap[HttpStatus::NotFound] = "data/404_2.html"; 17 | config.errorDocumentMap[HttpStatus::InternalServerError] = "data/404_2.html"; 18 | config.errorDocumentMap[HttpStatus::BadGateway] = "data/404_2.html"; 19 | 20 | RequestHandler *handler = new RequestHandler(); 21 | HttpServer *server = new HttpServer(config, handler); 22 | server->listen(); 23 | 24 | return a.exec(); 25 | } 26 | -------------------------------------------------------------------------------- /test/requestHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "requestHandler.h" 2 | 3 | RequestHandler::RequestHandler() 4 | { 5 | router.addRoute("GET", "^/users/(\\w*)/?$", this, &RequestHandler::handleGetUsername); 6 | router.addRoute({"GET", "POST"}, "^/gzipTest/?$", this, &RequestHandler::handleGzipTest); 7 | router.addRoute({"GET", "POST"}, "^/formTest/?$", this, &RequestHandler::handleFormTest); 8 | router.addRoute("GET", "^/fileTest/(\\d*)/?$", this, &RequestHandler::handleFileTest); 9 | router.addRoute("GET", "^/errorTest/(\\d*)/?$", this, &RequestHandler::handleErrorTest); 10 | router.addRoute("GET", "^/asyncTest/(\\d*)/?$", this, &RequestHandler::handleAsyncTest); 11 | } 12 | 13 | HttpPromise RequestHandler::handle(HttpDataPtr data) 14 | { 15 | bool foundRoute; 16 | HttpPromise promise = router.route(data, &foundRoute); 17 | if (foundRoute) 18 | return promise; 19 | 20 | if (data->request->mimeType().compare("application/json", Qt::CaseInsensitive) != 0) 21 | throw HttpException(HttpStatus::BadRequest, "Request body content type must be application/json"); 22 | 23 | QJsonDocument jsonDocument = data->request->parseJsonBody(); 24 | if (jsonDocument.isNull()) 25 | throw HttpException(HttpStatus::BadRequest, "Invalid JSON body"); 26 | 27 | QJsonObject object; 28 | object["test"] = 5; 29 | object["another test"] = "OK"; 30 | 31 | data->response->setStatus(HttpStatus::Ok, QJsonDocument(object)); 32 | return HttpPromise::resolve(data); 33 | } 34 | 35 | HttpPromise RequestHandler::handleGetUsername(HttpDataPtr data) 36 | { 37 | auto match = data->state["match"].value(); 38 | QString username = match.captured(1); 39 | QJsonObject object; 40 | 41 | object["username"] = username; 42 | 43 | data->response->setStatus(HttpStatus::Ok, QJsonDocument(object)); 44 | return HttpPromise::resolve(data); 45 | } 46 | 47 | HttpPromise RequestHandler::handleGzipTest(HttpDataPtr data) 48 | { 49 | QString output = "read 24 bytes \ 50 | read 24 bytes = 48 \ 51 | read 48 bytes = 96 \ 52 | read = \ 53 | \ 54 | \ 55 | \ 56 | 1024 = min \ 57 | 128 * 1024 = max \ 58 | \ 59 | compression = next power of two chunk size \ 60 | \ 61 | decompression = next power of two chunk size (data * 2) \ 62 | Just use that as the chunk size \ 63 | \ 64 | If only 16 bytes, then je"; 65 | if (data->request->headerDefault("Content-Encoding", "") == "gzip") 66 | { 67 | qInfo() << data->request->parseBodyStr(); 68 | } 69 | data->response->setStatus(HttpStatus::Ok, output, "text/plain"); 70 | data->response->compressBody(); 71 | return HttpPromise::resolve(data); 72 | } 73 | 74 | HttpPromise RequestHandler::handleFormTest(HttpDataPtr data) 75 | { 76 | auto formFields = data->request->formFields(); 77 | auto formFiles = data->request->formFiles(); 78 | for (auto kv : formFields) 79 | { 80 | qInfo().noquote() << QString("Field %1: %2").arg(kv.first).arg(kv.second); 81 | } 82 | for (auto kv : formFiles) 83 | { 84 | QByteArray data = kv.second.file->readAll(); 85 | qInfo().noquote() << QString("File %1 (%2) size=%3: %4").arg(kv.first).arg(kv.second.filename).arg(kv.second.file->size()).arg(QString(data)); 86 | kv.second.file->copy(QString("%1/Desktop/output/%2").arg(QDir::homePath()).arg(kv.second.filename)); 87 | } 88 | data->response->setStatus(HttpStatus::Ok); 89 | return HttpPromise::resolve(data); 90 | } 91 | 92 | HttpPromise RequestHandler::handleFileTest(HttpDataPtr data) 93 | { 94 | auto match = data->state["match"].value(); 95 | int id = match.captured(1).toInt(); 96 | 97 | switch (id) 98 | { 99 | case 1: 100 | data->response->sendFile("data/404.html", "text/html", "utf-8"); 101 | break; 102 | 103 | case 2: 104 | data->response->sendFile("data/404.html", "text/html", ""); 105 | break; 106 | 107 | case 3: 108 | data->response->sendFile("data/404.html", "text/html", "", -1, Z_DEFAULT_COMPRESSION); 109 | break; 110 | 111 | case 4: 112 | data->response->sendFile("data/colorPage.png", "image/png", "", -1, Z_DEFAULT_COMPRESSION, "colorPage.png"); 113 | break; 114 | 115 | case 5: 116 | data->response->sendFile("data/colorPage.png", "image/png", "", -1, -2, "colorPage.png"); 117 | break; 118 | 119 | case 6: 120 | data->response->sendFile("data/colorPage.png", "image/png", "", -1, -2, "", 3600); 121 | break; 122 | 123 | case 7: 124 | data->response->sendFile("data/colorPage.png", "image/png", "", -1, Z_DEFAULT_COMPRESSION, "", 3600); 125 | break; 126 | 127 | case 8: 128 | data->response->sendFile("data/404.html", "text/html", "utf-8", 100); 129 | break; 130 | 131 | case 9: 132 | data->response->sendFile("data/404.html"); 133 | break; 134 | 135 | case 10: 136 | data->response->sendFile("data/404.html", "", "utf-8"); 137 | break; 138 | 139 | case 11: 140 | data->response->sendFile("data/colorPage.png"); 141 | break; 142 | 143 | case 12: 144 | data->response->sendFile("data/presentation.pptx"); 145 | break; 146 | 147 | default: 148 | throw new HttpException(HttpStatus::BadRequest); 149 | } 150 | 151 | data->response->setStatus(HttpStatus::Ok); 152 | return HttpPromise::resolve(data); 153 | } 154 | 155 | HttpPromise RequestHandler::handleErrorTest(HttpDataPtr data) 156 | { 157 | auto match = data->state["match"].value(); 158 | int statusCode = match.captured(1).toInt(); 159 | HttpStatus status = (HttpStatus)statusCode; 160 | data->response->setError(status, "There was an error here. Details go here"); 161 | return HttpPromise::resolve(data); 162 | } 163 | 164 | HttpPromise RequestHandler::handleAsyncTest(HttpDataPtr data) 165 | { 166 | auto match = data->state["match"].value(); 167 | int delay = match.captured(1).toInt(); 168 | return HttpPromise::resolve(data).delay(delay * 1000).then([](HttpDataPtr data) { 169 | qInfo() << "Timeout reached"; 170 | data->checkFinished(); 171 | 172 | data->response->setStatus(HttpStatus::Ok); 173 | return data; 174 | }); 175 | } 176 | -------------------------------------------------------------------------------- /test/requestHandler.h: -------------------------------------------------------------------------------- 1 | #ifndef REQUESTHANDLER_H 2 | #define REQUESTHANDLER_H 3 | 4 | #include 5 | #include 6 | 7 | #include "httpServer/httpData.h" 8 | #include "httpServer/httpRequestHandler.h" 9 | #include "httpServer/httpRequestRouter.h" 10 | 11 | 12 | using QtPromise::QPromise; 13 | 14 | class RequestHandler : public HttpRequestHandler 15 | { 16 | private: 17 | HttpRequestRouter router; 18 | 19 | public: 20 | RequestHandler(); 21 | 22 | HttpPromise handle(HttpDataPtr data); 23 | 24 | HttpPromise handleGetUsername(HttpDataPtr data); 25 | HttpPromise handleGzipTest(HttpDataPtr data); 26 | HttpPromise handleFormTest(HttpDataPtr data); 27 | HttpPromise handleFileTest(HttpDataPtr data); 28 | HttpPromise handleErrorTest(HttpDataPtr data); 29 | HttpPromise handleAsyncTest(HttpDataPtr data); 30 | }; 31 | 32 | #endif // REQUESTHANDLER_H 33 | -------------------------------------------------------------------------------- /test/test.pro: -------------------------------------------------------------------------------- 1 | TARGET = httpServerTest 2 | 3 | QT += network 4 | QT -= gui 5 | 6 | CONFIG += c++11 console 7 | CONFIG -= app_bundle 8 | 9 | # The following define makes your compiler emit warnings if you use 10 | # any Qt feature that has been marked deprecated (the exact warnings 11 | # depend on your compiler). Please consult the documentation of the 12 | # deprecated API in order to know how to port your code away from it. 13 | DEFINES += QT_DEPRECATED_WARNINGS 14 | 15 | # You can also make your code fail to compile if it uses deprecated APIs. 16 | # In order to do so, uncomment the following line. 17 | # You can also select to disable deprecated APIs only up to a certain version of Qt. 18 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 19 | 20 | SOURCES += \ 21 | main.cpp \ 22 | requestHandler.cpp 23 | 24 | HEADERS += \ 25 | requestHandler.h 26 | 27 | # Copy data folder to build directory 28 | win32 { 29 | copydata.commands = $(COPY_DIR) $$shell_path($$PWD/data) $$shell_path($$OUT_PWD/data) 30 | first.depends = $(first) copydata 31 | export(first.depends) 32 | export(copydata.commands) 33 | QMAKE_EXTRA_TARGETS += first copydata 34 | } 35 | 36 | unix { 37 | CONFIG(debug, debug|release) { 38 | mkpath($$PWD/debug) 39 | 40 | DESTDIR = $$PWD/debug 41 | OBJECTS_DIR = $$PWD/debug 42 | } 43 | 44 | CONFIG(release, debug|release) { 45 | mkpath($$PWD/release) 46 | 47 | DESTDIR = $$PWD/release 48 | OBJECTS_DIR = $$PWD/release 49 | } 50 | } 51 | 52 | # Link to httpServer library 53 | INCLUDEPATH += $$PWD/../src 54 | DEPENDPATH += $$PWD/../src 55 | 56 | CONFIG(release, debug|release): LIBS += -L$$OUT_PWD/../src/release/ -lhttpServer 57 | CONFIG(debug, debug|release): LIBS += -L$$OUT_PWD/../src/debug/ -lhttpServer 58 | 59 | include(../common.pri) 60 | --------------------------------------------------------------------------------