From 2598d6494abfea6d10c032a84994c28151797ea1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20=C3=81ngel=20San=20Mart=C3=ADn?= Date: Sun, 20 May 2012 23:19:29 +0200 Subject: [PATCH] primera versi?n con servidor http incorporado --- YACReaderLibrary/YACReaderLibrary.pro | 10 +- YACReaderLibrary/bundle_creator.cpp | 13 + YACReaderLibrary/bundle_creator.h | 14 + YACReaderLibrary/main.cpp | 5 +- .../server/controllers/dumpcontroller.cpp | 62 +++ .../server/controllers/dumpcontroller.h | 29 ++ .../controllers/fileuploadcontroller.cpp | 38 ++ .../server/controllers/fileuploadcontroller.h | 30 ++ .../server/controllers/formcontroller.cpp | 33 ++ .../server/controllers/formcontroller.h | 30 ++ .../server/controllers/sessioncontroller.cpp | 31 ++ .../server/controllers/sessioncontroller.h | 29 ++ .../server/controllers/templatecontroller.cpp | 31 ++ .../server/controllers/templatecontroller.h | 30 ++ YACReaderLibrary/server/documentcache.h | 4 + .../server/lib/bfHttpServer/bfHttpServer.pri | 12 + .../bfHttpServer/httpconnectionhandler.cpp | 154 +++++++ .../lib/bfHttpServer/httpconnectionhandler.h | 97 ++++ .../httpconnectionhandlerpool.cpp | 55 +++ .../bfHttpServer/httpconnectionhandlerpool.h | 69 +++ .../server/lib/bfHttpServer/httpcookie.cpp | 199 ++++++++ .../server/lib/bfHttpServer/httpcookie.h | 110 +++++ .../server/lib/bfHttpServer/httplistener.cpp | 56 +++ .../server/lib/bfHttpServer/httplistener.h | 76 +++ .../server/lib/bfHttpServer/httprequest.cpp | 435 ++++++++++++++++++ .../server/lib/bfHttpServer/httprequest.h | 214 +++++++++ .../lib/bfHttpServer/httprequesthandler.cpp | 19 + .../lib/bfHttpServer/httprequesthandler.h | 45 ++ .../server/lib/bfHttpServer/httpresponse.cpp | 122 +++++ .../server/lib/bfHttpServer/httpresponse.h | 134 ++++++ .../server/lib/bfHttpServer/httpsession.cpp | 158 +++++++ .../server/lib/bfHttpServer/httpsession.h | 118 +++++ .../lib/bfHttpServer/httpsessionstore.cpp | 107 +++++ .../lib/bfHttpServer/httpsessionstore.h | 104 +++++ .../lib/bfHttpServer/staticfilecontroller.cpp | 114 +++++ .../lib/bfHttpServer/staticfilecontroller.h | 81 ++++ .../server/lib/bfLogging/bfLogging.pri | 7 + .../server/lib/bfLogging/dualfilelogger.cpp | 20 + .../server/lib/bfLogging/dualfilelogger.h | 60 +++ .../server/lib/bfLogging/filelogger.cpp | 176 +++++++ .../server/lib/bfLogging/filelogger.h | 131 ++++++ .../server/lib/bfLogging/logger.cpp | 154 +++++++ .../server/lib/bfLogging/logger.h | 144 ++++++ .../server/lib/bfLogging/logmessage.cpp | 68 +++ .../server/lib/bfLogging/logmessage.h | 74 +++ .../lib/bfTemplateEngine/bfTemplateEngine.pri | 7 + .../server/lib/bfTemplateEngine/template.cpp | 187 ++++++++ .../server/lib/bfTemplateEngine/template.h | 167 +++++++ .../lib/bfTemplateEngine/templatecache.cpp | 30 ++ .../lib/bfTemplateEngine/templatecache.h | 77 ++++ .../lib/bfTemplateEngine/templateloader.cpp | 103 +++++ .../lib/bfTemplateEngine/templateloader.h | 85 ++++ YACReaderLibrary/server/main.cpp | 58 +++ YACReaderLibrary/server/requestmapper.cpp | 46 ++ YACReaderLibrary/server/requestmapper.h | 36 ++ YACReaderLibrary/server/server.pri | 26 ++ YACReaderLibrary/server/startup.cpp | 77 ++++ YACReaderLibrary/server/startup.h | 30 ++ YACReaderLibrary/server/static.cpp | 59 +++ YACReaderLibrary/server/static.h | 64 +++ 60 files changed, 4751 insertions(+), 3 deletions(-) create mode 100644 YACReaderLibrary/bundle_creator.cpp create mode 100644 YACReaderLibrary/bundle_creator.h create mode 100644 YACReaderLibrary/server/controllers/dumpcontroller.cpp create mode 100644 YACReaderLibrary/server/controllers/dumpcontroller.h create mode 100644 YACReaderLibrary/server/controllers/fileuploadcontroller.cpp create mode 100644 YACReaderLibrary/server/controllers/fileuploadcontroller.h create mode 100644 YACReaderLibrary/server/controllers/formcontroller.cpp create mode 100644 YACReaderLibrary/server/controllers/formcontroller.h create mode 100644 YACReaderLibrary/server/controllers/sessioncontroller.cpp create mode 100644 YACReaderLibrary/server/controllers/sessioncontroller.h create mode 100644 YACReaderLibrary/server/controllers/templatecontroller.cpp create mode 100644 YACReaderLibrary/server/controllers/templatecontroller.h create mode 100644 YACReaderLibrary/server/documentcache.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/bfHttpServer.pri create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpcookie.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpcookie.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httplistener.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httplistener.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httprequest.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httprequest.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpresponse.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpresponse.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpsession.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpsession.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.h create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.cpp create mode 100644 YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.h create mode 100644 YACReaderLibrary/server/lib/bfLogging/bfLogging.pri create mode 100644 YACReaderLibrary/server/lib/bfLogging/dualfilelogger.cpp create mode 100644 YACReaderLibrary/server/lib/bfLogging/dualfilelogger.h create mode 100644 YACReaderLibrary/server/lib/bfLogging/filelogger.cpp create mode 100644 YACReaderLibrary/server/lib/bfLogging/filelogger.h create mode 100644 YACReaderLibrary/server/lib/bfLogging/logger.cpp create mode 100644 YACReaderLibrary/server/lib/bfLogging/logger.h create mode 100644 YACReaderLibrary/server/lib/bfLogging/logmessage.cpp create mode 100644 YACReaderLibrary/server/lib/bfLogging/logmessage.h create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/bfTemplateEngine.pri create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/template.cpp create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/template.h create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.cpp create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.h create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.cpp create mode 100644 YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.h create mode 100644 YACReaderLibrary/server/main.cpp create mode 100644 YACReaderLibrary/server/requestmapper.cpp create mode 100644 YACReaderLibrary/server/requestmapper.h create mode 100644 YACReaderLibrary/server/server.pri create mode 100644 YACReaderLibrary/server/startup.cpp create mode 100644 YACReaderLibrary/server/startup.h create mode 100644 YACReaderLibrary/server/static.cpp create mode 100644 YACReaderLibrary/server/static.h diff --git a/YACReaderLibrary/YACReaderLibrary.pro b/YACReaderLibrary/YACReaderLibrary.pro index 68e6c339..ea3aa965 100644 --- a/YACReaderLibrary/YACReaderLibrary.pro +++ b/YACReaderLibrary/YACReaderLibrary.pro @@ -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 + diff --git a/YACReaderLibrary/bundle_creator.cpp b/YACReaderLibrary/bundle_creator.cpp new file mode 100644 index 00000000..8ea6eef2 --- /dev/null +++ b/YACReaderLibrary/bundle_creator.cpp @@ -0,0 +1,13 @@ +#include "bundle_creator.h" + + +BundleCreator::BundleCreator(void) + :QObject() +{ + +} + + +BundleCreator::~BundleCreator(void) +{ +} diff --git a/YACReaderLibrary/bundle_creator.h b/YACReaderLibrary/bundle_creator.h new file mode 100644 index 00000000..ff4dbd08 --- /dev/null +++ b/YACReaderLibrary/bundle_creator.h @@ -0,0 +1,14 @@ +#ifndef __BUNDLE_CREATOR_H +#define __BUNDLE_CREATOR_H + +#include + +class BundleCreator : public QObject +{ +Q_OBJECT +public: + BundleCreator(void); + ~BundleCreator(void); +}; + +#endif \ No newline at end of file diff --git a/YACReaderLibrary/main.cpp b/YACReaderLibrary/main.cpp index 905c0cea..b7eddc30 100644 --- a/YACReaderLibrary/main.cpp +++ b/YACReaderLibrary/main.cpp @@ -1,7 +1,7 @@ #include "library_window.h" #include - +#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(); } diff --git a/YACReaderLibrary/server/controllers/dumpcontroller.cpp b/YACReaderLibrary/server/controllers/dumpcontroller.cpp new file mode 100644 index 00000000..2b67e536 --- /dev/null +++ b/YACReaderLibrary/server/controllers/dumpcontroller.cpp @@ -0,0 +1,62 @@ +/** + @file + @author Stefan Frings +*/ + +#include "dumpcontroller.h" +#include +#include + +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(""); + body.append("Request:"); + body.append("
Method: "); + body.append(request.getMethod()); + body.append("
Path: "); + body.append(request.getPath()); + body.append("
Version: "); + body.append(request.getVersion()); + + body.append("

