Add QtWebApp and turn one of its examples into the skeleton of Squeezer
This commit is contained in:
parent
2d98baf142
commit
2c9312a663
76 changed files with 7014 additions and 0 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -30,3 +30,8 @@
|
|||
*.exe
|
||||
*.out
|
||||
*.app
|
||||
|
||||
|
||||
*.kdev4
|
||||
build/
|
||||
install/
|
||||
|
|
96
CMakeLists.txt
Normal file
96
CMakeLists.txt
Normal 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
4
QtWebApp/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
build-*-Debug
|
||||
build-*-Release
|
||||
QtWebApp/doc/html
|
||||
*.pro.user*
|
274
QtWebApp/httpserver/httpconnectionhandler.cpp
Executable file
274
QtWebApp/httpserver/httpconnectionhandler.cpp
Executable 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;
|
||||
}
|
||||
}
|
||||
}
|
130
QtWebApp/httpserver/httpconnectionhandler.h
Executable file
130
QtWebApp/httpserver/httpconnectionhandler.h
Executable 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
|
194
QtWebApp/httpserver/httpconnectionhandlerpool.cpp
Executable file
194
QtWebApp/httpserver/httpconnectionhandlerpool.cpp
Executable 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
|
||||
}
|
||||
}
|
134
QtWebApp/httpserver/httpconnectionhandlerpool.h
Executable file
134
QtWebApp/httpserver/httpconnectionhandlerpool.h
Executable 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
|
285
QtWebApp/httpserver/httpcookie.cpp
Executable file
285
QtWebApp/httpserver/httpcookie.cpp
Executable 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
137
QtWebApp/httpserver/httpcookie.h
Executable 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
|
7
QtWebApp/httpserver/httpglobal.cpp
Executable file
7
QtWebApp/httpserver/httpglobal.cpp
Executable file
|
@ -0,0 +1,7 @@
|
|||
#include "httpglobal.h"
|
||||
|
||||
const char* getQtWebAppLibVersion()
|
||||
{
|
||||
return "1.8.5";
|
||||
}
|
||||
|
31
QtWebApp/httpserver/httpglobal.h
Executable file
31
QtWebApp/httpserver/httpglobal.h
Executable 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
|
||||
|
90
QtWebApp/httpserver/httplistener.cpp
Executable file
90
QtWebApp/httpserver/httplistener.cpp
Executable 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();
|
||||
}
|
||||
}
|
120
QtWebApp/httpserver/httplistener.h
Executable file
120
QtWebApp/httpserver/httplistener.h
Executable 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
|
579
QtWebApp/httpserver/httprequest.cpp
Executable file
579
QtWebApp/httpserver/httprequest.cpp
Executable 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
238
QtWebApp/httpserver/httprequest.h
Executable 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
|
23
QtWebApp/httpserver/httprequesthandler.cpp
Executable file
23
QtWebApp/httpserver/httprequesthandler.cpp
Executable 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);
|
||||
}
|
53
QtWebApp/httpserver/httprequesthandler.h
Executable file
53
QtWebApp/httpserver/httprequesthandler.h
Executable 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
|
201
QtWebApp/httpserver/httpresponse.cpp
Executable file
201
QtWebApp/httpserver/httpresponse.cpp
Executable 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();
|
||||
}
|
163
QtWebApp/httpserver/httpresponse.h
Executable file
163
QtWebApp/httpserver/httpresponse.h
Executable 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
|
188
QtWebApp/httpserver/httpsession.cpp
Executable file
188
QtWebApp/httpserver/httpsession.cpp
Executable 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
121
QtWebApp/httpserver/httpsession.h
Executable 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
|
131
QtWebApp/httpserver/httpsessionstore.cpp
Executable file
131
QtWebApp/httpserver/httpsessionstore.cpp
Executable 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();
|
||||
}
|
127
QtWebApp/httpserver/httpsessionstore.h
Executable file
127
QtWebApp/httpserver/httpsessionstore.h
Executable 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
|
197
QtWebApp/httpserver/staticfilecontroller.cpp
Executable file
197
QtWebApp/httpserver/staticfilecontroller.cpp
Executable 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));
|
||||
}
|
||||
}
|
100
QtWebApp/httpserver/staticfilecontroller.h
Executable file
100
QtWebApp/httpserver/staticfilecontroller.h
Executable 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
165
QtWebApp/lgpl-3.0.txt
Normal 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.
|
27
QtWebApp/logging/dualfilelogger.cpp
Executable file
27
QtWebApp/logging/dualfilelogger.cpp
Executable 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);
|
||||
}
|
82
QtWebApp/logging/dualfilelogger.h
Executable file
82
QtWebApp/logging/dualfilelogger.h
Executable 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
220
QtWebApp/logging/filelogger.cpp
Executable 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
134
QtWebApp/logging/filelogger.h
Executable 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
261
QtWebApp/logging/logger.cpp
Executable 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
196
QtWebApp/logging/logger.h
Executable 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
28
QtWebApp/logging/logglobal.h
Executable 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
87
QtWebApp/logging/logmessage.cpp
Executable 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
98
QtWebApp/logging/logmessage.h
Executable 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
20
QtWebApp/readme.txt
Executable 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
319
QtWebApp/releasenotes.txt
Executable 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
|
243
QtWebApp/templateengine/template.cpp
Executable file
243
QtWebApp/templateengine/template.cpp
Executable 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;
|
||||
}
|
||||
|
171
QtWebApp/templateengine/template.h
Executable file
171
QtWebApp/templateengine/template.h
Executable 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>
|
||||
<table>
|
||||
{loop row}
|
||||
<tr>
|
||||
{loop row.column}
|
||||
<td>{row.column.value}</td>
|
||||
{end row.column}
|
||||
</tr>
|
||||
{end row}
|
||||
</table>
|
||||
</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
|
38
QtWebApp/templateengine/templatecache.cpp
Executable file
38
QtWebApp/templateengine/templatecache.cpp
Executable 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;
|
||||
}
|
||||
|
89
QtWebApp/templateengine/templatecache.h
Executable file
89
QtWebApp/templateengine/templatecache.h
Executable 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
|
28
QtWebApp/templateengine/templateglobal.h
Executable file
28
QtWebApp/templateengine/templateglobal.h
Executable 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
|
||||
|
133
QtWebApp/templateengine/templateloader.cpp
Executable file
133
QtWebApp/templateengine/templateloader.cpp
Executable 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);
|
||||
}
|
87
QtWebApp/templateengine/templateloader.h
Executable file
87
QtWebApp/templateengine/templateloader.h
Executable 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
|
BIN
data/etc/docroot/Schmetterling klein.png
Executable file
BIN
data/etc/docroot/Schmetterling klein.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
13
data/etc/docroot/index.html
Executable file
13
data/etc/docroot/index.html
Executable 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
48
data/etc/squeezer.conf
Normal 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
5
data/etc/ssl/README.txt
Normal 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
21
data/etc/ssl/ca.crt
Normal 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
27
data/etc/ssl/ca.key
Normal 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
20
data/etc/ssl/client.crt
Normal 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
28
data/etc/ssl/client.key
Normal 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
BIN
data/etc/ssl/client.p12
Normal file
Binary file not shown.
20
data/etc/ssl/server.crt
Normal file
20
data/etc/ssl/server.crt
Normal 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
28
data/etc/ssl/server.key
Normal 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
12
data/etc/templates/demo-de.tpl
Executable 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
12
data/etc/templates/demo.tpl
Executable 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
8
data/logs/demo1.log
Normal 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
|
77
src/controller/dumpcontroller.cpp
Executable file
77
src/controller/dumpcontroller.cpp
Executable 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
31
src/controller/dumpcontroller.h
Executable 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
|
45
src/controller/fileuploadcontroller.cpp
Executable file
45
src/controller/fileuploadcontroller.cpp
Executable 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);
|
||||
}
|
||||
}
|
||||
|
32
src/controller/fileuploadcontroller.h
Executable file
32
src/controller/fileuploadcontroller.h
Executable 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
|
37
src/controller/formcontroller.cpp
Executable file
37
src/controller/formcontroller.cpp
Executable 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
32
src/controller/formcontroller.h
Executable 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
|
39
src/controller/logincontroller.cpp
Executable file
39
src/controller/logincontroller.cpp
Executable 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);
|
||||
}
|
||||
}
|
31
src/controller/logincontroller.h
Executable file
31
src/controller/logincontroller.h
Executable 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
|
34
src/controller/sessioncontroller.cpp
Executable file
34
src/controller/sessioncontroller.cpp
Executable 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>");
|
||||
}
|
||||
|
||||
}
|
31
src/controller/sessioncontroller.h
Executable file
31
src/controller/sessioncontroller.h
Executable 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
|
40
src/controller/templatecontroller.cpp
Executable file
40
src/controller/templatecontroller.cpp
Executable 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);
|
||||
}
|
32
src/controller/templatecontroller.h
Executable file
32
src/controller/templatecontroller.h
Executable 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
4
src/documentcache.h
Executable file
|
@ -0,0 +1,4 @@
|
|||
#ifndef DOCUMENTCACHE_H
|
||||
#define DOCUMENTCACHE_H
|
||||
|
||||
#endif // DOCUMENTCACHE_H
|
11
src/global.cpp
Executable file
11
src/global.cpp
Executable 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
33
src/global.h
Executable 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
84
src/main.cpp
Executable 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
82
src/requestmapper.cpp
Executable 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
43
src/requestmapper.h
Executable 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
|
Loading…
Reference in a new issue