Add QtWebApp and turn one of its examples into the skeleton of Squeezer

This commit is contained in:
Petr Mrázek 2022-07-02 22:12:29 +02:00
parent 2d98baf142
commit 2c9312a663
76 changed files with 7014 additions and 0 deletions

5
.gitignore vendored
View file

@ -30,3 +30,8 @@
*.exe
*.out
*.app
*.kdev4
build/
install/

96
CMakeLists.txt Normal file
View file

@ -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 .)

4
QtWebApp/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
build-*-Debug
build-*-Release
QtWebApp/doc/html
*.pro.user*

View file

@ -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<void*>(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<void*>(this));
}
void HttpConnectionHandler::thread_done()
{
readTimer.stop();
socket->close();
delete socket;
qDebug("HttpConnectionHandler (%p): thread stopped", static_cast<void*>(this));
}
HttpConnectionHandler::~HttpConnectionHandler()
{
thread->quit();
thread->wait();
thread->deleteLater();
qDebug("HttpConnectionHandler (%p): destroyed", static_cast<void*>(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<void*>(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<void*>(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<void*>(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<void*>(this));
(static_cast<QSslSocket*>(socket))->startServerEncryption();
}
#endif
// Start timer for read timeout
int readTimeout=settings->value("readTimeout",10000).toInt();
readTimer.start(readTimeout);
// delete previous request
delete currentRequest;
currentRequest=nullptr;
}
bool HttpConnectionHandler::isBusy()
{
return busy;
}
void HttpConnectionHandler::setBusy()
{
this->busy = true;
}
void HttpConnectionHandler::readTimeout()
{
qDebug("HttpConnectionHandler (%p): read timeout occured",static_cast<void*>(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<void*>(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<void*>(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<void*>(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<void*>(this));
}
// Finalize sending the response if not already done
if (!response.hasSentLastPart())
{
response.write(QByteArray(),true);
}
qDebug("HttpConnectionHandler (%p): finished request",static_cast<void*>(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;
}
}
}

View file

@ -0,0 +1,130 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPCONNECTIONHANDLER_H
#define HTTPCONNECTIONHANDLER_H
#ifndef QT_NO_SSL
#include <QSslConfiguration>
#endif
#include <QTcpSocket>
#include <QSettings>
#include <QTimer>
#include <QThread>
#include "httpglobal.h"
#include "httprequest.h"
#include "httprequesthandler.h"
namespace stefanfrings {
/** Alias type definition, for compatibility to different Qt versions */
#if QT_VERSION >= 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.
<p>
Example for the required configuration settings:
<code><pre>
readTimeout=60000
maxRequestSize=16000
maxMultiPartSize=1000000
</pre></code>
<p>
The readTimeout value defines the maximum time to wait for a complete HTTP request.
<p>
MaxRequestSize is the maximum size of a HTTP request. In case of
multipart/form-data requests (also known as file-upload), the maximum
size of the body must not exceed maxMultiPartSize.
*/
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

View file

@ -0,0 +1,194 @@
#ifndef QT_NO_SSL
#include <QSslSocket>
#include <QSslKey>
#include <QSslCertificate>
#include <QSslConfiguration>
#endif
#include <QDir>
#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()<maxConnectionHandlers)
{
freeHandler=new HttpConnectionHandler(settings,requestHandler,sslConfiguration);
freeHandler->setBusy();
pool.append(freeHandler);
}
}
mutex.unlock();
return freeHandler;
}
void HttpConnectionHandlerPool::cleanup()
{
int maxIdleHandlers=settings->value("minThreads",1).toInt();
int idleCounter=0;
mutex.lock();
foreach(HttpConnectionHandler* handler, pool)
{
if (!handler->isBusy())
{
if (++idleCounter > maxIdleHandlers)
{
delete handler;
pool.removeOne(handler);
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
}
}

View file

@ -0,0 +1,134 @@
#ifndef HTTPCONNECTIONHANDLERPOOL_H
#define HTTPCONNECTIONHANDLERPOOL_H
#include <QList>
#include <QTimer>
#include <QObject>
#include <QMutex>
#include "httpglobal.h"
#include "httpconnectionhandler.h"
namespace stefanfrings {
/**
Pool of http connection handlers. The size of the pool grows and
shrinks on demand.
<p>
Example for the required configuration settings:
<code><pre>
readTimeout=60000
maxRequestSize=16000
maxMultiPartSize=1000000
minThreads=4
maxThreads=100
cleanupInterval=60000
</pre></code>
<p>
The readTimeout value defines the maximum time to wait for a complete HTTP request.
<p>
MaxRequestSize is the maximum size of a HTTP request. In case of
multipart/form-data requests (also known as file-upload), the maximum
size of the body must not exceed maxMultiPartSize.
<p>
After server start, the size of the thread pool is always 0. Threads
are started on demand when requests come in. The cleanup timer reduces
the number of idle threads slowly by closing one thread in each interval.
But the configured minimum number of threads are kept running.
<p>
Additional settings for SSL (HTTPS):
<code><pre>
sslKeyFile=ssl/server.key
sslCertFile=ssl/server.crt
;caCertFile=ssl/ca.crt
verifyPeer=false
</pre></code>
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.
<p>
Example commands to create these files:
<code><pre>
# 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
</pre></code>
<p>
Please note that a listener with SSL can only handle HTTPS protocol. To support both
HTTP and HTTPS simultaneously, you need to start <b>two</b> 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<HttpConnectionHandler*> pool;
/** Timer to clean-up unused connection handler */
QTimer cleanupTimer;
/** Used to synchronize threads */
QMutex mutex;
/** The SSL configuration (certificate, key and other settings) */
QSslConfiguration* sslConfiguration;
/** Load SSL configuration */
void loadSslConfig();
private slots:
/** Received from the clean-up timer. */
void cleanup();
};
} // end of namespace
#endif // HTTPCONNECTIONHANDLERPOOL_H

View file

@ -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<QByteArray> list=splitCSV(source);
foreach(QByteArray part, list)
{
// Split the part into name and value
QByteArray name;
QByteArray value;
int posi=part.indexOf('=');
if (posi)
{
name=part.left(posi).trimmed();
value=part.mid(posi+1).trimmed();
}
else
{
name=part.trimmed();
value="";
}
// Set fields
if (name=="Comment")
{
comment=value;
}
else if (name=="Domain")
{
domain=value;
}
else if (name=="Max-Age")
{
maxAge=value.toInt();
}
else if (name=="Path")
{
path=value;
}
else if (name=="Secure")
{
secure=true;
}
else if (name=="HttpOnly")
{
httpOnly=true;
}
else if (name=="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<QByteArray> HttpCookie::splitCSV(const QByteArray source)
{
bool inString=false;
QList<QByteArray> list;
QByteArray buffer;
for (int i=0; i<source.size(); ++i)
{
char c=source.at(i);
if (inString==false)
{
if (c=='\"')
{
inString=true;
}
else if (c==';')
{
QByteArray trimmed=buffer.trimmed();
if (!trimmed.isEmpty())
{
list.append(trimmed);
}
buffer.clear();
}
else
{
buffer.append(c);
}
}
else
{
if (c=='\"')
{
inString=false;
}
else {
buffer.append(c);
}
}
}
QByteArray trimmed=buffer.trimmed();
if (!trimmed.isEmpty())
{
list.append(trimmed);
}
return list;
}

137
QtWebApp/httpserver/httpcookie.h Executable file
View file

@ -0,0 +1,137 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPCOOKIE_H
#define HTTPCOOKIE_H
#include <QList>
#include <QByteArray>
#include "httpglobal.h"
namespace stefanfrings {
/**
HTTP cookie as defined in RFC 2109.
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<QByteArray> splitCSV(const QByteArray source);
/** Set the name of this cookie */
void setName(const QByteArray name);
/** Set the value of this cookie */
void setValue(const QByteArray value);
/** Set the comment of this cookie */
void setComment(const QByteArray comment);
/** Set the domain of this cookie */
void setDomain(const QByteArray domain);
/** Set the maximum age of this cookie in seconds. 0=discard immediately */
void setMaxAge(const int maxAge);
/** Set the path for that the cookie will be sent, default="/" which means the whole domain */
void setPath(const QByteArray path);
/** Set secure mode, so that the cookie will be sent by the browser to the server only on secure connections */
void setSecure(const bool secure);
/** Set HTTP-only mode, so that 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

View file

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

View file

@ -0,0 +1,31 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPGLOBAL_H
#define HTTPGLOBAL_H
#include <QtGlobal>
// This is specific to Windows dll's
#if defined(Q_OS_WIN)
#if defined(QTWEBAPPLIB_EXPORT)
#define DECLSPEC Q_DECL_EXPORT
#elif defined(QTWEBAPPLIB_IMPORT)
#define DECLSPEC Q_DECL_IMPORT
#endif
#endif
#if !defined(DECLSPEC)
#define DECLSPEC
#endif
/** Get the library version number */
DECLSPEC const char* getQtWebAppLibVersion();
#if __cplusplus < 201103L
#define nullptr 0
#endif
#endif // HTTPGLOBAL_H

View file

@ -0,0 +1,90 @@
/**
@file
@author Stefan Frings
*/
#include "httplistener.h"
#include "httpconnectionhandler.h"
#include "httpconnectionhandlerpool.h"
#include <QCoreApplication>
using namespace stefanfrings;
HttpListener::HttpListener(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>("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();
}
}

View file

@ -0,0 +1,120 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPLISTENER_H
#define HTTPLISTENER_H
#include <QTcpServer>
#include <QSettings>
#include <QBasicTimer>
#include "httpglobal.h"
#include "httpconnectionhandler.h"
#include "httpconnectionhandlerpool.h"
#include "httprequesthandler.h"
namespace stefanfrings {
/**
Listens for incoming TCP connections and and passes all incoming HTTP requests to your implementation of HttpRequestHandler,
which processes the request and generates the response (usually a HTML document).
<p>
Example for the required settings in the config file:
<code><pre>
;host=192.168.0.100
port=8080
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
</pre></code>
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.
<p>
The readTimeout value defines the maximum time to wait for a complete HTTP request.
<p>
MaxRequestSize is the maximum size of a HTTP request. In case of
multipart/form-data requests (also known as file-upload), the maximum
size of the body must not exceed maxMultiPartSize.
<p>
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

View file

@ -0,0 +1,579 @@
/**
@file
@author Stefan Frings
*/
#include "httprequest.h"
#include <QList>
#include <QDir>
#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<QByteArray> list=newData.split(' ');
if (list.count()!=3 || !list.at(2).contains("HTTP"))
{
qWarning("HttpRequest: received broken HTTP request, invalid first line");
status=abort;
}
else
{
method=list.at(0).trimmed();
path=list.at(1);
version=list.at(2);
peerAddress = socket->peerAddress();
status=waitForHeader;
}
}
}
void HttpRequest::readHeader(QTcpSocket* socket)
{
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<QByteArray> list=rawParameters.split('&');
foreach (QByteArray part, list)
{
int equalsChar=part.indexOf('=');
if (equalsChar>=0)
{
QByteArray name=part.left(equalsChar).trimmed();
QByteArray value=part.mid(equalsChar+1).trimmed();
parameters.insert(urlDecode(name),urlDecode(value));
}
else if (!part.isEmpty())
{
// Name without value
parameters.insert(urlDecode(part),"");
}
}
}
void HttpRequest::extractCookies()
{
#ifdef SUPERVERBOSE
qDebug("HttpRequest: extract cookies");
#endif
foreach(QByteArray cookieStr, headers.values("cookie"))
{
QList<QByteArray> list=HttpCookie::splitCSV(cookieStr);
foreach(QByteArray part, list)
{
#ifdef SUPERVERBOSE
qDebug("HttpRequest: found cookie %s",part.data());
#endif // Split the part into name and value
QByteArray name;
QByteArray value;
int posi=part.indexOf('=');
if (posi)
{
name=part.left(posi).trimmed();
value=part.mid(posi+1).trimmed();
}
else
{
name=part.trimmed();
value="";
}
cookies.insert(name,value);
}
}
headers.remove("cookie");
}
void HttpRequest::readFromSocket(QTcpSocket* socket)
{
Q_ASSERT(status!=complete);
if (status==waitForRequest)
{
readRequest(socket);
}
else if (status==waitForHeader)
{
readHeader(socket);
}
else if (status==waitForBody)
{
readBody(socket);
}
if ((boundary.isEmpty() && currentSize>maxSize) || (!boundary.isEmpty() && currentSize>maxMultiPartSize))
{
qWarning("HttpRequest: received too many bytes");
status=abort;
}
if (status==complete)
{
// Extract and decode request parameters from url and body
decodeRequestParams();
// Extract cookies from headers
extractCookies();
}
}
HttpRequest::RequestStatus HttpRequest::getStatus() const
{
return status;
}
QByteArray HttpRequest::getMethod() const
{
return method;
}
QByteArray HttpRequest::getPath() const
{
return urlDecode(path);
}
const QByteArray& HttpRequest::getRawPath() const
{
return path;
}
QByteArray HttpRequest::getVersion() const
{
return version;
}
QByteArray HttpRequest::getHeader(const QByteArray& name) const
{
return headers.value(name.toLower());
}
QList<QByteArray> HttpRequest::getHeaders(const QByteArray& name) const
{
return headers.values(name.toLower());
}
QMultiMap<QByteArray,QByteArray> HttpRequest::getHeaderMap() const
{
return headers;
}
QByteArray HttpRequest::getParameter(const QByteArray& name) const
{
return parameters.value(name);
}
QList<QByteArray> HttpRequest::getParameters(const QByteArray& name) const
{
return parameters.values(name);
}
QMultiMap<QByteArray,QByteArray> HttpRequest::getParameterMap() const
{
return parameters;
}
QByteArray HttpRequest::getBody() const
{
return bodyData;
}
QByteArray HttpRequest::urlDecode(const QByteArray source)
{
QByteArray buffer(source);
buffer.replace('+',' ');
int percentChar=buffer.indexOf('%');
while (percentChar>=0)
{
bool ok;
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<QByteArray,QByteArray>& HttpRequest::getCookieMap()
{
return cookies;
}
/**
Get the address of the connected client.
Note that multiple clients may have the same IP address, if they
share an internet connection (which is very common).
*/
QHostAddress HttpRequest::getPeerAddress() const
{
return peerAddress;
}

238
QtWebApp/httpserver/httprequest.h Executable file
View file

@ -0,0 +1,238 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPREQUEST_H
#define HTTPREQUEST_H
#include <QByteArray>
#include <QHostAddress>
#include <QTcpSocket>
#include <QMap>
#include <QMultiMap>
#include <QSettings>
#include <QTemporaryFile>
#include <QUuid>
#include "httpglobal.h"
namespace stefanfrings {
/**
This object represents a single HTTP request. It reads the request
from a TCP socket and provides getters for the individual parts
of the request.
<p>
The following config settings are required:
<code><pre>
maxRequestSize=16000
maxMultiPartSize=1000000
</pre></code>
<p>
MaxRequestSize is the maximum size of a HTTP request. In case of
multipart/form-data requests (also known as file-upload), the maximum
size of the body must not exceed maxMultiPartSize.
*/
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<QByteArray> getHeaders(const QByteArray& name) const;
/**
* Get all HTTP request headers. Note that the header names
* are returned in lower-case.
*/
QMultiMap<QByteArray,QByteArray> getHeaderMap() const;
/**
Get the value of a HTTP request parameter.
@param name Name of the parameter, case-sensitive.
@return If the parameter occurs multiple times, only the last
one is returned.
*/
QByteArray getParameter(const QByteArray& name) const;
/**
Get the values of a HTTP request parameter.
@param name Name of the parameter, case-sensitive.
*/
QList<QByteArray> getParameters(const QByteArray& name) const;
/** Get all HTTP request parameters. */
QMultiMap<QByteArray,QByteArray> getParameterMap() const;
/** Get the HTTP request body. */
QByteArray getBody() const;
/**
Decode an URL parameter.
E.g. replace "%23" by '#' and replace '+' by ' '.
@param source The url encoded strings
@see QUrl::toPercentEncoding for the reverse direction
*/
static QByteArray urlDecode(const QByteArray source);
/**
Get an uploaded file. The file is already open. It will
be closed and deleted by the destructor of this HttpRequest
object (after processing the request).
<p>
For uploaded files, the method getParameters() returns
the original fileName as provided by the calling web browser.
*/
QTemporaryFile* getUploadedFile(const QByteArray fieldName) const;
/**
Get the value of a cookie.
@param name Name of the cookie
*/
QByteArray getCookie(const QByteArray& name) const;
/** Get all cookies. */
QMap<QByteArray,QByteArray>& getCookieMap();
/**
Get the address of the connected client.
Note that multiple clients may have the same IP address, if they
share an internet connection (which is very common).
*/
QHostAddress getPeerAddress() const;
private:
/** Request headers */
QMultiMap<QByteArray,QByteArray> headers;
/** Parameters of the request */
QMultiMap<QByteArray,QByteArray> parameters;
/** Uploaded files of the request, key is the field name. */
QMap<QByteArray,QTemporaryFile*> uploadedFiles;
/** Received cookies */
QMap<QByteArray,QByteArray> cookies;
/** Storage for raw body data */
QByteArray bodyData;
/** Request method */
QByteArray method;
/** Request path (in raw encoded format) */
QByteArray path;
/** Request protocol version */
QByteArray version;
/**
Status of this request. For the state engine.
@see RequestStatus
*/
RequestStatus status;
/** Address of the connected peer. */
QHostAddress peerAddress;
/** Maximum size of requests in bytes. */
int maxSize;
/** Maximum allowed size of multipart forms in bytes. */
int maxMultiPartSize;
/** Current size */
int currentSize;
/** Expected size of body */
int expectedBodySize;
/** Name of the current header, or empty if no header is being processed */
QByteArray currentHeader;
/** Boundary of multipart/form-data body. Empty if there is no such header */
QByteArray boundary;
/** Temp file, that is used to store the multipart/form-data body */
QTemporaryFile* tempFile;
/** Parse the multipart body, that has been stored in the temp file. */
void parseMultiPartFile();
/** Sub-procedure of readFromSocket(), read the first line of a request. */
void readRequest(QTcpSocket* socket);
/** Sub-procedure of readFromSocket(), read header lines. */
void readHeader(QTcpSocket* socket);
/** Sub-procedure of readFromSocket(), read the request body. */
void readBody(QTcpSocket* socket);
/** Sub-procedure of readFromSocket(), extract and decode request parameters. */
void decodeRequestParams();
/** Sub-procedure of readFromSocket(), extract cookies from headers */
void extractCookies();
/** Buffer for collecting characters of request and header lines */
QByteArray lineBuffer;
};
} // end of namespace
#endif // HTTPREQUEST_H

View file

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

View file

@ -0,0 +1,53 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPREQUESTHANDLER_H
#define HTTPREQUESTHANDLER_H
#include "httpglobal.h"
#include "httprequest.h"
#include "httpresponse.h"
namespace stefanfrings {
/**
The request handler generates a response for each HTTP request. Web Applications
usually have one central request handler that maps incoming requests to several
controllers (servlets) based on the requested path.
<p>
You need to override the service() method or you will always get an HTTP error 501.
<p>
@warning Be aware that the main request handler instance must be created on the heap and
that it is used by multiple threads simultaneously.
@see StaticFileController which delivers static local files.
*/
class DECLSPEC HttpRequestHandler : public QObject {
Q_OBJECT
Q_DISABLE_COPY(HttpRequestHandler)
public:
/**
* Constructor.
* @param parent Parent object.
*/
HttpRequestHandler(QObject* parent=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

View file

@ -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<QByteArray,QByteArray>& HttpResponse::getHeaders()
{
return headers;
}
void HttpResponse::setStatus(int statusCode, QByteArray description)
{
this->statusCode=statusCode;
statusText=description;
}
int HttpResponse::getStatusCode() const
{
return this->statusCode;
}
void HttpResponse::writeHeaders()
{
Q_ASSERT(sentHeaders==false);
QByteArray buffer;
buffer.append("HTTP/1.1 ");
buffer.append(QByteArray::number(statusCode));
buffer.append(' ');
buffer.append(statusText);
buffer.append("\r\n");
foreach(QByteArray name, headers.keys())
{
buffer.append(name);
buffer.append(": ");
buffer.append(headers.value(name));
buffer.append("\r\n");
}
foreach(HttpCookie cookie,cookies.values())
{
buffer.append("Set-Cookie: ");
buffer.append(cookie.toByteArray());
buffer.append("\r\n");
}
buffer.append("\r\n");
writeToSocket(buffer);
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<QByteArray,HttpCookie>& HttpResponse::getCookies()
{
return cookies;
}
void HttpResponse::redirect(const QByteArray& url)
{
setStatus(303,"See Other");
setHeader("Location",url);
write("Redirect",true);
}
void HttpResponse::flush()
{
socket->flush();
}
bool HttpResponse::isConnected() const
{
return socket->isOpen();
}

View file

@ -0,0 +1,163 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPRESPONSE_H
#define HTTPRESPONSE_H
#include <QMap>
#include <QString>
#include <QTcpSocket>
#include "httpglobal.h"
#include "httpcookie.h"
namespace stefanfrings {
/**
This object represents a HTTP response, used to return something to the web client.
<p>
<code><pre>
response.setStatus(200,"OK"); // optional, because this is the default
response.writeBody("Hello");
response.writeBody("World!",true);
</pre></code>
<p>
Example how to return an error:
<code><pre>
response.setStatus(500,"server error");
response.write("The request cannot be processed because the servers is broken",true);
</pre></code>
<p>
In case of large responses (e.g. file downloads), a Content-Length header should be set
before calling write(). Web Browsers use that information to display a progress bar.
*/
class DECLSPEC HttpResponse {
Q_DISABLE_COPY(HttpResponse)
public:
/**
Constructor.
@param socket used to write the response
*/
HttpResponse(QTcpSocket *socket);
/**
Set a HTTP response header.
You must call this method before the first write().
@param name name of the header
@param value value of the header
*/
void setHeader(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<QByteArray,QByteArray>& getHeaders();
/** Get the map of cookies */
QMap<QByteArray,HttpCookie>& getCookies();
/**
Set status code and description. The default is 200,OK.
You must call this method before the first write().
*/
void setStatus(const int statusCode, const QByteArray description=QByteArray());
/** Return the status code. */
int getStatusCode() const;
/**
Write body data to the socket.
<p>
The HTTP status line, headers and cookies are sent automatically before the body.
<p>
If the response contains only a single chunk (indicated by lastPart=true),
then a Content-Length header is automatically set.
<p>
Chunked mode is automatically selected if there is no Content-Length header
and also no Connection:close header.
@param data Data bytes of the body
@param lastPart Indicates that this is the last chunk of data and flushes the output buffer.
*/
void write(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<QByteArray,QByteArray> headers;
/** Socket for writing output */
QTcpSocket* socket;
/** HTTP status code*/
int statusCode;
/** HTTP status code description */
QByteArray statusText;
/** Indicator whether headers have been sent */
bool sentHeaders;
/** Indicator whether the body has been sent completely */
bool sentLastPart;
/** Whether the response is sent in chunked mode */
bool chunkedMode;
/** Cookies */
QMap<QByteArray,HttpCookie> cookies;
/** Write raw data to the socket. This method blocks until all bytes have been passed to the TCP buffer */
bool writeToSocket(QByteArray data);
/**
Write the response HTTP status and headers to the socket.
Calling this method is optional, because writeBody() calls
it automatically when required.
*/
void writeHeaders();
};
} // end of namespace
#endif // HTTPRESPONSE_H

View file

@ -0,0 +1,188 @@
/**
@file
@author Stefan Frings
*/
#include "httpsession.h"
#include <QDateTime>
#include <QUuid>
using namespace stefanfrings;
HttpSession::HttpSession(bool canStore)
{
if (canStore)
{
dataPtr=new HttpSessionData();
dataPtr->refCount=1;
dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch();
dataPtr->id=QUuid::createUuid().toString().toLocal8Bit();
#ifdef SUPERVERBOSE
qDebug("HttpSession: (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<QByteArray,QVariant> HttpSession::getAll() const
{
QMap<QByteArray,QVariant> values;
if (dataPtr)
{
dataPtr->lock.lockForRead();
values=dataPtr->values;
dataPtr->lock.unlock();
}
return values;
}
qint64 HttpSession::getLastAccess() const
{
qint64 value=0;
if (dataPtr)
{
dataPtr->lock.lockForRead();
value=dataPtr->lastAccess;
dataPtr->lock.unlock();
}
return value;
}
void HttpSession::setLastAccess()
{
if (dataPtr)
{
dataPtr->lock.lockForWrite();
dataPtr->lastAccess=QDateTime::currentMSecsSinceEpoch();
dataPtr->lock.unlock();
}
}

121
QtWebApp/httpserver/httpsession.h Executable file
View file

@ -0,0 +1,121 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPSESSION_H
#define HTTPSESSION_H
#include <QByteArray>
#include <QVariant>
#include <QReadWriteLock>
#include "httpglobal.h"
namespace stefanfrings {
/**
This class stores data for a single HTTP session.
A session can store any number of key/value pairs. This class uses implicit
sharing for read and write access. This class is thread safe.
@see HttpSessionStore should be used to create and get instances of this class.
*/
class DECLSPEC HttpSession {
public:
/**
Constructor.
@param canStore The session can store data, if this parameter is true.
Otherwise all calls to set() and remove() do not have any effect.
*/
HttpSession(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<QByteArray,QVariant> getAll() const;
/**
Get the timestamp of last access. That is the time when the last
HttpSessionStore::getSession() has been called.
This method is thread safe.
*/
qint64 getLastAccess() const;
/**
Set the timestamp of last access, to renew the timeout period.
Called by HttpSessionStore::getSession().
This method is thread safe.
*/
void setLastAccess();
private:
struct HttpSessionData {
/** Unique ID */
QByteArray id;
/** Timestamp of last access, set by the HttpSessionStore */
qint64 lastAccess;
/** Reference counter */
int refCount;
/** Used to synchronize threads */
QReadWriteLock lock;
/** Storage for the key/value pairs; */
QMap<QByteArray,QVariant> values;
};
/** Pointer to the shared data. */
HttpSessionData* dataPtr;
};
} // end of namespace
#endif // HTTPSESSION_H

View file

@ -0,0 +1,131 @@
/**
@file
@author Stefan Frings
*/
#include "httpsessionstore.h"
#include <QDateTime>
#include <QUuid>
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<QByteArray,HttpSession>::iterator i = sessions.begin();
while (i != sessions.end())
{
QMap<QByteArray,HttpSession>::iterator prev = i;
++i;
HttpSession session=prev.value();
qint64 lastAccess=session.getLastAccess();
if (now-lastAccess>expirationTime)
{
qDebug("HttpSessionStore: session %s expired",session.getId().data());
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();
}

View file

@ -0,0 +1,127 @@
/**
@file
@author Stefan Frings
*/
#ifndef HTTPSESSIONSTORE_H
#define HTTPSESSIONSTORE_H
#include <QObject>
#include <QMap>
#include <QTimer>
#include <QMutex>
#include "httpglobal.h"
#include "httpsession.h"
#include "httpresponse.h"
#include "httprequest.h"
namespace stefanfrings {
/**
Stores HTTP sessions and deletes them when they have expired.
The following configuration settings are required in the config file:
<code><pre>
expirationTime=3600000
cookieName=sessionid
</pre></code>
The following additional configurations settings are optionally:
<code><pre>
cookiePath=/
cookieComment=Session ID
;cookieDomain=stefanfrings.de
</pre></code>
*/
class DECLSPEC HttpSessionStore : public QObject {
Q_OBJECT
Q_DISABLE_COPY(HttpSessionStore)
public:
/**
Constructor.
@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<QByteArray,HttpSession> 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

View file

@ -0,0 +1,197 @@
/**
@file
@author Stefan Frings
*/
#include "staticfilecontroller.h"
#include <QFileInfo>
#include <QDir>
#include <QDateTime>
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));
}
}

View file

@ -0,0 +1,100 @@
/**
@file
@author Stefan Frings
*/
#ifndef STATICFILECONTROLLER_H
#define STATICFILECONTROLLER_H
#include <QCache>
#include <QMutex>
#include "httpglobal.h"
#include "httprequest.h"
#include "httpresponse.h"
#include "httprequesthandler.h"
namespace stefanfrings {
/**
Delivers static files. It is usually called by the applications main request handler when
the caller requests a path that is mapped to static files.
<p>
The following settings are required in the config file:
<code><pre>
path=../docroot
encoding=UTF-8
maxAge=60000
cacheTime=60000
cacheSize=1000000
maxCachedFileSize=65536
</pre></code>
The path is relative to the directory of the config file. In case of windows, if the
settings are in the registry, the path is relative to the current working directory.
<p>
The encoding is sent to the web browser in case of text and html files.
<p>
The cache improves performance of small files when loaded from a network
drive. Large files are not cached. Files are cached as long as possible,
when cacheTime=0. The maxAge value (in msec!) controls the remote browsers cache.
<p>
Do not instantiate this class in each request, because this would make the file cache
useless. Better create one instance during start-up and call it when the application
received a related HTTP request.
*/
class DECLSPEC StaticFileController : public HttpRequestHandler {
Q_OBJECT
Q_DISABLE_COPY(StaticFileController)
public:
/**
Constructor.
@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<QString,CacheEntry> cache;
/** Used to synchronize cache access for threads */
QMutex mutex;
/** Set a content-type header in the response depending on the ending of the filename */
void setContentType(const QString file, HttpResponse &response) const;
};
} // end of namespace
#endif // STATICFILECONTROLLER_H

165
QtWebApp/lgpl-3.0.txt Normal file
View file

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View file

@ -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);
}

View file

@ -0,0 +1,82 @@
/**
@file
@author Stefan Frings
*/
#ifndef DUALFILELOGGER_H
#define DUALFILELOGGER_H
#include <QString>
#include <QSettings>
#include <QtGlobal>
#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

220
QtWebApp/logging/filelogger.cpp Executable file
View file

@ -0,0 +1,220 @@
/**
@file
@author Stefan Frings
*/
#include "filelogger.h"
#include <QTime>
#include <QStringList>
#include <QThread>
#include <QtGlobal>
#include <QFile>
#include <QTimerEvent>
#include <QDir>
#include <QFileInfo>
#include <stdio.h>
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();
}
}

134
QtWebApp/logging/filelogger.h Executable file
View file

@ -0,0 +1,134 @@
/**
@file
@author Stefan Frings
*/
#ifndef FILELOGGER_H
#define FILELOGGER_H
#include <QtGlobal>
#include <QSettings>
#include <QFile>
#include <QMutex>
#include <QBasicTimer>
#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.
<p>
Example for the configuration settings:
<code><pre>
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
</pre></code>
- 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.<br>
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.<br>
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

261
QtWebApp/logging/logger.cpp Executable file
View file

@ -0,0 +1,261 @@
/**
@file
@author Stefan Frings
*/
#include "logger.h"
#include <stdio.h>
#include <stdlib.h>
#include <QDateTime>
#include <QThread>
#include <QObject>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QRecursiveMutex>
#endif
using namespace stefanfrings;
Logger* Logger::defaultLogger=nullptr;
QThreadStorage<QHash<QString,QString>*> 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<QString,QString>);
}
logVars.localData()->insert(name,value);
mutex.unlock();
}
void Logger::clear(const bool buffer, const bool variables)
{
mutex.lock();
if (buffer && buffers.hasLocalData())
{
QList<LogMessage*>* buffer=buffers.localData();
while (buffer && !buffer->isEmpty())
{
LogMessage* logMessage=buffer->takeLast();
delete logMessage;
}
}
if (variables && logVars.hasLocalData())
{
logVars.localData()->clear();
}
mutex.unlock();
}
void Logger::log(const QtMsgType type, const QString& message, 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<LogMessage*>());
}
QList<LogMessage*>* 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();
}

196
QtWebApp/logging/logger.h Executable file
View file

@ -0,0 +1,196 @@
/**
@file
@author Stefan Frings
*/
#ifndef LOGGER_H
#define LOGGER_H
#include <QtGlobal>
#include <QThreadStorage>
#include <QHash>
#include <QStringList>
#include <QMutex>
#include <QObject>
#include "logglobal.h"
#include "logmessage.h"
namespace stefanfrings {
/**
Decorates and writes log messages to the console, stderr.
<p>
The decorator uses a predefined msgFormat string to enrich log messages
with additional information (e.g. timestamp).
<p>
The msgFormat string and also the message text may contain additional
variable names in the form <i>{name}</i> that are filled by values
taken from a static thread local dictionary.
<p>
The 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).
<p>
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.
<p>
If the buffer is disabled, then only messages with severity >= minLevel
are written out.
<p>
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.<br>
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.
<p>
In case of a fatal message, the program will abort.
Variables in the in the message are replaced by their values.
This method is thread safe.
@param type Message type (level)
@param message Message text
@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<QHash<QString,QString>*> logVars;
/** Thread local backtrace buffers */
QThreadStorage<QList<LogMessage*>*> buffers;
};
} // end of namespace
#endif // LOGGER_H

28
QtWebApp/logging/logglobal.h Executable file
View file

@ -0,0 +1,28 @@
/**
@file
@author Stefan Frings
*/
#ifndef LOGGLOBAL_H
#define LOGGLOBAL_H
#include <QtGlobal>
// This is specific to Windows dll's
#if defined(Q_OS_WIN)
#if defined(QTWEBAPPLIB_EXPORT)
#define DECLSPEC Q_DECL_EXPORT
#elif defined(QTWEBAPPLIB_IMPORT)
#define DECLSPEC Q_DECL_IMPORT
#endif
#endif
#if !defined(DECLSPEC)
#define DECLSPEC
#endif
#if __cplusplus < 201103L
#define nullptr 0
#endif
#endif // LOGGLOBAL_H

87
QtWebApp/logging/logmessage.cpp Executable file
View file

@ -0,0 +1,87 @@
/**
@file
@author Stefan Frings
*/
#include "logmessage.h"
#include <QThread>
using namespace stefanfrings;
LogMessage::LogMessage(const QtMsgType type, const QString& message, const QHash<QString, QString> *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<QString> keys=logVars.keys();
foreach (QString key, keys)
{
decorated.replace("{"+key+"}",logVars.value(key));
}
}
return decorated;
}
QtMsgType LogMessage::getType() const
{
return type;
}

98
QtWebApp/logging/logmessage.h Executable file
View file

@ -0,0 +1,98 @@
/**
@file
@author Stefan Frings
*/
#ifndef LOGMESSAGE_H
#define LOGMESSAGE_H
#include <QtGlobal>
#include <QDateTime>
#include <QHash>
#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<QString,QString>* 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<QString,QString> logVars;
/** Date and time of creation */
QDateTime timestamp;
/** Type of the message */
QtMsgType type;
/** ID number of the thread */
Qt::HANDLE threadId;
/** Message text */
QString message;
/** 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

20
QtWebApp/readme.txt Executable file
View file

@ -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

319
QtWebApp/releasenotes.txt Executable file
View file

@ -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

View file

@ -0,0 +1,243 @@
/**
@file
@author Stefan Frings
*/
#include "template.h"
#include <QFileInfo>
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<end)
{
// there is an else part
if (value==true)
{
QString truePart=mid(start+startTag.length(), ellse-start-startTag.length());
replace(start, end-start+endTag.length(), truePart);
}
else
{
// value==false
QString falsePart=mid(ellse+elseTag.length(), end-ellse-elseTag.length());
replace(start, end-start+endTag.length(), falsePart);
}
}
else if (value==true)
{
// and no else part
QString truePart=mid(start+startTag.length(), end-start-startTag.length());
replace(start, end-start+endTag.length(), truePart);
}
else
{
// value==false and no else part
replace(start, end-start+endTag.length(), "");
}
start=indexOf(startTag,start);
}
else
{
qWarning("Template: missing condition end %s in %s",qPrintable(endTag),qPrintable(sourceName));
}
}
// search for ifnot-else-end
QString startTag2="{ifnot "+name+"}";
start=indexOf(startTag2);
while (start>=0)
{
int end=indexOf(endTag,start+startTag2.length());
if (end>=0)
{
count++;
int ellse=indexOf(elseTag,start+startTag2.length());
if (ellse>start && ellse<end)
{
// there is an else part
if (value==false)
{
QString falsePart=mid(start+startTag2.length(), ellse-start-startTag2.length());
replace(start, end-start+endTag.length(), falsePart);
}
else
{
// value==true
QString truePart=mid(ellse+elseTag.length(), end-ellse-elseTag.length());
replace(start, end-start+endTag.length(), truePart);
}
}
else if (value==false)
{
// and no else part
QString falsePart=mid(start+startTag2.length(), end-start-startTag2.length());
replace(start, end-start+endTag.length(), falsePart);
}
else
{
// value==true and no else part
replace(start, end-start+endTag.length(), "");
}
start=indexOf(startTag2,start);
}
else
{
qWarning("Template: missing condition end %s in %s",qPrintable(endTag),qPrintable(sourceName));
}
}
if (count==0 && warnings)
{
qWarning("Template: missing condition %s or %s in %s",qPrintable(startTag),qPrintable(startTag2),qPrintable(sourceName));
}
return count;
}
int Template::loop(const QString name, const int repetitions)
{
Q_ASSERT(repetitions>=0);
int count=0;
QString startTag="{loop "+name+"}";
QString elseTag="{else "+name+"}";
QString endTag="{end "+name+"}";
// search for loop-else-end
int start=indexOf(startTag);
while (start>=0)
{
int end=indexOf(endTag,start+startTag.length());
if (end>=0)
{
count++;
int ellse=indexOf(elseTag,start+startTag.length());
if (ellse>start && ellse<end)
{
// there is an else part
if (repetitions>0)
{
QString loopPart=mid(start+startTag.length(), ellse-start-startTag.length());
QString insertMe;
for (int i=0; i<repetitions; ++i)
{
// number variables, conditions and sub-loop within the loop
QString nameNum=name+QString::number(i);
QString s=loopPart;
s.replace(QString("{%1.").arg(name), QString("{%1.").arg(nameNum));
s.replace(QString("{if %1.").arg(name), QString("{if %1.").arg(nameNum));
s.replace(QString("{ifnot %1.").arg(name), QString("{ifnot %1.").arg(nameNum));
s.replace(QString("{else %1.").arg(name), QString("{else %1.").arg(nameNum));
s.replace(QString("{end %1.").arg(name), QString("{end %1.").arg(nameNum));
s.replace(QString("{loop %1.").arg(name), QString("{loop %1.").arg(nameNum));
insertMe.append(s);
}
replace(start, end-start+endTag.length(), insertMe);
}
else
{
// repetitions==0
QString elsePart=mid(ellse+elseTag.length(), end-ellse-elseTag.length());
replace(start, end-start+endTag.length(), elsePart);
}
}
else if (repetitions>0)
{
// and no else part
QString loopPart=mid(start+startTag.length(), end-start-startTag.length());
QString insertMe;
for (int i=0; i<repetitions; ++i)
{
// number variables, conditions and sub-loop within the loop
QString nameNum=name+QString::number(i);
QString s=loopPart;
s.replace(QString("{%1.").arg(name), QString("{%1.").arg(nameNum));
s.replace(QString("{if %1.").arg(name), QString("{if %1.").arg(nameNum));
s.replace(QString("{ifnot %1.").arg(name), QString("{ifnot %1.").arg(nameNum));
s.replace(QString("{else %1.").arg(name), QString("{else %1.").arg(nameNum));
s.replace(QString("{end %1.").arg(name), QString("{end %1.").arg(nameNum));
s.replace(QString("{loop %1.").arg(name), QString("{loop %1.").arg(nameNum));
insertMe.append(s);
}
replace(start, end-start+endTag.length(), insertMe);
}
else
{
// repetitions==0 and no else part
replace(start, end-start+endTag.length(), "");
}
start=indexOf(startTag,start);
}
else
{
qWarning("Template: missing loop end %s in %s",qPrintable(endTag),qPrintable(sourceName));
}
}
if (count==0 && warnings)
{
qWarning("Template: missing loop %s in %s",qPrintable(startTag),qPrintable(sourceName));
}
return count;
}
void Template::enableWarnings(const bool enable)
{
warnings=enable;
}

View file

@ -0,0 +1,171 @@
/**
@file
@author Stefan Frings
*/
#ifndef TEMPLATE_H
#define TEMPLATE_H
#include <QString>
#include <QIODevice>
#include <QTextCodec>
#include <QFile>
#include <QString>
#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:
<p><code><pre>
Hello {username}, how are you?
{if locked}
Your account is locked.
{else locked}
Welcome on our system.
{end locked}
The following users are on-line:
Username Time
{loop user}
{user.name} {user.time}
{end user}
</pre></code></p>
<p>
Example code to fill this template:
<p><code><pre>
Template t(QFile("test.tpl"),QTextCode::codecForName("UTF-8"));
t.setVariable("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");
</pre></code></p>
<p>
The code example above shows how variable within loops are numbered.
Counting starts with 0. Loops can be nested, for example:
<p><code><pre>
&lt;table&gt;
{loop row}
&lt;tr&gt;
{loop row.column}
&lt;td&gt;{row.column.value}&lt;/td&gt;
{end row.column}
&lt;/tr&gt;
{end row}
&lt;/table&gt;
</pre></code></p>
<p>
Example code to fill this nested loop with 3 rows and 4 columns:
<p><code><pre>
t.loop("row",3);
t.loop("row0.column",4);
t.setVariable("row0.column0.value","a");
t.setVariable("row0.column1.value","b");
t.setVariable("row0.column2.value","c");
t.setVariable("row0.column3.value","d");
t.loop("row1.column",4);
t.setVariable("row1.column0.value","e");
t.setVariable("row1.column1.value","f");
t.setVariable("row1.column2.value","g");
t.setVariable("row1.column3.value","h");
t.loop("row2.column",4);
t.setVariable("row2.column0.value","i");
t.setVariable("row2.column1.value","j");
t.setVariable("row2.column2.value","k");
t.setVariable("row2.column3.value","l");
</pre></code></p>
@see TemplateLoader
@see TemplateCache
*/
class 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

View file

@ -0,0 +1,38 @@
#include "templatecache.h"
#include <QDateTime>
#include <QStringList>
#include <QSet>
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;
}

View file

@ -0,0 +1,89 @@
#ifndef TEMPLATECACHE_H
#define TEMPLATECACHE_H
#include <QCache>
#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.
<p>
In case of local file system, the use of this cache is optionally, since
the operating system caches files already.
<p>
Loads localized versions of template files. If the caller requests a file with the
name "index" and the suffix is ".tpl" and the requested locale is "de_DE, de, en-US",
then files are searched in the following order:
- index-de_DE.tpl
- index-de.tpl
- index-en_US.tpl
- index-en.tpl
- index.tpl
<p>
The following settings are required:
<code><pre>
path=../templates
suffix=.tpl
encoding=UTF-8
cacheSize=1000000
cacheTime=60000
</pre></code>
The path is relative to the directory of the config file. In case of windows, if the
settings are in the registry, the path is relative to the current working directory.
<p>
Files are cached as long as possible, when cacheTime=0.
@see TemplateLoader
*/
class 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<QString,CacheEntry> cache;
/** Used to synchronize threads */
QMutex mutex;
};
} // end of namespace
#endif // TEMPLATECACHE_H

