mirror of
https://github.com/f4exb/sdrangel.git
synced 2024-11-25 01:18:38 -05:00
Added Stefan Frings' QtWebApp httpserver
This commit is contained in:
parent
9a1b60c6ec
commit
4adf80abc3
11
httpserver/copyright.txt
Normal file
11
httpserver/copyright.txt
Normal file
@ -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.
|
277
httpserver/httpconnectionhandler.cpp
Normal file
277
httpserver/httpconnectionhandler.cpp
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
124
httpserver/httpconnectionhandler.h
Normal file
124
httpserver/httpconnectionhandler.h
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPCONNECTIONHANDLER_H
|
||||
#define HTTPCONNECTIONHANDLER_H
|
||||
|
||||
#ifndef QT_NO_OPENSSL
|
||||
#include <QSslConfiguration>
|
||||
#endif
|
||||
#include <QTcpSocket>
|
||||
#include <QSettings>
|
||||
#include <QTimer>
|
||||
#include <QThread>
|
||||
#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.
|
||||
<p>
|
||||
Example for the required configuration settings:
|
||||
<code><pre>
|
||||
readTimeout=60000
|
||||
maxRequestSize=16000
|
||||
maxMultiPartSize=1000000
|
||||
</pre></code>
|
||||
<p>
|
||||
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
|
148
httpserver/httpconnectionhandlerpool.cpp
Normal file
148
httpserver/httpconnectionhandlerpool.cpp
Normal file
@ -0,0 +1,148 @@
|
||||
#ifndef QT_NO_OPENSSL
|
||||
#include <QSslSocket>
|
||||
#include <QSslKey>
|
||||
#include <QSslCertificate>
|
||||
#include <QSslConfiguration>
|
||||
#endif
|
||||
#include <QDir>
|
||||
#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()<maxConnectionHandlers)
|
||||
{
|
||||
freeHandler=new HttpConnectionHandler(settings,requestHandler,sslConfiguration);
|
||||
freeHandler->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
|
||||
}
|
||||
}
|
99
httpserver/httpconnectionhandlerpool.h
Normal file
99
httpserver/httpconnectionhandlerpool.h
Normal file
@ -0,0 +1,99 @@
|
||||
#ifndef HTTPCONNECTIONHANDLERPOOL_H
|
||||
#define HTTPCONNECTIONHANDLERPOOL_H
|
||||
|
||||
#include <QList>
|
||||
#include <QTimer>
|
||||
#include <QObject>
|
||||
#include <QMutex>
|
||||
#include "httpglobal.h"
|
||||
#include "httpconnectionhandler.h"
|
||||
|
||||
namespace stefanfrings {
|
||||
|
||||
/**
|
||||
Pool of http connection handlers. The size of the pool grows and
|
||||
shrinks on demand.
|
||||
<p>
|
||||
Example for the required configuration settings:
|
||||
<code><pre>
|
||||
minThreads=4
|
||||
maxThreads=100
|
||||
cleanupInterval=60000
|
||||
readTimeout=60000
|
||||
;sslKeyFile=ssl/my.key
|
||||
;sslCertFile=ssl/my.cert
|
||||
maxRequestSize=16000
|
||||
maxMultiPartSize=1000000
|
||||
</pre></code>
|
||||
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.
|
||||
<p>
|
||||
For SSL support, you need an OpenSSL certificate file and a key file.
|
||||
Both can be created with the command
|
||||
<code><pre>
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout my.key -out my.cert
|
||||
</pre></code>
|
||||
<p>
|
||||
Visit http://slproweb.com/products/Win32OpenSSL.html to download the Light version of OpenSSL for Windows.
|
||||
<p>
|
||||
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<HttpConnectionHandler*> 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
|
263
httpserver/httpcookie.cpp
Normal file
263
httpserver/httpcookie.cpp
Normal file
@ -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<QByteArray> 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<QByteArray> HttpCookie::splitCSV(const QByteArray source)
|
||||
{
|
||||
bool inString=false;
|
||||
QList<QByteArray> list;
|
||||
QByteArray buffer;
|
||||
for (int i=0; i<source.size(); ++i)
|
||||
{
|
||||
char c=source.at(i);
|
||||
if (inString==false)
|
||||
{
|
||||
if (c=='\"')
|
||||
{
|
||||
inString=true;
|
||||
}
|
||||
else if (c==';')
|
||||
{
|
||||
QByteArray trimmed=buffer.trimmed();
|
||||
if (!trimmed.isEmpty())
|
||||
{
|
||||
list.append(trimmed);
|
||||
}
|
||||
buffer.clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
buffer.append(c);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (c=='\"')
|
||||
{
|
||||
inString=false;
|
||||
}
|
||||
else {
|
||||
buffer.append(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
QByteArray trimmed=buffer.trimmed();
|
||||
if (!trimmed.isEmpty())
|
||||
{
|
||||
list.append(trimmed);
|
||||
}
|
||||
return list;
|
||||
}
|
123
httpserver/httpcookie.h
Normal file
123
httpserver/httpcookie.h
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPCOOKIE_H
|
||||
#define HTTPCOOKIE_H
|
||||
|
||||
#include <QList>
|
||||
#include <QByteArray>
|
||||
#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<QByteArray> 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
|
7
httpserver/httpglobal.cpp
Normal file
7
httpserver/httpglobal.cpp
Normal file
@ -0,0 +1,7 @@
|
||||
#include "httpglobal.h"
|
||||
|
||||
const char* getQtWebAppLibVersion()
|
||||
{
|
||||
return "1.7.3";
|
||||
}
|
||||
|
28
httpserver/httpglobal.h
Normal file
28
httpserver/httpglobal.h
Normal file
@ -0,0 +1,28 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPGLOBAL_H
|
||||
#define HTTPGLOBAL_H
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
// 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
|
||||
|
90
httpserver/httplistener.cpp
Normal file
90
httpserver/httplistener.cpp
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#include "httplistener.h"
|
||||
#include "httpconnectionhandler.h"
|
||||
#include "httpconnectionhandlerpool.h"
|
||||
#include <QCoreApplication>
|
||||
|
||||
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>("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();
|
||||
}
|
||||
}
|
102
httpserver/httplistener.h
Normal file
102
httpserver/httplistener.h
Normal file
@ -0,0 +1,102 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPLISTENER_H
|
||||
#define HTTPLISTENER_H
|
||||
|
||||
#include <QTcpServer>
|
||||
#include <QSettings>
|
||||
#include <QBasicTimer>
|
||||
#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).
|
||||
<p>
|
||||
Example for the required settings in the config file:
|
||||
<code><pre>
|
||||
;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
|
||||
</pre></code>
|
||||
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
|
569
httpserver/httprequest.cpp
Normal file
569
httpserver/httprequest.cpp
Normal file
@ -0,0 +1,569 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#include "httprequest.h"
|
||||
#include <QList>
|
||||
#include <QDir>
|
||||
#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<QByteArray> 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<QByteArray> 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<QByteArray> 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<QByteArray> HttpRequest::getHeaders(const QByteArray& name) const
|
||||
{
|
||||
return headers.values(name.toLower());
|
||||
}
|
||||
|
||||
QMultiMap<QByteArray,QByteArray> HttpRequest::getHeaderMap() const
|
||||
{
|
||||
return headers;
|
||||
}
|
||||
|
||||
QByteArray HttpRequest::getParameter(const QByteArray& name) const
|
||||
{
|
||||
return parameters.value(name);
|
||||
}
|
||||
|
||||
QList<QByteArray> HttpRequest::getParameters(const QByteArray& name) const
|
||||
{
|
||||
return parameters.values(name);
|
||||
}
|
||||
|
||||
QMultiMap<QByteArray,QByteArray> 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<QByteArray,QByteArray>& 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;
|
||||
}
|
||||
|
239
httpserver/httprequest.h
Normal file
239
httpserver/httprequest.h
Normal file
@ -0,0 +1,239 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPREQUEST_H
|
||||
#define HTTPREQUEST_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QHostAddress>
|
||||
#include <QTcpSocket>
|
||||
#include <QMap>
|
||||
#include <QMultiMap>
|
||||
#include <QSettings>
|
||||
#include <QTemporaryFile>
|
||||
#include <QUuid>
|
||||
#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.
|
||||
<p>
|
||||
The follwing config settings are required:
|
||||
<code><pre>
|
||||
maxRequestSize=16000
|
||||
maxMultiPartSize=1000000
|
||||
</pre></code>
|
||||
<p>
|
||||
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<QByteArray> getHeaders(const QByteArray& name) const;
|
||||
|
||||
/**
|
||||
* Get all HTTP request headers. Note that the header names
|
||||
* are returned in lower-case.
|
||||
*/
|
||||
QMultiMap<QByteArray,QByteArray> 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<QByteArray> getParameters(const QByteArray& name) const;
|
||||
|
||||
/** Get all HTTP request parameters. */
|
||||
QMultiMap<QByteArray,QByteArray> 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).
|
||||
<p>
|
||||
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<QByteArray,QByteArray>& 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<QByteArray,QByteArray> headers;
|
||||
|
||||
/** Parameters of the request */
|
||||
QMultiMap<QByteArray,QByteArray> parameters;
|
||||
|
||||
/** Uploaded files of the request, key is the field name. */
|
||||
QMap<QByteArray,QTemporaryFile*> uploadedFiles;
|
||||
|
||||
/** Received cookies */
|
||||
QMap<QByteArray,QByteArray> 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
|
23
httpserver/httprequesthandler.cpp
Normal file
23
httpserver/httprequesthandler.cpp
Normal file
@ -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);
|
||||
}
|
53
httpserver/httprequesthandler.h
Normal file
53
httpserver/httprequesthandler.h
Normal file
@ -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.
|
||||
<p>
|
||||
You need to override the service() method or you will always get an HTTP error 501.
|
||||
<p>
|
||||
@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
|
200
httpserver/httpresponse.cpp
Normal file
200
httpserver/httpresponse.cpp
Normal file
@ -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<QByteArray,QByteArray>& 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<QByteArray,HttpCookie>& 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();
|
||||
}
|
163
httpserver/httpresponse.h
Normal file
163
httpserver/httpresponse.h
Normal file
@ -0,0 +1,163 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPRESPONSE_H
|
||||
#define HTTPRESPONSE_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QString>
|
||||
#include <QTcpSocket>
|
||||
#include "httpglobal.h"
|
||||
#include "httpcookie.h"
|
||||
|
||||
namespace stefanfrings {
|
||||
|
||||
/**
|
||||
This object represents a HTTP response, used to return something to the web client.
|
||||
<p>
|
||||
<code><pre>
|
||||
response.setStatus(200,"OK"); // optional, because this is the default
|
||||
response.writeBody("Hello");
|
||||
response.writeBody("World!",true);
|
||||
</pre></code>
|
||||
<p>
|
||||
Example how to return an error:
|
||||
<code><pre>
|
||||
response.setStatus(500,"server error");
|
||||
response.write("The request cannot be processed because the servers is broken",true);
|
||||
</pre></code>
|
||||
<p>
|
||||
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<QByteArray,QByteArray>& getHeaders();
|
||||
|
||||
/** Get the map of cookies */
|
||||
QMap<QByteArray,HttpCookie>& 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.
|
||||
<p>
|
||||
The HTTP status line, headers and cookies are sent automatically before the body.
|
||||
<p>
|
||||
If the response contains only a single chunk (indicated by lastPart=true),
|
||||
then a Content-Length header is automatically set.
|
||||
<p>
|
||||
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<QByteArray,QByteArray> 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<QByteArray,HttpCookie> 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
|
33
httpserver/httpserver.pri
Normal file
33
httpserver/httpserver.pri
Normal file
@ -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
|
45
httpserver/httpserver.pro
Normal file
45
httpserver/httpserver.pro
Normal file
@ -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
|
||||
|
187
httpserver/httpsession.cpp
Normal file
187
httpserver/httpsession.cpp
Normal file
@ -0,0 +1,187 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#include "httpsession.h"
|
||||
#include <QDateTime>
|
||||
#include <QUuid>
|
||||
|
||||
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<QByteArray,QVariant> HttpSession::getAll() const
|
||||
{
|
||||
QMap<QByteArray,QVariant> 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();
|
||||
}
|
||||
}
|
122
httpserver/httpsession.h
Normal file
122
httpserver/httpsession.h
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPSESSION_H
|
||||
#define HTTPSESSION_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QVariant>
|
||||
#include <QReadWriteLock>
|
||||
#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<QByteArray,QVariant> 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<QByteArray,QVariant> values;
|
||||
|
||||
};
|
||||
|
||||
/** Pointer to the shared data. */
|
||||
HttpSessionData* dataPtr;
|
||||
|
||||
};
|
||||
|
||||
} // end of namespace
|
||||
|
||||
#endif // HTTPSESSION_H
|
127
httpserver/httpsessionstore.cpp
Normal file
127
httpserver/httpsessionstore.cpp
Normal file
@ -0,0 +1,127 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#include "httpsessionstore.h"
|
||||
#include <QDateTime>
|
||||
#include <QUuid>
|
||||
|
||||
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<QByteArray,HttpSession>::iterator i = sessions.begin();
|
||||
while (i != sessions.end())
|
||||
{
|
||||
QMap<QByteArray,HttpSession>::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();
|
||||
}
|
110
httpserver/httpsessionstore.h
Normal file
110
httpserver/httpsessionstore.h
Normal file
@ -0,0 +1,110 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef HTTPSESSIONSTORE_H
|
||||
#define HTTPSESSIONSTORE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QTimer>
|
||||
#include <QMutex>
|
||||
#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:
|
||||
<code><pre>
|
||||
expirationTime=3600000
|
||||
cookieName=sessionid
|
||||
</pre></code>
|
||||
The following additional configurations settings are optionally:
|
||||
<code><pre>
|
||||
cookiePath=/
|
||||
cookieComment=Session ID
|
||||
;cookieDomain=stefanfrings.de
|
||||
</pre></code>
|
||||
*/
|
||||
|
||||
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<QByteArray,HttpSession> 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
|
165
httpserver/lgpl-3.0.txt
Normal file
165
httpserver/lgpl-3.0.txt
Normal file
@ -0,0 +1,165 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
12
httpserver/readme.md
Normal file
12
httpserver/readme.md
Normal file
@ -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
|
250
httpserver/releasenotes.txt
Normal file
250
httpserver/releasenotes.txt
Normal file
@ -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
|
187
httpserver/staticfilecontroller.cpp
Normal file
187
httpserver/staticfilecontroller.cpp
Normal file
@ -0,0 +1,187 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#include "staticfilecontroller.h"
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QDateTime>
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
91
httpserver/staticfilecontroller.h
Normal file
91
httpserver/staticfilecontroller.h
Normal file
@ -0,0 +1,91 @@
|
||||
/**
|
||||
@file
|
||||
@author Stefan Frings
|
||||
*/
|
||||
|
||||
#ifndef STATICFILECONTROLLER_H
|
||||
#define STATICFILECONTROLLER_H
|
||||
|
||||
#include <QCache>
|
||||
#include <QMutex>
|
||||
#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.
|
||||
<p>
|
||||
The following settings are required in the config file:
|
||||
<code><pre>
|
||||
path=../docroot
|
||||
encoding=UTF-8
|
||||
maxAge=60000
|
||||
cacheTime=60000
|
||||
cacheSize=1000000
|
||||
maxCachedFileSize=65536
|
||||
</pre></code>
|
||||
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.
|
||||
<p>
|
||||
The encoding is sent to the web browser in case of text and html files.
|
||||
<p>
|
||||
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.
|
||||
<p>
|
||||
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<QString,CacheEntry> 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
|
@ -7,6 +7,7 @@
|
||||
TEMPLATE = subdirs
|
||||
SUBDIRS = sdrbase
|
||||
CONFIG(MINGW64)SUBDIRS += nanomsg
|
||||
SUBDIRS += httpserver
|
||||
SUBDIRS += fcdhid
|
||||
SUBDIRS += fcdlib
|
||||
SUBDIRS += librtlsdr
|
||||
|
Loading…
Reference in New Issue
Block a user