diff --git a/httpserver/copyright.txt b/httpserver/copyright.txt new file mode 100644 index 000000000..1c5a04719 --- /dev/null +++ b/httpserver/copyright.txt @@ -0,0 +1,11 @@ +This program and its source is distributed under the Lesser General Public License. +http://www.gnu.org/licenses/lgpl.html + +When you modify the library, you have to publish your modified version for free under LGPL license. + +Author and Copyright owner: Stefan Frings +stefan@stefanfrings.de +http://www.stefanfrings.de + +The qtservice module had been originally published by Trolltech under the LGPL license as well, +but is now re-published by Digia under the less restrictive BSD License. diff --git a/httpserver/httpconnectionhandler.cpp b/httpserver/httpconnectionhandler.cpp new file mode 100644 index 000000000..aa60ea0ef --- /dev/null +++ b/httpserver/httpconnectionhandler.cpp @@ -0,0 +1,277 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpconnectionhandler.h" +#include "httpresponse.h" + +using namespace stefanfrings; + +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(); + 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(); + delete socket; + readTimer.stop(); + 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"); + + while(socket->bytesToWrite()) socket->waitForBytesWritten(); + 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"); + while(socket->bytesToWrite()) socket->waitForBytesWritten(); + 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); + + // Copy the Connection:close header to the response + HttpResponse response(socket); + bool closeConnection=QString::compare(currentRequest->getHeader("Connection"),"close",Qt::CaseInsensitive)==0; + if (closeConnection) + { + response.setHeader("Connection","close"); + } + + // In case of HTTP 1.0 protocol add the Connection:close header. + // This ensures that the HttpResponse does not activate chunked mode, which is not spported by HTTP 1.0. + else + { + bool http1_0=QString::compare(currentRequest->getVersion(),"HTTP/1.0",Qt::CaseInsensitive)==0; + if (http1_0) + { + closeConnection=true; + response.setHeader("Connection","close"); + } + } + + // Call the request mapper + 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); + + // Find out whether the connection must be closed + if (!closeConnection) + { + // Maybe the request handler or mapper added a Connection:close header in the meantime + bool closeResponse=QString::compare(response.getHeaders().value("Connection"),"close",Qt::CaseInsensitive)==0; + if (closeResponse==true) + { + closeConnection=true; + } + else + { + // If we have no Content-Length header and did not use chunked mode, then we have to close the + // connection to tell the HTTP client that the end of the response has been reached. + bool hasContentLength=response.getHeaders().contains("Content-Length"); + if (!hasContentLength) + { + bool hasChunkedMode=QString::compare(response.getHeaders().value("Transfer-Encoding"),"chunked",Qt::CaseInsensitive)==0; + if (!hasChunkedMode) + { + closeConnection=true; + } + } + } + } + + // Close the connection or prepare for the next request on the same connection. + if (closeConnection) + { + while(socket->bytesToWrite()) socket->waitForBytesWritten(); + socket->disconnectFromHost(); + } + else + { + // Start timer for next request + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + } + delete currentRequest; + currentRequest=0; + } + } +} diff --git a/httpserver/httpconnectionhandler.h b/httpserver/httpconnectionhandler.h new file mode 100644 index 000000000..c68f4281a --- /dev/null +++ b/httpserver/httpconnectionhandler.h @@ -0,0 +1,124 @@ +/** + @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" + +namespace stefanfrings { + +/** 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(); + +}; + +} // end of namespace + +#endif // HTTPCONNECTIONHANDLER_H diff --git a/httpserver/httpconnectionhandlerpool.cpp b/httpserver/httpconnectionhandlerpool.cpp new file mode 100644 index 000000000..3a3615b56 --- /dev/null +++ b/httpserver/httpconnectionhandlerpool.cpp @@ -0,0 +1,148 @@ +#ifndef QT_NO_OPENSSL + #include + #include + #include + #include +#endif +#include +#include "httpconnectionhandlerpool.h" + +using namespace stefanfrings; + +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) + { + delete handler; + pool.removeOne(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..c6279f1a2 --- /dev/null +++ b/httpserver/httpconnectionhandlerpool.h @@ -0,0 +1,99 @@ +#ifndef HTTPCONNECTIONHANDLERPOOL_H +#define HTTPCONNECTIONHANDLERPOOL_H + +#include +#include +#include +#include +#include "httpglobal.h" +#include "httpconnectionhandler.h" + +namespace stefanfrings { + +/** + Pool of http connection handlers. The size of the pool grows and + shrinks on demand. +