View file

@ -0,0 +1,28 @@
/**
@file
@author Stefan Frings
*/
#ifndef TEMPLATEGLOBAL_H
#define TEMPLATEGLOBAL_H
#include <QtGlobal>
// This is specific to Windows dll's
#if defined(Q_OS_WIN)
#if defined(QTWEBAPPLIB_EXPORT)
#define DECLSPEC Q_DECL_EXPORT
#elif defined(QTWEBAPPLIB_IMPORT)
#define DECLSPEC Q_DECL_IMPORT
#endif
#endif
#if !defined(DECLSPEC)
#define DECLSPEC
#endif
#if __cplusplus < 201103L
#define nullptr 0
#endif
#endif // TEMPLATEGLOBAL_H

View file

@ -0,0 +1,133 @@
/**
@file
@author Stefan Frings
*/
#include "templateloader.h"
#include <QFile>
#include <QFileInfo>
#include <QStringList>
#include <QDir>
#include <QSet>
#include <QTextStream>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <QRegularExpression>
#else
#include <QRegExp>
#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<QString> 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);
}

View file

@ -0,0 +1,87 @@
/**
@file
@author Stefan Frings
*/
#ifndef TEMPLATELOADER_H
#define TEMPLATELOADER_H
#include <QString>
#include <QSettings>
#include <QMutex>
#include <QTextCodec>
#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:
<code><pre>
path=../templates
suffix=.tpl
encoding=UTF-8
</pre></code>
The path is relative to the directory of the config file. In case of windows, if the
settings are in the registry, the path is relative to the current working directory.
@see TemplateCache
*/
class 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

