add client max connections and connection timeout

fix OPTIONS bug
add persistent connection for webserver
This commit is contained in:
Simon Stürz 2015-08-27 02:05:20 +02:00 committed by Michael Zanetti
parent ba5aedaaab
commit 8cc576d12e
12 changed files with 188 additions and 15 deletions

View File

@ -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;
}
}

View File

@ -137,6 +137,8 @@ signals:
};
QDebug operator<< (QDebug debug, const HttpReply &httpReply);
}
#endif // HTTPREPLY_H

View File

@ -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";

View File

@ -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;

View File

@ -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";

View File

@ -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;

View File

@ -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)

View File

@ -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<bool, QVariant> 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<QPair<HttpRequest::RequestMethod, QString>, QString> m_descriptions;
QHash<QString, QVariantMap> m_params;

View File

@ -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;
}

View File

@ -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<QSslSocket *> 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<QTimer *>(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<QSslSocket *>(sender());
if (!m_runningConnections.values().contains(socket))
return;
QTimer *timer = m_runningConnections.key(socket);
m_runningConnections.remove(timer);
timer->deleteLater();
}
}

View File

@ -26,6 +26,7 @@
#include <QTcpSocket>
#include <QHash>
#include <QDir>
#include <QTimer>
#include <QSslSocket>
#include <QSslCertificate>
#include <QSslConfiguration>
@ -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<QSslSocket *> connections();
void addConnection(QSslSocket *socket);
void resetTimout(QSslSocket *socket);
private:
QHostAddress m_address;
QList<QSslSocket *> m_connections;
QHash<QTimer *, QSslSocket *> m_runningConnections;
private slots:
void onTimout();
void onDisconnected();
};
class WebServer : public QTcpServer
{
Q_OBJECT
@ -50,6 +75,7 @@ public:
private:
QHash<QUuid, QSslSocket *> m_clientList;
QList<WebServerClient *> m_webServerClients;
QHash<QSslSocket *, HttpRequest> 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);

View File

@ -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;
}