This commit is contained in:
Luis Ángel San Martín
2017-08-26 16:54:10 +02:00
parent 8b20014cc8
commit f149f314e0
105 changed files with 4145 additions and 2089 deletions

View File

@ -0,0 +1,275 @@
/**
@file
@author Stefan Frings
*/
#include "httpconnectionhandler.h"
#include "httpresponse.h"
HttpConnectionHandler::HttpConnectionHandler(QSettings* settings, HttpRequestHandler* requestHandler, QSslConfiguration* sslConfiguration)
: QThread()
{
Q_ASSERT(settings!=0);
Q_ASSERT(requestHandler!=0);
this->settings=settings;
this->requestHandler=requestHandler;
this->sslConfiguration=sslConfiguration;
currentRequest=0;
busy=false;
// Create TCP or SSL socket
createSocket();
// execute signals in my own thread
moveToThread(this);
socket->moveToThread(this);
readTimer.moveToThread(this);
// Connect signals
connect(socket, SIGNAL(readyRead()), SLOT(read()));
connect(socket, SIGNAL(disconnected()), SLOT(disconnected()));
connect(&readTimer, SIGNAL(timeout()), SLOT(readTimeout()));
readTimer.setSingleShot(true);
qDebug("HttpConnectionHandler (%p): constructed", this);
this->start();
}
HttpConnectionHandler::~HttpConnectionHandler()
{
quit();
wait();
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");
socket->flush();
socket->disconnectFromHost();
delete currentRequest;
currentRequest=0;
}
void HttpConnectionHandler::disconnected()
{
qDebug("HttpConnectionHandler (%p): disconnected", this);
socket->close();
readTimer.stop();
busy = false;
}
void HttpConnectionHandler::read()
{
// The loop adds support for HTTP pipelinig
while (socket->bytesAvailable())
{
#ifdef SUPERVERBOSE
qDebug("HttpConnectionHandler (%p): read input",this);
#endif
// Create new HttpRequest object if necessary
if (!currentRequest)
{
currentRequest=new HttpRequest(settings);
}
// Collect data for the request object
while (socket->bytesAvailable() && currentRequest->getStatus()!=HttpRequest::complete && currentRequest->getStatus()!=HttpRequest::abort)
{
currentRequest->readFromSocket(socket);
if (currentRequest->getStatus()==HttpRequest::waitForBody)
{
// Restart timer for read timeout, otherwise it would
// expire during large file uploads.
int readTimeout=settings->value("readTimeout",10000).toInt();
readTimer.start(readTimeout);
}
}
// If the request is aborted, return error message and close the connection
if (currentRequest->getStatus()==HttpRequest::abort)
{
socket->write("HTTP/1.1 413 entity too large\r\nConnection: close\r\n\r\n413 Entity too large\r\n");
socket->flush();
socket->disconnectFromHost();
delete currentRequest;
currentRequest=0;
return;
}
// If the request is complete, let the request mapper dispatch it
if (currentRequest->getStatus()==HttpRequest::complete)
{
readTimer.stop();
qDebug("HttpConnectionHandler (%p): received request",this);
// 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)
{
socket->flush();
socket->disconnectFromHost();
}
else
{
// Start timer for next request
int readTimeout=settings->value("readTimeout",10000).toInt();
readTimer.start(readTimeout);
}
delete currentRequest;
currentRequest=0;
}
}
}

View File

