primera versi?n con servidor http incorporado

This commit is contained in:
Luis Ángel San Martín 2012-05-20 23:19:29 +02:00
parent 95817d14ee
commit 2598d6494a
60 changed files with 4751 additions and 3 deletions

View File

@ -6,10 +6,11 @@ TEMPLATE = app
TARGET =
DEPENDPATH += .
INCLUDEPATH += .
INCLUDEPATH += ../common
INCLUDEPATH += ../common \
./server
CONFIG += release
CONFIG -= flat
QT += sql
QT += sql network
# Input
HEADERS += comic_flow.h \
@ -45,6 +46,10 @@ SOURCES += comic_flow.cpp \
../common/qnaturalsorting.cpp \
data_base_management.cpp \
bundle_creator.cpp
include(./server/server.pri)
RESOURCES += images.qrc files.qrc
RC_FILE = icon.rc
@ -52,3 +57,4 @@ TRANSLATIONS = yacreaderlibrary_es.ts
Release:DESTDIR = ../release
Debug:DESTDIR = ../debug

View File

@ -0,0 +1,13 @@
#include "bundle_creator.h"
BundleCreator::BundleCreator(void)
:QObject()
{
}
BundleCreator::~BundleCreator(void)
{
}

View File

@ -0,0 +1,14 @@
#ifndef __BUNDLE_CREATOR_H
#define __BUNDLE_CREATOR_H
#include <QtCore>
class BundleCreator : public QObject
{
Q_OBJECT
public:
BundleCreator(void);
~BundleCreator(void);
};
#endif

View File

@ -1,7 +1,7 @@
#include "library_window.h"
#include <QApplication>
#include "startup.h"
#define PICTUREFLOW_QT4 1
int main( int argc, char ** argv )
@ -18,5 +18,8 @@ int main( int argc, char ** argv )
mw->resize(800,480);
mw->showMaximized();
Startup * s = new Startup();
s->start();
return app.exec();
}

View File

@ -0,0 +1,62 @@
/**
@file
@author Stefan Frings
*/
#include "dumpcontroller.h"
#include <QVariant>
#include <QDateTime>
DumpController::DumpController(){}
void DumpController::service(HttpRequest& request, HttpResponse& response) {
response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
response.setCookie(HttpCookie("firstCookie","hello",600));
response.setCookie(HttpCookie("secondCookie","world",600));
QByteArray body("<html><body>");
body.append("<b>Request:</b>");
body.append("<br>Method: ");
body.append(request.getMethod());
body.append("<br>Path: ");
body.append(request.getPath());
body.append("<br>Version: ");
body.append(request.getVersion());
body.append("<p><b>Headers:</b>");
QMapIterator<QByteArray,QByteArray> i(request.getHeaderMap());
while (i.hasNext()) {
i.next();
body.append("<br>");
body.append(i.key());
body.append("=");
body.append(i.value());
}
body.append("<p><b>Parameters:</b>");
i=QMapIterator<QByteArray,QByteArray>(request.getParameterMap());
while (i.hasNext()) {
i.next();
body.append("<br>");
body.append(i.key());
body.append("=");
body.append(i.value());
}
body.append("<p><b>Cookies:</b>");
i=QMapIterator<QByteArray,QByteArray>(request.getCookieMap());
while (i.hasNext()) {
i.next();
body.append("<br>");
body.append(i.key());
body.append("=");
body.append(i.value());
}
body.append("<p><b>Body:</b><br>");
body.append(request.getBody());
body.append("</body></html>");
response.write(body,true);
}

View File

@ -0,0 +1,29 @@
/**
@file
@author Stefan Frings
*/
#ifndef DUMPCONTROLLER_H
#define DUMPCONTROLLER_H
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
/**
This controller dumps the received HTTP request in the response.
*/
class DumpController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(DumpController);
public:
/** Constructor */
DumpController();
/** Generates the response */
void service(HttpRequest& request, HttpResponse& response);
};
#endif // DUMPCONTROLLER_H

View File

@ -0,0 +1,38 @@
/**
@file
@author Stefan Frings
*/
#include "fileuploadcontroller.h"
FileUploadController::FileUploadController() {}
void FileUploadController::service(HttpRequest& request, HttpResponse& response) {
if (request.getParameter("action")=="show") {
response.setHeader("Content-Type", "image/jpeg");
QTemporaryFile* file=request.getUploadedFile("file1");
if (file) {
while (!file->atEnd() && !file->error()) {
QByteArray buffer=file->read(65536);
response.write(buffer);
}
}
else {
response.write("upload failed");
}
}
else {
response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
response.write("<html><body>");
response.write("Upload a JPEG image file<p>");
response.write("<form method=\"post\" enctype=\"multipart/form-data\">");
response.write(" <input type=\"hidden\" name=\"action\" value=\"show\">");
response.write(" File: <input type=\"file\" name=\"file1\"><br>");
response.write(" <input type=\"submit\">");
response.write("</form>");
response.write("</body></html>",true);
}
}

View File

@ -0,0 +1,30 @@
/**
@file
@author Stefan Frings
*/
#ifndef FILEUPLOADCONTROLLER_H
#define FILEUPLOADCONTROLLER_H
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
/**
This controller displays a HTML form for file upload and recieved the file.
*/
class FileUploadController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(FileUploadController);
public:
/** Constructor */
FileUploadController();
/** Generates the response */
void service(HttpRequest& request, HttpResponse& response);
};
#endif // FILEUPLOADCONTROLLER_H

View File

@ -0,0 +1,33 @@
/**
@file
@author Stefan Frings
*/
#include "formcontroller.h"
FormController::FormController() {}
void FormController::service(HttpRequest& request, HttpResponse& response) {
response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
if (request.getParameter("action")=="show") {
response.write("<html><body>");
response.write("Name = ");
response.write(request.getParameter("name"));
response.write("<br>City = ");
response.write(request.getParameter("city"));
response.write("</body></html>",true);
}
else {
response.write("<html><body>");
response.write("<form method=\"post\">");
response.write(" <input type=\"hidden\" name=\"action\" value=\"show\">");
response.write(" Name: <input type=\"text\" name=\"name\"><br>");
response.write(" City: <input type=\"text\" name=\"city\"><br>");
response.write(" <input type=\"submit\">");
response.write("</form>");
response.write("</body></html>",true);
}
}

View File

@ -0,0 +1,30 @@
/**
@file
@author Stefan Frings
*/
#ifndef FORMCONTROLLER_H
#define FORMCONTROLLER_H
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
/**
This controller displays a HTML form and dumps the submitted input.
*/
class FormController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(FormController);
public:
/** Constructor */
FormController();
/** Generates the response */
void service(HttpRequest& request, HttpResponse& response);
};
#endif // FORMCONTROLLER_H

View File

@ -0,0 +1,31 @@
/**
@file
@author Stefan Frings
*/
#include "sessioncontroller.h"
#include "../static.h"
#include <QVariant>
#include <QDateTime>
SessionController::SessionController(){}
void SessionController::service(HttpRequest& request, HttpResponse& response) {
response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
// Get current session, or create a new one
HttpSession session=Static::sessionStore->getSession(request,response);
if (!session.contains("startTime")) {
response.write("<html><body>New session started. Reload this page now.</body></html>");
session.set("startTime",QDateTime::currentDateTime());
}
else {
QDateTime startTime=session.get("startTime").toDateTime();
response.write("<html><body>Your session started ");
response.write(startTime.toString().toLatin1());
response.write("</body></html>");
}
}

View File

@ -0,0 +1,29 @@
/**
@file
@author Stefan Frings
*/
#ifndef SESSIONCONTROLLER_H
#define SESSIONCONTROLLER_H
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
/**
This controller demonstrates how to use sessions.
*/
class SessionController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(SessionController);
public:
/** Constructor */
SessionController();
/** Generates the response */
void service(HttpRequest& request, HttpResponse& response);
};
#endif // SESSIONCONTROLLER_H

View File

@ -0,0 +1,31 @@
/**
@file
@author Stefan Frings
*/
#include "templatecontroller.h"
#include "template.h"
#include "../static.h"
TemplateController::TemplateController(){}
void TemplateController::service(HttpRequest& request, HttpResponse& response) {
response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
Template t=Static::templateLoader->getTemplate("demo",request.getHeader("Accept-Language"));
t.enableWarnings();
t.setVariable("path",request.getPath());
QMap<QByteArray,QByteArray> headers=request.getHeaderMap();
QMapIterator<QByteArray,QByteArray> iterator(headers);
t.loop("header",headers.size());
int i=0;
while (iterator.hasNext()) {
iterator.next();
t.setVariable(QString("header%1.name").arg(i),QString(iterator.key()));
t.setVariable(QString("header%1.value").arg(i),QString(iterator.value()));
++i;
}
response.write(t.toLatin1(),true);
}

View File

@ -0,0 +1,30 @@
/**
@file
@author Stefan Frings
*/
#ifndef TEMPLATECONTROLLER_H
#define TEMPLATECONTROLLER_H
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
/**
This controller generates a website using the template engine.
It generates a Latin1 (ISO-8859-1) encoded website from a UTF-8 encoded template file.
*/
class TemplateController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(TemplateController);
public:
/** Constructor */
TemplateController();
/** Generates the response */
void service(HttpRequest& request, HttpResponse& response);
};
#endif // TEMPLATECONTROLLER_H

View File

@ -0,0 +1,4 @@
#ifndef DOCUMENTCACHE_H
#define DOCUMENTCACHE_H
#endif // DOCUMENTCACHE_H

View File

@ -0,0 +1,12 @@
INCLUDEPATH += $$PWD
DEPENDPATH += $$PWD
HEADERS += $$PWD/httplistener.h $$PWD/httpconnectionhandler.h $$PWD/httpconnectionhandlerpool.h $$PWD/httprequest.h $$PWD/httpresponse.h $$PWD/httpcookie.h $$PWD/httprequesthandler.h
HEADERS += $$PWD/httpsession.h $$PWD/httpsessionstore.h
HEADERS += $$PWD/staticfilecontroller.h
SOURCES += $$PWD/httplistener.cpp $$PWD/httpconnectionhandler.cpp $$PWD/httpconnectionhandlerpool.cpp $$PWD/httprequest.cpp $$PWD/httpresponse.cpp $$PWD/httpcookie.cpp $$PWD/httprequesthandler.cpp
SOURCES += $$PWD/httpsession.cpp $$PWD/httpsessionstore.cpp
SOURCES += $$PWD/staticfilecontroller.cpp
OTHER_FILES += $$PWD/../doc/readme.txt

View File