Headers:"); + QMapIterator i(request.getHeaderMap()); + while (i.hasNext()) { + i.next(); + body.append("
"); + body.append(i.key()); + body.append("="); + body.append(i.value()); + } + + body.append("

Parameters:"); + i=QMapIterator(request.getParameterMap()); + while (i.hasNext()) { + i.next(); + body.append("
"); + body.append(i.key()); + body.append("="); + body.append(i.value()); + } + + body.append("

Cookies:"); + i=QMapIterator(request.getCookieMap()); + while (i.hasNext()) { + i.next(); + body.append("
"); + body.append(i.key()); + body.append("="); + body.append(i.value()); + } + + body.append("

Body:
"); + body.append(request.getBody()); + + body.append(""); + response.write(body,true); +} diff --git a/YACReaderLibrary/server/controllers/dumpcontroller.h b/YACReaderLibrary/server/controllers/dumpcontroller.h new file mode 100644 index 00000000..a3787dbb --- /dev/null +++ b/YACReaderLibrary/server/controllers/dumpcontroller.h @@ -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 diff --git a/YACReaderLibrary/server/controllers/fileuploadcontroller.cpp b/YACReaderLibrary/server/controllers/fileuploadcontroller.cpp new file mode 100644 index 00000000..30d76035 --- /dev/null +++ b/YACReaderLibrary/server/controllers/fileuploadcontroller.cpp @@ -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(""); + response.write("Upload a JPEG image file

"); + response.write("

