diff --git a/.gitignore b/.gitignore index 259148f..4348752 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,8 @@ *.exe *.out *.app + + +*.kdev4 +build/ +install/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7cf15f2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,96 @@ +cmake_minimum_required(VERSION 3.1) +project(Squeezer CXX) + +find_package(QT NAMES Qt5 COMPONENTS Core REQUIRED HINTS $ENV{Qt5_DIR}) +find_package(Qt5 COMPONENTS Core Network REQUIRED) + +set(CMAKE_AUTOMOC ON) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=format -Werror=return-type") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=format -Werror=return-type") + +if(CMAKE_BUILD_TYPE MATCHES Debug) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -fsanitize=undefined") + set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") + add_definitions("-D_GLIBCXX_DEBUG") + add_definitions("-DQT_SHAREDPOINTER_TRACK_POINTERS") + add_definitions("-DCMAKE_DEBUG") + add_definitions("-DSUPERVERBOSE") +endif() + +add_library(QtWebApp STATIC + QtWebApp/httpserver/httpconnectionhandler.cpp + QtWebApp/httpserver/httpconnectionhandler.h + QtWebApp/httpserver/httpconnectionhandlerpool.cpp + QtWebApp/httpserver/httpconnectionhandlerpool.h + QtWebApp/httpserver/httpcookie.cpp + QtWebApp/httpserver/httpcookie.h + QtWebApp/httpserver/httpglobal.cpp + QtWebApp/httpserver/httpglobal.h + QtWebApp/httpserver/httplistener.cpp + QtWebApp/httpserver/httplistener.h + QtWebApp/httpserver/httprequest.cpp + QtWebApp/httpserver/httprequest.h + QtWebApp/httpserver/httprequesthandler.cpp + QtWebApp/httpserver/httprequesthandler.h + QtWebApp/httpserver/httpresponse.cpp + QtWebApp/httpserver/httpresponse.h + QtWebApp/httpserver/httpsession.cpp + QtWebApp/httpserver/httpsession.h + QtWebApp/httpserver/httpsessionstore.cpp + QtWebApp/httpserver/httpsessionstore.h + QtWebApp/httpserver/staticfilecontroller.cpp + QtWebApp/httpserver/staticfilecontroller.h + + QtWebApp/logging/dualfilelogger.cpp + QtWebApp/logging/dualfilelogger.h + QtWebApp/logging/filelogger.cpp + QtWebApp/logging/filelogger.h + QtWebApp/logging/logger.cpp + QtWebApp/logging/logger.h + QtWebApp/logging/logglobal.h + QtWebApp/logging/logmessage.cpp + QtWebApp/logging/logmessage.h + + QtWebApp/templateengine/template.cpp + QtWebApp/templateengine/template.h + QtWebApp/templateengine/templatecache.cpp + QtWebApp/templateengine/templatecache.h + QtWebApp/templateengine/templateglobal.h + QtWebApp/templateengine/templateloader.cpp + QtWebApp/templateengine/templateloader.h +) +target_include_directories(QtWebApp PUBLIC + QtWebApp/logging + QtWebApp/templateengine + QtWebApp/httpserver +) +target_link_libraries(QtWebApp Qt5::Core Qt5::Network) + +add_executable(squeezer + src/controller/dumpcontroller.cpp + src/controller/dumpcontroller.h + src/controller/fileuploadcontroller.cpp + src/controller/fileuploadcontroller.h + src/controller/formcontroller.cpp + src/controller/formcontroller.h + src/controller/logincontroller.cpp + src/controller/logincontroller.h + src/controller/sessioncontroller.cpp + src/controller/sessioncontroller.h + src/controller/templatecontroller.cpp + src/controller/templatecontroller.h + src/documentcache.h + src/global.cpp + src/global.h + src/main.cpp + src/requestmapper.cpp + src/requestmapper.h +) +target_link_libraries(squeezer QtWebApp) + +install(TARGETS squeezer DESTINATION bin) +install(DIRECTORY data/ DESTINATION .) diff --git a/QtWebApp/.gitignore b/QtWebApp/.gitignore new file mode 100644 index 0000000..189bbd7 --- /dev/null +++ b/QtWebApp/.gitignore @@ -0,0 +1,4 @@ +build-*-Debug +build-*-Release +QtWebApp/doc/html +*.pro.user* diff --git a/QtWebApp/httpserver/httpconnectionhandler.cpp b/QtWebApp/httpserver/httpconnectionhandler.cpp new file mode 100755 index 0000000..849425d --- /dev/null +++ b/QtWebApp/httpserver/httpconnectionhandler.cpp @@ -0,0 +1,274 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpconnectionhandler.h" +#include "httpresponse.h" + +using namespace stefanfrings; + +HttpConnectionHandler::HttpConnectionHandler(const QSettings *settings, HttpRequestHandler *requestHandler, const QSslConfiguration* sslConfiguration) + : QObject() +{ + Q_ASSERT(settings!=nullptr); + Q_ASSERT(requestHandler!=nullptr); + this->settings=settings; + this->requestHandler=requestHandler; + this->sslConfiguration=sslConfiguration; + currentRequest=nullptr; + busy=false; + + // execute signals in a new thread + thread = new QThread(); + thread->start(); + qDebug("HttpConnectionHandler (%p): thread started", static_cast(this)); + moveToThread(thread); + readTimer.moveToThread(thread); + readTimer.setSingleShot(true); + + // Create TCP or SSL socket + createSocket(); + socket->moveToThread(thread); + + // Connect signals + connect(socket, SIGNAL(readyRead()), SLOT(read())); + connect(socket, SIGNAL(disconnected()), SLOT(disconnected())); + connect(&readTimer, SIGNAL(timeout()), SLOT(readTimeout())); + connect(thread, SIGNAL(finished()), this, SLOT(thread_done())); + + qDebug("HttpConnectionHandler (%p): constructed", static_cast(this)); +} + + +void HttpConnectionHandler::thread_done() +{ + readTimer.stop(); + socket->close(); + delete socket; + qDebug("HttpConnectionHandler (%p): thread stopped", static_cast(this)); +} + + +HttpConnectionHandler::~HttpConnectionHandler() +{ + thread->quit(); + thread->wait(); + thread->deleteLater(); + qDebug("HttpConnectionHandler (%p): destroyed", static_cast(this)); +} + + +void HttpConnectionHandler::createSocket() +{ + // If SSL is supported and configured, then create an instance of QSslSocket + #ifndef QT_NO_SSL + if (sslConfiguration) + { + QSslSocket* sslSocket=new QSslSocket(); + sslSocket->setSslConfiguration(*sslConfiguration); + socket=sslSocket; + qDebug("HttpConnectionHandler (%p): SSL is enabled", static_cast(this)); + return; + } + #endif + // else create an instance of QTcpSocket + socket=new QTcpSocket(); +} + + +void HttpConnectionHandler::handleConnection(tSocketDescriptor socketDescriptor) +{ + qDebug("HttpConnectionHandler (%p): handle new connection", static_cast(this)); + busy = true; + Q_ASSERT(socket->isOpen()==false); // if not, then the handler is already busy + + //UGLY workaround - we need to clear writebuffer before reusing this socket + //https://bugreports.qt-project.org/browse/QTBUG-28914 + socket->connectToHost("",0); + socket->abort(); + + if (!socket->setSocketDescriptor(socketDescriptor)) + { + qCritical("HttpConnectionHandler (%p): cannot initialize socket: %s", + static_cast(this),qPrintable(socket->errorString())); + return; + } + + #ifndef QT_NO_SSL + // Switch on encryption, if SSL is configured + if (sslConfiguration) + { + qDebug("HttpConnectionHandler (%p): Starting encryption", static_cast(this)); + (static_cast(socket))->startServerEncryption(); + } + #endif + + // Start timer for read timeout + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + // delete previous request + delete currentRequest; + currentRequest=nullptr; +} + + +bool HttpConnectionHandler::isBusy() +{ + return busy; +} + +void HttpConnectionHandler::setBusy() +{ + this->busy = true; +} + + +void HttpConnectionHandler::readTimeout() +{ + qDebug("HttpConnectionHandler (%p): read timeout occured",static_cast(this)); + + //Commented out because QWebView cannot handle this. + //socket->write("HTTP/1.1 408 request timeout\r\nConnection: close\r\n\r\n408 request timeout\r\n"); + + while(socket->bytesToWrite()) socket->waitForBytesWritten(); + socket->disconnectFromHost(); + delete currentRequest; + currentRequest=nullptr; +} + + +void HttpConnectionHandler::disconnected() +{ + qDebug("HttpConnectionHandler (%p): disconnected", static_cast(this)); + socket->close(); + readTimer.stop(); + busy = false; +} + +void HttpConnectionHandler::read() +{ + // The loop adds support for HTTP pipelinig + while (socket->bytesAvailable()) + { + #ifdef SUPERVERBOSE + qDebug("HttpConnectionHandler (%p): read input",static_cast(this)); + #endif + + // Create new HttpRequest object if necessary + if (!currentRequest) + { + currentRequest=new HttpRequest(settings); + } + + // Collect data for the request object + while (socket->bytesAvailable() && currentRequest->getStatus()!=HttpRequest::complete && currentRequest->getStatus()!=HttpRequest::abort) + { + currentRequest->readFromSocket(socket); + if (currentRequest->getStatus()==HttpRequest::waitForBody) + { + // Restart timer for read timeout, otherwise it would + // expire during large file uploads. + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + } + } + + // If the request is aborted, return error message and close the connection + if (currentRequest->getStatus()==HttpRequest::abort) + { + socket->write("HTTP/1.1 413 entity too large\r\nConnection: close\r\n\r\n413 Entity too large\r\n"); + while(socket->bytesToWrite()) socket->waitForBytesWritten(); + socket->disconnectFromHost(); + delete currentRequest; + currentRequest=nullptr; + return; + } + + // If the request is complete, let the request mapper dispatch it + if (currentRequest->getStatus()==HttpRequest::complete) + { + readTimer.stop(); + qDebug("HttpConnectionHandler (%p): received request",static_cast(this)); + + // Copy the Connection:close header to the response + HttpResponse response(socket); + bool closeConnection=QString::compare(currentRequest->getHeader("Connection"),"close",Qt::CaseInsensitive)==0; + if (closeConnection) + { + response.setHeader("Connection","close"); + } + + // In case of HTTP 1.0 protocol add the Connection:close header. + // This ensures that the HttpResponse does not activate chunked mode, which is not spported by HTTP 1.0. + else + { + bool http1_0=QString::compare(currentRequest->getVersion(),"HTTP/1.0",Qt::CaseInsensitive)==0; + if (http1_0) + { + closeConnection=true; + response.setHeader("Connection","close"); + } + } + + // Call the request mapper + try + { + requestHandler->service(*currentRequest, response); + } + catch (...) + { + qCritical("HttpConnectionHandler (%p): An uncatched exception occured in the request handler", + static_cast(this)); + } + + // Finalize sending the response if not already done + if (!response.hasSentLastPart()) + { + response.write(QByteArray(),true); + } + + qDebug("HttpConnectionHandler (%p): finished request",static_cast(this)); + + // Find out whether the connection must be closed + if (!closeConnection) + { + // Maybe the request handler or mapper added a Connection:close header in the meantime + bool closeResponse=QString::compare(response.getHeaders().value("Connection"),"close",Qt::CaseInsensitive)==0; + if (closeResponse==true) + { + closeConnection=true; + } + else + { + // If we have no Content-Length header and did not use chunked mode, then we have to close the + // connection to tell the HTTP client that the end of the response has been reached. + bool hasContentLength=response.getHeaders().contains("Content-Length"); + if (!hasContentLength) + { + bool hasChunkedMode=QString::compare(response.getHeaders().value("Transfer-Encoding"),"chunked",Qt::CaseInsensitive)==0; + if (!hasChunkedMode) + { + closeConnection=true; + } + } + } + } + + // Close the connection or prepare for the next request on the same connection. + if (closeConnection) + { + while(socket->bytesToWrite()) socket->waitForBytesWritten(); + socket->disconnectFromHost(); + } + else + { + // Start timer for next request + int readTimeout=settings->value("readTimeout",10000).toInt(); + readTimer.start(readTimeout); + } + delete currentRequest; + currentRequest=nullptr; + } + } +} diff --git a/QtWebApp/httpserver/httpconnectionhandler.h b/QtWebApp/httpserver/httpconnectionhandler.h new file mode 100755 index 0000000..ae0d92c --- /dev/null +++ b/QtWebApp/httpserver/httpconnectionhandler.h @@ -0,0 +1,130 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPCONNECTIONHANDLER_H +#define HTTPCONNECTIONHANDLER_H + +#ifndef QT_NO_SSL + #include +#endif +#include +#include +#include +#include +#include "httpglobal.h" +#include "httprequest.h" +#include "httprequesthandler.h" + +namespace stefanfrings { + +/** Alias type definition, for compatibility to different Qt versions */ +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + typedef qintptr tSocketDescriptor; +#else + typedef int tSocketDescriptor; +#endif + +/** Alias for QSslConfiguration if OpenSSL is not supported */ +#ifdef QT_NO_SSL + #define QSslConfiguration QObject +#endif + +/** + The connection handler accepts incoming connections and dispatches incoming requests to to a + request mapper. Since HTTP clients can send multiple requests before waiting for the response, + the incoming requests are queued and processed one after the other. +

+ Example for the required configuration settings: +

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

+ The readTimeout value defines the maximum time to wait for a complete HTTP request. +

+ 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. +*/ +class DECLSPEC HttpConnectionHandler : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpConnectionHandler) + +public: + + /** + Constructor. + @param settings Configuration settings of the HTTP webserver + @param requestHandler Handler that will process each incoming HTTP request + @param sslConfiguration SSL (HTTPS) will be used if not NULL + */ + HttpConnectionHandler(const QSettings* settings, HttpRequestHandler* requestHandler, + const QSslConfiguration* sslConfiguration=nullptr); + + /** Destructor */ + virtual ~HttpConnectionHandler(); + + /** Returns true, if this handler is in use. */ + bool isBusy(); + + /** Mark this handler as busy */ + void setBusy(); + +private: + + /** Configuration settings */ + const QSettings* settings; + + /** TCP socket of the current connection */ + QTcpSocket* socket; + + /** The thread that processes events of this connection */ + QThread* thread; + + /** Time for read timeout detection */ + QTimer readTimer; + + /** Storage for the current incoming HTTP request */ + HttpRequest* currentRequest; + + /** Dispatches received requests to services */ + HttpRequestHandler* requestHandler; + + /** This shows the busy-state from a very early time */ + bool busy; + + /** Configuration for SSL */ + const QSslConfiguration* sslConfiguration; + + /** Create SSL or TCP socket */ + void createSocket(); + +public slots: + + /** + Received from from the listener, when the handler shall start processing a new connection. + @param socketDescriptor references the accepted connection. + */ + void handleConnection(const tSocketDescriptor socketDescriptor); + +private slots: + + /** Received from the socket when a read-timeout occured */ + void readTimeout(); + + /** Received from the socket when incoming data can be read */ + void read(); + + /** Received from the socket when a connection has been closed */ + void disconnected(); + + /** Cleanup after the thread is closed */ + void thread_done(); +}; + +} // end of namespace + +#endif // HTTPCONNECTIONHANDLER_H diff --git a/QtWebApp/httpserver/httpconnectionhandlerpool.cpp b/QtWebApp/httpserver/httpconnectionhandlerpool.cpp new file mode 100755 index 0000000..73f6603 --- /dev/null +++ b/QtWebApp/httpserver/httpconnectionhandlerpool.cpp @@ -0,0 +1,194 @@ +#ifndef QT_NO_SSL + #include + #include + #include + #include +#endif +#include +#include "httpconnectionhandlerpool.h" + +using namespace stefanfrings; + +HttpConnectionHandlerPool::HttpConnectionHandlerPool(const QSettings *settings, HttpRequestHandler *requestHandler) + : QObject() +{ + Q_ASSERT(settings!=0); + this->settings=settings; + this->requestHandler=requestHandler; + this->sslConfiguration=NULL; + loadSslConfig(); + cleanupTimer.start(settings->value("cleanupInterval",1000).toInt()); + connect(&cleanupTimer, SIGNAL(timeout()), SLOT(cleanup())); +} + + +HttpConnectionHandlerPool::~HttpConnectionHandlerPool() +{ + // delete all connection handlers and wait until their threads are closed + foreach(HttpConnectionHandler* handler, pool) + { + delete handler; + } + delete sslConfiguration; + qDebug("HttpConnectionHandlerPool (%p): destroyed", this); +} + + +HttpConnectionHandler* HttpConnectionHandlerPool::getConnectionHandler() +{ + HttpConnectionHandler* freeHandler=0; + mutex.lock(); + // find a free handler in pool + foreach(HttpConnectionHandler* handler, pool) + { + if (!handler->isBusy()) + { + freeHandler=handler; + freeHandler->setBusy(); + break; + } + } + // create a new handler, if necessary + if (!freeHandler) + { + int maxConnectionHandlers=settings->value("maxThreads",100).toInt(); + if (pool.count()setBusy(); + pool.append(freeHandler); + } + } + mutex.unlock(); + return freeHandler; +} + + +void HttpConnectionHandlerPool::cleanup() +{ + int maxIdleHandlers=settings->value("minThreads",1).toInt(); + int idleCounter=0; + mutex.lock(); + foreach(HttpConnectionHandler* handler, pool) + { + if (!handler->isBusy()) + { + if (++idleCounter > maxIdleHandlers) + { + delete handler; + pool.removeOne(handler); + long int poolSize=(long int)pool.size(); + qDebug("HttpConnectionHandlerPool: Removed connection handler (%p), pool size is now %li",handler,poolSize); + break; // remove only one handler in each interval + } + } + } + mutex.unlock(); +} + + +void HttpConnectionHandlerPool::loadSslConfig() +{ + // If certificate and key files are configured, then load them + QString sslKeyFileName=settings->value("sslKeyFile","").toString(); + QString sslCertFileName=settings->value("sslCertFile","").toString(); + QString caCertFileName=settings->value("caCertFile","").toString(); + bool verifyPeer=settings->value("verifyPeer","false").toBool(); + + if (!sslKeyFileName.isEmpty() && !sslCertFileName.isEmpty()) + { + #ifdef QT_NO_SSL + qWarning("HttpConnectionHandlerPool: SSL is not supported"); + #else + // Convert relative fileNames to absolute, based on the directory of the config file. + QFileInfo configFile(settings->fileName()); + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(sslKeyFileName) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(sslKeyFileName)) + #endif + { + sslKeyFileName=QFileInfo(configFile.absolutePath(),sslKeyFileName).absoluteFilePath(); + } + + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(sslCertFileName) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(sslCertFileName)) + #endif + { + sslCertFileName=QFileInfo(configFile.absolutePath(),sslCertFileName).absoluteFilePath(); + } + + // Load the SSL certificate + QFile certFile(sslCertFileName); + if (!certFile.open(QIODevice::ReadOnly)) + { + qCritical("HttpConnectionHandlerPool: cannot open sslCertFile %s", qPrintable(sslCertFileName)); + return; + } + QSslCertificate certificate(&certFile, QSsl::Pem); + certFile.close(); + + // Load the key file + QFile keyFile(sslKeyFileName); + if (!keyFile.open(QIODevice::ReadOnly)) + { + qCritical("HttpConnectionHandlerPool: cannot open sslKeyFile %s", qPrintable(sslKeyFileName)); + return; + } + QSslKey sslKey(&keyFile, QSsl::Rsa, QSsl::Pem); + keyFile.close(); + + // Create the SSL configuration + sslConfiguration=new QSslConfiguration(); + sslConfiguration->setProtocol(QSsl::AnyProtocol); + sslConfiguration->setLocalCertificate(certificate); + sslConfiguration->setPrivateKey(sslKey); + + // We can optionally use a CA certificate to validate the HTTP clients + if (!caCertFileName.isEmpty()) + { + #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) + qCritical("HttpConnectionHandlerPool: Using a caCertFile requires Qt 5.15 or newer"); + #else + + // Convert relative fileName to absolute, based on the directory of the config file. + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(sslCaCertFileName) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(caCertFileName)) + #endif + { + caCertFileName=QFileInfo(configFile.absolutePath(),caCertFileName).absoluteFilePath(); + } + + // Load the CA cert file + QFile caCertFile(caCertFileName); + if (!caCertFile.open(QIODevice::ReadOnly)) + { + qCritical("HttpConnectionHandlerPool: cannot open caCertFile %s", qPrintable(caCertFileName)); + return; + } + QSslCertificate caCertificate(&caCertFile, QSsl::Pem); + caCertFile.close(); + + // Configure SSL + sslConfiguration->addCaCertificate(caCertificate); + #endif + } + + // Enable or disable verification of the HTTP client + if (verifyPeer) + { + sslConfiguration->setPeerVerifyMode(QSslSocket::VerifyPeer); + } + else + { + sslConfiguration->setPeerVerifyMode(QSslSocket::VerifyNone); + } + + qDebug("HttpConnectionHandlerPool: SSL settings loaded"); + #endif + } +} diff --git a/QtWebApp/httpserver/httpconnectionhandlerpool.h b/QtWebApp/httpserver/httpconnectionhandlerpool.h new file mode 100755 index 0000000..7a358a7 --- /dev/null +++ b/QtWebApp/httpserver/httpconnectionhandlerpool.h @@ -0,0 +1,134 @@ +#ifndef HTTPCONNECTIONHANDLERPOOL_H +#define HTTPCONNECTIONHANDLERPOOL_H + +#include +#include +#include +#include +#include "httpglobal.h" +#include "httpconnectionhandler.h" + +namespace stefanfrings { + +/** + Pool of http connection handlers. The size of the pool grows and + shrinks on demand. +