@ -0,0 +1,154 @@
/**
@file
@author Stefan Frings
*/
#include "httpconnectionhandler.h"
#include "httpresponse.h"
#include <QTimer>
#include <QCoreApplication>
HttpConnectionHandler::HttpConnectionHandler(QSettings* settings, HttpRequestHandler* requestHandler)
: QThread()
{
Q_ASSERT(settings!=0);
Q_ASSERT(requestHandler!=0);
this->settings=settings;
this->requestHandler=requestHandler;
currentRequest=0;
busy = false; // pdiener: it is not busy if it is new
// execute signals in my own thread
moveToThread(this);
socket.moveToThread(this);
readTimer.moveToThread(this);
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() {
qDebug("HttpConnectionHandler (%p): destroyed", this);
}
void HttpConnectionHandler::run() {
qDebug("HttpConnectionHandler (%p): thread started", this);
try {
exec();
}
catch (...) {
qCritical("HttpConnectionHandler (%p): an uncatched exception occured in the thread",this);
}
qDebug("HttpConnectionHandler (%p): thread stopped", this);
// Change to the main thread, otherwise deleteLater() would not work
moveToThread(QCoreApplication::instance()->thread());
}
void HttpConnectionHandler::handleConnection(int socketDescriptor) {
qDebug("HttpConnectionHandler (%p): handle new connection", this);
busy = true;
Q_ASSERT(socket.isOpen()==false); // if not, then the handler is already busy
if (!socket.setSocketDescriptor(socketDescriptor)) {
qCritical("HttpConnectionHandler (%p): cannot initialize socket: %s", this,qPrintable(socket.errorString()));
return;
}
// Start timer for read timeout
int readTimeout=settings->value("readTimeout",10000).toInt();
readTimer.start(readTimeout);
currentRequest=0;
}
bool HttpConnectionHandler::isBusy() {
//return socket.isOpen();
return busy; // pdiener: changed this from socket readout to bool variable
}
void HttpConnectionHandler::setBusy() {
this->busy = true;
}
void HttpConnectionHandler::readTimeout() {
qDebug("HttpConnectionHandler (%p): read timeout occured",this);
socket.write("HTTP/1.1 408 request timeout\r\nConnection: close\r\n\r\n408 request timeout\r\n");
socket.disconnectFromHost();
delete currentRequest;
currentRequest=0;
}
void HttpConnectionHandler::disconnected() {
qDebug("HttpConnectionHandler (%p): disconnected", this);
socket.close();
delete currentRequest;
currentRequest=0;
readTimer.stop();
busy = false; // pdiener: now we have finished
}
void HttpConnectionHandler::read() {
#ifdef SUPERVERBOSE
qDebug("HttpConnectionHandler (%x): read input",(unsigned int) 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.disconnectFromHost();
delete currentRequest;
currentRequest=0;
return;
}
// If the request is complete, let the request mapper dispatch it
if (currentRequest->getStatus()==HttpRequest::complete) {
readTimer.stop();
qDebug("HttpConnectionHandler (%p): received request",this);
HttpResponse response(&socket);
try {
requestHandler->service(*currentRequest, response);
}
catch (...) {
qCritical("HttpConnectionHandler (%p): An uncatched exception occured in the request handler",this);
}
// Finalize sending the response if not already done
if (!response.hasSentLastPart()) {
response.write(QByteArray(),true);
}
// Close the connection after delivering the response, if requested
if (QString::compare(currentRequest->getHeader("Connection"),"close",Qt::CaseInsensitive)==0) {
socket.disconnectFromHost();
}
else {
// Start timer for next request
int readTimeout=settings->value("readTimeout",10000).toInt();
readTimer.start(readTimeout);
}
// Prepare for next request
delete currentRequest;
currentRequest=0;
}
}

View File

@ -0,0 +1,97 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPCONNECTIONHANDLER_H
#define HTTPCONNECTIONHANDLER_H
#include <QTcpSocket>
#include <QSettings>
#include <QTimer>
#include <QThread>
#include "httprequest.h"
#include "httprequesthandler.h"
/**
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 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 incomin HTTP request
*/
HttpConnectionHandler(QSettings* settings, HttpRequestHandler* requestHandler);
/** Destructor */
virtual ~HttpConnectionHandler();
/** Returns true, if this handler is busy */
bool isBusy();
/** Mark this handler as busy */
void setBusy();
/** This shows the busy-state from a very early time */
bool busy;
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;
/** Executes the htreads own event loop */
void run();
public slots:
/**
Received from from the listener, when the handler shall start processing a new connection.
@param socketDescriptor references the accepted connection.
*/
void handleConnection(int 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,55 @@
#include "httpconnectionhandlerpool.h"
HttpConnectionHandlerPool::HttpConnectionHandlerPool(QSettings* settings, HttpRequestHandler* requestHandler)
: QObject()
{
Q_ASSERT(settings!=0);
this->settings=settings;
this->requestHandler=requestHandler;
cleanupTimer.start(settings->value("cleanupInterval",1000).toInt());
connect(&cleanupTimer, SIGNAL(timeout()), SLOT(cleanup()));
}
HttpConnectionHandlerPool::~HttpConnectionHandlerPool() {
foreach(HttpConnectionHandler* handler, pool) {
delete handler;
}
}
HttpConnectionHandler* HttpConnectionHandlerPool::getConnectionHandler() {
HttpConnectionHandler* freeHandler=0;
foreach(HttpConnectionHandler* handler, pool) {
if (!handler->isBusy()) {
freeHandler=handler;
}
}
if (!freeHandler) {
int maxConnectionHandlers=settings->value("maxThreads",10).toInt();
if (pool.count()<maxConnectionHandlers) {
freeHandler=new HttpConnectionHandler(settings,requestHandler);
pool.append(freeHandler);
}
}
if (freeHandler) freeHandler->busy = true; // pdiener: set it to busy-state immediately
return freeHandler;
}
void HttpConnectionHandlerPool::cleanup() {
int maxIdleHandlers=settings->value("minThreads",1).toInt();
int idleCounter=0;
foreach(HttpConnectionHandler* handler, pool) {
if (!handler->isBusy()) {
if (++idleCounter > maxIdleHandlers) {
pool.removeOne(handler);
qDebug("HttpConnectionHandlerPool: Removed connection handler (%p), pool size is now %i",handler,pool.size());
connect(handler,SIGNAL(finished()),handler,SLOT(deleteLater()));
handler->quit();
break; // remove only one handler in each interval
}
}
}
}

View File

@ -0,0 +1,69 @@
#ifndef HTTPCONNECTIONHANDLERPOOL_H
#define HTTPCONNECTIONHANDLERPOOL_H
#include <QList>
#include <QTimer>
#include <QObject>
#include "httpconnectionhandler.h"
/**
Pool of http connection handlers. Connection handlers are created on demand and idle handlers are
cleaned up in regular time intervals.
<p>
Example for the required configuration settings:
<code><pre>
minThreads=1
maxThreads=10
cleanupInterval=1000
maxRequestSize=16000
maxMultiPartSize=1000000
</pre></code>
The pool is empty initially and grows with the number of concurrent
connections. A timer removes one idle connection handler at each
interval, but a it leaves some spare handlers in memory to improve
performance.
@see HttpConnectionHandler for description of config settings readTimeout
@see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize
*/
class 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;
private slots:
/** Received from the clean-up timer. */
void cleanup();
};
#endif // HTTPCONNECTIONHANDLERPOOL_H

View File

@ -0,0 +1,199 @@
/**
@file
@author Stefan Frings
*/
#include "httpcookie.h"
HttpCookie::HttpCookie() {
version=1;
maxAge=0;
secure=false;
}
HttpCookie::HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path, const QByteArray comment, const QByteArray domain, const bool secure) {
this->name=name;
this->value=value;
this->maxAge=maxAge;
this->path=path;
this->comment=comment;
this->domain=domain;
this->secure=secure;
this->version=1;
}
HttpCookie::HttpCookie(const QByteArray source) {
version=1;
maxAge=0;
secure=false;
QList<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=="Version") {
version=value.toInt();
}
else {
if (this->name.isEmpty()) {
this->name=name;
this->value=value;
}
else {
qWarning("HttpCookie: Ignoring unknown %s=%s",name.data(),value.data());
}
}
}
}
QByteArray HttpCookie::toByteArray() const {
QByteArray buffer(name);
buffer.append('=');
buffer.append(value);
if (!comment.isEmpty()) {
buffer.append("; Comment=");
buffer.append(comment);
}
if (!domain.isEmpty()) {
buffer.append("; Domain=");
buffer.append(domain);
}
if (maxAge!=0) {
buffer.append("; Max-Age=");
buffer.append(QByteArray::number(maxAge));
}
if (!path.isEmpty()) {
buffer.append("; Path=");
buffer.append(path);
}
if (secure) {
buffer.append("; Secure");
}
buffer.append("; Version=");
buffer.append(QByteArray::number(version));
return buffer;
}
void HttpCookie::setName(const QByteArray name){
this->name=name;
}
void HttpCookie::setValue(const QByteArray value){
this->value=value;
}
void HttpCookie::setComment(const QByteArray comment){
this->comment=comment;
}
void HttpCookie::setDomain(const QByteArray domain){
this->domain=domain;
}
void HttpCookie::setMaxAge(const int maxAge){
this->maxAge=maxAge;
}
void HttpCookie::setPath(const QByteArray path){
this->path=path;
}
void HttpCookie::setSecure(const bool secure){
this->secure=secure;
}
QByteArray HttpCookie::getName() const {
return name;
}
QByteArray HttpCookie::getValue() const {
return value;
}
QByteArray HttpCookie::getComment() const {
return comment;
}
QByteArray HttpCookie::getDomain() const {
return domain;
}
int HttpCookie::getMaxAge() const {
return maxAge;
}
QByteArray HttpCookie::getPath() const {
return path;
}
bool HttpCookie::getSecure() const {
return secure;
}
int HttpCookie::getVersion() const {
return version;
}
QList<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,110 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPCOOKIE_H
#define HTTPCOOKIE_H
#include <QList>
#include <QByteArray>
/**
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 HttpCookie
{
public:
/** Creates an empty cookie */
HttpCookie();
/**
Create a cookie and set name/value pair.
@param name name of the cookie
@param value value of the cookie
@param maxAge maximum age of the cookie in seconds. 0=discard immediately
@param path Path for that the cookie will be sent, default="/" which means the whole domain
@param comment Optional comment, may be displayed by the web browser somewhere
@param domain Optional domain for that the cookie will be sent. Defaults to the current domain
@param secure If true, the cookie will only be sent on secure connections
*/
HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path="/", const QByteArray comment=QByteArray(), const QByteArray domain=QByteArray(), const bool secure=false);
/**
Create a cookie from a string.
@param source String as received in a HTTP Cookie2 header.
*/
HttpCookie(const QByteArray source);
/** Convert this cookie to a string that may be used in a Set-Cookie2 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 cokkie will only be sent on secure connections */
void setSecure(const bool secure);
/** Get the name of this cookie */
QByteArray getName() const;
/** Get the value of this cookie */
QByteArray getValue() const;
/** Get the comment of this cookie */
QByteArray getComment() const;
/** Get the domain of this cookie */
QByteArray getDomain() const;
/** Set the maximum age of this cookie in seconds. */
int getMaxAge() const;
/** Set the path of this cookie */
QByteArray getPath() const;
/** Get the secure flag of this cookie */
bool getSecure() const;
/** Returns always 1 */
int getVersion() const;
private:
QByteArray name;
QByteArray value;
QByteArray comment;
QByteArray domain;
int maxAge;
QByteArray path;
bool secure;
int version;
};
#endif // HTTPCOOKIE_H

View File

@ -0,0 +1,56 @@
/**
@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), pool(settings,requestHandler)
{
Q_ASSERT(settings!=0);
this->settings=settings;
// Start listening
int port=settings->value("port").toInt();
listen(QHostAddress::Any, port);
if (!isListening()) {
qCritical("HttpListener: Cannot bind on port %i: %s",port,qPrintable(errorString()));
}
else {
qDebug("HttpListener: Listening on port %i",port);
}
}
HttpListener::~HttpListener() {
close();
qDebug("HttpListener: closed");
}
void HttpListener::incomingConnection(int socketDescriptor) {
#ifdef SUPERVERBOSE
qDebug("HttpListener: New connection");
#endif
HttpConnectionHandler* 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 called by another thread.
connect(this,SIGNAL(handleConnection(int)),freeHandler,SLOT(handleConnection(int)));
emit handleConnection(socketDescriptor);
disconnect(this,SIGNAL(handleConnection(int)),freeHandler,SLOT(handleConnection(int)));
}
else {
// Reject the connection
qDebug("HttpListener: Too many 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,76 @@
/**
@file
@author Stefan Frings
*/
#ifndef LISTENER_H
#define LISTENER_H
#include <QTcpServer>
#include <QSettings>
#include <QBasicTimer>
#include "httpconnectionhandler.h"
#include "httpconnectionhandlerpool.h"
#include "httprequesthandler.h"
/**
Listens for incoming TCP connections and passes control to
one of the pooled connection handlers. This class is also
responsible for managing the pool.
<p>
Example for the required settings in the config file:
<code><pre>
port=8080
minThreads=1
maxThreads=10
cleanupInterval=1000
readTimeout=60000
maxRequestSize=16000
maxMultiPartSize=1000000
</pre></code>
The port number is the incoming TCP port that this listener listens to.
@see HttpConnectionHandlerPool for description of config settings minThreads, maxThreads and cleanupInterval
@see HttpConnectionHandler for description of config settings readTimeout
@see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize
*/
class HttpListener : public QTcpServer {
Q_OBJECT
Q_DISABLE_COPY(HttpListener)
public:
/**
Constructor.
@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
*/
HttpListener(QSettings* settings, HttpRequestHandler* requestHandler, QObject* parent = 0);
/** Destructor */
virtual ~HttpListener();
protected:
/** Serves new incoming connection requests */
void incomingConnection(int socketDescriptor);
private:
/** Configuration settings for the HTTP server */
QSettings* settings;
/** Pool of connection handlers */
HttpConnectionHandlerPool pool;
signals:
/**
Emitted when the connection handler shall process a new incoming onnection.
@param socketDescriptor references the accepted connection.
*/
void handleConnection(int socketDescriptor);
};
#endif // LISTENER_H

