diff --git a/httpserver/httpconnectionhandler.cpp b/httpserver/httpconnectionhandler.cpp new file mode 100644 index 000000000..209656248 --- /dev/null +++ b/httpserver/httpconnectionhandler.cpp @@ -0,0 +1,204 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpconnectionhandler.h" +#include "httpresponse.h" + +HttpConnectionHandler::HttpConnectionHandler(QSettings* settings, HttpRequestHandler* requestHandler, QSslConfiguration* sslConfiguration) + : QThread() +{ + Q_ASSERT(settings!=0); + Q_ASSERT(requestHandler!=0); + this->settings=settings; + this->requestHandler=requestHandler; + this->sslConfiguration=sslConfiguration; + currentRequest=0; + busy=false; + + // Create TCP or SSL socket + createSocket(); + + // execute signals in my own thread + moveToThread(this); + socket->moveToThread(this); + readTimer.moveToThread(this); + + // Connect signals + connect(socket, SIGNAL(readyRead()), SLOT(read())); + connect(socket, SIGNAL(disconnected()), SLOT(disconnected())); + connect(&readTimer, SIGNAL(timeout()), SLOT(readTimeout())); + readTimer.setSingleShot(true); + + qDebug("HttpConnectionHandler (%p): constructed", this); + this->start(); +} + + +HttpConnectionHandler::~HttpConnectionHandler() { + quit(); + wait(); + delete socket; + qDebug("HttpConnectionHandler (%p): destroyed", this); +} + + +void HttpConnectionHandler::createSocket() { + // If SSL is supported and configured, then create an instance of QSslSocket + #ifndef QT_NO_OPENSSL + if (sslConfiguration) { + QSslSocket* sslSocket=new QSslSocket(); + sslSocket->setSslConfiguration(*sslConfiguration); + socket=sslSocket; + qDebug("HttpConnectionHandler (%p): SSL is enabled", this); + return; + } + #endif + // else create an instance of QTcpSocket + socket=new QTcpSocket(); +} + + +void HttpConnectionHandler::run() { + qDebug("HttpConnectionHandler (%p): thread started", this); + try { + exec(); + } + catch (...) { + qCritical("HttpConnectionHandler (%p): an uncatched exception occured in the thread",this); + } + socket->close(); + qDebug("HttpConnectionHandler (%p): thread stopped", this); +} + + +void HttpConnectionHandler::handleConnection(tSocketDescriptor socketDescriptor) { + qDebug("HttpConnectionHandler (%p): handle new connection", this); + busy = true; + Q_ASSERT(socket->isOpen()==false); // if not, then the handler is already busy + + //UGLY workaround - we need to clear writebuffer before reusing this socket + //https://bugreports.qt-project.org/browse/QTBUG-28914 + socket->connectToHost("",0); + socket->abort(); + + if (!socket->setSocketDescriptor(socketDescriptor)) { + qCritical("HttpConnectionHandler (%p): cannot initialize socket: %s", this,qPrintable(socket->errorString())); + return; + } + + #ifndef QT_NO_OPENSSL + // Switch on encryption, if SSL is configured + if (sslConfiguration) { + qDebug("HttpConnectionHandler (%p): Starting encryption", this); + ((QSslSocket*)socket)->startServerEncryption(); + } + #endif + + // Start timer for read timeout + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + // delete previous request + delete currentRequest; + currentRequest=0; +} + + +bool HttpConnectionHandler::isBusy() { + return busy; +} + +void HttpConnectionHandler::setBusy() { + this->busy = true; +} + + +void HttpConnectionHandler::readTimeout() { + qDebug("HttpConnectionHandler (%p): read timeout occured",this); + + //Commented out because QWebView cannot handle this. + //socket->write("HTTP/1.1 408 request timeout\r\nConnection: close\r\n\r\n408 request timeout\r\n"); + + socket->flush(); + socket->disconnectFromHost(); + delete currentRequest; + currentRequest=0; +} + + +void HttpConnectionHandler::disconnected() { + qDebug("HttpConnectionHandler (%p): disconnected", this); + socket->close(); + readTimer.stop(); + busy = false; +} + +void HttpConnectionHandler::read() { + // The loop adds support for HTTP pipelinig + while (socket->bytesAvailable()) { + #ifdef SUPERVERBOSE + qDebug("HttpConnectionHandler (%p): read input",this); + #endif + + // Create new HttpRequest object if necessary + if (!currentRequest) { + currentRequest=new HttpRequest(settings); + } + + // Collect data for the request object + while (socket->bytesAvailable() && currentRequest->getStatus()!=HttpRequest::complete && currentRequest->getStatus()!=HttpRequest::abort) { + currentRequest->readFromSocket(socket); + if (currentRequest->getStatus()==HttpRequest::waitForBody) { + // Restart timer for read timeout, otherwise it would + // expire during large file uploads. + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + } + } + + // If the request is aborted, return error message and close the connection + if (currentRequest->getStatus()==HttpRequest::abort) { + socket->write("HTTP/1.1 413 entity too large\r\nConnection: close\r\n\r\n413 Entity too large\r\n"); + socket->flush(); + socket->disconnectFromHost(); + delete currentRequest; + currentRequest=0; + return; + } + + // If the request is complete, let the request mapper dispatch it + if (currentRequest->getStatus()==HttpRequest::complete) { + readTimer.stop(); + qDebug("HttpConnectionHandler (%p): received request",this); + HttpResponse response(socket); + try { + requestHandler->service(*currentRequest, response); + } + catch (...) { + qCritical("HttpConnectionHandler (%p): An uncatched exception occured in the request handler",this); + } + + // Finalize sending the response if not already done + if (!response.hasSentLastPart()) { + response.write(QByteArray(),true); + } + + qDebug("HttpConnectionHandler (%p): finished request",this); + + // Close the connection after delivering the response, if requested + if (QString::compare(currentRequest->getHeader("Connection"),"close",Qt::CaseInsensitive)==0) { + socket->flush(); + socket->disconnectFromHost(); + } + else { + // Start timer for next request + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + } + // Prepare for next request + delete currentRequest; + currentRequest=0; + } + } +} diff --git a/httpserver/httpconnectionhandler.h b/httpserver/httpconnectionhandler.h new file mode 100644 index 000000000..79c0a7864 --- /dev/null +++ b/httpserver/httpconnectionhandler.h @@ -0,0 +1,120 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPCONNECTIONHANDLER_H +#define HTTPCONNECTIONHANDLER_H + +#ifndef QT_NO_OPENSSL + #include +#endif +#include +#include +#include +#include +#include "httpglobal.h" +#include "httprequest.h" +#include "httprequesthandler.h" + +/** Alias type definition, for compatibility to different Qt versions */ +#if QT_VERSION >= 0x050000 + typedef qintptr tSocketDescriptor; +#else + typedef int tSocketDescriptor; +#endif + +/** Alias for QSslConfiguration if OpenSSL is not supported */ +#ifdef QT_NO_OPENSSL + #define QSslConfiguration QObject +#endif + +/** + The connection handler accepts incoming connections and dispatches incoming requests to to a + request mapper. Since HTTP clients can send multiple requests before waiting for the response, + the incoming requests are queued and processed one after the other. +

+ Example for the required configuration settings: +

+  readTimeout=60000
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+