13
data/etc/docroot/index.html Executable file
View file

@ -0,0 +1,13 @@
<html><body>
<img src="Schmetterling klein.png">
Try one of the following examples:
<p>
<ul>
<li><a href="/dump">Dump HTTP request</a>
<li><a href="/template">Dynamic website using a template engine</a>
<li><a href="/form">HTML form</a>
<li><a href="/file">File upload form</a>
<li><a href="/session">Session demo</a>
<li><a href="/login">Login demo (HTTP basic auth)</a>
</ul>
</body></html>

48
data/etc/squeezer.conf Normal file
View file

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

5
data/etc/ssl/README.txt Normal file
View file

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

21
data/etc/ssl/ca.crt Normal file
View file

@ -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-----

27
data/etc/ssl/ca.key Normal file
View file

@ -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-----

20
data/etc/ssl/client.crt Normal file
View file

@ -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-----

28
data/etc/ssl/client.key Normal file
View file

@ -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-----

BIN
data/etc/ssl/client.p12 Normal file

Binary file not shown.

20
data/etc/ssl/server.crt Normal file
View file

@ -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-----

28
data/etc/ssl/server.key Normal file
View file

@ -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-----

12
data/etc/templates/demo-de.tpl Executable file
View file

@ -0,0 +1,12 @@
<html><body>
Hallo,<br>
du hast folgenden Pfad angefordert: {path}
<p>
Und dein Web Browser hat folgende Kopfzeilen geliefert:
<p>
{loop header}
<b>{header.name}:</b> {header.value}<br>
{end header}
</html></body>