View File

@ -0,0 +1,435 @@
/**
@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();
// Convert relative path to absolute, based on the directory of the config file.
#ifdef Q_OS_WIN32
if (QDir::isRelativePath(tempDir) && settings->format()!=QSettings::NativeFormat)
#else
if (QDir::isRelativePath(tempDir))
#endif
{
QFileInfo configFile(settings->fileName());
tempDir=QFileInfo(configFile.absolutePath(),tempDir).absoluteFilePath();
}
}
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
QByteArray newData=socket.readLine(toRead).trimmed();
currentSize+=newData.size();
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);
path=list.at(1);
version=list.at(2);
status=waitForHeader;
}
}
}
void HttpRequest::readHeader(QTcpSocket& socket) {
#ifdef SUPERVERBOSE
qDebug("HttpRequest: read header");
#endif
int toRead=maxSize-currentSize+1; // allow one byte more to be able to detect overflow
QByteArray newData=socket.readLine(toRead).trimmed();
currentSize+=newData.size();
int colon=newData.indexOf(':');
if (colon>0) {
// Received a line with a colon - a header
currentHeader=newData.left(colon);
QByteArray value=newData.mid(colon+1).trimmed();
headers.insert(currentHeader,value);
#ifdef SUPERVERBOSE
qDebug("HttpRequest: received header %s: %s",currentHeader.data(),value.data());
#endif
}
else if (!newData.isEmpty()) {
// received another line - belongs to the previous header
#ifdef SUPERVERBOSE
qDebug("HttpRequest: read additional line of header");
#endif
// Received additional line of previous header
if (headers.contains(currentHeader)) {
headers.insert(currentHeader,headers.value(currentHeader)+" "+newData);
}
}
else {
// received an empty line - end of headers reached
#ifdef SUPERVERBOSE
qDebug("HttpRequest: headers completed");
#endif
// Empty line received, that means all headers have been received
// Check for multipart/form-data
QByteArray contentType=headers.value("Content-Type");
if (contentType.startsWith("multipart/form-data")) {
int posi=contentType.indexOf("boundary=");
if (posi>=0) {
boundary=contentType.mid(posi+9);
}
}
QByteArray contentLength=getHeader("Content-Length");
if (!contentLength.isEmpty()) {
expectedBodySize=contentLength.toInt();
}
if (expectedBodySize==0) {
#ifdef SUPERVERBOSE
qDebug("HttpRequest: expect no body");
#endif
status=complete;
}
else if (boundary.isEmpty() && expectedBodySize+currentSize>maxSize) {
qWarning("HttpRequest: expected body is too large");
status=abort;
}
else if (!boundary.isEmpty() && expectedBodySize>maxMultiPartSize) {
qWarning("HttpRequest: expected multipart body is too large");
status=abort;
}
else {
#ifdef SUPERVERBOSE
qDebug("HttpRequest: expect %i bytes body",expectedBodySize);
#endif
status=waitForBody;
}
}
}
void HttpRequest::readBody(QTcpSocket& socket) {
Q_ASSERT(expectedBodySize!=0);
if (boundary.isEmpty()) {
// normal body, no multipart
#ifdef SUPERVERBOSE
qDebug("HttpRequest: receive body");
#endif
int toRead=expectedBodySize-bodyData.size();
QByteArray newData=socket.read(toRead);
currentSize+=newData.size();
bodyData.append(newData);
if (bodyData.size()>=expectedBodySize) {
status=complete;
}
}
else {
// multipart body, store into temp file
#ifdef SUPERVERBOSE
qDebug("HttpRequest: receiving multipart body");
#endif
if (!tempFile.isOpen()) {
tempFile.open();
}
// Transfer data in 64kb blocks
int fileSize=tempFile.size();
int toRead=expectedBodySize-fileSize;
if (toRead>65536) {
toRead=65536;
}
fileSize+=tempFile.write(socket.read(toRead));
if (fileSize>=maxMultiPartSize) {
qWarning("HttpRequest: received too many multipart bytes");
status=abort;
}
else if (fileSize>=expectedBodySize) {
#ifdef SUPERVERBOSE
qDebug("HttpRequest: received whole multipart body");
#endif
tempFile.flush();
if (tempFile.error()) {
qCritical("HttpRequest: Error writing temp file for multipart body");
}
parseMultiPartFile();
tempFile.close();
status=complete;
}
}
}
void HttpRequest::decodeRequestParams() {
#ifdef SUPERVERBOSE
qDebug("HttpRequest: extract and decode request parameters");
#endif
QByteArray rawParameters;
if (headers.value("Content-Type")=="application/x-www-form-urlencoded") {
rawParameters=bodyData;
}
else {
int questionMark=path.indexOf('?');
if (questionMark>=0) {
rawParameters=path.mid(questionMark+1);
path=path.left(questionMark);
}
}
// 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 (currentSize>maxSize) {
qWarning("HttpRequest: received too many bytes");
status=abort;
}
if (status==complete) {
// Extract and decode request parameters from url and body
decodeRequestParams();
// Extract cookies from headers
extractCookies();
}
}
HttpRequest::RequestStatus HttpRequest::getStatus() const {
return status;
}
QByteArray HttpRequest::getMethod() const {
return method;
}
QByteArray HttpRequest::getPath() const {
return urlDecode(path);
}
QByteArray HttpRequest::getVersion() const {
return version;
}
QByteArray HttpRequest::getHeader(const QByteArray& name) const {
return headers.value(name);
}
QList<QByteArray> HttpRequest::getHeaders(const QByteArray& name) const {
return headers.values(name);
}
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) {
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;
}

View File

@ -0,0 +1,214 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPREQUEST_H
#define HTTPREQUEST_H
#include <QByteArray>
#include <QTcpSocket>
#include <QMap>
#include <QMultiMap>
#include <QSettings>
#include <QTemporaryFile>
#include <QUuid>
/**
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 HttpRequest {
Q_DISABLE_COPY(HttpRequest)
friend class HttpSessionStore;
public:
/** Values for getStatus() */
enum RequestStatus {waitForRequest, waitForHeader, waitForBody, complete, abort};
/**
Constructor.
@param settings Configuration settings
*/
HttpRequest(QSettings* settings);
/**
Destructor.
*/
virtual ~HttpRequest();
/**
Read the request from a socket. This method must be called repeatedly
until the status is RequestStatus::complete or RequestStatus::abort.
@param socket Source of the data
*/
void readFromSocket(QTcpSocket& socket);
/**
Get the status of this reqeust.
@see RequestStatus
*/
RequestStatus getStatus() const;
/** Get the method of the HTTP request (e.g. "GET") */
QByteArray getMethod() const;
/** Get the decoded path of the HTPP request (e.g. "/index.html") */
QByteArray getPath() const;
/** Get the version of the HTPP request (e.g. "HTTP/1.1") */
QByteArray getVersion() const;
/**
Get the value of a HTTP request header.
@param name Name of the header
@return If the header occurs multiple times, only the last
one is returned.
*/
QByteArray getHeader(const QByteArray& name) const;
/**
Get the values of a HTTP request header.
@param name Name of the header
*/
QList<QByteArray> getHeaders(const QByteArray& name) const;
/** Get all HTTP request headers */
QMultiMap<QByteArray,QByteArray> getHeaderMap() const;
/**
Get the value of a HTTP request parameter.
@param name Name of the parameter
@return If the parameter occurs multiple times, only the last
one is returned.
*/
QByteArray getParameter(const QByteArray& name) const;
/**
Get the values of a HTTP request parameter.
@param name Name of the parameter
*/
QList<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 string
*/
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);
/**
Get the value of a cookie
@param name Name of the cookie
*/
QByteArray getCookie(const QByteArray& name) const;
/** Get the map of cookies */
QMap<QByteArray,QByteArray>& getCookieMap();
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.
@see RequestStatus
*/
RequestStatus status;
/** Maximum size of requests in bytes. */
int maxSize;
/** Maximum allowed size of multipart forms in bytes. */
int maxMultiPartSize;
/** Directory for temp files */
QString tempDir;
/** 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();
};
#endif // HTTPREQUEST_H

View File

@ -0,0 +1,19 @@
/**
@file
@author Stefan Frings
*/
#include "httprequesthandler.h"
HttpRequestHandler::HttpRequestHandler(QObject* parent)
: QObject(parent)
{}
HttpRequestHandler::~HttpRequestHandler() {}
void HttpRequestHandler::service(HttpRequest& request, HttpResponse& response) {
qCritical("HttpRequestHandler: you need to override the dispatch() 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,45 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPREQUESTHANDLER_H
#define HTTPREQUESTHANDLER_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 HttpRequestHandler : public QObject {
Q_OBJECT
Q_DISABLE_COPY(HttpRequestHandler)
public:
/** Constructor */
HttpRequestHandler(QObject* parent=0);
/** 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,122 @@
/**
@file
@author Stefan Frings
*/
#include "httpresponse.h"
HttpResponse::HttpResponse(QTcpSocket* socket) {
this->socket=socket;
statusCode=200;
statusText="OK";
sentHeaders=false;
sentLastPart=false;
}
void HttpResponse::setHeader(QByteArray name, QByteArray value) {
Q_ASSERT(sentHeaders==false);
headers.insert(name,value);
}
void HttpResponse::setHeader(QByteArray name, int value) {
Q_ASSERT(sentHeaders==false);
headers.insert(name,QByteArray::number(value));
}
QMap<QByteArray,QByteArray>& HttpResponse::getHeaders() {
return headers;
}
void HttpResponse::setStatus(int statusCode, QByteArray description) {
this->statusCode=statusCode;
statusText=description;
}
void HttpResponse::writeHeaders() {
Q_ASSERT(sentHeaders==false);
QByteArray buffer;
buffer.append("HTTP/1.1 ");
buffer.append(QByteArray::number(statusCode));
buffer.append(' ');
buffer.append(statusText);
buffer.append("\r\n");
foreach(QByteArray name, headers.keys()) {
buffer.append(name);
buffer.append(": ");
buffer.append(headers.value(name));
buffer.append("\r\n");
}
foreach(HttpCookie cookie,cookies.values()) {
buffer.append("Set-Cookie: ");
buffer.append(cookie.toByteArray());
buffer.append("\r\n");
}
buffer.append("\r\n");
writeToSocket(buffer);
sentHeaders=true;
}
void HttpResponse::writeToSocket(QByteArray data) {
int remaining=data.size();
char* ptr=data.data();
while (socket->isOpen() && remaining>0) {
int written=socket->write(data);
ptr+=written;
remaining-=written;
}
}
void HttpResponse::write(QByteArray data, bool lastPart) {
Q_ASSERT(sentLastPart==false);
if (sentHeaders==false) {
QByteArray connectionMode=headers.value("Connection");
if (!headers.contains("Content-Length") && !headers.contains("Transfer-Encoding") && connectionMode!="close" && connectionMode!="Close") {
if (!lastPart) {
headers.insert("Transfer-Encoding","chunked");
}
else {
headers.insert("Content-Length",QByteArray::number(data.size()));
}
}
writeHeaders();
}
bool chunked=headers.value("Transfer-Encoding")=="chunked" || headers.value("Transfer-Encoding")=="Chunked";
if (chunked) {
if (data.size()>0) {
QByteArray buffer=QByteArray::number(data.size(),16);
buffer.append("\r\n");
writeToSocket(buffer);
writeToSocket(data);
writeToSocket("\r\n");
}
}
else {
writeToSocket(data);
}
if (lastPart) {
if (chunked) {
writeToSocket("0\r\n\r\n");
}
else if (!headers.contains("Content-Length")) {
socket->disconnectFromHost();
}
sentLastPart=true;
}
}
bool HttpResponse::hasSentLastPart() const {
return sentLastPart;
}
void HttpResponse::setCookie(const HttpCookie& cookie) {
Q_ASSERT(sentHeaders==false);
if (!cookie.getName().isEmpty()) {
cookies.insert(cookie.getName(),cookie);
}
}
QMap<QByteArray,HttpCookie>& HttpResponse::getCookies() {
return cookies;
}

View File

@ -0,0 +1,134 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPRESPONSE_H
#define HTTPRESPONSE_H
#include <QMap>
#include <QString>
#include <QTcpSocket>
#include "httpcookie.h"
/**
This object represents a HTTP response, in particular the response headers.
<p>
Example code for proper response generation:
<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>
For performance reason, writing a single or few large packets is better than writing
many small packets. In case of large responses (e.g. file downloads), a Content-Length
header should be set before calling write(). Web Browsers use that information to display
a progress bar.
*/
class HttpResponse {
Q_DISABLE_COPY(HttpResponse)
public:
/**
Constructor.
@param socket used to write the response
*/
HttpResponse(QTcpSocket* socket);
/**
Set a HTTP response header
@param name name of the header
@param value value of the header
*/
void setHeader(QByteArray name, QByteArray value);
/**
Set a HTTP response header
@param name name of the header
@param value value of the header
*/
void setHeader(QByteArray name, int value);
/** Get the map of HTTP response headers */
QMap<QByteArray,QByteArray>& getHeaders();
/** Get the map of cookies */
QMap<QByteArray,HttpCookie>& getCookies();
/**
Set status code and description. The default is 200,OK.
*/
void setStatus(int statusCode, QByteArray description=QByteArray());
/**
Write body data to the socket.
<p>
The HTTP status line and headers are sent automatically before the first
byte of the body gets sent.
<p>
If the response contains only a single chunk (indicated by lastPart=true),
the response is transferred in traditional mode with a Content-Length
header, which is automatically added if not already set before.
<p>
Otherwise, each part is transferred in chunked mode.
@param data Data bytes of the body
@param lastPart Indicator, if this is the last part of the response.
*/
void write(QByteArray data, bool lastPart=false);
/**
Indicates wheter the body has been sent completely. Used by the connection
handler to terminate the body automatically when necessary.
*/
bool hasSentLastPart() const;
/**
Set a cookie. Cookies are sent together with the headers when the first
call to write() occurs.
*/
void setCookie(const HttpCookie& cookie);
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;
/** Cookies */
QMap<QByteArray,HttpCookie> cookies;
/** Write raw data to the socket. This method blocks until all bytes have been passed to the TCP buffer */
void 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,158 @@
/**
@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().toAscii();
#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>
/**
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 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,107 @@
/**
@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(timerEvent()));
cleanupTimer.start(60000);
cookieName=settings->value("cookieName","sessionid").toByteArray();
expirationTime=settings->value("expirationTime",3600000).toInt();
qDebug("HttpSessionStore: Sessions expire after %i milliseconds",expirationTime);
}
HttpSessionStore::~HttpSessionStore()
{
cleanupTimer.stop();
}
QByteArray HttpSessionStore::getSessionId(HttpRequest& request, HttpResponse& response) {
// The session ID in the response has priority because this one will be used in the next request.
mutex.lock();
// Get the session ID from the response cookie
QByteArray sessionId=response.getCookies().value(cookieName).getValue();
if (sessionId.isEmpty()) {
// Get the session ID from the request cookie
sessionId=request.getCookie(cookieName);
}
// Clear the session ID if there is no such session in the storage.
if (!sessionId.isEmpty()) {
if (!sessions.contains(sessionId)) {
qDebug("HttpSessionStore: received invalid session cookie with ID %s",sessionId.data());
sessionId.clear();
}
}
mutex.unlock();
return sessionId;
}
HttpSession HttpSessionStore::getSession(HttpRequest& request, HttpResponse& response, bool allowCreate) {
QByteArray sessionId=getSessionId(request,response);
mutex.lock();
if (!sessionId.isEmpty()) {
HttpSession session=sessions.value(sessionId);
if (!session.isNull()) {
mutex.unlock();
session.setLastAccess();
return session;
}
}
// Need to create a new session
if (allowCreate) {
QByteArray cookieName=settings->value("cookieName","sessionid").toByteArray();
QByteArray cookiePath=settings->value("cookiePath").toByteArray();
QByteArray cookieComment=settings->value("cookieComment").toByteArray();
QByteArray cookieDomain=settings->value("cookieDomain").toByteArray();
HttpSession session(true);
qDebug("HttpSessionStore: create new session with ID %s",session.getId().data());
sessions.insert(session.getId(),session);
response.setCookie(HttpCookie(cookieName,session.getId(),expirationTime/1000,cookiePath,cookieComment,cookieDomain));
mutex.unlock();
return session;
}
// Return a null session
mutex.unlock();
return HttpSession();
}
HttpSession HttpSessionStore::getSession(const QByteArray id) {
mutex.lock();
HttpSession session=sessions.value(id);
mutex.unlock();
session.setLastAccess();
return session;
}
void HttpSessionStore::timerEvent() {
// Todo: find a way to delete sessions only if no controller is accessing them
mutex.lock();
qint64 now=QDateTime::currentMSecsSinceEpoch();
QMap<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,104 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPSESSIONSTORE_H
#define HTTPSESSIONSTORE_H
#include <QObject>
#include <QMap>
#include <QTimer>
#include <QMutex>
#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 HttpSessionStore : public QObject {
Q_OBJECT
Q_DISABLE_COPY(HttpSessionStore);
public:
/** Constructor. */
HttpSessionStore(QSettings* settings, QObject* parent);
/** Destructor */
virtual ~HttpSessionStore();
/**
Get the ID of the current HTTP session, if it is valid.
This method is thread safe.
@warning Sessions may expire at any time, so subsequent calls of
getSession() might return a new session with a different ID.
@param request Used to get the session cookie
@param response Used to get and set the new session cookie
@return Empty string, if there is no valid session.
*/
QByteArray getSessionId(HttpRequest& request, HttpResponse& response);
/**
Get the session of a HTTP request, eventually create a new one.
This method is thread safe. New sessions can only be created before
the first byte has been written to the HTTP response.
@param request Used to get the session cookie
@param response Used to get and set the new session cookie
@param allowCreate can be set to false, to disable the automatic creation of a new session.
@return If autoCreate is disabled, the function returns a null session if there is no session.
@see HttpSession::isNull()
*/
HttpSession getSession(HttpRequest& request, HttpResponse& response, bool allowCreate=true);
/**
Get a HTTP session by it's ID number.
This method is thread safe.
@return If there is no such session, the function returns a null session.
@param id ID number of the session
@see HttpSession::isNull()
*/
HttpSession getSession(const QByteArray id);
/** Delete a session */
void removeSession(HttpSession session);
private:
/** Configuration settings */
QSettings* settings;
/** Storage for the sessions */
QMap<QByteArray,HttpSession> sessions;
/** Timer to remove expired sessions */
QTimer cleanupTimer;
/** Name of the session cookie */
QByteArray cookieName;
/** Time when sessions expire (in ms)*/
int expirationTime;
/** Used to synchronize threads */
QMutex mutex;
private slots:
/** Called every minute to cleanup expired sessions. */
void timerEvent();
};
#endif // HTTPSESSIONSTORE_H

View File

@ -0,0 +1,114 @@
/**
@file
@author Stefan Frings
*/
#include "staticfilecontroller.h"
#include <QFileInfo>
#include <QDir>
#include <QDateTime>
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();
// 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();
// Forbid access to files outside the docroot directory
if (path.startsWith("/..")) {
qWarning("StaticFileController: somebody attempted to access a file outside the docroot directory");
response.setStatus(403,"forbidden");
response.write("403 forbidden",true);
}
// Check if we have the file in cache
qint64 now=QDateTime::currentMSecsSinceEpoch();
CacheEntry* entry=cache.object(path);
if (entry && (cacheTimeout==0 || entry->created>now-cacheTimeout)) {
qDebug("StaticFileController: Cache hit for %s",path.data());
setContentType(path,response);
response.setHeader("Cache-Control","max-age="+QByteArray::number(maxAge/1000));
response.write(entry->document);
}
else {
qDebug("StaticFileController: Cache miss for %s",path.data());
// The file is not in cache.
// If the filename is a directory, append index.html.
if (QFileInfo(docroot+path).isDir()) {
path+="/index.html";
}
QFile file(docroot+path);
if (file.exists()) {
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;
cache.insert(request.getPath(),entry,entry->document.size());
}
else {
// Return the file content, do not store in cache
while (!file.atEnd() && !file.error()) {
response.write(file.read(65536));
}
}
file.close();
}
else {
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(".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=charset="+encoding));
}
// Todo: add all of your content types
}