@ -0,0 +1,120 @@
/**
@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"
/** 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();
};
#endif // HTTPCONNECTIONHANDLER_H

View File

@ -0,0 +1,146 @@
#ifndef QT_NO_OPENSSL
#include <QSslSocket>
#include <QSslKey>
#include <QSslCertificate>
#include <QSslConfiguration>
#endif
#include <QDir>
#include "httpconnectionhandlerpool.h"
HttpConnectionHandlerPool::HttpConnectionHandlerPool(QSettings* settings, HttpRequestHandler* requestHandler)
: QObject()
{
Q_ASSERT(settings!=0);
this->settings=settings;
this->requestHandler=requestHandler;
this->sslConfiguration=NULL;
loadSslConfig();
cleanupTimer.start(settings->value("cleanupInterval",1000).toInt());
connect(&cleanupTimer, SIGNAL(timeout()), SLOT(cleanup()));
}
HttpConnectionHandlerPool::~HttpConnectionHandlerPool()
{
// delete all connection handlers and wait until their threads are closed
foreach(HttpConnectionHandler* handler, pool)
{
delete handler;
}
delete sslConfiguration;
qDebug("HttpConnectionHandlerPool (%p): destroyed", this);
}
HttpConnectionHandler* HttpConnectionHandlerPool::getConnectionHandler()
{
HttpConnectionHandler* freeHandler=0;
mutex.lock();
// find a free handler in pool
foreach(HttpConnectionHandler* handler, pool)
{
if (!handler->isBusy())
{
freeHandler=handler;
freeHandler->setBusy();
break;
}
}
// create a new handler, if necessary
if (!freeHandler)
{
int maxConnectionHandlers=settings->value("maxThreads",100).toInt();
if (pool.count()<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
}
}

View File

@ -0,0 +1,95 @@
#ifndef HTTPCONNECTIONHANDLERPOOL_H
#define HTTPCONNECTIONHANDLERPOOL_H
#include <QList>
#include <QTimer>
#include <QObject>
#include <QMutex>
#include "httpglobal.h"
#include "httpconnectionhandler.h"
/**
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();
};
#endif // HTTPCONNECTIONHANDLERPOOL_H

View File

@ -0,0 +1,261 @@
/**
@file
@author Stefan Frings
*/
#include "httpcookie.h"
HttpCookie::HttpCookie()
{
version=1;
maxAge=0;
secure=false;
}
HttpCookie::HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path, const QByteArray comment, const QByteArray domain, const bool secure, 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;
}

View File

@ -0,0 +1,119 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPCOOKIE_H
#define HTTPCOOKIE_H
#include <QList>
#include <QByteArray>
#include "httpglobal.h"
/**
HTTP cookie as defined in RFC 2109. This class can also parse
RFC 2965 cookies, but skips fields that are not defined in RFC
2109.
*/
class DECLSPEC HttpCookie
{
public:
/** Creates an empty cookie */
HttpCookie();
/**
Create a cookie and set name/value pair.
@param name name of the cookie
@param value value of the cookie
@param maxAge maximum age of the cookie in seconds. 0=discard immediately
@param path Path for that the cookie will be sent, default="/" which means the whole domain
@param comment Optional comment, may be displayed by the web browser somewhere
@param domain Optional domain for that the cookie will be sent. Defaults to the current domain
@param secure If true, the cookie will 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 secure 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;
/** Set the maximum age of this cookie in seconds. */
int getMaxAge() const;
/** Set the path of this cookie */
QByteArray getPath() const;
/** Get the secure flag of this cookie */
bool getSecure() const;
/** Get the httpOnly 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;
};
#endif // HTTPCOOKIE_H

View File

@ -0,0 +1,7 @@
#include "httpglobal.h"
const char* getQtWebAppLibVersion()
{
return "1.6.5";
}

View File

@ -0,0 +1,27 @@
/**
@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

View File

@ -0,0 +1,101 @@
/**
@file
@author Stefan Frings
*/
#include "httplistener.h"
#include "httpconnectionhandler.h"
#include "httpconnectionhandlerpool.h"
#include <QCoreApplication>
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);
//YACReader---------------------------------------------
//try to listen even if the default port is no available
int i = 0;
while (!isListening() && i < 1000) {
QTcpServer::listen(QHostAddress::Any, (rand() % 45535)+20000);
i++;
}
//------------------------------------------------------
if (!isListening())
{
qDebug("HttpListener: Cannot bind on port %i: %s",port,qPrintable(errorString()));
}
else {
qDebug("HttpListener: Listening on port %i",port);
}
}
void HttpListener::close() {
QTcpServer::close();
qDebug("HttpListener: closed");
if (pool) {
delete pool;
pool=NULL;
}
}
void HttpListener::incomingConnection(tSocketDescriptor socketDescriptor) {
#ifdef SUPERVERBOSE
qDebug("HttpListener: New connection");
#endif
HttpConnectionHandler* freeHandler=NULL;
if (pool)
{
freeHandler=pool->getConnectionHandler();
}
// Let the handler process the new connection.
if (freeHandler)
{
// The descriptor is passed via signal/slot because the handler lives in another
// thread and cannot open the socket when directly called by another thread.
connect(this,SIGNAL(handleConnection(tSocketDescriptor)),freeHandler,SLOT(handleConnection(tSocketDescriptor)));
emit handleConnection(socketDescriptor);
disconnect(this,SIGNAL(handleConnection(tSocketDescriptor)),freeHandler,SLOT(handleConnection(tSocketDescriptor)));
}
else
{
// Reject the connection
qDebug("HttpListener: Too many incoming connections");
QTcpSocket* socket=new QTcpSocket(this);
socket->setSocketDescriptor(socketDescriptor);
connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater()));
socket->write("HTTP/1.1 503 too many connections\r\nConnection: close\r\n\r\nToo many connections\r\n");
socket->disconnectFromHost();
}
}