+ The readTimeout value defines the maximum time to wait for a complete HTTP request. + @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize. +*/ +class DECLSPEC HttpConnectionHandler : public QThread { + Q_OBJECT + Q_DISABLE_COPY(HttpConnectionHandler) + +public: + + /** + Constructor. + @param settings Configuration settings of the HTTP webserver + @param requestHandler Handler that will process each incoming HTTP request + @param sslConfiguration SSL (HTTPS) will be used if not NULL + */ + HttpConnectionHandler(QSettings* settings, HttpRequestHandler* requestHandler, QSslConfiguration* sslConfiguration=NULL); + + /** Destructor */ + virtual ~HttpConnectionHandler(); + + /** Returns true, if this handler is in use. */ + bool isBusy(); + + /** Mark this handler as busy */ + void setBusy(); + +private: + + /** Configuration settings */ + QSettings* settings; + + /** TCP socket of the current connection */ + QTcpSocket* socket; + + /** Time for read timeout detection */ + QTimer readTimer; + + /** Storage for the current incoming HTTP request */ + HttpRequest* currentRequest; + + /** Dispatches received requests to services */ + HttpRequestHandler* requestHandler; + + /** This shows the busy-state from a very early time */ + bool busy; + + /** Configuration for SSL */ + QSslConfiguration* sslConfiguration; + + /** Executes the threads own event loop */ + void run(); + + /** Create SSL or TCP socket */ + void createSocket(); + +public slots: + + /** + Received from from the listener, when the handler shall start processing a new connection. + @param socketDescriptor references the accepted connection. + */ + void handleConnection(tSocketDescriptor socketDescriptor); + +private slots: + + /** Received from the socket when a read-timeout occured */ + void readTimeout(); + + /** Received from the socket when incoming data can be read */ + void read(); + + /** Received from the socket when a connection has been closed */ + void disconnected(); + +}; + +#endif // HTTPCONNECTIONHANDLER_H diff --git a/httpserver/httpconnectionhandlerpool.cpp b/httpserver/httpconnectionhandlerpool.cpp new file mode 100644 index 000000000..a26952812 --- /dev/null +++ b/httpserver/httpconnectionhandlerpool.cpp @@ -0,0 +1,131 @@ +#ifndef QT_NO_OPENSSL + #include + #include + #include + #include +#endif +#include +#include "httpconnectionhandlerpool.h" + +HttpConnectionHandlerPool::HttpConnectionHandlerPool(QSettings* settings, HttpRequestHandler* requestHandler) + : QObject() +{ + Q_ASSERT(settings!=0); + this->settings=settings; + this->requestHandler=requestHandler; + this->sslConfiguration=NULL; + loadSslConfig(); + cleanupTimer.start(settings->value("cleanupInterval",1000).toInt()); + connect(&cleanupTimer, SIGNAL(timeout()), SLOT(cleanup())); +} + + +HttpConnectionHandlerPool::~HttpConnectionHandlerPool() { + // delete all connection handlers and wait until their threads are closed + foreach(HttpConnectionHandler* handler, pool) { + delete handler; + } + delete sslConfiguration; + qDebug("HttpConnectionHandlerPool (%p): destroyed", this); +} + + +HttpConnectionHandler* HttpConnectionHandlerPool::getConnectionHandler() { + HttpConnectionHandler* freeHandler=0; + mutex.lock(); + // find a free handler in pool + foreach(HttpConnectionHandler* handler, pool) { + if (!handler->isBusy()) { + freeHandler=handler; + freeHandler->setBusy(); + break; + } + } + // create a new handler, if necessary + if (!freeHandler) { + int maxConnectionHandlers=settings->value("maxThreads",100).toInt(); + if (pool.count()setBusy(); + pool.append(freeHandler); + } + } + mutex.unlock(); + return freeHandler; +} + + +void HttpConnectionHandlerPool::cleanup() { + int maxIdleHandlers=settings->value("minThreads",1).toInt(); + int idleCounter=0; + mutex.lock(); + foreach(HttpConnectionHandler* handler, pool) { + if (!handler->isBusy()) { + if (++idleCounter > maxIdleHandlers) { + pool.removeOne(handler); + delete handler; + qDebug("HttpConnectionHandlerPool: Removed connection handler (%p), pool size is now %i",handler,pool.size()); + break; // remove only one handler in each interval + } + } + } + mutex.unlock(); +} + + +void HttpConnectionHandlerPool::loadSslConfig() { + // If certificate and key files are configured, then load them + QString sslKeyFileName=settings->value("sslKeyFile","").toString(); + QString sslCertFileName=settings->value("sslCertFile","").toString(); + if (!sslKeyFileName.isEmpty() && !sslCertFileName.isEmpty()) { + #ifdef QT_NO_OPENSSL + qWarning("HttpConnectionHandlerPool: SSL is not supported"); + #else + // Convert relative fileNames to absolute, based on the directory of the config file. + QFileInfo configFile(settings->fileName()); + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(sslKeyFileName) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(sslKeyFileName)) + #endif + { + sslKeyFileName=QFileInfo(configFile.absolutePath(),sslKeyFileName).absoluteFilePath(); + } + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(sslCertFileName) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(sslCertFileName)) + #endif + { + sslCertFileName=QFileInfo(configFile.absolutePath(),sslCertFileName).absoluteFilePath(); + } + + // Load the SSL certificate + QFile certFile(sslCertFileName); + if (!certFile.open(QIODevice::ReadOnly)) { + qCritical("HttpConnectionHandlerPool: cannot open sslCertFile %s", qPrintable(sslCertFileName)); + return; + } + QSslCertificate certificate(&certFile, QSsl::Pem); + certFile.close(); + + // Load the key file + QFile keyFile(sslKeyFileName); + if (!keyFile.open(QIODevice::ReadOnly)) { + qCritical("HttpConnectionHandlerPool: cannot open sslKeyFile %s", qPrintable(sslKeyFileName)); + return; + } + QSslKey sslKey(&keyFile, QSsl::Rsa, QSsl::Pem); + keyFile.close(); + + // Create the SSL configuration + sslConfiguration=new QSslConfiguration(); + sslConfiguration->setLocalCertificate(certificate); + sslConfiguration->setPrivateKey(sslKey); + sslConfiguration->setPeerVerifyMode(QSslSocket::VerifyNone); + sslConfiguration->setProtocol(QSsl::TlsV1SslV3); + + qDebug("HttpConnectionHandlerPool: SSL settings loaded"); + #endif + } +} diff --git a/httpserver/httpconnectionhandlerpool.h b/httpserver/httpconnectionhandlerpool.h new file mode 100644 index 000000000..223ff3a1e --- /dev/null +++ b/httpserver/httpconnectionhandlerpool.h @@ -0,0 +1,95 @@ +#ifndef HTTPCONNECTIONHANDLERPOOL_H +#define HTTPCONNECTIONHANDLERPOOL_H + +#include +#include +#include +#include +#include "httpglobal.h" +#include "httpconnectionhandler.h" + +/** + Pool of http connection handlers. The size of the pool grows and + shrinks on demand. +

+ Example for the required configuration settings: +

+  minThreads=1
+  maxThreads=100
+  cleanupInterval=1000
+  readTimeout=60000
+  ;sslKeyFile=ssl/my.key
+  ;sslCertFile=ssl/my.cert
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+ After server start, the size of the thread pool is always 0. Threads + are started on demand when requests come in. The cleanup timer reduces + the number of idle threads slowly by closing one thread in each interval. + But the configured minimum number of threads are kept running. +

+ For SSL support, you need an OpenSSL certificate file and a key file. + Both can be created with the command +

+      openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout my.key -out my.cert
+  
+

+ Visit http://slproweb.com/products/Win32OpenSSL.html to download the Light version of OpenSSL for Windows. +