View File

@ -0,0 +1,81 @@
/**
@file
@author Stefan Frings
*/
#ifndef STATICFILECONTROLLER_H
#define STATICFILECONTROLLER_H
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
#include <QCache>
/**
Delivers static files. It is usually called by the applications main request handler when
the caller request a path that is mapped to static files.
<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 StaticFileController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(StaticFileController);
public:
/** Constructor */
StaticFileController(QSettings* settings, QObject* parent = 0);
/** 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;
};
/** 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;
/** Set a content-type header in the response depending on the ending of the filename */
void setContentType(QString file, HttpResponse& response) const;
};
#endif // STATICFILECONTROLLER_H

View File

@ -0,0 +1,7 @@
INCLUDEPATH += $$PWD
DEPENDPATH += $$PWD
HEADERS += $$PWD/logmessage.h $$PWD/logger.h $$PWD/filelogger.h $$PWD/dualfilelogger.h
SOURCES += $$PWD/logmessage.cpp $$PWD/logger.cpp $$PWD/filelogger.cpp $$PWD/dualfilelogger.cpp
OTHER_FILES += $$PWD/../doc/readme.txt

View File

@ -0,0 +1,20 @@
/**
@file
@author Stefan Frings
*/
#include "dualfilelogger.h"
DualFileLogger::DualFileLogger(QSettings* firstSettings, QSettings* secondSettings, const int refreshInterval, QObject* parent)
:Logger(parent)
{
firstLogger=new FileLogger(firstSettings, refreshInterval, this);
secondLogger=new FileLogger(secondSettings, refreshInterval, this);
}
void DualFileLogger::log(const QtMsgType type, const QString& message) {
firstLogger->log(type, message);
secondLogger->log(type, message);
}

View File

@ -0,0 +1,60 @@
/**
@file
@author Stefan Frings
*/
#ifndef DUALFILELOGGER_H
#define DUALFILELOGGER_H
#include "logger.h"
#include "filelogger.h"
#include <QString>
#include <QSettings>
#include <QtGlobal>
/**
Logs messages into two log files. This is specially useful to get one less detailed
logfile for normal operation plus one more detailed file for debugging.
@see FileLogger for a description of the required config settings.
@see set() describes how to set logger variables
@see LogMessage for a description of the message decoration.
*/
class DualFileLogger : public Logger {
Q_OBJECT
Q_DISABLE_COPY(DualFileLogger)
public:
/**
Constructor.
@param firstSettings Configuration settings for the first log file, usually stored in an INI file.
Must not be 0.
Settings are read from the current group, so the caller must have called settings->beginGroup().
Because the group must not change during runtime, it is recommended to provide a
separate QSettings instance to the logger that is not used by other parts of the program.
@param secondSettings Same as firstSettings, but for the second log file.
@param refreshInterval Interval of checking the config settings in msec, or 0=disabled
@param parent Parent object
*/
DualFileLogger(QSettings* firstSettings, QSettings* secondSettings, const int refreshInterval=10000, QObject *parent = 0);
/**
Decorate and log a message.
This method is thread safe.
@param type Message type (level)
@param message Message text
@see LogMessage for a description of the message decoration.
*/
virtual void log(const QtMsgType type, const QString& message);
private:
/** First logger */
FileLogger* firstLogger;
/** Second logger */
FileLogger* secondLogger;
};
#endif // DUALFILELOGGER_H

