From 8cc576d12e1d928f793c1eb0629f7f2c2fc90d62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 27 Aug 2015 02:05:20 +0200 Subject: [PATCH] add client max connections and connection timeout fix OPTIONS bug add persistent connection for webserver --- server/httpreply.cpp | 13 ++++ server/httpreply.h | 2 + server/rest/devicesresource.cpp | 11 +++ server/rest/devicesresource.h | 1 + server/rest/pluginsresource.cpp | 10 +++ server/rest/pluginsresource.h | 1 + server/rest/restresource.cpp | 18 +++++ server/rest/restresource.h | 2 + server/rest/restserver.cpp | 12 ++-- server/webserver.cpp | 98 +++++++++++++++++++++++++- server/webserver.h | 27 ++++++- tests/auto/webserver/testwebserver.cpp | 8 +-- 12 files changed, 188 insertions(+), 15 deletions(-) diff --git a/server/httpreply.cpp b/server/httpreply.cpp index c13d3c11..e71305ce 100644 --- a/server/httpreply.cpp +++ b/server/httpreply.cpp @@ -137,6 +137,7 @@ HttpReply::HttpReply(QObject *parent) : setHeader(HttpHeaderType::ServerHeader, "guh/" + QByteArray(GUH_VERSION_STRING)); setHeader(HttpHeaderType::DateHeader, QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8() + " GMT"); setRawHeader("Access-Control-Allow-Origin","*"); + setRawHeader("Keep-Alive", "timeout=6, max=50"); setHeader(HttpHeaderType::CacheControlHeader, "no-cache"); setHeader(HttpHeaderType::ConnectionHeader, "Keep-Alive"); packReply(); @@ -159,6 +160,7 @@ HttpReply::HttpReply(const HttpReply::HttpStatusCode &statusCode, const HttpRepl setHeader(HttpHeaderType::ServerHeader, "guh/" + QByteArray(GUH_VERSION_STRING)); setHeader(HttpHeaderType::DateHeader, QDateTime::currentDateTime().toString("ddd, dd MMM yyyy hh:mm:ss").toUtf8() + " GMT"); setRawHeader("Access-Control-Allow-Origin","*"); + setRawHeader("Keep-Alive", "timeout=10, max=50"); setHeader(HttpHeaderType::CacheControlHeader, "no-cache"); setHeader(HttpHeaderType::ConnectionHeader, "Keep-Alive"); packReply(); @@ -195,6 +197,7 @@ HttpReply::Type HttpReply::type() const void HttpReply::setClientId(const QUuid &clientId) { m_clientId = clientId; + packReply(); } /*! Returns the clientId of this \l{HttpReply}.*/ @@ -391,4 +394,14 @@ void HttpReply::timeout() emit finished(); } +QDebug operator<<(QDebug debug, const HttpReply &httpReply) +{ + debug << "-----------------------------------" << "\n"; + debug << httpReply.rawHeader() << "\n"; + debug << "-----------------------------------" << "\n"; + debug << httpReply.payload() << "\n"; + debug << "-----------------------------------" << "\n"; + return debug; +} + } diff --git a/server/httpreply.h b/server/httpreply.h index 2d93dcf0..b061bc90 100644 --- a/server/httpreply.h +++ b/server/httpreply.h @@ -137,6 +137,8 @@ signals: }; +QDebug operator<< (QDebug debug, const HttpReply &httpReply); + } #endif // HTTPREPLY_H diff --git a/server/rest/devicesresource.cpp b/server/rest/devicesresource.cpp index d34e9d39..8fb7c4d9 100644 --- a/server/rest/devicesresource.cpp +++ b/server/rest/devicesresource.cpp @@ -75,6 +75,9 @@ HttpReply *DevicesResource::proccessRequest(const HttpRequest &request, const QS case HttpRequest::Delete: reply = proccessDeleteRequest(request, urlTokens); break; + case HttpRequest::Options: + reply = proccessOptionsRequest(request, urlTokens); + break; default: reply = createErrorReply(HttpReply::BadRequest); break; @@ -192,6 +195,14 @@ HttpReply *DevicesResource::proccessPostRequest(const HttpRequest &request, cons return createErrorReply(HttpReply::NotImplemented); } +HttpReply *DevicesResource::proccessOptionsRequest(const HttpRequest &request, const QStringList &urlTokens) +{ + Q_UNUSED(request) + Q_UNUSED(urlTokens) + qCDebug(dcRest) << "process options request\n" << request; + return RestResource::createCorsSuccessReply(); +} + HttpReply *DevicesResource::getConfiguredDevices() const { qCDebug(dcRest) << "Get all configured devices"; diff --git a/server/rest/devicesresource.h b/server/rest/devicesresource.h index e88b587d..44d2c860 100644 --- a/server/rest/devicesresource.h +++ b/server/rest/devicesresource.h @@ -55,6 +55,7 @@ private: HttpReply *proccessDeleteRequest(const HttpRequest &request, const QStringList &urlTokens) override; HttpReply *proccessPutRequest(const HttpRequest &request, const QStringList &urlTokens) override; HttpReply *proccessPostRequest(const HttpRequest &request, const QStringList &urlTokens) override; + HttpReply *proccessOptionsRequest(const HttpRequest &request, const QStringList &urlTokens) override; // Get methods HttpReply *getConfiguredDevices() const; diff --git a/server/rest/pluginsresource.cpp b/server/rest/pluginsresource.cpp index bddcf4eb..2e870a5b 100644 --- a/server/rest/pluginsresource.cpp +++ b/server/rest/pluginsresource.cpp @@ -60,6 +60,9 @@ HttpReply *PluginsResource::proccessRequest(const HttpRequest &request, const QS case HttpRequest::Put: reply = proccessPutRequest(request, urlTokens); break; + case HttpRequest::Options: + reply = proccessOptionsRequest(request, urlTokens); + break; default: reply = createErrorReply(HttpReply::BadRequest); break; @@ -97,6 +100,13 @@ HttpReply *PluginsResource::proccessPutRequest(const HttpRequest &request, const return createErrorReply(HttpReply::NotImplemented); } +HttpReply *PluginsResource::proccessOptionsRequest(const HttpRequest &request, const QStringList &urlTokens) +{ + Q_UNUSED(request) + Q_UNUSED(urlTokens) + return RestResource::createCorsSuccessReply(); +} + HttpReply *PluginsResource::getPlugins() const { qCDebug(dcRest) << "Get plugins"; diff --git a/server/rest/pluginsresource.h b/server/rest/pluginsresource.h index f4963a81..d73046ae 100644 --- a/server/rest/pluginsresource.h +++ b/server/rest/pluginsresource.h @@ -48,6 +48,7 @@ private: // Process method HttpReply *proccessGetRequest(const HttpRequest &request, const QStringList &urlTokens) override; HttpReply *proccessPutRequest(const HttpRequest &request, const QStringList &urlTokens) override; + HttpReply *proccessOptionsRequest(const HttpRequest &request, const QStringList &urlTokens) override; // Get methods HttpReply *getPlugins() const; diff --git a/server/rest/restresource.cpp b/server/rest/restresource.cpp index b9fe1b6b..b42b17f7 100644 --- a/server/rest/restresource.cpp +++ b/server/rest/restresource.cpp @@ -86,6 +86,17 @@ HttpReply *RestResource::createSuccessReply() return reply; } +HttpReply *RestResource::createCorsSuccessReply() +{ + HttpReply *reply = RestResource::createSuccessReply(); + reply->setHeader(HttpReply::ContentTypeHeader, "text/plain"); + reply->setRawHeader("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS"); + reply->setRawHeader("Access-Control-Allow-Headers", "Origin, Content-Type, Accept"); + reply->setRawHeader("Access-Control-Max-Age", "1728000"); + reply->setCloseConnection(true); + return reply; +} + /*! Returns the pointer to a new created error \l{HttpReply} initialized with the given \a statusCode and \l{HttpReply::TypeSync}. */ HttpReply *RestResource::createErrorReply(const HttpReply::HttpStatusCode &statusCode) { @@ -130,6 +141,13 @@ HttpReply *RestResource::proccessPostRequest(const HttpRequest &request, const Q return createErrorReply(HttpReply::NotImplemented); } +HttpReply *RestResource::proccessOptionsRequest(const HttpRequest &request, const QStringList &urlTokens) +{ + Q_UNUSED(request) + Q_UNUSED(urlTokens) + return createErrorReply(HttpReply::NotImplemented); +} + HttpReply *RestResource::proccessPutRequest(const HttpRequest &request, const QStringList &urlTokens) { Q_UNUSED(request) diff --git a/server/rest/restresource.h b/server/rest/restresource.h index c745a365..954b3ac9 100644 --- a/server/rest/restresource.h +++ b/server/rest/restresource.h @@ -43,6 +43,7 @@ public: virtual HttpReply *proccessRequest(const HttpRequest &request, const QStringList &urlTokens) = 0; static HttpReply *createSuccessReply(); + static HttpReply *createCorsSuccessReply(); static HttpReply *createErrorReply(const HttpReply::HttpStatusCode &statusCode); static HttpReply *createAsyncReply(); static QPair verifyPayload(const QByteArray &payload); @@ -52,6 +53,7 @@ private: virtual HttpReply *proccessDeleteRequest(const HttpRequest &request, const QStringList &urlTokens); virtual HttpReply *proccessPutRequest(const HttpRequest &request, const QStringList &urlTokens); virtual HttpReply *proccessPostRequest(const HttpRequest &request, const QStringList &urlTokens); + virtual HttpReply *proccessOptionsRequest(const HttpRequest &request, const QStringList &urlTokens); QHash, QString> m_descriptions; QHash m_params; diff --git a/server/rest/restserver.cpp b/server/rest/restserver.cpp index a25109b3..87b73a67 100644 --- a/server/rest/restserver.cpp +++ b/server/rest/restserver.cpp @@ -111,16 +111,12 @@ void RestServer::processHttpRequest(const QUuid &clientId, const HttpRequest &re return; } - // check CORS call - if (request.method() == HttpRequest::Options) { - HttpReply *reply = RestResource::createSuccessReply(); - reply->setHeader(HttpReply::ContentTypeHeader, "text/plain;"); - reply->setRawHeader("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS"); - reply->setRawHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); + // check CORS call for main resource + if (request.method() == HttpRequest::Options && urlTokens.count() == 3) { + qCDebug(dcRest) << "process options request\n" << request; + HttpReply *reply = RestResource::createCorsSuccessReply(); reply->setClientId(clientId); - reply->setCloseConnection(true); m_webserver->sendHttpReply(reply); - reply->deleteLater(); return; } diff --git a/server/webserver.cpp b/server/webserver.cpp index 159382de..51750011 100644 --- a/server/webserver.cpp +++ b/server/webserver.cpp @@ -137,8 +137,11 @@ void WebServer::sendHttpReply(HttpReply *reply) return; } + //qCDebug(dcWebServer()) << "sending header to" << socket->peerAddress().toString() << socket->peerPort() << "\n" << reply->rawHeader(); writeData(socket, reply->data()); - socket->close(); + if (reply->closeConnection()) + socket->close(); + } bool WebServer::verifyFile(QSslSocket *socket, const QString &fileName) @@ -191,7 +194,7 @@ QString WebServer::fileName(const QString &query) void WebServer::writeData(QSslSocket *socket, const QByteArray &data) { - socket->write(data); + socket->write(data + "\r\n"); } void WebServer::incomingConnection(qintptr socketDescriptor) @@ -207,6 +210,28 @@ void WebServer::incomingConnection(qintptr socketDescriptor) return; } + // check webserver client + bool existing = false; + foreach (WebServerClient *client, m_webServerClients) { + if (client->address() == socket->peerAddress()) { + if (client->connections().count() >= 50) { + qCWarning(dcConnection) << QString("Maximum connections for this client reached: rejecting connection from client %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()); + socket->close(); + delete socket; + return; + } + client->addConnection(socket); + existing = true; + break; + } + } + + if (!existing) { + WebServerClient *webServerClient = new WebServerClient(socket->peerAddress()); + webServerClient->addConnection(socket); + m_webServerClients.append(webServerClient); + } + // append the new client to the client list QUuid clientId = QUuid::createUuid(); m_clientList.insert(clientId, socket); @@ -283,6 +308,14 @@ void WebServer::readClient() qCDebug(dcWebServer) << QString("Got valid request from %1:%2").arg(socket->peerAddress().toString()).arg(socket->peerPort()); qCDebug(dcWebServer) << request.methodString() << request.url().path(); + // reset timout + foreach (WebServerClient *webserverClient, m_webServerClients) { + if (webserverClient->address() == socket->peerAddress()) { + webserverClient->resetTimout(socket); + break; + } + } + // verify method if (request.method() == HttpRequest::Unhandled) { HttpReply reply(HttpReply::MethodNotAllowed); @@ -381,4 +414,65 @@ bool WebServer::stopServer() return true; } + +WebServerClient::WebServerClient(const QHostAddress &address, QObject *parent): + QObject(parent), + m_address(address) +{ +} + +QHostAddress WebServerClient::address() const +{ + return m_address; +} + +QList WebServerClient::connections() +{ + return m_connections; +} + +void WebServerClient::addConnection(QSslSocket *socket) +{ + QTimer *timer = new QTimer(this); + timer->setSingleShot(true); + timer->setInterval(6000); + connect(timer, &QTimer::timeout, this, &WebServerClient::onTimout); + + m_runningConnections.insert(timer, socket); + m_connections.append(socket); + + connect(socket, SIGNAL(disconnected()), this, SLOT(onDisconnected())); + + timer->start(); +} + +void WebServerClient::resetTimout(QSslSocket *socket) +{ + QTimer *timer = 0; + timer = m_runningConnections.key(socket); + if (timer) + timer->start(); +} + +void WebServerClient::onTimout() +{ + QTimer *timer = static_cast(sender()); + QSslSocket *socket = m_runningConnections.take(timer); + qCDebug(dcWebServer) << QString("Client connection timout %1:%2 -> closing connection").arg(socket->peerAddress().toString()).arg(socket->peerPort()); + socket->close(); + + timer->deleteLater(); +} + +void WebServerClient::onDisconnected() +{ + QSslSocket *socket = static_cast(sender()); + if (!m_runningConnections.values().contains(socket)) + return; + + QTimer *timer = m_runningConnections.key(socket); + m_runningConnections.remove(timer); + timer->deleteLater(); +} + } diff --git a/server/webserver.h b/server/webserver.h index 0eda335c..d17a29dd 100644 --- a/server/webserver.h +++ b/server/webserver.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,30 @@ namespace guhserver { class HttpRequest; class HttpReply; +class WebServerClient : public QObject +{ + Q_OBJECT +public: + WebServerClient(const QHostAddress &address, QObject *parent = 0); + + QHostAddress address() const; + + QList connections(); + void addConnection(QSslSocket *socket); + + void resetTimout(QSslSocket *socket); + +private: + QHostAddress m_address; + QList m_connections; + QHash m_runningConnections; + +private slots: + void onTimout(); + void onDisconnected(); +}; + + class WebServer : public QTcpServer { Q_OBJECT @@ -50,6 +75,7 @@ public: private: QHash m_clientList; + QList m_webServerClients; QHash m_incompleteRequests; QSslConfiguration m_sslConfiguration; @@ -59,7 +85,6 @@ private: qint16 m_port; QDir m_webinterfaceDir; - bool verifyFile(QSslSocket *socket, const QString &fileName); QString fileName(const QString &query); diff --git a/tests/auto/webserver/testwebserver.cpp b/tests/auto/webserver/testwebserver.cpp index 20dc1909..76f81561 100644 --- a/tests/auto/webserver/testwebserver.cpp +++ b/tests/auto/webserver/testwebserver.cpp @@ -191,8 +191,8 @@ void TestWebserver::checkAllowedMethodCall() } else if(method == "CONNECT") { reply = nam->sendCustomRequest(request, "CONNECT"); } else if(method == "OPTIONS") { - request.setUrl(QUrl("http://localhost:3333/api/v1/devices")); - reply = nam->sendCustomRequest(request, "OPTIONS"); + QNetworkRequest req(QUrl("http://localhost:3333/api/v1/devices")); + reply = nam->sendCustomRequest(req, "OPTIONS"); } else if(method == "TRACE") { reply = nam->sendCustomRequest(request, "TRACE"); } else { @@ -211,8 +211,8 @@ void TestWebserver::checkAllowedMethodCall() QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), expectedStatusCode); QVERIFY2(reply->hasRawHeader("Allow"), "405 should contain the allowed methods header"); } - reply->deleteLater(); + nam->deleteLater(); } void TestWebserver::badRequests_data() @@ -318,7 +318,7 @@ void TestWebserver::printResponse(QNetworkReply *reply) { qDebug() << "-------------------------------"; qDebug() << "Response header:"; - qDebug() << reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); + qDebug() << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); foreach (const QNetworkReply::RawHeaderPair &headerPair, reply->rawHeaderPairs()) { qDebug() << headerPair.first << ":" << headerPair.second; }