+ Example for the required configuration settings: +

+  readTimeout=60000
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+
+  minThreads=4
+  maxThreads=100
+  cleanupInterval=60000  
+  
+

+ The readTimeout value defines the maximum time to wait for a complete HTTP request. +

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

+ After server start, the size of the thread pool is always 0. Threads + are started on demand when requests come in. The cleanup timer reduces + the number of idle threads slowly by closing one thread in each interval. + But the configured minimum number of threads are kept running. +

+ Additional settings for SSL (HTTPS): +

+  sslKeyFile=ssl/server.key
+  sslCertFile=ssl/server.crt
+  ;caCertFile=ssl/ca.crt
+  verifyPeer=false
+  
+ For SSL support, you need at least a pair of OpenSSL x509 certificate and an RSA key, + both files in PEM format. To enable verification of the peer (the calling web browser), + you can either use the central certificate store of the operating system, or provide + a CA certificate file in PEM format. The certificates of the peers must have been + derived from the CA certificate. +

+ Example commands to create these files: +

+  # Generate CA key
+  openssl genrsa 2048 > ca.key
+
+  # Generate CA certificate
+  openssl req -new -x509 -nodes -days 365000 -key ca.key -out ca.crt
+
+  # Generate a server key and certificate request
+  openssl req -newkey rsa:2048 -nodes -days 365000 -keyout server.key -out server.req
+
+  # Generate a signed server certificate
+  openssl x509 -req -days 365000 -set_serial 01 -in server.req -out server.crt -CA ca.crt -CAkey ca.key
+
+  # Generate a client key and certificate request
+  openssl req -newkey rsa:2048 -nodes -days 365000 -keyout client.key -out client.req
+
+  # Generate a signed client certificate
+  openssl x509 -req -days 365000 -set_serial 01 -in client.req -out client.crt  -CA ca.crt -CAkey ca.key
+
+  # Combine client key and certificate into one PKCS12 file
+  openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -certfile ca.crt
+
+  # Remove temporary files
+  rm *.req
+  
+

+ Please note that a listener with SSL can only handle HTTPS protocol. To support both + HTTP and HTTPS simultaneously, you need to start two listeners on different ports + one with SLL and one without SSL (usually on public ports 80 and 443, or locally on 8080 and 8443). +*/ + +class DECLSPEC HttpConnectionHandlerPool : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpConnectionHandlerPool) +public: + + /** + Constructor. + @param settings Configuration settings for the HTTP server. Must not be 0. + @param requestHandler The handler that will process each received HTTP request. + */ + HttpConnectionHandlerPool(const QSettings* settings, HttpRequestHandler *requestHandler); + + /** Destructor */ + virtual ~HttpConnectionHandlerPool(); + + /** Get a free connection handler, or 0 if not available. */ + HttpConnectionHandler* getConnectionHandler(); + +private: + + /** Settings for this pool */ + const QSettings* settings; + + /** Will be assigned to each Connectionhandler during their creation */ + HttpRequestHandler* requestHandler; + + /** Pool of connection handlers */ + QList pool; + + /** Timer to clean-up unused connection handler */ + QTimer cleanupTimer; + + /** Used to synchronize threads */ + QMutex mutex; + + /** The SSL configuration (certificate, key and other settings) */ + QSslConfiguration* sslConfiguration; + + /** Load SSL configuration */ + void loadSslConfig(); + +private slots: + + /** Received from the clean-up timer. */ + void cleanup(); + +}; + +} // end of namespace + +#endif // HTTPCONNECTIONHANDLERPOOL_H diff --git a/QtWebApp/httpserver/httpcookie.cpp b/QtWebApp/httpserver/httpcookie.cpp new file mode 100755 index 0000000..d85232e --- /dev/null +++ b/QtWebApp/httpserver/httpcookie.cpp @@ -0,0 +1,285 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpcookie.h" + +using namespace stefanfrings; + +HttpCookie::HttpCookie() +{ + version=1; + maxAge=0; + secure=false; +} + +HttpCookie::HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path, + const QByteArray comment, const QByteArray domain, const bool secure, const bool httpOnly, + const QByteArray sameSite) +{ + this->name=name; + this->value=value; + this->maxAge=maxAge; + this->path=path; + this->comment=comment; + this->domain=domain; + this->secure=secure; + this->httpOnly=httpOnly; + this->sameSite=sameSite; + this->version=1; +} + +HttpCookie::HttpCookie(const QByteArray source) +{ + version=1; + maxAge=0; + secure=false; + httpOnly=false; + QList list=splitCSV(source); + foreach(QByteArray part, list) + { + + // Split the part into name and value + QByteArray name; + QByteArray value; + int posi=part.indexOf('='); + if (posi) + { + name=part.left(posi).trimmed(); + value=part.mid(posi+1).trimmed(); + } + else + { + name=part.trimmed(); + value=""; + } + + // Set fields + if (name=="Comment") + { + comment=value; + } + else if (name=="Domain") + { + domain=value; + } + else if (name=="Max-Age") + { + maxAge=value.toInt(); + } + else if (name=="Path") + { + path=value; + } + else if (name=="Secure") + { + secure=true; + } + else if (name=="HttpOnly") + { + httpOnly=true; + } + else if (name=="SameSite") + { + sameSite=value; + } + else if (name=="Version") + { + version=value.toInt(); + } + else { + if (this->name.isEmpty()) + { + this->name=name; + this->value=value; + } + else + { + qWarning("HttpCookie: Ignoring unknown %s=%s",name.data(),value.data()); + } + } + } +} + +QByteArray HttpCookie::toByteArray() const +{ + QByteArray buffer(name); + buffer.append('='); + buffer.append(value); + if (!comment.isEmpty()) + { + buffer.append("; Comment="); + buffer.append(comment); + } + if (!domain.isEmpty()) + { + buffer.append("; Domain="); + buffer.append(domain); + } + if (maxAge!=0) + { + buffer.append("; Max-Age="); + buffer.append(QByteArray::number(maxAge)); + } + if (!path.isEmpty()) + { + buffer.append("; Path="); + buffer.append(path); + } + if (secure) { + buffer.append("; Secure"); + } + if (httpOnly) { + buffer.append("; HttpOnly"); + } + if (!sameSite.isEmpty()) { + buffer.append("; SameSite="); + buffer.append(sameSite); + } + buffer.append("; Version="); + buffer.append(QByteArray::number(version)); + return buffer; +} + +void HttpCookie::setName(const QByteArray name) +{ + this->name=name; +} + +void HttpCookie::setValue(const QByteArray value) +{ + this->value=value; +} + +void HttpCookie::setComment(const QByteArray comment) +{ + this->comment=comment; +} + +void HttpCookie::setDomain(const QByteArray domain) +{ + this->domain=domain; +} + +void HttpCookie::setMaxAge(const int maxAge) +{ + this->maxAge=maxAge; +} + +void HttpCookie::setPath(const QByteArray path) +{ + this->path=path; +} + +void HttpCookie::setSecure(const bool secure) +{ + this->secure=secure; +} + +void HttpCookie::setHttpOnly(const bool httpOnly) +{ + this->httpOnly=httpOnly; +} + +void HttpCookie::setSameSite(const QByteArray sameSite) +{ + this->sameSite=sameSite; +} + +QByteArray HttpCookie::getName() const +{ + return name; +} + +QByteArray HttpCookie::getValue() const +{ + return value; +} + +QByteArray HttpCookie::getComment() const +{ + return comment; +} + +QByteArray HttpCookie::getDomain() const +{ + return domain; +} + +int HttpCookie::getMaxAge() const +{ + return maxAge; +} + +QByteArray HttpCookie::getPath() const +{ + return path; +} + +bool HttpCookie::getSecure() const +{ + return secure; +} + +bool HttpCookie::getHttpOnly() const +{ + return httpOnly; +} + +QByteArray HttpCookie::getSameSite() const +{ + return sameSite; +} + +int HttpCookie::getVersion() const +{ + return version; +} + +QList HttpCookie::splitCSV(const QByteArray source) +{ + bool inString=false; + QList list; + QByteArray buffer; + for (int i=0; i +#include +#include "httpglobal.h" + +namespace stefanfrings { + +/** + HTTP cookie as defined in RFC 2109. + Supports some additional attributes of RFC6265bis. +*/ + +class DECLSPEC HttpCookie +{ +public: + + /** Creates an empty cookie */ + HttpCookie(); + + /** + Create a cookie and set name/value pair. + @param name name of the cookie + @param value value of the cookie + @param maxAge maximum age of the cookie in seconds. 0=discard immediately + @param path Path for that the cookie will be sent, default="/" which means the whole domain + @param comment Optional comment, may be displayed by the web browser somewhere + @param domain Optional domain for that the cookie will be sent. Defaults to the current domain + @param secure If true, the cookie will be sent by the browser to the server only on secure connections + @param httpOnly If true, the browser does not allow client-side scripts to access the cookie + @param sameSite Declare if the cookie can only be read by the same site, which is a stronger + restriction than the domain. Allowed values: "Lax" and "Strict". + */ + HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, + const QByteArray path="/", const QByteArray comment=QByteArray(), + const QByteArray domain=QByteArray(), const bool secure=false, + const bool httpOnly=false, const QByteArray sameSite=QByteArray()); + + /** + Create a cookie from a string. + @param source String as received in a HTTP Cookie2 header. + */ + HttpCookie(const QByteArray source); + + /** Convert this cookie to a string that may be used in a Set-Cookie header. */ + QByteArray toByteArray() const ; + + /** + Split a string list into parts, where each part is delimited by semicolon. + Semicolons within double quotes are skipped. Double quotes are removed. + */ + static QList splitCSV(const QByteArray source); + + /** Set the name of this cookie */ + void setName(const QByteArray name); + + /** Set the value of this cookie */ + void setValue(const QByteArray value); + + /** Set the comment of this cookie */ + void setComment(const QByteArray comment); + + /** Set the domain of this cookie */ + void setDomain(const QByteArray domain); + + /** Set the maximum age of this cookie in seconds. 0=discard immediately */ + void setMaxAge(const int maxAge); + + /** Set the path for that the cookie will be sent, default="/" which means the whole domain */ + void setPath(const QByteArray path); + + /** Set secure mode, so that the cookie will be sent by the browser to the server only on secure connections */ + void setSecure(const bool secure); + + /** Set HTTP-only mode, so that the browser does not allow client-side scripts to access the cookie */ + void setHttpOnly(const bool httpOnly); + + /** + * Set same-site mode, so that the browser does not allow other web sites to access the cookie. + * Allowed values: "Lax" and "Strict". + */ + void setSameSite(const QByteArray sameSite); + + /** Get the name of this cookie */ + QByteArray getName() const; + + /** Get the value of this cookie */ + QByteArray getValue() const; + + /** Get the comment of this cookie */ + QByteArray getComment() const; + + /** Get the domain of this cookie */ + QByteArray getDomain() const; + + /** Get the maximum age of this cookie in seconds. */ + int getMaxAge() const; + + /** Set the path of this cookie */ + QByteArray getPath() const; + + /** Get the secure flag of this cookie */ + bool getSecure() const; + + /** Get the HTTP-only flag of this cookie */ + bool getHttpOnly() const; + + /** Get the same-site flag of this cookie */ + QByteArray getSameSite() const; + + /** Returns always 1 */ + int getVersion() const; + +private: + + QByteArray name; + QByteArray value; + QByteArray comment; + QByteArray domain; + int maxAge; + QByteArray path; + bool secure; + bool httpOnly; + QByteArray sameSite; + int version; + +}; + +} // end of namespace + +#endif // HTTPCOOKIE_H diff --git a/QtWebApp/httpserver/httpglobal.cpp b/QtWebApp/httpserver/httpglobal.cpp new file mode 100755 index 0000000..706e4a4 --- /dev/null +++ b/QtWebApp/httpserver/httpglobal.cpp @@ -0,0 +1,7 @@ +#include "httpglobal.h" + +const char* getQtWebAppLibVersion() +{ + return "1.8.5"; +} + diff --git a/QtWebApp/httpserver/httpglobal.h b/QtWebApp/httpserver/httpglobal.h new file mode 100755 index 0000000..2735fe2 --- /dev/null +++ b/QtWebApp/httpserver/httpglobal.h @@ -0,0 +1,31 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPGLOBAL_H +#define HTTPGLOBAL_H + +#include + +// This is specific to Windows dll's +#if defined(Q_OS_WIN) + #if defined(QTWEBAPPLIB_EXPORT) + #define DECLSPEC Q_DECL_EXPORT + #elif defined(QTWEBAPPLIB_IMPORT) + #define DECLSPEC Q_DECL_IMPORT + #endif +#endif +#if !defined(DECLSPEC) + #define DECLSPEC +#endif + +/** Get the library version number */ +DECLSPEC const char* getQtWebAppLibVersion(); + +#if __cplusplus < 201103L + #define nullptr 0 +#endif + +#endif // HTTPGLOBAL_H + diff --git a/QtWebApp/httpserver/httplistener.cpp b/QtWebApp/httpserver/httplistener.cpp new file mode 100755 index 0000000..7487158 --- /dev/null +++ b/QtWebApp/httpserver/httplistener.cpp @@ -0,0 +1,90 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httplistener.h" +#include "httpconnectionhandler.h" +#include "httpconnectionhandlerpool.h" +#include + +using namespace stefanfrings; + +HttpListener::HttpListener(const QSettings* settings, HttpRequestHandler* requestHandler, QObject *parent) + : QTcpServer(parent) +{ + Q_ASSERT(settings!=nullptr); + Q_ASSERT(requestHandler!=nullptr); + pool=nullptr; + this->settings=settings; + this->requestHandler=requestHandler; + // Reqister type of socketDescriptor for signal/slot handling + qRegisterMetaType("tSocketDescriptor"); + // Start listening + listen(); +} + + +HttpListener::~HttpListener() +{ + close(); + qDebug("HttpListener: destroyed"); +} + + +void HttpListener::listen() +{ + if (!pool) + { + pool=new HttpConnectionHandlerPool(settings,requestHandler); + } + QString host = settings->value("host").toString(); + quint16 port=settings->value("port").toUInt() & 0xFFFF; + QTcpServer::listen(host.isEmpty() ? QHostAddress::Any : QHostAddress(host), port); + if (!isListening()) + { + qCritical("HttpListener: Cannot bind on port %i: %s",port,qPrintable(errorString())); + } + else { + qDebug("HttpListener: Listening on port %i",port); + } +} + + +void HttpListener::close() { + QTcpServer::close(); + qDebug("HttpListener: closed"); + if (pool) { + delete pool; + pool=nullptr; + } +} + +void HttpListener::incomingConnection(tSocketDescriptor socketDescriptor) { +#ifdef SUPERVERBOSE + qDebug("HttpListener: New connection"); +#endif + + HttpConnectionHandler* freeHandler=nullptr; + if (pool) + { + freeHandler=pool->getConnectionHandler(); + } + + // Let the handler process the new connection. + if (freeHandler) + { + // The descriptor is passed via event queue because the handler lives in another thread + QMetaObject::invokeMethod(freeHandler, "handleConnection", Qt::QueuedConnection, Q_ARG(tSocketDescriptor, socketDescriptor)); + } + else + { + // Reject the connection + qDebug("HttpListener: Too many incoming connections"); + QTcpSocket* socket=new QTcpSocket(this); + socket->setSocketDescriptor(socketDescriptor); + connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); + socket->write("HTTP/1.1 503 too many connections\r\nConnection: close\r\n\r\nToo many connections\r\n"); + socket->disconnectFromHost(); + } +} diff --git a/QtWebApp/httpserver/httplistener.h b/QtWebApp/httpserver/httplistener.h new file mode 100755 index 0000000..9b11af3 --- /dev/null +++ b/QtWebApp/httpserver/httplistener.h @@ -0,0 +1,120 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPLISTENER_H +#define HTTPLISTENER_H + +#include +#include +#include +#include "httpglobal.h" +#include "httpconnectionhandler.h" +#include "httpconnectionhandlerpool.h" +#include "httprequesthandler.h" + +namespace stefanfrings { + +/** + Listens for incoming TCP connections and and passes all incoming HTTP requests to your implementation of HttpRequestHandler, + which processes the request and generates the response (usually a HTML document). +

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

+  ;host=192.168.0.100
+  port=8080
+
+  readTimeout=60000
+  maxRequestSize=16000
+  maxMultiPartSize=1000000
+
+  minThreads=1
+  maxThreads=10
+  cleanupInterval=1000
+
+  ;sslKeyFile=ssl/server.key
+  ;sslCertFile=ssl/server.crt
+  ;caCertFile=ssl/ca.crt
+  ;verifyPeer=false
+  
+ The optional host parameter binds the listener to a specific network interface, + otherwise the server accepts connections from any network interface on the given port. +

+ The readTimeout value defines the maximum time to wait for a complete HTTP request. +

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