+ Please note that a listener with SSL settings can only handle HTTPS protocol. To + support both HTTP and HTTPS simultaneously, you need to start two listeners on different ports - + one with SLL and one without SSL. + @see HttpConnectionHandler for description of the readTimeout + @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize +*/ + +class DECLSPEC HttpConnectionHandlerPool : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpConnectionHandlerPool) +public: + + /** + Constructor. + @param settings Configuration settings for the HTTP server. Must not be 0. + @param requestHandler The handler that will process each received HTTP request. + @warning The requestMapper gets deleted by the destructor of this pool + */ + HttpConnectionHandlerPool(QSettings* settings, HttpRequestHandler* requestHandler); + + /** Destructor */ + virtual ~HttpConnectionHandlerPool(); + + /** Get a free connection handler, or 0 if not available. */ + HttpConnectionHandler* getConnectionHandler(); + +private: + + /** Settings for this pool */ + QSettings* settings; + + /** Will be assigned to each Connectionhandler during their creation */ + HttpRequestHandler* requestHandler; + + /** Pool of connection handlers */ + QList pool; + + /** Timer to clean-up unused connection handler */ + QTimer cleanupTimer; + + /** Used to synchronize threads */ + QMutex mutex; + + /** The SSL configuration (certificate, key and other settings) */ + QSslConfiguration* sslConfiguration; + + /** Load SSL configuration */ + void loadSslConfig(); + +private slots: + + /** Received from the clean-up timer. */ + void cleanup(); + +}; + +#endif // HTTPCONNECTIONHANDLERPOOL_H diff --git a/httpserver/httpcookie.cpp b/httpserver/httpcookie.cpp new file mode 100644 index 000000000..3f5be9290 --- /dev/null +++ b/httpserver/httpcookie.cpp @@ -0,0 +1,199 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpcookie.h" + +HttpCookie::HttpCookie() { + version=1; + maxAge=0; + secure=false; +} + +HttpCookie::HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path, const QByteArray comment, const QByteArray domain, const bool secure) { + this->name=name; + this->value=value; + this->maxAge=maxAge; + this->path=path; + this->comment=comment; + this->domain=domain; + this->secure=secure; + this->version=1; +} + +HttpCookie::HttpCookie(const QByteArray source) { + version=1; + maxAge=0; + secure=false; + QList list=splitCSV(source); + foreach(QByteArray part, list) { + + // Split the part into name and value + QByteArray name; + QByteArray value; + int posi=part.indexOf('='); + if (posi) { + name=part.left(posi).trimmed(); + value=part.mid(posi+1).trimmed(); + } + else { + name=part.trimmed(); + value=""; + } + + // Set fields + if (name=="Comment") { + comment=value; + } + else if (name=="Domain") { + domain=value; + } + else if (name=="Max-Age") { + maxAge=value.toInt(); + } + else if (name=="Path") { + path=value; + } + else if (name=="Secure") { + secure=true; + } + else if (name=="Version") { + version=value.toInt(); + } + else { + if (this->name.isEmpty()) { + this->name=name; + this->value=value; + } + else { + qWarning("HttpCookie: Ignoring unknown %s=%s",name.data(),value.data()); + } + } + } +} + +QByteArray HttpCookie::toByteArray() const { + QByteArray buffer(name); + buffer.append('='); + buffer.append(value); + if (!comment.isEmpty()) { + buffer.append("; Comment="); + buffer.append(comment); + } + if (!domain.isEmpty()) { + buffer.append("; Domain="); + buffer.append(domain); + } + if (maxAge!=0) { + buffer.append("; Max-Age="); + buffer.append(QByteArray::number(maxAge)); + } + if (!path.isEmpty()) { + buffer.append("; Path="); + buffer.append(path); + } + if (secure) { + buffer.append("; Secure"); + } + buffer.append("; Version="); + buffer.append(QByteArray::number(version)); + return buffer; +} + +void HttpCookie::setName(const QByteArray name){ + this->name=name; +} + +void HttpCookie::setValue(const QByteArray value){ + this->value=value; +} + +void HttpCookie::setComment(const QByteArray comment){ + this->comment=comment; +} + +void HttpCookie::setDomain(const QByteArray domain){ + this->domain=domain; +} + +void HttpCookie::setMaxAge(const int maxAge){ + this->maxAge=maxAge; +} + +void HttpCookie::setPath(const QByteArray path){ + this->path=path; +} + +void HttpCookie::setSecure(const bool secure){ + this->secure=secure; +} + +QByteArray HttpCookie::getName() const { + return name; +} + +QByteArray HttpCookie::getValue() const { + return value; +} + +QByteArray HttpCookie::getComment() const { + return comment; +} + +QByteArray HttpCookie::getDomain() const { + return domain; +} + +int HttpCookie::getMaxAge() const { + return maxAge; +} + +QByteArray HttpCookie::getPath() const { + return path; +} + +bool HttpCookie::getSecure() const { + return secure; +} + +int HttpCookie::getVersion() const { + return version; +} + +QList HttpCookie::splitCSV(const QByteArray source) { + bool inString=false; + QList list; + QByteArray buffer; + for (int i=0; i +#include +#include "httpglobal.h" + +/** + HTTP cookie as defined in RFC 2109. This class can also parse + RFC 2965 cookies, but skips fields that are not defined in RFC + 2109. +*/ + +class DECLSPEC HttpCookie +{ +public: + + /** Creates an empty cookie */ + HttpCookie(); + + /** + Create a cookie and set name/value pair. + @param name name of the cookie + @param value value of the cookie + @param maxAge maximum age of the cookie in seconds. 0=discard immediately + @param path Path for that the cookie will be sent, default="/" which means the whole domain + @param comment Optional comment, may be displayed by the web browser somewhere + @param domain Optional domain for that the cookie will be sent. Defaults to the current domain + @param secure If true, the cookie will only be sent on secure connections + */ + HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path="/", const QByteArray comment=QByteArray(), const QByteArray domain=QByteArray(), const bool secure=false); + + /** + Create a cookie from a string. + @param source String as received in a HTTP Cookie2 header. + */ + HttpCookie(const QByteArray source); + + /** Convert this cookie to a string that may be used in a Set-Cookie header. */ + QByteArray toByteArray() const ; + + /** + Split a string list into parts, where each part is delimited by semicolon. + Semicolons within double quotes are skipped. Double quotes are removed. + */ + static QList splitCSV(const QByteArray source); + + /** Set the name of this cookie */ + void setName(const QByteArray name); + + /** Set the value of this cookie */ + void setValue(const QByteArray value); + + /** Set the comment of this cookie */ + void setComment(const QByteArray comment); + + /** Set the domain of this cookie */ + void setDomain(const QByteArray domain); + + /** Set the maximum age of this cookie in seconds. 0=discard immediately */ + void setMaxAge(const int maxAge); + + /** Set the path for that the cookie will be sent, default="/" which means the whole domain */ + void setPath(const QByteArray path); + + /** Set secure mode, so that the cokkie will only be sent on secure connections */ + void setSecure(const bool secure); + + /** Get the name of this cookie */ + QByteArray getName() const; + + /** Get the value of this cookie */ + QByteArray getValue() const; + + /** Get the comment of this cookie */ + QByteArray getComment() const; + + /** Get the domain of this cookie */ + QByteArray getDomain() const; + + /** Set the maximum age of this cookie in seconds. */ + int getMaxAge() const; + + /** Set the path of this cookie */ + QByteArray getPath() const; + + /** Get the secure flag of this cookie */ + bool getSecure() const; + + /** Returns always 1 */ + int getVersion() const; + +private: + + QByteArray name; + QByteArray value; + QByteArray comment; + QByteArray domain; + int maxAge; + QByteArray path; + bool secure; + int version; + +}; + +#endif // HTTPCOOKIE_H diff --git a/httpserver/httpglobal.cpp b/httpserver/httpglobal.cpp new file mode 100644 index 000000000..df6339459 --- /dev/null +++ b/httpserver/httpglobal.cpp @@ -0,0 +1,7 @@ +#include "httpglobal.h" + +const char* getQtWebAppLibVersion() +{ + return "1.5.8"; +} + diff --git a/httpserver/httpglobal.h b/httpserver/httpglobal.h new file mode 100644 index 000000000..a4ccbd3ee --- /dev/null +++ b/httpserver/httpglobal.h @@ -0,0 +1,27 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPGLOBAL_H +#define HTTPGLOBAL_H + +#include + +// This is specific to Windows dll's +#if defined(Q_OS_WIN) + #if defined(QTWEBAPPLIB_EXPORT) + #define DECLSPEC Q_DECL_EXPORT + #elif defined(QTWEBAPPLIB_IMPORT) + #define DECLSPEC Q_DECL_IMPORT + #endif +#endif +#if !defined(DECLSPEC) + #define DECLSPEC +#endif + +/** Get the library version number */ +DECLSPEC const char* getQtWebAppLibVersion(); + +#endif // HTTPGLOBAL_H + diff --git a/httpserver/httplistener.cpp b/httpserver/httplistener.cpp new file mode 100644 index 000000000..9ec5d9fea --- /dev/null +++ b/httpserver/httplistener.cpp @@ -0,0 +1,84 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httplistener.h" +#include "httpconnectionhandler.h" +#include "httpconnectionhandlerpool.h" +#include + +HttpListener::HttpListener(QSettings* settings, HttpRequestHandler* requestHandler, QObject *parent) + : QTcpServer(parent) +{ + Q_ASSERT(settings!=0); + Q_ASSERT(requestHandler!=0); + pool=NULL; + this->settings=settings; + this->requestHandler=requestHandler; + // Reqister type of socketDescriptor for signal/slot handling + qRegisterMetaType("tSocketDescriptor"); + // Start listening + listen(); +} + + +HttpListener::~HttpListener() { + close(); + qDebug("HttpListener: destroyed"); +} + + +void HttpListener::listen() { + if (!pool) { + pool=new HttpConnectionHandlerPool(settings,requestHandler); + } + QString host = settings->value("host").toString(); + int port=settings->value("port").toInt(); + QTcpServer::listen(host.isEmpty() ? QHostAddress::Any : QHostAddress(host), port); + if (!isListening()) { + qCritical("HttpListener: Cannot bind on port %i: %s",port,qPrintable(errorString())); + } + else { + qDebug("HttpListener: Listening on port %i",port); + } +} + + +void HttpListener::close() { + QTcpServer::close(); + qDebug("HttpListener: closed"); + if (pool) { + delete pool; + pool=NULL; + } +} + +void HttpListener::incomingConnection(tSocketDescriptor socketDescriptor) { +#ifdef SUPERVERBOSE + qDebug("HttpListener: New connection"); +#endif + + HttpConnectionHandler* freeHandler=NULL; + if (pool) { + freeHandler=pool->getConnectionHandler(); + } + + // Let the handler process the new connection. + if (freeHandler) { + // The descriptor is passed via signal/slot because the handler lives in another + // thread and cannot open the socket when directly called by another thread. + connect(this,SIGNAL(handleConnection(tSocketDescriptor)),freeHandler,SLOT(handleConnection(tSocketDescriptor))); + emit handleConnection(socketDescriptor); + disconnect(this,SIGNAL(handleConnection(tSocketDescriptor)),freeHandler,SLOT(handleConnection(tSocketDescriptor))); + } + else { + // Reject the connection + qDebug("HttpListener: Too many incoming connections"); + QTcpSocket* socket=new QTcpSocket(this); + socket->setSocketDescriptor(socketDescriptor); + connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); + socket->write("HTTP/1.1 503 too many connections\r\nConnection: close\r\n\r\nToo many connections\r\n"); + socket->disconnectFromHost(); + } +} diff --git a/httpserver/httplistener.h b/httpserver/httplistener.h new file mode 100644 index 000000000..a88176d08 --- /dev/null +++ b/httpserver/httplistener.h @@ -0,0 +1,98 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPLISTENER_H +#define HTTPLISTENER_H + +#include +#include +#include +#include "httpglobal.h" +#include "httpconnectionhandler.h" +#include "httpconnectionhandlerpool.h" +#include "httprequesthandler.h" + +/** + Listens for incoming TCP connections and and passes all incoming HTTP requests to your implementation of HttpRequestHandler, + which processes the request and generates the response (usually a HTML document). +

+ Example for the required settings in the config file: +