View File

@ -0,0 +1,98 @@
/**
@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"
/**
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);
};
#endif // HTTPLISTENER_H

View File

@ -0,0 +1,549 @@
/**
@file
@author Stefan Frings
*/
#include "httprequest.h"
#include <QList>
#include <QDir>
#include "httpcookie.h"
HttpRequest::HttpRequest(QSettings* settings)
{
status=waitForRequest;
currentSize=0;
expectedBodySize=0;
maxSize=settings->value("maxRequestSize","16000").toInt();
maxMultiPartSize=settings->value("maxMultiPartSize","1000000").toInt();
}
void HttpRequest::readRequest(QTcpSocket* socket)
{
#ifdef SUPERVERBOSE
qDebug("HttpRequest: read request");
#endif
int toRead=maxSize-currentSize+1; // allow one byte more to be able to detect overflow
lineBuffer.append(socket->readLine(toRead));
currentSize+=lineBuffer.size();
if (!lineBuffer.contains('\r') && !lineBuffer.contains('\n'))
{
#ifdef SUPERVERBOSE
qDebug("HttpRequest: collecting more parts until line break");
#endif
return;
}
QByteArray newData=lineBuffer.trimmed();
lineBuffer.clear();
if (!newData.isEmpty())
{
QList<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
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);
file->close();
delete file;
}
}
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;
}

View File

@ -0,0 +1,235 @@
/**
@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"
/**
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;
/** Parset he multipart body, that has been stored in the temp file. */
void parseMultiPartFile();
/** Sub-procedure of readFromSocket(), read the first line of a request. */
void readRequest(QTcpSocket* socket);
/** Sub-procedure of readFromSocket(), read header lines. */
void readHeader(QTcpSocket* socket);
/** Sub-procedure of readFromSocket(), read the request body. */
void readBody(QTcpSocket* socket);
/** Sub-procedure of readFromSocket(), extract and decode request parameters. */
void decodeRequestParams();
/** Sub-procedure of readFromSocket(), extract cookies from headers */
void extractCookies();
/** Buffer for collecting characters of request and header lines */
QByteArray lineBuffer;
};
#endif // HTTPREQUEST_H

View File

