├── .clang-format ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── build.yml │ └── codestyle.yml ├── .gitignore ├── CHANGELOG.txt ├── Demo1 ├── CMakeLists.txt ├── controller │ ├── dumpcontroller.cpp │ ├── dumpcontroller.h │ ├── fileuploadcontroller.cpp │ ├── fileuploadcontroller.h │ ├── formcontroller.cpp │ ├── formcontroller.h │ ├── logincontroller.cpp │ ├── logincontroller.h │ ├── sessioncontroller.cpp │ ├── sessioncontroller.h │ ├── templatecontroller.cpp │ └── templatecontroller.h ├── documentcache.h ├── etc │ ├── Demo1.ini │ ├── docroot │ │ ├── Schmetterling klein.png │ │ └── index.html │ ├── ssl │ │ ├── my.cert │ │ └── my.key │ └── templates │ │ ├── demo-de.tpl │ │ └── demo.tpl ├── global.cpp ├── global.h ├── logs │ └── for_log_files.txt ├── main.cpp ├── requestmapper.cpp └── requestmapper.h ├── Demo2 ├── CMakeLists.txt ├── main.cpp ├── requesthandler.cpp └── requesthandler.h ├── LICENSE ├── QtWebApp ├── CMakeLists.txt ├── Doxyfile ├── cmake │ ├── QtWebAppConfig.cmake.in │ └── QtWebAppConfigVersion.cmake.in ├── doc │ └── mainpage.dox ├── httpserver │ ├── CMakeLists.txt │ ├── httpconnectionhandler.cpp │ ├── httpconnectionhandler.h │ ├── httpconnectionhandlerpool.cpp │ ├── httpconnectionhandlerpool.h │ ├── httpcookie.cpp │ ├── httpcookie.h │ ├── httplistener.cpp │ ├── httplistener.h │ ├── httprequest.cpp │ ├── httprequest.h │ ├── httprequesthandler.h │ ├── httpresponse.cpp │ ├── httpresponse.h │ ├── httpserverconfig.cpp │ ├── httpserverconfig.h │ ├── httpsession.cpp │ ├── httpsession.h │ ├── httpsessionstore.cpp │ ├── httpsessionstore.h │ ├── staticfilecontroller.cpp │ └── staticfilecontroller.h ├── logging │ ├── CMakeLists.txt │ ├── dualfilelogger.cpp │ ├── dualfilelogger.h │ ├── filelogger.cpp │ ├── filelogger.h │ ├── logger.cpp │ ├── logger.h │ ├── logmessage.cpp │ └── logmessage.h ├── qtwebappglobal.cpp ├── qtwebappglobal.h.in └── templateengine │ ├── CMakeLists.txt │ ├── template.cpp │ ├── template.h │ ├── templatecache.cpp │ ├── templatecache.h │ ├── templateengineconfig.cpp │ ├── templateengineconfig.h │ ├── templateloader.cpp │ └── templateloader.h ├── README.md └── format.sh /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | BasedOnStyle: LLVM 3 | IndentWidth: 4 4 | 5 | --- 6 | Language: Cpp 7 | Standard: c++11 8 | 9 | AlignEscapedNewlines: Left 10 | AlignTrailingComments: true 11 | 12 | AllowShortBlocksOnASingleLine: Empty 13 | AllowShortFunctionsOnASingleLine: Empty 14 | AllowShortLambdasOnASingleLine: All 15 | 16 | BreakBeforeBraces: Attach 17 | 18 | ColumnLimit: 125 19 | ContinuationIndentWidth: 4 20 | IndentCaseLabels: true 21 | IndentWidth: 4 22 | NamespaceIndentation: All 23 | TabWidth: 4 24 | UseTab: ForIndentation 25 | 26 | DeriveLineEnding: false 27 | UseCRLF: false 28 | 29 | IncludeBlocks: Regroup 30 | SortIncludes: true 31 | SortUsingDeclarations: true 32 | 33 | PointerAlignment: Right 34 | 35 | ... 36 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: msrd0 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: "06:00" 8 | timezone: "Europe/Amsterdam" 9 | reviewers: 10 | - "msrd0" 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: ${{ matrix.os }}-${{ matrix.compiler.cc }}-Qt${{ matrix.qt.version }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: 16 | - "ubuntu-latest" 17 | - "macos-latest" 18 | compiler: 19 | - cc: "gcc" 20 | cxx: "g++" 21 | - cc: "clang" 22 | cxx: "clang++" 23 | qt: 24 | - version: "5.12.2" 25 | extra_modules: "" 26 | - version: "6.2.0" 27 | extra_modules: "qt5compat" 28 | exclude: 29 | - os: "macos-latest" 30 | compiler: 31 | cc: "gcc" 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: jurplel/install-qt-action@v3 35 | with: 36 | version: ${{ matrix.qt.version }} 37 | modules: ${{ matrix.qt.extra_modules }} 38 | 39 | - name: Build QtWebApp 40 | run: | 41 | if [ "${{ matrix.os }}" == "ubuntu-latest" ] && [ "$CC" == "clang" ]; then export LDFLAGS=-fuse-ld=lld; fi 42 | pushd QtWebApp 43 | mkdir build 44 | pushd build 45 | cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Release .. 46 | make 47 | sudo make install 48 | popd # build 49 | popd # QtWebApp 50 | env: 51 | CC: ${{ matrix.compiler.cc }} 52 | CXX: ${{ matrix.compiler.cxx }} 53 | 54 | - name: Build Demo1 55 | run: | 56 | if [ "${{ matrix.os }}" == "ubuntu-latest" ] && [ "$CC" == "clang" ]; then export LDFLAGS=-fuse-ld=lld; fi 57 | pushd Demo1 58 | mkdir build 59 | pushd build 60 | cmake -G 'Unix Makefiles' .. 61 | make 62 | popd # Demo1 63 | env: 64 | CC: ${{ matrix.compiler.cc }} 65 | CXX: ${{ matrix.compiler.cxx }} 66 | 67 | - name: Build Demo2 68 | run: | 69 | if [ "${{ matrix.os }}" == "ubuntu-latest" ] && [ "$CC" == "clang" ]; then export LDFLAGS=-fuse-ld=lld; fi 70 | pushd Demo2 71 | mkdir build 72 | pushd build 73 | cmake -G 'Unix Makefiles' .. 74 | make 75 | popd # Demo2 76 | env: 77 | CC: ${{ matrix.compiler.cc }} 78 | CXX: ${{ matrix.compiler.cxx }} 79 | 80 | windows-mingw: 81 | name: "Windows" 82 | runs-on: windows-2019 83 | strategy: 84 | matrix: 85 | include: 86 | - name: Windows MinGW 8.1 87 | arch: win32_mingw81 88 | generator: "-G 'Unix Makefiles'" 89 | qt_version: "5.15.2" 90 | - name: Windows MSVC 2019 91 | arch: win64_msvc2019_64 92 | generator: "" 93 | qt_version: "5.15.2" 94 | 95 | steps: 96 | - uses: actions/checkout@v3 97 | - uses: jurplel/install-qt-action@v3 98 | with: 99 | arch: ${{ matrix.arch }} 100 | version: ${{ matrix.qt_version }} 101 | 102 | - uses: jurplel/install-qt-action@v3 103 | if: matrix.arch == 'win32_mingw81' 104 | with: 105 | tools-only: 'true' 106 | tools: 'tools_mingw,qt.tools.win32_mingw810' 107 | 108 | - name: Add g++ to PATH 109 | if: matrix.arch == 'win32_mingw81' 110 | run: echo "$env:IQTA_TOOLS/mingw810_32/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append 111 | 112 | - run: cmake -S QtWebApp -B build-qtwebapp ${{ matrix.generator }} -DCMAKE_BUILD_TYPE=Release 113 | - run: cmake --build build-qtwebapp 114 | - run: cmake --build build-qtwebapp --target install 115 | 116 | - run: cmake -S Demo1 -B build-demo1 ${{ matrix.generator }} 117 | - run: cmake --build build-demo1 118 | 119 | - run: cmake -S Demo2 -B build-demo2 ${{ matrix.generator }} 120 | - run: cmake --build build-demo2 121 | -------------------------------------------------------------------------------- /.github/workflows/codestyle.yml: -------------------------------------------------------------------------------- 1 | name: Code Style 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | clang-format: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - run: ./format.sh --check 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | build-*/ 3 | *~ 4 | \#* 5 | .travis-keys.tar.xz 6 | *.user 7 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Dont forget to update the release number also in 2 | QtWebApp.pro and httpserver/httpglobal.cpp. 3 | 4 | 1.8.3 5 | 21.03.2021 6 | The minLevel for logging can now be configured as string: 7 | DEBUG/ALL=0, INFO=4, WARNING=1, ERROR/CRITICAL=2, FATAL=3 8 | Info messages are now positioned between DEBUG and WARNING. 9 | I also added an example for HTTP Basic authorization. 10 | 11 | 1.8.2 12 | 08.03.2021 13 | Fix threadId not printed in log file. 14 | 15 | 1.8.1 16 | 07.02.2021 17 | Add Cookie attribute "SameSite". 18 | SessionStore does now emit a signal when a session expires. 19 | 20 | 1.8.0 21 | 06.02.2021 22 | Fix compatibility issues to Qt 4.7 and 6.0. 23 | Removed qtservice, use the Non-Sucking Service Manager (https://nssm.cc/) instead. 24 | 25 | 1.7.11 26 | 28.12.2019 27 | Fix Http Headers are not properly received if the two characters of 28 | a line-break (\r\n) were not received together in the same ethernet 29 | package. 30 | 31 | 1.7.10 32 | 04.12.2019 33 | Add support for other SSL implementations than OpenSSL (as far Qt supports it). 34 | Fix log bufffer was triggered only by severities above minLevel (should be "at least" minLevel). 35 | 36 | 1.7.9 37 | 20.06.2019 38 | INFO messages do not trigger writing out buffered log messages anymore when 39 | bufferSize>0 and minLevel>0. 40 | 41 | 1.7.8 42 | 05.02.2019 43 | HttpConnectionHandler closes the socket now in the thread of the socket. 44 | Headers and Body sent to the browser are now separated into individual ethernet packets. 45 | 46 | 1.7.7 47 | 04.02.2019 48 | HttpConnectionHandler creates a new Qthread instead of being itself a QThread. 49 | Improved formatting of thread ID in logger. 50 | 51 | 1.7.6 52 | 18.01.2019 53 | Code cleanup with const keywords and type conversions. 54 | Update Documentation. 55 | 56 | 1.7.5 57 | 17.01.2019 58 | Added content-types for *.xml and *.json to the StaticFileController. 59 | Fixed locking and memory leak in HttpSession. 60 | 61 | 1.7.4 62 | 24.05.2018 63 | Fixed two possible null-pointer references in case of broken HTTP requests. 64 | 65 | 1.7.3 66 | 25.04.2017 67 | Wait until all data are sent before closing connections. 68 | 69 | 1.7.2 70 | 17.01.2017 71 | Fixed compile error with MSVC. 72 | 73 | 1.7.1 74 | 10.11.2016 75 | Fixed a possible memory leak in case of broken Multipart HTTP Requests. 76 | 77 | 1.7.0 78 | 08.11.2016 79 | Introduced namespace "stefanfrings". 80 | Improved performance a little. 81 | 82 | 1.6.7 83 | 10.10.2016 84 | Fix type of socketDescriptor in qtservice library. 85 | Add support for INFO log messages (new since QT 5.5). 86 | Improve indentation of log messages. 87 | 88 | 1.6.6 89 | 25.07.2016 90 | Removed useless mutex from TemplateLoader. 91 | Add mutex to TemplateCache (which is now needed). 92 | 93 | 1.6.5 94 | 10.06.2016 95 | Incoming HTTP request headers are now processed case-insensitive. 96 | Add support for the HttpOnly flag of cookies. 97 | 98 | 1.6.4 99 | 27.03.2016 100 | Fixed constructor of Template class did not load the source file properly. 101 | Template loader and cache were not affected. 102 | 103 | 1.6.3 104 | 11.03.2016 105 | Fixed compilation error. 106 | Added missing implementation of HttpRequest::getPeerAddress(). 107 | 108 | 1.6.2 109 | 06.03.2016 110 | Added mime types for some file extensions. 111 | 112 | 1.6.1 113 | 25.01.2016 114 | Fixed parser of boundary value in multi-part request, which caused that 115 | QHttpMultipart did not work on client side. 116 | 117 | 1.6.0 118 | 29.12.2015 119 | Much better output buffering, reduces the number of small IP packages. 120 | 121 | 1.5.13 122 | 29.12.2015 123 | Improved performance a little. 124 | Add support for old HTTP 1.0 clients. 125 | Add HttpResposne::flush() and HttpResponse::isConnected() which are helpful to support 126 | SSE from HTML 5 specification. 127 | 128 | 1.5.12 129 | 11.12.2015 130 | Fix program crash when using SSL with a variable sized thread pool on Windows. 131 | Changed name of HttpSessionStore::timerEvent() to fix compiler warnings since Qt 5.0. 132 | Add HttpRequest::getRawPath(). 133 | HttpSessionStore::sessions is now protected. 134 | 135 | 1.5.11 136 | 21.11.2015 137 | Fix project file for Mac OS. 138 | Add HttpRequest::getPeerAddress() and HttpResponse::getStatusCode(). 139 | 140 | 1.5.10 141 | 01.09.2015 142 | Modified StaticFileController to support ressource files (path starting with ":/" or "qrc://"). 143 | 144 | 1.5.9 145 | 06.08.2015 146 | New HttpListener::listen() method, to restart listening after close. 147 | Add missing include for QObject in logger.h. 148 | Add a call to flush() before closing connections, which solves an issue with nginx. 149 | 150 | 1.5.8 151 | 26.07.2015 152 | Fixed segmentation fault error when closing the application while a HTTP request is in progress. 153 | New HttpListener::close() method to simplifly proper shutdown. 154 | 155 | 1.5.7 156 | 20.07.2015 157 | Fix Qt 5.5 compatibility issue. 158 | 159 | 1.5.6 160 | 22.06.2015 161 | Fixed compilation failes if QT does not support SSL. 162 | 163 | 1.5.5 164 | 16.06.2015 165 | Improved performance of SSL connections. 166 | 167 | 1.5.4 168 | 15.06.2015 169 | Support for Qt versions without OpenSsl. 170 | 171 | 1.5.3 172 | 22.05.2015 173 | Fixed Windows issue: QsslSocket cannot be closed from other threads than it was created in. 174 | 175 | 1.5.2 176 | 12.05.2015 177 | Fixed Windows issue: QSslSocket cannot send signals to another thread than it was created in. 178 | 179 | 1.5.1 180 | 14.04.2015 181 | Add support for pipelining. 182 | 183 | 1.5.0 184 | 03.04.2015 185 | Add support for HTTPS. 186 | 187 | 1.4.2 188 | 03.04.2015 189 | Fixed HTTP request did not work if it was split into multipe IP packages. 190 | 191 | 1.4.1 192 | 20.03.2015 193 | Fixed session cookie expires while the user is active, expiration time was not prolonged on each request. 194 | 195 | 1.4.0 196 | 14.03.2015 197 | This release has a new directory structure and new project files to support the creation of a shared library (*.dll or *.so). 198 | 199 | 1.3.8 200 | 12.03.2015 201 | Improved shutdown procedure. 202 | New config setting "host" which binds the listener to a specific network interface. 203 | 204 | 1.3.7 205 | 14.01.2015 206 | Fixed setting maxMultiPartSize worked only with file-uploads but not with form-data. 207 | 208 | 1.3.6 209 | 16.09.2014 210 | Fixed DualFileLogger produces no output. 211 | 212 | 1.3.5 213 | 11.06.2014 214 | Fixed a multi-threading issue with race condition in StaticFileController. 215 | 216 | 1.3.4 217 | 04.06.2014 218 | Fixed wrong content type when the StaticFileController returns a cached index.html. 219 | 220 | 1.3.3 221 | 17.03.2014 222 | Improved security of StaticFileController by denying "/.." in any position of the request path. 223 | Improved performance of StaticFileController a little. 224 | New convenience method HttpResponse::redirect(url). 225 | Fixed a missing return statement in StaticFileController. 226 | 227 | 1.3.2 228 | 08.01.2014 229 | Fixed HTTP Server ignoring URL parameters when the request contains POST parameters. 230 | 231 | 1.3.1 232 | 15.08.2013 233 | Fixed HTTP server not accepting connections on 64bit OS with QT 5. 234 | 235 | 1.3.0 236 | 20.04.2013 237 | Updated for compatibility QT 5. You may still use QT 4.7 or 4.8, if you like. 238 | Also added support for logging source file name, line number and function name. 239 | 240 | 1.2.13 241 | 03.03.2013 242 | Fixed Logger writing wrong timestamp for buffered messages. 243 | Improved shutdown procedure. The webserver now processes all final signals before the destructor finishes. 244 | 245 | 1.2.12 246 | 01.03.2013 247 | Fixed HttpResponse sending first part of data repeatedly when the amount of data is larger than the available memory for I/O buffer. 248 | 249 | 1.2.11 250 | 06.01.2013 251 | 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). 252 | 253 | 1.2.10 254 | 18.12.2012 255 | Reduced memory usage of HttpResponse in case of large response. 256 | 257 | 1.2.9 258 | 29.07.2012 259 | Added a mutex to HttpConnectionHandlerPool to fix a concurrency issue when a pooled object gets taken from the cache while it times out. 260 | 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. 261 | 262 | 1.2.8 263 | 22.07.2012 264 | Fixed a possible concurrency issue when the file cache is so small that it stores less files than the number of threads. 265 | 266 | 1.2.7 267 | 18.07.2012 268 | Fixed HttpRequest ignores additional URL parameters of POST requests. 269 | Fixed HttpRequest ignores POST parameters of body if there is no Content-Type header. 270 | Removed unused tempdir variable from HttpRequest. 271 | Added mutex to cache of StaticFileController to prevent concurrency problems. 272 | Removed HTTP response with status 408 after read timeout. Connection gets simply closed now. 273 | 274 | 1.2.6 275 | 29.06.2012 276 | Fixed a compilation error on 64 bit if super verbose debugging is enabled. 277 | Fixed a typo in static file controller related to the document type header. 278 | 279 | 1.2.5 280 | 27.06.2012 281 | Fixed error message "QThread: Destroyed while thread is still running" during program termination. 282 | 283 | 1.2.4 284 | 02.06.2012 285 | Fixed template engine skipping variable tokens when a value is shorter than the token. 286 | 287 | 1.2.3 288 | 26.12.2011 289 | Fixed null pointer error when the HTTP server aborts a request that is too large. 290 | 291 | 1.2.2 292 | 06.11.2011 293 | Fixed compilation error on 64 bit platforms. 294 | 295 | 1.2.1 296 | 22.10.2011 297 | Fixed a multi-threading bug in HttpConnectionHandler. 298 | 299 | 1.2.0 300 | 05.12.2010 301 | Added a controller that serves static files, with cacheing. 302 | 303 | 304 | 1.1.0 305 | 19.10.2010 306 | Added support for sessions. 307 | Separated the base classes into individual libraries. 308 | 309 | 1.0.0 310 | 17.10.2010 311 | First release 312 | -------------------------------------------------------------------------------- /Demo1/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.11) 2 | project("QtWebApp Demo 1") 3 | 4 | set(CMAKE_AUTOMOC ON) 5 | 6 | find_package(QtWebApp REQUIRED COMPONENTS HttpServer Logging TemplateEngine) 7 | 8 | add_executable(demo1 9 | controller/dumpcontroller.cpp 10 | controller/dumpcontroller.h 11 | controller/fileuploadcontroller.cpp 12 | controller/fileuploadcontroller.h 13 | controller/formcontroller.cpp 14 | controller/formcontroller.h 15 | controller/logincontroller.cpp 16 | controller/logincontroller.h 17 | controller/sessioncontroller.cpp 18 | controller/sessioncontroller.h 19 | controller/templatecontroller.cpp 20 | controller/templatecontroller.h 21 | documentcache.h 22 | global.cpp 23 | global.h 24 | main.cpp 25 | requestmapper.cpp 26 | requestmapper.h 27 | ) 28 | target_link_libraries(demo1 ${QtWebApp_LIBRARIES}) 29 | -------------------------------------------------------------------------------- /Demo1/controller/dumpcontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "dumpcontroller.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace qtwebapp; 13 | 14 | DumpController::DumpController() {} 15 | 16 | void DumpController::service(HttpRequest &request, HttpResponse &response) { 17 | 18 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 19 | response.setCookie(HttpCookie("firstCookie", "hello", 600, QByteArray(), QByteArray(), QByteArray(), false, true)); 20 | response.setCookie(HttpCookie("secondCookie", "world", 600)); 21 | 22 | QByteArray body(""); 23 | body.append("Request:"); 24 | body.append("
Method: "); 25 | body.append(request.getMethod()); 26 | body.append("
Path: "); 27 | body.append(request.getPath()); 28 | body.append("
Version: "); 29 | body.append(request.getVersion()); 30 | 31 | body.append("

Headers:"); 32 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 33 | QMultiMapIterator i(request.getHeaderMap()); 34 | #else 35 | QMapIterator i(request.getHeaderMap()); 36 | #endif 37 | while (i.hasNext()) { 38 | i.next(); 39 | body.append("
"); 40 | body.append(i.key()); 41 | body.append("="); 42 | body.append(i.value()); 43 | } 44 | 45 | body.append("

Parameters:"); 46 | 47 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 48 | i = QMultiMapIterator(request.getParameterMap()); 49 | #else 50 | i = QMapIterator(request.getParameterMap()); 51 | #endif 52 | while (i.hasNext()) { 53 | i.next(); 54 | body.append("
"); 55 | body.append(i.key()); 56 | body.append("="); 57 | body.append(i.value()); 58 | } 59 | 60 | body.append("

Cookies:"); 61 | QMapIterator i2 = QMapIterator(request.getCookieMap()); 62 | while (i2.hasNext()) { 63 | i2.next(); 64 | body.append("
"); 65 | body.append(i2.key()); 66 | body.append("="); 67 | body.append(i2.value()); 68 | } 69 | 70 | body.append("

Body:
"); 71 | body.append(request.getBody()); 72 | 73 | body.append(""); 74 | response.write(body, true); 75 | } 76 | -------------------------------------------------------------------------------- /Demo1/controller/dumpcontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef DUMPCONTROLLER_H 7 | #define DUMPCONTROLLER_H 8 | 9 | #include "httprequest.h" 10 | #include "httprequesthandler.h" 11 | #include "httpresponse.h" 12 | 13 | /** 14 | This controller dumps the received HTTP request in the response. 15 | */ 16 | 17 | class DumpController : public qtwebapp::HttpRequestHandler { 18 | Q_OBJECT 19 | Q_DISABLE_COPY(DumpController) 20 | public: 21 | /** Constructor */ 22 | DumpController(); 23 | 24 | /** Generates the response */ 25 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 26 | }; 27 | 28 | #endif // DUMPCONTROLLER_H 29 | -------------------------------------------------------------------------------- /Demo1/controller/fileuploadcontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "fileuploadcontroller.h" 7 | 8 | using namespace qtwebapp; 9 | 10 | FileUploadController::FileUploadController() {} 11 | 12 | void FileUploadController::service(HttpRequest &request, HttpResponse &response) { 13 | 14 | if (request.getParameter("action") == "show") { 15 | response.setHeader("Content-Type", "image/jpeg"); 16 | QFile *file = request.getUploadedFile("file1"); 17 | if (file) { 18 | while (!file->atEnd() && !file->error()) { 19 | QByteArray buffer = file->read(65536); 20 | response.write(buffer); 21 | } 22 | } else { 23 | response.write("upload failed"); 24 | } 25 | } 26 | 27 | else { 28 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 29 | response.write(""); 30 | response.write("Upload a JPEG image file

"); 31 | response.write("

"); 32 | response.write(" "); 33 | response.write(" File:
"); 34 | response.write(" "); 35 | response.write("
"); 36 | response.write("", true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo1/controller/fileuploadcontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef FILEUPLOADCONTROLLER_H 7 | #define FILEUPLOADCONTROLLER_H 8 | 9 | #include "httprequest.h" 10 | #include "httprequesthandler.h" 11 | #include "httpresponse.h" 12 | 13 | /** 14 | This controller displays a HTML form for file upload and recieved the file. 15 | */ 16 | 17 | class FileUploadController : public qtwebapp::HttpRequestHandler { 18 | Q_OBJECT 19 | Q_DISABLE_COPY(FileUploadController) 20 | public: 21 | /** Constructor */ 22 | FileUploadController(); 23 | 24 | /** Generates the response */ 25 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 26 | }; 27 | 28 | #endif // FILEUPLOADCONTROLLER_H 29 | -------------------------------------------------------------------------------- /Demo1/controller/formcontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "formcontroller.h" 7 | 8 | using namespace qtwebapp; 9 | 10 | FormController::FormController() {} 11 | 12 | void FormController::service(HttpRequest &request, HttpResponse &response) { 13 | 14 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 15 | 16 | if (request.getParameter("action") == "show") { 17 | response.write(""); 18 | response.write("Name = "); 19 | response.write(request.getParameter("name")); 20 | response.write("
City = "); 21 | response.write(request.getParameter("city")); 22 | response.write("", true); 23 | } else { 24 | response.write(""); 25 | response.write("
"); 26 | response.write(" "); 27 | response.write(" Name:
"); 28 | response.write(" City:
"); 29 | response.write(" "); 30 | response.write("
"); 31 | response.write("", true); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Demo1/controller/formcontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef FORMCONTROLLER_H 7 | #define FORMCONTROLLER_H 8 | 9 | #include "httprequest.h" 10 | #include "httprequesthandler.h" 11 | #include "httpresponse.h" 12 | 13 | /** 14 | This controller displays a HTML form and dumps the submitted input. 15 | */ 16 | 17 | class FormController : public qtwebapp::HttpRequestHandler { 18 | Q_OBJECT 19 | Q_DISABLE_COPY(FormController) 20 | public: 21 | /** Constructor */ 22 | FormController(); 23 | 24 | /** Generates the response */ 25 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 26 | }; 27 | 28 | #endif // FORMCONTROLLER_H 29 | -------------------------------------------------------------------------------- /Demo1/controller/logincontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "logincontroller.h" 7 | 8 | #include "../global.h" 9 | 10 | #include 11 | 12 | using namespace qtwebapp; 13 | 14 | LoginController::LoginController() {} 15 | 16 | void LoginController::service(HttpRequest &request, HttpResponse &response) { 17 | QByteArray auth = request.getHeader("Authorization"); 18 | if (auth.isNull()) { 19 | qInfo("User is not logged in"); 20 | response.setStatus(401, "Unauthorized"); 21 | response.setHeader("WWW-Authenticate", "Basic realm=Please login with any name and password"); 22 | } else { 23 | QByteArray decoded = QByteArray::fromBase64(auth.mid(6)); // Skip the first 6 characters ("Basic ") 24 | qInfo("Authorization request from %s", qPrintable(decoded)); 25 | QList parts = decoded.split(':'); 26 | QByteArray name = parts[0]; 27 | QByteArray password = parts[1]; 28 | 29 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 30 | 31 | response.write(""); 32 | response.write("You logged in as name="); 33 | response.write(name); 34 | response.write("with password="); 35 | response.write(password); 36 | response.write("", true); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo1/controller/logincontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef LOGINCONTROLLER_H 7 | #define LOGINCONTROLLER_H 8 | 9 | #include "httprequest.h" 10 | #include "httprequesthandler.h" 11 | #include "httpresponse.h" 12 | 13 | /** 14 | This controller demonstrates how to use HTTP basic login. 15 | */ 16 | 17 | class LoginController : public qtwebapp::HttpRequestHandler { 18 | Q_OBJECT 19 | Q_DISABLE_COPY(LoginController) 20 | public: 21 | /** Constructor */ 22 | LoginController(); 23 | 24 | /** Generates the response */ 25 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 26 | }; 27 | 28 | #endif // LOGINCONTROLLER_H 29 | -------------------------------------------------------------------------------- /Demo1/controller/sessioncontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "sessioncontroller.h" 7 | 8 | #include "../global.h" 9 | #include "httpsessionstore.h" 10 | 11 | #include 12 | 13 | using namespace qtwebapp; 14 | 15 | SessionController::SessionController() {} 16 | 17 | void SessionController::service(HttpRequest &request, HttpResponse &response) { 18 | 19 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 20 | 21 | // Get current session, or create a new one 22 | HttpSession session = sessionStore->getSession(request, response); 23 | if (!session.contains("startTime")) { 24 | response.write("New session started. Reload this page now."); 25 | session.set("startTime", QDateTime::currentDateTime()); 26 | } else { 27 | QDateTime startTime = session.get("startTime").toDateTime(); 28 | response.write("Your session started "); 29 | response.write(startTime.toString().toUtf8()); 30 | response.write(""); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Demo1/controller/sessioncontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef SESSIONCONTROLLER_H 7 | #define SESSIONCONTROLLER_H 8 | 9 | #include "httprequest.h" 10 | #include "httprequesthandler.h" 11 | #include "httpresponse.h" 12 | 13 | /** 14 | This controller demonstrates how to use sessions. 15 | */ 16 | 17 | class SessionController : public qtwebapp::HttpRequestHandler { 18 | Q_OBJECT 19 | Q_DISABLE_COPY(SessionController) 20 | public: 21 | /** Constructor */ 22 | SessionController(); 23 | 24 | /** Generates the response */ 25 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 26 | }; 27 | 28 | #endif // SESSIONCONTROLLER_H 29 | -------------------------------------------------------------------------------- /Demo1/controller/templatecontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "templatecontroller.h" 7 | 8 | #include "../global.h" 9 | #include "template.h" 10 | #include "templatecache.h" 11 | 12 | using namespace qtwebapp; 13 | 14 | TemplateController::TemplateController() {} 15 | 16 | void TemplateController::service(HttpRequest &request, HttpResponse &response) { 17 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 18 | 19 | Template t = templateCache->getTemplate("demo", request.getHeader("Accept-Language")); 20 | t.enableWarnings(); 21 | t.setVariable("path", request.getPath()); 22 | 23 | QMultiMap headers = request.getHeaderMap(); 24 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 25 | QMultiMapIterator iterator(headers); 26 | #else 27 | QMapIterator iterator(headers); 28 | #endif 29 | 30 | t.loop("header", headers.size()); 31 | int i = 0; 32 | while (iterator.hasNext()) { 33 | iterator.next(); 34 | t.setVariable(QString("header%1.name").arg(i), QString(iterator.key())); 35 | t.setVariable(QString("header%1.value").arg(i), QString(iterator.value())); 36 | ++i; 37 | } 38 | 39 | response.write(t.toUtf8(), true); 40 | } 41 | -------------------------------------------------------------------------------- /Demo1/controller/templatecontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef TEMPLATECONTROLLER_H 7 | #define TEMPLATECONTROLLER_H 8 | 9 | #include "httprequest.h" 10 | #include "httprequesthandler.h" 11 | #include "httpresponse.h" 12 | 13 | /** 14 | This controller generates a website using the template engine. 15 | It generates a Latin1 (ISO-8859-1) encoded website from a UTF-8 encoded template file. 16 | */ 17 | 18 | class TemplateController : public qtwebapp::HttpRequestHandler { 19 | Q_OBJECT 20 | Q_DISABLE_COPY(TemplateController) 21 | public: 22 | /** Constructor */ 23 | TemplateController(); 24 | 25 | /** Generates the response */ 26 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 27 | }; 28 | 29 | #endif // TEMPLATECONTROLLER_H 30 | -------------------------------------------------------------------------------- /Demo1/documentcache.h: -------------------------------------------------------------------------------- 1 | #ifndef DOCUMENTCACHE_H 2 | #define DOCUMENTCACHE_H 3 | 4 | #endif // DOCUMENTCACHE_H 5 | -------------------------------------------------------------------------------- /Demo1/etc/Demo1.ini: -------------------------------------------------------------------------------- 1 | [listener] 2 | ;host=192.168.0.100 3 | port=8080 4 | minThreads=4 5 | maxThreads=100 6 | cleanupInterval=60000 7 | readTimeout=60000 8 | maxRequestSize=16000 9 | maxMultiPartSize=10000000 10 | ;sslKeyFile=ssl/my.key 11 | ;sslCertFile=ssl/my.cert 12 | 13 | [templates] 14 | path=templates 15 | suffix=.tpl 16 | encoding=UTF-8 17 | cacheSize=1000000 18 | cacheTime=60000 19 | 20 | [docroot] 21 | path=docroot 22 | encoding=UTF-8 23 | maxAge=60000 24 | cacheTime=60000 25 | cacheSize=1000000 26 | maxCachedFileSize=65536 27 | 28 | [sessions] 29 | expirationTime=3600000 30 | cookieName=sessionid 31 | cookiePath=/ 32 | cookieComment=Identifies the user 33 | ;cookieDomain=stefanfrings.de 34 | 35 | [logging] 36 | ; The logging settings become effective after you comment in the related lines of code in main.cpp. 37 | fileName=../logs/demo1.log 38 | minLevel=WARNING 39 | bufferSize=100 40 | maxSize=1000000 41 | maxBackups=2 42 | timestampFormat=dd.MM.yyyy hh:mm:ss.zzz 43 | msgFormat={timestamp} {typeNr} {type} {thread} {msg} 44 | ; QT5 supports: msgFormat={timestamp} {typeNr} {type} {thread} {msg}\n in {file} line {line} function {function} 45 | -------------------------------------------------------------------------------- /Demo1/etc/docroot/Schmetterling klein.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msrd0/QtWebApp/5b59e6129886f5335f6128cad852eaf2a20c5ec0/Demo1/etc/docroot/Schmetterling klein.png -------------------------------------------------------------------------------- /Demo1/etc/docroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Try one of the following examples: 4 |

5 |

13 | 14 | -------------------------------------------------------------------------------- /Demo1/etc/ssl/my.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDhTCCAm2gAwIBAgIJALk6oIpWZQI9MA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0xNTA0MDMxOTU1 5 | NTJaFw0xNjA0MDIxOTU1NTJaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21l 6 | LVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV 7 | BAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM9s 8 | 4rNgfasL/dTCQZVv6mtjsa7siGrmwsfZ8LNb+JG0SVfHWHTmE/Gm+BayNoupDpbv 9 | R3gaD6VjSZdzNqClt+7LGhjBYr4wHyDcbnzkgd03ARfN+IFKQp9yYEUbO7SvKrD+ 10 | rg0HLP96fsDPDAWVBrZ9tTpa+cU+H1Ea7BKmmcGuZbY6K3uMG7BDKqbEXOpuxdJ3 11 | TbaGgDtH5qPpJXEPUQgkwRkPoDyU1GYvOw+FFpAGP9MmyxofH3/CyaKfvrowcJW0 12 | tRAojfNXIxM9HTZCtuhB3icCY0895/X0KQCUGRWoS01ZatAV08TfwemWJ43EC6e5 13 | 8nPlwsjIxoYmUIFZm2cCAwEAAaNQME4wHQYDVR0OBBYEFMEJGRIr2QHEV2LThyxt 14 | x9Lh6GfIMB8GA1UdIwQYMBaAFMEJGRIr2QHEV2LThyxtx9Lh6GfIMAwGA1UdEwQF 15 | MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFslVIHPxQ5Fxw/gLZpeOaO7T6nV+87B 16 | tSCxfFlrDzivu1ZBd12yK0BNtwVLG9lvyQ66gRInFXrsEi4pWEGWn0pmufPlEuSQ 17 | OVjvPJi/ejdnFwQFY4PhHLbuXtZkBX8TCpq4wlNJWe+xdXdDnBM+chGrSp1KJW0J 18 | nr+lHw7oK0kG6ohZYz2i1qEoBU7NNuU5GrkVt7QMg23xFbLb/la+eXMGX9XPAGRb 19 | Dx32QWQDwDFdjQMVLeKtSWFGXT2LiPyry1CXo7beE5Ur617cbjeHW5ecyZ1yo3bJ 20 | F/jO238OjTAxnaY7PGtpKUMrIkyLaMxtGa8/DTaTkSdlgYRK8znVLes= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /Demo1/etc/ssl/my.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDPbOKzYH2rC/3U 3 | wkGVb+prY7Gu7Ihq5sLH2fCzW/iRtElXx1h05hPxpvgWsjaLqQ6W70d4Gg+lY0mX 4 | czagpbfuyxoYwWK+MB8g3G585IHdNwEXzfiBSkKfcmBFGzu0ryqw/q4NByz/en7A 5 | zwwFlQa2fbU6WvnFPh9RGuwSppnBrmW2Oit7jBuwQyqmxFzqbsXSd022hoA7R+aj 6 | 6SVxD1EIJMEZD6A8lNRmLzsPhRaQBj/TJssaHx9/wsmin766MHCVtLUQKI3zVyMT 7 | PR02QrboQd4nAmNPPef19CkAlBkVqEtNWWrQFdPE38HplieNxAunufJz5cLIyMaG 8 | JlCBWZtnAgMBAAECggEAEQsoYYbXLJvSoo3Hp8jHYUWSzwW+e9I0RQmiXop7FTXU 9 | JPNLUGerUdrfXbYNB1K/0SUeIT868+MBpAEihva281ca8NfLDkeT9zZFmduI1C11 10 | 2IfjGHSKnuFeCa+gK0uNXXe/w3BIgUGGcr9QhJDnOU2A+6eICG3/UsG8jhfEYIaX 11 | EpqCr7PT7ZnieN+ANr5P394+c8hgeQwFLzKV8W97IyTdJLEh5YBUgVZQ0Oe3WO8i 12 | JPqkWNyPfZ14l+bEuCTbHBq17KJ06WK0/kEJYkKONVZLHxgesEOPhYtNNW081lQ+ 13 | iE7B8uDwxSuAYGbHx9uCmJg1JJxfCfdHutffyEZPSQKBgQDq7KBkUtDxBuM5jk1r 14 | LySkJBg+95nJLnbSvAAdxjErElsFiVwjHWIK4qbxPdSNBpXiq9MzvlFh5p1hIq+d 15 | ki5jDEg3HY23tnbd6gcdbDsimIYQu6hTrZEUyRA10JIGs5h2GWYvnR7fFoBLaCf3 16 | 0ExZcb35tP/ihzknt+XsvtrF9QKBgQDiCLSTkZQngO12mQ9mMRE+uBx8tAvXW1QV 17 | 5Gc9k4F3degk7g3JJ6ZOyGDGmYUrVI1b9bSakT4PQz5/xsEttfslyKjpzF1v/Of0 18 | G6koLwGdWVw57vojAmfzTpq0EWtDm2P7+AlfWKsxz8e9DwsCsJ46pOc1mtauFf/s 19 | SgP8HdymawKBgQCy7PSgVH81BFTuElceyrIynhclZjnoA46WpH/GLZxnPLy8s8jM 20 | KeXxen1CAiCwJJLmKI5cu5iKYzX8tIljDuJrqAAcklLM2JkiHcu7eiJ0EA0UE4l5 21 | 6hk05oqFIk0F86WsVV4B576KlT2zsSm30htEiJ/z3wqBgxJiEOt67EX+PQKBgEOV 22 | 7sihIvec6WQo6B3aVhm2222+ODwwprfVD7mtvWyG6N8B/0VCgcvUxnsbtGH8ajgx 23 | A8uj2jaeGLYELAcK/wmRMlCWNuohaQnoq2/EfmsaKSV5e3m0Ynix7DgjbTtN31uc 24 | EtFbpfTC+CpjsRsgzG8kl63DmqV7FocEqqRcGXn5AoGBANLtQUtIZbVIH2Uxz3Fq 25 | SWHvkMmU5hOJj3adYNhiQ9b/J4Znonp+vGHMf31A+F7glCXMfo9k4+LyfjJplTNh 26 | K4YSDJQxhaXOLNdql/3tYEjR5PFyNdQU9MVePOAwq4sxnv39MHm3Mb6fAZFurh6c 27 | 1GAHkUPZFGPAiXzX2Hbg39sD 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /Demo1/etc/templates/demo-de.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hallo,
4 | du hast folgenden Pfad angefordert: {path} 5 |

6 | Und dein Web Browser hat folgende Kopfzeilen geliefert: 7 |

8 | {loop header} 9 | {header.name}: {header.value}
10 | {end header} 11 | 12 | 13 | -------------------------------------------------------------------------------- /Demo1/etc/templates/demo.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello,
4 | you requested the path: {path} 5 |

6 | And your web browser provided the following headers: 7 |

8 | {loop header} 9 | {header.name}: {header.value}
10 | {end header} 11 | 12 | 13 | -------------------------------------------------------------------------------- /Demo1/global.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "global.h" 7 | 8 | using namespace qtwebapp; 9 | 10 | TemplateCache *templateCache; 11 | HttpSessionStore *sessionStore; 12 | StaticFileController *staticFileController; 13 | FileLogger *logger; 14 | -------------------------------------------------------------------------------- /Demo1/global.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef GLOBAL_H 7 | #define GLOBAL_H 8 | 9 | #include "filelogger.h" 10 | #include "httpsessionstore.h" 11 | #include "staticfilecontroller.h" 12 | #include "templatecache.h" 13 | 14 | /** 15 | Global objects that are shared by multiple source files 16 | of this project. 17 | */ 18 | 19 | /** Cache for template files */ 20 | extern qtwebapp::TemplateCache *templateCache; 21 | 22 | /** Storage for session cookies */ 23 | extern qtwebapp::HttpSessionStore *sessionStore; 24 | 25 | /** Controller for static files */ 26 | extern qtwebapp::StaticFileController *staticFileController; 27 | 28 | /** Redirects log messages to a file */ 29 | extern qtwebapp::FileLogger *logger; 30 | 31 | #endif // GLOBAL_H 32 | -------------------------------------------------------------------------------- /Demo1/logs/for_log_files.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msrd0/QtWebApp/5b59e6129886f5335f6128cad852eaf2a20c5ec0/Demo1/logs/for_log_files.txt -------------------------------------------------------------------------------- /Demo1/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "global.h" 7 | #include "httplistener.h" 8 | #include "requestmapper.h" 9 | 10 | #include 11 | #include 12 | 13 | using namespace qtwebapp; 14 | 15 | /** Search the configuration file */ 16 | QString searchConfigFile() { 17 | QString binDir = QCoreApplication::applicationDirPath(); 18 | QString fileName("Demo1.ini"); 19 | 20 | QStringList searchList; 21 | searchList.append(binDir); 22 | searchList.append(binDir + "/etc"); 23 | searchList.append(binDir + "/../etc"); 24 | searchList.append(binDir + "/../Demo1/etc"); // for development with shadow build (Linux) 25 | searchList.append(binDir + "/../../Demo1/etc"); // for development with shadow build (Windows) 26 | searchList.append(QDir::rootPath() + "etc/opt"); 27 | searchList.append(QDir::rootPath() + "etc"); 28 | 29 | foreach (QString dir, searchList) { 30 | QFile file(dir + "/" + fileName); 31 | if (file.exists()) { 32 | fileName = QDir(file.fileName()).canonicalPath(); 33 | qDebug("Using config file %s", qPrintable(fileName)); 34 | return fileName; 35 | } 36 | } 37 | 38 | // not found 39 | foreach (QString dir, searchList) { 40 | qWarning("%s/%s not found", qPrintable(dir), qPrintable(fileName)); 41 | } 42 | qFatal("Cannot find config file %s", qPrintable(fileName)); 43 | return nullptr; 44 | } 45 | 46 | /** 47 | Entry point of the program. 48 | */ 49 | int main(int argc, char *argv[]) { 50 | QCoreApplication app(argc, argv); 51 | app.setApplicationName("Demo1"); 52 | 53 | // Find the configuration file 54 | QString configFileName = searchConfigFile(); 55 | 56 | // Configure logging into a file 57 | QSettings *logSettings = new QSettings(configFileName, QSettings::IniFormat, &app); 58 | logSettings->beginGroup("logging"); 59 | FileLogger *logger = new FileLogger(logSettings, 10000, &app); 60 | logger->installMsgHandler(); 61 | 62 | // Configure template loader and cache 63 | QSettings *templateSettings = new QSettings(configFileName, QSettings::IniFormat, &app); 64 | templateSettings->beginGroup("templates"); 65 | templateCache = new TemplateCache(templateSettings, &app); 66 | 67 | // Configure session store 68 | QSettings *sessionSettings = new QSettings(configFileName, QSettings::IniFormat, &app); 69 | sessionSettings->beginGroup("sessions"); 70 | sessionStore = new HttpSessionStore(sessionSettings, &app); 71 | 72 | // Configure static file controller 73 | QSettings *fileSettings = new QSettings(configFileName, QSettings::IniFormat, &app); 74 | fileSettings->beginGroup("docroot"); 75 | staticFileController = new StaticFileController(fileSettings, &app); 76 | 77 | // Configure and start the TCP listener 78 | QSettings *listenerSettings = new QSettings(configFileName, QSettings::IniFormat, &app); 79 | listenerSettings->beginGroup("listener"); 80 | new HttpListener(listenerSettings, new RequestMapper(&app), &app); 81 | 82 | qInfo("Application has started"); 83 | app.exec(); 84 | qInfo("Application has stopped"); 85 | } 86 | -------------------------------------------------------------------------------- /Demo1/requestmapper.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "requestmapper.h" 7 | 8 | #include "controller/dumpcontroller.h" 9 | #include "controller/fileuploadcontroller.h" 10 | #include "controller/formcontroller.h" 11 | #include "controller/logincontroller.h" 12 | #include "controller/sessioncontroller.h" 13 | #include "controller/templatecontroller.h" 14 | #include "filelogger.h" 15 | #include "global.h" 16 | #include "staticfilecontroller.h" 17 | 18 | #include 19 | 20 | using namespace qtwebapp; 21 | 22 | RequestMapper::RequestMapper(QObject *parent) : HttpRequestHandler(parent) { 23 | qDebug("RequestMapper: created"); 24 | } 25 | 26 | RequestMapper::~RequestMapper() { 27 | qDebug("RequestMapper: deleted"); 28 | } 29 | 30 | void RequestMapper::service(HttpRequest &request, HttpResponse &response) { 31 | QByteArray path = request.getPath(); 32 | qDebug("RequestMapper: path=%s", path.data()); 33 | 34 | // For the following pathes, each request gets its own new instance of the related controller. 35 | 36 | if (path.startsWith("/dump")) { 37 | DumpController().service(request, response); 38 | } 39 | 40 | else if (path.startsWith("/template")) { 41 | TemplateController().service(request, response); 42 | } 43 | 44 | else if (path.startsWith("/form")) { 45 | FormController().service(request, response); 46 | } 47 | 48 | else if (path.startsWith("/file")) { 49 | FileUploadController().service(request, response); 50 | } 51 | 52 | else if (path.startsWith("/session")) { 53 | SessionController().service(request, response); 54 | } 55 | 56 | else if (path.startsWith("/login")) { 57 | LoginController().service(request, response); 58 | } 59 | 60 | // All other pathes are mapped to the static file controller. 61 | // In this case, a single instance is used for multiple requests. 62 | else { 63 | staticFileController->service(request, response); 64 | } 65 | 66 | qDebug("RequestMapper: finished request"); 67 | 68 | // Clear the log buffer 69 | if (logger) { 70 | logger->clear(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Demo1/requestmapper.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef REQUESTMAPPER_H 7 | #define REQUESTMAPPER_H 8 | 9 | #include "httprequesthandler.h" 10 | 11 | /** 12 | The request mapper dispatches incoming HTTP requests to controller classes 13 | depending on the requested path. 14 | */ 15 | 16 | class RequestMapper : public qtwebapp::HttpRequestHandler { 17 | Q_OBJECT 18 | Q_DISABLE_COPY(RequestMapper) 19 | public: 20 | /** 21 | Constructor. 22 | @param parent Parent object 23 | */ 24 | RequestMapper(QObject *parent = 0); 25 | 26 | /** 27 | Destructor. 28 | */ 29 | ~RequestMapper(); 30 | 31 | /** 32 | Dispatch incoming HTTP requests to different controllers depending on the URL. 33 | @param request The received HTTP request 34 | @param response Must be used to return the response 35 | */ 36 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 37 | }; 38 | 39 | #endif // REQUESTMAPPER_H 40 | -------------------------------------------------------------------------------- /Demo2/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 2.8.11) 2 | project("QtWebApp Demo 2") 3 | 4 | set(CMAKE_AUTOMOC ON) 5 | 6 | find_package(QtWebApp REQUIRED COMPONENTS HttpServer Logging) 7 | 8 | add_executable(demo2 9 | main.cpp 10 | requesthandler.cpp 11 | requesthandler.h 12 | ) 13 | target_link_libraries(demo2 ${QtWebApp_LIBRARIES}) 14 | -------------------------------------------------------------------------------- /Demo2/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "requesthandler.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | using namespace qtwebapp; 16 | 17 | /** 18 | Entry point of the program. 19 | */ 20 | int main(int argc, char *argv[]) { 21 | 22 | // Initialize the core application 23 | QCoreApplication app(argc, argv); 24 | app.setApplicationName("Demo2"); 25 | 26 | // Configure and start the TCP listener 27 | HttpServerConfig conf; 28 | conf.port = 8080; 29 | conf.minThreads = 4; 30 | conf.maxThreads = 100; 31 | conf.cleanupInterval = 60e3; 32 | conf.readTimeout = 60e3; 33 | conf.maxRequestSize = 16e3; 34 | conf.maxMultipartSize = 10e6; 35 | new HttpListener(conf, new RequestHandler(&app), &app); 36 | 37 | qWarning("Application has started"); 38 | app.exec(); 39 | qWarning("Application has stopped"); 40 | } 41 | -------------------------------------------------------------------------------- /Demo2/requesthandler.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "requesthandler.h" 7 | 8 | #include 9 | 10 | using namespace qtwebapp; 11 | 12 | RequestHandler::RequestHandler(QObject *parent) : HttpRequestHandler(parent) { 13 | qDebug("RequestHandler: created"); 14 | } 15 | 16 | RequestHandler::~RequestHandler() { 17 | qDebug("RequestHandler: deleted"); 18 | } 19 | 20 | void RequestHandler::service(HttpRequest &request, HttpResponse &response) { 21 | QByteArray path = request.getPath(); 22 | qDebug("Conroller: path=%s", path.data()); 23 | 24 | // Set a response header 25 | response.setHeader("Content-Type", "text/html; charset=ISO-8859-1"); 26 | 27 | // Return a simple HTML document 28 | response.write("Hello World!", true); 29 | 30 | qDebug("RequestHandler: finished request"); 31 | } 32 | -------------------------------------------------------------------------------- /Demo2/requesthandler.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #ifndef REQUESTHANDLER_H 7 | #define REQUESTHANDLER_H 8 | 9 | #include 10 | 11 | /** 12 | The request handler receives incoming HTTP requests and generates responses. 13 | */ 14 | 15 | class RequestHandler : public qtwebapp::HttpRequestHandler { 16 | Q_OBJECT 17 | Q_DISABLE_COPY(RequestHandler) 18 | public: 19 | /** 20 | Constructor. 21 | @param parent Parent object 22 | */ 23 | RequestHandler(QObject *parent = 0); 24 | 25 | /** 26 | Destructor 27 | */ 28 | ~RequestHandler(); 29 | 30 | /** 31 | Process an incoming HTTP request. 32 | @param request The received HTTP request 33 | @param response Must be used to return the response 34 | */ 35 | void service(qtwebapp::HttpRequest &request, qtwebapp::HttpResponse &response); 36 | }; 37 | 38 | #endif // REQUESTHANDLER_H 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /QtWebApp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.1) 2 | project(QtWebApp CXX) 3 | 4 | set(qtwebapp_MAJOR 1) 5 | set(qtwebapp_MINOR 8) 6 | set(qtwebapp_PATCH 3) 7 | set(qtwebapp_VERSION ${qtwebapp_MAJOR}.${qtwebapp_MINOR}.${qtwebapp_PATCH}) 8 | 9 | find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED HINTS $ENV{Qt6_DIR} $ENV{Qt5_DIR}) 10 | find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core Network REQUIRED) 11 | 12 | if (Qt6_FOUND) 13 | find_package(Qt6 COMPONENTS Core5Compat REQUIRED) 14 | endif() 15 | 16 | set(CMAKE_AUTOMOC ON) 17 | 18 | add_definitions(-DQTWEBAPPLIB_EXPORT) 19 | 20 | set(CMAKE_CXX_STANDARD 11) 21 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 22 | 23 | if(NOT MSVC) 24 | set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=format -Werror=return-type") 25 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Werror=format -Werror=return-type") 26 | endif() 27 | 28 | if(CMAKE_BUILD_TYPE MATCHES Debug) 29 | if(MSVC) 30 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall") 31 | else() 32 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -fsanitize=undefined") 33 | set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=undefined") 34 | endif() 35 | add_definitions("-D_GLIBCXX_DEBUG") 36 | add_definitions("-DQT_SHAREDPOINTER_TRACK_POINTERS") 37 | add_definitions("-DCMAKE_DEBUG") 38 | add_definitions("-DSUPERVERBOSE") 39 | endif() 40 | add_definitions("-DCMAKE_QTWEBAPP_SO") 41 | 42 | configure_file(qtwebappglobal.h.in qtwebappglobal.h @ONLY) 43 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/qtwebappglobal.h 44 | DESTINATION include/qtwebapp/) 45 | include_directories(${CMAKE_CURRENT_BINARY_DIR}) 46 | 47 | add_library(QtWebAppGlobal SHARED qtwebappglobal.cpp) 48 | target_include_directories(QtWebAppGlobal PUBLIC 49 | $ 50 | $ 51 | $ 52 | ) 53 | target_link_libraries(QtWebAppGlobal Qt${QT_VERSION_MAJOR}::Core) 54 | set_target_properties(QtWebAppGlobal PROPERTIES 55 | VERSION ${qtwebapp_VERSION} 56 | SOVERSION ${qtwebapp_MAJOR} 57 | ) 58 | install(TARGETS QtWebAppGlobal 59 | EXPORT QtWebAppTargets 60 | LIBRARY DESTINATION lib 61 | RUNTIME DESTINATION bin 62 | ARCHIVE DESTINATION lib) 63 | 64 | add_subdirectory(logging) 65 | add_subdirectory(httpserver) 66 | add_subdirectory(templateengine) 67 | 68 | configure_file(cmake/QtWebAppConfig.cmake.in QtWebAppConfig.cmake @ONLY) 69 | configure_file(cmake/QtWebAppConfigVersion.cmake.in QtWebAppConfigVersion.cmake @ONLY) 70 | install(FILES ${CMAKE_CURRENT_BINARY_DIR}/QtWebAppConfig.cmake 71 | ${CMAKE_CURRENT_BINARY_DIR}/QtWebAppConfigVersion.cmake 72 | DESTINATION lib/cmake/QtWebApp) 73 | 74 | 75 | install(EXPORT QtWebAppTargets 76 | DESTINATION lib/cmake/QtWebApp) 77 | -------------------------------------------------------------------------------- /QtWebApp/Doxyfile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = QtWebApp 2 | OUTPUT_DIRECTORY = doc 3 | JAVADOC_AUTOBRIEF = YES 4 | QUIET = YES 5 | INPUT = templateengine logging httpserver doc/mainpage.dox 6 | INPUT_ENCODING = UTF-8 7 | RECURSIVE = YES 8 | SOURCE_BROWSER = YES 9 | GENERATE_TREEVIEW = YES 10 | GENERATE_LATEX = NO 11 | -------------------------------------------------------------------------------- /QtWebApp/cmake/QtWebAppConfig.cmake.in: -------------------------------------------------------------------------------- 1 | 2 | include("${CMAKE_CURRENT_LIST_DIR}/QtWebAppConfigVersion.cmake") 3 | include("${CMAKE_CURRENT_LIST_DIR}/QtWebAppTargets.cmake") 4 | 5 | if (NOT QtWebApp_FIND_COMPONENTS) 6 | set(QtWebApp_NOT_FOUND_MESSAGE "The QtWebApp package requires at least one component") 7 | set(QtWebApp_FOUND False) 8 | return() 9 | endif() 10 | 11 | set(_req) 12 | if (QtWebApp_FIND_REQUIRED) 13 | set(_req REQUIRED) 14 | endif() 15 | 16 | # TODO: Qt Network is not required for all modules 17 | find_package(Qt@QT_VERSION_MAJOR@ ${_req} COMPONENTS Core Network) 18 | if (NOT ${Qt@QT_VERSION_MAJOR@_FOUND}) 19 | set(QtWebApp_NOT_FOUND_MESSAGE "Unable to find Qt@QT_VERSION_MAJOR@") 20 | set(QtWebApp_FOUND False) 21 | return() 22 | endif() 23 | if (@QT_VERSION_MAJOR@ EQUAL 6) 24 | find_package(Qt6 COMPONENTS Core5Compat REQUIRED) 25 | endif() 26 | 27 | set(QtWebApp_LIBRARIES Qt@QT_VERSION_MAJOR@::Core Qt@QT_VERSION_MAJOR@::Network) 28 | 29 | set(_comps Global ${QtWebApp_FIND_COMPONENTS}) 30 | 31 | foreach(module ${_comps}) 32 | set(QtWebApp_LIBRARIES ${QtWebApp_LIBRARIES} QtWebApp${module}) 33 | endforeach() 34 | 35 | mark_as_advanced(QtWebApp_LIBRARIES) 36 | -------------------------------------------------------------------------------- /QtWebApp/cmake/QtWebAppConfigVersion.cmake.in: -------------------------------------------------------------------------------- 1 | set(PACKAGE_VERSION @qtwebapp_VERSION@) 2 | 3 | if("@qtwebapp_VERSION@" MATCHES "^([0-9]+\\.[0-9]+)\\.") # only care about major.minor 4 | set(_package_version "${CMAKE_MATCH_1}") 5 | else() 6 | set(_package_version "@qtwebapp_VERSION@") 7 | endif() 8 | 9 | if(PACKAGE_FIND_VERSION MATCHES "^([0-9]+\\.[0-9]+)\\.") # only care about major.minor 10 | set(_requested_version "${CMAKE_MATCH_1}") 11 | else() 12 | set(_requested_version "${PACKAGE_FIND_VERSION}") 13 | endif() 14 | 15 | if(_requested_version STREQUAL _package_version) 16 | set(PACKAGE_VERSION_COMPATIBLE TRUE) 17 | else() 18 | set(PACKAGE_VERSION_COMPATIBLE FALSE) 19 | endif() 20 | 21 | if (PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) 22 | set(PACKAGE_VERSION_EXACT TRUE) 23 | endif() 24 | 25 | # check bitness 26 | if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "" OR "@CMAKE_SIZEOF_VOID_P@" STREQUAL "") 27 | return() 28 | endif() 29 | 30 | if (NOT CMAKE_SIZEOF_VOID_P STREQUAL "@CMAKE_SIZEOF_VOID_P@") 31 | math(EXPR installedBits "@CMAKE_SIZEOF_VOID_P@ * @CMAKE_SIZEOF_VOID_P@") 32 | set(PACKAGE_VERSION "@qtwebapp_VERSION@ (${installedBits}bit)") 33 | set(PACKAGE_VERSION_UNSUITABLE TRUE) 34 | endif() 35 | -------------------------------------------------------------------------------- /QtWebApp/doc/mainpage.dox: -------------------------------------------------------------------------------- 1 | /** 2 | @mainpage 3 | @author Stefan Frings 4 | QtWebApp supports you in writing server-side web application in C++ 5 | based on the Qt framework. It is a light-weight implementation inspired 6 | by Java Servlets. 7 |

8 | Features: 9 | 10 | - qtwebapp::HttpListener is a HTTP 1.1 web server with support for 11 | - HTTPS 12 | - IP v4 and IP v6 13 | - persistent connections 14 | - pipelining 15 | - file uploads 16 | - cookies 17 | - dynamic thread pool 18 | - optional file cache in qtwebapp::StaticFileController 19 | - optional sessions via qtwebapp::HttpSessionStore 20 | - The qtwebapp::Template engine supports 21 | - multi languages via qtwebapp::TemplateLoader 22 | - optional file cache via qtwebapp::TemplateCache 23 | - for all text based file formats 24 | - The qtwebapp::Logger classes provide 25 | - automatic backups and file rotation 26 | - configurable message format, see qtwebapp::LogMessage 27 | - messages may contain thread-local info variables 28 | - optional buffer to improve performance and reduce disk usage 29 | - apply configuration changes without restart 30 | 31 | If you write a real application based on this source, take a look into the Demo 32 | applications. They set up a single listener on port 8080, however multiple 33 | listeners with individual configurations are also possible. 34 |

35 | To set up a HA system, I recommend the use of the 36 | nginx HTTP server with proxy balancer and sticky sessions. 37 |

38 | QtWebApp is published under the license LGPL v3.
39 | Author: Stefan Frings
40 | http://stefanfrings.de/qtwebapp 41 | */ 42 | 43 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(httpserver_HEADERS 2 | httpconnectionhandler.h 3 | httpconnectionhandlerpool.h 4 | httpcookie.h 5 | httplistener.h 6 | httpserverconfig.h 7 | httprequest.h 8 | httprequesthandler.h 9 | httpresponse.h 10 | httpsession.h 11 | httpsessionstore.h 12 | staticfilecontroller.h 13 | ) 14 | set(httpserver_SOURCES 15 | httpconnectionhandler.cpp 16 | httpconnectionhandlerpool.cpp 17 | httpcookie.cpp 18 | httplistener.cpp 19 | httpserverconfig.cpp 20 | httprequest.cpp 21 | httpresponse.cpp 22 | httpsession.cpp 23 | httpsessionstore.cpp 24 | staticfilecontroller.cpp 25 | ) 26 | 27 | add_library(QtWebAppHttpServer SHARED ${httpserver_HEADERS} ${httpserver_SOURCES}) 28 | target_include_directories(QtWebAppHttpServer PUBLIC 29 | $ 30 | $ 31 | ) 32 | target_link_libraries(QtWebAppHttpServer QtWebAppGlobal Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) 33 | set_target_properties(QtWebAppHttpServer PROPERTIES 34 | VERSION ${qtwebapp_VERSION} 35 | SOVERSION ${qtwebapp_MAJOR} 36 | ) 37 | 38 | install(TARGETS QtWebAppHttpServer 39 | EXPORT QtWebAppTargets 40 | LIBRARY DESTINATION lib 41 | RUNTIME DESTINATION bin 42 | ARCHIVE DESTINATION lib) 43 | install(FILES ${httpserver_HEADERS} 44 | DESTINATION include/qtwebapp/httpserver) 45 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpconnectionhandler.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "httpconnectionhandler.h" 7 | 8 | #include "httpresponse.h" 9 | 10 | using namespace qtwebapp; 11 | 12 | HttpConnectionHandler::HttpConnectionHandler(const HttpServerConfig &cfg, HttpRequestHandler *requestHandler, 13 | const QSslConfiguration *sslConfiguration) 14 | : QObject(), cfg(cfg) { 15 | Q_ASSERT(requestHandler != nullptr); 16 | this->requestHandler = requestHandler; 17 | this->sslConfiguration = sslConfiguration; 18 | currentRequest = nullptr; 19 | busy = false; 20 | 21 | // execute signals in a new thread 22 | thread = new QThread(); 23 | thread->start(); 24 | qDebug("HttpConnectionHandler (%p): thread started", static_cast(this)); 25 | moveToThread(thread); 26 | readTimer.moveToThread(thread); 27 | readTimer.setSingleShot(true); 28 | 29 | // Create TCP or SSL socket 30 | createSocket(); 31 | socket->moveToThread(thread); 32 | 33 | // Connect signals 34 | connect(socket, SIGNAL(readyRead()), SLOT(read())); 35 | connect(socket, SIGNAL(disconnected()), SLOT(disconnected())); 36 | connect(&readTimer, SIGNAL(timeout()), SLOT(readTimeout())); 37 | connect(thread, SIGNAL(finished()), this, SLOT(thread_done())); 38 | 39 | #ifdef CMAKE_DEBUG 40 | qDebug("HttpConnectionHandler (%p): constructed", static_cast(this)); 41 | #endif 42 | } 43 | 44 | void HttpConnectionHandler::thread_done() { 45 | readTimer.stop(); 46 | socket->close(); 47 | delete socket; 48 | qDebug("HttpConnectionHandler (%p): thread stopped", static_cast(this)); 49 | } 50 | 51 | HttpConnectionHandler::~HttpConnectionHandler() { 52 | thread->quit(); 53 | thread->wait(); 54 | thread->deleteLater(); 55 | #ifdef CMAKE_DEBUG 56 | qDebug("HttpConnectionHandler (%p): destroyed", static_cast(this)); 57 | #endif 58 | } 59 | 60 | void HttpConnectionHandler::createSocket() { 61 | // If SSL is supported and configured, then create an instance of QSslSocket 62 | #ifndef QT_NO_OPENSSL 63 | if (sslConfiguration) { 64 | QSslSocket *sslSocket = new QSslSocket(); 65 | sslSocket->setSslConfiguration(*sslConfiguration); 66 | socket = sslSocket; 67 | #ifdef CMAKE_DEBUG 68 | qDebug("HttpConnectionHandler (%p): SSL is enabled", static_cast(this)); 69 | #endif 70 | return; 71 | } 72 | #endif 73 | // else create an instance of QTcpSocket 74 | socket = new QTcpSocket(); 75 | } 76 | 77 | void HttpConnectionHandler::handleConnection(qintptr socketDescriptor) { 78 | #ifdef CMAKE_DEBUG 79 | qDebug("HttpConnectionHandler (%p): handle new connection", static_cast(this)); 80 | #endif 81 | busy = true; 82 | Q_ASSERT(socket->isOpen() == false); // if not, then the handler is already busy 83 | 84 | // UGLY workaround - we need to clear writebuffer before reusing this socket 85 | // https://bugreports.qt-project.org/browse/QTBUG-28914 86 | socket->connectToHost("", 0); 87 | socket->abort(); 88 | 89 | if (!socket->setSocketDescriptor(socketDescriptor)) { 90 | qCritical("HttpConnectionHandler (%p): cannot initialize socket: %s", static_cast(this), 91 | qPrintable(socket->errorString())); 92 | return; 93 | } 94 | 95 | #ifndef QT_NO_OPENSSL 96 | // Switch on encryption, if SSL is configured 97 | if (sslConfiguration) { 98 | #ifdef CMAKE_DEBUG 99 | qDebug("HttpConnectionHandler (%p): Starting encryption", static_cast(this)); 100 | (static_cast(socket))->startServerEncryption(); 101 | #endif 102 | } 103 | #endif 104 | 105 | // Start timer for read timeout 106 | readTimer.start(cfg.readTimeout); 107 | // delete previous request 108 | delete currentRequest; 109 | currentRequest = nullptr; 110 | } 111 | 112 | bool HttpConnectionHandler::isBusy() { 113 | return busy; 114 | } 115 | 116 | void HttpConnectionHandler::setBusy() { 117 | this->busy = true; 118 | } 119 | 120 | void HttpConnectionHandler::readTimeout() { 121 | qDebug("HttpConnectionHandler (%p): read timeout occured", static_cast(this)); 122 | 123 | socket->write("HTTP/1.1 408 request timeout\r\nConnection: close\r\n\r\n408 request timeout\r\n"); 124 | 125 | while (socket->bytesToWrite()) 126 | socket->waitForBytesWritten(); 127 | socket->disconnectFromHost(); 128 | delete currentRequest; 129 | currentRequest = nullptr; 130 | } 131 | 132 | void HttpConnectionHandler::disconnected() { 133 | #ifdef CMAKE_DEBUG 134 | qDebug("HttpConnectionHandler (%p): disconnected", static_cast(this)); 135 | #endif 136 | socket->close(); 137 | readTimer.stop(); 138 | busy = false; 139 | } 140 | 141 | void HttpConnectionHandler::read() { 142 | // The loop adds support for HTTP pipelinig 143 | while (socket->bytesAvailable()) { 144 | #ifdef SUPERVERBOSE 145 | qDebug("HttpConnectionHandler (%p): read input", static_cast(this)); 146 | #endif 147 | 148 | // Create new HttpRequest object if necessary 149 | if (!currentRequest) { 150 | currentRequest = new HttpRequest(cfg); 151 | } 152 | 153 | // Collect data for the request object 154 | while (socket->bytesAvailable() && currentRequest->getStatus() != HttpRequest::complete && 155 | currentRequest->getStatus() != HttpRequest::abort) { 156 | currentRequest->readFromSocket(socket); 157 | if (currentRequest->getStatus() == HttpRequest::waitForBody) { 158 | // Restart timer for read timeout, otherwise it would 159 | // expire during large file uploads. 160 | readTimer.start(cfg.readTimeout); 161 | } 162 | } 163 | 164 | // If the request is aborted, return error message and close the connection 165 | if (currentRequest->getStatus() == HttpRequest::abort) { 166 | socket->write("HTTP/1.1 413 entity too large\r\nConnection: close\r\n\r\n413 Entity too large\r\n"); 167 | while (socket->bytesToWrite()) 168 | socket->waitForBytesWritten(); 169 | socket->disconnectFromHost(); 170 | delete currentRequest; 171 | currentRequest = nullptr; 172 | return; 173 | } 174 | 175 | // If the request is complete, let the request mapper dispatch it 176 | if (currentRequest->getStatus() == HttpRequest::complete) { 177 | readTimer.stop(); 178 | #ifdef CMAKE_DEBUG 179 | qDebug("HttpConnectionHandler (%p): received request", static_cast(this)); 180 | #endif 181 | 182 | // Copy the Connection:close header to the response 183 | HttpResponse response(socket); 184 | bool closeConnection = 185 | QString::compare(currentRequest->getHeader("Connection"), "close", Qt::CaseInsensitive) == 0; 186 | if (closeConnection) { 187 | response.setHeader("Connection", "close"); 188 | } 189 | 190 | // In case of HTTP 1.0 protocol add the Connection:close header. 191 | // This ensures that the HttpResponse does not activate chunked mode, which is not spported by HTTP 1.0. 192 | else { 193 | bool http1_0 = QString::compare(currentRequest->getVersion(), "HTTP/1.0", Qt::CaseInsensitive) == 0; 194 | if (http1_0) { 195 | closeConnection = true; 196 | response.setHeader("Connection", "close"); 197 | } 198 | } 199 | 200 | // Call the request mapper 201 | try { 202 | requestHandler->service(*currentRequest, response); 203 | } catch (...) { 204 | qCritical("HttpConnectionHandler (%p): An uncatched exception occured in the request handler", 205 | static_cast(this)); 206 | } 207 | 208 | // Finalize sending the response if not already done 209 | if (!response.hasSentLastPart()) { 210 | response.write(QByteArray(), true); 211 | } 212 | 213 | #ifdef CMAKE_DEBUG 214 | qDebug("HttpConnectionHandler (%p): finished request", static_cast(this)); 215 | #endif 216 | 217 | // Find out whether the connection must be closed 218 | if (!closeConnection) { 219 | // Maybe the request handler or mapper added a Connection:close header in the meantime 220 | bool closeResponse = 221 | QString::compare(response.getHeaders().value("Connection"), "close", Qt::CaseInsensitive) == 0; 222 | if (closeResponse == true) { 223 | closeConnection = true; 224 | } else { 225 | // If we have no Content-Length header and did not use chunked mode, then we have to close the 226 | // connection to tell the HTTP client that the end of the response has been reached. 227 | bool hasContentLength = response.getHeaders().contains("Content-Length"); 228 | if (!hasContentLength) { 229 | bool hasChunkedMode = QString::compare(response.getHeaders().value("Transfer-Encoding"), "chunked", 230 | Qt::CaseInsensitive) == 0; 231 | if (!hasChunkedMode) { 232 | closeConnection = true; 233 | } 234 | } 235 | } 236 | } 237 | 238 | // Close the connection or prepare for the next request on the same connection. 239 | if (closeConnection) { 240 | while (socket->bytesToWrite()) 241 | socket->waitForBytesWritten(); 242 | socket->disconnectFromHost(); 243 | } else { 244 | // Start timer for next request 245 | readTimer.start(cfg.readTimeout); 246 | } 247 | delete currentRequest; 248 | currentRequest = nullptr; 249 | currentRequest = nullptr; 250 | } 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpconnectionhandler.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httprequest.h" 9 | #include "httprequesthandler.h" 10 | #include "httpserverconfig.h" 11 | #include "qtwebappglobal.h" 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | #ifndef QT_NO_SSL 18 | #include 19 | #endif 20 | 21 | namespace qtwebapp { 22 | 23 | /** Alias for QSslConfiguration if OpenSSL is not supported */ 24 | #ifdef QT_NO_SSL 25 | #define QSslConfiguration QObject 26 | #endif 27 | 28 | /** 29 | The connection handler accepts incoming connections and dispatches incoming requests to to a 30 | request mapper. Since HTTP clients can send multiple requests before waiting for the response, 31 | the incoming requests are queued and processed one after the other. 32 |

33 | Example for the required configuration settings: 34 |

 35 | 	  readTimeout=60000
 36 | 	  maxRequestSize=16000
 37 | 	  maxMultiPartSize=1000000
 38 | 	  
39 |

40 | The readTimeout value defines the maximum time to wait for a complete HTTP request. 41 | @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize. 42 | */ 43 | class QTWEBAPP_EXPORT HttpConnectionHandler : public QObject { 44 | Q_OBJECT 45 | Q_DISABLE_COPY(HttpConnectionHandler) 46 | 47 | public: 48 | /** 49 | Constructor. 50 | @param settings Configuration settings of the HTTP webserver 51 | @param requestHandler Handler that will process each incoming HTTP request 52 | @param sslConfiguration SSL (HTTPS) will be used if not NULL 53 | */ 54 | HttpConnectionHandler(const HttpServerConfig &cfg, HttpRequestHandler *requestHandler, 55 | const QSslConfiguration *sslConfiguration = nullptr); 56 | 57 | /** Destructor */ 58 | virtual ~HttpConnectionHandler(); 59 | 60 | /** Returns true, if this handler is in use. */ 61 | bool isBusy(); 62 | 63 | /** Mark this handler as busy */ 64 | void setBusy(); 65 | 66 | private: 67 | /** Configuration */ 68 | HttpServerConfig cfg; 69 | 70 | /** TCP socket of the current connection */ 71 | QTcpSocket *socket; 72 | 73 | /** The thread that processes events of this connection */ /** The thread that processes events of this connection */ 74 | QThread *thread; 75 | 76 | /** Time for read timeout detection */ 77 | QTimer readTimer; 78 | 79 | /** Storage for the current incoming HTTP request */ 80 | HttpRequest *currentRequest; 81 | 82 | /** Dispatches received requests to services */ 83 | HttpRequestHandler *requestHandler; 84 | 85 | /** This shows the busy-state from a very early time */ 86 | bool busy; 87 | 88 | /** Configuration for SSL */ 89 | const QSslConfiguration *sslConfiguration; 90 | 91 | /** Create SSL or TCP socket */ 92 | void createSocket(); 93 | 94 | public slots: 95 | 96 | /** 97 | Received from from the listener, when the handler shall start processing a new connection. 98 | @param socketDescriptor references the accepted connection. 99 | */ 100 | void handleConnection(qintptr socketDescriptor); 101 | 102 | private slots: 103 | 104 | /** Received from the socket when a read-timeout occured */ 105 | void readTimeout(); 106 | 107 | /** Received from the socket when incoming data can be read */ 108 | void read(); 109 | 110 | /** Received from the socket when a connection has been closed */ 111 | void disconnected(); 112 | 113 | /** Cleanup after the thread is closed */ 114 | void thread_done(); 115 | }; 116 | 117 | } // namespace qtwebapp 118 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpconnectionhandlerpool.cpp: -------------------------------------------------------------------------------- 1 | #include "httpconnectionhandlerpool.h" 2 | 3 | #include "httplistener.h" 4 | 5 | #include 6 | 7 | #ifndef QT_NO_OPENSSL 8 | #include 9 | #include 10 | #include 11 | #include 12 | #endif 13 | 14 | using namespace qtwebapp; 15 | 16 | HttpConnectionHandlerPool::HttpConnectionHandlerPool(const HttpServerConfig &cfg, HttpRequestHandler *requestHandler) 17 | : QObject(), cfg(cfg), requestHandler(requestHandler) { 18 | sslConfiguration = nullptr; 19 | loadSslConfig(); 20 | cleanupTimer.start(cfg.cleanupInterval); 21 | connect(&cleanupTimer, SIGNAL(timeout()), SLOT(cleanup())); 22 | } 23 | 24 | HttpConnectionHandlerPool::~HttpConnectionHandlerPool() { 25 | // delete all connection handlers and wait until their threads are closed 26 | foreach (HttpConnectionHandler *handler, pool) { 27 | delete handler; 28 | } 29 | delete sslConfiguration; 30 | #ifdef CMAKE_DEBUG 31 | qDebug("HttpConnectionHandlerPool (%p): destroyed", this); 32 | #endif 33 | } 34 | 35 | HttpConnectionHandler *HttpConnectionHandlerPool::getConnectionHandler() { 36 | HttpConnectionHandler *freeHandler = nullptr; 37 | mutex.lock(); 38 | // find a free handler in pool 39 | foreach (HttpConnectionHandler *handler, pool) { 40 | if (!handler->isBusy()) { 41 | freeHandler = handler; 42 | freeHandler->setBusy(); 43 | break; 44 | } 45 | } 46 | // create a new handler, if necessary 47 | if (!freeHandler) { 48 | int maxConnectionHandlers = cfg.maxThreads; 49 | if (pool.count() < maxConnectionHandlers) { 50 | freeHandler = new HttpConnectionHandler(cfg, requestHandler, sslConfiguration); 51 | freeHandler->setBusy(); 52 | pool.append(freeHandler); 53 | } 54 | } 55 | mutex.unlock(); 56 | return freeHandler; 57 | } 58 | 59 | void HttpConnectionHandlerPool::cleanup() { 60 | int maxIdleHandlers = cfg.minThreads; 61 | int idleCounter = 0; 62 | mutex.lock(); 63 | foreach (HttpConnectionHandler *handler, pool) { 64 | if (!handler->isBusy()) { 65 | if (++idleCounter > maxIdleHandlers) { 66 | delete handler; 67 | pool.removeOne(handler); 68 | long int poolSize = (long int)pool.size(); 69 | #ifdef CMAKE_DEBUG 70 | qDebug("HttpConnectionHandlerPool: Removed connection handler (%p), pool size is now %li", handler, 71 | poolSize); 72 | #endif 73 | break; // remove only one handler in each interval 74 | } 75 | } 76 | } 77 | mutex.unlock(); 78 | } 79 | 80 | void HttpConnectionHandlerPool::loadSslConfig() { 81 | // If certificate and key files are configured, then load them 82 | QString sslKeyFileName = cfg.sslKeyFile; 83 | QString sslCertFileName = cfg.sslCertFile; 84 | if (!sslKeyFileName.isEmpty() && !sslCertFileName.isEmpty()) { 85 | #ifdef QT_NO_OPENSSL 86 | qWarning("HttpConnectionHandlerPool: SSL is not supported"); 87 | #else 88 | // Convert relative fileNames to absolute, based on the directory of the config file. 89 | if (!cfg.fileName.isEmpty()) { 90 | QFileInfo configFile(cfg.fileName); 91 | if (QDir::isRelativePath(sslKeyFileName)) { 92 | sslKeyFileName = QFileInfo(configFile.absolutePath(), sslKeyFileName).absoluteFilePath(); 93 | } 94 | if (QDir::isRelativePath(sslCertFileName)) { 95 | sslCertFileName = QFileInfo(configFile.absolutePath(), sslCertFileName).absoluteFilePath(); 96 | } 97 | } 98 | 99 | // Load the SSL certificate 100 | QFile certFile(sslCertFileName); 101 | if (!certFile.open(QIODevice::ReadOnly)) { 102 | qCritical("HttpConnectionHandlerPool: cannot open sslCertFile %s", qPrintable(sslCertFileName)); 103 | return; 104 | } 105 | QSslCertificate certificate(&certFile, QSsl::Pem); 106 | certFile.close(); 107 | 108 | // Load the key file 109 | QFile keyFile(sslKeyFileName); 110 | if (!keyFile.open(QIODevice::ReadOnly)) { 111 | qCritical("HttpConnectionHandlerPool: cannot open sslKeyFile %s", qPrintable(sslKeyFileName)); 112 | return; 113 | } 114 | QSslKey sslKey(&keyFile, QSsl::Rsa, QSsl::Pem); 115 | keyFile.close(); 116 | 117 | // Create the SSL configuration 118 | sslConfiguration = new QSslConfiguration(); 119 | sslConfiguration->setLocalCertificate(certificate); 120 | sslConfiguration->setPrivateKey(sslKey); 121 | sslConfiguration->setPeerVerifyMode(QSslSocket::VerifyNone); 122 | sslConfiguration->setProtocol(QSsl::SecureProtocols); 123 | 124 | #ifdef CMAKE_DEBUG 125 | qDebug("HttpConnectionHandlerPool: SSL settings loaded"); 126 | #endif 127 | #endif // QT_NO_OPENSSL 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpconnectionhandlerpool.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "httpconnectionhandler.h" 4 | #include "httpserverconfig.h" 5 | #include "qtwebappglobal.h" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | namespace qtwebapp { 13 | 14 | /** 15 | Pool of http connection handlers. The size of the pool grows and 16 | shrinks on demand. 17 |