12
data/etc/templates/demo.tpl Executable file
View file

@ -0,0 +1,12 @@
<html><body>
Hello,<br>
you requested the path: {path}
<p>
And your web browser provided the following headers:
<p>
{loop header}
<b>{header.name}:</b> {header.value}<br>
{end header}
</html></body>

8
data/logs/demo1.log Normal file
View file

@ -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

View file

@ -0,0 +1,77 @@
/**
@file
@author Stefan Frings
*/
#include "dumpcontroller.h"
#include <QVariant>
#include <QDateTime>
#include <QThread>
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("<html><body>");
body.append("<b>Request:</b>");
body.append("<br>Method: ");
body.append(request.getMethod());
body.append("<br>Path: ");
body.append(request.getPath());
body.append("<br>Version: ");
body.append(request.getVersion());
body.append("<p><b>Headers:</b>");
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QMultiMapIterator<QByteArray,QByteArray> i(request.getHeaderMap());
#else
QMapIterator<QByteArray,QByteArray> i(request.getHeaderMap());
#endif
while (i.hasNext())
{
i.next();
body.append("<br>");
body.append(i.key());
body.append("=");
body.append(i.value());
}
body.append("<p><b>Parameters:</b>");
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
i=QMultiMapIterator<QByteArray,QByteArray>(request.getParameterMap());
#else
i=QMapIterator<QByteArray,QByteArray>(request.getParameterMap());
#endif
while (i.hasNext())
{
i.next();
body.append("<br>");
body.append(i.key());
body.append("=");
body.append(i.value());
}
body.append("<p><b>Cookies:</b>");
QMapIterator<QByteArray,QByteArray> i2 = QMapIterator<QByteArray,QByteArray>(request.getCookieMap());
while (i2.hasNext())
{
i2.next();
body.append("<br>");
body.append(i2.key());
body.append("=");
body.append(i2.value());
}
body.append("<p><b>Body:</b><br>");
body.append(request.getBody());
body.append("</body></html>");
response.write(body,true);
}