+ Example for the required configuration settings: +

+  minThreads=4
+  maxThreads=100
+  cleanupInterval=60000
+  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(); + +}; + +} // end of namespace + +#endif // HTTPCONNECTIONHANDLERPOOL_H diff --git a/httpserver/httpcookie.cpp b/httpserver/httpcookie.cpp new file mode 100644 index 000000000..f09a2c373 --- /dev/null +++ b/httpserver/httpcookie.cpp @@ -0,0 +1,263 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpcookie.h" + +using namespace stefanfrings; + +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, const bool httpOnly) +{ + this->name=name; + this->value=value; + this->maxAge=maxAge; + this->path=path; + this->comment=comment; + this->domain=domain; + this->secure=secure; + this->httpOnly=httpOnly; + 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=="HttpOnly") + { + httpOnly=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"); + } + if (httpOnly) { + buffer.append("; HttpOnly"); + } + 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; +} + +void HttpCookie::setHttpOnly(const bool httpOnly) +{ + this->httpOnly=httpOnly; +} + +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; +} + +bool HttpCookie::getHttpOnly() const +{ + return httpOnly; +} + +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" + +namespace stefanfrings { + +/** + 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 be sent by the browser to the server only on secure connections + @param httpOnly If true, the browser does not allow client-side scripts to access the cookie + */ + 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, const bool httpOnly=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 cookie will be sent by the browser to the server only on secure connections */ + void setSecure(const bool secure); + + /** Set HTTP-only mode, so that he browser does not allow client-side scripts to access the cookie */ + void setHttpOnly(const bool httpOnly); + + /** 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; + + /** Get 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; + + /** Get the HTTP-only flag of this cookie */ + bool getHttpOnly() const; + + /** Returns always 1 */ + int getVersion() const; + +private: + + QByteArray name; + QByteArray value; + QByteArray comment; + QByteArray domain; + int maxAge; + QByteArray path; + bool secure; + bool httpOnly; + int version; + +}; + +} // end of namespace + +#endif // HTTPCOOKIE_H diff --git a/httpserver/httpglobal.cpp b/httpserver/httpglobal.cpp new file mode 100644 index 000000000..8ccfb02dd --- /dev/null +++ b/httpserver/httpglobal.cpp @@ -0,0 +1,7 @@ +#include "httpglobal.h" + +const char* getQtWebAppLibVersion() +{ + return "1.7.3"; +} + diff --git a/httpserver/httpglobal.h b/httpserver/httpglobal.h new file mode 100644 index 000000000..e7e856a97 --- /dev/null +++ b/httpserver/httpglobal.h @@ -0,0 +1,28 @@ +/** + @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..b07c4b8c7 --- /dev/null +++ b/httpserver/httplistener.cpp @@ -0,0 +1,90 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httplistener.h" +#include "httpconnectionhandler.h" +#include "httpconnectionhandlerpool.h" +#include + +using namespace stefanfrings; + +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 event queue because the handler lives in another thread + QMetaObject::invokeMethod(freeHandler, "handleConnection", Qt::QueuedConnection, Q_ARG(tSocketDescriptor, socketDescriptor)); + } + 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..b1dbfbc9f --- /dev/null +++ b/httpserver/httplistener.h @@ -0,0 +1,102 @@ +/** + @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" + +namespace stefanfrings { + +/** + 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); + +}; + +} // end of namespace + +#endif // HTTPLISTENER_H diff --git a/httpserver/httprequest.cpp b/httpserver/httprequest.cpp new file mode 100644 index 000000000..a6ac16ec7 --- /dev/null +++ b/httpserver/httprequest.cpp @@ -0,0 +1,569 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequest.h" +#include +#include +#include "httpcookie.h" + +using namespace stefanfrings; + +HttpRequest::HttpRequest(QSettings* settings) +{ + status=waitForRequest; + currentSize=0; + expectedBodySize=0; + maxSize=settings->value("maxRequestSize","16000").toInt(); + maxMultiPartSize=settings->value("maxMultiPartSize","1000000").toInt(); + tempFile=NULL; +} + + +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); + peerAddress = socket->peerAddress(); + 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).toLower(); + 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); + if (boundary.startsWith('"') && boundary.endsWith('"')) + { + boundary = boundary.mid(1,boundary.length()-2); + } + } + } + QByteArray contentLength=headers.value("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 + // Create an object for the temporary file, if not already present + if (tempFile == NULL) + { + tempFile = new QTemporaryFile; + } + 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); +} + + +const QByteArray& HttpRequest::getRawPath() const +{ + return path; +} + + +QByteArray HttpRequest::getVersion() const +{ + return version; +} + + +QByteArray HttpRequest::getHeader(const QByteArray& name) const +{ + return headers.value(name.toLower()); +} + +QList HttpRequest::getHeaders(const QByteArray& name) const +{ + return headers.values(name.toLower()); +} + +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); + if (file->isOpen()) + { + file->close(); + } + delete file; + } + if (tempFile != NULL) + { + if (tempFile->isOpen()) + { + tempFile->close(); + } + delete tempFile; + } +} + +QTemporaryFile* HttpRequest::getUploadedFile(const QByteArray fieldName) const +{ + 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; +} + +/** + Get the address of the connected client. + Note that multiple clients may have the same IP address, if they + share an internet connection (which is very common). + */ +QHostAddress HttpRequest::getPeerAddress() const +{ + return peerAddress; +} + diff --git a/httpserver/httprequest.h b/httpserver/httprequest.h new file mode 100644 index 000000000..42ad41984 --- /dev/null +++ b/httpserver/httprequest.h @@ -0,0 +1,239 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUEST_H +#define HTTPREQUEST_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "httpglobal.h" + +namespace stefanfrings { + +/** + 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 HTTP request from a socket. + This method is called by the connection handler 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 raw path of the HTTP request (e.g. "/file%20with%20spaces.html") */ + const QByteArray& getRawPath() 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, not case-senitive. + @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, not case-senitive. + */ + QList getHeaders(const QByteArray& name) const; + + /** + * Get all HTTP request headers. Note that the header names + * are returned in lower-case. + */ + QMultiMap getHeaderMap() const; + + /** + Get the value of a HTTP request parameter. + @param name Name of the parameter, case-sensitive. + @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, case-sensitive. + */ + 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) const; + + /** + Get the value of a cookie. + @param name Name of the cookie + */ + QByteArray getCookie(const QByteArray& name) const; + + /** Get all cookies. */ + QMap& getCookieMap(); + + /** + Get the address of the connected client. + Note that multiple clients may have the same IP address, if they + share an internet connection (which is very common). + */ + QHostAddress getPeerAddress() const; + +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. For the state engine. + @see RequestStatus + */ + RequestStatus status; + + /** Address of the connected peer. */ + QHostAddress peerAddress; + + /** 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; + + /** Parse the 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; + +}; + +} // end of namespace + +#endif // HTTPREQUEST_H diff --git a/httpserver/httprequesthandler.cpp b/httpserver/httprequesthandler.cpp new file mode 100644 index 000000000..f3a5fbe76 --- /dev/null +++ b/httpserver/httprequesthandler.cpp @@ -0,0 +1,23 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequesthandler.h" + +using namespace stefanfrings; + +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..b9c33550e --- /dev/null +++ b/httpserver/httprequesthandler.h @@ -0,0 +1,53 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUESTHANDLER_H +#define HTTPREQUESTHANDLER_H + +#include "httpglobal.h" +#include "httprequest.h" +#include "httpresponse.h" + +namespace stefanfrings { + +/** + 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); + +}; + +} // end of namespace + +#endif // HTTPREQUESTHANDLER_H diff --git a/httpserver/httpresponse.cpp b/httpserver/httpresponse.cpp new file mode 100644 index 000000000..c7b3eec95 --- /dev/null +++ b/httpserver/httpresponse.cpp @@ -0,0 +1,200 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpresponse.h" + +using namespace stefanfrings; + +HttpResponse::HttpResponse(QTcpSocket* socket) +{ + this->socket=socket; + statusCode=200; + statusText="OK"; + sentHeaders=false; + sentLastPart=false; + chunkedMode=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; +} + +int HttpResponse::getStatusCode() const +{ + return this->statusCode; +} + +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) + { + // If the output buffer has become large, then wait until it has been sent. + if (socket->bytesToWrite()>16384) + { + 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); + + // Send HTTP headers, if not already done (that happens only on the first call to write()) + if (sentHeaders==false) + { + // If the whole response is generated with a single call to write(), then we know the total + // size of the response and therefore can set the Content-Length header automatically. + if (lastPart) + { + // Automatically set the Content-Length header + headers.insert("Content-Length",QByteArray::number(data.size())); + } + + // else if we will not close the connection at the end, them we must use the chunked mode. + else + { + QByteArray connectionValue=headers.value("Connection",headers.value("connection")); + bool connectionClose=QString::compare(connectionValue,"close",Qt::CaseInsensitive)==0; + if (!connectionClose) + { + headers.insert("Transfer-Encoding","chunked"); + chunkedMode=true; + } + } + + writeHeaders(); + } + + // Send data + if (data.size()>0) + { + if (chunkedMode) + { + if (data.size()>0) + { + QByteArray size=QByteArray::number(data.size(),16); + writeToSocket(size); + writeToSocket("\r\n"); + writeToSocket(data); + writeToSocket("\r\n"); + } + } + else + { + writeToSocket(data); + } + } + + // Only for the last chunk, send the terminating marker and flush the buffer. + if (lastPart) + { + if (chunkedMode) + { + writeToSocket("0\r\n\r\n"); + } + socket->flush(); + 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); +} + + +void HttpResponse::flush() +{ + socket->flush(); +} + + +bool HttpResponse::isConnected() const +{ + return socket->isOpen(); +} diff --git a/httpserver/httpresponse.h b/httpserver/httpresponse.h new file mode 100644 index 000000000..8aa6524a2 --- /dev/null +++ b/httpserver/httpresponse.h @@ -0,0 +1,163 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPRESPONSE_H +#define HTTPRESPONSE_H + +#include +#include +#include +#include "httpglobal.h" +#include "httpcookie.h" + +namespace stefanfrings { + +/** + This object represents a HTTP response, used to return something to the web client. +