@ -0,0 +1,21 @@
/**
@file
@author Stefan Frings
*/
#include "httprequesthandler.h"
HttpRequestHandler::HttpRequestHandler(QObject* parent)
: QObject(parent)
{}
HttpRequestHandler::~HttpRequestHandler()
{}
void HttpRequestHandler::service(HttpRequest& request, HttpResponse& response)
{
qCritical("HttpRequestHandler: you need to override the service() function");
qDebug("HttpRequestHandler: request=%s %s %s",request.getMethod().data(),request.getPath().data(),request.getVersion().data());
response.setStatus(501,"not implemented");
response.write("501 not implemented",true);
}

View File

@ -0,0 +1,49 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPREQUESTHANDLER_H
#define HTTPREQUESTHANDLER_H
#include "httpglobal.h"
#include "httprequest.h"
#include "httpresponse.h"
/**
The request handler generates a response for each HTTP request. Web Applications
usually have one central request handler that maps incoming requests to several
controllers (servlets) based on the requested path.
<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);
};
#endif // HTTPREQUESTHANDLER_H

View File

@ -0,0 +1,198 @@
/**
@file
@author Stefan Frings
*/
#include "httpresponse.h"
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();
}

View File

@ -0,0 +1,159 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPRESPONSE_H
#define HTTPRESPONSE_H
#include <QMap>
#include <QString>
#include <QTcpSocket>
#include "httpglobal.h"
#include "httpcookie.h"
/**
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();
};
#endif // HTTPRESPONSE_H

View 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

View File

@ -0,0 +1,186 @@
/**
@file
@author Stefan Frings
*/
#include "httpsession.h"
#include <QDateTime>
#include <QUuid>
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();
}
}

View File

@ -0,0 +1,118 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPSESSION_H
#define HTTPSESSION_H
#include <QByteArray>
#include <QVariant>
#include <QReadWriteLock>
#include "httpglobal.h"
/**
This class stores data for a single HTTP session.
A session can store any number of key/value pairs. This class uses implicit
sharing for read and write access. This class is thread safe.
@see HttpSessionStore should be used to create and get instances of this class.
*/
class DECLSPEC HttpSession {
public:
/**
Constructor.
@param canStore The session can store data, if this parameter is true.
Otherwise all calls to set() and remove() do not have any effect.
*/
HttpSession(bool canStore=false);
/**
Copy constructor. Creates another HttpSession object that shares the
data of the other object.
*/
HttpSession(const HttpSession& other);
/**
Copy operator. Detaches from the current shared data and attaches to
the data of the other object.
*/
HttpSession& operator= (const HttpSession& other);
/**
Destructor. Detaches from the shared data.
*/
virtual ~HttpSession();
/** Get the unique ID of this session. This method is thread safe. */
QByteArray getId() const;
/**
Null sessions cannot store data. All calls to set() and remove()
do not have any effect.This method is thread safe.
*/
bool isNull() const;
/** Set a value. This method is thread safe. */
void set(const QByteArray& key, const QVariant& value);
/** Remove a value. This method is thread safe. */
void remove(const QByteArray& key);
/** Get a value. This method is thread safe. */
QVariant get(const QByteArray& key) const;
/** Check if a key exists. This method is thread safe. */
bool contains(const QByteArray& key) const;
/**
Get a copy of all data stored in this session.
Changes to the session do not affect the copy and vice versa.
This method is thread safe.
*/
QMap<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;
};
#endif // HTTPSESSION_H

View File

@ -0,0 +1,125 @@
/**
@file
@author Stefan Frings
*/
#include "httpsessionstore.h"
#include <QDateTime>
#include <QUuid>
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();
}

View File