"); + response.write(" "); + response.write(" File:
"); + response.write(" "); + response.write("
"); + response.write("",true); + } +} + diff --git a/YACReaderLibrary/server/controllers/fileuploadcontroller.h b/YACReaderLibrary/server/controllers/fileuploadcontroller.h new file mode 100644 index 00000000..01865ea6 --- /dev/null +++ b/YACReaderLibrary/server/controllers/fileuploadcontroller.h @@ -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 diff --git a/YACReaderLibrary/server/controllers/formcontroller.cpp b/YACReaderLibrary/server/controllers/formcontroller.cpp new file mode 100644 index 00000000..bcfebd66 --- /dev/null +++ b/YACReaderLibrary/server/controllers/formcontroller.cpp @@ -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(""); + response.write("Name = "); + response.write(request.getParameter("name")); + response.write("
City = "); + response.write(request.getParameter("city")); + response.write("",true); + } + else { + response.write(""); + response.write("
"); + response.write(" "); + response.write(" Name:
"); + response.write(" City:
"); + response.write(" "); + response.write("
"); + response.write("",true); + } +} + diff --git a/YACReaderLibrary/server/controllers/formcontroller.h b/YACReaderLibrary/server/controllers/formcontroller.h new file mode 100644 index 00000000..5ae709a8 --- /dev/null +++ b/YACReaderLibrary/server/controllers/formcontroller.h @@ -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 diff --git a/YACReaderLibrary/server/controllers/sessioncontroller.cpp b/YACReaderLibrary/server/controllers/sessioncontroller.cpp new file mode 100644 index 00000000..34d56526 --- /dev/null +++ b/YACReaderLibrary/server/controllers/sessioncontroller.cpp @@ -0,0 +1,31 @@ +/** + @file + @author Stefan Frings +*/ + +#include "sessioncontroller.h" +#include "../static.h" +#include +#include + +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("New session started. Reload this page now."); + session.set("startTime",QDateTime::currentDateTime()); + } + + else { + QDateTime startTime=session.get("startTime").toDateTime(); + response.write("Your session started "); + response.write(startTime.toString().toLatin1()); + response.write(""); + } + +} diff --git a/YACReaderLibrary/server/controllers/sessioncontroller.h b/YACReaderLibrary/server/controllers/sessioncontroller.h new file mode 100644 index 00000000..a13ee51f --- /dev/null +++ b/YACReaderLibrary/server/controllers/sessioncontroller.h @@ -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 diff --git a/YACReaderLibrary/server/controllers/templatecontroller.cpp b/YACReaderLibrary/server/controllers/templatecontroller.cpp new file mode 100644 index 00000000..d1816808 --- /dev/null +++ b/YACReaderLibrary/server/controllers/templatecontroller.cpp @@ -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 headers=request.getHeaderMap(); + QMapIterator 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); +} diff --git a/YACReaderLibrary/server/controllers/templatecontroller.h b/YACReaderLibrary/server/controllers/templatecontroller.h new file mode 100644 index 00000000..c5b0077d --- /dev/null +++ b/YACReaderLibrary/server/controllers/templatecontroller.h @@ -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 diff --git a/YACReaderLibrary/server/documentcache.h b/YACReaderLibrary/server/documentcache.h new file mode 100644 index 00000000..06ff67ac --- /dev/null +++ b/YACReaderLibrary/server/documentcache.h @@ -0,0 +1,4 @@ +#ifndef DOCUMENTCACHE_H +#define DOCUMENTCACHE_H + +#endif // DOCUMENTCACHE_H diff --git a/YACReaderLibrary/server/lib/bfHttpServer/bfHttpServer.pri b/YACReaderLibrary/server/lib/bfHttpServer/bfHttpServer.pri new file mode 100644 index 00000000..a109a573 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/bfHttpServer.pri @@ -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 diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.cpp new file mode 100644 index 00000000..91fa3365 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.cpp @@ -0,0 +1,154 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpconnectionhandler.h" +#include "httpresponse.h" +#include +#include + +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; + } +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.h b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.h new file mode 100644 index 00000000..1b793daa --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandler.h @@ -0,0 +1,97 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPCONNECTIONHANDLER_H +#define HTTPCONNECTIONHANDLER_H + +#include +#include +#include +#include +#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. +

+ Example for the required configuration settings: +

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

+ The readTimeout value defines the maximum time to wait for a complete HTTP request. + @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize +*/ + +class 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 diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.cpp new file mode 100644 index 00000000..0bdd4796 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.cpp @@ -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()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 + } + } + } +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.h b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.h new file mode 100644 index 00000000..ec557d78 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpconnectionhandlerpool.h @@ -0,0 +1,69 @@ +#ifndef HTTPCONNECTIONHANDLERPOOL_H +#define HTTPCONNECTIONHANDLERPOOL_H + +#include +#include +#include +#include "httpconnectionhandler.h" + +/** + Pool of http connection handlers. Connection handlers are created on demand and idle handlers are + cleaned up in regular time intervals. +

+ Example for the required configuration settings: +

+  minThreads=1
+  maxThreads=10
+  cleanupInterval=1000
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+ 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 pool; + + /** Timer to clean-up unused connection handler */ + QTimer cleanupTimer; + +private slots: + + /** Received from the clean-up timer. */ + void cleanup(); + +}; + +#endif // HTTPCONNECTIONHANDLERPOOL_H diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpcookie.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httpcookie.cpp new file mode 100644 index 00000000..3f5be929 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpcookie.cpp @@ -0,0 +1,199 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpcookie.h" + +HttpCookie::HttpCookie() { + version=1; + maxAge=0; + secure=false; +} + +HttpCookie::HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path, const QByteArray comment, const QByteArray domain, const bool secure) { + this->name=name; + this->value=value; + this->maxAge=maxAge; + this->path=path; + this->comment=comment; + this->domain=domain; + this->secure=secure; + this->version=1; +} + +HttpCookie::HttpCookie(const QByteArray source) { + version=1; + maxAge=0; + secure=false; + QList list=splitCSV(source); + foreach(QByteArray part, list) { + + // Split the part into name and value + QByteArray name; + QByteArray value; + int posi=part.indexOf('='); + if (posi) { + name=part.left(posi).trimmed(); + value=part.mid(posi+1).trimmed(); + } + else { + name=part.trimmed(); + value=""; + } + + // Set fields + if (name=="Comment") { + comment=value; + } + else if (name=="Domain") { + domain=value; + } + else if (name=="Max-Age") { + maxAge=value.toInt(); + } + else if (name=="Path") { + path=value; + } + else if (name=="Secure") { + secure=true; + } + else if (name=="Version") { + version=value.toInt(); + } + else { + if (this->name.isEmpty()) { + this->name=name; + this->value=value; + } + else { + qWarning("HttpCookie: Ignoring unknown %s=%s",name.data(),value.data()); + } + } + } +} + +QByteArray HttpCookie::toByteArray() const { + QByteArray buffer(name); + buffer.append('='); + buffer.append(value); + if (!comment.isEmpty()) { + buffer.append("; Comment="); + buffer.append(comment); + } + if (!domain.isEmpty()) { + buffer.append("; Domain="); + buffer.append(domain); + } + if (maxAge!=0) { + buffer.append("; Max-Age="); + buffer.append(QByteArray::number(maxAge)); + } + if (!path.isEmpty()) { + buffer.append("; Path="); + buffer.append(path); + } + if (secure) { + buffer.append("; Secure"); + } + buffer.append("; Version="); + buffer.append(QByteArray::number(version)); + return buffer; +} + +void HttpCookie::setName(const QByteArray name){ + this->name=name; +} + +void HttpCookie::setValue(const QByteArray value){ + this->value=value; +} + +void HttpCookie::setComment(const QByteArray comment){ + this->comment=comment; +} + +void HttpCookie::setDomain(const QByteArray domain){ + this->domain=domain; +} + +void HttpCookie::setMaxAge(const int maxAge){ + this->maxAge=maxAge; +} + +void HttpCookie::setPath(const QByteArray path){ + this->path=path; +} + +void HttpCookie::setSecure(const bool secure){ + this->secure=secure; +} + +QByteArray HttpCookie::getName() const { + return name; +} + +QByteArray HttpCookie::getValue() const { + return value; +} + +QByteArray HttpCookie::getComment() const { + return comment; +} + +QByteArray HttpCookie::getDomain() const { + return domain; +} + +int HttpCookie::getMaxAge() const { + return maxAge; +} + +QByteArray HttpCookie::getPath() const { + return path; +} + +bool HttpCookie::getSecure() const { + return secure; +} + +int HttpCookie::getVersion() const { + return version; +} + +QList HttpCookie::splitCSV(const QByteArray source) { + bool inString=false; + QList list; + QByteArray buffer; + for (int i=0; i +#include + +/** + 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 splitCSV(const QByteArray source); + + /** Set the name of this cookie */ + void setName(const QByteArray name); + + /** Set the value of this cookie */ + void setValue(const QByteArray value); + + /** Set the comment of this cookie */ + void setComment(const QByteArray comment); + + /** Set the domain of this cookie */ + void setDomain(const QByteArray domain); + + /** Set the maximum age of this cookie in seconds. 0=discard immediately */ + void setMaxAge(const int maxAge); + + /** Set the path for that the cookie will be sent, default="/" which means the whole domain */ + void setPath(const QByteArray path); + + /** Set secure mode, so that the cokkie will only be sent on secure connections */ + void setSecure(const bool secure); + + /** Get the name of this cookie */ + QByteArray getName() const; + + /** Get the value of this cookie */ + QByteArray getValue() const; + + /** Get the comment of this cookie */ + QByteArray getComment() const; + + /** Get the domain of this cookie */ + QByteArray getDomain() const; + + /** Set the maximum age of this cookie in seconds. */ + int getMaxAge() const; + + /** Set the path of this cookie */ + QByteArray getPath() const; + + /** Get the secure flag of this cookie */ + bool getSecure() const; + + /** Returns always 1 */ + int getVersion() const; + +private: + + QByteArray name; + QByteArray value; + QByteArray comment; + QByteArray domain; + int maxAge; + QByteArray path; + bool secure; + int version; + +}; + +#endif // HTTPCOOKIE_H diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httplistener.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httplistener.cpp new file mode 100644 index 00000000..bcac23cb --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httplistener.cpp @@ -0,0 +1,56 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httplistener.h" +#include "httpconnectionhandler.h" +#include "httpconnectionhandlerpool.h" +#include + +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(); + } +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httplistener.h b/YACReaderLibrary/server/lib/bfHttpServer/httplistener.h new file mode 100644 index 00000000..614e3c5e --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httplistener.h @@ -0,0 +1,76 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LISTENER_H +#define LISTENER_H + +#include +#include +#include +#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. +

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