+ After server start, the size of the thread pool is always 0. Threads + are started on demand when requests come in. The cleanup timer reduces + the number of idle threads slowly by closing one thread in each interval. + But the configured minimum number of threads are kept running. + @see HttpConnectionHandlerPool for description of the optional ssl settings +*/ + +class DECLSPEC HttpListener : public QTcpServer { + Q_OBJECT + Q_DISABLE_COPY(HttpListener) +public: + + /** + Constructor. + Creates a connection pool and starts listening on the configured host and port. + @param settings Configuration settings, 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 that is not used by other parts of the program. + The HttpListener does not take over ownership of the QSettings instance, so the + caller should destroy it during shutdown. + @param requestHandler Processes each received HTTP request, usually by dispatching to controller classes. + @param parent Parent object. + @warning Ensure to close or delete the listener before deleting the request handler. + */ + HttpListener(const QSettings* settings, HttpRequestHandler* requestHandler, QObject* parent=nullptr); + + /** Destructor */ + virtual ~HttpListener(); + + /** + Restart listeing after close(). + */ + void listen(); + + /** + Closes the listener, waits until all pending requests are processed, + then closes the connection pool. + */ + void close(); + +protected: + + /** Serves new incoming connection requests */ + void incomingConnection(tSocketDescriptor socketDescriptor); + +private: + + /** Configuration settings for the HTTP server */ + const QSettings* settings; + + /** Point to the reuqest handler which processes all HTTP requests */ + HttpRequestHandler* requestHandler; + + /** Pool of connection handlers */ + HttpConnectionHandlerPool* pool; + +signals: + + /** + Sent to the connection handler to process a new incoming connection. + @param socketDescriptor references the accepted connection. + */ + + void handleConnection(tSocketDescriptor socketDescriptor); + +}; + +} // end of namespace + +#endif // HTTPLISTENER_H diff --git a/QtWebApp/httpserver/httprequest.cpp b/QtWebApp/httpserver/httprequest.cpp new file mode 100755 index 0000000..c9294b7 --- /dev/null +++ b/QtWebApp/httpserver/httprequest.cpp @@ -0,0 +1,579 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequest.h" +#include +#include +#include "httpcookie.h" + +using namespace stefanfrings; + +HttpRequest::HttpRequest(const QSettings* settings) +{ + status=waitForRequest; + currentSize=0; + expectedBodySize=0; + maxSize=settings->value("maxRequestSize","16000").toInt(); + maxMultiPartSize=settings->value("maxMultiPartSize","1000000").toInt(); + tempFile=nullptr; +} + + +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 dataRead = socket->readLine(toRead); + currentSize += dataRead.size(); + lineBuffer.append(dataRead); + if (!lineBuffer.contains("\r\n")) + { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: collecting more parts until line break"); + #endif + return; + } + QByteArray newData=lineBuffer.trimmed(); + lineBuffer.clear(); + if (!newData.isEmpty()) + { + qDebug("HttpRequest: from %s: %s",qPrintable(socket->peerAddress().toString()),newData.data()); + QList list=newData.split(' '); + if (list.count()!=3 || !list.at(2).contains("HTTP")) + { + qWarning("HttpRequest: received broken HTTP request, invalid first line"); + status=abort; + } + else + { + method=list.at(0).trimmed(); + path=list.at(1); + version=list.at(2); + peerAddress = socket->peerAddress(); + status=waitForHeader; + } + } +} + +void HttpRequest::readHeader(QTcpSocket* socket) +{ + int toRead=maxSize-currentSize+1; // allow one byte more to be able to detect overflow + QByteArray dataRead = socket->readLine(toRead); + currentSize += dataRead.size(); + lineBuffer.append(dataRead); + if (!lineBuffer.contains("\r\n")) + { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: collecting more parts until line break"); + #endif + return; + } + QByteArray newData=lineBuffer.trimmed(); + lineBuffer.clear(); + int colon=newData.indexOf(':'); + if (colon>0) + { + // Received a line with a colon - a header + currentHeader=newData.left(colon).toLower(); + QByteArray value=newData.mid(colon+1).trimmed(); + headers.insert(currentHeader,value); + #ifdef SUPERVERBOSE + qDebug("HttpRequest: received header %s: %s",currentHeader.data(),value.data()); + #endif + } + else if (!newData.isEmpty()) + { + // received another line - belongs to the previous header + #ifdef SUPERVERBOSE + qDebug("HttpRequest: read additional line of header"); + #endif + // Received additional line of previous header + if (headers.contains(currentHeader)) { + headers.insert(currentHeader,headers.value(currentHeader)+" "+newData); + } + } + else + { + // received an empty line - end of headers reached + #ifdef SUPERVERBOSE + qDebug("HttpRequest: headers completed"); + #endif + // Empty line received, that means all headers have been received + // Check for multipart/form-data + QByteArray contentType=headers.value("content-type"); + if (contentType.startsWith("multipart/form-data")) + { + int posi=contentType.indexOf("boundary="); + if (posi>=0) { + boundary=contentType.mid(posi+9); + if (boundary.startsWith('"') && boundary.endsWith('"')) + { + boundary = boundary.mid(1,boundary.length()-2); + } + } + } + QByteArray contentLength=headers.value("content-length"); + if (!contentLength.isEmpty()) + { + expectedBodySize=contentLength.toInt(); + } + if (expectedBodySize==0) + { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: expect no body"); + #endif + status=complete; + } + else if (boundary.isEmpty() && expectedBodySize+currentSize>maxSize) + { + qWarning("HttpRequest: expected body is too large"); + status=abort; + } + else if (!boundary.isEmpty() && expectedBodySize>maxMultiPartSize) + { + qWarning("HttpRequest: expected multipart body is too large"); + status=abort; + } + else { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: expect %i bytes body",expectedBodySize); + #endif + status=waitForBody; + } + } +} + +void HttpRequest::readBody(QTcpSocket* socket) +{ + Q_ASSERT(expectedBodySize!=0); + if (boundary.isEmpty()) + { + // normal body, no multipart + #ifdef SUPERVERBOSE + qDebug("HttpRequest: receive body"); + #endif + int toRead=expectedBodySize-bodyData.size(); + QByteArray newData=socket->read(toRead); + currentSize+=newData.size(); + bodyData.append(newData); + if (bodyData.size()>=expectedBodySize) + { + status=complete; + } + } + else + { + // multipart body, store into temp file + #ifdef SUPERVERBOSE + qDebug("HttpRequest: receiving multipart body"); + #endif + // Create an object for the temporary file, if not already present + if (tempFile == nullptr) + { + tempFile = new QTemporaryFile; + } + if (!tempFile->isOpen()) + { + tempFile->open(); + } + // Transfer data in 64kb blocks + qint64 fileSize=tempFile->size(); + qint64 toRead=expectedBodySize-fileSize; + if (toRead>65536) + { + toRead=65536; + } + fileSize+=tempFile->write(socket->read(toRead)); + if (fileSize>=maxMultiPartSize) + { + qWarning("HttpRequest: received too many multipart bytes"); + status=abort; + } + else if (fileSize>=expectedBodySize) + { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: received whole multipart body"); + #endif + tempFile->flush(); + if (tempFile->error()) + { + qCritical("HttpRequest: Error writing temp file for multipart body"); + } + parseMultiPartFile(); + tempFile->close(); + status=complete; + } + } +} + +void HttpRequest::decodeRequestParams() +{ + #ifdef SUPERVERBOSE + qDebug("HttpRequest: extract and decode request parameters"); + #endif + // Get URL parameters + QByteArray rawParameters; + int questionMark=path.indexOf('?'); + if (questionMark>=0) + { + rawParameters=path.mid(questionMark+1); + path=path.left(questionMark); + } + // Get request body parameters + QByteArray contentType=headers.value("content-type"); + if (!bodyData.isEmpty() && (contentType.isEmpty() || contentType.startsWith("application/x-www-form-urlencoded"))) + { + if (!rawParameters.isEmpty()) + { + rawParameters.append('&'); + rawParameters.append(bodyData); + } + else + { + rawParameters=bodyData; + } + } + // Split the parameters into pairs of value and name + QList list=rawParameters.split('&'); + foreach (QByteArray part, list) + { + int equalsChar=part.indexOf('='); + if (equalsChar>=0) + { + QByteArray name=part.left(equalsChar).trimmed(); + QByteArray value=part.mid(equalsChar+1).trimmed(); + parameters.insert(urlDecode(name),urlDecode(value)); + } + else if (!part.isEmpty()) + { + // Name without value + parameters.insert(urlDecode(part),""); + } + } +} + +void HttpRequest::extractCookies() +{ + #ifdef SUPERVERBOSE + qDebug("HttpRequest: extract cookies"); + #endif + foreach(QByteArray cookieStr, headers.values("cookie")) + { + QList list=HttpCookie::splitCSV(cookieStr); + foreach(QByteArray part, list) + { + #ifdef SUPERVERBOSE + qDebug("HttpRequest: found cookie %s",part.data()); + #endif // Split the part into name and value + QByteArray name; + QByteArray value; + int posi=part.indexOf('='); + if (posi) + { + name=part.left(posi).trimmed(); + value=part.mid(posi+1).trimmed(); + } + else + { + name=part.trimmed(); + value=""; + } + cookies.insert(name,value); + } + } + headers.remove("cookie"); +} + +void HttpRequest::readFromSocket(QTcpSocket* socket) +{ + Q_ASSERT(status!=complete); + if (status==waitForRequest) + { + readRequest(socket); + } + else if (status==waitForHeader) + { + readHeader(socket); + } + else if (status==waitForBody) + { + readBody(socket); + } + if ((boundary.isEmpty() && currentSize>maxSize) || (!boundary.isEmpty() && currentSize>maxMultiPartSize)) + { + qWarning("HttpRequest: received too many bytes"); + status=abort; + } + if (status==complete) + { + // Extract and decode request parameters from url and body + decodeRequestParams(); + // Extract cookies from headers + extractCookies(); + } +} + + +HttpRequest::RequestStatus HttpRequest::getStatus() const +{ + return status; +} + + +QByteArray HttpRequest::getMethod() const +{ + return method; +} + + +QByteArray HttpRequest::getPath() const +{ + return urlDecode(path); +} + + +const QByteArray& HttpRequest::getRawPath() const +{ + return path; +} + + +QByteArray HttpRequest::getVersion() const +{ + return version; +} + + +QByteArray HttpRequest::getHeader(const QByteArray& name) const +{ + return headers.value(name.toLower()); +} + +QList HttpRequest::getHeaders(const QByteArray& name) const +{ + return headers.values(name.toLower()); +} + +QMultiMap HttpRequest::getHeaderMap() const +{ + return headers; +} + +QByteArray HttpRequest::getParameter(const QByteArray& name) const +{ + return parameters.value(name); +} + +QList HttpRequest::getParameters(const QByteArray& name) const +{ + return parameters.values(name); +} + +QMultiMap HttpRequest::getParameterMap() const +{ + return parameters; +} + +QByteArray HttpRequest::getBody() const +{ + return bodyData; +} + +QByteArray HttpRequest::urlDecode(const QByteArray source) +{ + QByteArray buffer(source); + buffer.replace('+',' '); + int percentChar=buffer.indexOf('%'); + while (percentChar>=0) + { + bool ok; + int hexCode=buffer.mid(percentChar+1,2).toInt(&ok,16); + if (ok) + { + char c=char(hexCode); + buffer.replace(percentChar,3,&c,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=nullptr; + 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 + if (uploadedFile) + { + #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); + long int fileSize=(long int) uploadedFile->size(); + qDebug("HttpRequest: uploaded file size is %li",fileSize); + } + else + { + qWarning("HttpRequest: format error, unexpected end of file data"); + } + } + if (line.contains(boundary+"--")) + { + finished=true; + } + break; + } + else + { + if (fileName.isEmpty() && !fieldName.isEmpty()) + { + // this is a form field. + currentSize+=line.size(); + fieldValue.append(line); + } + else if (!fileName.isEmpty() && !fieldName.isEmpty()) + { + // this is a file + if (!uploadedFile) + { + uploadedFile=new QTemporaryFile(); + uploadedFile->open(); + } + uploadedFile->write(line); + if (uploadedFile->error()) + { + qCritical("HttpRequest: error writing temp file, %s",qPrintable(uploadedFile->errorString())); + } + } + } + } + } + if (tempFile->error()) + { + qCritical("HttpRequest: cannot read temp file, %s",qPrintable(tempFile->errorString())); + } + #ifdef SUPERVERBOSE + qDebug("HttpRequest: finished parsing multipart temp file"); + #endif +} + +HttpRequest::~HttpRequest() +{ + foreach(QByteArray key, uploadedFiles.keys()) + { + QTemporaryFile* file=uploadedFiles.value(key); + if (file->isOpen()) + { + file->close(); + } + delete file; + } + if (tempFile != nullptr) + { + if (tempFile->isOpen()) + { + tempFile->close(); + } + delete tempFile; + } +} + +QTemporaryFile* HttpRequest::getUploadedFile(const QByteArray fieldName) const +{ + return uploadedFiles.value(fieldName); +} + +QByteArray HttpRequest::getCookie(const QByteArray& name) const +{ + return cookies.value(name); +} + +/** Get the map of cookies */ +QMap& HttpRequest::getCookieMap() +{ + return cookies; +} + +/** + Get the address of the connected client. + Note that multiple clients may have the same IP address, if they + share an internet connection (which is very common). + */ +QHostAddress HttpRequest::getPeerAddress() const +{ + return peerAddress; +} + diff --git a/QtWebApp/httpserver/httprequest.h b/QtWebApp/httpserver/httprequest.h new file mode 100755 index 0000000..f0da084 --- /dev/null +++ b/QtWebApp/httpserver/httprequest.h @@ -0,0 +1,238 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUEST_H +#define HTTPREQUEST_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include "httpglobal.h" + +namespace stefanfrings { + +/** + This object represents a single HTTP request. It reads the request + from a TCP socket and provides getters for the individual parts + of the request. +

+ The following 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. +*/ + +class DECLSPEC HttpRequest { + Q_DISABLE_COPY(HttpRequest) + friend class HttpSessionStore; + +public: + + /** Values for getStatus() */ + enum RequestStatus {waitForRequest, waitForHeader, waitForBody, complete, abort}; + + /** + Constructor. + @param settings Configuration settings + */ + HttpRequest(const QSettings* settings); + + /** + Destructor. + */ + virtual ~HttpRequest(); + + /** + Read the HTTP request from a socket. + This method is called by the connection handler repeatedly + until the status is RequestStatus::complete or RequestStatus::abort. + @param socket Source of the data + */ + void readFromSocket(QTcpSocket *socket); + + /** + Get the status of this reqeust. + @see RequestStatus + */ + RequestStatus getStatus() const; + + /** Get the method of the HTTP request (e.g. "GET") */ + QByteArray getMethod() const; + + /** Get the decoded path of the HTPP request (e.g. "/index.html") */ + QByteArray getPath() const; + + /** Get the raw path of the HTTP request (e.g. "/file%20with%20spaces.html") */ + const QByteArray& getRawPath() const; + + /** Get the version of the HTPP request (e.g. "HTTP/1.1") */ + QByteArray getVersion() const; + + /** + Get the value of a HTTP request header. + @param name Name of the header, not case-senitive. + @return If the header occurs multiple times, only the last + one is returned. + */ + QByteArray getHeader(const QByteArray& name) const; + + /** + Get the values of a HTTP request header. + @param name Name of the header, not case-senitive. + */ + QList getHeaders(const QByteArray& name) const; + + /** + * Get all HTTP request headers. Note that the header names + * are returned in lower-case. + */ + QMultiMap getHeaderMap() const; + + /** + Get the value of a HTTP request parameter. + @param name Name of the parameter, case-sensitive. + @return If the parameter occurs multiple times, only the last + one is returned. + */ + QByteArray getParameter(const QByteArray& name) const; + + /** + Get the values of a HTTP request parameter. + @param name Name of the parameter, case-sensitive. + */ + QList getParameters(const QByteArray& name) const; + + /** Get all HTTP request parameters. */ + QMultiMap getParameterMap() const; + + /** Get the HTTP request body. */ + QByteArray getBody() const; + + /** + Decode an URL parameter. + E.g. replace "%23" by '#' and replace '+' by ' '. + @param source The url encoded strings + @see QUrl::toPercentEncoding for the reverse direction + */ + static QByteArray urlDecode(const QByteArray source); + + /** + Get an uploaded file. The file is already open. It will + be closed and deleted by the destructor of this HttpRequest + object (after processing the request). +

+ For uploaded files, the method getParameters() returns + the original fileName as provided by the calling web browser. + */ + QTemporaryFile* getUploadedFile(const QByteArray fieldName) const; + + /** + Get the value of a cookie. + @param name Name of the cookie + */ + QByteArray getCookie(const QByteArray& name) const; + + /** Get all cookies. */ + QMap& getCookieMap(); + + /** + Get the address of the connected client. + Note that multiple clients may have the same IP address, if they + share an internet connection (which is very common). + */ + QHostAddress getPeerAddress() const; + +private: + + /** Request headers */ + QMultiMap headers; + + /** Parameters of the request */ + QMultiMap parameters; + + /** Uploaded files of the request, key is the field name. */ + QMap uploadedFiles; + + /** Received cookies */ + QMap cookies; + + /** Storage for raw body data */ + QByteArray bodyData; + + /** Request method */ + QByteArray method; + + /** Request path (in raw encoded format) */ + QByteArray path; + + /** Request protocol version */ + QByteArray version; + + /** + Status of this request. For the state engine. + @see RequestStatus + */ + RequestStatus status; + + /** Address of the connected peer. */ + QHostAddress peerAddress; + + /** Maximum size of requests in bytes. */ + int maxSize; + + /** Maximum allowed size of multipart forms in bytes. */ + int maxMultiPartSize; + + /** Current size */ + int currentSize; + + /** Expected size of body */ + int expectedBodySize; + + /** Name of the current header, or empty if no header is being processed */ + QByteArray currentHeader; + + /** Boundary of multipart/form-data body. Empty if there is no such header */ + QByteArray boundary; + + /** Temp file, that is used to store the multipart/form-data body */ + QTemporaryFile* tempFile; + + /** Parse the multipart body, that has been stored in the temp file. */ + void parseMultiPartFile(); + + /** Sub-procedure of readFromSocket(), read the first line of a request. */ + void readRequest(QTcpSocket* socket); + + /** Sub-procedure of readFromSocket(), read header lines. */ + void readHeader(QTcpSocket* socket); + + /** Sub-procedure of readFromSocket(), read the request body. */ + void readBody(QTcpSocket* socket); + + /** Sub-procedure of readFromSocket(), extract and decode request parameters. */ + void decodeRequestParams(); + + /** Sub-procedure of readFromSocket(), extract cookies from headers */ + void extractCookies(); + + /** Buffer for collecting characters of request and header lines */ + QByteArray lineBuffer; + +}; + +} // end of namespace + +#endif // HTTPREQUEST_H diff --git a/QtWebApp/httpserver/httprequesthandler.cpp b/QtWebApp/httpserver/httprequesthandler.cpp new file mode 100755 index 0000000..f3a5fbe --- /dev/null +++ b/QtWebApp/httpserver/httprequesthandler.cpp @@ -0,0 +1,23 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httprequesthandler.h" + +using namespace stefanfrings; + +HttpRequestHandler::HttpRequestHandler(QObject* parent) + : QObject(parent) +{} + +HttpRequestHandler::~HttpRequestHandler() +{} + +void HttpRequestHandler::service(HttpRequest& request, HttpResponse& response) +{ + qCritical("HttpRequestHandler: you need to override the service() function"); + qDebug("HttpRequestHandler: request=%s %s %s",request.getMethod().data(),request.getPath().data(),request.getVersion().data()); + response.setStatus(501,"not implemented"); + response.write("501 not implemented",true); +} diff --git a/QtWebApp/httpserver/httprequesthandler.h b/QtWebApp/httpserver/httprequesthandler.h new file mode 100755 index 0000000..ea24612 --- /dev/null +++ b/QtWebApp/httpserver/httprequesthandler.h @@ -0,0 +1,53 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPREQUESTHANDLER_H +#define HTTPREQUESTHANDLER_H + +#include "httpglobal.h" +#include "httprequest.h" +#include "httpresponse.h" + +namespace stefanfrings { + +/** + The request handler generates a response for each HTTP request. Web Applications + usually have one central request handler that maps incoming requests to several + controllers (servlets) based on the requested path. +

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