View File

@ -0,0 +1,176 @@
/**
@file
@author Stefan Frings
*/
#include "filelogger.h"
#include <QTime>
#include <QStringList>
#include <QThread>
#include <QtGlobal>
#include <QFile>
#include <QTimerEvent>
#include <QDir>
#include <QFileInfo>
#include <stdio.h>
void FileLogger::refreshSettings() {
mutex.lock();
// Save old file name for later comparision with new settings
QString oldFileName=fileName;
// Load new config settings
settings->sync();
fileName=settings->value("fileName").toString();
// Convert relative fileName to absolute, based on the directory of the config file.
#ifdef Q_OS_WIN32
if (QDir::isRelativePath(fileName) && settings->format()!=QSettings::NativeFormat)
#else
if (QDir::isRelativePath(fileName))
#endif
{
QFileInfo configFile(settings->fileName());
fileName=QFileInfo(configFile.absolutePath(),fileName).absoluteFilePath();
}
maxSize=settings->value("maxSize",0).toLongLong();
maxBackups=settings->value("maxBackups",0).toInt();
msgFormat=settings->value("msgFormat","{timestamp} {type} {msg}").toString();
timestampFormat=settings->value("timestampFormat","{yyyy-MM-dd hh:mm:ss.zzz}").toString();
minLevel=static_cast<QtMsgType>(settings->value("minLevel",0).toInt());
bufferSize=settings->value("bufferSize",0).toInt();
disabled=settings->value("disabled",false).toBool();
// Create new file if the filename has been changed
if (oldFileName!=fileName) {
fprintf(stderr,"Logging to %s\n",qPrintable(fileName));
close();
open();
}
mutex.unlock();
}
FileLogger::FileLogger(QSettings* settings, const int refreshInterval, QObject* parent)
: Logger(parent)
{
Q_ASSERT(settings!=0);
Q_ASSERT(refreshInterval>=0);
this->settings=settings;
file=0;
if (refreshInterval>0)
refreshTimer.start(refreshInterval,this);
flushTimer.start(1000,this);
refreshSettings();
}
FileLogger::~FileLogger() {
close();
}
void FileLogger::write(const LogMessage* logMessage) {
// Write to the file
if (!disabled) {
// Try to write to the file
if (file) {
// Write the message
file->write(qPrintable(logMessage->toString(msgFormat,timestampFormat)));
// Flush error messages immediately, to ensure that no important message
// gets lost when the program terinates abnormally.
if (logMessage->getType()>=QtCriticalMsg) {
file->flush();
}
// Check for success
if (file->error()) {
close();
qWarning("Cannot write to log file %s: %s",qPrintable(fileName),qPrintable(file->errorString()));
}
}
// Fall-back to the super class method, if writing failed
if (!file) {
Logger::write(logMessage);
}
}
}
void FileLogger::open() {
if (fileName.isEmpty()) {
qWarning("Name of logFile is empty");
}
else {
file=new QFile(fileName);
if (!file->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
qWarning("Cannot open log file %s: %s",qPrintable(fileName),qPrintable(file->errorString()));
file=0;
}
}
}
void FileLogger::close() {
if (file) {
file->close();
delete file;
file=0;
}
}
void FileLogger::rotate() {
// count current number of existing backup files
int count=0;
forever {
QFile bakFile(QString("%1.%2").arg(fileName).arg(count+1));
if (bakFile.exists()) {
++count;
}
else {
break;
}
}
// Remove all old backup files that exceed the maximum number
while (maxBackups>0 && count>=maxBackups) {
QFile::remove(QString("%1.%2").arg(fileName).arg(count));
--count;
}
// Rotate backup files
for (int i=count; i>0; --i) {
QFile::rename(QString("%1.%2").arg(fileName).arg(i),QString("%1.%2").arg(fileName).arg(i+1));
}
// Backup the current logfile
QFile::rename(fileName,fileName+".1");
}
void FileLogger::timerEvent(QTimerEvent* event) {
if (!event) {
return;
}
else if (event->timerId()==refreshTimer.timerId()) {
refreshSettings();
}
else if (event->timerId()==flushTimer.timerId() && file) {
mutex.lock();
// Flush the I/O buffer
file->flush();
// Rotate the file if it is too large
if (maxSize>0 && file->size()>=maxSize) {
close();
rotate();
open();
}
mutex.unlock();
}
}

View File

@ -0,0 +1,131 @@
/**
@file
@author Stefan Frings
*/
#ifndef FILELOGGER_H
#define FILELOGGER_H
#include <QtGlobal>
#include <QSettings>
#include <QFile>
#include <QMutex>
#include <QBasicTimer>
#include "logger.h"
/**
Logger that uses a text file for output. Settings are read from a
config file using a QSettings object. Config settings can be changed at runtime.
They are copied to private fields in regular intervals for permance reason.
<p>
Example for the required configuration settings:
<code><pre>
disabled=false
fileName=logs/QtWebApp.log
maxSize=1000000
maxBackups=2
minLevel=0
msgformat={timestamp} {typeNr} {type} thread={thread}: {msg}
timestampFormat=dd.MM.yyyy hh:mm:ss.zzz
bufferSize=0
</pre></code>
- fileName is the name of the log file, relative to the directory of the settings file.
In case of windows, if the settings are in the registry, the path is relative to the current
working directory.
- maxSize is the maximum size of that file in bytes. The file will be backed up and
replaced by a new file if it becomes larger than this limit. Please note that
the actual file size may become a little bit larger than this limit. 0=unlimited.
- maxBackups defines the number of backup files to keep. 0=unlimited.
- minLevel defines the minimum level of message types to be written into the file.
- msgFormat defines the decoration of log messages.
- timestampFormat defines the format of timestamps.
- buffersize defines the size of the backtrace buffer. 0=disabled.
The buffer stores log messages of any level from the time before an error occurs.
It can be used to provide detailed debug information when an error occurs, while keeping
the logfile clean as long no error occurs. Using this buffer may reduce performance
significantly.
@see set() describes how to set logger variables
@see LogMessage for a description of the message decoration.
@see Logger for a descrition of the backtrace buffer
*/
class FileLogger : public Logger {
Q_OBJECT
Q_DISABLE_COPY(FileLogger)
public:
/**
Constructor.
@param settings Configuration settings, usually stored in an INI file. Must not be 0.
Settings are read from the current group, so the caller must have called settings->beginGroup().
Because the group must not change during runtime, it is recommended to provide a
separate QSettings instance to the logger that is not used by other parts of the program.
@param refreshInterval Interval of checking the config settings in msec, or 0=disabled
@param parent Parent object
*/
FileLogger(QSettings* settings, const int refreshInterval=10000, QObject* parent = 0);
/**
Destructor. Closes the file.
*/
virtual ~FileLogger();
/** Write a message to the log file */
virtual void write(const LogMessage* logMessage);
protected:
/**
Handler for timer events.
Refreshes config settings or synchronizes I/O buffer, depending on the event.
This method is thread-safe.
@param event used to distinguish between the two timers.
*/
void timerEvent(QTimerEvent* event);
private:
/** Configured name of the log file */
QString fileName;
/** Configured maximum size of the file in bytes, or 0=unlimited */
long maxSize;
/** Configured maximum number of backup files, or 0=unlimited */
int maxBackups;
/** Whether this logger is disabled */
bool disabled;
/** Pointer to the configuration settings */
QSettings* settings;
/** Output file, or 0=disabled */
QFile* file;
/** Timer for refreshing configuration settings */
QBasicTimer refreshTimer;
/** Timer for flushing the file I/O buffer */
QBasicTimer flushTimer;
/** Open the output file */
void open();
/** Close the output file */
void close();
/** Rotate files and delete some backups if there are too many */
void rotate();
/**
Refreshes the configuration settings.
This method is thread-safe.
*/
void refreshSettings();
};
#endif // FILELOGGER_H

View File

@ -0,0 +1,154 @@
/**
@file
@author Stefan Frings
*/
#include "logger.h"
#include <stdio.h>
#include <stdlib.h>
#include <QMutex>
#include <QDateTime>
#include <QThread>
Logger* Logger::defaultLogger=0;
QThreadStorage<QHash<QString,QString>*> Logger::logVars;
QThreadStorage<QList<LogMessage*>*> Logger::buffers;
QMutex Logger::mutex;
Logger::Logger(QObject* parent)
: QObject(parent),
msgFormat("{timestamp} {type} {msg}"),
timestampFormat("dd.MM.yyyy hh:mm:ss.zzz"),
minLevel(QtDebugMsg),
bufferSize(0)
{}
Logger::Logger(const QString msgFormat, const QString timestampFormat, const QtMsgType minLevel, const int bufferSize, QObject* parent)
:QObject(parent) {
this->msgFormat=msgFormat;
this->timestampFormat=timestampFormat;
this->minLevel=minLevel;
this->bufferSize=bufferSize;
}
void Logger::msgHandler(const QtMsgType type, const char* message) {
static QMutex recursiveMutex(QMutex::Recursive);
static QMutex nonRecursiveMutex(QMutex::NonRecursive);
// Prevent multiple threads from calling this method simultaneoulsy.
// But allow recursive calls, which is required to prevent a deadlock
// if the logger itself produces an error message.
recursiveMutex.lock();
// Fall back to stderr when this method has been called recursively.
if (defaultLogger && nonRecursiveMutex.tryLock()) {
defaultLogger->log(type,message);
nonRecursiveMutex.unlock();
}
else {
fputs(message,stderr);
fflush(stderr);
}
// Abort the program after logging a fatal message
if (type>=QtFatalMsg) {
abort();
}
recursiveMutex.unlock();
}
Logger::~Logger() {
if (defaultLogger==this) {
qInstallMsgHandler(0);
defaultLogger=0;
}
}
void Logger::write(const LogMessage* logMessage) {
fputs(qPrintable(logMessage->toString(msgFormat,timestampFormat)),stderr);
fflush(stderr);
}
void Logger::installMsgHandler() {
defaultLogger=this;
qInstallMsgHandler(msgHandler);
}
void Logger::set(const QString& name, const QString& value) {
mutex.lock();
if (!logVars.hasLocalData()) {
logVars.setLocalData(new QHash<QString,QString>);
}
logVars.localData()->insert(name,value);
mutex.unlock();
}
void Logger::clear(const bool buffer, const bool variables) {
mutex.lock();
if (buffer && buffers.hasLocalData()) {
QList<LogMessage*>* buffer=buffers.localData();
while (buffer && !buffer->isEmpty()) {
LogMessage* logMessage=buffer->takeLast();
delete logMessage;
}
}
if (variables && logVars.hasLocalData()) {
logVars.localData()->clear();
}
mutex.unlock();
}
void Logger::log(const QtMsgType type, const QString& message) {
mutex.lock();
// If the buffer is enabled, write the message into it
if (bufferSize>0) {
// Create new thread local buffer, if necessary
if (!buffers.hasLocalData()) {
buffers.setLocalData(new QList<LogMessage*>());
}
QList<LogMessage*>* buffer=buffers.localData();
// Append the decorated log message
LogMessage* logMessage=new LogMessage(type,message,logVars.localData());
buffer->append(logMessage);
// Delete oldest message if the buffer became too large
if (buffer->size()>bufferSize) {
delete buffer->takeFirst();
}
// If the type of the message is high enough, print the whole buffer
if (type>=minLevel) {
while (!buffer->isEmpty()) {
LogMessage* logMessage=buffer->takeFirst();
write(logMessage);
delete logMessage;
}
}
}
// Buffer is disabled, print the message if the type is high enough
else {
if (type>=minLevel) {
LogMessage logMessage(type,message,logVars.localData());
write(&logMessage);
}
}
mutex.unlock();
}

View File