18 | Example for the required configuration settings: 19 |

20 | 	  minThreads=4
21 | 	  maxThreads=100
22 | 	  cleanupInterval=60000
23 | 	  readTimeout=60000
24 | 	  ;sslKeyFile=ssl/my.key
25 | 	  ;sslCertFile=ssl/my.cert
26 | 	  maxRequestSize=16000
27 | 	  maxMultiPartSize=1000000
28 | 	  
29 | After server start, the size of the thread pool is always 0. Threads 30 | are started on demand when requests come in. The cleanup timer reduces 31 | the number of idle threads slowly by closing one thread in each interval. 32 | But the configured minimum number of threads are kept running. 33 |

34 | For SSL support, you need an OpenSSL certificate file and a key file. 35 | Both can be created with the command 36 |

37 | 	      openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout my.key -out my.cert
38 | 	  
39 |

40 | Visit http://slproweb.com/products/Win32OpenSSL.html to download the Light version of OpenSSL for Windows. 41 |

42 | Please note that a listener with SSL settings can only handle HTTPS protocol. To 43 | support both HTTP and HTTPS simultaneously, you need to start two listeners on different ports - 44 | one with SLL and one without SSL. 45 | @see HttpConnectionHandler for description of the readTimeout 46 | @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize 47 | */ 48 | 49 | class QTWEBAPP_EXPORT HttpConnectionHandlerPool : public QObject { 50 | Q_OBJECT 51 | Q_DISABLE_COPY(HttpConnectionHandlerPool) 52 | public: 53 | /** 54 | Constructor. 55 | @param settings Configuration settings for the HTTP server. Must not be 0. 56 | @param requestHandler The handler that will process each received HTTP request. 57 | @warning The requestMapper gets deleted by the destructor of this pool 58 | */ 59 | HttpConnectionHandlerPool(const HttpServerConfig &cfg, HttpRequestHandler *requestHandler); 60 | 61 | /** Destructor */ 62 | virtual ~HttpConnectionHandlerPool(); 63 | 64 | /** Get a free connection handler, or 0 if not available. */ 65 | HttpConnectionHandler *getConnectionHandler(); 66 | 67 | private: 68 | /** Config for this pool */ 69 | HttpServerConfig cfg; 70 | 71 | /** Will be assigned to each Connectionhandler during their creation */ 72 | HttpRequestHandler *requestHandler; 73 | 74 | /** Pool of connection handlers */ 75 | QList pool; 76 | 77 | /** Timer to clean-up unused connection handler */ 78 | QTimer cleanupTimer; 79 | 80 | /** Used to synchronize threads */ 81 | QMutex mutex; 82 | 83 | /** The SSL configuration (certificate, key and other settings) */ 84 | QSslConfiguration *sslConfiguration; 85 | 86 | /** Load SSL configuration */ 87 | void loadSslConfig(); 88 | 89 | private slots: 90 | 91 | /** Received from the clean-up timer. */ 92 | void cleanup(); 93 | }; 94 | 95 | } // namespace qtwebapp 96 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpcookie.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "httpcookie.h" 7 | 8 | using namespace qtwebapp; 9 | 10 | HttpCookie::HttpCookie() { 11 | version = 1; 12 | maxAge = 0; 13 | secure = false; 14 | } 15 | 16 | HttpCookie::HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path, 17 | const QByteArray comment, const QByteArray domain, const bool secure, const bool httpOnly, 18 | const QByteArray sameSite) { 19 | this->name = name; 20 | this->value = value; 21 | this->maxAge = maxAge; 22 | this->path = path; 23 | this->comment = comment; 24 | this->domain = domain; 25 | this->secure = secure; 26 | this->httpOnly = httpOnly; 27 | this->sameSite = sameSite; 28 | this->version = 1; 29 | } 30 | 31 | HttpCookie::HttpCookie(const QByteArray source) { 32 | version = 1; 33 | maxAge = 0; 34 | secure = false; 35 | httpOnly = false; 36 | QList list = splitCSV(source); 37 | foreach (QByteArray part, list) { 38 | 39 | // Split the part into name and value 40 | QByteArray name; 41 | QByteArray value; 42 | int posi = part.indexOf('='); 43 | if (posi) { 44 | name = part.left(posi).trimmed(); 45 | value = part.mid(posi + 1).trimmed(); 46 | } else { 47 | name = part.trimmed(); 48 | value = ""; 49 | } 50 | 51 | // Set fields 52 | if (name == "Comment") { 53 | comment = value; 54 | } else if (name == "Domain") { 55 | domain = value; 56 | } else if (name == "Max-Age") { 57 | maxAge = value.toInt(); 58 | } else if (name == "Path") { 59 | path = value; 60 | } else if (name == "Secure") { 61 | secure = true; 62 | } else if (name == "HttpOnly") { 63 | httpOnly = true; 64 | } else if (name == "SameSite") { 65 | sameSite = value; 66 | } else if (name == "Version") { 67 | version = value.toInt(); 68 | } else if (this->name.isEmpty()) { 69 | this->name = name; 70 | this->value = value; 71 | } else { 72 | qWarning("HttpCookie: Ignoring unknown %s=%s", name.data(), value.data()); 73 | } 74 | } 75 | } 76 | 77 | QByteArray HttpCookie::toByteArray() const { 78 | QByteArray buffer(name); 79 | buffer.append('='); 80 | buffer.append(value); 81 | if (!comment.isEmpty()) { 82 | buffer.append("; Comment="); 83 | buffer.append(comment); 84 | } 85 | if (!domain.isEmpty()) { 86 | buffer.append("; Domain="); 87 | buffer.append(domain); 88 | } 89 | if (maxAge != 0) { 90 | buffer.append("; Max-Age="); 91 | buffer.append(QByteArray::number(maxAge)); 92 | } 93 | if (!path.isEmpty()) { 94 | buffer.append("; Path="); 95 | buffer.append(path); 96 | } 97 | if (secure) { 98 | buffer.append("; Secure"); 99 | } 100 | if (httpOnly) { 101 | buffer.append("; HttpOnly"); 102 | } 103 | if (!sameSite.isEmpty()) { 104 | buffer.append("; SameSite="); 105 | buffer.append(sameSite); 106 | } 107 | buffer.append("; Version="); 108 | buffer.append(QByteArray::number(version)); 109 | return buffer; 110 | } 111 | 112 | void HttpCookie::setName(const QByteArray name) { 113 | this->name = name; 114 | } 115 | 116 | void HttpCookie::setValue(const QByteArray value) { 117 | this->value = value; 118 | } 119 | 120 | void HttpCookie::setComment(const QByteArray comment) { 121 | this->comment = comment; 122 | } 123 | 124 | void HttpCookie::setDomain(const QByteArray domain) { 125 | this->domain = domain; 126 | } 127 | 128 | void HttpCookie::setMaxAge(const int maxAge) { 129 | this->maxAge = maxAge; 130 | } 131 | 132 | void HttpCookie::setPath(const QByteArray path) { 133 | this->path = path; 134 | } 135 | 136 | void HttpCookie::setSecure(const bool secure) { 137 | this->secure = secure; 138 | } 139 | 140 | void HttpCookie::setHttpOnly(const bool httpOnly) { 141 | this->httpOnly = httpOnly; 142 | } 143 | 144 | void HttpCookie::setSameSite(const QByteArray sameSite) { 145 | this->sameSite = sameSite; 146 | } 147 | 148 | QByteArray HttpCookie::getName() const { 149 | return name; 150 | } 151 | 152 | QByteArray HttpCookie::getValue() const { 153 | return value; 154 | } 155 | 156 | QByteArray HttpCookie::getComment() const { 157 | return comment; 158 | } 159 | 160 | QByteArray HttpCookie::getDomain() const { 161 | return domain; 162 | } 163 | 164 | int HttpCookie::getMaxAge() const { 165 | return maxAge; 166 | } 167 | 168 | QByteArray HttpCookie::getPath() const { 169 | return path; 170 | } 171 | 172 | bool HttpCookie::getSecure() const { 173 | return secure; 174 | } 175 | 176 | bool HttpCookie::getHttpOnly() const { 177 | return httpOnly; 178 | } 179 | 180 | QByteArray HttpCookie::getSameSite() const { 181 | return sameSite; 182 | } 183 | 184 | int HttpCookie::getVersion() const { 185 | return version; 186 | } 187 | 188 | QList HttpCookie::splitCSV(const QByteArray source) { 189 | bool inString = false; 190 | QList list; 191 | QByteArray buffer; 192 | for (int i = 0; i < source.size(); ++i) { 193 | char c = source.at(i); 194 | if (inString == false) { 195 | if (c == '\"') { 196 | inString = true; 197 | } else if (c == ';') { 198 | QByteArray trimmed = buffer.trimmed(); 199 | if (!trimmed.isEmpty()) { 200 | list.append(trimmed); 201 | } 202 | buffer.clear(); 203 | } else { 204 | buffer.append(c); 205 | } 206 | } else if (c == '\"') { 207 | inString = false; 208 | } else { 209 | buffer.append(c); 210 | } 211 | } 212 | QByteArray trimmed = buffer.trimmed(); 213 | if (!trimmed.isEmpty()) { 214 | list.append(trimmed); 215 | } 216 | return list; 217 | } 218 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpcookie.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "qtwebappglobal.h" 9 | 10 | #include 11 | #include 12 | 13 | namespace qtwebapp { 14 | 15 | /** 16 | HTTP cookie as defined in RFC 2109. 17 | Supports some additional attributes of RFC6265bis. 18 | */ 19 | 20 | class QTWEBAPP_EXPORT HttpCookie { 21 | public: 22 | /** Creates an empty cookie */ 23 | HttpCookie(); 24 | 25 | /** 26 | Create a cookie and set name/value pair. 27 | @param name name of the cookie 28 | @param value value of the cookie 29 | @param maxAge maximum age of the cookie in seconds. 0=discard immediately 30 | @param path Path for that the cookie will be sent, default="/" which means the whole domain 31 | @param comment Optional comment, may be displayed by the web browser somewhere 32 | @param domain Optional domain for that the cookie will be sent. Defaults to the current domain 33 | @param secure If true, the cookie will be sent by the browser to the server only on secure connections 34 | @param httpOnly If true, the browser does not allow client-side scripts to access the cookie 35 | @param sameSite Declare if the cookie can only be read by the same site, which is a stronger 36 | restriction than the domain. Allowed values: "Lax" and "Strict". 37 | */ 38 | HttpCookie(const QByteArray name, const QByteArray value, const int maxAge, const QByteArray path = "/", 39 | const QByteArray comment = QByteArray(), const QByteArray domain = QByteArray(), 40 | const bool secure = false, const bool httpOnly = false, const QByteArray sameSite = QByteArray()); 41 | 42 | /** 43 | Create a cookie from a string. 44 | @param source String as received in a HTTP Cookie2 header. 45 | */ 46 | HttpCookie(const QByteArray source); 47 | 48 | /** Convert this cookie to a string that may be used in a Set-Cookie header. */ 49 | QByteArray toByteArray() const; 50 | 51 | /** 52 | Split a string list into parts, where each part is delimited by semicolon. 53 | Semicolons within double quotes are skipped. Double quotes are removed. 54 | */ 55 | static QList splitCSV(const QByteArray source); 56 | 57 | /** Set the name of this cookie */ 58 | void setName(const QByteArray name); 59 | 60 | /** Set the value of this cookie */ 61 | void setValue(const QByteArray value); 62 | 63 | /** Set the comment of this cookie */ 64 | void setComment(const QByteArray comment); 65 | 66 | /** Set the domain of this cookie */ 67 | void setDomain(const QByteArray domain); 68 | 69 | /** Set the maximum age of this cookie in seconds. 0=discard immediately */ 70 | void setMaxAge(const int maxAge); 71 | 72 | /** Set the path for that the cookie will be sent, default="/" which means the whole domain */ 73 | void setPath(const QByteArray path); 74 | 75 | /** Set secure mode, so that the cookie will be sent by the browser to the server only on secure connections */ 76 | void setSecure(const bool secure); 77 | 78 | /** Set HTTP-only mode, so that the browser does not allow client-side scripts to access the cookie */ 79 | void setHttpOnly(const bool httpOnly); 80 | 81 | /** 82 | * Set same-site mode, so that the browser does not allow other web sites to access the cookie. 83 | * Allowed values: "Lax" and "Strict". 84 | */ 85 | void setSameSite(const QByteArray sameSite); 86 | 87 | /** Get the name of this cookie */ 88 | QByteArray getName() const; 89 | 90 | /** Get the value of this cookie */ 91 | QByteArray getValue() const; 92 | 93 | /** Get the comment of this cookie */ 94 | QByteArray getComment() const; 95 | 96 | /** Get the domain of this cookie */ 97 | QByteArray getDomain() const; 98 | 99 | /** Get the maximum age of this cookie in seconds. */ 100 | int getMaxAge() const; 101 | 102 | /** Set the path of this cookie */ 103 | QByteArray getPath() const; 104 | 105 | /** Get the secure flag of this cookie */ 106 | bool getSecure() const; 107 | 108 | /** Get the HTTP-only flag of this cookie */ 109 | bool getHttpOnly() const; 110 | 111 | /** Get the same-site flag of this cookie */ 112 | QByteArray getSameSite() const; 113 | 114 | /** Returns always 1 */ 115 | int getVersion() const; 116 | 117 | private: 118 | QByteArray name; 119 | QByteArray value; 120 | QByteArray comment; 121 | QByteArray domain; 122 | int maxAge; 123 | QByteArray path; 124 | bool secure; 125 | bool httpOnly; 126 | QByteArray sameSite; 127 | int version; 128 | }; 129 | 130 | } // namespace qtwebapp 131 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httplistener.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "httplistener.h" 7 | 8 | #include "httpconnectionhandler.h" 9 | #include "httpconnectionhandlerpool.h" 10 | 11 | #include 12 | 13 | using namespace qtwebapp; 14 | 15 | HttpListener::HttpListener(const HttpServerConfig &cfg, HttpRequestHandler *requestHandler, QObject *parent) 16 | : QTcpServer(parent), cfg(cfg) { 17 | Q_ASSERT(requestHandler != nullptr); 18 | pool = nullptr; 19 | this->requestHandler = requestHandler; 20 | // Reqister type of socketDescriptor for signal/slot handling 21 | qRegisterMetaType("qintptr"); 22 | // Start listening 23 | listen(); 24 | } 25 | 26 | HttpListener::~HttpListener() { 27 | close(); 28 | #ifdef CMAKE_DEBUG 29 | qDebug("HttpListener: destroyed"); 30 | #endif 31 | } 32 | 33 | void HttpListener::listen() { 34 | if (!pool) { 35 | pool = new HttpConnectionHandlerPool(cfg, requestHandler); 36 | } 37 | QTcpServer::listen(cfg.host, cfg.port); 38 | if (!isListening()) { 39 | qCritical("HttpListener: Cannot bind on port %i: %s", cfg.port, qPrintable(errorString())); 40 | } else { 41 | qDebug("HttpListener: Listening on port %i", serverPort()); 42 | } 43 | } 44 | 45 | void HttpListener::close() { 46 | QTcpServer::close(); 47 | #ifdef CMAKE_DEBUG 48 | qDebug("HttpListener: closed"); 49 | #endif 50 | if (pool) { 51 | delete pool; 52 | pool = nullptr; 53 | } 54 | } 55 | 56 | void HttpListener::incomingConnection(qintptr socketDescriptor) { 57 | #ifdef SUPERVERBOSE 58 | qDebug("HttpListener: New connection"); 59 | #endif 60 | 61 | HttpConnectionHandler *freeHandler = nullptr; 62 | if (pool) { 63 | freeHandler = pool->getConnectionHandler(); 64 | } 65 | 66 | // Let the handler process the new connection. 67 | if (freeHandler) { 68 | // The descriptor is passed via event queue because the handler lives in another thread 69 | QMetaObject::invokeMethod(freeHandler, "handleConnection", Qt::QueuedConnection, Q_ARG(qintptr, socketDescriptor)); 70 | } else { 71 | // Reject the connection 72 | qWarning("HttpListener: Too many incoming connections"); 73 | QTcpSocket *socket = new QTcpSocket(this); 74 | socket->setSocketDescriptor(socketDescriptor); 75 | connect(socket, SIGNAL(disconnected()), socket, SLOT(deleteLater())); 76 | socket->write("HTTP/1.1 503 too many connections\r\nConnection: close\r\n\r\nToo many connections\r\n"); 77 | socket->disconnectFromHost(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httplistener.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httpconnectionhandler.h" 9 | #include "httpconnectionhandlerpool.h" 10 | #include "httprequesthandler.h" 11 | #include "httpserverconfig.h" 12 | #include "qtwebappglobal.h" 13 | 14 | #include 15 | #include 16 | 17 | namespace qtwebapp { 18 | 19 | /** 20 | Listens for incoming TCP connections and and passes all incoming HTTP requests to your implementation of 21 | HttpRequestHandler, which processes the request and generates the response (usually a HTML document).