+  ;host=192.168.0.100
+  port=8080
+  minThreads=1
+  maxThreads=10
+  cleanupInterval=1000
+  readTimeout=60000
+  ;sslKeyFile=ssl/my.key
+  ;sslCertFile=ssl/my.cert
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+ The optional host parameter binds the listener to one network interface. + The listener handles all network interfaces if no host is configured. + The port number specifies the incoming TCP port that this listener listens to. + @see HttpConnectionHandlerPool for description of config settings minThreads, maxThreads, cleanupInterval and ssl settings + @see HttpConnectionHandler for description of the readTimeout + @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize +*/ + +class DECLSPEC HttpListener : public QTcpServer { + Q_OBJECT + Q_DISABLE_COPY(HttpListener) +public: + + /** + Constructor. + Creates a connection pool and starts listening on the configured host and port. + @param settings Configuration settings for the HTTP server. Must not be 0. + @param requestHandler Processes each received HTTP request, usually by dispatching to controller classes. + @param parent Parent object. + @warning Ensure to close or delete the listener before deleting the request handler. + */ + HttpListener(QSettings* settings, HttpRequestHandler* requestHandler, QObject* parent = NULL); + + /** Destructor */ + virtual ~HttpListener(); + + /** + Restart listeing after close(). + */ + void listen(); + + /** + Closes the listener, waits until all pending requests are processed, + then closes the connection pool. + */ + void close(); + +protected: + + /** Serves new incoming connection requests */ + void incomingConnection(tSocketDescriptor socketDescriptor); + +private: + + /** Configuration settings for the HTTP server */ + QSettings* settings; + + /** Point to the reuqest handler which processes all HTTP requests */ + HttpRequestHandler* requestHandler; + + /** Pool of connection handlers */ + HttpConnectionHandlerPool* pool; + +signals: + + /** + Sent to the connection handler to process a new incoming connection. + @param socketDescriptor references the accepted connection. + */ + + void handleConnection(tSocketDescriptor socketDescriptor); + +}; + +#endif // HTTPLISTENER_H diff --git a/httpserver/httprequest.cpp b/httpserver/httprequest.cpp new file mode 100644 index 000000000..e4df686a4 --- /dev/null +++ b/httpserver/httprequest.cpp @@ -0,0 +1,446 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequest.h" +#include +#include +#include "httpcookie.h" + +HttpRequest::HttpRequest(QSettings* settings) { + status=waitForRequest; + currentSize=0; + expectedBodySize=0; + maxSize=settings->value("maxRequestSize","16000").toInt(); + maxMultiPartSize=settings->value("maxMultiPartSize","1000000").toInt(); +} + +void HttpRequest::readRequest(QTcpSocket* socket) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: read request"); + #endif + int toRead=maxSize-currentSize+1; // allow one byte more to be able to detect overflow + lineBuffer.append(socket->readLine(toRead)); + currentSize+=lineBuffer.size(); + if (!lineBuffer.contains('\r') && !lineBuffer.contains('\n')) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: collecting more parts until line break"); + #endif + return; + } + QByteArray newData=lineBuffer.trimmed(); + lineBuffer.clear(); + if (!newData.isEmpty()) { + QList list=newData.split(' '); + if (list.count()!=3 || !list.at(2).contains("HTTP")) { + qWarning("HttpRequest: received broken HTTP request, invalid first line"); + status=abort; + } + else { + method=list.at(0).trimmed(); + path=list.at(1); + version=list.at(2); + status=waitForHeader; + } + } +} + +void HttpRequest::readHeader(QTcpSocket* socket) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: read header"); + #endif + int toRead=maxSize-currentSize+1; // allow one byte more to be able to detect overflow + lineBuffer.append(socket->readLine(toRead)); + currentSize+=lineBuffer.size(); + if (!lineBuffer.contains('\r') && !lineBuffer.contains('\n')) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: collecting more parts until line break"); + #endif + return; + } + QByteArray newData=lineBuffer.trimmed(); + lineBuffer.clear(); + int colon=newData.indexOf(':'); + if (colon>0) { + // Received a line with a colon - a header + currentHeader=newData.left(colon); + QByteArray value=newData.mid(colon+1).trimmed(); + headers.insert(currentHeader,value); + #ifdef SUPERVERBOSE + qDebug("HttpRequest: received header %s: %s",currentHeader.data(),value.data()); + #endif + } + else if (!newData.isEmpty()) { + // received another line - belongs to the previous header + #ifdef SUPERVERBOSE + qDebug("HttpRequest: read additional line of header"); + #endif + // Received additional line of previous header + if (headers.contains(currentHeader)) { + headers.insert(currentHeader,headers.value(currentHeader)+" "+newData); + } + } + else { + // received an empty line - end of headers reached + #ifdef SUPERVERBOSE + qDebug("HttpRequest: headers completed"); + #endif + // Empty line received, that means all headers have been received + // Check for multipart/form-data + QByteArray contentType=headers.value("Content-Type"); + if (contentType.startsWith("multipart/form-data")) { + int posi=contentType.indexOf("boundary="); + if (posi>=0) { + boundary=contentType.mid(posi+9); + } + } + QByteArray contentLength=getHeader("Content-Length"); + if (!contentLength.isEmpty()) { + expectedBodySize=contentLength.toInt(); + } + if (expectedBodySize==0) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: expect no body"); + #endif + status=complete; + } + else if (boundary.isEmpty() && expectedBodySize+currentSize>maxSize) { + qWarning("HttpRequest: expected body is too large"); + status=abort; + } + else if (!boundary.isEmpty() && expectedBodySize>maxMultiPartSize) { + qWarning("HttpRequest: expected multipart body is too large"); + status=abort; + } + else { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: expect %i bytes body",expectedBodySize); + #endif + status=waitForBody; + } + } +} + +void HttpRequest::readBody(QTcpSocket* socket) { + Q_ASSERT(expectedBodySize!=0); + if (boundary.isEmpty()) { + // normal body, no multipart + #ifdef SUPERVERBOSE + qDebug("HttpRequest: receive body"); + #endif + int toRead=expectedBodySize-bodyData.size(); + QByteArray newData=socket->read(toRead); + currentSize+=newData.size(); + bodyData.append(newData); + if (bodyData.size()>=expectedBodySize) { + status=complete; + } + } + else { + // multipart body, store into temp file + #ifdef SUPERVERBOSE + qDebug("HttpRequest: receiving multipart body"); + #endif + if (!tempFile.isOpen()) { + tempFile.open(); + } + // Transfer data in 64kb blocks + int fileSize=tempFile.size(); + int toRead=expectedBodySize-fileSize; + if (toRead>65536) { + toRead=65536; + } + fileSize+=tempFile.write(socket->read(toRead)); + if (fileSize>=maxMultiPartSize) { + qWarning("HttpRequest: received too many multipart bytes"); + status=abort; + } + else if (fileSize>=expectedBodySize) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: received whole multipart body"); + #endif + tempFile.flush(); + if (tempFile.error()) { + qCritical("HttpRequest: Error writing temp file for multipart body"); + } + parseMultiPartFile(); + tempFile.close(); + status=complete; + } + } +} + +void HttpRequest::decodeRequestParams() { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: extract and decode request parameters"); + #endif + // Get URL parameters + QByteArray rawParameters; + int questionMark=path.indexOf('?'); + if (questionMark>=0) { + rawParameters=path.mid(questionMark+1); + path=path.left(questionMark); + } + // Get request body parameters + QByteArray contentType=headers.value("Content-Type"); + if (!bodyData.isEmpty() && (contentType.isEmpty() || contentType.startsWith("application/x-www-form-urlencoded"))) { + if (!rawParameters.isEmpty()) { + rawParameters.append('&'); + rawParameters.append(bodyData); + } + else { + rawParameters=bodyData; + } + } + // Split the parameters into pairs of value and name + QList list=rawParameters.split('&'); + foreach (QByteArray part, list) { + int equalsChar=part.indexOf('='); + if (equalsChar>=0) { + QByteArray name=part.left(equalsChar).trimmed(); + QByteArray value=part.mid(equalsChar+1).trimmed(); + parameters.insert(urlDecode(name),urlDecode(value)); + } + else if (!part.isEmpty()){ + // Name without value + parameters.insert(urlDecode(part),""); + } + } +} + +void HttpRequest::extractCookies() { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: extract cookies"); + #endif + foreach(QByteArray cookieStr, headers.values("Cookie")) { + QList list=HttpCookie::splitCSV(cookieStr); + foreach(QByteArray part, list) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: found cookie %s",part.data()); + #endif // Split the part into name and value + QByteArray name; + QByteArray value; + int posi=part.indexOf('='); + if (posi) { + name=part.left(posi).trimmed(); + value=part.mid(posi+1).trimmed(); + } + else { + name=part.trimmed(); + value=""; + } + cookies.insert(name,value); + } + } + headers.remove("Cookie"); +} + +void HttpRequest::readFromSocket(QTcpSocket* socket) { + Q_ASSERT(status!=complete); + if (status==waitForRequest) { + readRequest(socket); + } + else if (status==waitForHeader) { + readHeader(socket); + } + else if (status==waitForBody) { + readBody(socket); + } + if ((boundary.isEmpty() && currentSize>maxSize) || (!boundary.isEmpty() && currentSize>maxMultiPartSize)) { + qWarning("HttpRequest: received too many bytes"); + status=abort; + } + if (status==complete) { + // Extract and decode request parameters from url and body + decodeRequestParams(); + // Extract cookies from headers + extractCookies(); + } +} + + +HttpRequest::RequestStatus HttpRequest::getStatus() const { + return status; +} + + +QByteArray HttpRequest::getMethod() const { + return method; +} + + +QByteArray HttpRequest::getPath() const { + return urlDecode(path); +} + + +QByteArray HttpRequest::getVersion() const { + return version; +} + + +QByteArray HttpRequest::getHeader(const QByteArray& name) const { + return headers.value(name); +} + +QList HttpRequest::getHeaders(const QByteArray& name) const { + return headers.values(name); +} + +QMultiMap HttpRequest::getHeaderMap() const { + return headers; +} + +QByteArray HttpRequest::getParameter(const QByteArray& name) const { + return parameters.value(name); +} + +QList HttpRequest::getParameters(const QByteArray& name) const { + return parameters.values(name); +} + +QMultiMap HttpRequest::getParameterMap() const { + return parameters; +} + +QByteArray HttpRequest::getBody() const { + return bodyData; +} + +QByteArray HttpRequest::urlDecode(const QByteArray source) { + QByteArray buffer(source); + buffer.replace('+',' '); + int percentChar=buffer.indexOf('%'); + while (percentChar>=0) { + bool ok; + char byte=buffer.mid(percentChar+1,2).toInt(&ok,16); + if (ok) { + buffer.replace(percentChar,3,(char*)&byte,1); + } + percentChar=buffer.indexOf('%',percentChar+1); + } + return buffer; +} + + +void HttpRequest::parseMultiPartFile() { + qDebug("HttpRequest: parsing multipart temp file"); + tempFile.seek(0); + bool finished=false; + while (!tempFile.atEnd() && !finished && !tempFile.error()) { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: reading multpart headers"); + #endif + QByteArray fieldName; + QByteArray fileName; + while (!tempFile.atEnd() && !finished && !tempFile.error()) { + QByteArray line=tempFile.readLine(65536).trimmed(); + if (line.startsWith("Content-Disposition:")) { + if (line.contains("form-data")) { + int start=line.indexOf(" name=\""); + int end=line.indexOf("\"",start+7); + if (start>=0 && end>=start) { + fieldName=line.mid(start+7,end-start-7); + } + start=line.indexOf(" filename=\""); + end=line.indexOf("\"",start+11); + if (start>=0 && end>=start) { + fileName=line.mid(start+11,end-start-11); + } + #ifdef SUPERVERBOSE + qDebug("HttpRequest: multipart field=%s, filename=%s",fieldName.data(),fileName.data()); + #endif + } + else { + qDebug("HttpRequest: ignoring unsupported content part %s",line.data()); + } + } + else if (line.isEmpty()) { + break; + } + } + + #ifdef SUPERVERBOSE + qDebug("HttpRequest: reading multpart data"); + #endif + QTemporaryFile* uploadedFile=0; + QByteArray fieldValue; + while (!tempFile.atEnd() && !finished && !tempFile.error()) { + QByteArray line=tempFile.readLine(65536); + if (line.startsWith("--"+boundary)) { + // Boundary found. Until now we have collected 2 bytes too much, + // so remove them from the last result + if (fileName.isEmpty() && !fieldName.isEmpty()) { + // last field was a form field + fieldValue.remove(fieldValue.size()-2,2); + parameters.insert(fieldName,fieldValue); + qDebug("HttpRequest: set parameter %s=%s",fieldName.data(),fieldValue.data()); + } + else if (!fileName.isEmpty() && !fieldName.isEmpty()) { + // last field was a file + #ifdef SUPERVERBOSE + qDebug("HttpRequest: finishing writing to uploaded file"); + #endif + uploadedFile->resize(uploadedFile->size()-2); + uploadedFile->flush(); + uploadedFile->seek(0); + parameters.insert(fieldName,fileName); + qDebug("HttpRequest: set parameter %s=%s",fieldName.data(),fileName.data()); + uploadedFiles.insert(fieldName,uploadedFile); + qDebug("HttpRequest: uploaded file size is %i",(int) uploadedFile->size()); + } + if (line.contains(boundary+"--")) { + finished=true; + } + break; + } + else { + if (fileName.isEmpty() && !fieldName.isEmpty()) { + // this is a form field. + currentSize+=line.size(); + fieldValue.append(line); + } + else if (!fileName.isEmpty() && !fieldName.isEmpty()) { + // this is a file + if (!uploadedFile) { + uploadedFile=new QTemporaryFile(); + uploadedFile->open(); + } + uploadedFile->write(line); + if (uploadedFile->error()) { + qCritical("HttpRequest: error writing temp file, %s",qPrintable(uploadedFile->errorString())); + } + } + } + } + } + if (tempFile.error()) { + qCritical("HttpRequest: cannot read temp file, %s",qPrintable(tempFile.errorString())); + } + #ifdef SUPERVERBOSE + qDebug("HttpRequest: finished parsing multipart temp file"); + #endif +} + +HttpRequest::~HttpRequest() { + foreach(QByteArray key, uploadedFiles.keys()) { + QTemporaryFile* file=uploadedFiles.value(key); + file->close(); + delete file; + } +} + +QTemporaryFile* HttpRequest::getUploadedFile(const QByteArray fieldName) { + return uploadedFiles.value(fieldName); +} + +QByteArray HttpRequest::getCookie(const QByteArray& name) const { + return cookies.value(name); +} + +/** Get the map of cookies */ +QMap& HttpRequest::getCookieMap() { + return cookies; +} + diff --git a/httpserver/httprequest.h b/httpserver/httprequest.h new file mode 100644 index 000000000..1b1a70201 --- /dev/null +++ b/httpserver/httprequest.h @@ -0,0 +1,217 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUEST_H +#define HTTPREQUEST_H + +#include +#include +#include +#include +#include +#include +#include +#include "httpglobal.h" + +/** + This object represents a single HTTP request. It reads the request + from a TCP socket and provides getters for the individual parts + of the request. +