@ -0,0 +1,144 @@
/**
@file
@author Stefan Frings
*/
#ifndef LOGGER_H
#define LOGGER_H
#include <QtGlobal>
#include <QThreadStorage>
#include <QHash>
#include <QStringList>
#include <QMutex>
#include "logmessage.h"
/**
Decorates and writes log messages to the console, stderr.
<p>
The decorator uses a predefined msgFormat string to enrich log messages
with additional information (e.g. timestamp).
<p>
The msgFormat string and also the message text may contain additional
variable names in the form <i>{name}</i> that are filled by values
taken from a static thread local dictionary.
<p>
The buffer stores log messages of any level from the time before an error occurs.
It can be used to provide detailed debug information when an error occurs, while
keeping the logfile clean as long no error occurs. Using this buffer may
reduce performance significantly.
<p>
The logger can be registered to handle messages from
the static global functions qDebug(), qWarning(), qCritical() and qFatal().
@see set() describes how to set logger variables
@see LogMessage for a description of the message decoration.
@warning You should prefer a derived class, for example FileLogger,
because logging to the console is less useful.
*/
class Logger : public QObject {
Q_OBJECT
Q_DISABLE_COPY(Logger)
public:
/**
Constructor.
Uses the same defaults as the other constructor.
@param parent Parent object
*/
Logger(QObject* parent);
/**
Constructor.
@param msgFormat Format of the decoration, e.g. "{timestamp} {type} thread={thread}: {msg}"
@param timestampFormat Format of timestamp, e.g. "dd.MM.yyyy hh:mm:ss.zzz"
@param minLevel Minimum type of messages that are written out.
@param bufferSize Size of the backtrace buffer, number of messages per thread. 0=disabled.
@param parent Parent object
@see LogMessage for a description of the message decoration.
*/
Logger(const QString msgFormat="{timestamp} {type} {msg}", const QString timestampFormat="dd.MM.yyyy hh:mm:ss.zzz", const QtMsgType minLevel=QtDebugMsg, const int bufferSize=0, QObject* parent = 0);
/** Destructor */
virtual ~Logger();
/**
Decorate and log the message, if type>=minLevel.
This method is thread safe.
@param type Message type (level)
@param message Message text
@see LogMessage for a description of the message decoration.
*/
virtual void log(const QtMsgType type, const QString& message);
/**
Installs this logger as the default message handler, so it
can be used through the global static logging functions (e.g. qDebug()).
*/
void installMsgHandler();
/**
Sets a thread-local variable that may be used to decorate log messages.
This method is thread safe.
@param name Name of the variable
@param value Value of the variable
*/
static void set(const QString& name, const QString& value);
/**
Clear the thread-local data of the current thread.
@param buffer Whether to clear the backtrace buffer
@param variables Whether to clear the log variables
*/
static void clear(const bool buffer=true, const bool variables=true);
protected:
/** Format string for message decoration */
QString msgFormat;
/** Format string of timestamps */
QString timestampFormat;
/** Minimum level of message types that are written out */
QtMsgType minLevel;
/** Size of backtrace buffer, number of messages per thread. 0=disabled */
int bufferSize;
/** Used to synchronize access to the static members */
static QMutex mutex;
/**
Decorate and write a log message to stderr. Override this method
to provide a different output medium.
*/
virtual void write(const LogMessage* logMessage);
private:
/** Pointer to the default logger, used by msgHandler() */
static Logger* defaultLogger;
/**
Message Handler for the global static logging functions (e.g. qDebug()).
Forward calls to the default logger.
<p>
In case of a fatal message, the program will abort.
Variables in the in the message are replaced by their values.
This method is thread safe.
@param type Message type (level)
@param message Message text
*/
static void msgHandler(const QtMsgType type, const char* message);
/** Thread local variables to be used in log messages */
static QThreadStorage<QHash<QString,QString>*> logVars;
/** Thread local backtrace buffers */
static QThreadStorage<QList<LogMessage*>*> buffers;
};
#endif // LOGGER_H

View File

@ -0,0 +1,68 @@
/**
@file
@author Stefan Frings
*/
#include "logmessage.h"
#include <QThread>
LogMessage::LogMessage(const QtMsgType type, const QString& message, QHash<QString,QString>* logVars) {
this->type=type;
this->message=message;
timestamp=QDateTime::currentDateTime();
threadId=QThread::currentThreadId();
// Copy the logVars if not null,
// so that later changes in the original do not affect the copy
if (logVars) {
this->logVars=*logVars;
}
}
QString LogMessage::toString(const QString& msgFormat, const QString& timestampFormat) const {
QString decorated=msgFormat+"\n";
decorated.replace("{msg}",message);
if (decorated.contains("{timestamp}")) {
decorated.replace("{timestamp}",QDateTime::currentDateTime().toString(timestampFormat));
}
QString typeNr;
typeNr.setNum(type);
decorated.replace("{typeNr}",typeNr);
switch (type) {
case QtDebugMsg:
decorated.replace("{type}","DEBUG");
break;
case QtWarningMsg:
decorated.replace("{type}","WARNING");
break;
case QtCriticalMsg:
decorated.replace("{type}","CRITICAL");
break;
case QtFatalMsg:
decorated.replace("{type}","FATAL");
break;
default:
decorated.replace("{type}",typeNr);
}
QString threadId;
threadId.setNum((unsigned int)QThread::currentThreadId());
decorated.replace("{thread}",threadId);
// Fill in variables
if (decorated.contains("{") && !logVars.isEmpty()) {
QList<QString> keys=logVars.keys();
foreach (QString key, keys) {
decorated.replace("{"+key+"}",logVars.value(key));
}
}
return decorated;
}
QtMsgType LogMessage::getType() const {
return type;
}

View File

@ -0,0 +1,74 @@
/**
@file
@author Stefan Frings
*/
#ifndef LOGMESSAGE_H
#define LOGMESSAGE_H
#include <QtGlobal>
#include <QDateTime>
#include <QHash>
/**
Represents a single log message together with some data
that are used to decorate the log message.
The following variables may be used in the message and in msgFormat:
- {timestamp} Date and time of creation
- {typeNr} Type of the message in numeric format (0-3)
- {type} Type of the message in string format (DEBUG, WARNING, CRITICAL, FATAL)
- {thread} ID number of the thread
- {msg} Message text (only useable in msgFormat)
- {xxx} For any user-defined logger variable
*/
class LogMessage
{
Q_DISABLE_COPY(LogMessage)
public:
/**
Constructor. All parameters are copied, so that later changes to them do not
affect this object.
@param type Type of the message
@param message Message text
@param logVars Logger variables, 0 is allowed
*/
LogMessage(const QtMsgType type, const QString& message, QHash<QString,QString>* logVars);
/**
Returns the log message as decorated string.
@param msgFormat Format of the decoration. May contain variables and static text,
e.g. "{timestamp} {type} thread={thread}: {msg}"
@param timestampFormat Format of timestamp, e.g. "dd.MM.yyyy hh:mm:ss.zzz"
@see QDatetime for a description of the timestamp format pattern
*/
QString toString(const QString& msgFormat, const QString& timestampFormat) const;
/**
Get the message type.
*/
QtMsgType getType() const;
private:
/** Logger variables */
QHash<QString,QString> logVars;
/** Date and time of creation */
QDateTime timestamp;
/** Type of the message */
QtMsgType type;
/** ID number of the thread */
Qt::HANDLE threadId;
/** Message text */
QString message;
};
#endif // LOGMESSAGE_H

View File

@ -0,0 +1,7 @@
INCLUDEPATH += $$PWD
DEPENDPATH += $$PWD
HEADERS += $$PWD/template.h $$PWD/templateloader.h $$PWD/templatecache.h
SOURCES += $$PWD/template.cpp $$PWD/templateloader.cpp $$PWD/templatecache.cpp
OTHER_FILES += $$PWD/../doc/readme.txt

View File

@ -0,0 +1,187 @@
/**
@file
@author Stefan Frings
*/
#include "template.h"
#include <QFileInfo>
Template::Template(QString source, QString sourceName)
: QString(source) {
this->sourceName=sourceName;
this->warnings=false;
}
Template::Template(QFile& file, QTextCodec* textCodec) {
sourceName=QFileInfo(file.fileName()).baseName();
if (!file.isOpen()) {
file.open(QFile::ReadOnly | QFile::Text);
}
QByteArray data=file.readAll();
file.close();
if (data.size()==0 || file.error()) {
qCritical("Template: cannot read from %s, %s",qPrintable(sourceName),qPrintable(file.errorString()));
append(textCodec->toUnicode(data));
}
}
int Template::setVariable(QString name, QString value) {
int count=0;
QString variable="{"+name+"}";
int start=indexOf(variable);
while (start>=0) {
replace(start, variable.length(), value);
count++;
start=indexOf(variable,start+variable.length());
}
if (count==0 && warnings) {
qWarning("Template: missing variable %s in %s",qPrintable(variable),qPrintable(sourceName));
}
return count;
}
int Template::setCondition(QString name, bool value) {
int count=0;
QString startTag=QString("{if %1}").arg(name);
QString elseTag=QString("{else %1}").arg(name);
QString endTag=QString("{end %1}").arg(name);
// search for if-else-end
int start=indexOf(startTag);
while (start>=0) {
int end=indexOf(endTag,start+startTag.length());
if (end>=0) {
count++;
int ellse=indexOf(elseTag,start+startTag.length());
if (ellse>start && ellse<end) { // there is an else part
if (value==true) {
QString truePart=mid(start+startTag.length(), ellse-start-startTag.length());
replace(start, end-start+endTag.length(), truePart);
}
else { // value==false
QString falsePart=mid(ellse+elseTag.length(), end-ellse-elseTag.length());
replace(start, end-start+endTag.length(), falsePart);
}
}
else if (value==true) { // and no else part
QString truePart=mid(start+startTag.length(), end-start-startTag.length());
replace(start, end-start+endTag.length(), truePart);
}
else { // value==false and no else part
replace(start, end-start+endTag.length(), "");
}
start=indexOf(startTag,start);
}
else {
qWarning("Template: missing condition end %s in %s",qPrintable(endTag),qPrintable(sourceName));
}
}
// search for ifnot-else-end
QString startTag2="{ifnot "+name+"}";
start=indexOf(startTag2);
while (start>=0) {
int end=indexOf(endTag,start+startTag2.length());
if (end>=0) {
count++;
int ellse=indexOf(elseTag,start+startTag2.length());
if (ellse>start && ellse<end) { // there is an else part
if (value==false) {
QString falsePart=mid(start+startTag2.length(), ellse-start-startTag2.length());
replace(start, end-start+endTag.length(), falsePart);
}
else { // value==true
QString truePart=mid(ellse+elseTag.length(), end-ellse-elseTag.length());
replace(start, end-start+endTag.length(), truePart);
}
}
else if (value==false) { // and no else part
QString falsePart=mid(start+startTag2.length(), end-start-startTag2.length());
replace(start, end-start+endTag.length(), falsePart);
}
else { // value==true and no else part
replace(start, end-start+endTag.length(), "");
}
start=indexOf(startTag2,start);
}
else {
qWarning("Template: missing condition end %s in %s",qPrintable(endTag),qPrintable(sourceName));
}
}
if (count==0 && warnings) {
qWarning("Template: missing condition %s or %s in %s",qPrintable(startTag),qPrintable(startTag2),qPrintable(sourceName));
}
return count;
}
int Template::loop(QString name, int repetitions) {
Q_ASSERT(repetitions>=0);
int count=0;
QString startTag="{loop "+name+"}";
QString elseTag="{else "+name+"}";
QString endTag="{end "+name+"}";
// search for loop-else-end
int start=indexOf(startTag);
while (start>=0) {
int end=indexOf(endTag,start+startTag.length());
if (end>=0) {
count++;
int ellse=indexOf(elseTag,start+startTag.length());
if (ellse>start && ellse<end) { // there is an else part
if (repetitions>0) {
QString loopPart=mid(start+startTag.length(), ellse-start-startTag.length());
QString insertMe;
for (int i=0; i<repetitions; ++i) {
// number variables, conditions and sub-loop within the loop
QString nameNum=name+QString::number(i);
QString s=loopPart;
s.replace(QString("{%1.").arg(name), QString("{%1.").arg(nameNum));
s.replace(QString("{if %1.").arg(name), QString("{if %1.").arg(nameNum));
s.replace(QString("{ifnot %1.").arg(name), QString("{ifnot %1.").arg(nameNum));
s.replace(QString("{else %1.").arg(name), QString("{else %1.").arg(nameNum));
s.replace(QString("{end %1.").arg(name), QString("{end %1.").arg(nameNum));
s.replace(QString("{loop %1.").arg(name), QString("{loop %1.").arg(nameNum));
insertMe.append(s);
}
replace(start, end-start+endTag.length(), insertMe);
}
else { // repetitions==0
QString elsePart=mid(ellse+elseTag.length(), end-ellse-elseTag.length());
replace(start, end-start+endTag.length(), elsePart);
}
}
else if (repetitions>0) { // and no else part
QString loopPart=mid(start+startTag.length(), end-start-startTag.length());
QString insertMe;
for (int i=0; i<repetitions; ++i) {
// number variables, conditions and sub-loop within the loop
QString nameNum=name+QString::number(i);
QString s=loopPart;
s.replace(QString("{%1.").arg(name), QString("{%1.").arg(nameNum));
s.replace(QString("{if %1.").arg(name), QString("{if %1.").arg(nameNum));
s.replace(QString("{ifnot %1.").arg(name), QString("{ifnot %1.").arg(nameNum));
s.replace(QString("{else %1.").arg(name), QString("{else %1.").arg(nameNum));
s.replace(QString("{end %1.").arg(name), QString("{end %1.").arg(nameNum));
s.replace(QString("{loop %1.").arg(name), QString("{loop %1.").arg(nameNum));
insertMe.append(s);
}
replace(start, end-start+endTag.length(), insertMe);
}
else { // repetitions==0 and no else part
replace(start, end-start+endTag.length(), "");
}
start=indexOf(startTag,start);
}
else {
qWarning("Template: missing loop end %s in %s",qPrintable(endTag),qPrintable(sourceName));
}
}
if (count==0 && warnings) {
qWarning("Template: missing loop %s in %s",qPrintable(startTag),qPrintable(sourceName));
}
return count;
}
void Template::enableWarnings(bool enable) {
warnings=enable;
}