+  port=8080
+  minThreads=1
+  maxThreads=10
+  cleanupInterval=1000
+  readTimeout=60000
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+ 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 diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httprequest.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httprequest.cpp new file mode 100644 index 00000000..f63b3502 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httprequest.cpp @@ -0,0 +1,435 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequest.h" +#include +#include +#include "httpcookie.h" + +HttpRequest::HttpRequest(QSettings* settings) { + status=waitForRequest; + currentSize=0; + expectedBodySize=0; + maxSize=settings->value("maxRequestSize","16000").toInt(); + maxMultiPartSize=settings->value("maxMultiPartSize","1000000").toInt(); + + // 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 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 list=rawParameters.split('&'); + foreach (QByteArray part, list) { + int equalsChar=part.indexOf('='); + if (equalsChar>=0) { + QByteArray name=part.left(equalsChar).trimmed(); + QByteArray value=part.mid(equalsChar+1).trimmed(); + parameters.insert(urlDecode(name),urlDecode(value)); + } + else if (!part.isEmpty()){ + // Name without value + parameters.insert(urlDecode(part),""); + } + } +} + +void HttpRequest::extractCookies() { +#ifdef SUPERVERBOSE + qDebug("HttpRequest: extract cookies"); +#endif + foreach(QByteArray cookieStr, headers.values("Cookie")) { + QList list=HttpCookie::splitCSV(cookieStr); + foreach(QByteArray part, list) { +#ifdef SUPERVERBOSE + qDebug("HttpRequest: found cookie %s",part.data()); +#endif // Split the part into name and value + QByteArray name; + QByteArray value; + int posi=part.indexOf('='); + if (posi) { + name=part.left(posi).trimmed(); + value=part.mid(posi+1).trimmed(); + } + else { + name=part.trimmed(); + value=""; + } + cookies.insert(name,value); + } + } + headers.remove("Cookie"); +} + +void HttpRequest::readFromSocket(QTcpSocket& socket) { + Q_ASSERT(status!=complete); + if (status==waitForRequest) { + readRequest(socket); + } + else if (status==waitForHeader) { + readHeader(socket); + } + else if (status==waitForBody) { + readBody(socket); + } + if (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 HttpRequest::getHeaders(const QByteArray& name) const { + return headers.values(name); +} + +QMultiMap HttpRequest::getHeaderMap() const { + return headers; +} + +QByteArray HttpRequest::getParameter(const QByteArray& name) const { + return parameters.value(name); +} + +QList HttpRequest::getParameters(const QByteArray& name) const { + return parameters.values(name); +} + +QMultiMap HttpRequest::getParameterMap() const { + return parameters; +} + +QByteArray HttpRequest::getBody() const { + return bodyData; +} + +QByteArray HttpRequest::urlDecode(const QByteArray source) { + QByteArray buffer(source); + buffer.replace('+',' '); + int percentChar=buffer.indexOf('%'); + while (percentChar>=0) { + bool ok; + char byte=buffer.mid(percentChar+1,2).toInt(&ok,16); + if (ok) { + buffer.replace(percentChar,3,(char*)&byte,1); + } + percentChar=buffer.indexOf('%',percentChar+1); + } + return buffer; +} + + +void HttpRequest::parseMultiPartFile() { + qDebug("HttpRequest: parsing multipart temp file"); + tempFile.seek(0); + bool finished=false; + while (!tempFile.atEnd() && !finished && !tempFile.error()) { + +#ifdef SUPERVERBOSE + qDebug("HttpRequest: reading multpart headers"); +#endif + QByteArray fieldName; + QByteArray fileName; + while (!tempFile.atEnd() && !finished && !tempFile.error()) { + QByteArray line=tempFile.readLine(65536).trimmed(); + if (line.startsWith("Content-Disposition:")) { + if (line.contains("form-data")) { + int start=line.indexOf(" name=\""); + int end=line.indexOf("\"",start+7); + if (start>=0 && end>=start) { + fieldName=line.mid(start+7,end-start-7); + } + start=line.indexOf(" filename=\""); + end=line.indexOf("\"",start+11); + if (start>=0 && end>=start) { + fileName=line.mid(start+11,end-start-11); + } +#ifdef SUPERVERBOSE + qDebug("HttpRequest: multipart field=%s, filename=%s",fieldName.data(),fileName.data()); +#endif + } + else { + qDebug("HttpRequest: ignoring unsupported content part %s",line.data()); + } + } + else if (line.isEmpty()) { + break; + } + } + +#ifdef SUPERVERBOSE + qDebug("HttpRequest: reading multpart data"); +#endif + QTemporaryFile* uploadedFile=0; + QByteArray fieldValue; + while (!tempFile.atEnd() && !finished && !tempFile.error()) { + QByteArray line=tempFile.readLine(65536); + if (line.startsWith("--"+boundary)) { + // Boundary found. Until now we have collected 2 bytes too much, + // so remove them from the last result + if (fileName.isEmpty() && !fieldName.isEmpty()) { + // last field was a form field + fieldValue.remove(fieldValue.size()-2,2); + parameters.insert(fieldName,fieldValue); + qDebug("HttpRequest: set parameter %s=%s",fieldName.data(),fieldValue.data()); + } + else if (!fileName.isEmpty() && !fieldName.isEmpty()) { + // last field was a file +#ifdef SUPERVERBOSE + qDebug("HttpRequest: finishing writing to uploaded file"); +#endif + uploadedFile->resize(uploadedFile->size()-2); + uploadedFile->flush(); + uploadedFile->seek(0); + parameters.insert(fieldName,fileName); + qDebug("HttpRequest: set parameter %s=%s",fieldName.data(),fileName.data()); + uploadedFiles.insert(fieldName,uploadedFile); + qDebug("HttpRequest: uploaded file size is %i",(int) uploadedFile->size()); + } + if (line.contains(boundary+"--")) { + finished=true; + } + break; + } + else { + if (fileName.isEmpty() && !fieldName.isEmpty()) { + // this is a form field. + currentSize+=line.size(); + fieldValue.append(line); + } + else if (!fileName.isEmpty() && !fieldName.isEmpty()) { + // this is a file + if (!uploadedFile) { + uploadedFile=new QTemporaryFile(); + uploadedFile->open(); + } + uploadedFile->write(line); + if (uploadedFile->error()) { + qCritical("HttpRequest: error writing temp file, %s",qPrintable(uploadedFile->errorString())); + } + } + } + } + } + if (tempFile.error()) { + qCritical("HttpRequest: cannot read temp file, %s",qPrintable(tempFile.errorString())); + } +#ifdef SUPERVERBOSE + qDebug("HttpRequest: finished parsing multipart temp file"); +#endif +} + +HttpRequest::~HttpRequest() { + foreach(QByteArray key, uploadedFiles.keys()) { + QTemporaryFile* file=uploadedFiles.value(key); + file->close(); + delete file; + } +} + +QTemporaryFile* HttpRequest::getUploadedFile(const QByteArray fieldName) { + return uploadedFiles.value(fieldName); +} + +QByteArray HttpRequest::getCookie(const QByteArray& name) const { + return cookies.value(name); +} + +/** Get the map of cookies */ +QMap& HttpRequest::getCookieMap() { + return cookies; +} + diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httprequest.h b/YACReaderLibrary/server/lib/bfHttpServer/httprequest.h new file mode 100644 index 00000000..943a62c5 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httprequest.h @@ -0,0 +1,214 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUEST_H +#define HTTPREQUEST_H + +#include +#include +#include +#include +#include +#include +#include + +/** + This object represents a single HTTP request. It reads the request + from a TCP socket and provides getters for the individual parts + of the request. +