+ The follwing config settings are required: +

+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+

+ MaxRequestSize is the maximum size of a HTTP request. In case of + multipart/form-data requests (also known as file-upload), the maximum + size of the body must not exceed maxMultiPartSize. + The body is always a little larger than the file itself. +*/ + +class DECLSPEC HttpRequest { + Q_DISABLE_COPY(HttpRequest) + friend class HttpSessionStore; + +public: + + /** Values for getStatus() */ + enum RequestStatus {waitForRequest, waitForHeader, waitForBody, complete, abort}; + + /** + Constructor. + @param settings Configuration settings + */ + HttpRequest(QSettings* settings); + + /** + Destructor. + */ + virtual ~HttpRequest(); + + /** + Read the request from a socket. This method must be called repeatedly + until the status is RequestStatus::complete or RequestStatus::abort. + @param socket Source of the data + */ + void readFromSocket(QTcpSocket* socket); + + /** + Get the status of this reqeust. + @see RequestStatus + */ + RequestStatus getStatus() const; + + /** Get the method of the HTTP request (e.g. "GET") */ + QByteArray getMethod() const; + + /** Get the decoded path of the HTPP request (e.g. "/index.html") */ + QByteArray getPath() const; + + /** Get the version of the HTPP request (e.g. "HTTP/1.1") */ + QByteArray getVersion() const; + + /** + Get the value of a HTTP request header. + @param name Name of the header + @return If the header occurs multiple times, only the last + one is returned. + */ + QByteArray getHeader(const QByteArray& name) const; + + /** + Get the values of a HTTP request header. + @param name Name of the header + */ + QList getHeaders(const QByteArray& name) const; + + /** Get all HTTP request headers */ + QMultiMap getHeaderMap() const; + + /** + Get the value of a HTTP request parameter. + @param name Name of the parameter + @return If the parameter occurs multiple times, only the last + one is returned. + */ + QByteArray getParameter(const QByteArray& name) const; + + /** + Get the values of a HTTP request parameter. + @param name Name of the parameter + */ + QList getParameters(const QByteArray& name) const; + + /** Get all HTTP request parameters */ + QMultiMap getParameterMap() const; + + /** Get the HTTP request body */ + QByteArray getBody() const; + + /** + Decode an URL parameter. + E.g. replace "%23" by '#' and replace '+' by ' '. + @param source The url encoded strings + @see QUrl::toPercentEncoding for the reverse direction + */ + static QByteArray urlDecode(const QByteArray source); + + /** + Get an uploaded file. The file is already open. It will + be closed and deleted by the destructor of this HttpRequest + object (after processing the request). +

+ For uploaded files, the method getParameters() returns + the original fileName as provided by the calling web browser. + */ + QTemporaryFile* getUploadedFile(const QByteArray fieldName); + + /** + Get the value of a cookie + @param name Name of the cookie + */ + QByteArray getCookie(const QByteArray& name) const; + + /** Get the map of cookies */ + QMap& getCookieMap(); + +private: + + /** Request headers */ + QMultiMap headers; + + /** Parameters of the request */ + QMultiMap parameters; + + /** Uploaded files of the request, key is the field name. */ + QMap uploadedFiles; + + /** Received cookies */ + QMap cookies; + + /** Storage for raw body data */ + QByteArray bodyData; + + /** Request method */ + QByteArray method; + + /** Request path (in raw encoded format) */ + QByteArray path; + + /** Request protocol version */ + QByteArray version; + + /** + Status of this request. + @see RequestStatus + */ + RequestStatus status; + + /** Maximum size of requests in bytes. */ + int maxSize; + + /** Maximum allowed size of multipart forms in bytes. */ + int maxMultiPartSize; + + /** Current size */ + int currentSize; + + /** Expected size of body */ + int expectedBodySize; + + /** Name of the current header, or empty if no header is being processed */ + QByteArray currentHeader; + + /** Boundary of multipart/form-data body. Empty if there is no such header */ + QByteArray boundary; + + /** Temp file, that is used to store the multipart/form-data body */ + QTemporaryFile tempFile; + + /** Parset he multipart body, that has been stored in the temp file. */ + void parseMultiPartFile(); + + /** Sub-procedure of readFromSocket(), read the first line of a request. */ + void readRequest(QTcpSocket* socket); + + /** Sub-procedure of readFromSocket(), read header lines. */ + void readHeader(QTcpSocket* socket); + + /** Sub-procedure of readFromSocket(), read the request body. */ + void readBody(QTcpSocket* socket); + + /** Sub-procedure of readFromSocket(), extract and decode request parameters. */ + void decodeRequestParams(); + + /** Sub-procedure of readFromSocket(), extract cookies from headers */ + void extractCookies(); + + /** Buffer for collecting characters of request and header lines */ + QByteArray lineBuffer; + +}; + +#endif // HTTPREQUEST_H diff --git a/httpserver/httprequesthandler.cpp b/httpserver/httprequesthandler.cpp new file mode 100644 index 000000000..fcfd85c2f --- /dev/null +++ b/httpserver/httprequesthandler.cpp @@ -0,0 +1,19 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequesthandler.h" + +HttpRequestHandler::HttpRequestHandler(QObject* parent) + : QObject(parent) +{} + +HttpRequestHandler::~HttpRequestHandler() {} + +void HttpRequestHandler::service(HttpRequest& request, HttpResponse& response) { + qCritical("HttpRequestHandler: you need to override the service() function"); + qDebug("HttpRequestHandler: request=%s %s %s",request.getMethod().data(),request.getPath().data(),request.getVersion().data()); + response.setStatus(501,"not implemented"); + response.write("501 not implemented",true); +} diff --git a/httpserver/httprequesthandler.h b/httpserver/httprequesthandler.h new file mode 100644 index 000000000..12ac0cf9b --- /dev/null +++ b/httpserver/httprequesthandler.h @@ -0,0 +1,49 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUESTHANDLER_H +#define HTTPREQUESTHANDLER_H + +#include "httpglobal.h" +#include "httprequest.h" +#include "httpresponse.h" + +/** + The request handler generates a response for each HTTP request. Web Applications + usually have one central request handler that maps incoming requests to several + controllers (servlets) based on the requested path. +

+ You need to override the service() method or you will always get an HTTP error 501. +