View File

@ -0,0 +1,167 @@
/**
@file
@author Stefan Frings
*/
#ifndef TEMPLATE_H
#define TEMPLATE_H
#include <QString>
#include <QRegExp>
#include <QIODevice>
#include <QTextCodec>
#include <QFile>
#include <QString>
/**
Enhanced version of QString for template processing. Templates
are usually loaded from files, but may also be loaded from
prepared Strings.
Example template file:
<p><code><pre>
Hello {username}, how are you?
{if locked}
Your account is locked.
{else locked}
Welcome on our system.
{end locked}
The following users are on-line:
Username Time
{loop user}
{user.name} {user.time}
{end user}
</pre></code></p>
<p>
Example code to fill this template:
<p><code><pre>
Template t(QFile("test.tpl"),QTextCode::codecForName("UTF-8"));
t.setVariable("user", "Stefan");
t.setCondition("locked",false);
t.loop("user",2);
t.setVariable("user0.name,"Markus");
t.setVariable("user0.time,"8:30");
t.setVariable("user1.name,"Roland");
t.setVariable("user1.time,"8:45");
</pre></code></p>
<p>
The code example above shows how variable within loops are numbered.
Counting starts with 0. Loops can be nested, for example:
<p><code><pre>
&lt;table&gt;
{loop row}
&lt;tr&gt;
{loop row.column}
&lt;td&gt;{row.column.value}&lt;/td&gt;
{end row.column}
&lt;/tr&gt;
{end row}
&lt;/table&gt;
</pre></code></p>
<p>
Example code to fill this nested loop with 3 rows and 4 columns:
<p><code><pre>
t.loop("row",3);
t.loop("row0.column",4);
t.setVariable("row0.column0.value","a");
t.setVariable("row0.column1.value","b");
t.setVariable("row0.column2.value","c");
t.setVariable("row0.column3.value","d");
t.loop("row1.column",4);
t.setVariable("row1.column0.value","e");
t.setVariable("row1.column1.value","f");
t.setVariable("row1.column2.value","g");
t.setVariable("row1.column3.value","h");
t.loop("row2.column",4);
t.setVariable("row2.column0.value","i");
t.setVariable("row2.column1.value","j");
t.setVariable("row2.column2.value","k");
t.setVariable("row2.column3.value","l");
</pre></code></p>
@see TemplateLoader
@see TemplateCache
*/
class Template : public QString {
public:
/**
Constructor that reads the template from a string.
@param source The template source text
@param sourceName Name of the source file, used for logging
*/
Template(QString source, QString sourceName);
/**
Constructor that reads the template from a file. Note that this class does not
cache template files by itself, so using this constructor is only recommended
to be used on local filesystem.
@param file File that provides the source text
@param textCodec Encoding of the source
@see TemplateLoader
@see TemplateCache
*/
Template(QFile& file, QTextCodec* textCodec);
/**
Replace a variable by the given value.
Affects tags with the syntax
- {name}
After settings the
value of a variable, the variable does not exist anymore,
it it cannot be changed multiple times.
@param name name of the variable
@param value new value
@return The count of variables that have been processed
*/
int setVariable(QString name, QString value);
/**
Set a condition. This affects tags with the syntax
- {if name}...{end name}
- {if name}...{else name}...{end name}
- {ifnot name}...{end name}
- {ifnot name}...{else name}...{end name}
@param name Name of the condition
@param value Value of the condition
@return The count of conditions that have been processed
*/
int setCondition(QString name, bool value);
/**
Set number of repetitions of a loop.
This affects tags with the syntax
- {loop name}...{end name}
- {loop name}...{else name}...{end name}
@param name Name of the loop
@param repetitions The number of repetitions
@return The number of loops that have been processed
*/
int loop(QString name, int repetitions);
/**
Enable warnings for missing tags
@param enable Warnings are enabled, if true
*/
void enableWarnings(bool enable=true);
private:
/** Name of the source file */
QString sourceName;
/** Enables warnings, if true */
bool warnings;
};
#endif // TEMPLATE_H

View File

@ -0,0 +1,30 @@
#include "templatecache.h"
#include <QDateTime>
#include <QStringList>
#include <QSet>
TemplateCache::TemplateCache(QSettings* settings, QObject* parent)
:TemplateLoader(settings,parent)
{
cache.setMaxCost(settings->value("cacheSize","1000000").toInt());
cacheTimeout=settings->value("cacheTime","60000").toInt();
qDebug("TemplateCache: timeout=%i, size=%i",cacheTimeout,cache.maxCost());
}
QString TemplateCache::tryFile(QString localizedName) {
qint64 now=QDateTime::currentMSecsSinceEpoch();
// search in cache
qDebug("TemplateCache: trying cached %s",qPrintable(localizedName));
CacheEntry* entry=cache.object(localizedName);
if (entry && (cacheTimeout==0 || entry->created>now-cacheTimeout)) {
return entry->document;
}
// search on filesystem
entry=new CacheEntry();
entry->created=now;
entry->document=TemplateLoader::tryFile(localizedName);
// Store in cache even when the file did not exist, to remember that there is no such file
cache.insert(localizedName,entry,entry->document.size());
return entry->document;
}

View File

@ -0,0 +1,77 @@
#ifndef TEMPLATECACHE_H
#define TEMPLATECACHE_H
#include "templateloader.h"
#include <QCache>
/**
Caching template loader, reduces the amount of I/O and improves performance
on remote file systems. The cache has a limited size, it prefers to keep
the last recently used files. Optionally, the maximum time of cached entries
can be defined to enforce a reload of the template file after a while.
<p>
In case of local file system, the use of this cache is optionally, since
the operating system caches files already.
<p>
Loads localized versions of template files. If the caller requests a file with the
name "index" and the suffix is ".tpl" and the requested locale is "de_DE, de, en-US",
then files are searched in the following order:
- index-de_DE.tpl
- index-de.tpl
- index-en_US.tpl
- index-en.tpl
- index.tpl
<p>
The following settings are required:
<code><pre>
path=.
suffix=.tpl
encoding=UTF-8
cacheSize=1000000
cacheTime=60000
</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>
Files are cached as long as possible, when cacheTime=0.
@see TemplateLoader
*/
class TemplateCache : public TemplateLoader {
Q_OBJECT
Q_DISABLE_COPY(TemplateCache);
public:
/**
Constructor.
@param settings configurations settings
@param parent Parent object
*/
TemplateCache(QSettings* settings, QObject* parent=0);
protected:
/**
Try to get a file from cache or filesystem.
@param localizedName Name of the template with locale to find
@return The template document, or empty string if not found
*/
virtual QString tryFile(QString localizedName);
private:
struct CacheEntry {
QString document;
qint64 created;
};
/** Timeout for each cached file */
int cacheTimeout;
/** Cache storage */
QCache<QString,CacheEntry> cache;
};
#endif // TEMPLATECACHE_H

View File

@ -0,0 +1,103 @@
/**
@file
@author Stefan Frings
*/
#include "templateloader.h"
#include <QFile>
#include <QFileInfo>
#include <QStringList>
#include <QDir>
#include <QSet>
TemplateLoader::TemplateLoader(QSettings* settings, QObject* parent)
: QObject(parent)
{
templatePath=settings->value("path",".").toString();
// Convert relative path to absolute, based on the directory of the config file.
#ifdef Q_OS_WIN32
if (QDir::isRelativePath(templatePath) && settings->format()!=QSettings::NativeFormat)
#else
if (QDir::isRelativePath(templatePath))
#endif
{
QFileInfo configFile(settings->fileName());
templatePath=QFileInfo(configFile.absolutePath(),templatePath).absoluteFilePath();
}
fileNameSuffix=settings->value("suffix",".tpl").toString();
QString encoding=settings->value("encoding").toString();
if (encoding.isEmpty()) {
textCodec=QTextCodec::codecForLocale();
}
else {
textCodec=QTextCodec::codecForName(encoding.toAscii());
}
qDebug("TemplateLoader: path=%s, codec=%s",qPrintable(templatePath),textCodec->name().data());
}
TemplateLoader::~TemplateLoader() {}
QString TemplateLoader::tryFile(QString localizedName) {
QString fileName=templatePath+"/"+localizedName+fileNameSuffix;
qDebug("TemplateCache: trying file %s",qPrintable(fileName));
QFile file(fileName);
if (file.exists()) {
file.open(QIODevice::ReadOnly);
QString document=textCodec->toUnicode(file.readAll());
file.close();
if (file.error()) {
qCritical("TemplateLoader: cannot load file %s, %s",qPrintable(fileName),qPrintable(file.errorString()));
return "";
}
else {
return document;
}
}
return "";
}
Template TemplateLoader::getTemplate(QString templateName, QString locales) {
mutex.lock();
QSet<QString> tried; // used to suppress duplicate attempts
QStringList locs=locales.split(',',QString::SkipEmptyParts);
// Search for exact match
foreach (QString loc,locs) {
loc.replace(QRegExp(";.*"),"");
loc.replace('-','_');
QString localizedName=templateName+"-"+loc.trimmed();
if (!tried.contains(localizedName)) {
QString document=tryFile(localizedName);
if (!document.isEmpty()) {
mutex.unlock();
return Template(document,localizedName);
}
tried.insert(localizedName);
}
}
// Search for correct language but any country
foreach (QString loc,locs) {
loc.replace(QRegExp("[;_-].*"),"");
QString localizedName=templateName+"-"+loc.trimmed();
if (!tried.contains(localizedName)) {
QString document=tryFile(localizedName);
if (!document.isEmpty()) {
mutex.unlock();
return Template(document,localizedName);
}
tried.insert(localizedName);
}
}
// Search for default file
QString document=tryFile(templateName);
if (!document.isEmpty()) {
mutex.unlock();
return Template(document,templateName);
}
qCritical("TemplateCache: cannot find template %s",qPrintable(templateName));
mutex.unlock();
return Template("",templateName);
}

