Revision control

+ // Copyright (C) 2026 The Qt Company Ltd.
+ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
+
+ #include "httpserver.h"
+
+ #include <QHostAddress>
+ #include <QList>
+ #include <QTcpServer>
+ #include <QTcpSocket>
+
+ #include <optional>
+
+ namespace {
+
+ constexpr int maxRequestSize = 8 * 1024;
+
+ QByteArray makeHttpResponse(int statusCode, const char *reasonPhrase,
+ const QByteArray &mimeType, const QByteArray &body)
+ {
+ return QByteArray("HTTP/1.1 ") + QByteArray::number(statusCode) + ' ' + reasonPhrase + "\r\n"
+ + "Content-Type: " + mimeType + "\r\n"
+ + "Content-Length: " + QByteArray::number(body.size()) + "\r\n"
+ + "Connection: close\r\n"
+ + "\r\n" + body;
+ }
+
+ // Returns the request target (the path, without any query string) once the HTTP
+ // request line has fully arrived, or nullopt while it is still incomplete.
+ std::optional<QByteArray> tryParseRequestTarget(const QByteArray &request)
+ {
+ const int endOfLine = request.indexOf("\r\n");
+ if (endOfLine < 0)
+ return std::nullopt;
+
+ const QList<QByteArray> parts = request.left(endOfLine).split(' ');
+ if (parts.size() < 2)
+ return QByteArray();
+
+ QByteArray target = parts.at(1);
+ const int queryStart = target.indexOf('?');
+ if (queryStart >= 0)
+ target.truncate(queryStart);
+ return target;
+ }
+
+ } // namespace
+
+ HttpServer::HttpServer(QHash<QByteArray, HttpResource> routes, QObject *parent)
+ : QObject(parent)
+ , m_routes(std::move(routes))
+ , m_server(new QTcpServer(this))
+ {
+ connect(m_server, &QTcpServer::newConnection, this, [this]() {
+ while (QTcpSocket *socket = m_server->nextPendingConnection())
+ serveConnection(socket);
+ });
+ }
+
+ bool HttpServer::listen()
+ {
+ if (!m_server->listen(QHostAddress::LocalHost)) {
+ qWarning("Failed to start HTTP server: %s", qPrintable(m_server->errorString()));
+ return false;
+ }
+ return true;
+ }
+
+ quint16 HttpServer::port() const
+ {
+ return m_server->serverPort();
+ }
+
+ void HttpServer::serveConnection(QTcpSocket *socket)
+ {
+ connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater);
+ connect(socket, &QTcpSocket::readyRead, socket,
+ [this, socket, request = QByteArray()]() mutable {
+ if (socket->state() != QAbstractSocket::ConnectedState)
+ return;
+
+ request += socket->readAll();
+
+ const std::optional<QByteArray> target = tryParseRequestTarget(request);
+ if (!target) {
+ if (request.size() > maxRequestSize) {
+ qWarning("HTTP server: request line exceeded %d bytes; dropping connection",
+ maxRequestSize);
+ socket->disconnectFromHost();
+ }
+ return;
+ }
+
+ const auto route = m_routes.constFind(*target);
+ const auto response = route != m_routes.constEnd()
+ ? makeHttpResponse(200, "OK", route->mimeType, route->body)
+ : makeHttpResponse(404, "Not Found", "text/plain", "Not Found\n");
+ socket->write(response);
+ socket->disconnectFromHost();
+ });
+ }