+ The follwing config settings are required: +

+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+  
+

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

+ For uploaded files, the method getParameters() returns + the original fileName as provided by the calling web browser. + */ + QTemporaryFile* getUploadedFile(const QByteArray fieldName); + + /** + Get the value of a cookie + @param name Name of the cookie + */ + QByteArray getCookie(const QByteArray& name) const; + + /** Get the map of cookies */ + QMap& getCookieMap(); + +private: + + /** Request headers */ + QMultiMap headers; + + /** Parameters of the request */ + QMultiMap parameters; + + /** Uploaded files of the request, key is the field name. */ + QMap uploadedFiles; + + /** Received cookies */ + QMap cookies; + + /** Storage for raw body data */ + QByteArray bodyData; + + /** Request method */ + QByteArray method; + + /** Request path (in raw encoded format) */ + QByteArray path; + + /** Request protocol version */ + QByteArray version; + + /** + Status of this request. + @see RequestStatus + */ + RequestStatus status; + + /** Maximum size of requests in bytes. */ + int maxSize; + + /** Maximum allowed size of multipart forms in bytes. */ + int maxMultiPartSize; + + /** 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 diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.cpp new file mode 100644 index 00000000..d8ad7caf --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.cpp @@ -0,0 +1,19 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequesthandler.h" + +HttpRequestHandler::HttpRequestHandler(QObject* parent) + : QObject(parent) +{} + +HttpRequestHandler::~HttpRequestHandler() {} + +void HttpRequestHandler::service(HttpRequest& request, HttpResponse& response) { + qCritical("HttpRequestHandler: you need to override the 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); +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.h b/YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.h new file mode 100644 index 00000000..a5f9f4e2 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httprequesthandler.h @@ -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. +

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

+ @warning Be aware that the main request handler instance must be created on the heap and + that it is used by multiple threads simultaneously. + @see StaticFileController which delivers static local files. +*/ + +class 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 diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpresponse.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httpresponse.cpp new file mode 100644 index 00000000..efa3ee8a --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpresponse.cpp @@ -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& 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& HttpResponse::getCookies() { + return cookies; +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpresponse.h b/YACReaderLibrary/server/lib/bfHttpServer/httpresponse.h new file mode 100644 index 00000000..3c28a903 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpresponse.h @@ -0,0 +1,134 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPRESPONSE_H +#define HTTPRESPONSE_H + +#include +#include +#include +#include "httpcookie.h" + +/** + This object represents a HTTP response, in particular the response headers. +

+ Example code for proper response generation: +

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

+ Example how to return an error: +

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

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

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

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

+ Otherwise, each part is transferred in chunked mode. + @param data Data bytes of the body + @param lastPart Indicator, if this is the last part of the response. + */ + void write(QByteArray data, bool lastPart=false); + + /** + Indicates wheter the body has been sent completely. Used by the connection + handler to terminate the body automatically when necessary. + */ + bool hasSentLastPart() const; + + /** + Set a cookie. Cookies are sent together with the headers when the first + call to write() occurs. + */ + void setCookie(const HttpCookie& cookie); + +private: + + /** Request headers */ + QMap headers; + + /** Socket for writing output */ + QTcpSocket* socket; + + /** HTTP status code*/ + int statusCode; + + /** HTTP status code description */ + QByteArray statusText; + + /** Indicator whether headers have been sent */ + bool sentHeaders; + + /** Indicator whether the body has been sent completely */ + bool sentLastPart; + + /** Cookies */ + QMap cookies; + + /** Write raw data to the socket. This method blocks until all bytes have been passed to the TCP buffer */ + 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 diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpsession.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httpsession.cpp new file mode 100644 index 00000000..9c7bdd62 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpsession.cpp @@ -0,0 +1,158 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsession.h" +#include +#include + + +HttpSession::HttpSession(bool canStore) { + if (canStore) { + dataPtr=new HttpSessionData(); + dataPtr->refCount=1; + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->id=QUuid::createUuid().toString().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 HttpSession::getAll() const { + QMap values; + if (dataPtr) { + dataPtr->lock.lockForRead(); + values=dataPtr->values; + dataPtr->lock.unlock(); + } + return values; +} + +qint64 HttpSession::getLastAccess() const { + qint64 value=0; + if (dataPtr) { + dataPtr->lock.lockForRead(); + value=dataPtr->lastAccess; + dataPtr->lock.unlock(); + } + return value; +} + + +void HttpSession::setLastAccess() { + if (dataPtr) { + dataPtr->lock.lockForRead(); + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->lock.unlock(); + } +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpsession.h b/YACReaderLibrary/server/lib/bfHttpServer/httpsession.h new file mode 100644 index 00000000..7a9470aa --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpsession.h @@ -0,0 +1,118 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSION_H +#define HTTPSESSION_H + +#include +#include +#include + + +/** + 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 getAll() const; + + /** + Get the timestamp of last access. That is the time when the last + HttpSessionStore::getSession() has been called. + This method is thread safe. + */ + qint64 getLastAccess() const; + + /** + Set the timestamp of last access, to renew the timeout period. + Called by HttpSessionStore::getSession(). + This method is thread safe. + */ + void setLastAccess(); + +private: + + struct HttpSessionData { + + /** Unique ID */ + QByteArray id; + + /** Timestamp of last access, set by the HttpSessionStore */ + qint64 lastAccess; + + /** Reference counter */ + int refCount; + + /** Used to synchronize threads */ + QReadWriteLock lock; + + /** Storage for the key/value pairs; */ + QMap values; + + }; + + /** Pointer to the shared data. */ + HttpSessionData* dataPtr; + +}; + +#endif // HTTPSESSION_H diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.cpp b/YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.cpp new file mode 100644 index 00000000..5eb281aa --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.cpp @@ -0,0 +1,107 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsessionstore.h" +#include +#include + +HttpSessionStore::HttpSessionStore(QSettings* settings, QObject* parent) + :QObject(parent) +{ + this->settings=settings; + connect(&cleanupTimer,SIGNAL(timeout()),this,SLOT(timerEvent())); + cleanupTimer.start(60000); + cookieName=settings->value("cookieName","sessionid").toByteArray(); + expirationTime=settings->value("expirationTime",3600000).toInt(); + qDebug("HttpSessionStore: Sessions expire after %i milliseconds",expirationTime); +} + +HttpSessionStore::~HttpSessionStore() +{ + cleanupTimer.stop(); +} + +QByteArray HttpSessionStore::getSessionId(HttpRequest& request, HttpResponse& response) { + // The session ID in the response has priority because this one will be used in the next request. + mutex.lock(); + // Get the session ID from the response cookie + QByteArray sessionId=response.getCookies().value(cookieName).getValue(); + if (sessionId.isEmpty()) { + // Get the session ID from the request cookie + sessionId=request.getCookie(cookieName); + } + // Clear the session ID if there is no such session in the storage. + if (!sessionId.isEmpty()) { + if (!sessions.contains(sessionId)) { + qDebug("HttpSessionStore: received invalid session cookie with ID %s",sessionId.data()); + sessionId.clear(); + } + } + mutex.unlock(); + return sessionId; +} + +HttpSession HttpSessionStore::getSession(HttpRequest& request, HttpResponse& response, bool allowCreate) { + QByteArray sessionId=getSessionId(request,response); + mutex.lock(); + if (!sessionId.isEmpty()) { + HttpSession session=sessions.value(sessionId); + if (!session.isNull()) { + mutex.unlock(); + session.setLastAccess(); + return session; + } + } + // Need to create a new session + if (allowCreate) { + QByteArray cookieName=settings->value("cookieName","sessionid").toByteArray(); + QByteArray cookiePath=settings->value("cookiePath").toByteArray(); + QByteArray cookieComment=settings->value("cookieComment").toByteArray(); + QByteArray cookieDomain=settings->value("cookieDomain").toByteArray(); + HttpSession session(true); + qDebug("HttpSessionStore: create new session with ID %s",session.getId().data()); + sessions.insert(session.getId(),session); + response.setCookie(HttpCookie(cookieName,session.getId(),expirationTime/1000,cookiePath,cookieComment,cookieDomain)); + mutex.unlock(); + return session; + } + // Return a null session + mutex.unlock(); + return HttpSession(); +} + +HttpSession HttpSessionStore::getSession(const QByteArray id) { + mutex.lock(); + HttpSession session=sessions.value(id); + mutex.unlock(); + session.setLastAccess(); + return session; +} + +void HttpSessionStore::timerEvent() { + // Todo: find a way to delete sessions only if no controller is accessing them + mutex.lock(); + qint64 now=QDateTime::currentMSecsSinceEpoch(); + QMap::iterator i = sessions.begin(); + while (i != sessions.end()) { + QMap::iterator prev = i; + ++i; + HttpSession session=prev.value(); + qint64 lastAccess=session.getLastAccess(); + if (now-lastAccess>expirationTime) { + qDebug("HttpSessionStore: session %s expired",session.getId().data()); + sessions.erase(prev); + } + } + mutex.unlock(); +} + + +/** Delete a session */ +void HttpSessionStore::removeSession(HttpSession session) { + mutex.lock(); + sessions.remove(session.getId()); + mutex.unlock(); +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.h b/YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.h new file mode 100644 index 00000000..e68a2267 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/httpsessionstore.h @@ -0,0 +1,104 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSIONSTORE_H +#define HTTPSESSIONSTORE_H + +#include +#include +#include +#include +#include "httpsession.h" +#include "httpresponse.h" +#include "httprequest.h" + +/** + Stores HTTP sessions and deletes them when they have expired. + The following configuration settings are required in the config file: +

+  expirationTime=3600000
+  cookieName=sessionid
+  
+ The following additional configurations settings are optionally: +
+  cookiePath=/
+  cookieComment=Session ID
+  cookieDomain=stefanfrings.de
+  
+*/ + +class 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 sessions; + + /** Timer to remove expired sessions */ + QTimer cleanupTimer; + + /** Name of the session cookie */ + QByteArray cookieName; + + /** Time when sessions expire (in ms)*/ + int expirationTime; + + /** Used to synchronize threads */ + QMutex mutex; + +private slots: + + /** Called every minute to cleanup expired sessions. */ + void timerEvent(); +}; + +#endif // HTTPSESSIONSTORE_H diff --git a/YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.cpp b/YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.cpp new file mode 100644 index 00000000..e0e00347 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.cpp @@ -0,0 +1,114 @@ +/** + @file + @author Stefan Frings +*/ + +#include "staticfilecontroller.h" +#include +#include +#include + +StaticFileController::StaticFileController(QSettings* settings, QObject* parent) + :HttpRequestHandler(parent) +{ + maxAge=settings->value("maxAge","60000").toInt(); + encoding=settings->value("encoding","UTF-8").toString(); + docroot=settings->value("path",".").toString(); + // 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 +} diff --git a/YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.h b/YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.h new file mode 100644 index 00000000..e01d79f3 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfHttpServer/staticfilecontroller.h @@ -0,0 +1,81 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef STATICFILECONTROLLER_H +#define STATICFILECONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" +#include + +/** + Delivers static files. It is usually called by the applications main request handler when + the caller request a path that is mapped to static files. +

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

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

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

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