View File

@ -0,0 +1,85 @@
/**
@file
@author Stefan Frings
*/
#ifndef TEMPLATELOADER_H
#define TEMPLATELOADER_H
#include <QString>
#include <QSettings>
#include <QTextCodec>
#include "template.h"
#include <QMutex>
/**
Loads localized versions of template files. If the caller requests a file with the
name "index" and the suffix is ".tpl" and the requested locale is "de_DE, de, en-US",
then files are searched in the following order:
- index-de_DE.tpl
- index-de.tpl
- index-en_US.tpl
- index-en.tpl
- index.tpl
The following settings are required:
<code><pre>
path=.
suffix=.tpl
encoding=UTF-8
</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.
@see TemplateCache
*/
class TemplateLoader : public QObject {
Q_OBJECT
Q_DISABLE_COPY(TemplateLoader);
public:
/**
Constructor.
@param settings configurations settings
@param parent parent object
*/
TemplateLoader(QSettings* settings, QObject* parent=0);
/** Destructor */
virtual ~TemplateLoader();
/**
Get a template for a given locale.
This method is thread safe.
@param templateName base name of the template file, without suffix and without locale
@param locales Requested locale(s), e.g. "de_DE, en_EN". Strings in the format of
the HTTP header Accept-Locale may be used. Badly formatted parts in the string are silently
ignored.
@return If the template cannot be loaded, an error message is logged and an empty template is returned.
*/
Template getTemplate(QString templateName, QString locales=QString());
protected:
/**
Try to get a file from cache or filesystem.
@param localizedName Name of the template with locale to find
@return The template document, or empty string if not found
*/
virtual QString tryFile(QString localizedName);
/** Directory where the templates are searched */
QString templatePath;
/** Suffix to the filenames */
QString fileNameSuffix;
/** Codec for decoding the files */
QTextCodec* textCodec;
/** Used to synchronize threads */
QMutex mutex;
};
#endif // TEMPLATELOADER_H

View File

@ -0,0 +1,58 @@
/**
@file
@author Stefan Frings
*/
/**
@mainpage
This project provies libraries for writing server-side web application in C++
based on the Qt toolkit. It is a light-weight implementation that works like
Java Servlets from the programmers point of view.
<p>
Features:
- HTTP 1.1 web server
- persistent connections
- chunked and non-chunked transfer
- file uploads (multipart encoded forms)
- cookies
- sessions
- uses dynamic thread pool
- optional file cache
- Template engine
- multi-language
- optional file cache
- Logger
- configurable without program restart
- automatic backup and file rotation
- configurable message format
- messages may contain thread-local variables
- optional buffer for writing history details in case of an error
- Example application
- Install and run as Windows service, unix daemon or at the command-line
- Search config file in multiple common directories
- Demonstrates how to write servlets for common use-cases
If you write a real application based on this source, take a look into startup.cpp,
which contains startup and shutdown procedures. The example source sets
up a single listener on port 8080, however multiple listeners with individual
configuration could be set up.
<p>
Incoming requests are mapped to controller classes in requestmapper.cpp, based on the
requested path. If you want to learn form the example, then focus on these classes.
<p>
High-availability and HTTPS encryption can be easily added by putting an Apache HTTP server
in front of the self-written web application using the mod-proxy module with sticky sessions.
*/
#include "startup.h"
/**
Entry point of the program.
Passes control to the service helper.
*/
int main(int argc, char *argv[]) {
// Use a qtservice wrapper to start the application as a Windows service or Unix daemon
Startup startup(argc, argv);
return startup.exec();
}

View File

@ -0,0 +1,46 @@
/**
@file
@author Stefan Frings
*/
#include "requestmapper.h"
#include "static.h"
#include "staticfilecontroller.h"
#include "controllers/dumpcontroller.h"
#include "controllers/templatecontroller.h"
#include "controllers/formcontroller.h"
#include "controllers/fileuploadcontroller.h"
#include "controllers/sessioncontroller.h"
RequestMapper::RequestMapper(QObject* parent)
:HttpRequestHandler(parent) {}
void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
QByteArray path=request.getPath();
qDebug("RequestMapper: path=%s",path.data());
if (path.startsWith("/dump")) {
DumpController().service(request, response);
}
else if (path.startsWith("/template")) {
TemplateController().service(request, response);
}
else if (path.startsWith("/form")) {
FormController().service(request, response);
}
else if (path.startsWith("/file")) {
FileUploadController().service(request, response);
}
else if (path.startsWith("/session")) {
SessionController().service(request, response);
}
// All other pathes are mapped to the static file controller.
else {
Static::staticFileController->service(request, response);
}
}

View File

@ -0,0 +1,36 @@
/**
@file
@author Stefan Frings
*/
#ifndef REQUESTMAPPER_H
#define REQUESTMAPPER_H
#include "httprequesthandler.h"
/**
The request mapper dispatches incoming HTTP requests to controller classes
depending on the requested path.
*/
class RequestMapper : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(RequestMapper)
public:
/**
Constructor.
@param parent Parent object
*/
RequestMapper(QObject* parent=0);
/**
Dispatch a request to a controller.
@param request The received HTTP request
@param response Must be used to return the response
*/
void service(HttpRequest& request, HttpResponse& response);
};
#endif // REQUESTMAPPER_H

View File

@ -0,0 +1,26 @@
INCLUDEPATH += $$PWD
DEPENDPATH += $$PWD
HEADERS += \
$$PWD/static.h \
$$PWD/startup.h \
$$PWD/requestmapper.h \
$$PWD/controllers/dumpcontroller.h \
$$PWD/controllers/templatecontroller.h \
$$PWD/controllers/formcontroller.h \
$$PWD/controllers/fileuploadcontroller.h \
$$PWD/controllers/sessioncontroller.h
SOURCES += \
$$PWD/static.cpp \
$$PWD/startup.cpp \
$$PWD/requestmapper.cpp \
$$PWD/controllers/dumpcontroller.cpp \
$$PWD/controllers/templatecontroller.cpp \
$$PWD/controllers/formcontroller.cpp \
$$PWD/controllers/fileuploadcontroller.cpp \
$$PWD/controllers/sessioncontroller.cpp
include(lib/bfLogging/bfLogging.pri)
include(lib/bfHttpServer/bfHttpServer.pri)
include(lib/bfTemplateEngine/bfTemplateEngine.pri)

View File

@ -0,0 +1,77 @@
/**
@file
@author Stefan Frings
*/
#include "static.h"
#include "startup.h"
#include "dualfilelogger.h"
#include "httplistener.h"
#include "requestmapper.h"
#include "staticfilecontroller.h"
#include <QDir>
#include <QApplication>
/** Name of this application */
#define APPNAME "YACReaderLibrary"
/** Publisher of this application */
#define ORGANISATION "Butterfly"
/** Short description of this application */
#define DESCRIPTION "Web service based on Qt"
void Startup::start() {
// Initialize the core application
QCoreApplication* app = QApplication::instance();
app->setApplicationName(APPNAME);
//app->setOrganizationName(ORGANISATION);
QString configFileName=Static::getConfigDir()+"/"+QCoreApplication::applicationName()+".ini";
// Configure logging into files
QSettings* mainLogSettings=new QSettings(configFileName,QSettings::IniFormat,app);
mainLogSettings->beginGroup("mainLogFile");
QSettings* debugLogSettings=new QSettings(configFileName,QSettings::IniFormat,app);
debugLogSettings->beginGroup("debugLogFile");
Logger* logger=new DualFileLogger(mainLogSettings,debugLogSettings,10000,app);
logger->installMsgHandler();
// Configure template loader and cache
QSettings* templateSettings=new QSettings(configFileName,QSettings::IniFormat,app);
templateSettings->beginGroup("templates");
Static::templateLoader=new TemplateCache(templateSettings,app);
// Configure session store
QSettings* sessionSettings=new QSettings(configFileName,QSettings::IniFormat,app);
sessionSettings->beginGroup("sessions");
Static::sessionStore=new HttpSessionStore(sessionSettings,app);
// Configure static file controller
QSettings* fileSettings=new QSettings(configFileName,QSettings::IniFormat,app);
fileSettings->beginGroup("docroot");
Static::staticFileController=new StaticFileController(fileSettings,app);
// Configure and start the TCP listener
qDebug("ServiceHelper: Starting service");
QSettings* listenerSettings=new QSettings(configFileName,QSettings::IniFormat,app);
listenerSettings->beginGroup("listener");
listener = new HttpListener(listenerSettings,new RequestMapper(app),app);
qDebug("ServiceHelper: Service has started");
}
void Startup::stop() {
qDebug("ServiceHelper: Service has been stopped");
// QCoreApplication destroys all objects that have been created in start().
listener->close();
}
Startup::Startup()
{
}

View File

@ -0,0 +1,30 @@
/**
@file
@author Stefan Frings
*/
#ifndef STARTUP_H
#define STARTUP_H
class HttpListener;
/**
Helper class to install and run the application as a windows
service.
*/
class Startup
{
private:
//QTcpServer
HttpListener * listener;
public:
/** Constructor */
Startup();
/** Start the server */
void start();
/** Stop the server */
void stop();
protected:
};
#endif // STARTUP_H

View File

@ -0,0 +1,59 @@
/**
@file
@author Stefan Frings
*/
#include "static.h"
#include <QCoreApplication>
#include <QDir>
#include <QFile>
#include <QString>
QString Static::configDir=0;
TemplateLoader* Static::templateLoader=0;
HttpSessionStore* Static::sessionStore=0;
StaticFileController* Static::staticFileController=0;
QString Static::getConfigFileName() {
return QString("%1/%2.ini").arg(getConfigDir()).arg(QCoreApplication::applicationName());
}
QString Static::getConfigDir() {
if (!configDir.isNull()) {
return configDir;
}
// Search config file
QString binDir=QCoreApplication::applicationDirPath();
QString organization=QCoreApplication::organizationName();
QString configFileName=QCoreApplication::applicationName()+".ini";
QStringList searchList;
searchList.append(QDir::cleanPath(binDir));
searchList.append(QDir::cleanPath(binDir+"/../etc"));
searchList.append(QDir::cleanPath(binDir+"/../../etc")); // for development under windows
searchList.append(QDir::rootPath()+"etc/xdg/"+organization);
searchList.append(QDir::rootPath()+"etc/opt");
searchList.append(QDir::rootPath()+"etc");
foreach (QString dir, searchList) {
QFile file(dir+"/"+configFileName);
if (file.exists()) {
// found
configDir=dir;
qDebug("Using config file %s",qPrintable(file.fileName()));
return configDir;
}
}
// not found
foreach (QString dir, searchList) {
qWarning("%s/%s not found",qPrintable(dir),qPrintable(configFileName));
}
qFatal("Cannot find config file %s",qPrintable(configFileName));
return 0;
}

View File

@ -0,0 +1,64 @@
/**
@file
@author Stefan Frings
*/
#ifndef STATIC_H
#define STATIC_H
#include <QString>
#include "templatecache.h"
#include "httpsessionstore.h"
#include "staticfilecontroller.h"
/**
This class contains some static resources that are used by the application.
*/
class Static
{
public:
/**
Search the main config file and return its full path.
On the first call, the INI file gets searched. If not found,
the application aborts with an error message.
<p>
The filename is the applications name plus the ending ".ini". It is searched
in the following directories:
- Same directory as the applications executable file
- In ../etc relative to the applications executable file
- In ../../etc relative to the applications executable file
- In /etc/xdg/{organisation name} on the root drive
- In /etc/opt on the root drive
- In /etc on the root drive
*/
static QString getConfigFileName();
/**
Gets the directory where the main config file is located.
On the first call, the INI file gets searched. If not found,
the application aborts with an error message.
@see getConfigFileName()
*/
static QString getConfigDir();
/** Cache for template files */
static TemplateLoader* templateLoader;
/** Storage for session cookies */
static HttpSessionStore* sessionStore;
/** Controller for static files */
static StaticFileController* staticFileController;
private:
/** Directory of the main config file */
static QString configDir;
};
#endif // STATIC_H