Example for 22 | the required settings in the config file:

 ;host=192.168.0.100 port=8080 minThreads=1 maxThreads=10
23 | 	  cleanupInterval=1000
24 | 	  readTimeout=60000
25 | 	  ;sslKeyFile=ssl/my.key
26 | 	  ;sslCertFile=ssl/my.cert
27 | 	  maxRequestSize=16000
28 | 	  maxMultiPartSize=1000000
29 | 	  
30 | The optional host parameter binds the listener to one network interface. 31 | The listener handles all network interfaces if no host is configured. 32 | The port number specifies the incoming TCP port that this listener listens to. 33 | @see HttpConnectionHandlerPool for description of config settings minThreads, maxThreads, cleanupInterval and ssl 34 | settings 35 | @see HttpConnectionHandler for description of the readTimeout 36 | @see HttpRequest for description of config settings maxRequestSize and maxMultiPartSize 37 | */ 38 | 39 | class QTWEBAPP_EXPORT HttpListener : public QTcpServer { 40 | Q_OBJECT 41 | Q_DISABLE_COPY(HttpListener) 42 | public: 43 | /** 44 | Constructor. 45 | Creates a connection pool and starts listening on the configured host and port. 46 | @param cfg The configuration for the HTTP server. 47 | @param requestHandler Processes each received HTTP request, usually by dispatching to controller classes. 48 | @param parent Parent object. 49 | @warning Ensure to close or delete the listener before deleting the request handler. 50 | */ 51 | HttpListener(const HttpServerConfig &cfg, HttpRequestHandler *requestHandler, QObject *parent = NULL); 52 | 53 | /** Destructor */ 54 | virtual ~HttpListener(); 55 | 56 | /** 57 | Restart listeing after close(). 58 | */ 59 | void listen(); 60 | 61 | /** 62 | Closes the listener, waits until all pending requests are processed, 63 | then closes the connection pool. 64 | */ 65 | void close(); 66 | 67 | protected: 68 | /** Serves new incoming connection requests */ 69 | void incomingConnection(qintptr socketDescriptor); 70 | 71 | private: 72 | /** Configuration settings for the HTTP server */ 73 | HttpServerConfig cfg; 74 | 75 | /** Point to the reuqest handler which processes all HTTP requests */ 76 | HttpRequestHandler *requestHandler; 77 | 78 | /** Pool of connection handlers */ 79 | HttpConnectionHandlerPool *pool; 80 | 81 | signals: 82 | 83 | /** 84 | Sent to the connection handler to process a new incoming connection. 85 | @param socketDescriptor references the accepted connection. 86 | */ 87 | void handleConnection(qintptr socketDescriptor); 88 | }; 89 | 90 | } // namespace qtwebapp 91 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httprequest.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httpserverconfig.h" 9 | #include "qtwebappglobal.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | namespace qtwebapp { 20 | 21 | /** 22 | This object represents a single HTTP request. It reads the request 23 | from a TCP socket and provides getters for the individual parts 24 | of the request. 25 |