+ Do not instantiate this class in each request, because this would make the file cache + useless. Better create one instance during start-up and call it when the application + received a related HTTP request. +*/ + +class 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 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 diff --git a/YACReaderLibrary/server/lib/bfLogging/bfLogging.pri b/YACReaderLibrary/server/lib/bfLogging/bfLogging.pri new file mode 100644 index 00000000..a5d95282 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/bfLogging.pri @@ -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 diff --git a/YACReaderLibrary/server/lib/bfLogging/dualfilelogger.cpp b/YACReaderLibrary/server/lib/bfLogging/dualfilelogger.cpp new file mode 100644 index 00000000..7329cae0 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/dualfilelogger.cpp @@ -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); +} diff --git a/YACReaderLibrary/server/lib/bfLogging/dualfilelogger.h b/YACReaderLibrary/server/lib/bfLogging/dualfilelogger.h new file mode 100644 index 00000000..b680ced5 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/dualfilelogger.h @@ -0,0 +1,60 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef DUALFILELOGGER_H +#define DUALFILELOGGER_H + +#include "logger.h" +#include "filelogger.h" +#include +#include +#include + +/** + 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 diff --git a/YACReaderLibrary/server/lib/bfLogging/filelogger.cpp b/YACReaderLibrary/server/lib/bfLogging/filelogger.cpp new file mode 100644 index 00000000..7e35a6d9 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/filelogger.cpp @@ -0,0 +1,176 @@ +/** + @file + @author Stefan Frings +*/ + +#include "filelogger.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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(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(); + } +} diff --git a/YACReaderLibrary/server/lib/bfLogging/filelogger.h b/YACReaderLibrary/server/lib/bfLogging/filelogger.h new file mode 100644 index 00000000..bd15d4c8 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/filelogger.h @@ -0,0 +1,131 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef FILELOGGER_H +#define FILELOGGER_H + +#include +#include +#include +#include +#include +#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. +