31
src/controller/dumpcontroller.h Executable file
View file

@ -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

View file

@ -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("<html><body>");
response.write("Upload a JPEG image file<p>");
response.write("<form method=\"post\" enctype=\"multipart/form-data\">");
response.write(" <input type=\"hidden\" name=\"action\" value=\"show\">");
response.write(" File: <input type=\"file\" name=\"file1\"><br>");
response.write(" <input type=\"submit\">");
response.write("</form>");
response.write("</body></html>",true);
}
}

View file

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

View file

@ -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("<html><body>");
response.write("Name = ");
response.write(request.getParameter("name"));
response.write("<br>City = ");
response.write(request.getParameter("city"));
response.write("</body></html>",true);
}
else
{
response.write("<html><body>");
response.write("<form method=\"post\">");
response.write(" <input type=\"hidden\" name=\"action\" value=\"show\">");
response.write(" Name: <input type=\"text\" name=\"name\"><br>");
response.write(" City: <input type=\"text\" name=\"city\"><br>");
response.write(" <input type=\"submit\">");
response.write("</form>");
response.write("</body></html>",true);
}
}

32
src/controller/formcontroller.h Executable file
View file

@ -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

View file

@ -0,0 +1,39 @@
/**
@file
@author Stefan Frings
*/
#include <QDateTime>
#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<QByteArray> parts = decoded.split(':');
QByteArray name=parts[0];
QByteArray password=parts[1];
response.setHeader("Content-Type", "text/html; charset=UTF-8");
response.write("<html><body>");
response.write("You logged in as name=");
response.write(name);
response.write(" with password=");
response.write(password);
response.write("</body></html>", true);
}
}