+ @warning Be aware that the main request handler instance must be created on the heap and + that it is used by multiple threads simultaneously. + @see StaticFileController which delivers static local files. +*/ + +class DECLSPEC HttpRequestHandler : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpRequestHandler) +public: + + /** + * Constructor. + * @param parent Parent object. + */ + HttpRequestHandler(QObject* parent=nullptr); + + /** Destructor */ + virtual ~HttpRequestHandler(); + + /** + Generate a response for an incoming HTTP request. + @param request The received HTTP request + @param response Must be used to return the response + @warning This method must be thread safe + */ + virtual void service(HttpRequest& request, HttpResponse& response); + +}; + +} // end of namespace + +#endif // HTTPREQUESTHANDLER_H diff --git a/QtWebApp/httpserver/httpresponse.cpp b/QtWebApp/httpserver/httpresponse.cpp new file mode 100755 index 0000000..a2afba8 --- /dev/null +++ b/QtWebApp/httpserver/httpresponse.cpp @@ -0,0 +1,201 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpresponse.h" + +using namespace stefanfrings; + +HttpResponse::HttpResponse(QTcpSocket *socket) +{ + this->socket=socket; + statusCode=200; + statusText="OK"; + sentHeaders=false; + sentLastPart=false; + chunkedMode=false; +} + +void HttpResponse::setHeader(QByteArray name, QByteArray value) +{ + Q_ASSERT(sentHeaders==false); + headers.insert(name,value); +} + +void HttpResponse::setHeader(QByteArray name, int value) +{ + Q_ASSERT(sentHeaders==false); + headers.insert(name,QByteArray::number(value)); +} + +QMap& HttpResponse::getHeaders() +{ + return headers; +} + +void HttpResponse::setStatus(int statusCode, QByteArray description) +{ + this->statusCode=statusCode; + statusText=description; +} + +int HttpResponse::getStatusCode() const +{ + return this->statusCode; +} + +void HttpResponse::writeHeaders() +{ + Q_ASSERT(sentHeaders==false); + QByteArray buffer; + buffer.append("HTTP/1.1 "); + buffer.append(QByteArray::number(statusCode)); + buffer.append(' '); + buffer.append(statusText); + buffer.append("\r\n"); + foreach(QByteArray name, headers.keys()) + { + buffer.append(name); + buffer.append(": "); + buffer.append(headers.value(name)); + buffer.append("\r\n"); + } + foreach(HttpCookie cookie,cookies.values()) + { + buffer.append("Set-Cookie: "); + buffer.append(cookie.toByteArray()); + buffer.append("\r\n"); + } + buffer.append("\r\n"); + writeToSocket(buffer); + socket->flush(); + sentHeaders=true; +} + +bool HttpResponse::writeToSocket(QByteArray data) +{ + int remaining=data.size(); + char* ptr=data.data(); + while (socket->isOpen() && remaining>0) + { + // If the output buffer has become large, then wait until it has been sent. + if (socket->bytesToWrite()>16384) + { + socket->waitForBytesWritten(-1); + } + + qint64 written=socket->write(ptr,remaining); + if (written==-1) + { + return false; + } + ptr+=written; + remaining-=written; + } + return true; +} + +void HttpResponse::write(QByteArray data, bool lastPart) +{ + Q_ASSERT(sentLastPart==false); + + // Send HTTP headers, if not already done (that happens only on the first call to write()) + if (sentHeaders==false) + { + // If the whole response is generated with a single call to write(), then we know the total + // size of the response and therefore can set the Content-Length header automatically. + if (lastPart) + { + // Automatically set the Content-Length header + headers.insert("Content-Length",QByteArray::number(data.size())); + } + + // else if we will not close the connection at the end, them we must use the chunked mode. + else + { + QByteArray connectionValue=headers.value("Connection",headers.value("connection")); + bool connectionClose=QString::compare(connectionValue,"close",Qt::CaseInsensitive)==0; + if (!connectionClose) + { + headers.insert("Transfer-Encoding","chunked"); + chunkedMode=true; + } + } + + writeHeaders(); + } + + // Send data + if (data.size()>0) + { + if (chunkedMode) + { + if (data.size()>0) + { + QByteArray size=QByteArray::number(data.size(),16); + writeToSocket(size); + writeToSocket("\r\n"); + writeToSocket(data); + writeToSocket("\r\n"); + } + } + else + { + writeToSocket(data); + } + } + + // Only for the last chunk, send the terminating marker and flush the buffer. + if (lastPart) + { + if (chunkedMode) + { + writeToSocket("0\r\n\r\n"); + } + socket->flush(); + sentLastPart=true; + } +} + + +bool HttpResponse::hasSentLastPart() const +{ + return sentLastPart; +} + + +void HttpResponse::setCookie(const HttpCookie& cookie) +{ + Q_ASSERT(sentHeaders==false); + if (!cookie.getName().isEmpty()) + { + cookies.insert(cookie.getName(),cookie); + } +} + + +QMap& HttpResponse::getCookies() +{ + return cookies; +} + + +void HttpResponse::redirect(const QByteArray& url) +{ + setStatus(303,"See Other"); + setHeader("Location",url); + write("Redirect",true); +} + + +void HttpResponse::flush() +{ + socket->flush(); +} + + +bool HttpResponse::isConnected() const +{ + return socket->isOpen(); +} diff --git a/QtWebApp/httpserver/httpresponse.h b/QtWebApp/httpserver/httpresponse.h new file mode 100755 index 0000000..62050d0 --- /dev/null +++ b/QtWebApp/httpserver/httpresponse.h @@ -0,0 +1,163 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPRESPONSE_H +#define HTTPRESPONSE_H + +#include +#include +#include +#include "httpglobal.h" +#include "httpcookie.h" + +namespace stefanfrings { + +/** + This object represents a HTTP response, used to return something to the web client. +

+

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

+ Example how to return an error: +

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

+ In case of large responses (e.g. file downloads), a Content-Length header should be set + before calling write(). Web Browsers use that information to display a progress bar. +*/ + +class DECLSPEC HttpResponse { + Q_DISABLE_COPY(HttpResponse) +public: + + /** + Constructor. + @param socket used to write the response + */ + HttpResponse(QTcpSocket *socket); + + /** + Set a HTTP response header. + You must call this method before the first write(). + @param name name of the header + @param value value of the header + */ + void setHeader(const QByteArray name, const QByteArray value); + + /** + Set a HTTP response header. + You must call this method before the first write(). + @param name name of the header + @param value value of the header + */ + void setHeader(const QByteArray name, const int value); + + /** Get the map of HTTP response headers */ + QMap& getHeaders(); + + /** Get the map of cookies */ + QMap& getCookies(); + + /** + Set status code and description. The default is 200,OK. + You must call this method before the first write(). + */ + void setStatus(const int statusCode, const QByteArray description=QByteArray()); + + /** Return the status code. */ + int getStatusCode() const; + + /** + Write body data to the socket. +

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

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

+ Chunked mode is automatically selected if there is no Content-Length header + and also no Connection:close header. + @param data Data bytes of the body + @param lastPart Indicates that this is the last chunk of data and flushes the output buffer. + */ + void write(const QByteArray data, const bool lastPart=false); + + /** + Indicates whether the body has been sent completely (write() has been called with lastPart=true). + */ + bool hasSentLastPart() const; + + /** + Set a cookie. + You must call this method before the first write(). + */ + void setCookie(const HttpCookie& cookie); + + /** + Send a redirect response to the browser. + Cannot be combined with write(). + @param url Destination URL + */ + void redirect(const QByteArray& url); + + /** + * Flush the output buffer (of the underlying socket). + * You normally don't need to call this method because flush is + * automatically called after HttpRequestHandler::service() returns. + */ + void flush(); + + /** + * May be used to check whether the connection to the web client has been lost. + * This might be useful to cancel the generation of large or slow responses. + */ + bool isConnected() const; + +private: + + /** Request headers */ + QMap headers; + + /** Socket for writing output */ + QTcpSocket* socket; + + /** HTTP status code*/ + int statusCode; + + /** HTTP status code description */ + QByteArray statusText; + + /** Indicator whether headers have been sent */ + bool sentHeaders; + + /** Indicator whether the body has been sent completely */ + bool sentLastPart; + + /** Whether the response is sent in chunked mode */ + bool chunkedMode; + + /** Cookies */ + QMap cookies; + + /** Write raw data to the socket. This method blocks until all bytes have been passed to the TCP buffer */ + bool writeToSocket(QByteArray data); + + /** + Write the response HTTP status and headers to the socket. + Calling this method is optional, because writeBody() calls + it automatically when required. + */ + void writeHeaders(); + +}; + +} // end of namespace + +#endif // HTTPRESPONSE_H diff --git a/QtWebApp/httpserver/httpsession.cpp b/QtWebApp/httpserver/httpsession.cpp new file mode 100755 index 0000000..4337082 --- /dev/null +++ b/QtWebApp/httpserver/httpsession.cpp @@ -0,0 +1,188 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsession.h" +#include +#include + +using namespace stefanfrings; + +HttpSession::HttpSession(bool canStore) +{ + if (canStore) + { + dataPtr=new HttpSessionData(); + dataPtr->refCount=1; + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->id=QUuid::createUuid().toString().toLocal8Bit(); +#ifdef SUPERVERBOSE + qDebug("HttpSession: (constructor) new session %s with refCount=1",dataPtr->id.constData()); +#endif + } + else + { + dataPtr=nullptr; + } +} + +HttpSession::HttpSession(const HttpSession& other) +{ + dataPtr=other.dataPtr; + if (dataPtr) + { + dataPtr->lock.lockForWrite(); + dataPtr->refCount++; +#ifdef SUPERVERBOSE + qDebug("HttpSession: (constructor) copy session %s refCount=%i",dataPtr->id.constData(),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: (operator=) session %s refCount=%i",dataPtr->id.constData(),dataPtr->refCount); +#endif + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->lock.unlock(); + } + if (oldPtr) + { + int refCount; + oldPtr->lock.lockForWrite(); + refCount=--oldPtr->refCount; +#ifdef SUPERVERBOSE + qDebug("HttpSession: (operator=) session %s refCount=%i",oldPtr->id.constData(),oldPtr->refCount); +#endif + oldPtr->lock.unlock(); + if (refCount==0) + { + qDebug("HttpSession: deleting old data"); + delete oldPtr; + } + } + return *this; +} + +HttpSession::~HttpSession() +{ + if (dataPtr) { + int refCount; + dataPtr->lock.lockForWrite(); + refCount=--dataPtr->refCount; +#ifdef SUPERVERBOSE + qDebug("HttpSession: (destructor) session %s refCount=%i",dataPtr->id.constData(),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==nullptr; +} + +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.lockForWrite(); + dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch(); + dataPtr->lock.unlock(); + } +} diff --git a/QtWebApp/httpserver/httpsession.h b/QtWebApp/httpserver/httpsession.h new file mode 100755 index 0000000..e9d40b0 --- /dev/null +++ b/QtWebApp/httpserver/httpsession.h @@ -0,0 +1,121 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSION_H +#define HTTPSESSION_H + +#include +#include +#include +#include "httpglobal.h" + +namespace stefanfrings { + +/** + This class stores data for a single HTTP session. + A session can store any number of key/value pairs. This class uses implicit + sharing for read and write access. This class is thread safe. + @see HttpSessionStore should be used to create and get instances of this class. +*/ + +class DECLSPEC HttpSession { + +public: + + /** + Constructor. + @param canStore The session can store data, if this parameter is true. + Otherwise all calls to set() and remove() do not have any effect. + */ + HttpSession(const bool canStore=false); + + /** + Copy constructor. Creates another HttpSession object that shares the + data of the other object. + */ + HttpSession(const HttpSession& other); + + /** + Copy operator. Detaches from the current shared data and attaches to + the data of the other object. + */ + HttpSession& operator= (const HttpSession& other); + + /** + Destructor. Detaches from the shared data. + */ + virtual ~HttpSession(); + + /** Get the unique ID of this session. This method is thread safe. */ + QByteArray getId() const; + + /** + Null sessions cannot store data. All calls to set() and remove() + do not have any effect. This method is thread safe. + */ + bool isNull() const; + + /** Set a value. This method is thread safe. */ + void set(const QByteArray& key, const QVariant& value); + + /** Remove a value. This method is thread safe. */ + void remove(const QByteArray& key); + + /** Get a value. This method is thread safe. */ + QVariant get(const QByteArray& key) const; + + /** Check if a key exists. This method is thread safe. */ + bool contains(const QByteArray& key) const; + + /** + Get a copy of all data stored in this session. + Changes to the session do not affect the copy and vice versa. + This method is thread safe. + */ + QMap getAll() const; + + /** + Get the timestamp of last access. That is the time when the last + HttpSessionStore::getSession() has been called. + This method is thread safe. + */ + qint64 getLastAccess() const; + + /** + Set the timestamp of last access, to renew the timeout period. + Called by HttpSessionStore::getSession(). + This method is thread safe. + */ + void setLastAccess(); + +private: + + struct HttpSessionData { + + /** Unique ID */ + QByteArray id; + + /** Timestamp of last access, set by the HttpSessionStore */ + qint64 lastAccess; + + /** Reference counter */ + int refCount; + + /** Used to synchronize threads */ + QReadWriteLock lock; + + /** Storage for the key/value pairs; */ + QMap values; + + }; + + /** Pointer to the shared data. */ + HttpSessionData* dataPtr; + +}; + +} // end of namespace + +#endif // HTTPSESSION_H diff --git a/QtWebApp/httpserver/httpsessionstore.cpp b/QtWebApp/httpserver/httpsessionstore.cpp new file mode 100755 index 0000000..123dea8 --- /dev/null +++ b/QtWebApp/httpserver/httpsessionstore.cpp @@ -0,0 +1,131 @@ +/** + @file + @author Stefan Frings +*/ + +#include "httpsessionstore.h" +#include +#include + +using namespace stefanfrings; + +HttpSessionStore::HttpSessionStore(const QSettings *settings, QObject* parent) + :QObject(parent) +{ + this->settings=settings; + connect(&cleanupTimer,SIGNAL(timeout()),this,SLOT(sessionTimerEvent())); + cleanupTimer.start(60000); + cookieName=settings->value("cookieName","sessionid").toByteArray(); + expirationTime=settings->value("expirationTime",3600000).toInt(); + qDebug("HttpSessionStore: Sessions expire after %i milliseconds",expirationTime); +} + +HttpSessionStore::~HttpSessionStore() +{ + cleanupTimer.stop(); +} + +QByteArray HttpSessionStore::getSessionId(HttpRequest& request, HttpResponse& response) +{ + // The session ID in the response has priority because this one will be used in the next request. + mutex.lock(); + // Get the session ID from the response cookie + QByteArray sessionId=response.getCookies().value(cookieName).getValue(); + if (sessionId.isEmpty()) + { + // Get the session ID from the request cookie + sessionId=request.getCookie(cookieName); + } + // Clear the session ID if there is no such session in the storage. + if (!sessionId.isEmpty()) + { + if (!sessions.contains(sessionId)) + { + qDebug("HttpSessionStore: received invalid session cookie with ID %s",sessionId.data()); + sessionId.clear(); + } + } + mutex.unlock(); + return sessionId; +} + +HttpSession HttpSessionStore::getSession(HttpRequest& request, HttpResponse& response, bool allowCreate) +{ + QByteArray sessionId=getSessionId(request,response); + mutex.lock(); + if (!sessionId.isEmpty()) + { + HttpSession session=sessions.value(sessionId); + if (!session.isNull()) + { + mutex.unlock(); + // Refresh the session cookie + QByteArray cookieName=settings->value("cookieName","sessionid").toByteArray(); + QByteArray cookiePath=settings->value("cookiePath").toByteArray(); + QByteArray cookieComment=settings->value("cookieComment").toByteArray(); + QByteArray cookieDomain=settings->value("cookieDomain").toByteArray(); + response.setCookie(HttpCookie(cookieName,session.getId(),expirationTime/1000, + cookiePath,cookieComment,cookieDomain,false,false,"Lax")); + 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,false,false,"Lax")); + mutex.unlock(); + return session; + } + // Return a null session + mutex.unlock(); + return HttpSession(); +} + +HttpSession HttpSessionStore::getSession(const QByteArray id) +{ + mutex.lock(); + HttpSession session=sessions.value(id); + mutex.unlock(); + session.setLastAccess(); + return session; +} + +void HttpSessionStore::sessionTimerEvent() +{ + mutex.lock(); + qint64 now=QDateTime::currentMSecsSinceEpoch(); + QMap::iterator i = sessions.begin(); + while (i != sessions.end()) + { + QMap::iterator prev = i; + ++i; + HttpSession session=prev.value(); + qint64 lastAccess=session.getLastAccess(); + if (now-lastAccess>expirationTime) + { + qDebug("HttpSessionStore: session %s expired",session.getId().data()); + emit sessionDeleted(session.getId()); + sessions.erase(prev); + } + } + mutex.unlock(); +} + + +/** Delete a session */ +void HttpSessionStore::removeSession(HttpSession session) +{ + mutex.lock(); + emit sessionDeleted(session.getId()); + sessions.remove(session.getId()); + mutex.unlock(); +} diff --git a/QtWebApp/httpserver/httpsessionstore.h b/QtWebApp/httpserver/httpsessionstore.h new file mode 100755 index 0000000..65fa2c9 --- /dev/null +++ b/QtWebApp/httpserver/httpsessionstore.h @@ -0,0 +1,127 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef HTTPSESSIONSTORE_H +#define HTTPSESSIONSTORE_H + +#include +#include +#include +#include +#include "httpglobal.h" +#include "httpsession.h" +#include "httpresponse.h" +#include "httprequest.h" + +namespace stefanfrings { + +/** + Stores HTTP sessions and deletes them when they have expired. + The following configuration settings are required in the config file: +