+ Example for the required configuration settings: +

+  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
+  
+ + - 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 diff --git a/YACReaderLibrary/server/lib/bfLogging/logger.cpp b/YACReaderLibrary/server/lib/bfLogging/logger.cpp new file mode 100644 index 00000000..065bb897 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/logger.cpp @@ -0,0 +1,154 @@ +/** + @file + @author Stefan Frings +*/ + +#include "logger.h" +#include +#include +#include +#include +#include + +Logger* Logger::defaultLogger=0; + + +QThreadStorage*> Logger::logVars; + + +QThreadStorage*> 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); + } + logVars.localData()->insert(name,value); + mutex.unlock(); +} + + +void Logger::clear(const bool buffer, const bool variables) { + mutex.lock(); + if (buffer && buffers.hasLocalData()) { + QList* 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()); + } + QList* 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(); +} diff --git a/YACReaderLibrary/server/lib/bfLogging/logger.h b/YACReaderLibrary/server/lib/bfLogging/logger.h new file mode 100644 index 00000000..bd37385e --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/logger.h @@ -0,0 +1,144 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include +#include +#include +#include "logmessage.h" + +/** + Decorates and writes log messages to the console, stderr. +

+ The decorator uses a predefined msgFormat string to enrich log messages + with additional information (e.g. timestamp). +

+ The msgFormat string and also the message text may contain additional + variable names in the form {name} that are filled by values + taken from a static thread local dictionary. +

+ 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. +

+ 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. +