+ @warning Be aware that the main request handler instance must be created on the heap and + that it is used by multiple threads simultaneously. + @see StaticFileController which delivers static local files. +*/ + +class DECLSPEC HttpRequestHandler : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpRequestHandler) +public: + + /** + * Constructor. + * @param parent Parent object. + */ + HttpRequestHandler(QObject* parent=NULL); + + /** Destructor */ + virtual ~HttpRequestHandler(); + + /** + Generate a response for an incoming HTTP request. + @param request The received HTTP request + @param response Must be used to return the response + @warning This method must be thread safe + */ + virtual void service(HttpRequest& request, HttpResponse& response); + +}; + +#endif // HTTPREQUESTHANDLER_H diff --git a/httpserver/httpresponse.cpp b/httpserver/httpresponse.cpp new file mode 100644 index 000000000..542177011 --- /dev/null +++ b/httpserver/httpresponse.cpp @@ -0,0 +1,134 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpresponse.h" + +HttpResponse::HttpResponse(QTcpSocket* socket) { + this->socket=socket; + statusCode=200; + statusText="OK"; + sentHeaders=false; + sentLastPart=false; +} + +void HttpResponse::setHeader(QByteArray name, QByteArray value) { + Q_ASSERT(sentHeaders==false); + headers.insert(name,value); +} + +void HttpResponse::setHeader(QByteArray name, int value) { + Q_ASSERT(sentHeaders==false); + headers.insert(name,QByteArray::number(value)); +} + +QMap& HttpResponse::getHeaders() { + return headers; +} + +void HttpResponse::setStatus(int statusCode, QByteArray description) { + this->statusCode=statusCode; + statusText=description; +} + +void HttpResponse::writeHeaders() { + Q_ASSERT(sentHeaders==false); + QByteArray buffer; + buffer.append("HTTP/1.1 "); + buffer.append(QByteArray::number(statusCode)); + buffer.append(' '); + buffer.append(statusText); + buffer.append("\r\n"); + foreach(QByteArray name, headers.keys()) { + buffer.append(name); + buffer.append(": "); + buffer.append(headers.value(name)); + buffer.append("\r\n"); + } + foreach(HttpCookie cookie,cookies.values()) { + buffer.append("Set-Cookie: "); + buffer.append(cookie.toByteArray()); + buffer.append("\r\n"); + } + buffer.append("\r\n"); + writeToSocket(buffer); + sentHeaders=true; +} + +bool HttpResponse::writeToSocket(QByteArray data) { + int remaining=data.size(); + char* ptr=data.data(); + while (socket->isOpen() && remaining>0) { + // Wait until the previous buffer content is written out, otherwise it could become very large + socket->waitForBytesWritten(-1); + int written=socket->write(ptr,remaining); + if (written==-1) { + return false; + } + ptr+=written; + remaining-=written; + } + return true; +} + +void HttpResponse::write(QByteArray data, bool lastPart) { + Q_ASSERT(sentLastPart==false); + if (sentHeaders==false) { + QByteArray connectionMode=headers.value("Connection"); + if (!headers.contains("Content-Length") && !headers.contains("Transfer-Encoding") && connectionMode!="close" && connectionMode!="Close") { + if (!lastPart) { + headers.insert("Transfer-Encoding","chunked"); + } + else { + headers.insert("Content-Length",QByteArray::number(data.size())); + } + } + writeHeaders(); + } + bool chunked=headers.value("Transfer-Encoding")=="chunked" || headers.value("Transfer-Encoding")=="Chunked"; + if (chunked) { + if (data.size()>0) { + QByteArray buffer=QByteArray::number(data.size(),16); + buffer.append("\r\n"); + writeToSocket(buffer); + writeToSocket(data); + writeToSocket("\r\n"); + } + } + else { + writeToSocket(data); + } + if (lastPart) { + if (chunked) { + writeToSocket("0\r\n\r\n"); + } + else if (!headers.contains("Content-Length")) { + socket->disconnectFromHost(); + } + sentLastPart=true; + } +} + + +bool HttpResponse::hasSentLastPart() const { + return sentLastPart; +} + + +void HttpResponse::setCookie(const HttpCookie& cookie) { + Q_ASSERT(sentHeaders==false); + if (!cookie.getName().isEmpty()) { + cookies.insert(cookie.getName(),cookie); + } +} + +QMap& HttpResponse::getCookies() { + return cookies; +} + +void HttpResponse::redirect(const QByteArray& url) { + setStatus(303,"See Other"); + setHeader("Location",url); + write("Redirect",true); +} diff --git a/httpserver/httpresponse.h b/httpserver/httpresponse.h new file mode 100644 index 000000000..8c47af4d3 --- /dev/null +++ b/httpserver/httpresponse.h @@ -0,0 +1,141 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPRESPONSE_H +#define HTTPRESPONSE_H + +#include +#include +#include +#include "httpglobal.h" +#include "httpcookie.h" + +/** + This object represents a HTTP response, in particular the response headers. +

+ Example code for proper response generation: +

+    response.setStatus(200,"OK"); // optional, because this is the default
+    response.writeBody("Hello");
+    response.writeBody("World!",true);
+  
+

+ Example how to return an error: +

+    response.setStatus(500,"server error");
+    response.write("The request cannot be processed because the servers is broken",true);
+  
+