+  expirationTime=3600000
+  cookieName=sessionid
+  
+ The following additional configurations settings are optionally: +
+  cookiePath=/
+  cookieComment=Session ID
+  ;cookieDomain=stefanfrings.de
+  
+*/ + +class DECLSPEC HttpSessionStore : public QObject { + Q_OBJECT + Q_DISABLE_COPY(HttpSessionStore) +public: + + /** + Constructor. + @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 that is not used by other parts of the program. + The HttpSessionStore does not take over ownership of the QSettings instance, so the + caller should destroy it during shutdown. + @param parent Parent object + */ + HttpSessionStore(const QSettings* settings, QObject* parent=nullptr); + + /** 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, const 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(const HttpSession session); + +protected: + /** Storage for the sessions */ + QMap sessions; + +private: + + /** Configuration settings */ + const QSettings* settings; + + /** Timer to remove expired sessions */ + QTimer cleanupTimer; + + /** Name of the session cookie */ + QByteArray cookieName; + + /** Time when sessions expire (in ms)*/ + int expirationTime; + + /** Used to synchronize threads */ + QMutex mutex; + +private slots: + + /** Called every minute to cleanup expired sessions. */ + void sessionTimerEvent(); + +signals: + + /** + Emitted when the session is deleted. + @param sessionId The ID number of the session. + */ + void sessionDeleted(const QByteArray& sessionId); +}; + +} // end of namespace + +#endif // HTTPSESSIONSTORE_H diff --git a/QtWebApp/httpserver/staticfilecontroller.cpp b/QtWebApp/httpserver/staticfilecontroller.cpp new file mode 100755 index 0000000..ac25925 --- /dev/null +++ b/QtWebApp/httpserver/staticfilecontroller.cpp @@ -0,0 +1,197 @@ +/** + @file + @author Stefan Frings +*/ + +#include "staticfilecontroller.h" +#include +#include +#include + +using namespace stefanfrings; + +StaticFileController::StaticFileController(const QSettings *settings, QObject* parent) + :HttpRequestHandler(parent) +{ + maxAge=settings->value("maxAge","60000").toInt(); + encoding=settings->value("encoding","UTF-8").toString(); + docroot=settings->value("path",".").toString(); + if(!(docroot.startsWith(":/") || docroot.startsWith("qrc://"))) + { + // Convert relative path to absolute, based on the directory of the config file. + #ifdef Q_OS_WIN32 + if (QDir::isRelativePath(docroot) && settings->format()!=QSettings::NativeFormat) + #else + if (QDir::isRelativePath(docroot)) + #endif + { + QFileInfo configFile(settings->fileName()); + docroot=QFileInfo(configFile.absolutePath(),docroot).absoluteFilePath(); + } + } + qDebug("StaticFileController: docroot=%s, encoding=%s, maxAge=%i",qPrintable(docroot),qPrintable(encoding),maxAge); + maxCachedFileSize=settings->value("maxCachedFileSize","65536").toInt(); + cache.setMaxCost(settings->value("cacheSize","1000000").toInt()); + cacheTimeout=settings->value("cacheTime","60000").toInt(); + long int cacheMaxCost=(long int)cache.maxCost(); + qDebug("StaticFileController: cache timeout=%i, size=%li",cacheTimeout,cacheMaxCost); +} + + +void StaticFileController::service(HttpRequest &request, HttpResponse &response) +{ + QByteArray path=request.getPath(); + // Check if we have the file in cache + qint64 now=QDateTime::currentMSecsSinceEpoch(); + mutex.lock(); + CacheEntry* entry=cache.object(path); + if (entry && (cacheTimeout==0 || entry->created>now-cacheTimeout)) + { + QByteArray document=entry->document; //copy the cached document, because other threads may destroy the cached entry immediately after mutex unlock. + QByteArray filename=entry->filename; + mutex.unlock(); + qDebug("StaticFileController: Cache hit for %s",path.data()); + setContentType(filename,response); + response.setHeader("Cache-Control","max-age="+QByteArray::number(maxAge/1000)); + response.write(document,true); + } + else + { + mutex.unlock(); + // The file is not in cache. + qDebug("StaticFileController: Cache miss for %s",path.data()); + // Forbid access to files outside the docroot directory + if (path.contains("/..")) + { + qWarning("StaticFileController: detected forbidden characters in path %s",path.data()); + response.setStatus(403,"forbidden"); + response.write("403 forbidden",true); + return; + } + // If the filename is a directory, append index.html. + if (QFileInfo(docroot+path).isDir()) + { + path+="/index.html"; + } + // Try to open the file + QFile file(docroot+path); + qDebug("StaticFileController: Open file %s",qPrintable(file.fileName())); + if (file.open(QIODevice::ReadOnly)) + { + setContentType(path,response); + response.setHeader("Cache-Control","max-age="+QByteArray::number(maxAge/1000)); + response.setHeader("Content-Length",QByteArray::number(file.size())); + if (file.size()<=maxCachedFileSize) + { + // Return the file content and store it also in the cache + entry=new CacheEntry(); + while (!file.atEnd() && !file.error()) + { + QByteArray buffer=file.read(65536); + response.write(buffer); + entry->document.append(buffer); + } + entry->created=now; + entry->filename=path; + mutex.lock(); + cache.insert(request.getPath(),entry,entry->document.size()); + mutex.unlock(); + } + else + { + // Return the file content, do not store in cache + while (!file.atEnd() && !file.error()) + { + response.write(file.read(65536)); + } + } + file.close(); + } + else { + if (file.exists()) + { + qWarning("StaticFileController: Cannot open existing file %s for reading",qPrintable(file.fileName())); + response.setStatus(403,"forbidden"); + response.write("403 forbidden",true); + } + else + { + response.setStatus(404,"not found"); + response.write("404 not found",true); + } + } + } +} + +void StaticFileController::setContentType(const QString fileName, HttpResponse &response) const +{ + if (fileName.endsWith(".png")) + { + response.setHeader("Content-Type", "image/png"); + } + else if (fileName.endsWith(".jpg")) + { + response.setHeader("Content-Type", "image/jpeg"); + } + else if (fileName.endsWith(".gif")) + { + response.setHeader("Content-Type", "image/gif"); + } + else if (fileName.endsWith(".pdf")) + { + response.setHeader("Content-Type", "application/pdf"); + } + else if (fileName.endsWith(".txt")) + { + response.setHeader("Content-Type", qPrintable("text/plain; charset="+encoding)); + } + else if (fileName.endsWith(".html") || fileName.endsWith(".htm")) + { + response.setHeader("Content-Type", qPrintable("text/html; charset="+encoding)); + } + else if (fileName.endsWith(".css")) + { + response.setHeader("Content-Type", "text/css"); + } + else if (fileName.endsWith(".js")) + { + response.setHeader("Content-Type", "text/javascript"); + } + else if (fileName.endsWith(".svg")) + { + response.setHeader("Content-Type", "image/svg+xml"); + } + else if (fileName.endsWith(".woff")) + { + response.setHeader("Content-Type", "font/woff"); + } + else if (fileName.endsWith(".woff2")) + { + response.setHeader("Content-Type", "font/woff2"); + } + else if (fileName.endsWith(".ttf")) + { + response.setHeader("Content-Type", "application/x-font-ttf"); + } + else if (fileName.endsWith(".eot")) + { + response.setHeader("Content-Type", "application/vnd.ms-fontobject"); + } + else if (fileName.endsWith(".otf")) + { + response.setHeader("Content-Type", "application/font-otf"); + } + else if (fileName.endsWith(".json")) + { + response.setHeader("Content-Type", "application/json"); + } + else if (fileName.endsWith(".xml")) + { + response.setHeader("Content-Type", "text/xml"); + } + // Todo: add all of your content types + else + { + qDebug("StaticFileController: unknown MIME type for filename '%s'", qPrintable(fileName)); + } +} diff --git a/QtWebApp/httpserver/staticfilecontroller.h b/QtWebApp/httpserver/staticfilecontroller.h new file mode 100755 index 0000000..8e61a9e --- /dev/null +++ b/QtWebApp/httpserver/staticfilecontroller.h @@ -0,0 +1,100 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef STATICFILECONTROLLER_H +#define STATICFILECONTROLLER_H + +#include +#include +#include "httpglobal.h" +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +namespace stefanfrings { + +/** + Delivers static files. It is usually called by the applications main request handler when + the caller requests a path that is mapped to static files. +

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

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

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

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

+ Do not instantiate this class in each request, because this would make the file cache + useless. Better create one instance during start-up and call it when the application + received a related HTTP request. +*/ + +class DECLSPEC StaticFileController : public HttpRequestHandler { + Q_OBJECT + Q_DISABLE_COPY(StaticFileController) +public: + + /** + Constructor. + @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 that is not used by other parts of the program. + The StaticFileController does not take over ownership of the QSettings instance, so the + caller should destroy it during shutdown. + @param parent Parent object + */ + StaticFileController(const QSettings* settings, QObject* parent = nullptr); + + /** Generates the response */ + void service(HttpRequest& request, HttpResponse& response); + +private: + + /** Encoding of text files */ + QString encoding; + + /** Root directory of documents */ + QString docroot; + + /** Maximum age of files in the browser cache */ + int maxAge; + + struct CacheEntry { + QByteArray document; + qint64 created; + QByteArray filename; + }; + + /** Timeout for each cached file */ + int cacheTimeout; + + /** Maximum size of files in cache, larger files are not cached */ + int maxCachedFileSize; + + /** Cache storage */ + QCache cache; + + /** Used to synchronize cache access for threads */ + QMutex mutex; + + /** Set a content-type header in the response depending on the ending of the filename */ + void setContentType(const QString file, HttpResponse &response) const; +}; + +} // end of namespace + +#endif // STATICFILECONTROLLER_H diff --git a/QtWebApp/lgpl-3.0.txt b/QtWebApp/lgpl-3.0.txt new file mode 100644 index 0000000..0a04128 --- /dev/null +++ b/QtWebApp/lgpl-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/QtWebApp/logging/dualfilelogger.cpp b/QtWebApp/logging/dualfilelogger.cpp new file mode 100755 index 0000000..8d212e6 --- /dev/null +++ b/QtWebApp/logging/dualfilelogger.cpp @@ -0,0 +1,27 @@ +/** + @file + @author Stefan Frings +*/ + +#include "dualfilelogger.h" + +using namespace stefanfrings; + +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, const QString &file, const QString &function, const int line) +{ + firstLogger->log(type,message,file,function,line); + secondLogger->log(type,message,file,function,line); +} + +void DualFileLogger::clear(const bool buffer, const bool variables) +{ + firstLogger->clear(buffer,variables); + secondLogger->clear(buffer,variables); +} diff --git a/QtWebApp/logging/dualfilelogger.h b/QtWebApp/logging/dualfilelogger.h new file mode 100755 index 0000000..b2518a4 --- /dev/null +++ b/QtWebApp/logging/dualfilelogger.h @@ -0,0 +1,82 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef DUALFILELOGGER_H +#define DUALFILELOGGER_H + +#include +#include +#include +#include "logglobal.h" +#include "logger.h" +#include "filelogger.h" + +namespace stefanfrings { + +/** + Writes log messages into two log files simultaneously. + I recommend to configure: + - The primary logfile with minLevel=INFO or WARNING and bufferSize=0. This file is for the operator to see when a problem occured. + - The secondary logfile with minLevel=WARNING or ERROR and bufferSize=100. This file is for the developer who may need more details (the debug messages) about the + situation that leaded to the error. + + @see FileLogger for a description of the two underlying loggers. +*/ + +class DECLSPEC DualFileLogger : public Logger { + Q_OBJECT + Q_DISABLE_COPY(DualFileLogger) +public: + + /** + Constructor. + @param firstSettings Configuration settings for the primary FileLogger instance, 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 that is not used by other parts of the program. + The FileLogger does not take over ownership of the QSettings instance, so the caller + should destroy it during shutdown. + @param secondSettings Same as firstSettings, but for the secondary FileLogger instance. + @param refreshInterval Interval of checking for changed config settings in msec, or 0=disabled + @param parent Parent object. + */ + DualFileLogger(QSettings* firstSettings, QSettings* secondSettings, + const int refreshInterval=10000, QObject *parent = nullptr); + + /** + Decorate and log the message, if type>=minLevel. + This method is thread safe. + @param type Message type (level) + @param message Message text + @param file Name of the source file where the message was generated (usually filled with the macro __FILE__) + @param function Name of the function where the message was generated (usually filled with the macro __LINE__) + @param line Line Number of the source file, where the message was generated (usually filles with the macro __func__ or __FUNCTION__) + @see LogMessage for a description of the message decoration. + */ + virtual void log(const QtMsgType type, const QString& message, const QString &file="", + const QString &function="", const int line=0); + + /** + Clear the thread-local data of the current thread. + This method is thread safe. + @param buffer Whether to clear the backtrace buffer + @param variables Whether to clear the log variables + */ + virtual void clear(const bool buffer=true, const bool variables=true); + +private: + + /** First logger */ + FileLogger* firstLogger; + + /** Second logger */ + FileLogger* secondLogger; + +}; + +} // end of namespace + +#endif // DUALFILELOGGER_H diff --git a/QtWebApp/logging/filelogger.cpp b/QtWebApp/logging/filelogger.cpp new file mode 100755 index 0000000..9a49e9f --- /dev/null +++ b/QtWebApp/logging/filelogger.cpp @@ -0,0 +1,220 @@ +/** + @file + @author Stefan Frings +*/ + +#include "filelogger.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace stefanfrings; + +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(); + bufferSize=settings->value("bufferSize",0).toInt(); + + // Translate log level settings to enumeration value + QByteArray minLevelStr = settings->value("minLevel","ALL").toByteArray(); + if (minLevelStr=="ALL" || minLevelStr=="DEBUG" || minLevelStr=="0") + { + minLevel=QtMsgType::QtDebugMsg; + } + else if (minLevelStr=="WARNING" || minLevelStr=="WARN" || minLevelStr=="1") + { + minLevel=QtMsgType::QtWarningMsg; + } + else if (minLevelStr=="ERROR" || minLevelStr=="CRITICAL" || minLevelStr=="2") + { + minLevel=QtMsgType::QtCriticalMsg; + } + else if (minLevelStr=="FATAL" || minLevelStr=="3") + { + minLevel=QtMsgType::QtFatalMsg; + } + else if (minLevelStr=="INFO" || minLevelStr=="4") + { + minLevel=QtMsgType::QtInfoMsg; + } + + // 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!=nullptr); + Q_ASSERT(refreshInterval>=0); + this->settings=settings; + file=nullptr; + if (refreshInterval>0) + { + refreshTimer.start(refreshInterval,this); + } + flushTimer.start(1000,this); + refreshSettings(); +} + + +FileLogger::~FileLogger() +{ + close(); +} + + +void FileLogger::write(const LogMessage* logMessage) +{ + // 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()) + { + qWarning("Cannot write to log file %s: %s",qPrintable(fileName),qPrintable(file->errorString())); + close(); + } + + } + + // 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=nullptr; + } + } +} + + +void FileLogger::close() +{ + if (file) + { + file->close(); + delete file; + file=nullptr; + } +} + +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/QtWebApp/logging/filelogger.h b/QtWebApp/logging/filelogger.h new file mode 100755 index 0000000..7537950 --- /dev/null +++ b/QtWebApp/logging/filelogger.h @@ -0,0 +1,134 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef FILELOGGER_H +#define FILELOGGER_H + +#include +#include +#include +#include +#include +#include "logglobal.h" +#include "logger.h" + +namespace stefanfrings { + +/** + 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. +

+ Example for the configuration settings: +

+  fileName=logs/QtWebApp.log
+  maxSize=1000000
+  maxBackups=2
+  bufferSize=0
+  minLevel=WARNING
+  msgformat={timestamp} {typeNr} {type} thread={thread}: {msg}
+  timestampFormat=dd.MM.yyyy hh:mm:ss.zzz  
+  
+ + - Possible log levels are: ALL/DEBUG=0, INFO=4, WARN/WARNING=1, ERROR/CRITICAL=2, FATAL=3 + - 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. Default is 0=unlimited. + - maxBackups defines the number of backup files to keep. Default is 0=unlimited. + - bufferSize defines the size of the ring buffer. Default is 0=disabled. + - minLevel If bufferSize=0: Messages with lower level are discarded.
+ If buffersize>0: Messages with lower level are buffered, messages with equal or higher + level (except INFO) trigger writing the buffered messages into the file.
+ Defaults is 0=debug. + - msgFormat defines the decoration of log messages, see LogMessage class. Default is "{timestamp} {type} {msg}". + - timestampFormat defines the format of timestamps, see QDateTime::toString(). Default is "yyyy-MM-dd hh:mm:ss.zzz". + + + @see set() describes how to set logger variables + @see LogMessage for a description of the message decoration. + @see Logger for a descrition of the buffer. +*/ + +class DECLSPEC 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 that is not used by other parts of the program. + The FileLogger does not take over ownership of the QSettings instance, so the caller + should destroy it during shutdown. + @param refreshInterval Interval of checking for changed config settings in msec, or 0=disabled + @param parent Parent object + */ + FileLogger(QSettings* settings, const int refreshInterval=10000, QObject* parent = nullptr); + + /** + 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; + + /** 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(); + +}; + +} // end of namespace + +#endif // FILELOGGER_H diff --git a/QtWebApp/logging/logger.cpp b/QtWebApp/logging/logger.cpp new file mode 100755 index 0000000..afaf78f --- /dev/null +++ b/QtWebApp/logging/logger.cpp @@ -0,0 +1,261 @@ +/** + @file + @author Stefan Frings +*/ + +#include "logger.h" +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #include +#endif + +using namespace stefanfrings; + +Logger* Logger::defaultLogger=nullptr; + + +QThreadStorage*> Logger::logVars; + + +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; +} + +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + static QRecursiveMutex recursiveMutex; + static QMutex nonRecursiveMutex; +#else + static QMutex recursiveMutex(QMutex::Recursive); + static QMutex nonRecursiveMutex(QMutex::NonRecursive); +#endif + +void Logger::msgHandler(const QtMsgType type, const QString &message, const QString &file, const QString &function, const int line) +{ + // 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, file, function, line); + nonRecursiveMutex.unlock(); + } + else + { + fputs(qPrintable(message),stderr); + fflush(stderr); + } + + // Abort the program after logging a fatal message + if (type==QtFatalMsg) + { + abort(); + } + + recursiveMutex.unlock(); +} + + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + void Logger::msgHandler5(const QtMsgType type, const QMessageLogContext &context, const QString &message) + { + (void)(context); // suppress "unused parameter" warning + msgHandler(type,message,context.file,context.function,context.line); + } +#else + void Logger::msgHandler4(const QtMsgType type, const char* message) + { + msgHandler(type,message); + } +#endif + + +Logger::~Logger() +{ + if (defaultLogger==this) + { +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + qInstallMessageHandler(nullptr); +#else + qInstallMsgHandler(nullptr); +#endif + defaultLogger=nullptr; + } +} + + +void Logger::write(const LogMessage* logMessage) +{ + fputs(qPrintable(logMessage->toString(msgFormat,timestampFormat)),stderr); + fflush(stderr); +} + + +void Logger::installMsgHandler() +{ + defaultLogger=this; +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + qInstallMessageHandler(msgHandler5); +#else + qInstallMsgHandler(msgHandler4); +#endif +} + + +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, const QString &file, const QString &function, const int line) +{ + // Check if the type of the message reached the configured minLevel in the order + // DEBUG, INFO, WARNING, CRITICAL, FATAL + // Since Qt 5.5: INFO messages are between DEBUG and WARNING + bool toPrint=false; + switch (type) + { + case QtDebugMsg: + if (minLevel==QtDebugMsg) + { + toPrint=true; + } + break; + + #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) + case QtInfoMsg: + if (minLevel==QtDebugMsg || + minLevel==QtInfoMsg) + { + toPrint=true; + } + break; + #endif + + case QtWarningMsg: + if (minLevel==QtDebugMsg || + #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) + minLevel==QtInfoMsg || + #endif + minLevel==QtWarningMsg) + { + toPrint=true; + } + break; + + case QtCriticalMsg: // or QtSystemMsg which has the same int value + if (minLevel==QtDebugMsg || + #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) + minLevel==QtInfoMsg || + #endif + minLevel==QtWarningMsg || + minLevel==QtCriticalMsg) + { + toPrint=true; + } + break; + + case QtFatalMsg: + toPrint=true; + break; + + default: // For additional type that might get introduced in future + toPrint=true; + } + + 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 to the buffer + LogMessage* logMessage=new LogMessage(type,message,logVars.localData(),file,function,line); + buffer->append(logMessage); + + // Delete oldest message if the buffer became too large + if (buffer->size()>bufferSize) + { + delete buffer->takeFirst(); + } + + // Print the whole buffer if the type is high enough + if (toPrint) + { + // Print the whole buffer content + 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 (toPrint) + { + LogMessage logMessage(type,message,logVars.localData(),file,function,line); + write(&logMessage); + } + } + mutex.unlock(); +} diff --git a/QtWebApp/logging/logger.h b/QtWebApp/logging/logger.h new file mode 100755 index 0000000..8021a78 --- /dev/null +++ b/QtWebApp/logging/logger.h @@ -0,0 +1,196 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LOGGER_H +#define LOGGER_H + +#include +#include +#include +#include +#include +#include +#include "logglobal.h" +#include "logmessage.h" + +namespace stefanfrings { + +/** + 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 logger can collect a configurable number of messages in thread-local + FIFO buffers. A log message with severity >= minLevel flushes the buffer, + so the messages are written out. There is one exception: + INFO messages are treated like DEBUG messages (level 0). +

+ Example: If you enable the buffer and use minLevel=2, then the application + waits until an error occurs. Then it writes out the error message together + with all buffered lower level messages of the same thread. But as long no + error occurs, nothing gets written out. +

+ If the buffer is disabled, then only messages with severity >= minLevel + are written out. +