26 | The follwing config settings are required: 27 |

 28 | 	  maxRequestSize=16000
 29 | 	  maxMultiPartSize=1000000
 30 | 	  
31 |

32 | MaxRequestSize is the maximum size of a HTTP request. In case of 33 | multipart/form-data requests (also known as file-upload), the maximum 34 | size of the body must not exceed maxMultiPartSize. 35 | The body is always a little larger than the file itself. 36 | */ 37 | 38 | class QTWEBAPP_EXPORT HttpRequest { 39 | Q_DISABLE_COPY(HttpRequest) 40 | friend class HttpSessionStore; 41 | 42 | public: 43 | /** Values for getStatus() */ 44 | enum RequestStatus { waitForRequest, waitForHeader, waitForBody, complete, abort }; 45 | 46 | /** 47 | Constructor. 48 | @param cfg Configuration 49 | */ 50 | HttpRequest(const HttpServerConfig &cfg); 51 | 52 | /** 53 | Destructor. 54 | */ 55 | virtual ~HttpRequest(); 56 | 57 | /** 58 | Read the HTTP request from a socket. 59 | This method is called by the connection handler repeatedly 60 | until the status is RequestStatus::complete or RequestStatus::abort. 61 | @param socket Source of the data 62 | */ 63 | void readFromSocket(QTcpSocket *socket); 64 | 65 | /** 66 | Get the status of this reqeust. 67 | @see RequestStatus 68 | */ 69 | RequestStatus getStatus() const; 70 | 71 | /** Get the method of the HTTP request (e.g. "GET") */ 72 | QByteArray getMethod() const; 73 | 74 | /** Get the decoded path of the HTPP request (e.g. "/index.html") */ 75 | QByteArray getPath() const; 76 | 77 | /** Get the raw path of the HTTP request (e.g. "/file%20with%20spaces.html") */ 78 | const QByteArray &getRawPath() const; 79 | 80 | /** Get the version of the HTPP request (e.g. "HTTP/1.1") */ 81 | QByteArray getVersion() const; 82 | 83 | /** 84 | Get the value of a HTTP request header. 85 | @param name Name of the header, not case-senitive. 86 | @return If the header occurs multiple times, only the last 87 | one is returned. 88 | */ 89 | QByteArray getHeader(const QByteArray &name) const; 90 | 91 | /** 92 | Get the values of a HTTP request header. 93 | @param name Name of the header, not case-senitive. 94 | */ 95 | QList getHeaders(const QByteArray &name) const; 96 | 97 | /** 98 | * Get all HTTP request headers. Note that the header names 99 | * are returned in lower-case. 100 | */ 101 | QMultiMap getHeaderMap() const; 102 | 103 | /** 104 | Get the value of a HTTP request parameter. 105 | @param name Name of the parameter, case-sensitive. 106 | @return If the parameter occurs multiple times, only the last 107 | one is returned. 108 | */ 109 | QByteArray getParameter(const QByteArray &name) const; 110 | 111 | /** 112 | Get the values of a HTTP request parameter. 113 | @param name Name of the parameter, case-sensitive. 114 | */ 115 | QList getParameters(const QByteArray &name) const; 116 | 117 | /** Get all HTTP request parameters. */ 118 | QMultiMap getParameterMap() const; 119 | 120 | /** Get the HTTP request body. */ 121 | QByteArray getBody() const; 122 | 123 | /** 124 | Decode an URL parameter. 125 | E.g. replace "%23" by '#' and replace '+' by ' '. 126 | @param source The url encoded strings 127 | @see QUrl::toPercentEncoding for the reverse direction 128 | */ 129 | static QByteArray urlDecode(const QByteArray source); 130 | 131 | /** 132 | Get an uploaded file. The file is already open. It will 133 | be closed and deleted by the destructor of this HttpRequest 134 | object (after processing the request). 135 |

136 | For uploaded files, the method getParameters() returns 137 | the original fileName as provided by the calling web browser. 138 | */ 139 | QFile *getUploadedFile(const QByteArray fieldName) const; 140 | 141 | /** 142 | Get the value of a cookie. 143 | @param name Name of the cookie 144 | */ 145 | QByteArray getCookie(const QByteArray &name) const; 146 | 147 | /** Get all cookies. */ 148 | QMap &getCookieMap(); 149 | 150 | /** 151 | Get the address of the connected client. 152 | Note that multiple clients may have the same IP address, if they 153 | share an internet connection (which is very common). 154 | */ 155 | QHostAddress getPeerAddress() const; 156 | 157 | private: 158 | /** Request headers */ 159 | QMultiMap headers; 160 | 161 | /** Parameters of the request */ 162 | QMultiMap parameters; 163 | 164 | /** Uploaded files of the request, key is the field name. */ 165 | QMap uploadedFiles; 166 | 167 | /** Received cookies */ 168 | QMap cookies; 169 | 170 | /** Storage for raw body data */ 171 | QByteArray bodyData; 172 | 173 | /** Request method */ 174 | QByteArray method; 175 | 176 | /** Request path (in raw encoded format) */ 177 | QByteArray path; 178 | 179 | /** Request protocol version */ 180 | QByteArray version; 181 | 182 | /** 183 | Status of this request. For the state engine. 184 | @see RequestStatus 185 | */ 186 | RequestStatus status; 187 | 188 | /** Address of the connected peer. */ 189 | QHostAddress peerAddress; 190 | 191 | /** Maximum size of requests in bytes. */ 192 | int maxSize; 193 | 194 | /** Maximum allowed size of multipart forms in bytes. */ 195 | int maxMultiPartSize; 196 | 197 | /** Current size */ 198 | int currentSize; 199 | 200 | /** Expected size of body */ 201 | int expectedBodySize; 202 | 203 | /** Name of the current header, or empty if no header is being processed */ 204 | QByteArray currentHeader; 205 | 206 | /** Boundary of multipart/form-data body. Empty if there is no such header */ 207 | QByteArray boundary; 208 | 209 | /** Temp file, that is used to store the multipart/form-data body */ 210 | QTemporaryFile *tempFile; 211 | 212 | /** Parse the multipart body, that has been stored in the temp file. */ 213 | void parseMultiPartFile(); 214 | 215 | /** Sub-procedure of readFromSocket(), read the first line of a request. */ 216 | void readRequest(QTcpSocket *socket); 217 | 218 | /** Sub-procedure of readFromSocket(), read header lines. */ 219 | void readHeader(QTcpSocket *socket); 220 | 221 | /** Sub-procedure of readFromSocket(), read the request body. */ 222 | void readBody(QTcpSocket *socket); 223 | 224 | /** Sub-procedure of readFromSocket(), extract and decode request parameters. */ 225 | void decodeRequestParams(); 226 | 227 | /** Sub-procedure of readFromSocket(), extract cookies from headers */ 228 | void extractCookies(); 229 | 230 | /** Buffer for collecting characters of request and header lines */ 231 | QByteArray lineBuffer; 232 | 233 | /** Directory for temporary files */ 234 | QString tmpDir; 235 | }; 236 | 237 | } // namespace qtwebapp 238 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httprequesthandler.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httprequest.h" 9 | #include "httpresponse.h" 10 | #include "qtwebappglobal.h" 11 | 12 | namespace qtwebapp { 13 | 14 | /** 15 | The request handler generates a response for each HTTP request. Web Applications 16 | usually have one central request handler that maps incoming requests to several 17 | controllers (servlets) based on the requested path. 18 |

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