+

+    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);
+  
+

+ 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. + You must call this method before the first write(). + @param name name of the header + @param value value of the header + */ + void setHeader(QByteArray name, QByteArray value); + + /** + Set a HTTP response header. + You must call this method before the first write(). + @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. + You must call this method before the first write(). + */ + void setStatus(int statusCode, QByteArray description=QByteArray()); + + /** Return the status code. */ + int getStatusCode() const; + + /** + Write body data to the socket. +

+ The HTTP status line, headers and cookies are sent automatically before the body. +

+ If the response contains only a single chunk (indicated by lastPart=true), + then a Content-Length header is automatically set. +

+ Chunked mode is automatically selected if there is no Content-Length header + and also no Connection:close header. + @param data Data bytes of the body + @param lastPart Indicates that this is the last chunk of data and flushes the output buffer. + */ + void write(QByteArray data, bool lastPart=false); + + /** + Indicates whether the body has been sent completely (write() has been called with lastPart=true). + */ + bool hasSentLastPart() const; + + /** + Set a cookie. + You must call this method before the first write(). + */ + void setCookie(const HttpCookie& cookie); + + /** + Send a redirect response to the browser. + Cannot be combined with write(). + @param url Destination URL + */ + void redirect(const QByteArray& url); + + /** + * Flush the output buffer (of the underlying socket). + * You normally don't need to call this method because flush is + * automatically called after HttpRequestHandler::service() returns. + */ + void flush(); + + /** + * May be used to check whether the connection to the web client has been lost. + * This might be useful to cancel the generation of large or slow responses. + */ + bool isConnected() const; + +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; + + /** Whether the response is sent in chunked mode */ + bool chunkedMode; + + /** 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(); + +}; + +} // end of namespace + +#endif // HTTPRESPONSE_H diff --git a/httpserver/httpserver.pri b/httpserver/httpserver.pri new file mode 100644 index 000000000..9bfabd24e --- /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/httpserver.pro b/httpserver/httpserver.pro new file mode 100644 index 000000000..33d84ee65 --- /dev/null +++ b/httpserver/httpserver.pro @@ -0,0 +1,45 @@ +#-------------------------------------------------------- +# +# Pro file for Android and Windows builds with Qt Creator +# +#-------------------------------------------------------- + +QT += core network + +TEMPLATE = lib +TARGET = httpserver + +INCLUDEPATH += $$PWD + +CONFIG(Release):build_subdir = release +CONFIG(Debug):build_subdir = debug + +# 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 + \ No newline at end of file diff --git a/httpserver/httpsession.cpp b/httpserver/httpsession.cpp new file mode 100644 index 000000000..7bcac7f01 --- /dev/null +++ b/httpserver/httpsession.cpp @@ -0,0 +1,187 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsession.h" +#include +#include + +using namespace stefanfrings; + +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..e303eb1ef --- /dev/null +++ b/httpserver/httpsession.h @@ -0,0 +1,122 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSION_H +#define HTTPSESSION_H + +#include +#include +#include +#include "httpglobal.h" + +namespace stefanfrings { + +/** + 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; + +}; + +} // end of namespace + +#endif // HTTPSESSION_H diff --git a/httpserver/httpsessionstore.cpp b/httpserver/httpsessionstore.cpp new file mode 100644 index 000000000..79e99c851 --- /dev/null +++ b/httpserver/httpsessionstore.cpp @@ -0,0 +1,127 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsessionstore.h" +#include +#include + +using namespace stefanfrings; + +HttpSessionStore::HttpSessionStore(QSettings* settings, QObject* parent) + :QObject(parent) +{ + this->settings=settings; + connect(&cleanupTimer,SIGNAL(timeout()),this,SLOT(sessionTimerEvent())); + 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::sessionTimerEvent() +{ + 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..1d0d5ca86 --- /dev/null +++ b/httpserver/httpsessionstore.h @@ -0,0 +1,110 @@ +/** + @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" + +namespace stefanfrings { + +/** + 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); + +protected: + /** Storage for the sessions */ + QMap sessions; + +private: + + /** Configuration settings */ + QSettings* settings; + + /** 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 sessionTimerEvent(); +}; + +} // end of namespace + +#endif // HTTPSESSIONSTORE_H diff --git a/httpserver/lgpl-3.0.txt b/httpserver/lgpl-3.0.txt new file mode 100644 index 000000000..65c5ca88a --- /dev/null +++ b/httpserver/lgpl-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/httpserver/readme.md b/httpserver/readme.md new file mode 100644 index 000000000..a1522ecde --- /dev/null +++ b/httpserver/readme.md @@ -0,0 +1,12 @@ +## QtWebApp httpserver ## + +This is the httpserver part of QtWebApp from Stefan Frings + + - [Link to the main page](http://stefanfrings.de/qtwebapp/index-en.html) + - [Link to API documentation](http://stefanfrings.de/qtwebapp/api/index.html) + +Files copied over from the original 'doc' folder: + + - copyright.txt + - lgpl-3.0.txt + - releasenotes.txts diff --git a/httpserver/releasenotes.txt b/httpserver/releasenotes.txt new file mode 100644 index 000000000..43e1da05d --- /dev/null +++ b/httpserver/releasenotes.txt @@ -0,0 +1,250 @@ +Dont forget to update the release number also in +QtWebApp.pro and httpserver/httpglobal.cpp. + +1.7.3 +25.04.2017 +Wait until all data are sent before closing connections. + +1.7.2 +17.01.2017 +Fixed compile error with MSVC. + +1.7.1 +10.11.2016 +Fixed a possible memory leak in case of broken Multipart HTTP Requests. + +1.7.0 +08.11.2016 +Introduced namespace "stefanfrings". +Improved performance a little. + +1.6.7 +10.10.2016 +Fix type of socketDescriptor in qtservice library. +Add support for INFO log messages (new since QT 5.5). +Improve indentation of log messages. + +1.6.6 +25.07.2016 +Removed useless mutex from TemplateLoader. +Add mutex to TemplateCache (which is now needed). + +1.6.5 +10.06.2016 +Incoming HTTP request headers are now processed case-insensitive. +Add support for the HttpOnly flag of cookies. + +1.6.4 +27.03.2016 +Fixed constructor of Template class did not load the source file properly. +Template loader and cache were not affected. + +1.6.3 +11.03.2016 +Fixed compilation error. +Added missing implementation of HttpRequest::getPeerAddress(). + +1.6.2 +06.03.2016 +Added mime types for some file extensions. + +1.6.1 +25.01.2016 +Fixed parser of boundary value in multi-part request, which caused that +QHttpMultipart did not work on client side. + +1.6.0 +29.12.2015 +Much better output buffering, reduces the number of small IP packages. + +1.5.13 +29.12.2015 +Improved performance a little. +Add support for old HTTP 1.0 clients. +Add HttpResposne::flush() and HttpResponse::isConnected() which are helpful to support +SSE from HTML 5 specification. + +1.5.12 +11.12.2015 +Fix program crash when using SSL with a variable sized thread pool on Windows. +Changed name of HttpSessionStore::timerEvent() to fix compiler warnings since Qt 5.0. +Add HttpRequest::getRawPath(). +HttpSessionStore::sessions is now protected. + +1.5.11 +21.11.2015 +Fix project file for Mac OS. +Add HttpRequest::getPeerAddress() and HttpResponse::getStatusCode(). + +1.5.10 +01.09.2015 +Modified StaticFileController to support ressource files (path starting with ":/" or "qrc://"). + +1.5.9 +06.08.2015 +New HttpListener::listen() method, to restart listening after close. +Add missing include for QObject in logger.h. +Add a call to flush() before closing connections, which solves an issue with nginx. + +1.5.8 +26.07.2015 +Fixed segmentation fault error when closing the application while a HTTP request is in progress. +New HttpListener::close() method to simplifly proper shutdown. + +1.5.7 +20.07.2015 +Fix Qt 5.5 compatibility issue. + +1.5.6 +22.06.2015 +Fixed compilation failes if QT does not support SSL. + +1.5.5 +16.06.2015 +Improved performance of SSL connections. + +1.5.4 +15.06.2015 +Support for Qt versions without OpenSsl. + +1.5.3 +22.05.2015 +Fixed Windows issue: QsslSocket cannot be closed from other threads than it was created in. + +1.5.2 +12.05.2015 +Fixed Windows issue: QSslSocket cannot send signals to another thread than it was created in. + +1.5.1 +14.04.2015 +Add support for pipelining. + +1.5.0 +03.04.2015 +Add support for HTTPS. + +1.4.2 +03.04.2015 +Fixed HTTP request did not work if it was split into multipe IP packages. + +1.4.1 +20.03.2015 +Fixed session cookie expires while the user is active, expiration time was not prolonged on each request. + +1.4.0 +14.03.2015 +This release has a new directory structure and new project files to support the creation of a shared library (*.dll or *.so). + +1.3.8 +12.03.2015 +Improved shutdown procedure. +New config setting "host" which binds the listener to a specific network interface. + +1.3.7 +14.01.2015 +Fixed setting maxMultiPartSize worked only with file-uploads but not with form-data. + +1.3.6 +16.09.2014 +Fixed DualFileLogger produces no output. + +1.3.5 +11.06.2014 +Fixed a multi-threading issue with race condition in StaticFileController. + +1.3.4 +04.06.2014 +Fixed wrong content type when the StaticFileController returns a cached index.html. + +1.3.3 +17.03.2014 +Improved security of StaticFileController by denying "/.." in any position of the request path. +Improved performance of StaticFileController a little. +New convenience method HttpResponse::redirect(url). +Fixed a missing return statement in StaticFileController. + +1.3.2 +08.01.2014 +Fixed HTTP Server ignoring URL parameters when the request contains POST parameters. + +1.3.1 +15.08.2013 +Fixed HTTP server not accepting connections on 64bit OS with QT 5. + +1.3.0 +20.04.2013 +Updated for compatibility QT 5. You may still use QT 4.7 or 4.8, if you like. +Also added support for logging source file name, line number and function name. + +1.2.13 +03.03.2013 +Fixed Logger writing wrong timestamp for buffered messages. +Improved shutdown procedure. The webserver now processes all final signals before the destructor finishes. + +1.2.12 +01.03.2013 +Fixed HttpResponse sending first part of data repeatedly when the amount of data is larger than the available memory for I/O buffer. + +1.2.11 +06.01.2013 +Added clearing the write buffer when accepting a new connection, so that it does not send remaining data from an aborted previous connection (which is possibly a bug in QT). + +1.2.10 +18.12.2012 +Reduced memory usage of HttpResponse in case of large response. + +1.2.9 +29.07.2012 +Added a mutex to HttpConnectionHandlerPool to fix a concurrency issue when a pooled object gets taken from the cache while it times out. +Modified HttpConnectionHandler so that it does not throw an exception anymore when a connection gets closed by the peer in the middle of a read. + +1.2.8 +22.07.2012 +Fixed a possible concurrency issue when the file cache is so small that it stores less files than the number of threads. + +1.2.7 +18.07.2012 +Fixed HttpRequest ignores additional URL parameters of POST requests. +Fixed HttpRequest ignores POST parameters of body if there is no Content-Type header. +Removed unused tempdir variable from HttpRequest. +Added mutex to cache of StaticFileController to prevent concurrency problems. +Removed HTTP response with status 408 after read timeout. Connection gets simply closed now. + +1.2.6 +29.06.2012 +Fixed a compilation error on 64 bit if super verbose debugging is enabled. +Fixed a typo in static file controller related to the document type header. + +1.2.5 +27.06.2012 +Fixed error message "QThread: Destroyed while thread is still running" during program termination. + +1.2.4 +02.06.2012 +Fixed template engine skipping variable tokens when a value is shorter than the token. + +1.2.3 +26.12.2011 +Fixed null pointer error when the HTTP server aborts a request that is too large. + +1.2.2 +06.11.2011 +Fixed compilation error on 64 bit platforms. + +1.2.1 +22.10.2011 +Fixed a multi-threading bug in HttpConnectionHandler. + +1.2.0 +05.12.2010 +Added a controller that serves static files, with cacheing. + + +1.1.0 +19.10.2010 +Added support for sessions. +Separated the base classes into individual libraries. + +1.0.0 +17.10.2010 +First release diff --git a/httpserver/staticfilecontroller.cpp b/httpserver/staticfilecontroller.cpp new file mode 100644 index 000000000..e6195766d --- /dev/null +++ b/httpserver/staticfilecontroller.cpp @@ -0,0 +1,187 @@ +/** + @file + @author Stefan Frings +*/ + +#include "staticfilecontroller.h" +#include +#include +#include + +using namespace stefanfrings; + +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"); + } + else if (fileName.endsWith(".svg")) + { + response.setHeader("Content-Type", "image/svg+xml"); + } + else if (fileName.endsWith(".woff")) + { + response.setHeader("Content-Type", "font/woff"); + } + else if (fileName.endsWith(".woff2")) + { + response.setHeader("Content-Type", "font/woff2"); + } + else if (fileName.endsWith(".ttf")) + { + response.setHeader("Content-Type", "application/x-font-ttf"); + } + else if (fileName.endsWith(".eot")) + { + response.setHeader("Content-Type", "application/vnd.ms-fontobject"); + } + else if (fileName.endsWith(".otf")) + { + response.setHeader("Content-Type", "application/font-otf"); + } + // Todo: add all of your content types + else + { + qDebug("StaticFileController: unknown MIME type for filename '%s'", qPrintable(fileName)); + } +} diff --git a/httpserver/staticfilecontroller.h b/httpserver/staticfilecontroller.h new file mode 100644 index 000000000..6b2bd1c7a --- /dev/null +++ b/httpserver/staticfilecontroller.h @@ -0,0 +1,91 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef STATICFILECONTROLLER_H +#define STATICFILECONTROLLER_H + +#include +#include +#include "httpglobal.h" +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +namespace stefanfrings { + +/** + Delivers static files. It is usually called by the applications main request handler when + the caller requests 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; +}; + +} // end of namespace + +#endif // STATICFILECONTROLLER_H diff --git a/sdrangel.windows.pro b/sdrangel.windows.pro index 0e41e3d60..499e74d95 100644 --- a/sdrangel.windows.pro +++ b/sdrangel.windows.pro @@ -7,6 +7,7 @@ TEMPLATE = subdirs SUBDIRS = sdrbase CONFIG(MINGW64)SUBDIRS += nanomsg +SUBDIRS += httpserver SUBDIRS += fcdhid SUBDIRS += fcdlib SUBDIRS += librtlsdr