+ The logger can be registered to handle messages from + the static global functions qDebug(), qWarning(), qCritical(), qFatal() and qInfo(). + + @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 DECLSPEC 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. + Possible log levels are: 0=DEBUG, 1=WARNING, 2=CRITICAL, 3=FATAL, 4=INFO + @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 If bufferSize=0: Messages with lower level discarded.
+ If buffersize>0: Messages with lower level are buffered, messages with equal or higher level trigger writing the buffered content. + @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 = nullptr); + + /** 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 + @param file Name of the source file where the message was generated (usually filled with the macro __FILE__) + @param function Name of the function where the message was generated (usually filled with the macro __LINE__) + @param line Line Number of the source file, where the message was generated (usually filles with the macro __func__ or __FUNCTION__) + @see LogMessage for a description of the message decoration. + */ + virtual void log(const QtMsgType type, const QString& message, const QString &file="", + const QString &function="", const int line=0); + + /** + 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. + This method is thread safe. + @param buffer Whether to clear the backtrace buffer + @param variables Whether to clear the log variables + */ + virtual 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 directly or trigger writing the buffered content. */ + QtMsgType minLevel; + + /** Size of backtrace buffer, number of messages per thread. 0=disabled */ + int bufferSize; + + /** Used to synchronize access of concurrent threads */ + 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 + @param file Name of the source file where the message was generated (usually filled with the macro __FILE__) + @param function Name of the function where the message was generated (usually filled with the macro __LINE__) + @param line Line Number of the source file, where the message was generated (usually filles with the macro __func__ or __FUNCTION__) + */ + static void msgHandler(const QtMsgType type, const QString &message, const QString &file="", + const QString &function="", const int line=0); + + +#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) + + /** + Wrapper for QT version 5. + @param type Message type (level) + @param context Message context + @param message Message text + @see msgHandler() + */ + static void msgHandler5(const QtMsgType type, const QMessageLogContext& context, const QString &message); + +#else + + /** + Wrapper for QT version 4. + @param type Message type (level) + @param message Message text + @see msgHandler() + */ + static void msgHandler4(const QtMsgType type, const char * message); + +#endif + + /** Thread local variables to be used in log messages */ + static QThreadStorage*> logVars; + + /** Thread local backtrace buffers */ + QThreadStorage*> buffers; +}; + +} // end of namespace + +#endif // LOGGER_H diff --git a/QtWebApp/logging/logglobal.h b/QtWebApp/logging/logglobal.h new file mode 100755 index 0000000..02f9d5a --- /dev/null +++ b/QtWebApp/logging/logglobal.h @@ -0,0 +1,28 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LOGGLOBAL_H +#define LOGGLOBAL_H + +#include + +// This is specific to Windows dll's +#if defined(Q_OS_WIN) + #if defined(QTWEBAPPLIB_EXPORT) + #define DECLSPEC Q_DECL_EXPORT + #elif defined(QTWEBAPPLIB_IMPORT) + #define DECLSPEC Q_DECL_IMPORT + #endif +#endif +#if !defined(DECLSPEC) + #define DECLSPEC +#endif + +#if __cplusplus < 201103L + #define nullptr 0 +#endif + +#endif // LOGGLOBAL_H + diff --git a/QtWebApp/logging/logmessage.cpp b/QtWebApp/logging/logmessage.cpp new file mode 100755 index 0000000..6a8e5ea --- /dev/null +++ b/QtWebApp/logging/logmessage.cpp @@ -0,0 +1,87 @@ +/** + @file + @author Stefan Frings +*/ + +#include "logmessage.h" +#include + +using namespace stefanfrings; + +LogMessage::LogMessage(const QtMsgType type, const QString& message, const QHash *logVars, const QString &file, const QString &function, const int line) +{ + this->type=type; + this->message=message; + this->file=file; + this->function=function; + this->line=line; + 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}",timestamp.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: // or QtSystemMsg which has the same int value + decorated.replace("{type}","FATAL "); + break; + #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) + case QtInfoMsg: + decorated.replace("{type}","INFO "); + break; + #endif + } + + decorated.replace("{file}",file); + decorated.replace("{function}",function); + decorated.replace("{line}",QString::number(line)); + + QString threadId = QString("0x%1").arg(qulonglong(QThread::currentThreadId()), 8, 16, QLatin1Char('0')); + 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/QtWebApp/logging/logmessage.h b/QtWebApp/logging/logmessage.h new file mode 100755 index 0000000..3fb6859 --- /dev/null +++ b/QtWebApp/logging/logmessage.h @@ -0,0 +1,98 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LOGMESSAGE_H +#define LOGMESSAGE_H + +#include +#include +#include +#include "logglobal.h" + +namespace stefanfrings { + +/** + 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 + - {xxx} For any user-defined logger variable + + Plus some new variables since QT 5.0, only filled when compiled in debug mode: + + - {file} Filename where the message was generated + - {function} Function where the message was generated + - {line} Line number where the message was generated +*/ + +class DECLSPEC 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 + @param file Name of the source file where the message was generated + @param function Name of the function where the message was generated + @param line Line Number of the source file, where the message was generated + */ + LogMessage(const QtMsgType type, const QString& message, const QHash* logVars, + const QString &file, const QString &function, const int line); + + /** + 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::toString(). + @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; + + /** Filename where the message was generated */ + QString file; + + /** Function name where the message was generated */ + QString function; + + /** Line number where the message was generated */ + int line; + +}; + +} // end of namespace + +#endif // LOGMESSAGE_H diff --git a/QtWebApp/readme.txt b/QtWebApp/readme.txt new file mode 100755 index 0000000..3299c05 --- /dev/null +++ b/QtWebApp/readme.txt @@ -0,0 +1,20 @@ +QtWebAppLib is a library to develop server-side web applications in C++. +Works with Qt SDK version 4.7 until at least 6.0 + +License: LGPL v3. + +Project homepage: http://stefanfrings.de/qtwebapp/index-en.html +Tutorial: http://stefanfrings.de/qtwebapp/tutorial/index.html +API doc: http://stefanfrings.de/qtwebapp/api/index.html + +In Qt 6.x you must install the "Qt5Compat" libraries. + +Demo1 shows how to use the library by including the source code into your +project, the preferred method. + +Demo2 shows how to link against the shared library. +Build the project QtWebApp to generate the shared library. + +Stefan Frings +http://stefanfrings.de + diff --git a/QtWebApp/releasenotes.txt b/QtWebApp/releasenotes.txt new file mode 100755 index 0000000..21503ed --- /dev/null +++ b/QtWebApp/releasenotes.txt @@ -0,0 +1,319 @@ +Dont forget to update the release number also in +QtWebApp.pro and httpserver/httpglobal.cpp. + +1.8.5 +19.03.2022 +Add support for SSL peer verification and CA certificate. + +1.8.4 +29.10.2021 +Add Content-Length header to static file controller. + +1.8.3 +21.03.2021 +The minLevel for logging can now be configured as string: +DEBUG/ALL=0, INFO=4, WARNING=1, ERROR/CRITICAL=2, FATAL=3 +Info messages are now positioned between DEBUG and WARNING. +I also added an example for HTTP Basic authorization. + +1.8.2 +08.03.2021 +Fix threadId not printed in log file. + +1.8.1 +07.02.2021 +Add Cookie attribute "SameSite". +SessionStore does now emit a signal when a session expires. + +1.8.0 +06.02.2021 +Fix compatibility issues to Qt 4.7 and 6.0. +Removed qtservice, use the Non-Sucking Service Manager (https://nssm.cc/) instead. + +1.7.11 +28.12.2019 +Fix Http Headers are not properly received if the two characters of +a line-break (\r\n) were not received together in the same ethernet +package. + +1.7.10 +04.12.2019 +Add support for other SSL implementations than OpenSSL (as far Qt supports it). +Fix log bufffer was triggered only by severities above minLevel (should be "at least" minLevel). + +1.7.9 +20.06.2019 +INFO messages do not trigger writing out buffered log messages anymore when +bufferSize>0 and minLevel>0. + +1.7.8 +05.02.2019 +HttpConnectionHandler closes the socket now in the thread of the socket. +Headers and Body sent to the browser are now separated into individual ethernet packets. + +1.7.7 +04.02.2019 +HttpConnectionHandler creates a new Qthread instead of being itself a QThread. +Improved formatting of thread ID in logger. + +1.7.6 +18.01.2019 +Code cleanup with const keywords and type conversions. +Update Documentation. + +1.7.5 +17.01.2019 +Added content-types for *.xml and *.json to the StaticFileController. +Fixed locking and memory leak in HttpSession. + +1.7.4 +24.05.2018 +Fixed two possible null-pointer references in case of broken HTTP requests. + +1.7.3 +25.04.2017 +Wait until all data are sent before closing connections. + +1.7.2 +17.01.2017 +Fixed compile error with MSVC. + +1.7.1 +10.11.2016 +Fixed a possible memory leak in case of broken Multipart HTTP Requests. + +1.7.0 +08.11.2016 +Introduced namespace "stefanfrings". +Improved performance a little. + +1.6.7 +10.10.2016 +Fix type of socketDescriptor in qtservice library. +Add support for INFO log messages (new since QT 5.5). +Improve indentation of log messages. + +1.6.6 +25.07.2016 +Removed useless mutex from TemplateLoader. +Add mutex to TemplateCache (which is now needed). + +1.6.5 +10.06.2016 +Incoming HTTP request headers are now processed case-insensitive. +Add support for the HttpOnly flag of cookies. + +1.6.4 +27.03.2016 +Fixed constructor of Template class did not load the source file properly. +Template loader and cache were not affected. + +1.6.3 +11.03.2016 +Fixed compilation error. +Added missing implementation of HttpRequest::getPeerAddress(). + +1.6.2 +06.03.2016 +Added mime types for some file extensions. + +1.6.1 +25.01.2016 +Fixed parser of boundary value in multi-part request, which caused that +QHttpMultipart did not work on client side. + +1.6.0 +29.12.2015 +Much better output buffering, reduces the number of small IP packages. + +1.5.13 +29.12.2015 +Improved performance a little. +Add support for old HTTP 1.0 clients. +Add HttpResposne::flush() and HttpResponse::isConnected() which are helpful to support +SSE from HTML 5 specification. + +1.5.12 +11.12.2015 +Fix program crash when using SSL with a variable sized thread pool on Windows. +Changed name of HttpSessionStore::timerEvent() to fix compiler warnings since Qt 5.0. +Add HttpRequest::getRawPath(). +HttpSessionStore::sessions is now protected. + +1.5.11 +21.11.2015 +Fix project file for Mac OS. +Add HttpRequest::getPeerAddress() and HttpResponse::getStatusCode(). + +1.5.10 +01.09.2015 +Modified StaticFileController to support ressource files (path starting with ":/" or "qrc://"). + +1.5.9 +06.08.2015 +New HttpListener::listen() method, to restart listening after close. +Add missing include for QObject in logger.h. +Add a call to flush() before closing connections, which solves an issue with nginx. + +1.5.8 +26.07.2015 +Fixed segmentation fault error when closing the application while a HTTP request is in progress. +New HttpListener::close() method to simplifly proper shutdown. + +1.5.7 +20.07.2015 +Fix Qt 5.5 compatibility issue. + +1.5.6 +22.06.2015 +Fixed compilation failes if QT does not support SSL. + +1.5.5 +16.06.2015 +Improved performance of SSL connections. + +1.5.4 +15.06.2015 +Support for Qt versions without OpenSsl. + +1.5.3 +22.05.2015 +Fixed Windows issue: QsslSocket cannot be closed from other threads than it was created in. + +1.5.2 +12.05.2015 +Fixed Windows issue: QSslSocket cannot send signals to another thread than it was created in. + +1.5.1 +14.04.2015 +Add support for pipelining. + +1.5.0 +03.04.2015 +Add support for HTTPS. + +1.4.2 +03.04.2015 +Fixed HTTP request did not work if it was split into multipe IP packages. + +1.4.1 +20.03.2015 +Fixed session cookie expires while the user is active, expiration time was not prolonged on each request. + +1.4.0 +14.03.2015 +This release has a new directory structure and new project files to support the creation of a shared library (*.dll or *.so). + +1.3.8 +12.03.2015 +Improved shutdown procedure. +New config setting "host" which binds the listener to a specific network interface. + +1.3.7 +14.01.2015 +Fixed setting maxMultiPartSize worked only with file-uploads but not with form-data. + +1.3.6 +16.09.2014 +Fixed DualFileLogger produces no output. + +1.3.5 +11.06.2014 +Fixed a multi-threading issue with race condition in StaticFileController. + +1.3.4 +04.06.2014 +Fixed wrong content type when the StaticFileController returns a cached index.html. + +1.3.3 +17.03.2014 +Improved security of StaticFileController by denying "/.." in any position of the request path. +Improved performance of StaticFileController a little. +New convenience method HttpResponse::redirect(url). +Fixed a missing return statement in StaticFileController. + +1.3.2 +08.01.2014 +Fixed HTTP Server ignoring URL parameters when the request contains POST parameters. + +1.3.1 +15.08.2013 +Fixed HTTP server not accepting connections on 64bit OS with QT 5. + +1.3.0 +20.04.2013 +Updated for compatibility QT 5. You may still use QT 4.7 or 4.8, if you like. +Also added support for logging source file name, line number and function name. + +1.2.13 +03.03.2013 +Fixed Logger writing wrong timestamp for buffered messages. +Improved shutdown procedure. The webserver now processes all final signals before the destructor finishes. + +1.2.12 +01.03.2013 +Fixed HttpResponse sending first part of data repeatedly when the amount of data is larger than the available memory for I/O buffer. + +1.2.11 +06.01.2013 +Added clearing the write buffer when accepting a new connection, so that it does not send remaining data from an aborted previous connection (which is possibly a bug in QT). + +1.2.10 +18.12.2012 +Reduced memory usage of HttpResponse in case of large response. + +1.2.9 +29.07.2012 +Added a mutex to HttpConnectionHandlerPool to fix a concurrency issue when a pooled object gets taken from the cache while it times out. +Modified HttpConnectionHandler so that it does not throw an exception anymore when a connection gets closed by the peer in the middle of a read. + +1.2.8 +22.07.2012 +Fixed a possible concurrency issue when the file cache is so small that it stores less files than the number of threads. + +1.2.7 +18.07.2012 +Fixed HttpRequest ignores additional URL parameters of POST requests. +Fixed HttpRequest ignores POST parameters of body if there is no Content-Type header. +Removed unused tempdir variable from HttpRequest. +Added mutex to cache of StaticFileController to prevent concurrency problems. +Removed HTTP response with status 408 after read timeout. Connection gets simply closed now. + +1.2.6 +29.06.2012 +Fixed a compilation error on 64 bit if super verbose debugging is enabled. +Fixed a typo in static file controller related to the document type header. + +1.2.5 +27.06.2012 +Fixed error message "QThread: Destroyed while thread is still running" during program termination. + +1.2.4 +02.06.2012 +Fixed template engine skipping variable tokens when a value is shorter than the token. + +1.2.3 +26.12.2011 +Fixed null pointer error when the HTTP server aborts a request that is too large. + +1.2.2 +06.11.2011 +Fixed compilation error on 64 bit platforms. + +1.2.1 +22.10.2011 +Fixed a multi-threading bug in HttpConnectionHandler. + +1.2.0 +05.12.2010 +Added a controller that serves static files, with cacheing. + + +1.1.0 +19.10.2010 +Added support for sessions. +Separated the base classes into individual libraries. + +1.0.0 +17.10.2010 +First release diff --git a/QtWebApp/templateengine/template.cpp b/QtWebApp/templateengine/template.cpp new file mode 100755 index 0000000..a150dd8 --- /dev/null +++ b/QtWebApp/templateengine/template.cpp @@ -0,0 +1,243 @@ +/** + @file + @author Stefan Frings +*/ + +#include "template.h" +#include + +using namespace stefanfrings; + +Template::Template(const QString source, const QString sourceName) + : QString(source) +{ + this->sourceName=sourceName; + this->warnings=false; +} + +Template::Template(QFile& file, const QTextCodec* textCodec) +{ + this->warnings=false; + 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())); + } + else + { + append(textCodec->toUnicode(data)); + } +} + + +int Template::setVariable(const QString name, const 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+value.length()); + } + if (count==0 && warnings) + { + qWarning("Template: missing variable %s in %s",qPrintable(variable),qPrintable(sourceName)); + } + return count; +} + +int Template::setCondition(const QString name, const 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 "templateglobal.h" + +namespace stefanfrings { + +/** + 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("username", "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 DECLSPEC 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(const QString source, const 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, const 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(const QString name, const 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(const 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, const int repetitions); + + /** + Enable warnings for missing tags + @param enable Warnings are enabled, if true + */ + void enableWarnings(const bool enable=true); + +private: + + /** Name of the source file */ + QString sourceName; + + /** Enables warnings, if true */ + bool warnings; +}; + +} // end of namespace + +#endif // TEMPLATE_H diff --git a/QtWebApp/templateengine/templatecache.cpp b/QtWebApp/templateengine/templatecache.cpp new file mode 100755 index 0000000..60135f6 --- /dev/null +++ b/QtWebApp/templateengine/templatecache.cpp @@ -0,0 +1,38 @@ +#include "templatecache.h" +#include +#include +#include + +using namespace stefanfrings; + +TemplateCache::TemplateCache(const QSettings* settings, QObject* parent) + :TemplateLoader(settings,parent) +{ + cache.setMaxCost(settings->value("cacheSize","1000000").toInt()); + cacheTimeout=settings->value("cacheTime","60000").toInt(); + long int cacheMaxCost=(long int)cache.maxCost(); + qDebug("TemplateCache: timeout=%i, size=%li",cacheTimeout,cacheMaxCost); +} + +QString TemplateCache::tryFile(const QString localizedName) +{ + qint64 now=QDateTime::currentMSecsSinceEpoch(); + mutex.lock(); + // search in cache + qDebug("TemplateCache: trying cached %s",qPrintable(localizedName)); + CacheEntry* entry=cache.object(localizedName); + if (entry && (cacheTimeout==0 || entry->created>now-cacheTimeout)) + { + mutex.unlock(); + 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()); + mutex.unlock(); + return entry->document; +} + diff --git a/QtWebApp/templateengine/templatecache.h b/QtWebApp/templateengine/templatecache.h new file mode 100755 index 0000000..9251254 --- /dev/null +++ b/QtWebApp/templateengine/templatecache.h @@ -0,0 +1,89 @@ +#ifndef TEMPLATECACHE_H +#define TEMPLATECACHE_H + +#include +#include "templateglobal.h" +#include "templateloader.h" + +namespace stefanfrings { + +/** + 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=../templates
+  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 DECLSPEC TemplateCache : public TemplateLoader { + Q_OBJECT + Q_DISABLE_COPY(TemplateCache) +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 that is not used by other parts of the program. + The TemplateCache does not take over ownership of the QSettings instance, so the caller + should destroy it during shutdown. + @param parent Parent object + */ + TemplateCache(const QSettings* settings, QObject* parent=nullptr); + +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(const QString localizedName); + +private: + + struct CacheEntry { + QString document; + qint64 created; + }; + + /** Timeout for each cached file */ + int cacheTimeout; + + /** Cache storage */ + QCache cache; + + /** Used to synchronize threads */ + QMutex mutex; +}; + +} // end of namespace + +#endif // TEMPLATECACHE_H diff --git a/QtWebApp/templateengine/templateglobal.h b/QtWebApp/templateengine/templateglobal.h new file mode 100755 index 0000000..736b03d --- /dev/null +++ b/QtWebApp/templateengine/templateglobal.h @@ -0,0 +1,28 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef TEMPLATEGLOBAL_H +#define TEMPLATEGLOBAL_H + +#include + +// This is specific to Windows dll's +#if defined(Q_OS_WIN) + #if defined(QTWEBAPPLIB_EXPORT) + #define DECLSPEC Q_DECL_EXPORT + #elif defined(QTWEBAPPLIB_IMPORT) + #define DECLSPEC Q_DECL_IMPORT + #endif +#endif +#if !defined(DECLSPEC) + #define DECLSPEC +#endif + +#if __cplusplus < 201103L + #define nullptr 0 +#endif + +#endif // TEMPLATEGLOBAL_H + diff --git a/QtWebApp/templateengine/templateloader.cpp b/QtWebApp/templateengine/templateloader.cpp new file mode 100755 index 0000000..0db3115 --- /dev/null +++ b/QtWebApp/templateengine/templateloader.cpp @@ -0,0 +1,133 @@ +/** + @file + @author Stefan Frings +*/ + +#include "templateloader.h" +#include +#include +#include +#include +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + #include +#else + #include +#endif + +using namespace stefanfrings; + +TemplateLoader::TemplateLoader(const 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.toLocal8Bit()); + } + qDebug("TemplateLoader: path=%s, codec=%s",qPrintable(templatePath),qPrintable(encoding)); +} + +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) +{ + QSet tried; // used to suppress duplicate attempts + + #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + QStringList locs=locales.split(',',Qt::SkipEmptyParts); + #else + QStringList locs=locales.split(',',QString::SkipEmptyParts); + #endif + + // Search for exact match + foreach (QString loc,locs) + { + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + loc.replace(QRegularExpression(";.*"),""); + #else + loc.replace(QRegExp(";.*"),""); + #endif + loc.replace('-','_'); + + QString localizedName=templateName+"-"+loc.trimmed(); + if (!tried.contains(localizedName)) + { + QString document=tryFile(localizedName); + if (!document.isEmpty()) { + return Template(document,localizedName); + } + tried.insert(localizedName); + } + } + + // Search for correct language but any country + foreach (QString loc,locs) + { + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + loc.replace(QRegularExpression("[;_-].*"),""); + #else + loc.replace(QRegExp("[;_-].*"),""); + #endif + QString localizedName=templateName+"-"+loc.trimmed(); + if (!tried.contains(localizedName)) + { + QString document=tryFile(localizedName); + if (!document.isEmpty()) + { + return Template(document,localizedName); + } + tried.insert(localizedName); + } + } + + // Search for default file + QString document=tryFile(templateName); + if (!document.isEmpty()) + { + return Template(document,templateName); + } + + qCritical("TemplateCache: cannot find template %s",qPrintable(templateName)); + return Template("",templateName); +} diff --git a/QtWebApp/templateengine/templateloader.h b/QtWebApp/templateengine/templateloader.h new file mode 100755 index 0000000..faf2497 --- /dev/null +++ b/QtWebApp/templateengine/templateloader.h @@ -0,0 +1,87 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef TEMPLATELOADER_H +#define TEMPLATELOADER_H + +#include +#include +#include +#include +#include "templateglobal.h" +#include "template.h" + +namespace stefanfrings { + +/** + 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=../templates
+  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 DECLSPEC TemplateLoader : public QObject { + Q_OBJECT + Q_DISABLE_COPY(TemplateLoader) +public: + + /** + Constructor. + @param settings configurations settings + @param parent parent object + */ + TemplateLoader(const QSettings* settings, QObject* parent=nullptr); + + /** 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(const QString templateName, const 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(const QString localizedName); + + /** Directory where the templates are searched */ + QString templatePath; + + /** Suffix to the filenames */ + QString fileNameSuffix; + + /** Codec for decoding the files */ + QTextCodec* textCodec; +}; + +} // end of namespace + +#endif // TEMPLATELOADER_H diff --git a/data/etc/docroot/Schmetterling klein.png b/data/etc/docroot/Schmetterling klein.png new file mode 100755 index 0000000..0a94f85 Binary files /dev/null and b/data/etc/docroot/Schmetterling klein.png differ diff --git a/data/etc/docroot/index.html b/data/etc/docroot/index.html new file mode 100755 index 0000000..ddf5cf6 --- /dev/null +++ b/data/etc/docroot/index.html @@ -0,0 +1,13 @@ + + + Try one of the following examples: +