21 | @warning Be aware that the main request handler instance must be created on the heap and 22 | that it is used by multiple threads simultaneously. 23 | @see StaticFileController which delivers static local files. 24 | */ 25 | 26 | class QTWEBAPP_EXPORT HttpRequestHandler : public QObject { 27 | Q_OBJECT 28 | Q_DISABLE_COPY(HttpRequestHandler) 29 | public: 30 | /** 31 | * Constructor. 32 | * @param parent Parent object. 33 | */ 34 | HttpRequestHandler(QObject *parent = nullptr) : QObject(parent) {} 35 | 36 | /** 37 | Generate a response for an incoming HTTP request. 38 | @param request The received HTTP request 39 | @param response Must be used to return the response 40 | @warning This method must be thread safe 41 | */ 42 | virtual void service(HttpRequest &request, HttpResponse &response) = 0; 43 | }; 44 | 45 | } // namespace qtwebapp 46 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpresponse.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "httpresponse.h" 7 | 8 | using namespace qtwebapp; 9 | 10 | HttpResponse::HttpResponse(QTcpSocket *socket) { 11 | this->socket = socket; 12 | statusCode = 200; 13 | statusText = "OK"; 14 | sentHeaders = false; 15 | sentLastPart = false; 16 | chunkedMode = false; 17 | } 18 | 19 | void HttpResponse::setHeader(QByteArray name, QByteArray value) { 20 | Q_ASSERT(sentHeaders == false); 21 | headers.insert(name, value); 22 | } 23 | 24 | void HttpResponse::setHeader(QByteArray name, int value) { 25 | Q_ASSERT(sentHeaders == false); 26 | headers.insert(name, QByteArray::number(value)); 27 | } 28 | 29 | QMap &HttpResponse::getHeaders() { 30 | return headers; 31 | } 32 | 33 | void HttpResponse::setStatus(int statusCode, QByteArray description) { 34 | this->statusCode = statusCode; 35 | statusText = description; 36 | } 37 | 38 | int HttpResponse::getStatusCode() const { 39 | return this->statusCode; 40 | } 41 | 42 | void HttpResponse::writeHeaders() { 43 | Q_ASSERT(sentHeaders == false); 44 | QByteArray buffer; 45 | buffer.append("HTTP/1.1 "); 46 | buffer.append(QByteArray::number(statusCode)); 47 | buffer.append(' '); 48 | buffer.append(statusText); 49 | buffer.append("\r\n"); 50 | foreach (QByteArray name, headers.keys()) { 51 | buffer.append(name); 52 | buffer.append(": "); 53 | buffer.append(headers.value(name)); 54 | buffer.append("\r\n"); 55 | } 56 | foreach (HttpCookie cookie, cookies.values()) { 57 | buffer.append("Set-Cookie: "); 58 | buffer.append(cookie.toByteArray()); 59 | buffer.append("\r\n"); 60 | } 61 | buffer.append("\r\n"); 62 | writeToSocket(buffer); 63 | socket->flush(); 64 | sentHeaders = true; 65 | } 66 | 67 | bool HttpResponse::writeToSocket(QByteArray data) { 68 | int remaining = data.size(); 69 | char *ptr = data.data(); 70 | while (socket->isOpen() && remaining > 0) { 71 | // If the output buffer has become large, then wait until it has been sent. 72 | if (socket->bytesToWrite() > 16384) { 73 | socket->waitForBytesWritten(-1); 74 | } 75 | 76 | qint64 written = socket->write(ptr, remaining); 77 | if (written == -1) { 78 | return false; 79 | } 80 | ptr += written; 81 | remaining -= written; 82 | } 83 | return true; 84 | } 85 | 86 | void HttpResponse::write(QByteArray data, bool lastPart) { 87 | Q_ASSERT(sentLastPart == false); 88 | 89 | // Send HTTP headers, if not already done (that happens only on the first call to write()) 90 | if (sentHeaders == false) { 91 | // If the whole response is generated with a single call to write(), then we know the total 92 | // size of the response and therefore can set the Content-Length header automatically. 93 | if (lastPart) { 94 | // Automatically set the Content-Length header 95 | headers.insert("Content-Length", QByteArray::number(data.size())); 96 | } 97 | 98 | // else if we will not close the connection at the end, them we must use the chunked mode. 99 | else { 100 | QByteArray connectionValue = headers.value("Connection", headers.value("connection")); 101 | bool connectionClose = QString::compare(connectionValue, "close", Qt::CaseInsensitive) == 0; 102 | if (!connectionClose) { 103 | headers.insert("Transfer-Encoding", "chunked"); 104 | chunkedMode = true; 105 | } 106 | } 107 | 108 | writeHeaders(); 109 | } 110 | 111 | // Send data 112 | if (data.size() > 0) { 113 | if (chunkedMode) { 114 | if (data.size() > 0) { 115 | QByteArray size = QByteArray::number(data.size(), 16); 116 | writeToSocket(size); 117 | writeToSocket("\r\n"); 118 | writeToSocket(data); 119 | writeToSocket("\r\n"); 120 | } 121 | } else { 122 | writeToSocket(data); 123 | } 124 | } 125 | 126 | // Only for the last chunk, send the terminating marker and flush the buffer. 127 | if (lastPart) { 128 | if (chunkedMode) { 129 | writeToSocket("0\r\n\r\n"); 130 | } 131 | socket->flush(); 132 | sentLastPart = true; 133 | } 134 | } 135 | 136 | bool HttpResponse::hasSentLastPart() const { 137 | return sentLastPart; 138 | } 139 | 140 | void HttpResponse::setCookie(const HttpCookie &cookie) { 141 | Q_ASSERT(sentHeaders == false); 142 | if (!cookie.getName().isEmpty()) { 143 | cookies.insert(cookie.getName(), cookie); 144 | } 145 | } 146 | 147 | QMap &HttpResponse::getCookies() { 148 | return cookies; 149 | } 150 | 151 | void HttpResponse::redirect(const QByteArray &url) { 152 | setStatus(303, "See Other"); 153 | setHeader("Location", url); 154 | write("Redirect", true); 155 | } 156 | 157 | void HttpResponse::flush() { 158 | socket->flush(); 159 | } 160 | 161 | bool HttpResponse::isConnected() const { 162 | return socket->isOpen(); 163 | } 164 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpresponse.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httpcookie.h" 9 | #include "qtwebappglobal.h" 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace qtwebapp { 16 | 17 | /** 18 | This object represents a HTTP response, used to return something to the web client. 19 |

20 |

 21 | 	    response.setStatus(200,"OK"); // optional, because this is the default
 22 | 	    response.writeBody("Hello");
 23 | 	    response.writeBody("World!",true);
 24 | 	  
25 |

26 | Example how to return an error: 27 |

 28 | 	    response.setStatus(500,"server error");
 29 | 	    response.write("The request cannot be processed because the servers is broken",true);
 30 | 	  
31 |

32 | In case of large responses (e.g. file downloads), a Content-Length header should be set 33 | before calling write(). Web Browsers use that information to display a progress bar. 34 | */ 35 | 36 | class QTWEBAPP_EXPORT HttpResponse { 37 | Q_DISABLE_COPY(HttpResponse) 38 | public: 39 | /** 40 | Constructor. 41 | @param socket used to write the response 42 | */ 43 | HttpResponse(QTcpSocket *socket); 44 | 45 | /** 46 | Set a HTTP response header. 47 | You must call this method before the first write(). 48 | @param name name of the header 49 | @param value value of the header 50 | */ 51 | void setHeader(const QByteArray name, const QByteArray value); 52 | 53 | /** 54 | Set a HTTP response header. 55 | You must call this method before the first write(). 56 | @param name name of the header 57 | @param value value of the header 58 | */ 59 | void setHeader(const QByteArray name, const int value); 60 | 61 | /** Get the map of HTTP response headers */ 62 | QMap &getHeaders(); 63 | 64 | /** Get the map of cookies */ 65 | QMap &getCookies(); 66 | 67 | /** 68 | Set status code and description. The default is 200,OK. 69 | You must call this method before the first write(). 70 | */ 71 | void setStatus(const int statusCode, const QByteArray description = QByteArray()); 72 | 73 | /** Return the status code. */ 74 | int getStatusCode() const; 75 | 76 | /** 77 | Write body data to the socket. 78 |

79 | The HTTP status line, headers and cookies are sent automatically before the body. 80 |

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

84 | Chunked mode is automatically selected if there is no Content-Length header 85 | and also no Connection:close header. 86 | @param data Data bytes of the body 87 | @param lastPart Indicates that this is the last chunk of data and flushes the output buffer. 88 | */ 89 | void write(const QByteArray data, const bool lastPart = false); 90 | 91 | /** 92 | Indicates whether the body has been sent completely (write() has been called with lastPart=true). 93 | */ 94 | bool hasSentLastPart() const; 95 | 96 | /** 97 | Set a cookie. 98 | You must call this method before the first write(). 99 | */ 100 | void setCookie(const HttpCookie &cookie); 101 | 102 | /** 103 | Send a redirect response to the browser. 104 | Cannot be combined with write(). 105 | @param url Destination URL 106 | */ 107 | void redirect(const QByteArray &url); 108 | 109 | /** 110 | * Flush the output buffer (of the underlying socket). 111 | * You normally don't need to call this method because flush is 112 | * automatically called after HttpRequestHandler::service() returns. 113 | */ 114 | void flush(); 115 | 116 | /** 117 | * May be used to check whether the connection to the web client has been lost. 118 | * This might be useful to cancel the generation of large or slow responses. 119 | */ 120 | bool isConnected() const; 121 | 122 | private: 123 | /** Request headers */ 124 | QMap headers; 125 | 126 | /** Socket for writing output */ 127 | QTcpSocket *socket; 128 | 129 | /** HTTP status code*/ 130 | int statusCode; 131 | 132 | /** HTTP status code description */ 133 | QByteArray statusText; 134 | 135 | /** Indicator whether headers have been sent */ 136 | bool sentHeaders; 137 | 138 | /** Indicator whether the body has been sent completely */ 139 | bool sentLastPart; 140 | 141 | /** Whether the response is sent in chunked mode */ 142 | bool chunkedMode; 143 | 144 | /** Cookies */ 145 | QMap cookies; 146 | 147 | /** Write raw data to the socket. This method blocks until all bytes have been passed to the TCP buffer */ 148 | bool writeToSocket(QByteArray data); 149 | 150 | /** 151 | Write the response HTTP status and headers to the socket. 152 | Calling this method is optional, because writeBody() calls 153 | it automatically when required. 154 | */ 155 | void writeHeaders(); 156 | }; 157 | 158 | } // namespace qtwebapp 159 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpserverconfig.cpp: -------------------------------------------------------------------------------- 1 | #include "httpserverconfig.h" 2 | 3 | using namespace qtwebapp; 4 | 5 | HttpServerConfig::HttpServerConfig() {} 6 | 7 | HttpServerConfig::HttpServerConfig(const QSettings &settings) { 8 | parseSettings(settings); 9 | } 10 | 11 | HttpServerConfig::HttpServerConfig(QSettings *settings) { 12 | parseSettings(*settings); 13 | } 14 | 15 | void HttpServerConfig::parseSettings(const QSettings &settings) { 16 | #ifdef Q_OS_WIN 17 | if (settings.format() != QSettings::NativeFormat) 18 | #endif 19 | fileName = settings.fileName(); 20 | 21 | QString hoststr = settings.value("host").toString(); 22 | host = hoststr.isEmpty() ? QHostAddress::Any : QHostAddress(hoststr); 23 | port = settings.value("port", port).toUInt(); 24 | 25 | maxRequestSize = parseNum(settings.value("maxRequestSize", maxRequestSize), 1024); 26 | maxMultipartSize = parseNum(settings.value("maxMultipartSize", maxMultipartSize), 1024); 27 | 28 | cleanupInterval = parseNum(settings.value("cleanupInterval", cleanupInterval)); 29 | 30 | minThreads = parseNum(settings.value("minThreads", minThreads)); 31 | maxThreads = parseNum(settings.value("maxThreads", maxThreads)); 32 | 33 | sslKeyFile = settings.value("sslKeyFile").toString(); 34 | sslCertFile = settings.value("sslCertFile").toString(); 35 | } 36 | 37 | // ########################################################################################### 38 | 39 | HttpSessionStoreConfig::HttpSessionStoreConfig() {} 40 | 41 | HttpSessionStoreConfig::HttpSessionStoreConfig(const QSettings &settings) { 42 | parseSettings(settings); 43 | } 44 | 45 | HttpSessionStoreConfig::HttpSessionStoreConfig(QSettings *settings) { 46 | parseSettings(*settings); 47 | } 48 | 49 | void HttpSessionStoreConfig::parseSettings(const QSettings &settings) { 50 | expirationTime = parseNum(settings.value("expirationTime", expirationTime), 1000); 51 | cookieName = settings.value("cookieName", cookieName).toByteArray(); 52 | 53 | cookiePath = settings.value("cookiePath", cookiePath).toByteArray(); 54 | cookieComment = settings.value("cookieComment", cookieComment).toByteArray(); 55 | cookieDomain = settings.value("cookieDomain", cookieDomain).toByteArray(); 56 | } 57 | 58 | // ########################################################################################### 59 | 60 | StaticFileControllerConfig::StaticFileControllerConfig() {} 61 | 62 | StaticFileControllerConfig::StaticFileControllerConfig(const QSettings &settings) { 63 | parseSettings(settings); 64 | } 65 | 66 | StaticFileControllerConfig::StaticFileControllerConfig(QSettings *settings) { 67 | parseSettings(*settings); 68 | } 69 | 70 | void StaticFileControllerConfig::parseSettings(const QSettings &settings) { 71 | #ifdef Q_OS_WIN 72 | if (settings.format() != QSettings::NativeFormat) 73 | #endif 74 | fileName = settings.fileName(); 75 | 76 | path = settings.value("path", path).toString(); 77 | encoding = settings.value("encoding", encoding).toString(); 78 | 79 | maxAge = parseNum(settings.value("maxAge", maxAge)); 80 | maxCachedFileSize = parseNum(settings.value("maxCachedFileSize", maxCachedFileSize), 1024); 81 | 82 | cacheSize = parseNum(settings.value("cacheSize", cacheSize), 1024); 83 | cacheTime = parseNum(settings.value("cacheTime", cacheTime)); 84 | } 85 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpserverconfig.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qtwebappglobal.h" 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace qtwebapp { 10 | class HttpConnectionHandlerPool; 11 | class StaticFileController; 12 | 13 | /** 14 | * This class stores all configuration information for the `HttpListener` 15 | * class. It can be either created as a standard object and filled from 16 | * c++ code, or be constructed from a `QSettings` object. See `HttpListener` 17 | * for an example of such a configuration file. 18 | */ 19 | class QTWEBAPP_EXPORT HttpServerConfig { 20 | friend class HttpConnectionHandlerPool; 21 | 22 | public: 23 | /** Creates a config with all standard values. */ 24 | HttpServerConfig(); 25 | /** Reads the configuration from the `QSettings` object. */ 26 | HttpServerConfig(const QSettings &settings); 27 | /** Reads the configuration from the `QSettings` object. */ 28 | HttpServerConfig(QSettings *settings); 29 | 30 | /// The address for the server to listen on. 31 | QHostAddress host = QHostAddress::Any; 32 | /// The port for the server to listen on. 33 | quint16 port = 0; 34 | 35 | /// The maximum size of an HTTP request. 36 | int maxRequestSize = 16e3; 37 | /// The maximum size of a body of a multipart/form-data request. 38 | int maxMultipartSize = 1e6; 39 | 40 | /// The maximum amount of time to wait for an HTTP request to complete. 41 | int readTimeout = 1e4; 42 | 43 | /// The interval to search for idle connection handlers and kill them. 44 | int cleanupInterval = 1e3; 45 | /// The minimum of idle connection handlers to keep. 46 | int minThreads = 1; 47 | /// The maximum amount of connection handlers. 48 | int maxThreads = 100; 49 | 50 | /// The file required for SSL support. 51 | QString sslKeyFile, sslCertFile; 52 | 53 | // Temporary directory 54 | QString tmpDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); 55 | 56 | private: 57 | void parseSettings(const QSettings &settings); 58 | 59 | /// The filename of the settings if read from a file. It is used to resolve 60 | /// relative paths. 61 | QString fileName; 62 | }; 63 | 64 | /** 65 | * This class stores all configuration information for the `HttpSessionStore` 66 | * class. It can be either created as a standard object and filled from 67 | * c++ code, or be constructed from a `QSettings` object. See `HttpSessionStore` 68 | * for an example of such a configuration file. 69 | */ 70 | class QTWEBAPP_EXPORT HttpSessionStoreConfig { 71 | public: 72 | /** Creates a config with all standard values. */ 73 | HttpSessionStoreConfig(); 74 | /** Reads the configuration from the `QSettings` object. */ 75 | HttpSessionStoreConfig(const QSettings &settings); 76 | /** Reads the configuration frem the `QSettings` pointer. */ 77 | HttpSessionStoreConfig(QSettings *settings); 78 | 79 | /// The expiration time of the cookie. 80 | qint64 expirationTime = 3600e3; 81 | /// The name of the cookie. 82 | QByteArray cookieName = "sessionid"; 83 | 84 | /// The url path where the session is valid. This is usefull when you have 85 | /// data not related to the session in `/static/` and session related data in 86 | /// `/content/` or similar. 87 | QByteArray cookiePath = "/"; 88 | /// The comment of the cookie. 89 | QByteArray cookieComment; 90 | /// The domain of the cookie. 91 | QByteArray cookieDomain; 92 | 93 | private: 94 | void parseSettings(const QSettings &settings); 95 | }; 96 | 97 | /** 98 | * This class stores all configuration information for the `StaticFileController` 99 | * class. It can be either created as a astandard object and filled from c++ 100 | * code, or be constructed from a `QSettings` object. See `StaticFileController` 101 | * for an example of such a configuration file. 102 | */ 103 | class QTWEBAPP_EXPORT StaticFileControllerConfig { 104 | friend class StaticFileController; 105 | 106 | public: 107 | /** Creates a config with all standard values. */ 108 | StaticFileControllerConfig(); 109 | /** Reads the configuration from the `QSettings` object. */ 110 | StaticFileControllerConfig(const QSettings &settings); 111 | /** Reads the configuration from the `QSettings` object. */ 112 | StaticFileControllerConfig(QSettings *settings); 113 | 114 | /// The path where the static files can be found. This can be either an 115 | /// absolute or relativ path or an qt resource path. 116 | QString path = "."; 117 | 118 | /// The encoding that is sent to the web browser in case of text files. 119 | QString encoding = "UTF-8"; 120 | 121 | /// The amount of time the file should reside in the browsers cache. 122 | int maxAge = 6e4; 123 | 124 | /// The maximum size of a file to get cached. 125 | int maxCachedFileSize = 2 << 15; 126 | 127 | /// The size of the server cache. 128 | int cacheSize = 1e6; 129 | /// The timeout of each file in the servers cache. 130 | int cacheTime = 6e4; 131 | 132 | private: 133 | void parseSettings(const QSettings &settings); 134 | 135 | /// The filename of the settings if read from a file. It is used to resolve 136 | /// relative paths. 137 | QString fileName; 138 | }; 139 | 140 | } // namespace qtwebapp 141 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpsession.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "httpsession.h" 7 | 8 | #include 9 | #include 10 | 11 | using namespace qtwebapp; 12 | 13 | HttpSession::HttpSession(bool canStore) { 14 | if (canStore) { 15 | dataPtr = new HttpSessionData(); 16 | dataPtr->refCount = 1; 17 | dataPtr->lastAccess = QDateTime::currentMSecsSinceEpoch(); 18 | dataPtr->id = QUuid::createUuid().toString().toLocal8Bit(); 19 | #ifdef SUPERVERBOSE 20 | qDebug("HttpSession: (constructor) new session %s with refCount=1", dataPtr->id.constData()); 21 | #endif 22 | } else { 23 | dataPtr = nullptr; 24 | } 25 | } 26 | 27 | HttpSession::HttpSession(const HttpSession &other) { 28 | dataPtr = other.dataPtr; 29 | if (dataPtr) { 30 | dataPtr->lock.lockForWrite(); 31 | dataPtr->refCount++; 32 | #ifdef SUPERVERBOSE 33 | qDebug("HttpSession: (constructor) copy session %s refCount=%i", dataPtr->id.constData(), dataPtr->refCount); 34 | #endif 35 | dataPtr->lock.unlock(); 36 | } 37 | } 38 | 39 | HttpSession &HttpSession::operator=(const HttpSession &other) { 40 | HttpSessionData *oldPtr = dataPtr; 41 | dataPtr = other.dataPtr; 42 | if (dataPtr) { 43 | dataPtr->lock.lockForWrite(); 44 | dataPtr->refCount++; 45 | #ifdef SUPERVERBOSE 46 | qDebug("HttpSession: (operator=) session %s refCount=%i", dataPtr->id.constData(), dataPtr->refCount); 47 | #endif 48 | dataPtr->lastAccess = QDateTime::currentMSecsSinceEpoch(); 49 | dataPtr->lock.unlock(); 50 | } 51 | if (oldPtr) { 52 | int refCount; 53 | oldPtr->lock.lockForWrite(); 54 | refCount = --oldPtr->refCount; 55 | #ifdef SUPERVERBOSE 56 | qDebug("HttpSession: (operator=) session %s refCount=%i", oldPtr->id.constData(), oldPtr->refCount); 57 | #endif 58 | oldPtr->lock.unlock(); 59 | if (refCount == 0) { 60 | qDebug("HttpSession: deleting old data"); 61 | delete oldPtr; 62 | } 63 | } 64 | return *this; 65 | } 66 | 67 | HttpSession::~HttpSession() { 68 | if (dataPtr) { 69 | int refCount; 70 | dataPtr->lock.lockForWrite(); 71 | refCount = --dataPtr->refCount; 72 | #ifdef SUPERVERBOSE 73 | qDebug("HttpSession: (destructor) session %s refCount=%i", dataPtr->id.constData(), dataPtr->refCount); 74 | #endif 75 | dataPtr->lock.unlock(); 76 | if (refCount == 0) { 77 | #ifdef CMAKE_DEBUG 78 | qDebug("HttpSession: deleting data"); 79 | #endif 80 | delete dataPtr; 81 | } 82 | } 83 | } 84 | 85 | QByteArray HttpSession::getId() const { 86 | if (dataPtr) { 87 | return dataPtr->id; 88 | } else { 89 | return QByteArray(); 90 | } 91 | } 92 | 93 | bool HttpSession::isNull() const { 94 | return dataPtr == nullptr; 95 | } 96 | 97 | void HttpSession::set(const QByteArray &key, const QVariant &value) { 98 | if (dataPtr) { 99 | dataPtr->lock.lockForWrite(); 100 | dataPtr->values.insert(key, value); 101 | dataPtr->lock.unlock(); 102 | } 103 | } 104 | 105 | void HttpSession::remove(const QByteArray &key) { 106 | if (dataPtr) { 107 | dataPtr->lock.lockForWrite(); 108 | dataPtr->values.remove(key); 109 | dataPtr->lock.unlock(); 110 | } 111 | } 112 | 113 | QVariant HttpSession::get(const QByteArray &key) const { 114 | QVariant value; 115 | if (dataPtr) { 116 | dataPtr->lock.lockForRead(); 117 | value = dataPtr->values.value(key); 118 | dataPtr->lock.unlock(); 119 | } 120 | return value; 121 | } 122 | 123 | bool HttpSession::contains(const QByteArray &key) const { 124 | bool found = false; 125 | if (dataPtr) { 126 | dataPtr->lock.lockForRead(); 127 | found = dataPtr->values.contains(key); 128 | dataPtr->lock.unlock(); 129 | } 130 | return found; 131 | } 132 | 133 | QMap HttpSession::getAll() const { 134 | QMap values; 135 | if (dataPtr) { 136 | dataPtr->lock.lockForRead(); 137 | values = dataPtr->values; 138 | dataPtr->lock.unlock(); 139 | } 140 | return values; 141 | } 142 | 143 | qint64 HttpSession::getLastAccess() const { 144 | qint64 value = 0; 145 | if (dataPtr) { 146 | dataPtr->lock.lockForRead(); 147 | value = dataPtr->lastAccess; 148 | dataPtr->lock.unlock(); 149 | } 150 | return value; 151 | } 152 | 153 | void HttpSession::setLastAccess() { 154 | if (dataPtr) { 155 | dataPtr->lock.lockForWrite(); 156 | dataPtr->lastAccess = QDateTime::currentMSecsSinceEpoch(); 157 | dataPtr->lock.unlock(); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpsession.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "qtwebappglobal.h" 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | namespace qtwebapp { 15 | 16 | /** 17 | This class stores data for a single HTTP session. 18 | A session can store any number of key/value pairs. This class uses implicit 19 | sharing for read and write access. This class is thread safe. 20 | @see HttpSessionStore should be used to create and get instances of this class. 21 | */ 22 | 23 | class QTWEBAPP_EXPORT HttpSession { 24 | 25 | public: 26 | /** 27 | Constructor. 28 | @param canStore The session can store data, if this parameter is true. 29 | Otherwise all calls to set() and remove() do not have any effect. 30 | */ 31 | HttpSession(const bool canStore = false); 32 | 33 | /** 34 | Copy constructor. Creates another HttpSession object that shares the 35 | data of the other object. 36 | */ 37 | HttpSession(const HttpSession &other); 38 | 39 | /** 40 | Copy operator. Detaches from the current shared data and attaches to 41 | the data of the other object. 42 | */ 43 | HttpSession &operator=(const HttpSession &other); 44 | 45 | /** 46 | Destructor. Detaches from the shared data. 47 | */ 48 | virtual ~HttpSession(); 49 | 50 | /** Get the unique ID of this session. This method is thread safe. */ 51 | QByteArray getId() const; 52 | 53 | /** 54 | Null sessions cannot store data. All calls to set() and remove() 55 | do not have any effect.This method is thread safe. 56 | */ 57 | bool isNull() const; 58 | 59 | /** Set a value. This method is thread safe. */ 60 | void set(const QByteArray &key, const QVariant &value); 61 | 62 | /** Remove a value. This method is thread safe. */ 63 | void remove(const QByteArray &key); 64 | 65 | /** Get a value. This method is thread safe. */ 66 | QVariant get(const QByteArray &key) const; 67 | 68 | /** Check if a key exists. This method is thread safe. */ 69 | bool contains(const QByteArray &key) const; 70 | 71 | /** 72 | Get a copy of all data stored in this session. 73 | Changes to the session do not affect the copy and vice versa. 74 | This method is thread safe. 75 | */ 76 | QMap getAll() const; 77 | 78 | /** 79 | Get the timestamp of last access. That is the time when the last 80 | HttpSessionStore::getSession() has been called. 81 | This method is thread safe. 82 | */ 83 | qint64 getLastAccess() const; 84 | 85 | /** 86 | Set the timestamp of last access, to renew the timeout period. 87 | Called by HttpSessionStore::getSession(). 88 | This method is thread safe. 89 | */ 90 | void setLastAccess(); 91 | 92 | private: 93 | struct HttpSessionData { 94 | 95 | /** Unique ID */ 96 | QByteArray id; 97 | 98 | /** Timestamp of last access, set by the HttpSessionStore */ 99 | qint64 lastAccess; 100 | 101 | /** Reference counter */ 102 | int refCount; 103 | 104 | /** Used to synchronize threads */ 105 | QReadWriteLock lock; 106 | 107 | /** Storage for the key/value pairs; */ 108 | QMap values; 109 | }; 110 | 111 | /** Pointer to the shared data. */ 112 | HttpSessionData *dataPtr; 113 | }; 114 | 115 | } // namespace qtwebapp 116 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpsessionstore.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "httpsessionstore.h" 7 | 8 | #include 9 | #include 10 | 11 | using namespace qtwebapp; 12 | 13 | HttpSessionStore::HttpSessionStore(const HttpSessionStoreConfig &cfg, QObject *parent) : QObject(parent), cfg(cfg) { 14 | connect(&cleanupTimer, SIGNAL(timeout()), this, SLOT(sessionTimerEvent())); 15 | cleanupTimer.start(60000); 16 | #ifdef CMAKE_DEBUG 17 | qDebug("HttpSessionStore: Sessions expire after %lli milliseconds", cfg.expirationTime); 18 | #endif 19 | } 20 | 21 | HttpSessionStore::~HttpSessionStore() { 22 | cleanupTimer.stop(); 23 | } 24 | 25 | QByteArray HttpSessionStore::getSessionId(HttpRequest &request, HttpResponse &response) { 26 | // The session ID in the response has priority because this one will be used in the next request. 27 | mutex.lock(); 28 | // Get the session ID from the response cookie 29 | QByteArray sessionId = response.getCookies().value(cfg.cookieName).getValue(); 30 | if (sessionId.isEmpty()) { 31 | // Get the session ID from the request cookie 32 | sessionId = request.getCookie(cfg.cookieName); 33 | } 34 | // Clear the session ID if there is no such session in the storage. 35 | if (!sessionId.isEmpty()) { 36 | if (!sessions.contains(sessionId)) { 37 | qDebug("HttpSessionStore: received invalid session cookie with ID %s", sessionId.data()); 38 | sessionId.clear(); 39 | } 40 | } 41 | mutex.unlock(); 42 | return sessionId; 43 | } 44 | 45 | HttpSession HttpSessionStore::getSession(HttpRequest &request, HttpResponse &response, bool allowCreate) { 46 | QByteArray sessionId = getSessionId(request, response); 47 | mutex.lock(); 48 | if (!sessionId.isEmpty()) { 49 | HttpSession session = sessions.value(sessionId); 50 | if (!session.isNull()) { 51 | mutex.unlock(); 52 | // Refresh the session cookie 53 | response.setCookie(HttpCookie(cfg.cookieName, session.getId(), cfg.expirationTime / 1000, cfg.cookiePath, 54 | cfg.cookieComment, cfg.cookieDomain, false, false, "Lax")); 55 | session.setLastAccess(); 56 | return session; 57 | } 58 | } 59 | // Need to create a new session 60 | if (allowCreate) { 61 | HttpSession session(true); 62 | #ifdef CMAKE_DEBUG 63 | qDebug("HttpSessionStore: create new session with ID %s", session.getId().data()); 64 | #endif 65 | sessions.insert(session.getId(), session); 66 | response.setCookie(HttpCookie(cfg.cookieName, session.getId(), cfg.expirationTime / 1000, cfg.cookiePath, 67 | cfg.cookieComment, cfg.cookieDomain, false, false, "Lax")); 68 | mutex.unlock(); 69 | return session; 70 | } 71 | // Return a null session 72 | mutex.unlock(); 73 | return HttpSession(); 74 | } 75 | 76 | HttpSession HttpSessionStore::getSession(const QByteArray id) { 77 | mutex.lock(); 78 | HttpSession session = sessions.value(id); 79 | mutex.unlock(); 80 | session.setLastAccess(); 81 | return session; 82 | } 83 | 84 | void HttpSessionStore::sessionTimerEvent() { 85 | mutex.lock(); 86 | qint64 now = QDateTime::currentMSecsSinceEpoch(); 87 | QMap::iterator i = sessions.begin(); 88 | while (i != sessions.end()) { 89 | QMap::iterator prev = i; 90 | ++i; 91 | HttpSession session = prev.value(); 92 | qint64 lastAccess = session.getLastAccess(); 93 | if (now - lastAccess > cfg.expirationTime) { 94 | qDebug("HttpSessionStore: session %s expired", session.getId().data()); 95 | emit sessionDeleted(session.getId()); 96 | sessions.erase(prev); 97 | } 98 | } 99 | mutex.unlock(); 100 | } 101 | 102 | /** Delete a session */ 103 | void HttpSessionStore::removeSession(HttpSession session) { 104 | mutex.lock(); 105 | emit sessionDeleted(session.getId()); 106 | sessions.remove(session.getId()); 107 | mutex.unlock(); 108 | } 109 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/httpsessionstore.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httprequest.h" 9 | #include "httpresponse.h" 10 | #include "httpsession.h" 11 | #include "qtwebappglobal.h" 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace qtwebapp { 19 | 20 | /** 21 | Stores HTTP sessions and deletes them when they have expired. 22 | The following configuration settings are required in the config file: 23 |

 24 | 	  expirationTime=3600000
 25 | 	  cookieName=sessionid
 26 | 	  