+ 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*> logVars; + + /** Thread local backtrace buffers */ + static QThreadStorage*> buffers; + +}; + +#endif // LOGGER_H diff --git a/YACReaderLibrary/server/lib/bfLogging/logmessage.cpp b/YACReaderLibrary/server/lib/bfLogging/logmessage.cpp new file mode 100644 index 00000000..59e4d8da --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/logmessage.cpp @@ -0,0 +1,68 @@ +/** + @file + @author Stefan Frings +*/ + +#include "logmessage.h" +#include + +LogMessage::LogMessage(const QtMsgType type, const QString& message, QHash* 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 keys=logVars.keys(); + foreach (QString key, keys) { + decorated.replace("{"+key+"}",logVars.value(key)); + } + } + + return decorated; +} + +QtMsgType LogMessage::getType() const { + return type; +} diff --git a/YACReaderLibrary/server/lib/bfLogging/logmessage.h b/YACReaderLibrary/server/lib/bfLogging/logmessage.h new file mode 100644 index 00000000..b348a20e --- /dev/null +++ b/YACReaderLibrary/server/lib/bfLogging/logmessage.h @@ -0,0 +1,74 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LOGMESSAGE_H +#define LOGMESSAGE_H + +#include +#include +#include + +/** + 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* 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 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 diff --git a/YACReaderLibrary/server/lib/bfTemplateEngine/bfTemplateEngine.pri b/YACReaderLibrary/server/lib/bfTemplateEngine/bfTemplateEngine.pri new file mode 100644 index 00000000..d3eba98b --- /dev/null +++ b/YACReaderLibrary/server/lib/bfTemplateEngine/bfTemplateEngine.pri @@ -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 diff --git a/YACReaderLibrary/server/lib/bfTemplateEngine/template.cpp b/YACReaderLibrary/server/lib/bfTemplateEngine/template.cpp new file mode 100644 index 00000000..427f132e --- /dev/null +++ b/YACReaderLibrary/server/lib/bfTemplateEngine/template.cpp @@ -0,0 +1,187 @@ +/** + @file + @author Stefan Frings +*/ + +#include "template.h" +#include + +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=0) { + int end=indexOf(endTag,start+startTag2.length()); + if (end>=0) { + count++; + int ellse=indexOf(elseTag,start+startTag2.length()); + if (ellse>start && ellse=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 && ellse0) { + QString loopPart=mid(start+startTag.length(), ellse-start-startTag.length()); + QString insertMe; + for (int i=0; i0) { // and no else part + QString loopPart=mid(start+startTag.length(), end-start-startTag.length()); + QString insertMe; + for (int i=0; i +#include +#include +#include +#include +#include + +/** + Enhanced version of QString for template processing. Templates + are usually loaded from files, but may also be loaded from + prepared Strings. + Example template file: +

+ 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}
+ 

+

+ Example code to fill this template: +

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

+

+ The code example above shows how variable within loops are numbered. + Counting starts with 0. Loops can be nested, for example: +

+ <table>
+ {loop row}
+     <tr>
+     {loop row.column}
+         <td>{row.column.value}</td>
+     {end row.column}
+     </tr>
+ {end row}
+ </table>
+ 

+

+ Example code to fill this nested loop with 3 rows and 4 columns: +

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

+ @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 diff --git a/YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.cpp b/YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.cpp new file mode 100644 index 00000000..9073e34b --- /dev/null +++ b/YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.cpp @@ -0,0 +1,30 @@ +#include "templatecache.h" +#include +#include +#include + +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; +} + diff --git a/YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.h b/YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.h new file mode 100644 index 00000000..6e79f119 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfTemplateEngine/templatecache.h @@ -0,0 +1,77 @@ +#ifndef TEMPLATECACHE_H +#define TEMPLATECACHE_H + +#include "templateloader.h" +#include + +/** + 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. +

+ In case of local file system, the use of this cache is optionally, since + the operating system caches files already. +

+ 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: +

+  path=.
+  suffix=.tpl
+  encoding=UTF-8
+  cacheSize=1000000
+  cacheTime=60000
+  
+ 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. +

+ 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 cache; + +}; + +#endif // TEMPLATECACHE_H diff --git a/YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.cpp b/YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.cpp new file mode 100644 index 00000000..33ba7d02 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.cpp @@ -0,0 +1,103 @@ +/** + @file + @author Stefan Frings +*/ + +#include "templateloader.h" +#include +#include +#include +#include +#include + +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 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); +} diff --git a/YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.h b/YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.h new file mode 100644 index 00000000..5635af40 --- /dev/null +++ b/YACReaderLibrary/server/lib/bfTemplateEngine/templateloader.h @@ -0,0 +1,85 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef TEMPLATELOADER_H +#define TEMPLATELOADER_H + +#include +#include +#include +#include "template.h" +#include + +/** + 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: +

+  path=.
+  suffix=.tpl
+  encoding=UTF-8
+  
+ 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 diff --git a/YACReaderLibrary/server/main.cpp b/YACReaderLibrary/server/main.cpp new file mode 100644 index 00000000..d5ac4be5 --- /dev/null +++ b/YACReaderLibrary/server/main.cpp @@ -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. +

+ 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. +

+ 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. +

+ 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(); +} diff --git a/YACReaderLibrary/server/requestmapper.cpp b/YACReaderLibrary/server/requestmapper.cpp new file mode 100644 index 00000000..107cba28 --- /dev/null +++ b/YACReaderLibrary/server/requestmapper.cpp @@ -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); + } +} diff --git a/YACReaderLibrary/server/requestmapper.h b/YACReaderLibrary/server/requestmapper.h new file mode 100644 index 00000000..d44586f6 --- /dev/null +++ b/YACReaderLibrary/server/requestmapper.h @@ -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 diff --git a/YACReaderLibrary/server/server.pri b/YACReaderLibrary/server/server.pri new file mode 100644 index 00000000..1f4e5df4 --- /dev/null +++ b/YACReaderLibrary/server/server.pri @@ -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) diff --git a/YACReaderLibrary/server/startup.cpp b/YACReaderLibrary/server/startup.cpp new file mode 100644 index 00000000..221d9dce --- /dev/null +++ b/YACReaderLibrary/server/startup.cpp @@ -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 +#include + +/** 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() +{ + +} + + + diff --git a/YACReaderLibrary/server/startup.h b/YACReaderLibrary/server/startup.h new file mode 100644 index 00000000..7405f0d1 --- /dev/null +++ b/YACReaderLibrary/server/startup.h @@ -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 diff --git a/YACReaderLibrary/server/static.cpp b/YACReaderLibrary/server/static.cpp new file mode 100644 index 00000000..dd1bc4e9 --- /dev/null +++ b/YACReaderLibrary/server/static.cpp @@ -0,0 +1,59 @@ +/** + @file + @author Stefan Frings +*/ + +#include "static.h" +#include +#include +#include +#include + +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; +} + diff --git a/YACReaderLibrary/server/static.h b/YACReaderLibrary/server/static.h new file mode 100644 index 00000000..74abf55b --- /dev/null +++ b/YACReaderLibrary/server/static.h @@ -0,0 +1,64 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef STATIC_H +#define STATIC_H + +#include +#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. +

+ 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