+

+ diff --git a/data/etc/squeezer.conf b/data/etc/squeezer.conf new file mode 100644 index 0000000..61be40d --- /dev/null +++ b/data/etc/squeezer.conf @@ -0,0 +1,48 @@ +[listener] +;host=192.168.0.100 +port=8080 + +readTimeout=60000 +maxRequestSize=16000 +maxMultiPartSize=10000000 + +minThreads=4 +maxThreads=100 +cleanupInterval=60000 + +;sslKeyFile=ssl/server.key +;sslCertFile=ssl/server.crt +;caCertFile=ssl/ca.crt +;verifyPeer=true + +[templates] +path=templates +suffix=.tpl +encoding=UTF-8 +cacheSize=1000000 +cacheTime=60000 + +[docroot] +path=docroot +encoding=UTF-8 +maxAge=60000 +cacheTime=60000 +cacheSize=1000000 +maxCachedFileSize=65536 + +[sessions] +expirationTime=3600000 +cookieName=sessionid +cookiePath=/ +cookieComment=Identifies the user +;cookieDomain=squeezer.multimc.org + +[logging] +;The logging settings become effective after you comment in the related lines of code in main.cpp. +fileName=../logs/squeezer.log +minLevel=WARNING +bufferSize=100 +maxSize=1000000 +maxBackups=2 +timestampFormat=dd.MM.yyyy hh:mm:ss.zzz +msgFormat={timestamp} {typeNr} {type} {thread} {msg} diff --git a/data/etc/ssl/README.txt b/data/etc/ssl/README.txt new file mode 100644 index 0000000..5b350cc --- /dev/null +++ b/data/etc/ssl/README.txt @@ -0,0 +1,5 @@ +This folder contains example certificates for "localhost". + +The file client.p12 has been made for your web browser. It contains +both the client certificate and key. You need to enter the password +"test" when you import it in yur web browser. diff --git a/data/etc/ssl/ca.crt b/data/etc/ssl/ca.crt new file mode 100644 index 0000000..1079d93 --- /dev/null +++ b/data/etc/ssl/ca.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDiTCCAnGgAwIBAgIUdZNzIJ1j31fd2N8O6yxYZKR9R04wDQYJKoZIhvcNAQEL +BQAwUzELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0dlcm1hbnkxFDASBgNVBAcMC0R1 +ZXNzZWxkb3JmMQ0wCwYDVQQKDAR0ZXN0MQ0wCwYDVQQDDAR0ZXN0MCAXDTIyMDMx +OTA5MTcwOFoYDzMwMjEwNzIwMDkxNzA4WjBTMQswCQYDVQQGEwJERTEQMA4GA1UE +CAwHR2VybWFueTEUMBIGA1UEBwwLRHVlc3NlbGRvcmYxDTALBgNVBAoMBHRlc3Qx +DTALBgNVBAMMBHRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCu +/z8KMNPhc+isqeNo/fqLJHQ+xCa8EvU1xPLQ6YrGRv+w1ihb6KUU6PlrVPymNviQ +X2YFoHqXLTQwDDKS7GIoLvTKwlp06QuXq3FJvq0UueSSe3Q66dn6r8kS+8aJdGMw +5HKYZsKDeGl3y98A9GB2NV9NWZURAJbKRtThwA/YUFxF23u8JVMsD04jW0+s3txI +pqgd4SFYPE2r/xfBgOVI/xFw7rDl/W7xpQK596Ry+vn0PQiLxkqjPUWb8VjXEG7A +t5LmucBwaENZphBktvnxh4cu/2NJIfwV4ZD9DkDSn3LBZeStDBlcZwaLI8zkJQi+ +UWUhDK5OaFqHI/M0Kg+NAgMBAAGjUzBRMB0GA1UdDgQWBBRUeraXV5+kKbIpkJOI +0V3wLJ9WUzAfBgNVHSMEGDAWgBRUeraXV5+kKbIpkJOI0V3wLJ9WUzAPBgNVHRMB +Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQA3g7fONl4G4IKl0sONVzaksEnD +ZDc+QeYfND9b6FGdUQ7qtaVNX+nkeygpd1ISxr2ZkkY986isIoHJOpwq41npLhWj +UN3Z/4NiDJs/s1qdrJF3vGLYUWxrdCTScJOuiBSFeNET9wtJQayHdYZenqJ9uCUL +ARy48nRpWhJMi7dvNkohS+TQa2IvgIyNPcGJu4D68h139euSBJ4pxky1U47QKqBG +agxJZ1vpTq83I7uJiDj9ZlgYwx2GvRFLQyAW66u+5sdfjqYpOHvsKfLv75pbwxFN +S3leZDNXQ7tzdz7354WIMXa68/tm85GmPBf1auoEpxIThhDoAHyDUhjdmkza +-----END CERTIFICATE----- diff --git a/data/etc/ssl/ca.key b/data/etc/ssl/ca.key new file mode 100644 index 0000000..c07c8f8 --- /dev/null +++ b/data/etc/ssl/ca.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEArv8/CjDT4XPorKnjaP36iyR0PsQmvBL1NcTy0OmKxkb/sNYo +W+ilFOj5a1T8pjb4kF9mBaB6ly00MAwykuxiKC70ysJadOkLl6txSb6tFLnkknt0 +OunZ+q/JEvvGiXRjMORymGbCg3hpd8vfAPRgdjVfTVmVEQCWykbU4cAP2FBcRdt7 +vCVTLA9OI1tPrN7cSKaoHeEhWDxNq/8XwYDlSP8RcO6w5f1u8aUCufekcvr59D0I +i8ZKoz1Fm/FY1xBuwLeS5rnAcGhDWaYQZLb58YeHLv9jSSH8FeGQ/Q5A0p9ywWXk +rQwZXGcGiyPM5CUIvlFlIQyuTmhahyPzNCoPjQIDAQABAoIBAQCWukMSA/x7s9o0 +3h+Bz0B9mGiHp2u1kp6iMYDzcDSXk4+oQM2CXF/UItayHAGBKNfvgjvdnNv6WnUY +7WiiI/hnpAo0mjJPgGr7uC9b1WA++d5mTO9PzxxxT/dg4nue6SCGfD44BkqD8rLk +/DSYHeT37ACqHv7GJju6/kdeKo97QE+UqWYeAXshnifiBTxno5Ea+S6v6EBFLPC7 +vN36+vivwQ3lsuniq7JQpdHOtxnuCXovm7+AfGesycNyvP9V9lCxoYqmYDfLf9MO +W7vTj2MmxpCGN9lQfSB/OsSigPrNPO03MZ6d/nzVd0TFPmrsKw4/gC60XiTennKJ +VZ3K29ABAoGBANZtCICalc3494n66bsH/D7TR6UT0s+9tO7G5Zm/fPqPFf7d8c/M +Zkb9+Ad2ONTGSeUa6NXtab3TKzD4LD3eBWqWW6FtWyZ5tlsauxXJzsNzqqexDFn6 +sYX23jZU15gqau6Ve4lQzgB9fQSoFmqwJR/7t+y9GEsygeeB408zttIpAoGBANDt +LGbHsafezkpAnri4xHZIC6qd+qdPuspkmj8PizqkK4ziOOPnkdYeO6DCQPLqpP+x +LMznO5HZtwOH52r+5RvI5dQffPDXGIRV7PboWlKwwetVkRRMTkeyoFyOWxjrSk9m +Z++bYowCYOQBzeQKrz7Fb8FLg/I3dXbnUeDOjmbFAoGBALBch4S3IIWDw52ySTGy +1K6bui61SkvhXYKTBt9ZFyNCMrYouC3QkULMuobwnreqy7ZrVpw1pCYkHD8vr7vG +86+CMaVpO3I+41S1fLDkBnLNnMxGG8GaJw7nSEdpqtWV9dN8EVqUoorWq8/7rExd +ynsu300RDn0y8pOGSn6nKzRZAoGACREByEQKNZq5oQdE3AdIn0lpGDJa2j/ff0D2 +YJ4wEI9nRGncxicacQxG0icb4m7EUkRCCXJPZ3jnNEQFiuMc1iPVtWrYZSswaS3B +ZsWWhdgd0jSYYyUckIfz5ZBX67DqPJ/ZCtDXafQAeGSLpsW/7R1sSBsa0rwNYOeQ +6gyMqXECgYA7dvXFO/PMkX7UAetOO4nAkp1KaEYitfvBCO2ZpuOeIA9K74WWZGV2 +9DohEJmHLYQY3NuzMp4vtltUld7X6t2cQvHgTfSM9fbkEpe+lOWHGsBdZ2DJVpMv +bh315xcPuClye4SFl12trCyMO43yItUu+HPMx57L0cmf/nhUXJOdeA== +-----END RSA PRIVATE KEY----- diff --git a/data/etc/ssl/client.crt b/data/etc/ssl/client.crt new file mode 100644 index 0000000..e062e35 --- /dev/null +++ b/data/etc/ssl/client.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNTCCAh0CAQEwDQYJKoZIhvcNAQELBQAwUzELMAkGA1UEBhMCREUxEDAOBgNV +BAgMB0dlcm1hbnkxFDASBgNVBAcMC0R1ZXNzZWxkb3JmMQ0wCwYDVQQKDAR0ZXN0 +MQ0wCwYDVQQDDAR0ZXN0MCAXDTIyMDMxOTA5NDkyNloYDzMwMjEwNzIwMDk0OTI2 +WjBsMQswCQYDVQQGEwJERTEQMA4GA1UECAwHR2VybWFueTEUMBIGA1UEBwwLRHVl +c3NlbGRvcmYxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAG +A1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +vqf9StdDpc3nXyTeE5aXhGAWoDQ5dBFkQEJFZKmsWGOoyfhP03oE08gt3PZdQvnU +9m1UAJD4mcEl7ibMqroektHRg2jx/a5dacataXCpCgBEkK1M9EvWQXQtSWlIZ1mB +Qt84VJ95IAPo2nd6aYGF0n6NiL8JV/M7UsS//d4tb8F6SeswY8FJO/PZOLIVJgUm +0OvOLhVCLIa3uGyivqdBOtBHn2fbrL+mqnyR8GOIN3YxUVtNP255GEMbAXW1sPgM +2eFxKrHibE7U0bz2+V160myuca5Fud0BlrE+s8LXzk+0UFr0aguZms7lddupDbYo +bopFcuhj3+suAEQAbn+piQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBo0bKqHPVe +HcPA1ICU35jxS0naaYFR7biHJTIwCWSHkYi8whmLOWhwQ3r2ICqtIxf2LhhAaEsX +qr4N1bRTZ0wbdOuBKgSAIklrbWvpeZa9rsbu3Q2joBDxlfMn/kSszp7NrJR7jyt/ +XuHMVCHZdkosNt1OByVMgnYu33dJhbE5K9ANGI71mU+z84pGTYmDga5xkKmg/sMC +OJOgYIH/QObpvQv4W3DBjnBS9uH+pnMqtj+smEmOPW9tB5HO1ZxEtG2+gjB4Stvt +Yv/m8CyaJqnWJbQMAuRXCcM9Mycr4moV0kJfBCFceBYjSPD+2+ngGAu6+FTRtbF1 +RDd06vdaz3PE +-----END CERTIFICATE----- diff --git a/data/etc/ssl/client.key b/data/etc/ssl/client.key new file mode 100644 index 0000000..858ad84 --- /dev/null +++ b/data/etc/ssl/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+p/1K10Olzedf +JN4TlpeEYBagNDl0EWRAQkVkqaxYY6jJ+E/TegTTyC3c9l1C+dT2bVQAkPiZwSXu +Jsyquh6S0dGDaPH9rl1pxq1pcKkKAESQrUz0S9ZBdC1JaUhnWYFC3zhUn3kgA+ja +d3ppgYXSfo2IvwlX8ztSxL/93i1vwXpJ6zBjwUk789k4shUmBSbQ684uFUIshre4 +bKK+p0E60EefZ9usv6aqfJHwY4g3djFRW00/bnkYQxsBdbWw+AzZ4XEqseJsTtTR +vPb5XXrSbK5xrkW53QGWsT6zwtfOT7RQWvRqC5mazuV126kNtihuikVy6GPf6y4A +RABuf6mJAgMBAAECggEAcnl/Vk6GKanGAJSsWuqSs0LWkv6IeK5wmTyxWc2e07uS +/yH/HCUpfNe24fNy7+H+ArCGPYjOG9OjKKlXPjNeZB1jRRngIsdtAzPtr1+bv4uF +n7DOgeh/DvHotyll9dgCCtrogbb3DUgLqhEPCQZiCY8/ABpkS9CZkArelFmwwmZI ++bhGn7RwEH3WFF0E0AYeSE4giY7Cq0ED8ebYx7cjAFW6aQ/QmAnwdXan42+Rw7Tk +FQWvC7jJvFF5MFCYHkWaksSnUNlJfeieJckQT6Y92LDjuhl1AE2x3pLY2EeGNJjr +lDhsEZ0n4NIEXvNuhZwiNkG//7kQwbrDZkmIAQ9tAQKBgQDwihdDQpfx7sC3uTFp +BUmVD0op4r0bQiEV1JRwANuJ4QUV5PXgFNq8mNbmfk+c1NjXRogMyrg5r2Dl59el +wDiW6XtgUJe3+J9NbbPJZYvGrYoofekLy2MFJ+Q62XXNKZ77T5jkc8I7u3H65G1r +VhqmSeLiMsbQbohPAkrtdfHueQKBgQDK6RrgGu9jvATOcdwH8VocmUEXaJXqc413 +MYst2G5Db3Ckj+V5ety6Fi5OmG0Q5uJ4SAun6egwadAFSM+ytTWZWpBkOC8ZWNr5 +Q6R7Xj9atd3SypTeXyNUKsxk785zPdCI08O2ghl5kvKNlQKTK2esBkJnfuQyqJ91 +PcZkhdKPkQKBgQDq19fIek8BDPopJe1AvMHPf2MIK/A3mcPVnXvjMmMlZYVij+0i +fxnkQlCmLzIpS4H+BEW2P4HICBtRu55GnLpjVMd5DJZkLp/Rp8Z9XeAu9KXLzMpo +EoW1tfHVJxUlXnpyoI8ElKRRTzwEGVtfDWztZ3vVHoAPZas9gF6JIrs2+QKBgEqZ +336riH4Tn3TDWdE1xBqloc/YbN3Q9B7xgSku3INAkpp+KTE7obFs/EN7OQYwzOzK +GDb5AZvjG08GEQ60Huut505hdbeM+p0QaIXPBd305YRdZNRJCDUmsxUdMbse6++S +Y+9S78jJ5RF2yoaPO8N8XaeteHrDkjTJrIpCxUJxAoGBAJ4aU8ik0Im/MK0Dc55f +flXNQgtLsEsBmc+96QHaGIoG21qzSHDhaRDXmJG2H6sDfR2JMaOoHcEl9ppFvvn3 +9njV7MBPCDshpueD2Lq1tFKzNN4Ca+BS1vVPckWKJCQjTPj7HKgxDSdoefQ6WEy0 +hSpBoYCAnHklqkZeNXjTkkD6 +-----END PRIVATE KEY----- diff --git a/data/etc/ssl/client.p12 b/data/etc/ssl/client.p12 new file mode 100644 index 0000000..e6cf942 Binary files /dev/null and b/data/etc/ssl/client.p12 differ diff --git a/data/etc/ssl/server.crt b/data/etc/ssl/server.crt new file mode 100644 index 0000000..aa2dad3 --- /dev/null +++ b/data/etc/ssl/server.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNTCCAh0CAQEwDQYJKoZIhvcNAQELBQAwUzELMAkGA1UEBhMCREUxEDAOBgNV +BAgMB0dlcm1hbnkxFDASBgNVBAcMC0R1ZXNzZWxkb3JmMQ0wCwYDVQQKDAR0ZXN0 +MQ0wCwYDVQQDDAR0ZXN0MCAXDTIyMDMxOTA5NDcxMVoYDzMwMjEwNzIwMDk0NzEx +WjBsMQswCQYDVQQGEwJERTEQMA4GA1UECAwHR2VybWFueTEUMBIGA1UEBwwLRHVl +c3NlbGRvcmYxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAG +A1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +xpdPfa7gD1TJA4lZJpHYfdx8yZ2yZUxkIjQaHxYFUxtFV+e4W1J/aufhrkpZ+lfl +MLHDdRObGLQ8cuFHt9w7Bw5z+urc4/5CPAiBzZsg61GBSZkZ1RfLXOM0I4GP7Z7b +0oA0Q0HoigzTPzm/9GcjKRYKCCTmluyIcxz1IGb8I2CEmjue6FFML7OpCnmCNnRA +iMFkeQ3gfthDrhAB63bCLMUu2Z6fN560nuy2sFgs0eUGjAiR1UMZxJJ0JUBJxZ3c +D7XMbj+cp8LpNfhSrFyqbRpHPIZ7CdY3sE3ryXWH0tB7ssD0/IPTUjO7tWhD7gjt +W2K7c90YOkvXY4J2DxOWXwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBKrOFEGeM4 +jWcf/qmEvX11snxulzQzU0HxeD9mMsMROUp56djwcGoq5Zm1S2a06TP6YrzHVHnb +4ncNfa644XG0VWXHpnVRvNZUniZjYjgIkhH+T4DaLRff5tFZdlvW6cUn68+3Suai +wsFHbJhdETOz2IDPUeazKYBP9+kpST/osjVIXz/zbb1Ce8YCvrwsrZVSJlQcLl63 +PbByD7jQOMS0Hq8jDy6yeMF/6o/xAN/72UNyesYcqHPtfKyIsGhRoyjMILln//+u +aAxfQrrwCWOA7fMhp7jz+7LRQCDmLy0OYYtnaUte0oHsOejaPXH8neghi+x8h4Wi +5E/lKmm6cjct +-----END CERTIFICATE----- diff --git a/data/etc/ssl/server.key b/data/etc/ssl/server.key new file mode 100644 index 0000000..93e1e99 --- /dev/null +++ b/data/etc/ssl/server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGl099ruAPVMkD +iVkmkdh93HzJnbJlTGQiNBofFgVTG0VX57hbUn9q5+GuSln6V+UwscN1E5sYtDxy +4Ue33DsHDnP66tzj/kI8CIHNmyDrUYFJmRnVF8tc4zQjgY/tntvSgDRDQeiKDNM/ +Ob/0ZyMpFgoIJOaW7IhzHPUgZvwjYISaO57oUUwvs6kKeYI2dECIwWR5DeB+2EOu +EAHrdsIsxS7Znp83nrSe7LawWCzR5QaMCJHVQxnEknQlQEnFndwPtcxuP5ynwuk1 ++FKsXKptGkc8hnsJ1jewTevJdYfS0HuywPT8g9NSM7u1aEPuCO1bYrtz3Rg6S9dj +gnYPE5ZfAgMBAAECggEBAI/Ozo9y7WnsucvH0Dkv8BfkbLELcz4LvY9PL4NHTP/L +hcGMWWI4MXDXDgRKbzHsKFnEwIetdOjEy+lc3bR01IHdo3sWTHMFki0q8+RR69q8 +IOWM6rn3CxrupLj5f6JRIVoj4LS7q4scknT8etafQUTlYspW/mxYSM8jLxcRvJBY +c4JLwe7JRhHPKs6J8PCbp5UkARhTAqpMBmLN6/EZw2PD/ZViOTFekgFaKy8dWLdP +4Zxz4z0PYaJMK51hsx/WsUa794e9ky9/AaaWMyK4ljCZ7WlP0ABiWkkdi19ddqvo +9Up8MSkNBgfvhgYdb6p/+oE5rxDn+RoDoB0BG0n30fECgYEA7FqjmeaSYDxeycNx +IkG17fBQXew5IkAhMpuQ32a1yJeRFxP5qWn5p4Md6klSlYSgpntx8fCNakzatqxN +BXiy3Rk4KnR+KusDlEwj/qksEh3ZBjGK0ttfgoo20+qiWHKyFtVY3oMtuTRH04QK +CAUnsDj5DkDfapZ6BdWjoY5q/O0CgYEA1xkdgRfSxJPWAc1d7iq5z+C8xTETCpPH +Uj2z7f6kyvJdi7VgWmL1awMUZF93j78PkkiNC8tzVR2lTZZlTyU3oZ+72/nhD3LK +kS4Zia4iiFuYpssQwI03kdzOEDdqXSpJQs8BuoGWqk+Ewi0ZSTZVEcIaW6gYJzcn +b3C2nqxKwvsCgYEAudP9wzf0qDNu90VxwtRVPPFvzpi2xwYS095aBjuT+1WnnrR2 +28tVnW3KbHUfuCzhvmNaUDWoigZJA8zudbnTL2DvtvmGZSoH02YV+th5rPjItETp +eCVAr7sJpo5Y/B+Zg7hUOgZ7QZ0oR9YNqQackMIKlzlML1qGL+Yr1A7McXUCgYAQ +YKssbyHvMcpzrK1gOwSW3WfCI/BtN79Pdb9DecYWZcnVn2PMvggts7hTxCkYWtXW +r4t9wGnxqyYw+CiSlCTeO4lUQHxwbq8ZysbLAuVCOKcw2/lUj+wRQRy3g2Cn41Zc +reJVzxQnt5JGLqTkPCzSA1N6cxwTsFFiXNSq1DeFDQKBgFUIBnghVJGPFKJOJq4a +iLaKuqJWCAU0hXzb90+2gsm2ZNBrw/mRXyz6GXhtxlNDol+RrnCaydA61Fc+prRJ +Rm9Jzu7btm64gNs1HGjZDXLnD0QIkpyoQpDxzJrd0EK6psIkc9AcVbCcI6g9NZH0 +LGrm91K1FKh16cKhh+0I4whB +-----END PRIVATE KEY----- diff --git a/data/etc/templates/demo-de.tpl b/data/etc/templates/demo-de.tpl new file mode 100755 index 0000000..9b52311 --- /dev/null +++ b/data/etc/templates/demo-de.tpl @@ -0,0 +1,12 @@ + + +Hallo,
+du hast folgenden Pfad angefordert: {path} +