27 | The following additional configurations settings are optionally: 28 |
 29 | 	  cookiePath=/
 30 | 	  cookieComment=Session ID
 31 | 	  ;cookieDomain=stefanfrings.de
 32 | 	  
33 | */ 34 | 35 | class QTWEBAPP_EXPORT HttpSessionStore : public QObject { 36 | Q_OBJECT 37 | Q_DISABLE_COPY(HttpSessionStore) 38 | public: 39 | /** Constructor. */ 40 | HttpSessionStore(const HttpSessionStoreConfig &cfg, QObject *parent = nullptr); 41 | 42 | /** Destructor */ 43 | virtual ~HttpSessionStore(); 44 | 45 | /** 46 | Get the ID of the current HTTP session, if it is valid. 47 | This method is thread safe. 48 | @warning Sessions may expire at any time, so subsequent calls of 49 | getSession() might return a new session with a different ID. 50 | @param request Used to get the session cookie 51 | @param response Used to get and set the new session cookie 52 | @return Empty string, if there is no valid session. 53 | */ 54 | QByteArray getSessionId(HttpRequest &request, HttpResponse &response); 55 | 56 | /** 57 | Get the session of a HTTP request, eventually create a new one. 58 | This method is thread safe. New sessions can only be created before 59 | the first byte has been written to the HTTP response. 60 | @param request Used to get the session cookie 61 | @param response Used to get and set the new session cookie 62 | @param allowCreate can be set to false, to disable the automatic creation of a new session. 63 | @return If autoCreate is disabled, the function returns a null session if there is no session. 64 | @see HttpSession::isNull() 65 | */ 66 | HttpSession getSession(HttpRequest &request, HttpResponse &response, const bool allowCreate = true); 67 | 68 | /** 69 | Get a HTTP session by it's ID number. 70 | This method is thread safe. 71 | @return If there is no such session, the function returns a null session. 72 | @param id ID number of the session 73 | @see HttpSession::isNull() 74 | */ 75 | HttpSession getSession(const QByteArray id); 76 | 77 | /** Delete a session */ 78 | void removeSession(const HttpSession session); 79 | 80 | protected: 81 | /** Storage for the sessions */ 82 | QMap sessions; 83 | 84 | private: 85 | /** Configuration settings */ 86 | HttpSessionStoreConfig cfg; 87 | 88 | /** Timer to remove expired sessions */ 89 | QTimer cleanupTimer; 90 | 91 | /** Used to synchronize threads */ 92 | QMutex mutex; 93 | 94 | private slots: 95 | 96 | /** Called every minute to cleanup expired sessions. */ 97 | void sessionTimerEvent(); 98 | 99 | signals: 100 | 101 | /** 102 | Emitted when the session is deleted. 103 | @param sessionId The ID number of the session. 104 | */ 105 | void sessionDeleted(const QByteArray &sessionId); 106 | }; 107 | 108 | } // namespace qtwebapp 109 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/staticfilecontroller.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "staticfilecontroller.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace qtwebapp; 14 | 15 | StaticFileController::StaticFileController(const StaticFileControllerConfig &cfg, QObject *parent) 16 | : HttpRequestHandler(parent) { 17 | maxAge = cfg.maxAge; 18 | encoding = cfg.encoding; 19 | docroot = cfg.path; 20 | if (!cfg.fileName.isEmpty() && !docroot.startsWith(":/") && !docroot.startsWith("qrc://")) { 21 | // Convert relative path to absolute, based on the directory of the config file. 22 | if (QDir::isRelativePath(docroot)) { 23 | QFileInfo configFile(cfg.fileName); 24 | docroot = QFileInfo(configFile.absolutePath(), docroot).absoluteFilePath(); 25 | } 26 | } 27 | #ifdef CMAKE_DEBUG 28 | qDebug("StaticFileController: docroot=%s, encoding=%s, maxAge=%i", qPrintable(docroot), qPrintable(encoding), maxAge); 29 | #endif 30 | maxCachedFileSize = cfg.maxCachedFileSize; 31 | cache.setMaxCost(cfg.cacheSize); 32 | cacheTimeout = cfg.cacheTime; 33 | long int cacheMaxCost = (long int)cache.maxCost(); 34 | #ifdef CMAKE_DEBUG 35 | qDebug("StaticFileController: cache timeout=%i, size=%li", cacheTimeout, cacheMaxCost); 36 | #endif 37 | } 38 | 39 | void StaticFileController::service(HttpRequest &request, HttpResponse &response) { 40 | QByteArray path = request.getPath(); 41 | // Check whether the browsers cache is up to date 42 | if (!request.getHeader("If-None-Match").isEmpty() && 43 | request.getHeader("If-None-Match") == ("\"" + etag.value(path) + "\"")) { 44 | response.setStatus(304, "Not Modified"); 45 | return; 46 | } 47 | // Check if we have the file in cache 48 | qint64 now = QDateTime::currentMSecsSinceEpoch(); 49 | mutex.lock(); 50 | CacheEntry *entry = cache.object(path); 51 | if (entry && (cacheTimeout == 0 || entry->created > now - cacheTimeout)) { 52 | QByteArray document = entry->document; // copy the cached document, because other threads may destroy the cached 53 | // entry immediately after mutex unlock. 54 | QByteArray filename = entry->filename; 55 | response.setHeader("ETag", "\"" + etag.value(path) + "\""); 56 | mutex.unlock(); 57 | #ifdef CMAKE_DEBUG 58 | qDebug("StaticFileController: Cache hit for %s", path.data()); 59 | #endif 60 | setContentType(filename, response); 61 | response.setHeader("Cache-Control", "max-age=" + QByteArray::number(maxAge / 1000)); 62 | response.write(document); 63 | } else { 64 | mutex.unlock(); 65 | // The file is not in cache. 66 | #ifdef CMAKE_DEBUG 67 | qDebug("StaticFileController: Cache miss for %s", path.data()); 68 | #endif 69 | // Forbid access to files outside the docroot directory 70 | if (path.contains("/..")) { 71 | qWarning("StaticFileController: detected forbidden characters in path %s", path.data()); 72 | response.setStatus(403, "forbidden"); 73 | response.write("403 forbidden", true); 74 | return; 75 | } 76 | // If the filename is a directory, append index.html. 77 | if (QFileInfo(docroot + path).isDir()) { 78 | path += "/index.html"; 79 | } 80 | // Try to open the file 81 | QFile file(docroot + path); 82 | #ifdef CMAKE_DEBUG 83 | qDebug("StaticFileController: Open file %s", qPrintable(file.fileName())); 84 | #endif 85 | if (file.open(QIODevice::ReadOnly)) { 86 | setContentType(path, response); 87 | response.setHeader("Cache-Control", "max-age=" + QByteArray::number(maxAge / 1000)); 88 | if (file.size() <= maxCachedFileSize) { 89 | // Return the file content and store it also in the cache 90 | entry = new CacheEntry(); 91 | while (!file.atEnd() && !file.error()) { 92 | QByteArray buffer = file.read(65536); 93 | entry->document.append(buffer); 94 | } 95 | entry->created = now; 96 | entry->filename = path; 97 | mutex.lock(); 98 | etag.insert(path, QCryptographicHash::hash(entry->document, QCryptographicHash::Md5).toHex()); 99 | response.setHeader("ETag", "\"" + etag.value(path) + "\""); 100 | response.write(entry->document); 101 | cache.insert(request.getPath(), entry, entry->document.size()); 102 | mutex.unlock(); 103 | } else { 104 | // Return the file content, do not store in cache 105 | while (!file.atEnd() && !file.error()) { 106 | response.write(file.read(65536)); 107 | } 108 | } 109 | file.close(); 110 | } else { 111 | if (file.exists()) { 112 | qWarning("StaticFileController: Cannot open existing file %s for reading", qPrintable(file.fileName())); 113 | response.setStatus(403, "forbidden"); 114 | response.write("403 forbidden", true); 115 | } else { 116 | response.setStatus(404, "not found"); 117 | response.write("404 not found", true); 118 | } 119 | } 120 | } 121 | } 122 | 123 | void StaticFileController::setContentType(const QString &fileName, HttpResponse &response) const { 124 | if (fileName.endsWith(".png")) { 125 | response.setHeader("Content-Type", "image/png"); 126 | } else if (fileName.endsWith(".jpg")) { 127 | response.setHeader("Content-Type", "image/jpeg"); 128 | } else if (fileName.endsWith(".gif")) { 129 | response.setHeader("Content-Type", "image/gif"); 130 | } else if (fileName.endsWith(".pdf")) { 131 | response.setHeader("Content-Type", "application/pdf"); 132 | } else if (fileName.endsWith(".txt")) { 133 | response.setHeader("Content-Type", qPrintable("text/plain; charset=" + encoding)); 134 | } else if (fileName.endsWith(".html") || fileName.endsWith(".htm")) { 135 | response.setHeader("Content-Type", qPrintable("text/html; charset=" + encoding)); 136 | } else if (fileName.endsWith(".css")) { 137 | response.setHeader("Content-Type", "text/css"); 138 | } else if (fileName.endsWith(".js")) { 139 | response.setHeader("Content-Type", "text/javascript"); 140 | } else if (fileName.endsWith(".svg")) { 141 | response.setHeader("Content-Type", "image/svg+xml"); 142 | } else if (fileName.endsWith(".woff")) { 143 | response.setHeader("Content-Type", "font/woff"); 144 | } else if (fileName.endsWith(".woff2")) { 145 | response.setHeader("Content-Type", "font/woff2"); 146 | } else if (fileName.endsWith(".ttf")) { 147 | response.setHeader("Content-Type", "application/x-font-ttf"); 148 | } else if (fileName.endsWith(".eot")) { 149 | response.setHeader("Content-Type", "application/vnd.ms-fontobject"); 150 | } else if (fileName.endsWith(".otf")) { 151 | response.setHeader("Content-Type", "application/font-otf"); 152 | } else if (fileName.endsWith(".json")) { 153 | response.setHeader("Content-Type", "application/json"); 154 | } else if (fileName.endsWith(".xml")) { 155 | response.setHeader("Content-Type", "text/xml"); 156 | } 157 | // Todo: add all of your content types 158 | else { 159 | qDebug("StaticFileController: unknown MIME type for filename '%s'", qPrintable(fileName)); 160 | response.setHeader("Content-Type", "application/octet-stream"); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /QtWebApp/httpserver/staticfilecontroller.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "httprequest.h" 9 | #include "httprequesthandler.h" 10 | #include "httpresponse.h" 11 | #include "qtwebappglobal.h" 12 | 13 | #include 14 | #include 15 | 16 | namespace qtwebapp { 17 | 18 | /** 19 | Delivers static files. It is usually called by the applications main request handler when 20 | the caller requests a path that is mapped to static files. 21 |

22 | The following settings are required in the config file: 23 |

24 | 	  path=../docroot
25 | 	  encoding=UTF-8
26 | 	  maxAge=60000
27 | 	  cacheTime=60000
28 | 	  cacheSize=1000000
29 | 	  maxCachedFileSize=65536
30 | 	  
31 | The path is relative to the directory of the config file. In case of windows, if the 32 | settings are in the registry, the path is relative to the current working directory. 33 |

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

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

40 | Do not instantiate this class in each request, because this would make the file cache 41 | useless. Better create one instance during start-up and call it when the application 42 | received a related HTTP request. 43 | */ 44 | 45 | class QTWEBAPP_EXPORT StaticFileController : public HttpRequestHandler { 46 | Q_OBJECT 47 | Q_DISABLE_COPY(StaticFileController) 48 | public: 49 | /** Constructor */ 50 | StaticFileController(const StaticFileControllerConfig &cfg, QObject *parent = NULL); 51 | 52 | /** Generates the response */ 53 | void service(HttpRequest &request, HttpResponse &response); 54 | 55 | private: 56 | /** Encoding of text files */ 57 | QString encoding; 58 | 59 | /** Root directory of documents */ 60 | QString docroot; 61 | 62 | /** Maximum age of files in the browser cache */ 63 | int maxAge; 64 | 65 | struct CacheEntry { 66 | QByteArray document; 67 | qint64 created; 68 | QByteArray filename; 69 | }; 70 | 71 | /** Timeout for each cached file */ 72 | int cacheTimeout; 73 | 74 | /** Maximum size of files in cache, larger files are not cached */ 75 | int maxCachedFileSize; 76 | 77 | /** Cache storage */ 78 | QCache cache; 79 | 80 | /** ETag storage */ 81 | QHash etag; 82 | 83 | /** Used to synchronize cache access for threads */ 84 | QMutex mutex; 85 | 86 | /** Set a content-type header in the response depending on the ending of the filename */ 87 | void setContentType(const QString &fileName, HttpResponse &response) const; 88 | }; 89 | 90 | } // namespace qtwebapp 91 | -------------------------------------------------------------------------------- /QtWebApp/logging/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(logging_HEADERS 2 | dualfilelogger.h 3 | filelogger.h 4 | logger.h 5 | logmessage.h 6 | ) 7 | set(logging_SOURCES 8 | dualfilelogger.cpp 9 | filelogger.cpp 10 | logger.cpp 11 | logmessage.cpp 12 | ) 13 | 14 | add_library(QtWebAppLogging SHARED ${logging_HEADERS} ${logging_SOURCES}) 15 | target_include_directories(QtWebAppLogging PUBLIC 16 | $ 17 | $ 18 | ) 19 | target_link_libraries(QtWebAppLogging Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) 20 | set_target_properties(QtWebAppLogging PROPERTIES 21 | VERSION ${qtwebapp_VERSION} 22 | SOVERSION ${qtwebapp_MAJOR} 23 | ) 24 | 25 | install(TARGETS QtWebAppLogging 26 | EXPORT QtWebAppTargets 27 | LIBRARY DESTINATION lib 28 | RUNTIME DESTINATION bin 29 | ARCHIVE DESTINATION lib) 30 | install(FILES ${logging_HEADERS} 31 | DESTINATION include/qtwebapp/logging) 32 | -------------------------------------------------------------------------------- /QtWebApp/logging/dualfilelogger.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "dualfilelogger.h" 7 | 8 | using namespace qtwebapp; 9 | 10 | DualFileLogger::DualFileLogger(QSettings *firstSettings, QSettings *secondSettings, const int refreshInterval, 11 | QObject *parent) 12 | : Logger(parent) { 13 | firstLogger = new FileLogger(firstSettings, refreshInterval, this); 14 | secondLogger = new FileLogger(secondSettings, refreshInterval, this); 15 | } 16 | 17 | void DualFileLogger::log(const QtMsgType type, const QString &message, const QString &file, const QString &function, 18 | const int line) { 19 | firstLogger->log(type, message, file, function, line); 20 | secondLogger->log(type, message, file, function, line); 21 | } 22 | 23 | void DualFileLogger::clear(const bool buffer, const bool variables) { 24 | firstLogger->clear(buffer, variables); 25 | secondLogger->clear(buffer, variables); 26 | } 27 | -------------------------------------------------------------------------------- /QtWebApp/logging/dualfilelogger.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "filelogger.h" 9 | #include "logger.h" 10 | #include "qtwebappglobal.h" 11 | 12 | #include 13 | 14 | namespace qtwebapp { 15 | 16 | /** 17 | Writes log messages into two log files simultaneously. 18 | I recommend to configure: 19 | - One "main" logfile with minLevel=1 or 2 and bufferSize=0. This file is for the operator to see when a problem 20 | occured. 21 | - A second "debug" logfile with minLevel=1 or 2 and bufferSize=100. This file is for the developer who may need more 22 | details (the debug messages) about the situation that leaded to the error. 23 | 24 | @see FileLogger for a description of the two underlying loggers. 25 | */ 26 | 27 | class QTWEBAPP_EXPORT DualFileLogger : public Logger { 28 | Q_OBJECT 29 | Q_DISABLE_COPY(DualFileLogger) 30 | public: 31 | /** 32 | Constructor. 33 | @param firstSettings Configuration settings for the first log file, usually stored in an INI file. 34 | Must not be 0. 35 | Settings are read from the current group, so the caller must have called settings->beginGroup(). 36 | Because the group must not change during runtime, it is recommended to provide a 37 | separate QSettings instance that is not used by other parts of the program. 38 | The FileLogger does not take over ownership of the QSettings instance, so the caller 39 | should destroy it during shutdown. 40 | @param secondSettings Same as firstSettings, but for the second log file. 41 | @param refreshInterval Interval of checking for changed config settings in msec, or 0=disabled 42 | @param parent Parent object. 43 | */ 44 | DualFileLogger(QSettings *firstSettings, QSettings *secondSettings, const int refreshInterval = 10000, 45 | QObject *parent = nullptr); 46 | 47 | /** 48 | Decorate and log the message, if type>=minLevel. 49 | This method is thread safe. 50 | @param type Message type (level) 51 | @param message Message text 52 | @param file Name of the source file where the message was generated (usually filled with the macro __FILE__) 53 | @param function Name of the function where the message was generated (usually filled with the macro __LINE__) 54 | @param line Line Number of the source file, where the message was generated (usually filles with the macro __func__ 55 | or __FUNCTION__) 56 | @see LogMessage for a description of the message decoration. 57 | */ 58 | virtual void log(const QtMsgType type, const QString &message, const QString &file = QString(), 59 | const QString &function = QString(), const int line = 0); 60 | 61 | /** 62 | Clear the thread-local data of the current thread. 63 | This method is thread safe. 64 | @param buffer Whether to clear the backtrace buffer 65 | @param variables Whether to clear the log variables 66 | */ 67 | virtual void clear(const bool buffer = true, const bool variables = true); 68 | 69 | private: 70 | /** First logger */ 71 | FileLogger *firstLogger; 72 | 73 | /** Second logger */ 74 | FileLogger *secondLogger; 75 | }; 76 | 77 | } // namespace qtwebapp 78 | -------------------------------------------------------------------------------- /QtWebApp/logging/filelogger.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "filelogger.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | using namespace qtwebapp; 19 | 20 | void FileLogger::refreshSettings() { 21 | mutex.lock(); 22 | // Save old file name for later comparision with new settings 23 | QString oldFileName = fileName; 24 | 25 | // Load new config settings 26 | settings->sync(); 27 | fileName = settings->value("fileName").toString(); 28 | // Convert relative fileName to absolute, based on the directory of the config file. 29 | #ifdef Q_OS_WIN32 30 | if (QDir::isRelativePath(fileName) && settings->format() != QSettings::NativeFormat) 31 | #else 32 | if (QDir::isRelativePath(fileName)) 33 | #endif 34 | { 35 | QFileInfo configFile(settings->fileName()); 36 | fileName = QFileInfo(configFile.absolutePath(), fileName).absoluteFilePath(); 37 | } 38 | maxSize = settings->value("maxSize", 0).toLongLong(); 39 | maxBackups = settings->value("maxBackups", 0).toInt(); 40 | msgFormat = settings->value("msgFormat", "{timestamp} {type} {msg}").toString(); 41 | timestampFormat = settings->value("timestampFormat", "yyyy-MM-dd hh:mm:ss.zzz").toString(); 42 | bufferSize = settings->value("bufferSize", 0).toInt(); 43 | 44 | // Translate log level settings to enumeration value 45 | QByteArray minLevelStr = settings->value("minLevel", "ALL").toByteArray(); 46 | if (minLevelStr == "ALL" || minLevelStr == "DEBUG" || minLevelStr == "0") { 47 | minLevel = QtMsgType::QtDebugMsg; 48 | } else if (minLevelStr == "WARNING" || minLevelStr == "WARN" || minLevelStr == "1") { 49 | minLevel = QtMsgType::QtWarningMsg; 50 | } else if (minLevelStr == "ERROR" || minLevelStr == "CRITICAL" || minLevelStr == "2") { 51 | minLevel = QtMsgType::QtCriticalMsg; 52 | } else if (minLevelStr == "FATAL" || minLevelStr == "3") { 53 | minLevel = QtMsgType::QtFatalMsg; 54 | } else if (minLevelStr == "INFO" || minLevelStr == "4") { 55 | minLevel = QtMsgType::QtInfoMsg; 56 | } 57 | 58 | // Create new file if the filename has been changed 59 | if (oldFileName != fileName) { 60 | fprintf(stderr, "Logging to %s\n", qPrintable(fileName)); 61 | close(); 62 | open(); 63 | } 64 | mutex.unlock(); 65 | } 66 | 67 | FileLogger::FileLogger(QSettings *settings, const int refreshInterval, QObject *parent) : Logger(parent) { 68 | Q_ASSERT(settings != nullptr); 69 | Q_ASSERT(refreshInterval >= 0); 70 | this->settings = settings; 71 | file = nullptr; 72 | if (refreshInterval > 0) { 73 | refreshTimer.start(refreshInterval, this); 74 | } 75 | flushTimer.start(1000, this); 76 | refreshSettings(); 77 | } 78 | 79 | FileLogger::~FileLogger() { 80 | close(); 81 | } 82 | 83 | void FileLogger::write(const LogMessage *logMessage) { 84 | // Try to write to the file 85 | if (file) { 86 | 87 | // Write the message 88 | file->write(qPrintable(logMessage->toString(msgFormat, timestampFormat))); 89 | 90 | // Flush error messages immediately, to ensure that no important message 91 | // gets lost when the program terinates abnormally. 92 | if (logMessage->getType() >= QtCriticalMsg) { 93 | file->flush(); 94 | } 95 | 96 | // Check for success 97 | if (file->error()) { 98 | qWarning("Cannot write to log file %s: %s", qPrintable(fileName), qPrintable(file->errorString())); 99 | close(); 100 | } 101 | } 102 | 103 | // Fall-back to the super class method, if writing failed 104 | if (!file) { 105 | Logger::write(logMessage); 106 | } 107 | } 108 | 109 | void FileLogger::open() { 110 | if (fileName.isEmpty()) { 111 | qWarning("Name of logFile is empty"); 112 | } else { 113 | file = new QFile(fileName); 114 | if (!file->open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) { 115 | qWarning("Cannot open log file %s: %s", qPrintable(fileName), qPrintable(file->errorString())); 116 | file = nullptr; 117 | } 118 | } 119 | } 120 | 121 | void FileLogger::close() { 122 | if (file) { 123 | file->close(); 124 | delete file; 125 | file = nullptr; 126 | } 127 | } 128 | 129 | void FileLogger::rotate() { 130 | // count current number of existing backup files 131 | int count = 0; 132 | forever { 133 | QFile bakFile(QString("%1.%2").arg(fileName).arg(count + 1)); 134 | if (bakFile.exists()) { 135 | ++count; 136 | } else { 137 | break; 138 | } 139 | } 140 | 141 | // Remove all old backup files that exceed the maximum number 142 | while (maxBackups > 0 && count >= maxBackups) { 143 | QFile::remove(QString("%1.%2").arg(fileName).arg(count)); 144 | --count; 145 | } 146 | 147 | // Rotate backup files 148 | for (int i = count; i > 0; --i) { 149 | QFile::rename(QString("%1.%2").arg(fileName).arg(i), QString("%1.%2").arg(fileName).arg(i + 1)); 150 | } 151 | 152 | // Backup the current logfile 153 | QFile::rename(fileName, fileName + ".1"); 154 | } 155 | 156 | void FileLogger::timerEvent(QTimerEvent *event) { 157 | if (!event) { 158 | return; 159 | } else if (event->timerId() == refreshTimer.timerId()) { 160 | refreshSettings(); 161 | } else if (event->timerId() == flushTimer.timerId() && file) { 162 | mutex.lock(); 163 | 164 | // Flush the I/O buffer 165 | file->flush(); 166 | 167 | // Rotate the file if it is too large 168 | if (maxSize > 0 && file->size() >= maxSize) { 169 | close(); 170 | rotate(); 171 | open(); 172 | } 173 | 174 | mutex.unlock(); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /QtWebApp/logging/filelogger.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "logger.h" 9 | #include "qtwebappglobal.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace qtwebapp { 17 | 18 | /** 19 | Logger that uses a text file for output. Settings are read from a 20 | config file using a QSettings object. Config settings can be changed at runtime. 21 |

