summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesus Fernandez <jesus.fernandez@qt.io>2018-03-13 15:20:47 +0100
committerJesus Fernandez <Jesus.Fernandez@qt.io>2018-09-26 12:38:56 +0000
commitf53818c8ef84999286da2fd83d32aeb2291668f5 (patch)
tree05d505d7e11ba015d102793028e702492624fbb5
parent98561703596d9ff77f74eeedb14cf81de5bc2c94 (diff)
Introduce QHttpServerResponder
It encapsulates the socket and gives an API to answer the received requests. Change-Id: Ic95db2c50224a650a02b206faca9a0ff8d1cc62b Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io> Reviewed-by: Ryan Chu <ryan.chu@qt.io> Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
-rw-r--r--src/httpserver/httpserver.pro3
-rw-r--r--src/httpserver/qabstracthttpserver.cpp8
-rw-r--r--src/httpserver/qabstracthttpserver.h3
-rw-r--r--src/httpserver/qhttpserverresponder.cpp392
-rw-r--r--src/httpserver/qhttpserverresponder.h179
-rw-r--r--src/httpserver/qhttpserverresponder_p.h114
-rw-r--r--tests/auto/auto.pro4
-rw-r--r--tests/auto/qhttpserverresponder/qhttpserverresponder.pro5
-rw-r--r--tests/auto/qhttpserverresponder/tst_qhttpserverresponder.cpp159
9 files changed, 865 insertions, 2 deletions
diff --git a/src/httpserver/httpserver.pro b/src/httpserver/httpserver.pro
index d2139f6..3389915 100644
--- a/src/httpserver/httpserver.pro
+++ b/src/httpserver/httpserver.pro
@@ -9,11 +9,14 @@ HEADERS += \
qthttpserverglobal.h \
qabstracthttpserver.h \
qabstracthttpserver_p.h \
+ qhttpserverresponder.h \
+ qhttpserverresponder_p.h \
qhttpserverrequest.h \
qhttpserverrequest_p.h
SOURCES += \
qabstracthttpserver.cpp \
+ qhttpserverresponder.cpp \
qhttpserverrequest.cpp
include(../3rdparty/http-parser.pri)
diff --git a/src/httpserver/qabstracthttpserver.cpp b/src/httpserver/qabstracthttpserver.cpp
index cbb9ad5..7a3ea58 100644
--- a/src/httpserver/qabstracthttpserver.cpp
+++ b/src/httpserver/qabstracthttpserver.cpp
@@ -38,6 +38,7 @@
#include <QtHttpServer/qabstracthttpserver.h>
#include <QtHttpServer/qhttpserverrequest.h>
+#include <QtHttpServer/qhttpserverresponder.h>
#include <private/qabstracthttpserver_p.h>
#include <private/qhttpserverrequest_p.h>
@@ -89,6 +90,7 @@ void QAbstractHttpServerPrivate::handleReadyRead()
{
Q_Q(QAbstractHttpServer);
auto socket = qobject_cast<QTcpSocket *>(currentSender->sender);
+ Q_ASSERT(socket);
#if !defined(QT_NO_USERDATA)
auto request = static_cast<QHttpServerRequest *>(socket->userData(uint(userDataId)));
#else
@@ -271,4 +273,10 @@ QWebSocket *QAbstractHttpServer::nextPendingWebSocketConnection()
}
#endif
+QHttpServerResponder QAbstractHttpServer::makeResponder(const QHttpServerRequest &request,
+ QTcpSocket *socket)
+{
+ return QHttpServerResponder(request, socket);
+}
+
QT_END_NAMESPACE
diff --git a/src/httpserver/qabstracthttpserver.h b/src/httpserver/qabstracthttpserver.h
index 101a65b..8089c0b 100644
--- a/src/httpserver/qabstracthttpserver.h
+++ b/src/httpserver/qabstracthttpserver.h
@@ -49,6 +49,7 @@
QT_BEGIN_NAMESPACE
class QHttpServerRequest;
+class QHttpServerResponder;
class QTcpServer;
class QTcpSocket;
class QWebSocket;
@@ -81,6 +82,8 @@ protected:
QAbstractHttpServer(QAbstractHttpServerPrivate &dd, QObject *parent = nullptr);
virtual bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) = 0;
+ static QHttpServerResponder makeResponder(const QHttpServerRequest &request,
+ QTcpSocket *socket);
private:
Q_DECLARE_PRIVATE(QAbstractHttpServer)
diff --git a/src/httpserver/qhttpserverresponder.cpp b/src/httpserver/qhttpserverresponder.cpp
new file mode 100644
index 0000000..1a0d8d3
--- /dev/null
+++ b/src/httpserver/qhttpserverresponder.cpp
@@ -0,0 +1,392 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtHttpServer module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtHttpServer/qhttpserverresponder.h>
+#include <QtHttpServer/qhttpserverrequest.h>
+#include <private/qhttpserverresponder_p.h>
+#include <private/qhttpserverrequest_p.h>
+#include <QtCore/qjsondocument.h>
+#include <QtCore/qloggingcategory.h>
+#include <QtCore/qtimer.h>
+#include <QtNetwork/qtcpsocket.h>
+#if defined(QT_WEBSOCKETS_LIB)
+#include <QtWebSockets/qwebsocket.h>
+#include <private/qwebsocket_p.h>
+#include <private/qwebsockethandshakeresponse_p.h>
+#include <private/qwebsockethandshakerequest_p.h>
+#endif
+
+#include <map>
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+static const QLoggingCategory &lc()
+{
+ static const QLoggingCategory category("qt.httpserver.response");
+ return category;
+}
+
+// https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+static const std::map<QHttpServerResponder::StatusCode, QByteArray> statusString {
+#define STATUS_CODE(CODE, TEXT) { QHttpServerResponder::StatusCode::CODE, QByteArrayLiteral(TEXT) }
+ STATUS_CODE(Continue, "Continue"),
+ STATUS_CODE(SwitchingProtocols, "Switching Protocols"),
+ STATUS_CODE(Processing, "Processing"),
+ STATUS_CODE(Ok, "OK"),
+ STATUS_CODE(Created, "Created"),
+ STATUS_CODE(Accepted, "Accepted"),
+ STATUS_CODE(NonAuthoritativeInformation, "Non-Authoritative Information"),
+ STATUS_CODE(NoContent, "No Content"),
+ STATUS_CODE(ResetContent, "Reset Content"),
+ STATUS_CODE(PartialContent, "Partial Content"),
+ STATUS_CODE(MultiStatus, "Multi Status"),
+ STATUS_CODE(AlreadyReported, "Already Reported"),
+ STATUS_CODE(IMUsed, "IM Used"),
+ STATUS_CODE(MultipleChoices, "Multiple Choices"),
+ STATUS_CODE(MovedPermanently, "Moved Permanently"),
+ STATUS_CODE(Found, "Found"),
+ STATUS_CODE(SeeOther, "See Other"),
+ STATUS_CODE(NotModified, "Not Modified"),
+ STATUS_CODE(UseProxy, "Use Proxy"),
+ STATUS_CODE(TemporaryRedirect, "Temporary Redirect"),
+ STATUS_CODE(PermanentRedirect, "Permanent Redirect"),
+ STATUS_CODE(BadRequest, "Bad Request"),
+ STATUS_CODE(Unauthorized, "Unauthorized"),
+ STATUS_CODE(PaymentRequired, "Payment Required"),
+ STATUS_CODE(Forbidden, "Forbidden"),
+ STATUS_CODE(NotFound, "Not Found"),
+ STATUS_CODE(MethodNotAllowed, "Method Not Allowed"),
+ STATUS_CODE(NotAcceptable, "Not Acceptable"),
+ STATUS_CODE(ProxyAuthenticationRequired, "Proxy Authentication Required"),
+ STATUS_CODE(RequestTimeout, "Request Time-out"),
+ STATUS_CODE(Conflict, "Conflict"),
+ STATUS_CODE(Gone, "Gone"),
+ STATUS_CODE(LengthRequired, "Length Required"),
+ STATUS_CODE(PreconditionFailed, "Precondition Failed"),
+ STATUS_CODE(PayloadTooLarge, "Payload Too Large"),
+ STATUS_CODE(UriTooLong, "URI Too Long"),
+ STATUS_CODE(UnsupportedMediaType, "Unsupported Media Type"),
+ STATUS_CODE(RequestRangeNotSatisfiable, "Request Range Not Satisfiable"),
+ STATUS_CODE(ExpectationFailed, "Expectation Failed"),
+ STATUS_CODE(ImATeapot, "I'm A Teapot"),
+ STATUS_CODE(MisdirectedRequest, "Misdirected Request"),
+ STATUS_CODE(UnprocessableEntity, "Unprocessable Entity"),
+ STATUS_CODE(Locked, "Locked"),
+ STATUS_CODE(FailedDependency, "Failed Dependency"),
+ STATUS_CODE(UpgradeRequired, "Upgrade Required"),
+ STATUS_CODE(PreconditionRequired, "Precondition Required"),
+ STATUS_CODE(TooManyRequests, "Too Many Requests"),
+ STATUS_CODE(RequestHeaderFieldsTooLarge, "Request Header Fields Too Large"),
+ STATUS_CODE(UnavailableForLegalReasons, "Unavailable For Legal Reasons"),
+ STATUS_CODE(InternalServerError, "Internal Server Error"),
+ STATUS_CODE(NotImplemented, "Not Implemented"),
+ STATUS_CODE(BadGateway, "Bad Gateway"),
+ STATUS_CODE(ServiceUnavailable, "Service Unavailable"),
+ STATUS_CODE(GatewayTimeout, "Gateway Time-out"),
+ STATUS_CODE(HttpVersionNotSupported, "HTTP Version not supported"),
+ STATUS_CODE(VariantAlsoNegotiates, "Variant Also Negotiates"),
+ STATUS_CODE(InsufficientStorage, "Insufficient Storage"),
+ STATUS_CODE(LoopDetected, "Loop Detected"),
+ STATUS_CODE(NotExtended, "Not Extended"),
+ STATUS_CODE(NetworkAuthenticationRequired, "Network Authentication Required"),
+ STATUS_CODE(NetworkConnectTimeoutError, "Network Connect Timeout Error"),
+#undef STATUS_CODE
+};
+
+static const QByteArray contentTypeString(QByteArrayLiteral("Content-Type"));
+static const QByteArray contentLengthString(QByteArrayLiteral("Content-Length"));
+
+template <qint64 BUFFERSIZE = 512>
+struct IOChunkedTransfer
+{
+ // TODO This is not the fastest implementation, as it does read & write
+ // in a sequential fashion, but these operation could potentially overlap.
+ // TODO Can we implement it without the buffer? Direct write to the target buffer
+ // would be great.
+
+ const qint64 bufferSize = BUFFERSIZE;
+ char buffer[BUFFERSIZE];
+ qint64 beginIndex = -1;
+ qint64 endIndex = -1;
+ QScopedPointer<QIODevice, QScopedPointerDeleteLater> source;
+ const QPointer<QIODevice> sink;
+ const QMetaObject::Connection bytesWrittenConnection;
+ const QMetaObject::Connection readyReadConnection;
+ IOChunkedTransfer(QIODevice *input, QIODevice *output) :
+ source(input),
+ sink(output),
+ bytesWrittenConnection(QObject::connect(sink, &QIODevice::bytesWritten, [this] () {
+ writeToOutput();
+ })),
+ readyReadConnection(QObject::connect(source.get(), &QIODevice::readyRead, [this] () {
+ readFromInput();
+ }))
+ {
+ Q_ASSERT(!source->atEnd()); // TODO error out
+ readFromInput();
+ }
+
+ ~IOChunkedTransfer()
+ {
+ QObject::disconnect(bytesWrittenConnection);
+ QObject::disconnect(readyReadConnection);
+ }
+
+ inline bool isBufferEmpty()
+ {
+ Q_ASSERT(beginIndex <= endIndex);
+ return beginIndex == endIndex;
+ }
+
+ void readFromInput()
+ {
+ if (!isBufferEmpty()) // We haven't consumed all the data yet.
+ return;
+ beginIndex = 0;
+ endIndex = source->read(buffer, bufferSize);
+ if (endIndex < 0) {
+ endIndex = beginIndex; // Mark the buffer as empty
+ qCWarning(lc, "Error reading chunk: %s", qPrintable(source->errorString()));
+ return;
+ } else if (endIndex) {
+ memset(buffer + endIndex, 0, sizeof(buffer) - std::size_t(endIndex));
+ writeToOutput();
+ }
+ }
+
+ void writeToOutput()
+ {
+ if (isBufferEmpty())
+ return;
+
+ const auto writtenBytes = sink->write(buffer + beginIndex, endIndex);
+ if (writtenBytes < 0) {
+ qCWarning(lc, "Error writing chunk: %s", qPrintable(sink->errorString()));
+ return;
+ }
+ beginIndex += writtenBytes;
+ if (isBufferEmpty()) {
+ if (source->bytesAvailable())
+ QTimer::singleShot(0, source.get(), [this]() { readFromInput(); });
+ else if (source->atEnd()) // Finishing
+ source.reset();
+ }
+ }
+};
+
+/*!
+ Constructs a QHttpServerResponder using the request \a request
+ and the socket \a socket.
+*/
+QHttpServerResponder::QHttpServerResponder(const QHttpServerRequest &request,
+ QTcpSocket *socket) :
+ d_ptr(new QHttpServerResponderPrivate(request, socket))
+{
+ Q_ASSERT(socket);
+}
+
+/*!
+ Move-constructs a QHttpServerResponder instance, making it point
+ at the same object that \a other was pointing to.
+*/
+QHttpServerResponder::QHttpServerResponder(QHttpServerResponder &&other) :
+ d_ptr(other.d_ptr.take())
+{}
+
+/*!
+ Destroys a QHttpServerResponder.
+*/
+QHttpServerResponder::~QHttpServerResponder()
+{}
+
+/*!
+ Answers a request with an HTTP status code \a status and a
+ MIME type \a mimeType. The I/O device \a data provides the body
+ of the response. If \a data is sequential, the body of the
+ message is sent in chunks: otherwise, the function assumes all
+ the content is available and sends it all at once but the read
+ is done in chunks.
+
+ \note This function takes the ownership of \a data.
+*/
+void QHttpServerResponder::write(QIODevice *data,
+ const QByteArray &mimeType,
+ StatusCode status)
+{
+ Q_D(QHttpServerResponder);
+ Q_ASSERT(d->socket);
+ QScopedPointer<QIODevice, QScopedPointerDeleteLater> input(data);
+ auto socket = d->socket;
+ QObject::connect(input.get(), &QIODevice::aboutToClose, [&input](){ input.reset(); });
+ // TODO protect keep alive sockets
+ QObject::connect(input.get(), &QObject::destroyed, socket, &QObject::deleteLater);
+ QObject::connect(socket, &QObject::destroyed, [&input](){ input.reset(); });
+
+ input->setParent(nullptr);
+ auto openMode = input->openMode();
+ if (!(openMode & QIODevice::ReadOnly)) {
+ if (openMode == QIODevice::NotOpen) {
+ if (!input->open(QIODevice::ReadOnly)) {
+ // TODO Add developer error handling
+ // TODO Send 500
+ qCDebug(lc, "500: Could not open device %s", qPrintable(input->errorString()));
+ return;
+ }
+ } else {
+ // TODO Handle that and send 500, the device is opened but not for reading.
+ // That doesn't make sense
+ qCDebug(lc) << "500: Device is opened in a wrong mode" << openMode
+ << qPrintable(input->errorString());
+ return;
+ }
+ }
+ if (!socket->isOpen()) {
+ qCWarning(lc, "Cannot write to socket. It's disconnected");
+ delete socket;
+ return;
+ }
+
+ d->writeStatusLine(status);
+
+ if (!input->isSequential()) // Non-sequential QIODevice should know its data size
+ d->addHeader(contentLengthString, QByteArray::number(input->size()));
+
+ d->addHeader(contentTypeString, mimeType);
+
+ d->writeHeaders();
+ socket->write("\r\n");
+
+ if (input->atEnd()) {
+ qCDebug(lc, "No more data available.");
+ return;
+ }
+
+ auto transfer = new IOChunkedTransfer<>(input.take(), socket);
+ QObject::connect(transfer->source.get(), &QObject::destroyed, [transfer]() {
+ delete transfer;
+ });
+}
+
+/*!
+ Answers a request with an HTTP status code \a status, a
+ MIME type \a mimeType and a body \a data.
+*/
+void QHttpServerResponder::write(const QByteArray &data,
+ const QByteArray &mimeType,
+ StatusCode status)
+{
+ Q_D(QHttpServerResponder);
+ d->writeStatusLine(status);
+ addHeaders(contentTypeString, mimeType,
+ contentLengthString, QByteArray::number(data.size()));
+ d->writeHeaders();
+ d->writeBody(data);
+}
+
+/*!
+ Answers a request with an HTTP status code \a status, and JSON
+ document \a document.
+*/
+void QHttpServerResponder::write(const QJsonDocument &document, StatusCode status)
+{
+ write(document.toJson(), QByteArrayLiteral("text/json"), status);
+}
+
+/*!
+ Answers a request with an HTTP status code \a status.
+*/
+void QHttpServerResponder::write(StatusCode status)
+{
+ write(QByteArray(), QByteArrayLiteral("application/x-empty"), status);
+}
+
+/*!
+ Returns the socket used.
+*/
+QTcpSocket *QHttpServerResponder::socket() const
+{
+ Q_D(const QHttpServerResponder);
+ return d->socket;
+}
+
+bool QHttpServerResponder::addHeader(const QByteArray &key, const QByteArray &value)
+{
+ Q_D(QHttpServerResponder);
+ return d->addHeader(key, value);
+}
+
+void QHttpServerResponderPrivate::writeStatusLine(StatusCode status,
+ const QPair<quint8, quint8> &version) const
+{
+ Q_ASSERT(socket->isOpen());
+ socket->write("HTTP/");
+ socket->write(QByteArray::number(version.first));
+ socket->write(".");
+ socket->write(QByteArray::number(version.second));
+ socket->write(" ");
+ socket->write(QByteArray::number(quint32(status)));
+ socket->write(" ");
+ socket->write(statusString.at(status));
+ socket->write("\r\n");
+}
+
+void QHttpServerResponderPrivate::writeHeader(const QByteArray &header,
+ const QByteArray &value) const
+{
+ socket->write(header);
+ socket->write(": ");
+ socket->write(value);
+ socket->write("\r\n");
+}
+
+void QHttpServerResponderPrivate::writeHeaders() const
+{
+ for (const auto &pair : qAsConst(headers()))
+ writeHeader(pair.first, pair.second);
+}
+
+void QHttpServerResponderPrivate::writeBody(const QByteArray &body) const
+{
+ Q_ASSERT(socket->isOpen());
+ socket->write("\r\n");
+ socket->write(body);
+}
+
+QT_END_NAMESPACE
diff --git a/src/httpserver/qhttpserverresponder.h b/src/httpserver/qhttpserverresponder.h
new file mode 100644
index 0000000..e3c72f1
--- /dev/null
+++ b/src/httpserver/qhttpserverresponder.h
@@ -0,0 +1,179 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtHttpServer module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QHTTPSERVERRESPONDER_H
+#define QHTTPSERVERRESPONDER_H
+
+#include <QtHttpServer/qthttpserverglobal.h>
+
+#include <QtCore/qdebug.h>
+#include <QtCore/qglobal.h>
+#include <QtCore/qstring.h>
+#include <QtCore/qscopedpointer.h>
+#include <QtCore/qmimetype.h>
+
+QT_BEGIN_NAMESPACE
+
+class QTcpSocket;
+class QHttpServerRequest;
+class QWebSocket;
+
+class QHttpServerResponderPrivate;
+class Q_HTTPSERVER_EXPORT QHttpServerResponder final
+{
+ Q_DECLARE_PRIVATE(QHttpServerResponder)
+
+ friend class QAbstractHttpServer;
+
+public:
+ enum class StatusCode {
+ // 1xx: Informational
+ Continue = 100,
+ SwitchingProtocols,
+ Processing,
+
+ // 2xx: Success
+ Ok = 200,
+ Created,
+ Accepted,
+ NonAuthoritativeInformation,
+ NoContent,
+ ResetContent,
+ PartialContent,
+ MultiStatus,
+ AlreadyReported,
+ IMUsed = 226,
+
+ // 3xx: Redirection
+ MultipleChoices = 300,
+ MovedPermanently,
+ Found,
+ SeeOther,
+ NotModified,
+ UseProxy,
+ // 306: not used, was proposed as "Switch Proxy" but never standardized
+ TemporaryRedirect = 307,
+ PermanentRedirect,
+
+ // 4xx: Client Error
+ BadRequest = 400,
+ Unauthorized,
+ PaymentRequired,
+ Forbidden,
+ NotFound,
+ MethodNotAllowed,
+ NotAcceptable,
+ ProxyAuthenticationRequired,
+ RequestTimeout,
+ Conflict,
+ Gone,
+ LengthRequired,
+ PreconditionFailed,
+ PayloadTooLarge,
+ UriTooLong,
+ UnsupportedMediaType,
+ RequestRangeNotSatisfiable,
+ ExpectationFailed,
+ ImATeapot,
+ MisdirectedRequest = 421,
+ UnprocessableEntity,
+ Locked,
+ FailedDependency,
+ UpgradeRequired = 426,
+ PreconditionRequired = 428,
+ TooManyRequests,
+ RequestHeaderFieldsTooLarge = 431,
+ UnavailableForLegalReasons = 451,
+
+ // 5xx: Server Error
+ InternalServerError = 500,
+ NotImplemented,
+ BadGateway,
+ ServiceUnavailable,
+ GatewayTimeout,
+ HttpVersionNotSupported,
+ VariantAlsoNegotiates,
+ InsufficientStorage,
+ LoopDetected,
+ NotExtended = 510,
+ NetworkAuthenticationRequired,
+ NetworkConnectTimeoutError = 599,
+ };
+
+ QHttpServerResponder(QHttpServerResponder &&other);
+ ~QHttpServerResponder();
+
+ void write(QIODevice *data, const QByteArray &mimeType, StatusCode status = StatusCode::Ok);
+ void write(const QByteArray &data,
+ const QByteArray &mimeType,
+ StatusCode status = StatusCode::Ok);
+ void write(const QJsonDocument &document, StatusCode status = StatusCode::Ok);
+ void write(StatusCode status = StatusCode::Ok);
+
+ QTcpSocket *socket() const;
+
+ bool addHeader(const QByteArray &key, const QByteArray &value);
+
+ template <typename... Args>
+ inline void addHeaders(const QPair<QByteArray, QByteArray> &first, Args &&... others)
+ {
+ addHeader(first.first, first.second);
+ addHeaders(std::forward<Args>(others)...);
+ }
+
+ template <typename... Args>
+ inline void addHeaders(const QByteArray &key, const QByteArray &value, Args &&... others)
+ {
+ addHeader(key, value);
+ addHeaders(std::forward<Args>(others)...);
+ }
+
+private:
+ QHttpServerResponder(const QHttpServerRequest &request, QTcpSocket *socket);
+
+ inline void addHeaders() {}
+
+ QScopedPointer<QHttpServerResponderPrivate> d_ptr;
+};
+
+Q_DECLARE_METATYPE(QHttpServerResponder::StatusCode)
+
+QT_END_NAMESPACE
+
+#endif // QHTTPSERVERRESPONDER_H
diff --git a/src/httpserver/qhttpserverresponder_p.h b/src/httpserver/qhttpserverresponder_p.h
new file mode 100644
index 0000000..c166e46
--- /dev/null
+++ b/src/httpserver/qhttpserverresponder_p.h
@@ -0,0 +1,114 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the QtHttpServer module of the Qt Toolkit.
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef QHTTPSERVERRESPONDER_P_H
+#define QHTTPSERVERRESPONDER_P_H
+
+#include <QtHttpServer/qthttpserverglobal.h>
+#include <QtHttpServer/qhttpserverrequest.h>
+#include <QtHttpServer/qhttpserverresponder.h>
+
+#include <QtCore/qcoreapplication.h>
+#include <QtCore/qpair.h>
+#include <QtCore/qpointer.h>
+#include <QtCore/qsysinfo.h>
+
+#include <type_traits>
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists for the convenience
+// of QHttpServer. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+
+QT_BEGIN_NAMESPACE
+
+class QHttpServerResponderPrivate
+{
+ using StatusCode = QHttpServerResponder::StatusCode;
+
+public:
+ QHttpServerResponderPrivate(const QHttpServerRequest &request, QTcpSocket *const socket) :
+ request(request),
+ socket(socket)
+ {
+ const auto server = QStringLiteral("%1/%2(%3)")
+ .arg(QCoreApplication::instance()->applicationName())
+ .arg(QCoreApplication::instance()->applicationVersion())
+ .arg(QSysInfo::prettyProductName());
+ addHeader(QByteArrayLiteral("Server"), server.toUtf8());
+ }
+
+ inline bool addHeader(const QByteArray &key, const QByteArray &value)
+ {
+ const auto hash = qHash(key.toLower());
+ if (m_headers.contains(hash))
+ return false;
+ m_headers.insert(hash, qMakePair(key, value));
+ return true;
+ }
+
+ void writeStatusLine(StatusCode status = StatusCode::Ok,
+ const QPair<quint8, quint8> &version = qMakePair(1u, 1u)) const;
+ void writeHeaders() const;
+ void writeBody(const QByteArray &body) const;
+
+ const QHttpServerRequest &request;
+#if defined(QT_DEBUG)
+ const QPointer<QTcpSocket> socket;
+#else
+ QTcpSocket *const socket;
+#endif
+
+ QMap<uint, QPair<QByteArray, QByteArray>> m_headers;
+
+private:
+ void writeHeader(const QByteArray &header, const QByteArray &value) const;
+
+public:
+ const decltype(m_headers) &headers() const { return m_headers; }
+};
+
+QT_END_NAMESPACE
+
+#endif // QHTTPSERVERRESPONDER_P_H
diff --git a/tests/auto/auto.pro b/tests/auto/auto.pro
index cfbe887..fdd7afa 100644
--- a/tests/auto/auto.pro
+++ b/tests/auto/auto.pro
@@ -2,5 +2,5 @@ TEMPLATE = subdirs
SUBDIRS = \
cmake \
- qabstracthttpserver
-
+ qabstracthttpserver \
+ qhttpserverresponder
diff --git a/tests/auto/qhttpserverresponder/qhttpserverresponder.pro b/tests/auto/qhttpserverresponder/qhttpserverresponder.pro
new file mode 100644
index 0000000..0459f40
--- /dev/null
+++ b/tests/auto/qhttpserverresponder/qhttpserverresponder.pro
@@ -0,0 +1,5 @@
+CONFIG += testcase
+TARGET = tst_qhttpserverresponder
+SOURCES += tst_qhttpserverresponder.cpp
+
+QT = httpserver testlib
diff --git a/tests/auto/qhttpserverresponder/tst_qhttpserverresponder.cpp b/tests/auto/qhttpserverresponder/tst_qhttpserverresponder.cpp
new file mode 100644
index 0000000..7a22b88
--- /dev/null
+++ b/tests/auto/qhttpserverresponder/tst_qhttpserverresponder.cpp
@@ -0,0 +1,159 @@
+/****************************************************************************
+**
+** Copyright (C) 2018 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** $QT_BEGIN_LICENSE:LGPL$
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU Lesser General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU Lesser
+** General Public License version 3 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL3 included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 3 requirements
+** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 2.0 or (at your option) the GNU General
+** Public license version 3 or any later version approved by the KDE Free
+** Qt Foundation. The licenses are as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-2.0.html and
+** https://www.gnu.org/licenses/gpl-3.0.html.
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#include <QtHttpServer/qhttpserverresponder.h>
+#include <QtHttpServer/qabstracthttpserver.h>
+
+#include <QtCore/qjsondocument.h>
+#include <QtTest/qsignalspy.h>
+#include <QtTest/qtest.h>
+#include <QtNetwork/qnetworkaccessmanager.h>
+
+#include <functional>
+
+QT_BEGIN_NAMESPACE
+
+class tst_QHttpServerResponder : public QObject
+{
+ Q_OBJECT
+
+ std::unique_ptr<QNetworkAccessManager> networkAccessManager;
+
+private slots:
+ void init() { networkAccessManager.reset(new QNetworkAccessManager); }
+ void cleanup() { networkAccessManager.reset(); }
+
+ void defaultStatusCodeNoParameters();
+ void defaultStatusCodeByteArray();
+ void defaultStatusCodeJson();
+ void writeStatusCode_data();
+ void writeStatusCode();
+ void writeJson();
+};
+
+#define qWaitForFinished(REPLY) QVERIFY(QSignalSpy(REPLY, &QNetworkReply::finished).wait())
+
+struct HttpServer : QAbstractHttpServer {
+ std::function<void(QHttpServerResponder responder)> handleRequestFunction;
+ QUrl url { QStringLiteral("http://localhost:%1").arg(listen()) };
+
+ HttpServer(decltype(handleRequestFunction) function) : handleRequestFunction(function) {}
+ bool handleRequest(const QHttpServerRequest &request, QTcpSocket *socket) override;
+};
+
+bool HttpServer::handleRequest(const QHttpServerRequest &request, QTcpSocket *socket)
+{
+ handleRequestFunction(makeResponder(request, socket));
+ return true;
+}
+
+void tst_QHttpServerResponder::defaultStatusCodeNoParameters()
+{
+ HttpServer server([](QHttpServerResponder responder) { responder.write(); });
+ auto reply = networkAccessManager->get(QNetworkRequest(server.url));
+ qWaitForFinished(reply);
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+}
+
+void tst_QHttpServerResponder::defaultStatusCodeByteArray()
+{
+ HttpServer server([](QHttpServerResponder responder) {
+ responder.write(QByteArray(), QByteArrayLiteral("application/x-empty"));
+ });
+ auto reply = networkAccessManager->get(QNetworkRequest(server.url));
+ qWaitForFinished(reply);
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+}
+
+void tst_QHttpServerResponder::defaultStatusCodeJson()
+{
+ const auto json = QJsonDocument::fromJson(QByteArrayLiteral("{}"));
+ HttpServer server([json](QHttpServerResponder responder) { responder.write(json); });
+ auto reply = networkAccessManager->get(QNetworkRequest(server.url));
+ qWaitForFinished(reply);
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+}
+
+void tst_QHttpServerResponder::writeStatusCode_data()
+{
+ using StatusCode = QHttpServerResponder::StatusCode;
+
+ QTest::addColumn<QHttpServerResponder::StatusCode>("statusCode");
+ QTest::addColumn<QNetworkReply::NetworkError>("networkError");
+
+ QTest::addRow("OK") << StatusCode::Ok << QNetworkReply::NoError;
+ QTest::addRow("Content Access Denied") << StatusCode::Forbidden
+ << QNetworkReply::ContentAccessDenied;
+ QTest::addRow("Connection Refused") << StatusCode::NotFound
+ << QNetworkReply::ContentNotFoundError;
+}
+
+void tst_QHttpServerResponder::writeStatusCode()
+{
+ QFETCH(QHttpServerResponder::StatusCode, statusCode);
+ QFETCH(QNetworkReply::NetworkError, networkError);
+ HttpServer server([statusCode](QHttpServerResponder responder) {
+ responder.write(statusCode);
+ });
+ auto reply = networkAccessManager->get(QNetworkRequest(server.url));
+ qWaitForFinished(reply);
+ QCOMPARE(reply->bytesAvailable(), 0);
+ QCOMPARE(reply->error(), networkError);
+ QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader),
+ QByteArrayLiteral("application/x-empty"));
+ QCOMPARE(reply->header(QNetworkRequest::ServerHeader), QStringLiteral("%1/%2(%3)")
+ .arg(QCoreApplication::instance()->applicationName())
+ .arg(QCoreApplication::instance()->applicationVersion())
+ .arg(QSysInfo::prettyProductName()).toUtf8());
+}
+
+void tst_QHttpServerResponder::writeJson()
+{
+ const auto json = QJsonDocument::fromJson(QByteArrayLiteral(R"JSON({ "key" : "value" })JSON"));
+ HttpServer server([json](QHttpServerResponder responder) { responder.write(json); });
+ auto reply = networkAccessManager->get(QNetworkRequest(server.url));
+ qWaitForFinished(reply);
+ QCOMPARE(reply->error(), QNetworkReply::NoError);
+ QCOMPARE(reply->header(QNetworkRequest::ContentTypeHeader), QByteArrayLiteral("text/json"));
+ QCOMPARE(QJsonDocument::fromJson(reply->readAll()), json);
+}
+
+QT_END_NAMESPACE
+
+QTEST_MAIN(tst_QHttpServerResponder)
+
+#include "tst_qhttpserverresponder.moc"