+Und dein Web Browser hat folgende Kopfzeilen geliefert: +

+{loop header} + {header.name}: {header.value}
+{end header} + + diff --git a/data/etc/templates/demo.tpl b/data/etc/templates/demo.tpl new file mode 100755 index 0000000..3801854 --- /dev/null +++ b/data/etc/templates/demo.tpl @@ -0,0 +1,12 @@ + + +Hello,
+you requested the path: {path} +

+And your web browser provided the following headers: +

+{loop header} + {header.name}: {header.value}
+{end header} + + diff --git a/data/logs/demo1.log b/data/logs/demo1.log new file mode 100644 index 0000000..b1fcfee --- /dev/null +++ b/data/logs/demo1.log @@ -0,0 +1,8 @@ +19.03.2022 10:27:05.612 0 DEBUG 0x7ff6a1a41800 TemplateLoader: path=/home/stefan/Programmierung/Qt/QtWebApp/Demo1/etc/templates, codec=UTF-8 +19.03.2022 10:27:05.612 0 DEBUG 0x7ff6a1a41800 TemplateCache: timeout=60000, size=1000000 +19.03.2022 10:27:05.612 0 DEBUG 0x7ff6a1a41800 HttpSessionStore: Sessions expire after 3600000 milliseconds +19.03.2022 10:27:05.612 0 DEBUG 0x7ff6a1a41800 StaticFileController: docroot=/home/stefan/Programmierung/Qt/QtWebApp/Demo1/etc/docroot, encoding=UTF-8, maxAge=60000 +19.03.2022 10:27:05.612 0 DEBUG 0x7ff6a1a41800 StaticFileController: cache timeout=60000, size=1000000 +19.03.2022 10:27:05.612 0 DEBUG 0x7ff6a1a41800 RequestMapper: created +19.03.2022 10:27:05.613 0 DEBUG 0x7ff6a1a41800 HttpListener: Listening on port 8080 +19.03.2022 10:27:05.613 1 WARNING 0x7ff6a1a41800 Application has started diff --git a/src/controller/dumpcontroller.cpp b/src/controller/dumpcontroller.cpp new file mode 100755 index 0000000..20857c0 --- /dev/null +++ b/src/controller/dumpcontroller.cpp @@ -0,0 +1,77 @@ +/** + @file + @author Stefan Frings +*/ + +#include "dumpcontroller.h" +#include +#include +#include + +DumpController::DumpController() +{} + +void DumpController::service(HttpRequest& request, HttpResponse& response) +{ + + response.setHeader("Content-Type", "text/html; charset=UTF-8"); + response.setCookie(HttpCookie("firstCookie","hello",600,QByteArray(),QByteArray(),QByteArray(),false,true)); + 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:"); + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QMultiMapIterator i(request.getHeaderMap()); + #else + QMapIterator i(request.getHeaderMap()); + #endif + while (i.hasNext()) + { + i.next(); + body.append("
"); + body.append(i.key()); + body.append("="); + body.append(i.value()); + } + + body.append("

Parameters:"); + + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + i=QMultiMapIterator(request.getParameterMap()); + #else + i=QMapIterator(request.getParameterMap()); + #endif + while (i.hasNext()) + { + i.next(); + body.append("
"); + body.append(i.key()); + body.append("="); + body.append(i.value()); + } + + body.append("

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

Body:
"); + body.append(request.getBody()); + + body.append(""); + response.write(body,true); +} diff --git a/src/controller/dumpcontroller.h b/src/controller/dumpcontroller.h new file mode 100755 index 0000000..8b3c3e5 --- /dev/null +++ b/src/controller/dumpcontroller.h @@ -0,0 +1,31 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef DUMPCONTROLLER_H +#define DUMPCONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + 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/src/controller/fileuploadcontroller.cpp b/src/controller/fileuploadcontroller.cpp new file mode 100755 index 0000000..4293a8a --- /dev/null +++ b/src/controller/fileuploadcontroller.cpp @@ -0,0 +1,45 @@ +/** + @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=UTF-8"); + 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/src/controller/fileuploadcontroller.h b/src/controller/fileuploadcontroller.h new file mode 100755 index 0000000..cbfdd20 --- /dev/null +++ b/src/controller/fileuploadcontroller.h @@ -0,0 +1,32 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef FILEUPLOADCONTROLLER_H +#define FILEUPLOADCONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + 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/src/controller/formcontroller.cpp b/src/controller/formcontroller.cpp new file mode 100755 index 0000000..aab65a8 --- /dev/null +++ b/src/controller/formcontroller.cpp @@ -0,0 +1,37 @@ +/** + @file + @author Stefan Frings +*/ + +#include "formcontroller.h" + +FormController::FormController() +{} + +void FormController::service(HttpRequest& request, HttpResponse& response) +{ + + response.setHeader("Content-Type", "text/html; charset=UTF-8"); + + 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/src/controller/formcontroller.h b/src/controller/formcontroller.h new file mode 100755 index 0000000..920c1c7 --- /dev/null +++ b/src/controller/formcontroller.h @@ -0,0 +1,32 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef FORMCONTROLLER_H +#define FORMCONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + 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/src/controller/logincontroller.cpp b/src/controller/logincontroller.cpp new file mode 100755 index 0000000..8d96274 --- /dev/null +++ b/src/controller/logincontroller.cpp @@ -0,0 +1,39 @@ +/** + @file + @author Stefan Frings +*/ + +#include +#include "../global.h" +#include "logincontroller.h" + +LoginController::LoginController() +{} + +void LoginController::service(HttpRequest& request, HttpResponse& response) +{ + QByteArray auth = request.getHeader("Authorization"); + if (auth.isNull()) + { + qInfo("User is not logged in"); + response.setStatus(401,"Unauthorized"); + response.setHeader("WWW-Authenticate","Basic realm=Please login with any name and password"); + } + else + { + QByteArray decoded = QByteArray::fromBase64(auth.mid(6)); // Skip the first 6 characters ("Basic ") + qInfo("Authorization request from %s",qPrintable(decoded)); + QList parts = decoded.split(':'); + QByteArray name=parts[0]; + QByteArray password=parts[1]; + + response.setHeader("Content-Type", "text/html; charset=UTF-8"); + + response.write(""); + response.write("You logged in as name="); + response.write(name); + response.write(" with password="); + response.write(password); + response.write("", true); + } +} diff --git a/src/controller/logincontroller.h b/src/controller/logincontroller.h new file mode 100755 index 0000000..57b5f20 --- /dev/null +++ b/src/controller/logincontroller.h @@ -0,0 +1,31 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef LOGINCONTROLLER_H +#define LOGINCONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + This controller demonstrates how to use HTTP basic login. +*/ + +class LoginController : public HttpRequestHandler { + Q_OBJECT + Q_DISABLE_COPY(LoginController) +public: + + /** Constructor */ + LoginController(); + + /** Generates the response */ + void service(HttpRequest& request, HttpResponse& response); +}; + +#endif // LOGINCONTROLLER_H diff --git a/src/controller/sessioncontroller.cpp b/src/controller/sessioncontroller.cpp new file mode 100755 index 0000000..a0a709b --- /dev/null +++ b/src/controller/sessioncontroller.cpp @@ -0,0 +1,34 @@ +/** + @file + @author Stefan Frings +*/ + +#include +#include "../global.h" +#include "sessioncontroller.h" +#include "httpsessionstore.h" + +SessionController::SessionController() +{} + +void SessionController::service(HttpRequest& request, HttpResponse& response) +{ + + response.setHeader("Content-Type", "text/html; charset=UTF-8"); + + // Get current session, or create a new one + HttpSession session=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().toUtf8()); + response.write(""); + } + +} diff --git a/src/controller/sessioncontroller.h b/src/controller/sessioncontroller.h new file mode 100755 index 0000000..3168dad --- /dev/null +++ b/src/controller/sessioncontroller.h @@ -0,0 +1,31 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef SESSIONCONTROLLER_H +#define SESSIONCONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + 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/src/controller/templatecontroller.cpp b/src/controller/templatecontroller.cpp new file mode 100755 index 0000000..3e0876f --- /dev/null +++ b/src/controller/templatecontroller.cpp @@ -0,0 +1,40 @@ +/** + @file + @author Stefan Frings +*/ + +#include "../global.h" +#include "templatecontroller.h" +#include "templatecache.h" +#include "template.h" + +TemplateController::TemplateController() +{} + +void TemplateController::service(HttpRequest& request, HttpResponse& response) +{ + response.setHeader("Content-Type", "text/html; charset=UTF-8"); + + Template t=templateCache->getTemplate("demo",request.getHeader("Accept-Language")); + t.enableWarnings(); + t.setVariable("path",request.getPath()); + + QMultiMap headers=request.getHeaderMap(); + #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QMultiMapIterator iterator(headers); + #else + QMapIterator iterator(headers); + #endif + + 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.toUtf8(),true); +} diff --git a/src/controller/templatecontroller.h b/src/controller/templatecontroller.h new file mode 100755 index 0000000..23ebddb --- /dev/null +++ b/src/controller/templatecontroller.h @@ -0,0 +1,32 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef TEMPLATECONTROLLER_H +#define TEMPLATECONTROLLER_H + +#include "httprequest.h" +#include "httpresponse.h" +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + 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/src/documentcache.h b/src/documentcache.h new file mode 100755 index 0000000..ac344a0 --- /dev/null +++ b/src/documentcache.h @@ -0,0 +1,4 @@ +#ifndef DOCUMENTCACHE_H +#define DOCUMENTCACHE_H + +#endif // DOCUMENTCACHE_H diff --git a/src/global.cpp b/src/global.cpp new file mode 100755 index 0000000..e0dedd9 --- /dev/null +++ b/src/global.cpp @@ -0,0 +1,11 @@ +/** + @file + @author Stefan Frings +*/ + +#include "global.h" + +TemplateCache* templateCache; +HttpSessionStore* sessionStore; +StaticFileController* staticFileController; +FileLogger* logger; diff --git a/src/global.h b/src/global.h new file mode 100755 index 0000000..3a490e2 --- /dev/null +++ b/src/global.h @@ -0,0 +1,33 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef GLOBAL_H +#define GLOBAL_H + +#include "templatecache.h" +#include "httpsessionstore.h" +#include "staticfilecontroller.h" +#include "filelogger.h" + +using namespace stefanfrings; + +/** + Global objects that are shared by multiple source files + of this project. +*/ + +/** Cache for template files */ +extern TemplateCache* templateCache; + +/** Storage for session cookies */ +extern HttpSessionStore* sessionStore; + +/** Controller for static files */ +extern StaticFileController* staticFileController; + +/** Redirects log messages to a file */ +extern FileLogger* logger; + +#endif // GLOBAL_H diff --git a/src/main.cpp b/src/main.cpp new file mode 100755 index 0000000..ff88f53 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,84 @@ +/** + @file + @author Stefan Frings +*/ + +#include +#include +#include "global.h" +#include "httplistener.h" +#include "requestmapper.h" + +using namespace stefanfrings; + +/** Search the configuration file */ +QString searchConfigFile() +{ + QString binDir=QCoreApplication::applicationDirPath(); + QString fileName("squeezer.conf"); + + QStringList searchList; + searchList.append(binDir+"/../etc"); + + foreach (QString dir, searchList) + { + QFile file(dir+"/"+fileName); + if (file.exists()) + { + fileName=QDir(file.fileName()).canonicalPath(); + qDebug("Using config file %s",qPrintable(fileName)); + return fileName; + } + } + + // not found + foreach (QString dir, searchList) + { + qWarning("%s/%s not found",qPrintable(dir),qPrintable(fileName)); + } + qFatal("Cannot find config file %s",qPrintable(fileName)); + return nullptr; +} + + +/** + Entry point of the program. +*/ +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc,argv); + app.setApplicationName("Squeezer"); + + // Find the configuration file + QString configFileName=searchConfigFile(); + + // Configure logging into a file + QSettings* logSettings=new QSettings(configFileName,QSettings::IniFormat,&app); + logSettings->beginGroup("logging"); + FileLogger* logger=new FileLogger(logSettings,10000,&app); + logger->installMsgHandler(); + + // Configure template loader and cache + QSettings* templateSettings=new QSettings(configFileName,QSettings::IniFormat,&app); + templateSettings->beginGroup("templates"); + templateCache=new TemplateCache(templateSettings,&app); + + // Configure session store + QSettings* sessionSettings=new QSettings(configFileName,QSettings::IniFormat,&app); + sessionSettings->beginGroup("sessions"); + sessionStore=new HttpSessionStore(sessionSettings,&app); + + // Configure static file controller + QSettings* fileSettings=new QSettings(configFileName,QSettings::IniFormat,&app); + fileSettings->beginGroup("docroot"); + staticFileController=new StaticFileController(fileSettings,&app); + + // Configure and start the TCP listener + QSettings* listenerSettings=new QSettings(configFileName,QSettings::IniFormat,&app); + listenerSettings->beginGroup("listener"); + new HttpListener(listenerSettings,new RequestMapper(&app),&app); + + qWarning("Application has started"); + app.exec(); + qWarning("Application has stopped"); +} diff --git a/src/requestmapper.cpp b/src/requestmapper.cpp new file mode 100755 index 0000000..7593f9d --- /dev/null +++ b/src/requestmapper.cpp @@ -0,0 +1,82 @@ +/** + @file + @author Stefan Frings +*/ + +#include +#include "global.h" +#include "requestmapper.h" +#include "filelogger.h" +#include "staticfilecontroller.h" +#include "controller/dumpcontroller.h" +#include "controller/templatecontroller.h" +#include "controller/formcontroller.h" +#include "controller/fileuploadcontroller.h" +#include "controller/sessioncontroller.h" +#include "controller/logincontroller.h" + +RequestMapper::RequestMapper(QObject* parent) + :HttpRequestHandler(parent) +{ + qDebug("RequestMapper: created"); +} + + +RequestMapper::~RequestMapper() +{ + qDebug("RequestMapper: deleted"); +} + + +void RequestMapper::service(HttpRequest& request, HttpResponse& response) +{ + QByteArray path=request.getPath(); + qDebug("RequestMapper: path=%s",path.data()); + + // For the following pathes, each request gets its own new instance of the related controller. + + 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); + } + + else if (path.startsWith("/login")) + { + LoginController().service(request, response); + } + + // All other pathes are mapped to the static file controller. + // In this case, a single instance is used for multiple requests. + else + { + staticFileController->service(request, response); + } + + qDebug("RequestMapper: finished request"); + + // Clear the log buffer + if (logger) + { + logger->clear(); + } +} diff --git a/src/requestmapper.h b/src/requestmapper.h new file mode 100755 index 0000000..7fb5078 --- /dev/null +++ b/src/requestmapper.h @@ -0,0 +1,43 @@ +/** + @file + @author Stefan Frings +*/ + +#ifndef REQUESTMAPPER_H +#define REQUESTMAPPER_H + +#include "httprequesthandler.h" + +using namespace stefanfrings; + +/** + 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); + + /** + Destructor. + */ + ~RequestMapper(); + + /** + Dispatch incoming HTTP requests to different controllers depending on the URL. + @param request The received HTTP request + @param response Must be used to return the response + */ + void service(HttpRequest& request, HttpResponse& response); + +}; + +#endif // REQUESTMAPPER_H