@ -0,0 +1,106 @@
/**
@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"
/**
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();
};
#endif // HTTPSESSIONSTORE_H

View File

@ -0,0 +1,288 @@
/**
@file
@author Stefan Frings
*/
#include "staticfilecontroller.h"
#include <QFileInfo>
#include <QDir>
#include <QDateTime>
//YACReader-----
#include "httpsession.h"
#include "yacreader_http_session.h"
#include "static.h"
//--
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();
//TODO(DONE) carga sensible al dispositivo y a la localización
QString stringPath = path;
QStringList paths = QString(path).split('/');
QString fileName = paths.last();
stringPath.remove(fileName);
HttpSession session=Static::sessionStore->getSession(request,response,false);
YACReaderHttpSession *ySession = Static::yacreaderSessionStore->getYACReaderSessionHttpSession(session.getId());
QString device = ySession->getDeviceType();
QString display = ySession->getDisplayType();
if(fileName.endsWith(".png"))
fileName = getDeviceAwareFileName(fileName, device, display, request.getHeader("Accept-Language"), stringPath);
else
fileName = getDeviceAwareFileName(fileName, device, request.getHeader("Accept-Language"), stringPath);
QString newPath = stringPath.append(fileName);
path = newPath.toLocal8Bit();
//CAMBIADO
//response.setHeader("Connection","close");
//END_TODO
// 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));
}
}
//YACReader------------------------------------------------------------------------
bool StaticFileController::exists(QString localizedName, QString path) const
{
QString fileName=docroot+"/"+path + localizedName;
QFile file(fileName);
return file.exists();
}
//retorna fileName si no se encontró alternativa traducida ó fileName-locale.extensión si se encontró
QString StaticFileController::getLocalizedFileName(QString fileName, QString locales, QString path) const
{
QSet<QString> tried; // used to suppress duplicate attempts
QStringList locs=locales.split(',',QString::SkipEmptyParts);
QStringList fileNameParts = fileName.split('.');
QString file = fileNameParts.first();
QString extension = fileNameParts.last();
// Search for exact match
foreach (QString loc,locs) {
loc.replace(QRegExp(";.*"),"");
loc.replace('-','_');
QString localizedName=file+"-"+loc.trimmed()+"."+extension;
if (!tried.contains(localizedName)) {
if(exists(localizedName, path))
return localizedName;
tried.insert(localizedName);
}
}
// Search for correct language but any country
foreach (QString loc,locs) {
loc.replace(QRegExp("[;_-].*"),"");
QString localizedName=file+"-"+loc.trimmed()+"."+extension;
if (!tried.contains(localizedName)) {
if(exists(localizedName, path))
return localizedName;
tried.insert(localizedName);
}
}
return fileName;
}
QString StaticFileController::getDeviceAwareFileName(QString fileName, QString device, QString locales, QString path) const
{
QFileInfo fi(fileName);
QString baseName = fi.baseName();
QString extension = fi.completeSuffix();
QString completeFileName = getLocalizedFileName(baseName+"_"+device+"."+extension,locales,path);
if(QFile(docroot+"/"+path+completeFileName).exists())
return completeFileName; //existe un archivo específico para este dispositivo y locales
else
return getLocalizedFileName(fileName,locales,path); //no hay archivo específico para el dispositivo, pero puede haberlo para estas locales
}
QString StaticFileController::getDeviceAwareFileName(QString fileName, QString device, QString display, QString locales, QString path) const
{
QFileInfo fi(fileName);
QString baseName = fi.baseName();
QString extension = fi.completeSuffix();
QString completeFileName = baseName+display+"."+extension;
if(QFile(docroot+"/"+path+completeFileName).exists())
return completeFileName;
else
{
completeFileName = baseName+"_"+device+display+"."+extension;
if((QFile(docroot+"/"+path+completeFileName).exists()))
return completeFileName;
}
return fileName;
}

View File

@ -0,0 +1,94 @@
/**
@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"
/**
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;
//YACReader------------------------------------------------------------------------
QString getLocalizedFileName(QString fileName, QString locales, QString path) const;
QString getDeviceAwareFileName(QString fileName, QString device, QString locales, QString path) const;
QString getDeviceAwareFileName(QString fileName, QString device, QString display, QString locales, QString path) const;
bool exists(QString localizedName, QString path) const;
};
#endif // STATICFILECONTROLLER_H