+ For performance reason, writing a single or few large packets is better than writing + many small packets. In case of large responses (e.g. file downloads), a Content-Length + header should be set before calling write(). Web Browsers use that information to display + a progress bar. +*/ + +class DECLSPEC HttpResponse { + Q_DISABLE_COPY(HttpResponse) +public: + + /** + Constructor. + @param socket used to write the response + */ + HttpResponse(QTcpSocket* socket); + + /** + Set a HTTP response header + @param name name of the header + @param value value of the header + */ + void setHeader(QByteArray name, QByteArray value); + + /** + Set a HTTP response header + @param name name of the header + @param value value of the header + */ + void setHeader(QByteArray name, int value); + + /** Get the map of HTTP response headers */ + QMap& getHeaders(); + + /** Get the map of cookies */ + QMap& getCookies(); + + /** + Set status code and description. The default is 200,OK. + */ + void setStatus(int statusCode, QByteArray description=QByteArray()); + + /** + Write body data to the socket. +

+ The HTTP status line and headers are sent automatically before the first + byte of the body gets sent. +

+ If the response contains only a single chunk (indicated by lastPart=true), + the response is transferred in traditional mode with a Content-Length + header, which is automatically added if not already set before. +

+ Otherwise, each part is transferred in chunked mode. + @param data Data bytes of the body + @param lastPart Indicator, if this is the last part of the response. + */ + void write(QByteArray data, bool lastPart=false); + + /** + Indicates wheter the body has been sent completely. Used by the connection + handler to terminate the body automatically when necessary. + */ + bool hasSentLastPart() const; + + /** + Set a cookie. Cookies are sent together with the headers when the first + call to write() occurs. + */ + void setCookie(const HttpCookie& cookie); + + /** + Send a redirect response to the browser. + @param url Destination URL + */ + void redirect(const QByteArray& url); + +private: + + /** Request headers */ + QMap headers; + + /** Socket for writing output */ + QTcpSocket* socket; + + /** HTTP status code*/ + int statusCode; + + /** HTTP status code description */ + QByteArray statusText; + + /** Indicator whether headers have been sent */ + bool sentHeaders; + + /** Indicator whether the body has been sent completely */ + bool sentLastPart; + + /** Cookies */ + QMap cookies; + + /** Write raw data to the socket. This method blocks until all bytes have been passed to the TCP buffer */ + bool writeToSocket(QByteArray data); + + /** + Write the response HTTP status and headers to the socket. + Calling this method is optional, because writeBody() calls + it automatically when required. + */ + void writeHeaders(); + +}; + +#endif // HTTPRESPONSE_H diff --git a/httpserver/httpserver.pri b/httpserver/httpserver.pri new file mode 100644 index 000000000..fb78772ea --- /dev/null +++ b/httpserver/httpserver.pri @@ -0,0 +1,33 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD + +QT += network + +# Enable very detailed debug messages when compiling the debug version +CONFIG(debug, debug|release) { + DEFINES += SUPERVERBOSE +} + +HEADERS += $$PWD/httpglobal.h \ + $$PWD/httplistener.h \ + $$PWD/httpconnectionhandler.h \ + $$PWD/httpconnectionhandlerpool.h \ + $$PWD/httprequest.h \ + $$PWD/httpresponse.h \ + $$PWD/httpcookie.h \ + $$PWD/httprequesthandler.h \ + $$PWD/httpsession.h \ + $$PWD/httpsessionstore.h \ + $$PWD/staticfilecontroller.h + +SOURCES += $$PWD/httpglobal.cpp \ + $$PWD/httplistener.cpp \ + $$PWD/httpconnectionhandler.cpp \ + $$PWD/httpconnectionhandlerpool.cpp \ + $$PWD/httprequest.cpp \ + $$PWD/httpresponse.cpp \ + $$PWD/httpcookie.cpp \ + $$PWD/httprequesthandler.cpp \ + $$PWD/httpsession.cpp \ + $$PWD/httpsessionstore.cpp \ + $$PWD/staticfilecontroller.cpp diff --git a/httpserver/httpsession.cpp b/httpserver/httpsession.cpp new file mode 100644 index 000000000..9855ce9f9 --- /dev/null +++ b/httpserver/httpsession.cpp @@ -0,0 +1,158 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsession.h" +#include +#include + + +HttpSession::HttpSession(bool canStore) { + if (canStore) { + dataPtr=new HttpSessionData(); + dataPtr->refCount=1; + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->id=QUuid::createUuid().toString().toLocal8Bit(); +#ifdef SUPERVERBOSE + qDebug("HttpSession: created new session data with id %s",dataPtr->id.data()); +#endif + } + else { + dataPtr=0; + } +} + +HttpSession::HttpSession(const HttpSession& other) { + dataPtr=other.dataPtr; + if (dataPtr) { + dataPtr->lock.lockForWrite(); + dataPtr->refCount++; +#ifdef SUPERVERBOSE + qDebug("HttpSession: refCount of %s is %i",dataPtr->id.data(),dataPtr->refCount); +#endif + dataPtr->lock.unlock(); + } +} + +HttpSession& HttpSession::operator= (const HttpSession& other) { + HttpSessionData* oldPtr=dataPtr; + dataPtr=other.dataPtr; + if (dataPtr) { + dataPtr->lock.lockForWrite(); + dataPtr->refCount++; +#ifdef SUPERVERBOSE + qDebug("HttpSession: refCount of %s is %i",dataPtr->id.data(),dataPtr->refCount); +#endif + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->lock.unlock(); + } + if (oldPtr) { + int refCount; + oldPtr->lock.lockForRead(); + refCount=oldPtr->refCount--; +#ifdef SUPERVERBOSE + qDebug("HttpSession: refCount of %s is %i",oldPtr->id.data(),oldPtr->refCount); +#endif + oldPtr->lock.unlock(); + if (refCount==0) { + delete oldPtr; + } + } + return *this; +} + +HttpSession::~HttpSession() { + if (dataPtr) { + int refCount; + dataPtr->lock.lockForRead(); + refCount=--dataPtr->refCount; +#ifdef SUPERVERBOSE + qDebug("HttpSession: refCount of %s is %i",dataPtr->id.data(),dataPtr->refCount); +#endif + dataPtr->lock.unlock(); + if (refCount==0) { + qDebug("HttpSession: deleting data"); + delete dataPtr; + } + } +} + + +QByteArray HttpSession::getId() const { + if (dataPtr) { + return dataPtr->id; + } + else { + return QByteArray(); + } +} + +bool HttpSession::isNull() const { + return dataPtr==0; +} + +void HttpSession::set(const QByteArray& key, const QVariant& value) { + if (dataPtr) { + dataPtr->lock.lockForWrite(); + dataPtr->values.insert(key,value); + dataPtr->lock.unlock(); + } +} + +void HttpSession::remove(const QByteArray& key) { + if (dataPtr) { + dataPtr->lock.lockForWrite(); + dataPtr->values.remove(key); + dataPtr->lock.unlock(); + } +} + +QVariant HttpSession::get(const QByteArray& key) const { + QVariant value; + if (dataPtr) { + dataPtr->lock.lockForRead(); + value=dataPtr->values.value(key); + dataPtr->lock.unlock(); + } + return value; +} + +bool HttpSession::contains(const QByteArray& key) const { + bool found=false; + if (dataPtr) { + dataPtr->lock.lockForRead(); + found=dataPtr->values.contains(key); + dataPtr->lock.unlock(); + } + return found; +} + +QMap HttpSession::getAll() const { + QMap values; + if (dataPtr) { + dataPtr->lock.lockForRead(); + values=dataPtr->values; + dataPtr->lock.unlock(); + } + return values; +} + +qint64 HttpSession::getLastAccess() const { + qint64 value=0; + if (dataPtr) { + dataPtr->lock.lockForRead(); + value=dataPtr->lastAccess; + dataPtr->lock.unlock(); + } + return value; +} + + +void HttpSession::setLastAccess() { + if (dataPtr) { + dataPtr->lock.lockForRead(); + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->lock.unlock(); + } +} diff --git a/httpserver/httpsession.h b/httpserver/httpsession.h new file mode 100644 index 000000000..fa0ee3f9f --- /dev/null +++ b/httpserver/httpsession.h @@ -0,0 +1,118 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSION_H +#define HTTPSESSION_H + +#include +#include +#include +#include "httpglobal.h" + +/** + This class stores data for a single HTTP session. + A session can store any number of key/value pairs. This class uses implicit + sharing for read and write access. This class is thread safe. + @see HttpSessionStore should be used to create and get instances of this class. +*/ + +class DECLSPEC HttpSession { + +public: + + /** + Constructor. + @param canStore The session can store data, if this parameter is true. + Otherwise all calls to set() and remove() do not have any effect. + */ + HttpSession(bool canStore=false); + + /** + Copy constructor. Creates another HttpSession object that shares the + data of the other object. + */ + HttpSession(const HttpSession& other); + + /** + Copy operator. Detaches from the current shared data and attaches to + the data of the other object. + */ + HttpSession& operator= (const HttpSession& other); + + + /** + Destructor. Detaches from the shared data. + */ + virtual ~HttpSession(); + + /** Get the unique ID of this session. This method is thread safe. */ + QByteArray getId() const; + + /** + Null sessions cannot store data. All calls to set() and remove() + do not have any effect.This method is thread safe. + */ + bool isNull() const; + + /** Set a value. This method is thread safe. */ + void set(const QByteArray& key, const QVariant& value); + + /** Remove a value. This method is thread safe. */ + void remove(const QByteArray& key); + + /** Get a value. This method is thread safe. */ + QVariant get(const QByteArray& key) const; + + /** Check if a key exists. This method is thread safe. */ + bool contains(const QByteArray& key) const; + + /** + Get a copy of all data stored in this session. + Changes to the session do not affect the copy and vice versa. + This method is thread safe. + */ + QMap getAll() const; + + /** + Get the timestamp of last access. That is the time when the last + HttpSessionStore::getSession() has been called. + This method is thread safe. + */ + qint64 getLastAccess() const; + + /** + Set the timestamp of last access, to renew the timeout period. + Called by HttpSessionStore::getSession(). + This method is thread safe. + */ + void setLastAccess(); + +private: + + struct HttpSessionData { + + /** Unique ID */ + QByteArray id; + + /** Timestamp of last access, set by the HttpSessionStore */ + qint64 lastAccess; + + /** Reference counter */ + int refCount; + + /** Used to synchronize threads */ + QReadWriteLock lock; + + /** Storage for the key/value pairs; */ + QMap values; + + }; + + /** Pointer to the shared data. */ + HttpSessionData* dataPtr; + +}; + +#endif // HTTPSESSION_H diff --git a/httpserver/httpsessionstore.cpp b/httpserver/httpsessionstore.cpp new file mode 100644 index 000000000..3e635dad6 --- /dev/null +++ b/httpserver/httpsessionstore.cpp @@ -0,0 +1,113 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsessionstore.h" +#include +#include + +HttpSessionStore::HttpSessionStore(QSettings* settings, QObject* parent) + :QObject(parent) +{ + this->settings=settings; + connect(&cleanupTimer,SIGNAL(timeout()),this,SLOT(timerEvent())); + cleanupTimer.start(60000); + cookieName=settings->value("cookieName","sessionid").toByteArray(); + expirationTime=settings->value("expirationTime",3600000).toInt(); + qDebug("HttpSessionStore: Sessions expire after %i milliseconds",expirationTime); +} + +HttpSessionStore::~HttpSessionStore() +{ + cleanupTimer.stop(); +} + +QByteArray HttpSessionStore::getSessionId(HttpRequest& request, HttpResponse& response) { + // The session ID in the response has priority because this one will be used in the next request. + mutex.lock(); + // Get the session ID from the response cookie + QByteArray sessionId=response.getCookies().value(cookieName).getValue(); + if (sessionId.isEmpty()) { + // Get the session ID from the request cookie + sessionId=request.getCookie(cookieName); + } + // Clear the session ID if there is no such session in the storage. + if (!sessionId.isEmpty()) { + if (!sessions.contains(sessionId)) { + qDebug("HttpSessionStore: received invalid session cookie with ID %s",sessionId.data()); + sessionId.clear(); + } + } + mutex.unlock(); + return sessionId; +} + +HttpSession HttpSessionStore::getSession(HttpRequest& request, HttpResponse& response, bool allowCreate) { + QByteArray sessionId=getSessionId(request,response); + mutex.lock(); + if (!sessionId.isEmpty()) { + HttpSession session=sessions.value(sessionId); + if (!session.isNull()) { + mutex.unlock(); + // Refresh the session cookie + QByteArray cookieName=settings->value("cookieName","sessionid").toByteArray(); + QByteArray cookiePath=settings->value("cookiePath").toByteArray(); + QByteArray cookieComment=settings->value("cookieComment").toByteArray(); + QByteArray cookieDomain=settings->value("cookieDomain").toByteArray(); + response.setCookie(HttpCookie(cookieName,session.getId(),expirationTime/1000,cookiePath,cookieComment,cookieDomain)); + session.setLastAccess(); + return session; + } + } + // Need to create a new session + if (allowCreate) { + QByteArray cookieName=settings->value("cookieName","sessionid").toByteArray(); + QByteArray cookiePath=settings->value("cookiePath").toByteArray(); + QByteArray cookieComment=settings->value("cookieComment").toByteArray(); + QByteArray cookieDomain=settings->value("cookieDomain").toByteArray(); + HttpSession session(true); + qDebug("HttpSessionStore: create new session with ID %s",session.getId().data()); + sessions.insert(session.getId(),session); + response.setCookie(HttpCookie(cookieName,session.getId(),expirationTime/1000,cookiePath,cookieComment,cookieDomain)); + mutex.unlock(); + return session; + } + // Return a null session + mutex.unlock(); + return HttpSession(); +} + +HttpSession HttpSessionStore::getSession(const QByteArray id) { + mutex.lock(); + HttpSession session=sessions.value(id); + mutex.unlock(); + session.setLastAccess(); + return session; +} + +void HttpSessionStore::timerEvent() { + // Todo: find a way to delete sessions only if no controller is accessing them + mutex.lock(); + qint64 now=QDateTime::currentMSecsSinceEpoch(); + QMap::iterator i = sessions.begin(); + while (i != sessions.end()) { + QMap::iterator prev = i; + ++i; + HttpSession session=prev.value(); + qint64 lastAccess=session.getLastAccess(); + if (now-lastAccess>expirationTime) { + qDebug("HttpSessionStore: session %s expired",session.getId().data()); + sessions.erase(prev); + } + } + mutex.unlock(); +} + + +/** Delete a session */ +void HttpSessionStore::removeSession(HttpSession session) { + mutex.lock(); + sessions.remove(session.getId()); + mutex.unlock(); +} diff --git a/httpserver/httpsessionstore.h b/httpserver/httpsessionstore.h new file mode 100644 index 000000000..1a7f275a5 --- /dev/null +++ b/httpserver/httpsessionstore.h @@ -0,0 +1,105 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSIONSTORE_H +#define HTTPSESSIONSTORE_H + +#include +#include +#include +#include +#include "httpglobal.h" +#include "httpsession.h" +#include "httpresponse.h" +#include "httprequest.h" + +/** + Stores HTTP sessions and deletes them when they have expired. + The following configuration settings are required in the config file: +