View file

@ -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

View file

@ -0,0 +1,34 @@
/**
@file
@author Stefan Frings
*/
#include <QDateTime>
#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("<html><body>New session started. Reload this page now.</body></html>");
session.set("startTime",QDateTime::currentDateTime());
}
else
{
QDateTime startTime=session.get("startTime").toDateTime();
response.write("<html><body>Your session started ");
response.write(startTime.toString().toUtf8());
response.write("</body></html>");
}
}

View file

@ -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

View file

@ -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<QByteArray,QByteArray> headers=request.getHeaderMap();
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
QMultiMapIterator<QByteArray,QByteArray> iterator(headers);
#else
QMapIterator<QByteArray,QByteArray> 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);
}

View file

@ -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

4
src/documentcache.h Executable file
View file

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

11
src/global.cpp Executable file
View file

@ -0,0 +1,11 @@
/**
@file
@author Stefan Frings
*/
#include "global.h"
TemplateCache* templateCache;
HttpSessionStore* sessionStore;
StaticFileController* staticFileController;
FileLogger* logger;

33
src/global.h Executable file
View file

@ -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

84
src/main.cpp Executable file
View file

@ -0,0 +1,84 @@
/**
@file
@author Stefan Frings
*/
#include <QCoreApplication>
#include <QDir>
#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");
}

82
src/requestmapper.cpp Executable file
View file

@ -0,0 +1,82 @@
/**
@file
@author Stefan Frings
*/
#include <QCoreApplication>
#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();
}
}

43
src/requestmapper.h Executable file
View file

@ -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