22 | Example for the configuration settings: 23 |

 24 | 	  fileName=logs/QtWebApp.log
 25 | 	  maxSize=1000000
 26 | 	  maxBackups=2
 27 | 	  bufferSize=0
 28 | 	  minLevel=WARNING
 29 | 	  msgformat={timestamp} {typeNr} {type} thread={thread}: {msg}
 30 | 	  timestampFormat=dd.MM.yyyy hh:mm:ss.zzz
 31 | 	  
32 | 33 | - Possible log levels are: ALL/DEBUG=0, INFO=4, WARN/WARNING=1, ERROR/CRITICAL=2, FATAL=3 34 | - fileName is the name of the log file, relative to the directory of the settings file. 35 | In case of windows, if the settings are in the registry, the path is relative to the current 36 | working directory. 37 | - maxSize is the maximum size of that file in bytes. The file will be backed up and 38 | replaced by a new file if it becomes larger than this limit. Please note that 39 | the actual file size may become a little bit larger than this limit. Default is 0=unlimited. 40 | - maxBackups defines the number of backup files to keep. Default is 0=unlimited. 41 | - bufferSize defines the size of the ring buffer. Default is 0=disabled. 42 | - minLevel If bufferSize=0: Messages with lower level are discarded.
43 | If buffersize>0: Messages with lower level are buffered, messages with equal or higher 44 | level (except INFO) trigger writing the buffered messages into the file.
45 | Defaults is 0=debug. 46 | - msgFormat defines the decoration of log messages, see LogMessage class. Default is "{timestamp} {type} {msg}". 47 | - timestampFormat defines the format of timestamps, see QDateTime::toString(). Default is "yyyy-MM-dd hh:mm:ss.zzz". 48 | 49 | 50 | @see set() describes how to set logger variables 51 | @see LogMessage for a description of the message decoration. 52 | @see Logger for a descrition of the buffer. 53 | */ 54 | 55 | class QTWEBAPP_EXPORT FileLogger : public Logger { 56 | Q_OBJECT 57 | Q_DISABLE_COPY(FileLogger) 58 | public: 59 | /** 60 | Constructor. 61 | @param settings Configuration settings, usually stored in an INI file. Must not be 0. 62 | Settings are read from the current group, so the caller must have called settings->beginGroup(). 63 | Because the group must not change during runtime, it is recommended to provide a 64 | separate QSettings instance that is not used by other parts of the program. 65 | The FileLogger does not take over ownership of the QSettings instance, so the caller 66 | should destroy it during shutdown. 67 | @param refreshInterval Interval of checking for changed config settings in msec, or 0=disabled 68 | @param parent Parent object 69 | */ 70 | FileLogger(QSettings *settings, const int refreshInterval = 10000, QObject *parent = nullptr); 71 | 72 | /** 73 | Destructor. Closes the file. 74 | */ 75 | virtual ~FileLogger(); 76 | 77 | /** Write a message to the log file */ 78 | virtual void write(const LogMessage *logMessage); 79 | 80 | protected: 81 | /** 82 | Handler for timer events. 83 | Refreshes config settings or synchronizes I/O buffer, depending on the event. 84 | This method is thread-safe. 85 | @param event used to distinguish between the two timers. 86 | */ 87 | void timerEvent(QTimerEvent *event); 88 | 89 | private: 90 | /** Configured name of the log file */ 91 | QString fileName; 92 | 93 | /** Configured maximum size of the file in bytes, or 0=unlimited */ 94 | long maxSize; 95 | 96 | /** Configured maximum number of backup files, or 0=unlimited */ 97 | int maxBackups; 98 | 99 | /** Pointer to the configuration settings */ 100 | QSettings *settings; 101 | 102 | /** Output file, or 0=disabled */ 103 | QFile *file; 104 | 105 | /** Timer for refreshing configuration settings */ 106 | QBasicTimer refreshTimer; 107 | 108 | /** Timer for flushing the file I/O buffer */ 109 | QBasicTimer flushTimer; 110 | 111 | /** Open the output file */ 112 | void open(); 113 | 114 | /** Close the output file */ 115 | void close(); 116 | 117 | /** Rotate files and delete some backups if there are too many */ 118 | void rotate(); 119 | 120 | /** 121 | Refreshes the configuration settings. 122 | This method is thread-safe. 123 | */ 124 | void refreshSettings(); 125 | }; 126 | 127 | } // namespace qtwebapp 128 | -------------------------------------------------------------------------------- /QtWebApp/logging/logger.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "logger.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 15 | #include 16 | #endif 17 | 18 | using namespace qtwebapp; 19 | 20 | Logger *Logger::defaultLogger = nullptr; 21 | 22 | QThreadStorage *> Logger::logVars; 23 | 24 | QMutex Logger::mutex; 25 | 26 | Logger::Logger(QObject *parent) 27 | : QObject(parent), msgFormat("{timestamp} {type} {msg}"), timestampFormat("dd.MM.yyyy hh:mm:ss.zzz"), 28 | minLevel(QtDebugMsg), bufferSize(0) {} 29 | 30 | Logger::Logger(const QString msgFormat, const QString timestampFormat, const QtMsgType minLevel, const int bufferSize, 31 | QObject *parent) 32 | : QObject(parent) { 33 | this->msgFormat = msgFormat; 34 | this->timestampFormat = timestampFormat; 35 | this->minLevel = minLevel; 36 | this->bufferSize = bufferSize; 37 | } 38 | 39 | void Logger::msgHandler(const QtMsgType type, const QString &message, const QString &file, const QString &function, 40 | const int line) { 41 | #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) 42 | static QMutex recursiveMutex(QMutex::Recursive); 43 | #else 44 | static QRecursiveMutex recursiveMutex; 45 | #endif 46 | static QMutex nonRecursiveMutex; 47 | 48 | // Prevent multiple threads from calling this method simultaneoulsy. 49 | // But allow recursive calls, which is required to prevent a deadlock 50 | // if the logger itself produces an error message. 51 | recursiveMutex.lock(); 52 | 53 | // Fall back to stderr when this method has been called recursively. 54 | if (defaultLogger && nonRecursiveMutex.tryLock()) { 55 | defaultLogger->log(type, message, file, function, line); 56 | nonRecursiveMutex.unlock(); 57 | } else { 58 | fputs(qPrintable(message), stderr); 59 | fflush(stderr); 60 | } 61 | 62 | // Abort the program after logging a fatal message 63 | if (type == QtFatalMsg) { 64 | abort(); 65 | } 66 | 67 | recursiveMutex.unlock(); 68 | } 69 | 70 | void Logger::msgHandler5(const QtMsgType type, const QMessageLogContext &context, const QString &message) { 71 | (void)(context); // suppress "unused parameter" warning 72 | msgHandler(type, message, context.file, context.function, context.line); 73 | } 74 | 75 | Logger::~Logger() { 76 | if (defaultLogger == this) { 77 | qInstallMessageHandler(nullptr); 78 | defaultLogger = nullptr; 79 | } 80 | } 81 | 82 | void Logger::write(const LogMessage *logMessage) { 83 | fputs(qPrintable(logMessage->toString(msgFormat, timestampFormat)), stderr); 84 | fflush(stderr); 85 | } 86 | 87 | void Logger::installMsgHandler() { 88 | defaultLogger = this; 89 | qInstallMessageHandler(msgHandler5); 90 | } 91 | 92 | void Logger::set(const QString &name, const QString &value) { 93 | mutex.lock(); 94 | if (!logVars.hasLocalData()) { 95 | logVars.setLocalData(new QHash); 96 | } 97 | logVars.localData()->insert(name, value); 98 | mutex.unlock(); 99 | } 100 | 101 | void Logger::clear(const bool buffer, const bool variables) { 102 | mutex.lock(); 103 | if (buffer && buffers.hasLocalData()) { 104 | QList *buffer = buffers.localData(); 105 | while (buffer && !buffer->isEmpty()) { 106 | LogMessage *logMessage = buffer->takeLast(); 107 | delete logMessage; 108 | } 109 | } 110 | if (variables && logVars.hasLocalData()) { 111 | logVars.localData()->clear(); 112 | } 113 | mutex.unlock(); 114 | } 115 | 116 | void Logger::log(const QtMsgType type, const QString &message, const QString &file, const QString &function, 117 | const int line) { 118 | // Check if the type of the message reached the configured minLevel in the order 119 | // DEBUG, INFO, WARNING, CRITICAL, FATAL 120 | // Since Qt 5.5: INFO messages are between DEBUG and WARNING 121 | bool toPrint = false; 122 | switch (type) { 123 | case QtDebugMsg: 124 | if (minLevel == QtDebugMsg) { 125 | toPrint = true; 126 | } 127 | break; 128 | 129 | case QtInfoMsg: 130 | if (minLevel == QtDebugMsg || minLevel == QtInfoMsg) { 131 | toPrint = true; 132 | } 133 | break; 134 | 135 | case QtWarningMsg: 136 | if (minLevel == QtDebugMsg || minLevel == QtInfoMsg || minLevel == QtWarningMsg) { 137 | toPrint = true; 138 | } 139 | break; 140 | 141 | case QtCriticalMsg: // or QtSystemMsg which has the same int value 142 | if (minLevel == QtDebugMsg || minLevel == QtInfoMsg || minLevel == QtWarningMsg || minLevel == QtCriticalMsg) { 143 | toPrint = true; 144 | } 145 | break; 146 | 147 | case QtFatalMsg: 148 | toPrint = true; 149 | break; 150 | 151 | default: // For additional type that might get introduced in future 152 | toPrint = true; 153 | } 154 | mutex.lock(); 155 | 156 | // If the buffer is enabled, write the message into it 157 | if (bufferSize > 0) { 158 | // Create new thread local buffer, if necessary 159 | if (!buffers.hasLocalData()) { 160 | buffers.setLocalData(new QList()); 161 | } 162 | QList *buffer = buffers.localData(); 163 | 164 | // Append the decorated log message to the buffer 165 | LogMessage *logMessage = new LogMessage(type, message, logVars.localData(), file, function, line); 166 | buffer->append(logMessage); 167 | 168 | // Delete oldest message if the buffer became too large 169 | if (buffer->size() > bufferSize) { 170 | delete buffer->takeFirst(); 171 | } 172 | 173 | // Print the whole buffer if the type is high enough 174 | if (toPrint) { 175 | // Print the whole buffer content 176 | while (!buffer->isEmpty()) { 177 | LogMessage *logMessage = buffer->takeFirst(); 178 | write(logMessage); 179 | delete logMessage; 180 | } 181 | } 182 | } 183 | 184 | // Buffer is disabled, print the message if the type is high enough 185 | else { 186 | if (toPrint) { 187 | LogMessage logMessage(type, message, logVars.localData(), file, function, line); 188 | write(&logMessage); 189 | } 190 | } 191 | mutex.unlock(); 192 | } 193 | -------------------------------------------------------------------------------- /QtWebApp/logging/logger.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "logmessage.h" 9 | #include "qtwebappglobal.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace qtwebapp { 18 | 19 | /** 20 | Decorates and writes log messages to the console, stderr. 21 |

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

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

29 | The logger can collect a configurable number of messages in thread-local 30 | FIFO buffers. A log message with severity >= minLevel flushes the buffer, 31 | so the messages are written out. There is one exception: 32 | INFO messages are treated like DEBUG messages (level 0). 33 |

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

39 | If the buffer is disabled, then only messages with severity >= minLevel 40 | are written out. 41 |

42 | The logger can be registered to handle messages from 43 | the static global functions qDebug(), qWarning(), qCritical(), qFatal() and qInfo(). 44 | 45 | @see set() describes how to set logger variables 46 | @see LogMessage for a description of the message decoration. 47 | @warning You should prefer a derived class, for example FileLogger, 48 | because logging to the console is less useful. 49 | */ 50 | 51 | class QTWEBAPP_EXPORT Logger : public QObject { 52 | Q_OBJECT 53 | Q_DISABLE_COPY(Logger) 54 | public: 55 | /** 56 | Constructor. 57 | Uses the same defaults as the other constructor. 58 | @param parent Parent object 59 | */ 60 | Logger(QObject *parent); 61 | 62 | /** 63 | Constructor. 64 | Possible log levels are: 0=DEBUG, 1=WARNING, 2=CRITICAL, 3=FATAL, 4=INFO 65 | @param msgFormat Format of the decoration, e.g. "{timestamp} {type} thread={thread}: {msg}" 66 | @param timestampFormat Format of timestamp, e.g. "dd.MM.yyyy hh:mm:ss.zzz" 67 | @param minLevel If bufferSize=0: Messages with lower level discarded.
68 | If buffersize>0: Messages with lower level are buffered, messages with equal or higher level 69 | trigger writing the buffered content. 70 | @param bufferSize Size of the backtrace buffer, number of messages per thread. 0=disabled. 71 | @param parent Parent object 72 | @see LogMessage for a description of the message decoration. 73 | */ 74 | Logger(const QString msgFormat = "{timestamp} {type} {msg}", 75 | const QString timestampFormat = "dd.MM.yyyy hh:mm:ss.zzz", const QtMsgType minLevel = QtDebugMsg, 76 | const int bufferSize = 0, QObject *parent = nullptr); 77 | 78 | /** Destructor */ 79 | virtual ~Logger(); 80 | 81 | /** 82 | Decorate and log the message, if type>=minLevel. 83 | This method is thread safe. 84 | @param type Message type (level) 85 | @param message Message text 86 | @param file Name of the source file where the message was generated (usually filled with the macro __FILE__) 87 | @param function Name of the function where the message was generated (usually filled with the macro __LINE__) 88 | @param line Line Number of the source file, where the message was generated (usually filles with the macro __func__ 89 | or __FUNCTION__) 90 | @see LogMessage for a description of the message decoration. 91 | */ 92 | virtual void log(const QtMsgType type, const QString &message, const QString &file = "", 93 | const QString &function = "", const int line = 0); 94 | 95 | /** 96 | Installs this logger as the default message handler, so it 97 | can be used through the global static logging functions (e.g. qDebug()). 98 | */ 99 | void installMsgHandler(); 100 | 101 | /** 102 | Sets a thread-local variable that may be used to decorate log messages. 103 | This method is thread safe. 104 | @param name Name of the variable 105 | @param value Value of the variable 106 | */ 107 | static void set(const QString &name, const QString &value); 108 | 109 | /** 110 | Clear the thread-local data of the current thread. 111 | This method is thread safe. 112 | @param buffer Whether to clear the backtrace buffer 113 | @param variables Whether to clear the log variables 114 | */ 115 | virtual void clear(const bool buffer = true, const bool variables = true); 116 | 117 | protected: 118 | /** Format string for message decoration */ 119 | QString msgFormat; 120 | 121 | /** Format string of timestamps */ 122 | QString timestampFormat; 123 | 124 | /** Minimum level of message types that are written out directly or trigger writing the buffered content. */ 125 | QtMsgType minLevel; 126 | 127 | /** Size of backtrace buffer, number of messages per thread. 0=disabled */ 128 | int bufferSize; 129 | 130 | /** Used to synchronize access of concurrent threads */ 131 | static QMutex mutex; 132 | 133 | /** 134 | Decorate and write a log message to stderr. Override this method 135 | to provide a different output medium. 136 | */ 137 | virtual void write(const LogMessage *logMessage); 138 | 139 | private: 140 | /** Pointer to the default logger, used by msgHandler() */ 141 | static Logger *defaultLogger; 142 | 143 | /** 144 | Message Handler for the global static logging functions (e.g. qDebug()). 145 | Forward calls to the default logger. 146 |

147 | In case of a fatal message, the program will abort. 148 | Variables in the in the message are replaced by their values. 149 | This method is thread safe. 150 | @param type Message type (level) 151 | @param message Message text 152 | @param file Name of the source file where the message was generated (usually filled with the macro __FILE__) 153 | @param function Name of the function where the message was generated (usually filled with the macro __LINE__) 154 | @param line Line Number of the source file, where the message was generated (usually filles with the macro __func__ 155 | or __FUNCTION__) 156 | */ 157 | static void msgHandler(const QtMsgType type, const QString &message, const QString &file = "", 158 | const QString &function = "", const int line = 0); 159 | 160 | /** 161 | Wrapper for QT version 5. 162 | @param type Message type (level) 163 | @param context Message context 164 | @param message Message text 165 | @see msgHandler() 166 | */ 167 | static void msgHandler5(const QtMsgType type, const QMessageLogContext &context, const QString &message); 168 | 169 | /** Thread local variables to be used in log messages */ 170 | static QThreadStorage *> logVars; 171 | 172 | /** Thread local backtrace buffers */ 173 | QThreadStorage *> buffers; 174 | }; 175 | 176 | } // namespace qtwebapp 177 | -------------------------------------------------------------------------------- /QtWebApp/logging/logmessage.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "logmessage.h" 7 | 8 | #include 9 | #include 10 | 11 | using namespace qtwebapp; 12 | 13 | LogMessage::LogMessage(const QtMsgType type, const QString &message, const QHash *logVars, 14 | const QString &file, const QString &function, const int line) { 15 | this->type = type; 16 | this->message = message; 17 | this->file = file; 18 | this->function = function; 19 | this->line = line; 20 | timestamp = QDateTime::currentDateTime(); 21 | threadId = QThread::currentThreadId(); 22 | 23 | // Copy the logVars if not null, 24 | // so that later changes in the original do not affect the copy 25 | if (logVars) { 26 | this->logVars = *logVars; 27 | } 28 | } 29 | 30 | QString LogMessage::toString(const QString &msgFormat, const QString ×tampFormat) const { 31 | QString decorated = msgFormat + "\n"; 32 | decorated.replace("{msg}", message); 33 | 34 | if (decorated.contains("{timestamp}")) { 35 | decorated.replace("{timestamp}", timestamp.toString(timestampFormat)); 36 | } 37 | 38 | QString typeNr; 39 | typeNr.setNum(type); 40 | decorated.replace("{typeNr}", typeNr); 41 | 42 | switch (type) { 43 | case QtDebugMsg: 44 | decorated.replace("{type}", "DEBUG "); 45 | break; 46 | case QtWarningMsg: 47 | decorated.replace("{type}", "WARNING "); 48 | break; 49 | case QtCriticalMsg: 50 | decorated.replace("{type}", "CRITICAL"); 51 | break; 52 | case QtFatalMsg: // or QtSystemMsg which has the same int value 53 | decorated.replace("{type}", "FATAL "); 54 | break; 55 | case QtInfoMsg: 56 | decorated.replace("{type}", "INFO "); 57 | break; 58 | } 59 | 60 | decorated.replace("{file}", file); 61 | decorated.replace("{function}", function); 62 | decorated.replace("{line}", QString::number(line)); 63 | 64 | QString threadId = QString("0x%1").arg(qulonglong(QThread::currentThreadId()), 8, 16, QLatin1Char('0')); 65 | decorated.replace("{thread}", threadId); 66 | 67 | // Fill in variables 68 | if (decorated.contains("{") && !logVars.isEmpty()) { 69 | QList keys = logVars.keys(); 70 | foreach (QString key, keys) { 71 | decorated.replace("{" + key + "}", logVars.value(key)); 72 | } 73 | } 74 | 75 | return decorated; 76 | } 77 | 78 | QtMsgType LogMessage::getType() const { 79 | return type; 80 | } 81 | -------------------------------------------------------------------------------- /QtWebApp/logging/logmessage.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "qtwebappglobal.h" 9 | 10 | #include 11 | #include 12 | 13 | namespace qtwebapp { 14 | 15 | /** 16 | Represents a single log message together with some data 17 | that are used to decorate the log message. 18 | 19 | The following variables may be used in the message and in msgFormat: 20 | 21 | - {timestamp} Date and time of creation 22 | - {typeNr} Type of the message in numeric format (0-3) 23 | - {type} Type of the message in string format (DEBUG, WARNING, CRITICAL, FATAL) 24 | - {thread} ID number of the thread 25 | - {msg} Message text 26 | - {xxx} For any user-defined logger variable 27 | 28 | Plus some new variables since QT 5.0, only filled when compiled in debug mode: 29 | 30 | - {file} Filename where the message was generated 31 | - {function} Function where the message was generated 32 | - {line} Line number where the message was generated 33 | */ 34 | 35 | class QTWEBAPP_EXPORT LogMessage { 36 | Q_DISABLE_COPY(LogMessage) 37 | public: 38 | /** 39 | Constructor. All parameters are copied, so that later changes to them do not 40 | affect this object. 41 | @param type Type of the message 42 | @param message Message text 43 | @param logVars Logger variables, 0 is allowed 44 | @param file Name of the source file where the message was generated 45 | @param function Name of the function where the message was generated 46 | @param line Line Number of the source file, where the message was generated 47 | */ 48 | LogMessage(const QtMsgType type, const QString &message, const QHash *logVars, const QString &file, 49 | const QString &function, const int line); 50 | 51 | /** 52 | Returns the log message as decorated string. 53 | @param msgFormat Format of the decoration. May contain variables and static text, 54 | e.g. "{timestamp} {type} thread={thread}: {msg}". 55 | @param timestampFormat Format of timestamp, e.g. "dd.MM.yyyy hh:mm:ss.zzz", see QDateTime::toString(). 56 | @see QDatetime for a description of the timestamp format pattern 57 | */ 58 | QString toString(const QString &msgFormat, const QString ×tampFormat) const; 59 | 60 | /** 61 | Get the message type. 62 | */ 63 | QtMsgType getType() const; 64 | 65 | private: 66 | /** Logger variables */ 67 | QHash logVars; 68 | 69 | /** Date and time of creation */ 70 | QDateTime timestamp; 71 | 72 | /** Type of the message */ 73 | QtMsgType type; 74 | 75 | /** ID number of the thread */ 76 | Qt::HANDLE threadId; 77 | 78 | /** Message text */ 79 | QString message; 80 | 81 | /** Filename where the message was generated */ 82 | QString file; 83 | 84 | /** Function name where the message was generated */ 85 | QString function; 86 | 87 | /** Line number where the message was generated */ 88 | int line; 89 | }; 90 | 91 | } // namespace qtwebapp 92 | -------------------------------------------------------------------------------- /QtWebApp/qtwebappglobal.cpp: -------------------------------------------------------------------------------- 1 | #include "qtwebappglobal.h" 2 | 3 | const char *qtwebapp::getQtWebAppLibVersion() { 4 | return QTWEBAPP_VERSION_STR; 5 | } 6 | 7 | int qtwebapp::parseNum(const QVariant &v, int base) { 8 | QString str = v.toString(); 9 | int mul = 1; 10 | if (str.endsWith('K')) 11 | mul *= base; 12 | else if (str.endsWith('M')) 13 | mul *= base * base; 14 | else if (str.endsWith('G')) 15 | mul *= base * base * base; 16 | if (mul != 1) 17 | str = str.mid(str.length() - 1); 18 | return str.toInt() * mul; 19 | } 20 | -------------------------------------------------------------------------------- /QtWebApp/qtwebappglobal.h.in: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define QTWEBAPP_MAJOR @qtwebapp_MAJOR@ 8 | #define QTWEBAPP_MINOR @qtwebapp_MINOR@ 9 | #define QTWEBAPP_PATCH @qtwebapp_PATCH@ 10 | #define QTWEBAPP_VERSION ((@qtwebapp_MAJOR@ << 16) | (@qtwebapp_MINOR@ << 8) | @qtwebapp_PATCH@) 11 | #define QTWEBAPP_VERSION_STR "@qtwebapp_VERSION@" 12 | 13 | #ifdef CMAKE_QTWEBAPP_SO 14 | #define QTWEBAPP_EXPORT Q_DECL_EXPORT 15 | #else 16 | #define QTWEBAPP_EXPORT Q_DECL_IMPORT 17 | #endif 18 | 19 | namespace qtwebapp { 20 | 21 | /// The version of QtWebApp. 22 | QTWEBAPP_EXPORT const char *getQtWebAppLibVersion(); 23 | 24 | /// Parses the given number by respecting its suffix. 25 | QTWEBAPP_EXPORT int parseNum(const QVariant &v, int base = 1e3); 26 | 27 | } // namespace qtwebapp 28 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | set(templateengine_HEADERS 2 | template.h 3 | templatecache.h 4 | templateengineconfig.h 5 | templateloader.h 6 | ) 7 | set(templateengine_SOURCES 8 | template.cpp 9 | templatecache.cpp 10 | templateengineconfig.cpp 11 | templateloader.cpp 12 | ) 13 | 14 | add_library(QtWebAppTemplateEngine SHARED ${templateengine_HEADERS} ${templateengine_SOURCES}) 15 | target_include_directories(QtWebAppTemplateEngine PUBLIC 16 | $ 17 | $ 18 | ) 19 | target_link_libraries(QtWebAppTemplateEngine QtWebAppGlobal Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) 20 | if (Qt6_FOUND) 21 | target_link_libraries(QtWebAppTemplateEngine Qt6::Core5Compat) 22 | endif() 23 | set_target_properties(QtWebAppTemplateEngine PROPERTIES 24 | VERSION ${qtwebapp_VERSION} 25 | SOVERSION ${qtwebapp_MAJOR} 26 | ) 27 | 28 | install(TARGETS QtWebAppTemplateEngine 29 | EXPORT QtWebAppTargets 30 | LIBRARY DESTINATION lib 31 | RUNTIME DESTINATION bin 32 | ARCHIVE DESTINATION lib) 33 | install(FILES ${templateengine_HEADERS} 34 | DESTINATION include/qtwebapp/templateengine) 35 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/template.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "template.h" 7 | 8 | #include 9 | 10 | using namespace qtwebapp; 11 | 12 | Template::Template(const QString &source, const QString &sourceName) : QString(source) { 13 | this->sourceName = sourceName; 14 | this->warnings = false; 15 | } 16 | 17 | Template::Template(QFile &file, const QTextCodec *textCodec) { 18 | this->warnings = false; 19 | sourceName = QFileInfo(file.fileName()).baseName(); 20 | if (!file.isOpen()) { 21 | file.open(QFile::ReadOnly | QFile::Text); 22 | } 23 | QByteArray data = file.readAll(); 24 | file.close(); 25 | if (data.size() == 0 || file.error()) { 26 | qCritical("Template: cannot read from %s, %s", qPrintable(sourceName), qPrintable(file.errorString())); 27 | } else { 28 | append(textCodec->toUnicode(data)); 29 | } 30 | } 31 | 32 | int Template::setVariable(const QString &name, const QString &value) { 33 | int count = 0; 34 | QString variable = "{" + name + "}"; 35 | int start = indexOf(variable); 36 | while (start >= 0) { 37 | replace(start, variable.length(), value); 38 | count++; 39 | start = indexOf(variable, start + value.length()); 40 | } 41 | if (count == 0 && warnings) { 42 | qWarning("Template: missing variable %s in %s", qPrintable(variable), qPrintable(sourceName)); 43 | } 44 | return count; 45 | } 46 | 47 | int Template::setCondition(const QString &name, const bool value) { 48 | int count = 0; 49 | QString startTag = QString("{if %1}").arg(name); 50 | QString elseTag = QString("{else %1}").arg(name); 51 | QString endTag = QString("{end %1}").arg(name); 52 | // search for if-else-end 53 | int start = indexOf(startTag); 54 | while (start >= 0) { 55 | int end = indexOf(endTag, start + startTag.length()); 56 | if (end >= 0) { 57 | count++; 58 | int ellse = indexOf(elseTag, start + startTag.length()); 59 | if (ellse > start && ellse < end) { 60 | // there is an else part 61 | if (value == true) { 62 | QString truePart = mid(start + startTag.length(), ellse - start - startTag.length()); 63 | replace(start, end - start + endTag.length(), truePart); 64 | } else { 65 | // value==false 66 | QString falsePart = mid(ellse + elseTag.length(), end - ellse - elseTag.length()); 67 | replace(start, end - start + endTag.length(), falsePart); 68 | } 69 | } else if (value == true) { 70 | // and no else part 71 | QString truePart = mid(start + startTag.length(), end - start - startTag.length()); 72 | replace(start, end - start + endTag.length(), truePart); 73 | } else { 74 | // value==false and no else part 75 | replace(start, end - start + endTag.length(), ""); 76 | } 77 | start = indexOf(startTag, start); 78 | } else { 79 | qWarning("Template: missing condition end %s in %s", qPrintable(endTag), qPrintable(sourceName)); 80 | } 81 | } 82 | // search for ifnot-else-end 83 | QString startTag2 = "{ifnot " + name + "}"; 84 | start = indexOf(startTag2); 85 | while (start >= 0) { 86 | int end = indexOf(endTag, start + startTag2.length()); 87 | if (end >= 0) { 88 | count++; 89 | int ellse = indexOf(elseTag, start + startTag2.length()); 90 | if (ellse > start && ellse < end) { 91 | // there is an else part 92 | if (value == false) { 93 | QString falsePart = mid(start + startTag2.length(), ellse - start - startTag2.length()); 94 | replace(start, end - start + endTag.length(), falsePart); 95 | } else { 96 | // value==true 97 | QString truePart = mid(ellse + elseTag.length(), end - ellse - elseTag.length()); 98 | replace(start, end - start + endTag.length(), truePart); 99 | } 100 | } else if (value == false) { 101 | // and no else part 102 | QString falsePart = mid(start + startTag2.length(), end - start - startTag2.length()); 103 | replace(start, end - start + endTag.length(), falsePart); 104 | } else { 105 | // value==true and no else part 106 | replace(start, end - start + endTag.length(), ""); 107 | } 108 | start = indexOf(startTag2, start); 109 | } else { 110 | qWarning("Template: missing condition end %s in %s", qPrintable(endTag), qPrintable(sourceName)); 111 | } 112 | } 113 | if (count == 0 && warnings) { 114 | qWarning("Template: missing condition %s or %s in %s", qPrintable(startTag), qPrintable(startTag2), 115 | qPrintable(sourceName)); 116 | } 117 | return count; 118 | } 119 | 120 | int Template::loop(const QString &name, const int repetitions) { 121 | Q_ASSERT(repetitions >= 0); 122 | int count = 0; 123 | QString startTag = "{loop " + name + "}"; 124 | QString elseTag = "{else " + name + "}"; 125 | QString endTag = "{end " + name + "}"; 126 | // search for loop-else-end 127 | int start = indexOf(startTag); 128 | while (start >= 0) { 129 | int end = indexOf(endTag, start + startTag.length()); 130 | if (end >= 0) { 131 | count++; 132 | int ellse = indexOf(elseTag, start + startTag.length()); 133 | if (ellse > start && ellse < end) { 134 | // there is an else part 135 | if (repetitions > 0) { 136 | QString loopPart = mid(start + startTag.length(), ellse - start - startTag.length()); 137 | QString insertMe; 138 | for (int i = 0; i < repetitions; ++i) { 139 | // number variables, conditions and sub-loop within the loop 140 | QString nameNum = name + QString::number(i); 141 | QString s = loopPart; 142 | s.replace(QString("{%1.").arg(name), QString("{%1.").arg(nameNum)); 143 | s.replace(QString("{if %1.").arg(name), QString("{if %1.").arg(nameNum)); 144 | s.replace(QString("{ifnot %1.").arg(name), QString("{ifnot %1.").arg(nameNum)); 145 | s.replace(QString("{else %1.").arg(name), QString("{else %1.").arg(nameNum)); 146 | s.replace(QString("{end %1.").arg(name), QString("{end %1.").arg(nameNum)); 147 | s.replace(QString("{loop %1.").arg(name), QString("{loop %1.").arg(nameNum)); 148 | insertMe.append(s); 149 | } 150 | replace(start, end - start + endTag.length(), insertMe); 151 | } else { 152 | // repetitions==0 153 | QString elsePart = mid(ellse + elseTag.length(), end - ellse - elseTag.length()); 154 | replace(start, end - start + endTag.length(), elsePart); 155 | } 156 | } else if (repetitions > 0) { 157 | // and no else part 158 | QString loopPart = mid(start + startTag.length(), end - start - startTag.length()); 159 | QString insertMe; 160 | for (int i = 0; i < repetitions; ++i) { 161 | // number variables, conditions and sub-loop within the loop 162 | QString nameNum = name + QString::number(i); 163 | QString s = loopPart; 164 | s.replace(QString("{%1.").arg(name), QString("{%1.").arg(nameNum)); 165 | s.replace(QString("{if %1.").arg(name), QString("{if %1.").arg(nameNum)); 166 | s.replace(QString("{ifnot %1.").arg(name), QString("{ifnot %1.").arg(nameNum)); 167 | s.replace(QString("{else %1.").arg(name), QString("{else %1.").arg(nameNum)); 168 | s.replace(QString("{end %1.").arg(name), QString("{end %1.").arg(nameNum)); 169 | s.replace(QString("{loop %1.").arg(name), QString("{loop %1.").arg(nameNum)); 170 | insertMe.append(s); 171 | } 172 | replace(start, end - start + endTag.length(), insertMe); 173 | } else { 174 | // repetitions==0 and no else part 175 | replace(start, end - start + endTag.length(), ""); 176 | } 177 | start = indexOf(startTag, start); 178 | } else { 179 | qWarning("Template: missing loop end %s in %s", qPrintable(endTag), qPrintable(sourceName)); 180 | } 181 | } 182 | if (count == 0 && warnings) { 183 | qWarning("Template: missing loop %s in %s", qPrintable(startTag), qPrintable(sourceName)); 184 | } 185 | return count; 186 | } 187 | 188 | void Template::enableWarnings(const bool enable) { 189 | warnings = enable; 190 | } 191 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/template.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "qtwebappglobal.h" 9 | 10 | #include 11 | #include 12 | 13 | namespace qtwebapp { 14 | 15 | /** 16 | Enhanced version of QString for template processing. Templates 17 | are usually loaded from files, but may also be loaded from 18 | prepared Strings. 19 | Example template file: 20 |

 21 | 	 Hello {username}, how are you?
 22 | 
 23 | 	 {if locked}
 24 | 	     Your account is locked.
 25 | 	 {else locked}
 26 | 	     Welcome on our system.
 27 | 	 {end locked}
 28 | 
 29 | 	 The following users are on-line:
 30 | 	     Username       Time
 31 | 	 {loop user}
 32 | 	     {user.name}    {user.time}
 33 | 	 {end user}
 34 | 	 