+  expirationTime=3600000
+  cookieName=sessionid
+  
+ The following additional configurations settings are optionally: +
+  cookiePath=/
+  cookieComment=Session ID
+  ;cookieDomain=stefanfrings.de
+  
+*/ + +class DECLSPEC HttpSessionStore : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpSessionStore) +public: + + /** Constructor. */ + HttpSessionStore(QSettings* settings, QObject* parent=NULL); + + /** Destructor */ + virtual ~HttpSessionStore(); + + /** + Get the ID of the current HTTP session, if it is valid. + This method is thread safe. + @warning Sessions may expire at any time, so subsequent calls of + getSession() might return a new session with a different ID. + @param request Used to get the session cookie + @param response Used to get and set the new session cookie + @return Empty string, if there is no valid session. + */ + QByteArray getSessionId(HttpRequest& request, HttpResponse& response); + + /** + Get the session of a HTTP request, eventually create a new one. + This method is thread safe. New sessions can only be created before + the first byte has been written to the HTTP response. + @param request Used to get the session cookie + @param response Used to get and set the new session cookie + @param allowCreate can be set to false, to disable the automatic creation of a new session. + @return If autoCreate is disabled, the function returns a null session if there is no session. + @see HttpSession::isNull() + */ + HttpSession getSession(HttpRequest& request, HttpResponse& response, bool allowCreate=true); + + /** + Get a HTTP session by it's ID number. + This method is thread safe. + @return If there is no such session, the function returns a null session. + @param id ID number of the session + @see HttpSession::isNull() + */ + HttpSession getSession(const QByteArray id); + + /** Delete a session */ + void removeSession(HttpSession session); + +private: + + /** Configuration settings */ + QSettings* settings; + + /** Storage for the sessions */ + QMap sessions; + + /** Timer to remove expired sessions */ + QTimer cleanupTimer; + + /** Name of the session cookie */ + QByteArray cookieName; + + /** Time when sessions expire (in ms)*/ + int expirationTime; + + /** Used to synchronize threads */ + QMutex mutex; + +private slots: + + /** Called every minute to cleanup expired sessions. */ + void timerEvent(); +}; + +#endif // HTTPSESSIONSTORE_H diff --git a/httpserver/staticfilecontroller.cpp b/httpserver/staticfilecontroller.cpp new file mode 100644 index 000000000..34e675ec4 --- /dev/null +++ b/httpserver/staticfilecontroller.cpp @@ -0,0 +1,136 @@ +/** + @file + @author Stefan Frings +*/ + +#include "staticfilecontroller.h" +#include +#include +#include + +StaticFileController::StaticFileController(QSettings* settings, QObject* parent) + :HttpRequestHandler(parent) +{ + maxAge=settings->value("maxAge","60000").toInt(); + encoding=settings->value("encoding","UTF-8").toString(); + docroot=settings->value("path",".").toString(); + if(!(docroot.startsWith(":/") || docroot.startsWith("qrc://"))) + { + // Convert relative path to absolute, based on the directory of the config file. + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(docroot) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(docroot)) + #endif + { + QFileInfo configFile(settings->fileName()); + docroot=QFileInfo(configFile.absolutePath(),docroot).absoluteFilePath(); + } + } + qDebug("StaticFileController: docroot=%s, encoding=%s, maxAge=%i",qPrintable(docroot),qPrintable(encoding),maxAge); + maxCachedFileSize=settings->value("maxCachedFileSize","65536").toInt(); + cache.setMaxCost(settings->value("cacheSize","1000000").toInt()); + cacheTimeout=settings->value("cacheTime","60000").toInt(); + qDebug("StaticFileController: cache timeout=%i, size=%i",cacheTimeout,cache.maxCost()); +} + + +void StaticFileController::service(HttpRequest& request, HttpResponse& response) { + QByteArray path=request.getPath(); + // Check if we have the file in cache + qint64 now=QDateTime::currentMSecsSinceEpoch(); + mutex.lock(); + CacheEntry* entry=cache.object(path); + if (entry && (cacheTimeout==0 || entry->created>now-cacheTimeout)) { + QByteArray document=entry->document; //copy the cached document, because other threads may destroy the cached entry immediately after mutex unlock. + QByteArray filename=entry->filename; + mutex.unlock(); + qDebug("StaticFileController: Cache hit for %s",path.data()); + setContentType(filename,response); + response.setHeader("Cache-Control","max-age="+QByteArray::number(maxAge/1000)); + response.write(document); + } + else { + mutex.unlock(); + // The file is not in cache. + qDebug("StaticFileController: Cache miss for %s",path.data()); + // Forbid access to files outside the docroot directory + if (path.contains("/..")) { + qWarning("StaticFileController: detected forbidden characters in path %s",path.data()); + response.setStatus(403,"forbidden"); + response.write("403 forbidden",true); + return; + } + // If the filename is a directory, append index.html. + if (QFileInfo(docroot+path).isDir()) { + path+="/index.html"; + } + // Try to open the file + QFile file(docroot+path); + qDebug("StaticFileController: Open file %s",qPrintable(file.fileName())); + if (file.open(QIODevice::ReadOnly)) { + setContentType(path,response); + response.setHeader("Cache-Control","max-age="+QByteArray::number(maxAge/1000)); + if (file.size()<=maxCachedFileSize) { + // Return the file content and store it also in the cache + entry=new CacheEntry(); + while (!file.atEnd() && !file.error()) { + QByteArray buffer=file.read(65536); + response.write(buffer); + entry->document.append(buffer); + } + entry->created=now; + entry->filename=path; + mutex.lock(); + cache.insert(request.getPath(),entry,entry->document.size()); + mutex.unlock(); + } + else { + // Return the file content, do not store in cache + while (!file.atEnd() && !file.error()) { + response.write(file.read(65536)); + } + } + file.close(); + } + else { + if (file.exists()) { + qWarning("StaticFileController: Cannot open existing file %s for reading",qPrintable(file.fileName())); + response.setStatus(403,"forbidden"); + response.write("403 forbidden",true); + } + else { + response.setStatus(404,"not found"); + response.write("404 not found",true); + } + } + } +} + +void StaticFileController::setContentType(QString fileName, HttpResponse& response) const { + if (fileName.endsWith(".png")) { + response.setHeader("Content-Type", "image/png"); + } + else if (fileName.endsWith(".jpg")) { + response.setHeader("Content-Type", "image/jpeg"); + } + else if (fileName.endsWith(".gif")) { + response.setHeader("Content-Type", "image/gif"); + } + else if (fileName.endsWith(".pdf")) { + response.setHeader("Content-Type", "application/pdf"); + } + else if (fileName.endsWith(".txt")) { + response.setHeader("Content-Type", qPrintable("text/plain; charset="+encoding)); + } + else if (fileName.endsWith(".html") || fileName.endsWith(".htm")) { + response.setHeader("Content-Type", qPrintable("text/html; charset="+encoding)); + } + else if (fileName.endsWith(".css")) { + response.setHeader("Content-Type", "text/css"); + } + else if (fileName.endsWith(".js")) { + response.setHeader("Content-Type", "text/javascript"); + } + // Todo: add all of your content types +} diff --git a/httpserver/staticfilecontroller.h b/httpserver/staticfilecontroller.h new file mode 100644 index 000000000..2e5231bb4 --- /dev/null +++ b/httpserver/staticfilecontroller.h @@ -0,0 +1,87 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef STATICFILECONTROLLER_H +#define STATICFILECONTROLLER_H + +#include +#include +#include "httpglobal.h" +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +/** + Delivers static files. It is usually called by the applications main request handler when + the caller request a path that is mapped to static files. +

+ The following settings are required in the config file: +

+  path=../docroot
+  encoding=UTF-8
+  maxAge=60000
+  cacheTime=60000
+  cacheSize=1000000
+  maxCachedFileSize=65536
+  
+ The path is relative to the directory of the config file. In case of windows, if the + settings are in the registry, the path is relative to the current working directory. +

+ The encoding is sent to the web browser in case of text and html files. +

+ The cache improves performance of small files when loaded from a network + drive. Large files are not cached. Files are cached as long as possible, + when cacheTime=0. The maxAge value (in msec!) controls the remote browsers cache. +

+ Do not instantiate this class in each request, because this would make the file cache + useless. Better create one instance during start-up and call it when the application + received a related HTTP request. +*/ + +class DECLSPEC StaticFileController : public HttpRequestHandler { + Q_OBJECT + Q_DISABLE_COPY(StaticFileController) +public: + + /** Constructor */ + StaticFileController(QSettings* settings, QObject* parent = NULL); + + /** Generates the response */ + void service(HttpRequest& request, HttpResponse& response); + +private: + + /** Encoding of text files */ + QString encoding; + + /** Root directory of documents */ + QString docroot; + + /** Maximum age of files in the browser cache */ + int maxAge; + + struct CacheEntry { + QByteArray document; + qint64 created; + QByteArray filename; + }; + + /** Timeout for each cached file */ + int cacheTimeout; + + /** Maximum size of files in cache, larger files are not cached */ + int maxCachedFileSize; + + /** Cache storage */ + QCache cache; + + /** Used to synchronize cache access for threads */ + QMutex mutex; + + /** Set a content-type header in the response depending on the ending of the filename */ + void setContentType(QString file, HttpResponse& response) const; +}; + +#endif // STATICFILECONTROLLER_H diff --git a/src/gcconfig.pri.in b/src/gcconfig.pri.in index 5bdad7802..0dbb75637 100644 --- a/src/gcconfig.pri.in +++ b/src/gcconfig.pri.in @@ -226,6 +226,9 @@ macx { #INCLUDEPATH += /Library/Developer/CommandLineTools/SDKs/MacOSX10.9.sdk/usr/include/ } +# uncomment below for R integration via webservices +#HTPATH = ../httpserver + #if you want a 'robot' to test realtime code without having #to get on your trainer and ride then uncomment below #DEFINES += GC_WANT_ROBOT diff --git a/src/src.pro b/src/src.pro index 46f629da4..e67f1be6d 100644 --- a/src/src.pro +++ b/src/src.pro @@ -270,6 +270,36 @@ DEFINES += GC_HAVE_SOAP HEADERS += ../qtsolutions/qwtcurve/qwt_plot_gapped_curve.h SOURCES += ../qtsolutions/qwtcurve/qwt_plot_gapped_curve.cpp +# web server to provide web-services for external integration with R +!isEmpty ( HTPATH ) { + INCLUDEPATH += $$HTPATH + DEPENDPATH += $$HTPATH + HEADERS += $$HTPATH/httpglobal.h \ + $$HTPATH/httplistener.h \ + $$HTPATH/httpconnectionhandler.h \ + $$HTPATH/httpconnectionhandlerpool.h \ + $$HTPATH/httprequest.h \ + $$HTPATH/httpresponse.h \ + $$HTPATH/httpcookie.h \ + $$HTPATH/httprequesthandler.h \ + $$HTPATH/httpsession.h \ + $$HTPATH/httpsessionstore.h \ + $$HTPATH/staticfilecontroller.h + SOURCES += $$HTPATH/httpglobal.cpp \ + $$HTPATH/httplistener.cpp \ + $$HTPATH/httpconnectionhandler.cpp \ + $$HTPATH/httpconnectionhandlerpool.cpp \ + $$HTPATH/httprequest.cpp \ + $$HTPATH/httpresponse.cpp \ + $$HTPATH/httpcookie.cpp \ + $$HTPATH/httprequesthandler.cpp \ + $$HTPATH/httpsession.cpp \ + $$HTPATH/httpsessionstore.cpp \ + $$HTPATH/staticfilecontroller.cpp +} + +HEADERS += $${LOCALHEADERS} +SOURCE += $${LOCALSOURCE} HEADERS += \ AboutDialog.h \ AddDeviceWizard.h \ @@ -757,5 +787,3 @@ OTHER_FILES += \ web/StreetViewWindow.html \ web/Window.css -HEADERS += $${LOCALHEADERS} -SOURCE += $${LOCALSOURCE}