35 |

36 | Example code to fill this template: 37 |

 38 | 	 Template t(QFile("test.tpl"),QTextCode::codecForName("UTF-8"));
 39 | 	 t.setVariable("username", "Stefan");
 40 | 	 t.setCondition("locked",false);
 41 | 	 t.loop("user",2);
 42 | 	 t.setVariable("user0.name","Markus");
 43 | 	 t.setVariable("user0.time","8:30");
 44 | 	 t.setVariable("user1.name","Roland");
 45 | 	 t.setVariable("user1.time","8:45");
 46 | 	 

47 |

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

 51 | 	 <table>
 52 | 	 {loop row}
 53 | 	     <tr>
 54 | 	     {loop row.column}
 55 | 	         <td>{row.column.value}</td>
 56 | 	     {end row.column}
 57 | 	     </tr>
 58 | 	 {end row}
 59 | 	 </table>
 60 | 	 

61 |

62 | Example code to fill this nested loop with 3 rows and 4 columns: 63 |

 64 | 	 t.loop("row",3);
 65 | 
 66 | 	 t.loop("row0.column",4);
 67 | 	 t.setVariable("row0.column0.value","a");
 68 | 	 t.setVariable("row0.column1.value","b");
 69 | 	 t.setVariable("row0.column2.value","c");
 70 | 	 t.setVariable("row0.column3.value","d");
 71 | 
 72 | 	 t.loop("row1.column",4);
 73 | 	 t.setVariable("row1.column0.value","e");
 74 | 	 t.setVariable("row1.column1.value","f");
 75 | 	 t.setVariable("row1.column2.value","g");
 76 | 	 t.setVariable("row1.column3.value","h");
 77 | 
 78 | 	 t.loop("row2.column",4);
 79 | 	 t.setVariable("row2.column0.value","i");
 80 | 	 t.setVariable("row2.column1.value","j");
 81 | 	 t.setVariable("row2.column2.value","k");
 82 | 	 t.setVariable("row2.column3.value","l");
 83 | 	 

84 | @see TemplateLoader 85 | @see TemplateCache 86 | */ 87 | 88 | class QTWEBAPP_EXPORT Template : public QString { 89 | public: 90 | /** 91 | Constructor that reads the template from a string. 92 | @param source The template source text 93 | @param sourceName Name of the source file, used for logging 94 | */ 95 | Template(const QString &source, const QString &sourceName); 96 | 97 | /** 98 | Constructor that reads the template from a file. Note that this class does not 99 | cache template files by itself, so using this constructor is only recommended 100 | to be used on local filesystem. 101 | @param file File that provides the source text 102 | @param textCodec Encoding of the source 103 | @see TemplateLoader 104 | @see TemplateCache 105 | */ 106 | Template(QFile &file, const QTextCodec *textCodec); 107 | 108 | /** 109 | Replace a variable by the given value. 110 | Affects tags with the syntax 111 | 112 | - {name} 113 | 114 | After settings the 115 | value of a variable, the variable does not exist anymore, 116 | it it cannot be changed multiple times. 117 | @param name name of the variable 118 | @param value new value 119 | @return The count of variables that have been processed 120 | */ 121 | int setVariable(const QString &name, const QString &value); 122 | 123 | /** 124 | Set a condition. This affects tags with the syntax 125 | 126 | - {if name}...{end name} 127 | - {if name}...{else name}...{end name} 128 | - {ifnot name}...{end name} 129 | - {ifnot name}...{else name}...{end name} 130 | 131 | @param name Name of the condition 132 | @param value Value of the condition 133 | @return The count of conditions that have been processed 134 | */ 135 | int setCondition(const QString &name, bool value); 136 | 137 | /** 138 | Set number of repetitions of a loop. 139 | This affects tags with the syntax 140 | 141 | - {loop name}...{end name} 142 | - {loop name}...{else name}...{end name} 143 | 144 | @param name Name of the loop 145 | @param repetitions The number of repetitions 146 | @return The number of loops that have been processed 147 | */ 148 | int loop(const QString &name, const int repetitions); 149 | 150 | /** 151 | Enable warnings for missing tags 152 | @param enable Warnings are enabled, if true 153 | */ 154 | void enableWarnings(const bool enable = true); 155 | 156 | private: 157 | /** Name of the source file */ 158 | QString sourceName; 159 | 160 | /** Enables warnings, if true */ 161 | bool warnings; 162 | }; 163 | 164 | } // namespace qtwebapp 165 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/templatecache.cpp: -------------------------------------------------------------------------------- 1 | #include "templatecache.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | using namespace qtwebapp; 8 | 9 | TemplateCache::TemplateCache(const TemplateEngineConfig &cfg, QObject *parent) : TemplateLoader(cfg, parent) { 10 | cache.setMaxCost(cfg.cacheSize); 11 | cacheTimeout = cfg.cacheTime; 12 | long int cacheMaxCost = (long int)cache.maxCost(); 13 | #ifdef CMAKE_DEBUG 14 | qDebug("TemplateCache: timeout=%i, size=%li", cacheTimeout, cacheMaxCost); 15 | #endif 16 | } 17 | 18 | QString TemplateCache::tryFile(const QString &localizedName) { 19 | qint64 now = QDateTime::currentMSecsSinceEpoch(); 20 | mutex.lock(); 21 | // search in cache 22 | #ifdef CMAKE_DEBUG 23 | qDebug("TemplateCache: trying cached %s", qPrintable(localizedName)); 24 | #endif 25 | CacheEntry *entry = cache.object(localizedName); 26 | if (entry && (cacheTimeout == 0 || entry->created > now - cacheTimeout)) { 27 | mutex.unlock(); 28 | return entry->document; 29 | } 30 | // search on filesystem 31 | entry = new CacheEntry(); 32 | entry->created = now; 33 | entry->document = TemplateLoader::tryFile(localizedName); 34 | // Store in cache even when the file did not exist, to remember that there is no such file 35 | cache.insert(localizedName, entry, entry->document.size()); 36 | mutex.unlock(); 37 | return entry->document; 38 | } 39 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/templatecache.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qtwebappglobal.h" 4 | #include "templateengineconfig.h" 5 | #include "templateloader.h" 6 | 7 | #include 8 | 9 | namespace qtwebapp { 10 | 11 | /** 12 | Caching template loader, reduces the amount of I/O and improves performance 13 | on remote file systems. The cache has a limited size, it prefers to keep 14 | the last recently used files. Optionally, the maximum time of cached entries 15 | can be defined to enforce a reload of the template file after a while. 16 |

17 | In case of local file system, the use of this cache is optionally, since 18 | the operating system caches files already. 19 |

20 | Loads localized versions of template files. If the caller requests a file with the 21 | name "index" and the suffix is ".tpl" and the requested locale is "de_DE, de, en-US", 22 | then files are searched in the following order: 23 | 24 | - index-de_DE.tpl 25 | - index-de.tpl 26 | - index-en_US.tpl 27 | - index-en.tpl 28 | - index.tpl 29 |

30 | The following settings are required: 31 |

32 | 	  path=../templates
33 | 	  suffix=.tpl
34 | 	  encoding=UTF-8
35 | 	  cacheSize=1000000
36 | 	  cacheTime=60000
37 | 	  
38 | The path is relative to the directory of the config file. In case of windows, if the 39 | settings are in the registry, the path is relative to the current working directory. 40 |

41 | Files are cached as long as possible, when cacheTime=0. 42 | @see TemplateLoader 43 | */ 44 | 45 | class QTWEBAPP_EXPORT TemplateCache : public TemplateLoader { 46 | Q_OBJECT 47 | Q_DISABLE_COPY(TemplateCache) 48 | public: 49 | /** 50 | Constructor. 51 | @param settings Configuration settings, usually stored in an INI file. Must not be 0. 52 | Settings are read from the current group, so the caller must have called settings->beginGroup(). 53 | Because the group must not change during runtime, it is recommended to provide a 54 | separate QSettings instance that is not used by other parts of the program. 55 | The TemplateCache does not take over ownership of the QSettings instance, so the caller 56 | should destroy it during shutdown. 57 | @param parent Parent object 58 | */ 59 | TemplateCache(const TemplateEngineConfig &cfg, QObject *parent = nullptr); 60 | 61 | protected: 62 | /** 63 | Try to get a file from cache or filesystem. 64 | @param localizedName Name of the template with locale to find 65 | @return The template document, or empty string if not found 66 | */ 67 | QString tryFile(const QString &localizedName) override; 68 | 69 | private: 70 | struct CacheEntry { 71 | QString document; 72 | qint64 created; 73 | }; 74 | 75 | /** Timeout for each cached file */ 76 | int cacheTimeout; 77 | 78 | /** Cache storage */ 79 | QCache cache; 80 | 81 | /** Used to synchronize threads */ 82 | QMutex mutex; 83 | }; 84 | 85 | } // namespace qtwebapp 86 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/templateengineconfig.cpp: -------------------------------------------------------------------------------- 1 | #include "templateengineconfig.h" 2 | 3 | using namespace qtwebapp; 4 | 5 | TemplateEngineConfig::TemplateEngineConfig() {} 6 | 7 | TemplateEngineConfig::TemplateEngineConfig(const QSettings &settings) { 8 | parseSettings(settings); 9 | } 10 | 11 | TemplateEngineConfig::TemplateEngineConfig(QSettings *settings) { 12 | parseSettings(*settings); 13 | } 14 | 15 | void TemplateEngineConfig::parseSettings(const QSettings &settings) { 16 | #ifdef Q_OS_WIN 17 | if (settings.format() != QSettings::NativeFormat) 18 | #endif 19 | fileName = settings.fileName(); 20 | 21 | path = settings.value("path", path).toString(); 22 | suffix = settings.value("suffix", suffix).toString(); 23 | encoding = settings.value("encoding", encoding).toString(); 24 | 25 | cacheSize = parseNum(settings.value("cacheSize", cacheSize), 1024); 26 | cacheTime = parseNum(settings.value("cacheTime", cacheTime)); 27 | } 28 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/templateengineconfig.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "qtwebappglobal.h" 4 | 5 | #include 6 | 7 | namespace qtwebapp { 8 | class TemplateLoader; 9 | 10 | /** 11 | * This class stores all configuration information for the `TemplateCache` 12 | * class. It can be either created as a standard object and filled from 13 | * c++ code, or be constructed from a `QSettings` object. See `TemplateCache` 14 | * for an example of such a configuration file. 15 | */ 16 | class QTWEBAPP_EXPORT TemplateEngineConfig { 17 | friend class TemplateLoader; 18 | 19 | public: 20 | /** Creates a config with all standard values. */ 21 | TemplateEngineConfig(); 22 | /** Reads the configuration from the `QSettings` object. */ 23 | TemplateEngineConfig(const QSettings &settings); 24 | /** Reads the configuration from the `QSettings` object. */ 25 | TemplateEngineConfig(QSettings *settings); 26 | 27 | /// The path where the static files can be found. This can be either an 28 | /// absolute or relativ path or an qt resource path. 29 | QString path = "."; 30 | 31 | /// The default suffix of the template files. 32 | QString suffix = ".tpl"; 33 | 34 | /// The encoding that is sent to the web browser in case of text files. 35 | QString encoding; 36 | 37 | /// The size of the server cache. 38 | int cacheSize = 1e6; 39 | /// The timeout of each file in the servers cache. 40 | int cacheTime = 6e4; 41 | 42 | private: 43 | void parseSettings(const QSettings &settings); 44 | 45 | /// The filename of the settings if read from a file. It is used to resolve 46 | /// relative paths. 47 | QString fileName; 48 | }; 49 | 50 | } // namespace qtwebapp 51 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/templateloader.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #include "templateloader.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | using namespace qtwebapp; 17 | 18 | TemplateLoader::TemplateLoader(const TemplateEngineConfig &cfg, QObject *parent) : QObject(parent) { 19 | templatePath = cfg.path; 20 | // Convert relative path to absolute, based on the directory of the config file. 21 | if (!cfg.fileName.isEmpty() && QDir::isRelativePath(templatePath)) { 22 | QFileInfo configFile(cfg.fileName); 23 | templatePath = QFileInfo(configFile.absolutePath(), templatePath).absoluteFilePath(); 24 | } 25 | fileNameSuffix = cfg.suffix; 26 | QString encoding = cfg.encoding; 27 | if (encoding.isEmpty()) { 28 | textCodec = QTextCodec::codecForLocale(); 29 | } else { 30 | textCodec = QTextCodec::codecForName(encoding.toLocal8Bit()); 31 | } 32 | #ifdef CMAKE_DEBUG 33 | qDebug("TemplateLoader: path=%s, codec=%s", qPrintable(templatePath), qPrintable(encoding)); 34 | #endif 35 | } 36 | 37 | TemplateLoader::~TemplateLoader() {} 38 | 39 | QString TemplateLoader::tryFile(const QString &localizedName) { 40 | QString fileName = templatePath + "/" + localizedName + fileNameSuffix; 41 | #ifdef CMAKE_DEBUG 42 | qDebug("TemplateCache: trying file %s", qPrintable(fileName)); 43 | #endif 44 | QFile file(fileName); 45 | if (file.exists()) { 46 | file.open(QIODevice::ReadOnly); 47 | QString document = textCodec->toUnicode(file.readAll()); 48 | file.close(); 49 | if (file.error()) { 50 | qCritical("TemplateLoader: cannot load file %s, %s", qPrintable(fileName), qPrintable(file.errorString())); 51 | return ""; 52 | } else { 53 | return document; 54 | } 55 | } 56 | return ""; 57 | } 58 | 59 | Template TemplateLoader::getTemplate(const QString &templateName, const QString &locales) { 60 | QSet tried; // used to suppress duplicate attempts 61 | QStringList locs = locales.split(',', 62 | #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) 63 | Qt::SkipEmptyParts 64 | #else 65 | QString::SkipEmptyParts 66 | #endif 67 | ); 68 | 69 | // Search for exact match 70 | foreach (QString loc, locs) { 71 | loc.replace(QRegularExpression(";.*"), ""); 72 | loc.replace('-', '_'); 73 | 74 | QString localizedName = templateName + "-" + loc.trimmed(); 75 | if (!tried.contains(localizedName)) { 76 | QString document = tryFile(localizedName); 77 | if (!document.isEmpty()) { 78 | return Template(document, localizedName); 79 | } 80 | tried.insert(localizedName); 81 | } 82 | } 83 | 84 | // Search for correct language but any country 85 | foreach (QString loc, locs) { 86 | loc.replace(QRegularExpression("[;_-].*"), ""); 87 | QString localizedName = templateName + "-" + loc.trimmed(); 88 | if (!tried.contains(localizedName)) { 89 | QString document = tryFile(localizedName); 90 | if (!document.isEmpty()) { 91 | return Template(document, localizedName); 92 | } 93 | tried.insert(localizedName); 94 | } 95 | } 96 | 97 | // Search for default file 98 | QString document = tryFile(templateName); 99 | if (!document.isEmpty()) { 100 | return Template(document, templateName); 101 | } 102 | 103 | qCritical("TemplateCache: cannot find template %s", qPrintable(templateName)); 104 | return Template("", templateName); 105 | } 106 | -------------------------------------------------------------------------------- /QtWebApp/templateengine/templateloader.h: -------------------------------------------------------------------------------- 1 | /** 2 | @file 3 | @author Stefan Frings 4 | */ 5 | 6 | #pragma once 7 | 8 | #include "qtwebappglobal.h" 9 | #include "template.h" 10 | #include "templateengineconfig.h" 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace qtwebapp { 18 | 19 | /** 20 | Loads localized versions of template files. If the caller requests a file with the 21 | name "index" and the suffix is ".tpl" and the requested locale is "de_DE, de, en-US", 22 | then files are searched in the following order: 23 | 24 | - index-de_DE.tpl 25 | - index-de.tpl 26 | - index-en_US.tpl 27 | - index-en.tpl 28 | - index.tpl 29 | 30 | The following settings are required: 31 |

32 | 	  path=../templates
33 | 	  suffix=.tpl
34 | 	  encoding=UTF-8
35 | 	  
36 | The path is relative to the directory of the config file. In case of windows, if the 37 | settings are in the registry, the path is relative to the current working directory. 38 | @see TemplateCache 39 | */ 40 | 41 | class QTWEBAPP_EXPORT TemplateLoader : public QObject { 42 | Q_OBJECT 43 | Q_DISABLE_COPY(TemplateLoader) 44 | public: 45 | /** 46 | Constructor. 47 | @param settings configurations settings 48 | @param parent parent object 49 | */ 50 | TemplateLoader(const TemplateEngineConfig &cfg, QObject *parent = nullptr); 51 | 52 | /** Destructor */ 53 | virtual ~TemplateLoader(); 54 | 55 | /** 56 | Get a template for a given locale. 57 | This method is thread safe. 58 | @param templateName base name of the template file, without suffix and without locale 59 | @param locales Requested locale(s), e.g. "de_DE, en_EN". Strings in the format of 60 | the HTTP header Accept-Locale may be used. Badly formatted parts in the string are silently 61 | ignored. 62 | @return If the template cannot be loaded, an error message is logged and an empty template is returned. 63 | */ 64 | Template getTemplate(const QString &templateName, const QString &locales = QString()); 65 | 66 | protected: 67 | /** 68 | Try to get a file from cache or filesystem. 69 | @param localizedName Name of the template with locale to find 70 | @return The template document, or empty string if not found 71 | */ 72 | virtual QString tryFile(const QString &localizedName); 73 | 74 | /** Directory where the templates are searched */ 75 | QString templatePath; 76 | 77 | /** Suffix to the filenames */ 78 | QString fileNameSuffix; 79 | 80 | /** Codec for decoding the files */ 81 | QTextCodec *textCodec; 82 | }; 83 | 84 | } // namespace qtwebapp 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **THIS PROJECT IS NO LONGER MAINTAINED** 2 | 3 | # QtWebApp [![Build](https://github.com/msrd0/QtWebApp/workflows/Build/badge.svg)](https://github.com/msrd0/QtWebApp/actions?query=workflow%3ABuild) [![License](https://img.shields.io/badge/license-LGPL--3.0-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) 4 | 5 | This library was forked from http://stefanfrings.de/qtwebapp/index-en.html 6 | 7 | QtWebApp is a library to develop server-side web applications in C++. It depends on Qt5, minimum required version is 5.5. 8 | 9 | The library comes with 3 components: 10 | 11 | - HttpServer 12 | - TemplateEngine 13 | - Logging 14 | 15 | ## Usage 16 | 17 | This short example demonstrates how to use the library: 18 | 19 | ```C++ 20 | class DefaultRequestHandler : public HttpRequestHandler 21 | { 22 | public: 23 | void service(HttpRequest &request, HttpResponse &response); 24 | } 25 | 26 | void DefaultRequestHandler::service(HttpRequest &request, HttpResponse &response) 27 | { 28 | response.write("

" + request.getPath() + "

"); 29 | } 30 | 31 | int main(int argc, char **argv) 32 | { 33 | QCoreApplication app(argc, argv); 34 | 35 | QSettings *config = new QSettings; 36 | // ... 37 | 38 | new HttpListener(config, new DefaultRequestHandler); 39 | return app.exec(); 40 | } 41 | ``` 42 | 43 | You can the compile it using cmake, like this: 44 | 45 | ```CMake 46 | # ... 47 | find_package(QtWebApp REQUIRED COMPONENTS HttpServer) 48 | add_executable(thinkofabettername main.cpp) 49 | target_link_libraries(thinkofabettername ${QtWebApp_LIBRARIES}) 50 | # ... 51 | ``` 52 | 53 | ## Build 54 | 55 | QtWebApp uses CMake as the build system. To compile, simply run: 56 | 57 | ```bash 58 | mkdir build && cd build 59 | cmake -DCMAKE_BUILD_TYPE=Release .. 60 | make -j4 61 | sudo make install 62 | ``` 63 | -------------------------------------------------------------------------------- /format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/busybox ash 2 | set -eo pipefail 3 | 4 | check=n 5 | if [ "$1" == "--check" ] 6 | then 7 | check=y 8 | fi 9 | 10 | set -u 11 | 12 | format_file() { 13 | local tmpfile 14 | tmpfile=$(mktemp) 15 | clang-format "$1" >$tmpfile 16 | 17 | if [ $check == y ] 18 | then 19 | diff "$1" $tmpfile 20 | else 21 | mv $tmpfile "$1" 22 | fi 23 | } 24 | 25 | format_dir() { 26 | find "$1" \( -name '*.h' -or -name '*.cpp' \) -not -path '*build*' \ 27 | | while read file; do 28 | format_file "$file" 29 | done 30 | } 31 | 32 | format_dir Demo1 33 | format_dir Demo2 34 | format_dir QtWebApp 35 | --------------------------------------------------------------------------------