Compare commits

...

75 Commits

Author SHA1 Message Date
an-tao
a22956b82b Bump version to 1.9.11 2025-06-20 16:51:27 +08:00
Heran Yang
3c5749bbc2
Fix compile warning (#2337) 2025-06-18 17:43:25 +08:00
Heran Yang
7cd1ae8940
chore(workflow): upgrade Windows image and re-enable tests on Windows (#2336)
* chore(workflow): upgrade Windows image to 2022

* chore(test): re-enable tests on Windows
2025-06-18 15:47:28 +08:00
LordMZTE
c3f9192541
Add RawParameter API to pass raw SQL parameters to the database directly (#2335) 2025-06-17 23:15:47 +08:00
KEBE Mouhamad
e46e05e94a
dg_ctl: fix segfault when using --output option (#2330) 2025-06-05 13:52:33 +08:00
dm
26e7c6913c
Add support for continuation frame in WebSocketMessageParser (#2320)
Co-authored-by: antao <antao2002@gmail.com>
2025-06-04 10:02:07 +08:00
曹梦轩
8d640bafb4
Add cors example to demonstrate cross-origin support in drogon (#2323) 2025-05-26 10:10:39 +08:00
Leonardo Monteiro
f6b5404dbb
Add a new overload for execSqlCoro (#2314) 2025-05-22 18:16:11 +08:00
程憨憨
46b5c9044d
Fix issue with precision loss of double-type parameters in ORM inputs (#2310) 2025-05-19 19:07:37 +08:00
an-tao
ac0d4d0f89 Update trantor 2025-05-16 11:10:24 +08:00
cjserio
5c4057331e
Support for iOS compiling (#2307) 2025-05-15 17:25:56 +08:00
an-tao
95a518e7f2 Add qrcode for WeChat official account​ to the README file 2025-05-13 11:15:48 +08:00
KEBE Mouhamad
c03a3df106
added -o|--output option to drogon_ctl create models (#2304)
Co-authored-by: Mouhamad Kebe <mouhamad.kebe@ses.com>
2025-05-10 21:20:52 +08:00
Alexey Gerasimchuck
d6a33f93c9
Added handleFatalError in handleClosed (#2291) 2025-04-21 11:15:00 +08:00
An Tao
59cd4366c7
Fix CI on MacOS (#2289) 2025-04-08 14:11:03 +08:00
Alexey Gerasimchuck
c92d146374
Improved Postgres connection stability (#2286) 2025-04-08 11:03:02 +08:00
Axel Svensson
3c7c66e310
fix: Do not write to source directory during build (#2288)
Fixes #2287
2025-04-08 10:23:43 +08:00
Tanglong3bf
1fb67d68be
fix: Fix a bug in isAutoCreationClass<T>. (#2277) 2025-03-22 15:46:41 +08:00
antao
cbf63f8fc4 Bump version to 1.9.10 2025-02-20 21:19:18 +08:00
an-tao
d68e8aa554 Fix the CI status badge 2025-02-20 14:16:30 +08:00
An Tao
41537a6e86
Fix ci: codespell (#2259) 2025-02-18 22:29:47 +08:00
TheEnigmist
a32dc67867
Added path_exempt in AccessLogger plugin config to exclude desired path from logging (#2258) 2025-02-18 10:28:57 +08:00
ereynalabs
e155df9f66
Fix the issue in view generation by including the missing header file drogon/utils/Utilities.h (#2248)
Co-authored-by: dlinten <david.linten@gmail.com>
2025-02-06 15:19:44 +08:00
Alexey Gerasimchuck
f5de41f5d7
Make quit function thread safe (#2247) 2025-02-02 22:22:17 +08:00
pan93412
a3b4779540
Improve the zh-TW README translation (#2239)
* Improve the zh-TW README translation

* Unify the "View" term in the zh-TW README
2025-01-14 20:08:30 +08:00
fantasy-peak
686f68a12f
Add setConnectionCallback (#2204) 2025-01-08 20:00:44 +08:00
An Tao
152a69f1e9
ORM:Avoid unnecessary copies when returning search results (#2237) 2025-01-02 22:44:09 +08:00
antao
38dd5fea31 Bump version to 1.9.9 2025-01-01 11:26:30 +08:00
Alexey Gerasimchuck
3a6268f7e9
Added emptiness check to the LogStream &operator<< with std::string_view (#2234) 2024-12-19 10:36:20 +08:00
antao
47f8af7ca1 Update README 2024-12-14 01:17:49 +08:00
Heran Yang
44f796b796
Chore(workflow/cmake.yml): upgrade macos runner (#2228) 2024-12-10 12:56:24 +08:00
Antonio Nesic
df7e83ae74
Added Partitioned flag for cookies (#2230)
Co-authored-by: Antonio Nesic <anesic@collectivemind.dev>
2024-12-10 12:55:16 +08:00
EasyMoney322
99e816283d
Fix: Removed dependency on locales being installed (#2227) 2024-12-09 10:53:15 +08:00
EasyMoney322
3e944d28d8
Fixed issues created by the date string being localized (#2217) 2024-12-05 18:08:24 +08:00
Eyal Niv
1765223755
Fix CMAKE issues mentioned in #2144 and a linking problem which manifest with gcc12.3 when building with shared libs (#2208) (#2213)
* Respect find_package QUIET

* Add policy_max to cmake_minimum_required
Avoid deprecation warning introduced by cmake 3.31

* Add missing DROGON_EXPORT
2024-11-26 22:53:09 +08:00
hanhuayin
71b6d57cae
Update FindFilesystem.cmake to check for GNU instead of GCC for CMAKE_CXX_COMPILER_ID (#2211)
This patch fixes a build issue encountered in our Amazon Linux 2 environment when integrating Drogon alongside other dependencies. The issue occurred because the filesystem check failed to compile when the compiler ID was set to "GNU", due to missing -std=c++17 flag. The existing CXX_FILESYSTEM_HAVE_FS check was looking for "GCC" instead of "GNU". "GNU" is the correct identifier for gcc according to CMake documentation (see https://cmake.org/cmake/help/latest/variable/CMAKE_LANG_COMPILER_ID.html).

Interestingly, this issue only manifested when Drogon was integrated alongside other dependencies. When Drogon was the sole project added to our build, compilation proceeded without error.
2024-11-25 16:15:21 +08:00
Tanglong3bf
882c1d9ecd
fix a bug in plugin Redirector. (#2198) 2024-11-16 15:04:57 +08:00
dependabot[bot]
8541e67143
Bump docker/login-action from 1 to 3 (#2197)
Bumps [docker/login-action](https://github.com/docker/login-action) from 1 to 3.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v1...v3)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-11-02 09:48:46 +08:00
antao
6d9ecb8d8d Bump version to 1.9.8 2024-10-27 14:31:56 +08:00
fantasy-peak
23c561f072
Add check the client connection status (#2191) 2024-10-25 16:08:03 +08:00
Tanglong3bf
284d14b8ca
Fix some bugs in plugin PromExporter. (#2189) 2024-10-21 09:51:34 +08:00
fantasy-peak
ca2210331d
Add sending customized http requests to drogon_ctl (#2186) 2024-10-17 10:47:04 +08:00
Martin Chang
3fce70b535
Replace rejection sampling and remove use of rand() (#2180) 2024-10-10 13:45:46 +08:00
An Tao
bf1fc03bff
Fix lint (#2181) 2024-10-09 18:20:29 +08:00
an-tao
5225bb3295 Update trantor and add docker actions 2024-10-09 16:45:19 +08:00
An Tao
ac0a1b873e
Fix a bug after removing content-length header in some responses (#2176) 2024-10-02 16:42:40 +08:00
Muhammad
912f1d803c
Optimize query params and allow for empty values (#2171) 2024-09-26 15:47:58 +08:00
An Tao
13d7148764
Remove websocketResponseTest from windows shared library env (#2170) 2024-09-25 20:16:40 +08:00
Chad Barth
b0c5331bc1
Remove content-length header from 101 Switching Protocols response (#2164) 2024-09-25 17:52:41 +08:00
Christopher T
c9f5754423
Add support for escaped identifiers in Postgresql (#2167) 2024-09-23 16:00:50 +08:00
an-tao
31fb18fb46 Update trantor 2024-09-20 16:22:09 +08:00
toge
f918ead0ae
include exception header for std::exception_ptr (#2159) 2024-09-19 10:11:04 +08:00
Muhammad
fee34095a2
Add Hodor whitelists (#2154) 2024-09-19 09:56:11 +08:00
Omar Mohamed Khallaf
2911a7c08a
Fix coroutine continuation handle (#2163)
Using coroutines directly by the user e.g. declaring a function with
drogon::Task<> return type, will have it's continuation set to nullptr.

Returning nullptr causes a segfault and it must be checked before
returning e.g. the snippet form https://en.cppreference.com/w/cpp/coroutine/noop_coroutine

```cpp
struct final_awaiter
{
    std::coroutine_handle<>
        await_suspend(std::coroutine_handle<promise_type> h) noexcept
    {
        // final_awaiter::await_suspend is called when the execution of the
        // current coroutine (referred to by 'h') is about to finish.
        // If the current coroutine was resumed by another coroutine via
        // co_await get_task(), a handle to that coroutine has been stored
        // as h.promise().previous. In that case, return the handle to resume
        // the previous coroutine.
        // Otherwise, return noop_coroutine(), whose resumption does nothing.

        if (auto previous = h.promise().previous; previous)
            return previous;
        else
            return std::noop_coroutine();
    }
};
```

This commit default initializes the continuation handle to no op coroutine and
avoids the check.

Signed-off-by: Omar Mohamed <mohamed.omar67492@gmail.com>
2024-09-19 09:52:51 +08:00
Muhammad
1b4653577f
Revert original path to its initial behavior (#2157) 2024-09-17 11:54:33 +08:00
Muhammad
bbcad71458
Add in-place base64 encode and decode (#2153) 2024-09-14 09:57:07 +08:00
Muhammad
beec858eba
Partially revert commit 93d8fb425d7e95939d398afd51b9d5adc4392c43 (#2156) 2024-09-13 17:34:23 +08:00
Muhammad
93d8fb425d
Fix forwarding with space in url by encoding (#2155) 2024-09-13 09:46:06 +08:00
antao
73406d1225 Bump version to 1.9.7 2024-09-10 23:12:02 +08:00
Ponder
6bafdf30fd
Feature: TcpServer hot reload SSL file (#2150) 2024-09-10 11:38:56 +08:00
元路
59919f33ef
Refine SQLite3 error types with new exception handling (#2145)
Signed-off-by: yuanlu <2573580691@qq.com>
2024-09-06 15:37:09 +08:00
an-tao
1326205483 Change a log level 2024-08-23 11:34:50 +08:00
Omar Mohamed Khallaf
80ec7d9211
Use correct libraries when compiling statically (#2136)
When compiling statically, cmake pulls shared libraries as dependencies
for drogon e.g. libpq.so instead of libpq.a. This causes linkage errors
when compiling the whole program.

The flag USE_STATIC_LIBS_ONLY should set the CMAKE_FIND_LIBRARY_SUFFIXES
to the appropriate suffix on different platforms, thus find_* commands
find the right library.
2024-08-22 15:46:47 +08:00
fantasy-peak
5b5d1906bf
Add requestsBufferSize function (#2124) 2024-08-14 11:15:33 +08:00
Muhammad
206ef0d881
Modernize cookies (#2112) 2024-08-08 22:23:28 +08:00
fantasy-peak
c46f149c2c
Add coroutine mutex (#2095) 2024-08-08 15:17:06 +08:00
Bohdan Tsehelnyk
0546032edc
change stoi to stoul (#2115) 2024-08-06 17:16:06 +08:00
Bohdan Tsehelnyk
f743cfd4d1
add quotes (#2116) 2024-08-06 17:15:01 +08:00
Muhammad
500d44faac
Allow MultiPartParser to be movable (#2107) 2024-07-23 11:52:52 +03:00
antao
e786907478 Bump version to 1.9.6 2024-07-20 23:35:17 +08:00
Nitromelon
5d4523a3a6
Support request stream (#2055) 2024-07-03 11:31:39 +08:00
fantasy-peak
dfacd1b454
Add setsockopt to HttpServer (#2086) 2024-07-02 10:04:56 +08:00
Chuck
85b918f9e9
Update README.md (#2080)
A few minor typos.
2024-06-21 10:04:11 +08:00
fantasy-peak
7b8d0085b1
Delay parsing json for HttpClient (#2077) 2024-06-20 00:09:08 +08:00
An Tao
f6913f6328 Add an example of prometheus (#2076) 2024-06-19 14:13:15 +08:00
116 changed files with 5143 additions and 636 deletions

View File

@ -17,7 +17,7 @@ env:
jobs: jobs:
windows: windows:
name: windows/msvc - ${{ matrix.link }} name: windows/msvc - ${{ matrix.link }}
runs-on: windows-2019 runs-on: windows-2022
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -44,9 +44,6 @@ jobs:
- name: Create Build Environment & Configure Cmake - name: Create Build Environment & Configure Cmake
shell: bash shell: bash
working-directory: ./build working-directory: ./build
# For unknown reasons, we fail to create file in windows ci environment.
# So examples, drogon_ctl and integration tests can not be built in windows ci.
# We should try to enable them again in the future.
run: | run: |
[[ ${{ matrix.link }} == "SHARED" ]] && shared="ON" || shared="OFF" [[ ${{ matrix.link }} == "SHARED" ]] && shared="ON" || shared="OFF"
cmake .. \ cmake .. \
@ -54,8 +51,8 @@ jobs:
-DBUILD_TESTING=on \ -DBUILD_TESTING=on \
-DBUILD_SHARED_LIBS=$shared \ -DBUILD_SHARED_LIBS=$shared \
-DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" \ -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" \
-DBUILD_CTL=OFF \ -DBUILD_CTL=ON \
-DBUILD_EXAMPLES=OFF \ -DBUILD_EXAMPLES=ON \
-DUSE_SPDLOG=ON \ -DUSE_SPDLOG=ON \
-DCMAKE_INSTALL_PREFIX=../install \ -DCMAKE_INSTALL_PREFIX=../install \
-DCMAKE_POLICY_DEFAULT_CMP0091=NEW \ -DCMAKE_POLICY_DEFAULT_CMP0091=NEW \
@ -73,7 +70,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
osver: [12, 13] osver: [13, 14, 15]
steps: steps:
- name: Checkout Drogon source code - name: Checkout Drogon source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -102,7 +99,6 @@ jobs:
- name: Prepare for testing - name: Prepare for testing
run: | run: |
brew tap homebrew/services
brew services restart postgresql@14 brew services restart postgresql@14
brew services start mariadb brew services start mariadb
brew services start redis brew services start redis

View File

@ -11,5 +11,5 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- run: pip install --user codespell[toml] - run: sudo apt-get install -y codespell
- run: codespell --ignore-words-list="coo,folx,ot,statics,xwindows,NotIn," --skip="*.csp" - run: codespell --ignore-words-list="coo,folx,ot,statics,xwindows,NotIn,aNULL," --skip="*.csp"

View File

@ -30,7 +30,7 @@ jobs:
CLANG_FORMAT: clang-format-17 CLANG_FORMAT: clang-format-17
cpplint: cpplint:
runs-on: ubuntu-22.04 runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4

28
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,28 @@
name: Build and Push Docker Image
on:
release:
types: [created] # 当新版本被创建时触发
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build Docker image
run: |
cd docker/ubuntu
docker build -t drogonframework/drogon:latest .
- name: Push Docker image
run: |
docker push drogonframework/drogon:latest

1
.gitignore vendored
View File

@ -35,7 +35,6 @@ build/
cmake-build-debug/ cmake-build-debug/
cmake-build-debug-visual-studio/ cmake-build-debug-visual-studio/
.idea/ .idea/
lib/inc/drogon/version.h
html/ html/
latex/ latex/
.vscode .vscode

View File

@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5) cmake_minimum_required(VERSION 3.5...3.31)
project(drogon) project(drogon)
@ -13,6 +13,7 @@ option(BUILD_DOC "Build Doxygen documentation" OFF)
option(BUILD_BROTLI "Build Brotli" ON) option(BUILD_BROTLI "Build Brotli" ON)
option(BUILD_YAML_CONFIG "Build yaml config" ON) option(BUILD_YAML_CONFIG "Build yaml config" ON)
option(USE_SUBMODULE "Use trantor as a submodule" ON) option(USE_SUBMODULE "Use trantor as a submodule" ON)
option(USE_STATIC_LIBS_ONLY "Use only static libraries as dependencies" OFF)
include(CMakeDependentOption) include(CMakeDependentOption)
CMAKE_DEPENDENT_OPTION(BUILD_POSTGRESQL "Build with postgresql support" ON "BUILD_ORM" OFF) CMAKE_DEPENDENT_OPTION(BUILD_POSTGRESQL "Build with postgresql support" ON "BUILD_ORM" OFF)
@ -24,7 +25,7 @@ CMAKE_DEPENDENT_OPTION(USE_SPDLOG "Allow using the spdlog logging library" OFF "
set(DROGON_MAJOR_VERSION 1) set(DROGON_MAJOR_VERSION 1)
set(DROGON_MINOR_VERSION 9) set(DROGON_MINOR_VERSION 9)
set(DROGON_PATCH_VERSION 5) set(DROGON_PATCH_VERSION 11)
set(DROGON_VERSION set(DROGON_VERSION
${DROGON_MAJOR_VERSION}.${DROGON_MINOR_VERSION}.${DROGON_PATCH_VERSION}) ${DROGON_MAJOR_VERSION}.${DROGON_MINOR_VERSION}.${DROGON_PATCH_VERSION})
set(DROGON_VERSION_STRING "${DROGON_VERSION}") set(DROGON_VERSION_STRING "${DROGON_VERSION}")
@ -41,7 +42,7 @@ set(INSTALL_DROGON_CMAKE_DIR ${DEF_INSTALL_DROGON_CMAKE_DIR}
if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC") if (CMAKE_CXX_COMPILER_ID MATCHES "MSVC")
# Force MSVC to use UTF-8 because that's what we use. Otherwise it uses # Force MSVC to use UTF-8 because that's what we use. Otherwise it uses
# the default of whatever Windows sets and causes encoding issues. # the default of whatever Windows sets and causes encoding issues.
message(STATUS "You are using MSVC. Forceing to use UTF-8") message(STATUS "You are using MSVC. Forcing to use UTF-8")
add_compile_options("$<$<C_COMPILER_ID:MSVC>:/utf-8>") add_compile_options("$<$<C_COMPILER_ID:MSVC>:/utf-8>")
add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>") add_compile_options("$<$<CXX_COMPILER_ID:MSVC>:/utf-8>")
if (MSVC_VERSION GREATER_EQUAL 1914) if (MSVC_VERSION GREATER_EQUAL 1914)
@ -77,6 +78,10 @@ if (BUILD_SHARED_LIBS)
endif () endif ()
endif (BUILD_SHARED_LIBS) endif (BUILD_SHARED_LIBS)
if(USE_STATIC_LIBS_ONLY)
set(CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_STATIC_LIBRARY_SUFFIX}")
endif(USE_STATIC_LIBS_ONLY)
if(USE_SPDLOG) if(USE_SPDLOG)
find_package(spdlog CONFIG) find_package(spdlog CONFIG)
if(spdlog_FOUND) if(spdlog_FOUND)
@ -116,6 +121,7 @@ endif()
target_include_directories( target_include_directories(
${PROJECT_NAME} ${PROJECT_NAME}
PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/lib/inc> PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/lib/inc>
$<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}/lib/inc>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/orm_lib/inc> $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/orm_lib/inc>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/nosql_lib/redis/inc> $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/nosql_lib/redis/inc>
$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}> $<BUILD_INTERFACE:${PROJECT_BINARY_DIR}>
@ -268,6 +274,7 @@ set(DROGON_SOURCES
lib/src/HttpFileUploadRequest.cc lib/src/HttpFileUploadRequest.cc
lib/src/HttpRequestImpl.cc lib/src/HttpRequestImpl.cc
lib/src/HttpRequestParser.cc lib/src/HttpRequestParser.cc
lib/src/RequestStream.cc
lib/src/HttpResponseImpl.cc lib/src/HttpResponseImpl.cc
lib/src/HttpResponseParser.cc lib/src/HttpResponseParser.cc
lib/src/HttpServer.cc lib/src/HttpServer.cc
@ -278,6 +285,7 @@ set(DROGON_SOURCES
lib/src/ListenerManager.cc lib/src/ListenerManager.cc
lib/src/LocalHostFilter.cc lib/src/LocalHostFilter.cc
lib/src/MultiPart.cc lib/src/MultiPart.cc
lib/src/MultipartStreamParser.cc
lib/src/NotFound.cc lib/src/NotFound.cc
lib/src/PluginsManager.cc lib/src/PluginsManager.cc
lib/src/PromExporter.cc lib/src/PromExporter.cc
@ -332,23 +340,24 @@ set(private_headers
lib/src/ConfigAdapterManager.h lib/src/ConfigAdapterManager.h
lib/src/JsonConfigAdapter.h lib/src/JsonConfigAdapter.h
lib/src/YamlConfigAdapter.h lib/src/YamlConfigAdapter.h
lib/src/ConfigAdapter.h) lib/src/ConfigAdapter.h
lib/src/MultipartStreamParser.h)
if (NOT WIN32) if (NOT WIN32 AND NOT CMAKE_SYSTEM_NAME STREQUAL "iOS")
set(DROGON_SOURCES set(DROGON_SOURCES
${DROGON_SOURCES} ${DROGON_SOURCES}
lib/src/SharedLibManager.cc) lib/src/SharedLibManager.cc)
set(private_headers set(private_headers
${private_headers} ${private_headers}
lib/src/SharedLibManager.h) lib/src/SharedLibManager.h)
else (NOT WIN32) elseif(WIN32)
set(DROGON_SOURCES set(DROGON_SOURCES
${DROGON_SOURCES} ${DROGON_SOURCES}
third_party/mman-win32/mman.c) third_party/mman-win32/mman.c)
set(private_headers set(private_headers
${private_headers} ${private_headers}
third_party/mman-win32/mman.h) third_party/mman-win32/mman.h)
endif (NOT WIN32) endif()
if (BUILD_POSTGRESQL) if (BUILD_POSTGRESQL)
# find postgres # find postgres
@ -502,7 +511,7 @@ execute_process(COMMAND "git" rev-parse HEAD
OUTPUT_VARIABLE GIT_SHA1 OUTPUT_VARIABLE GIT_SHA1
ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE) ERROR_QUIET OUTPUT_STRIP_TRAILING_WHITESPACE)
configure_file("${PROJECT_SOURCE_DIR}/cmake/templates/version.h.in" configure_file("${PROJECT_SOURCE_DIR}/cmake/templates/version.h.in"
"${PROJECT_SOURCE_DIR}/lib/inc/drogon/version.h" @ONLY) "${CMAKE_CURRENT_BINARY_DIR}/lib/inc/drogon/version.h" @ONLY)
if (DROGON_CXX_STANDARD EQUAL 20) if (DROGON_CXX_STANDARD EQUAL 20)
option(USE_COROUTINE "Enable C++20 coroutine support" ON) option(USE_COROUTINE "Enable C++20 coroutine support" ON)
@ -559,6 +568,7 @@ set(DROGON_HEADERS
lib/inc/drogon/HttpFilter.h lib/inc/drogon/HttpFilter.h
lib/inc/drogon/HttpMiddleware.h lib/inc/drogon/HttpMiddleware.h
lib/inc/drogon/HttpRequest.h lib/inc/drogon/HttpRequest.h
lib/inc/drogon/RequestStream.h
lib/inc/drogon/HttpResponse.h lib/inc/drogon/HttpResponse.h
lib/inc/drogon/HttpSimpleController.h lib/inc/drogon/HttpSimpleController.h
lib/inc/drogon/HttpTypes.h lib/inc/drogon/HttpTypes.h
@ -574,7 +584,7 @@ set(DROGON_HEADERS
lib/inc/drogon/WebSocketConnection.h lib/inc/drogon/WebSocketConnection.h
lib/inc/drogon/WebSocketController.h lib/inc/drogon/WebSocketController.h
lib/inc/drogon/drogon.h lib/inc/drogon/drogon.h
lib/inc/drogon/version.h ${CMAKE_CURRENT_BINARY_DIR}/lib/inc/drogon/version.h
lib/inc/drogon/drogon_callbacks.h lib/inc/drogon/drogon_callbacks.h
lib/inc/drogon/PubSubService.h lib/inc/drogon/PubSubService.h
lib/inc/drogon/drogon_test.h lib/inc/drogon/drogon_test.h

View File

@ -10,8 +10,9 @@ filter=-runtime/references
# CHECK macros are from Drogon, not Google Test. # CHECK macros are from Drogon, not Google Test.
filter=-readability/check filter=-readability/check
# Don't warn about the use of C++11 features. # Don't warn about the use of C++11 or C++17 features.
filter=-build/c++11 filter=-build/c++11
filter=-build/c++17
filter=-build/include_subdir filter=-build/include_subdir

View File

@ -4,6 +4,175 @@ All notable changes to this project will be documented in this file.
## [Unreleased] ## [Unreleased]
## [1.9.11] - 2025-06-20
### API changes list
- Add a new overload for execSqlCoro.
### Changed
- Do not write to source directory during build.
- Improve Postgres connection stability.
- Add handleFatalError in handleClosed.
- Add -o|--output option to drogon_ctl create models.
- Add qrcode for WeChat official account to the README file.
- Support for iOS compiling.
- Add cors example to demonstrate cross-origin support in drogon.
- Add support for continuation frame in WebSocketMessageParser.
- Add RawParameter API to pass raw SQL parameters.
- Upgrade Windows image and re-enable tests on Windows.
### Fixed
- Fix a bug in isAutoCreationClass<T>.
- Fix CI on MacOS.
- Fix issue with precision loss of double-type parameters in ORM inputs.
## [1.9.10] - 2025-02-20
### API changes list
- Add setConnectionCallback.
### Changed
- ORM:Avoid unnecessary copies when returning search results.
- Improve the zh-TW README translation.
- Make quit function thread safe.
- Added path_exempt in AccessLogger plugin config to exclude desired paths.
### Fixed
- Fix the issue in view generation by including the missing header file.
- Fix ci: codespell.
## [1.9.9] - 2025-01-01
### API changes list
- Added Partitioned flag for cookies.
### Changed
- Update FindFilesystem.cmake to check for GNU instead of GCC for CMAKE_CXX_COMPILER_ID.
- Update README.
- Chore(workflow/cmake.yml): upgrade macos runner.
- Add emptiness check to the LogStream &operator<< with std::string_view.
### Fixed
- Fix a bug in plugin Redirector.
- Fix CMAKE issues mentioned in #2144 and a linking problem which manifest with gcc12.3 when building with shared libs.
- Fix: Remove dependency on locales being installed on the system.
## [1.9.8] - 2024-10-27
### API changes list
- Add in-place base64 encode and decode.
- Add check the client connection status.
### Changed
- Add Hodor whitelists.
- Include exception header for std::exception_ptr.
- Add support for escaped identifiers in Postgresql.
- Remove content-length header from 101 Switching Protocols response.
- Remove websocketResponseTest from windows shared library env.
- Optimize query params and allow for empty values.
- Replace rejection sampling and remove use of rand().
- Add sending customized http requests to drogon_ctl.
### Fixed
- Fix coroutine continuation handle.
- Fix some bugs in plugin PromExporter.
- Fix a bug after removing content-length header in some responses.
## [1.9.7] - 2024-09-10
### API changes list
- Add coroutine mutex.
- Add requestsBufferSize function.
- Refine SQLite3 error types with new exception handling.
- Add a new method to reload SSL files on the fly.
### Changed
- Allow MultiPartParser to be movable.
- Add quotes to the table name in the ORM generator.
- Change stoi to stoul in the Field class.
- Modernize cookies.
- Change a log level.
### Fixed
- Use correct libraries when compiling statically.
## [1.9.6] - 2024-07-20
### API changes list
- Add setsockopt to HttpServer.
- Support request stream.
### Changed
- Allow MultiPartParser to parse PATCH requests.
- Add an example of prometheus.
- Delay parsing json for HttpClient.
- Update README.md.
### Fixed
- Fix some compilation warnings.
- Fix typo in yaml config.
## [1.9.5] - 2024-06-08 ## [1.9.5] - 2024-06-08
### API changes list ### API changes list
@ -351,7 +520,7 @@ All notable changes to this project will be documented in this file.
- Remove unused CI files and Jekyll config. - Remove unused CI files and Jekyll config.
- Ensure that all filters, AOP advices, and handlers are executed within the IO threads. - Ensure that all filters, AOP advice, and handlers are executed within the IO threads.
- Update test.sh and build.sh by appending prefix "X" to string variable comparisons. - Update test.sh and build.sh by appending prefix "X" to string variable comparisons.
@ -683,7 +852,7 @@ All notable changes to this project will be documented in this file.
- Check HTTP client is not sending requests in sync mode on the same event loop. - Check HTTP client is not sending requests in sync mode on the same event loop.
- Start listening after beginning advices. - Start listening after beginning advice.
- Allow using json_cpp in other sublibraries. - Allow using json_cpp in other sublibraries.
@ -1673,7 +1842,19 @@ All notable changes to this project will be documented in this file.
## [1.0.0-beta1] - 2019-06-11 ## [1.0.0-beta1] - 2019-06-11
[Unreleased]: https://github.com/an-tao/drogon/compare/v1.9.5...HEAD [Unreleased]: https://github.com/an-tao/drogon/compare/v1.9.11...HEAD
[1.9.11]: https://github.com/an-tao/drogon/compare/v1.9.10...v1.9.11
[1.9.10]: https://github.com/an-tao/drogon/compare/v1.9.9...v1.9.10
[1.9.9]: https://github.com/an-tao/drogon/compare/v1.9.8...v1.9.9
[1.9.8]: https://github.com/an-tao/drogon/compare/v1.9.7...v1.9.8
[1.9.7]: https://github.com/an-tao/drogon/compare/v1.9.6...v1.9.7
[1.9.6]: https://github.com/an-tao/drogon/compare/v1.9.5...v1.9.6
[1.9.5]: https://github.com/an-tao/drogon/compare/v1.9.4...v1.9.5 [1.9.5]: https://github.com/an-tao/drogon/compare/v1.9.4...v1.9.5

View File

@ -1,6 +1,6 @@
![](https://github.com/an-tao/drogon/wiki/images/drogon-white17.jpg) ![](https://github.com/an-tao/drogon/wiki/images/drogon-white17.jpg)
[![Build Status](https://github.com/an-tao/drogon/workflows/Build%20Drogon/badge.svg?branch=master)](https://github.com/drogonframework/drogon/actions) [![Build Status](https://github.com/drogonframework/drogon/actions/workflows/cmake.yml/badge.svg?branch=master)](https://github.com/drogonframework/drogon/actions)
[![Conan Center](https://img.shields.io/conan/v/drogon)](https://conan.io/center/recipes/drogon) [![Conan Center](https://img.shields.io/conan/v/drogon)](https://conan.io/center/recipes/drogon)
[![Join the telegram group at https://t.me/joinchat/_mMNGv0748ZkMDAx](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/joinchat/_mMNGv0748ZkMDAx) [![Join the telegram group at https://t.me/joinchat/_mMNGv0748ZkMDAx](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/joinchat/_mMNGv0748ZkMDAx)
[![Join our Discord](https://dcbadge.vercel.app/api/server/3DvHY6Ewuj?style=flat)](https://discord.gg/3DvHY6Ewuj) [![Join our Discord](https://dcbadge.vercel.app/api/server/3DvHY6Ewuj?style=flat)](https://discord.gg/3DvHY6Ewuj)
@ -8,7 +8,7 @@
English | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md) English | [简体中文](./README.zh-CN.md) | [繁體中文](./README.zh-TW.md)
### Overview ### Overview
**Drogon** is a C++17/20 based HTTP application framework. Drogon can be used to easily build various types of web application server programs using C++. **Drogon** is the name of a dragon in the American TV series "Game of Thrones" that I really like. **Drogon** is a C++17/20 based HTTP application framework. Drogon can be used to easily build various types of web application server programs using C++. **Drogon** is the name of a dragon from the American TV series *Game of Thrones*, which I really enjoy.
Drogon is a cross-platform framework, It supports Linux, macOS, FreeBSD, OpenBSD, HaikuOS, and Windows. Its main features are as follows: Drogon is a cross-platform framework, It supports Linux, macOS, FreeBSD, OpenBSD, HaikuOS, and Windows. Its main features are as follows:
@ -183,7 +183,7 @@ As you can see, users can use the `HttpController` to map paths and parameters a
In addition, you can also find that all handler interfaces are in asynchronous mode, where the response is returned by a callback object. This design is for performance reasons because in asynchronous mode the drogon application can handle a large number of concurrent requests with a small number of threads. In addition, you can also find that all handler interfaces are in asynchronous mode, where the response is returned by a callback object. This design is for performance reasons because in asynchronous mode the drogon application can handle a large number of concurrent requests with a small number of threads.
After compiling all of the above source files, we get a very simple web application. This is a good start. **For more information, please visit the [wiki](https://github.com/an-tao/drogon/wiki/ENG-01-Overview)** After compiling all of the above source files, we get a very simple web application. This is a good start. **For more information, please visit the [documentation](https://drogonframework.github.io/drogon-docs/#/) on GitHub**.
## Cross-compilation ## Cross-compilation

View File

@ -1,6 +1,6 @@
![](https://github.com/an-tao/drogon/wiki/images/drogon-white17.jpg) ![](https://github.com/an-tao/drogon/wiki/images/drogon-white17.jpg)
[![Build Status](https://github.com/an-tao/drogon/workflows/Build%20Drogon/badge.svg?branch=master)](https://github.com/drogonframework/drogon/actions) [![Build Status](https://github.com/drogonframework/drogon/actions/workflows/cmake.yml/badge.svg?branch=master)](https://github.com/drogonframework/drogon/actions)
[![Conan Center](https://img.shields.io/conan/v/drogon)](https://conan.io/center/recipes/drogon) [![Conan Center](https://img.shields.io/conan/v/drogon)](https://conan.io/center/recipes/drogon)
[![Join the telegram group at https://t.me/joinchat/_mMNGv0748ZkMDAx](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/joinchat/_mMNGv0748ZkMDAx) [![Join the telegram group at https://t.me/joinchat/_mMNGv0748ZkMDAx](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/joinchat/_mMNGv0748ZkMDAx)
[![Join our Discord](https://dcbadge.vercel.app/api/server/3DvHY6Ewuj?style=flat)](https://discord.gg/3DvHY6Ewuj) [![Join our Discord](https://dcbadge.vercel.app/api/server/3DvHY6Ewuj?style=flat)](https://discord.gg/3DvHY6Ewuj)
@ -186,7 +186,7 @@ class User : public drogon::HttpController<User>
另外,你可以发现前面所有的处理函数接口都是异步的,处理器的响应是通过回调对象返回的。这种设计是出于对高性能的考虑,因为在异步模式下,可以使用少量的线程(比如和处理器核心数相等的线程)处理大量的并发请求。 另外,你可以发现前面所有的处理函数接口都是异步的,处理器的响应是通过回调对象返回的。这种设计是出于对高性能的考虑,因为在异步模式下,可以使用少量的线程(比如和处理器核心数相等的线程)处理大量的并发请求。
编译上述的所有源文件后我们得到了一个非常简单的web应用程序这是一个不错的开始。**请访问[wiki](https://github.com/an-tao/drogon/wiki/CHN-01-概述)** 编译上述的所有源文件后我们得到了一个非常简单的web应用程序这是一个不错的开始。**请访问GitHub上的[文档](https://drogonframework.github.io/drogon-docs/#/CHN/CHN-01-%E6%A6%82%E8%BF%B0)**
## 贡献方式 ## 贡献方式
@ -197,3 +197,9 @@ class User : public drogon::HttpController<User>
## QQ交流群1137909452 ## QQ交流群1137909452
欢迎交流探讨。 欢迎交流探讨。
## 微信公众号:
![](https://github.com/an-tao/drogon/wiki/images/qrcode_wechat.jpg)
会不定期推送一些Drogon的使用技巧和更新信息欢迎关注。

View File

@ -1,6 +1,6 @@
![](https://github.com/an-tao/drogon/wiki/images/drogon-white17.jpg) ![](https://github.com/an-tao/drogon/wiki/images/drogon-white17.jpg)
[![Build Status](https://github.com/an-tao/drogon/workflows/Build%20Drogon/badge.svg?branch=master)](https://github.com/drogonframework/drogon/actions) [![Build Status](https://github.com/drogonframework/drogon/actions/workflows/cmake.yml/badge.svg?branch=master)](https://github.com/drogonframework/drogon/actions)
[![Conan Center](https://img.shields.io/conan/v/drogon)](https://conan.io/center/recipes/drogon) [![Conan Center](https://img.shields.io/conan/v/drogon)](https://conan.io/center/recipes/drogon)
[![Join the telegram group at https://t.me/joinchat/_mMNGv0748ZkMDAx](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/joinchat/_mMNGv0748ZkMDAx) [![Join the telegram group at https://t.me/joinchat/_mMNGv0748ZkMDAx](https://img.shields.io/badge/Telegram-2CA5E0?style=flat&logo=telegram&logoColor=white)](https://t.me/joinchat/_mMNGv0748ZkMDAx)
[![Join our Discord](https://dcbadge.vercel.app/api/server/3DvHY6Ewuj?style=flat)](https://discord.gg/3DvHY6Ewuj) [![Join our Discord](https://dcbadge.vercel.app/api/server/3DvHY6Ewuj?style=flat)](https://discord.gg/3DvHY6Ewuj)
@ -8,41 +8,42 @@
[English](./README.md) | [简体中文](./README.zh-CN.md) | 繁體中文 [English](./README.md) | [简体中文](./README.zh-CN.md) | 繁體中文
**Drogon**是一個基於C++17/20的Http應用框架使用Drogon可以方便的使用C++構建各種類型的Web App伺服器程式。 **Drogon** 是一個基於 C++17/20 的 HTTP 應用程式框架,使用 Drogon 可以方便地用 C++ 建立各種類型的 Web App 伺服器端程式。
本版本庫是github上[Drogon](https://github.com/an-tao/drogon)的鏡像庫。 **Drogon**是作者非常喜歡的美劇《冰與火之歌:權力遊戲》中的一條龍的名字(漢譯作卓耿)和龍有關但並不是dragon的誤寫為了不至於引起不必要的誤會這裡說明一下。
Drogon是一個跨平台框架它支援Linux也支援macOS、FreeBSD/OpenBSD、HaikuOS和Windows。它的主要特點如下 這個版本庫是 GitHub 上 [Drogon](https://github.com/an-tao/drogon) 的鏡像庫。**Drogon** 是作者非常喜歡的美劇《冰與火之歌:權力遊戲》中的一條龍的名字(中文譯作卓耿),和龍有關但並不是 dragon 的誤寫,為了避免不必要的誤會在此說明。
* 網路層使用基於epoll(macOS/FreeBSD下是kqueue)的非阻塞IO框架提供高並發、高性能的網路IO。詳細請見[TFB Tests Results](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite) Drogon 是一個跨平台框架,支援 Linux、macOS、FreeBSD/OpenBSD、HaikuOS 和 Windows。主要特點如下
* 全異步程式設計;
* 支援Http1.0/1.1(server端和client端) * 網路層使用基於 epollmacOS/FreeBSD 下是 kqueue的非阻塞 IO 框架,提供高並行、高效能的網路 IO。詳細請見 [TFB Tests Results](https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite)
* 基於模板(template)實現了簡單的反射機制,使主程式框架、控制器(controller)和視圖(view)完全去耦; * 完全非同步的程式撰寫邏輯;
* 支援cookies和內建的session * 支援 HTTP 1.0/1.1(伺服器端和用戶端);
* 支援後端渲染把控制器生成的數據交給視圖生成Html頁面視圖由CSP模板文件描述通過CSP標籤把C++程式碼嵌入到Html頁面由drogon的指令列工具在編譯階段自動生成C++程式碼並編譯; * 基於樣板template實作的簡單反射機制使主程式框架、控制器controller和視圖view完全解耦
* 支援運行期的視圖頁面動態加載(動態編譯和載入so文件) * 支援 cookies 和內建的 session
* 非常方便靈活的路徑(path)到控制器處理函數(handler)的映射方案; * 支援後端算繪,將控制器產生的資料交給視圖產生 HTML 頁面,視圖由 CSP 樣板檔案描述,透過 CSP 標籤將 C++ 程式碼嵌入 HTML 頁面,由 drogon 的命令列工具在編譯階段自動產生 C++ 程式碼並編譯;
* 支援過濾器(filter)鏈,方便在控制器之前執行統一的邏輯(如登錄驗證、Http Method約束驗證等) * 支援執行期的視圖頁面動態載入(動態編譯和載入 so 檔案);
* 支援https(基於OpenSSL); * 非常方便靈活的路徑path到控制器處理函式handler的對應方案
* 支援websocket(server端和client端); * 支援過濾器filter方便在控制器之前執行統一的邏輯如登入驗證、HTTP Method 限制驗證等);
* 支援Json格式的請求和回應, 方便開發Restful API; * 支援 HTTPS基於 OpenSSL
* 支援文件下載和上傳,支援sendfile系統呼叫 * 支援 WebSocket伺服器端和用戶端
* 支援gzip/brotli壓縮傳輸 * 支援 JSON 格式的請求和回應,方便開發 RESTful API
* 支援pipelining * 支援檔案下載和上傳,支援 `sendfile` 系統呼叫;
* 提供一個輕量的指令列工具drogon_ctl幫助簡化各種類的創造和視圖程式碼的生成過程 * 支援 Gzip/Brotli 壓縮傳輸;
* 非同步的讀寫資料庫目前支援PostgreSQL和MySQL(MariaDB)資料庫; * 支援 pipelining
* 支援異步讀寫Redis; * 提供輕量的命令列工具 `drogon_ctl`,幫助簡化各種類別的建立和視圖程式碼的產生過程;
* 基於執行序池實現sqlite3資料庫的異步讀寫提供與上文資料庫相同的接口 * 非同步的讀寫資料庫,目前支援 PostgreSQL 和 MySQLMariaDB資料庫
* 支援ARM架構 * 支援非同步讀寫 Redis
* 方便的輕量級ORM實現一般物件到資料庫的雙向映射 * 基於執行緒池實作 sqlite3 資料庫的非同步讀寫,提供與上述資料庫相同的介面;
* 支援外掛,可通過設定文件在載入時動態載入; * 支援 ARM 架構;
* 支援內建插入點的AOP * 方便的輕量級 ORM 實現,一般物件到資料庫的雙向對應;
* 支援C++ coroutine * 支援外掛,可透過設定檔案在載入時動態載入;
* 支援內建插入點的 AOP
* 支援 C++ coroutine。
## 一個非常簡單的例子 ## 一個非常簡單的例子
不像大多數C++框架那樣drogon的主程式可以非常簡單。 Drogon使用了一些小技巧使主程式和控制器去耦. 控制器的路由設定可以在控制器類別中定義或者設定文件中完成. 不像大多數 C++ 框架drogon 的主程式可以非常簡單。Drogon 使用了一些小技巧使主程式和控制器解耦。控制器的路由設定可以在控制器類別中定義或在設定檔案中完成。
下面是一個典型的主程式的樣子: 下面是一個典型主程式的樣子:
```c++ ```c++
#include <drogon/drogon.h> #include <drogon/drogon.h>
@ -58,7 +59,7 @@ int main()
} }
``` ```
如果使用設定文件,可以進一步簡化成這樣: 如果使用設定檔案,可以進一步簡化成:
```c++ ```c++
#include <drogon/drogon.h> #include <drogon/drogon.h>
@ -69,7 +70,7 @@ int main()
} }
``` ```
當然Drogon也提供了一些函數使使用者可以在main()函數中直接添加控制器邏輯比如使用者可以註冊一個lambda處理器到drogon框架中,如下所示: 當然Drogon 也提供了一些函式,讓使用者可以在 `main()` 函式中直接加入控制器邏輯,例如,使用者可以註冊一個 lambda 處理常式到 drogon 框架中,如下所示:
```c++ ```c++
app().registerHandler("/test?username={name}", app().registerHandler("/test?username={name}",
@ -86,9 +87,7 @@ app().registerHandler("/test?username={name}",
{Get,"LoginFilter"}); {Get,"LoginFilter"});
``` ```
這看起來很方便,但不適用於複雜的場景,試想如果有數十個或數百個處理函式要註冊進框架,`main()` 函式將變得難以閱讀。顯然,讓每個包含處理函式的類別在自己的定義中完成註冊是更好的選擇。所以,除非你的應用邏輯非常簡單,我們不建議使用上述介面,更好的做法是建立一個 HttpSimpleController 類別,如下:
這看起來是很方便但是這並不適用於復雜的場景試想假如有數十個或者數百個處理函數要註冊進框架main()函數將膨脹到不可讀的程度。顯然讓每個包含處理函數的類在自己的定義中完成註冊是更好的選擇。所以除非你的應用邏輯非常簡單我們不推薦使用上述接口更好的實踐是我們可以創造一個HttpSimpleController類別如下
```c++ ```c++
/// The TestCtrl.h file /// The TestCtrl.h file
@ -117,9 +116,9 @@ void TestCtrl::asyncHandleHttpRequest(const HttpRequestPtr& req,
} }
``` ```
**上面程式的大部分程式碼都可以由`drogon_ctl`指令創造**(這個指令是`drogon_ctl create controller TestCtr`。使用者所需做的就是添加自己的業務邏輯。在這個例子中當客戶端訪問URL`http://ip/test`時,控制器簡單的回傳了一個`Hello, world!`頁面。 **上述程式的大部分程式碼都可以由 `drogon_ctl` 指令產生**(使用指令 `drogon_ctl create controller TestCtr`)。使用者只需要加入自己的業務邏輯。在這個範例中,當用戶端存取 URL `http://ip/test` 時,控制器簡單地回傳一個 `Hello, world!` 頁面。
對於JSON格式的回應我們可以像下面這樣創造控制器: 對於 JSON 格式的回應,我們可以這樣建立控制器:
```c++ ```c++
/// The header file /// The header file
@ -148,7 +147,7 @@ void JsonCtrl::asyncHandleHttpRequest(const HttpRequestPtr &req,
} }
``` ```
讓我們更進一步,通過HttpController類別創造一個RESTful API的例子如下所示忽略了實做文件 讓我們更進一步,透過 HttpController 類別建立一個 RESTful API 的範例,如下所示(省略實作檔案
```c++ ```c++
/// The header file /// The header file
@ -182,18 +181,18 @@ class User : public drogon::HttpController<User>
} // namespace api } // namespace api
``` ```
如你所見,通過`HttpController`類別使用者可以同時映射路徑和路徑參數這對RESTful API應用來說非常方便。 如你所見,透過 `HttpController` 類別,使用者可以同時對應路徑和路徑參數,這對 RESTful API 應用來說非常方便。
另外,你可以發現前面所有的處理函數接口都是異步的,處理器的回應是通過回調對象回傳的。這種設計是出於對高性能的考慮,因為在異步模式下,可以使用少量的執行序(比如和處理器核心數相等的執行序)處理大量的並發請求。 另外,你可以發現前面所有的處理函式介面都是非同步的,處理器的回應是透過回呼物件回傳的。這種設計是考慮到效能,因為在非同步模式下,可以使用少量的執行緒(例如和處理器核心數相等的執行緒)處理大量的並行請求。
編譯上述的所有源文件後我們得到了一個非常簡單的web應用程式這是一個不錯的開始。 **請瀏覽[wiki](https://github.com/an-tao/drogon/wiki/CHN-01-概述)** 編譯上述所有原始檔案後,我們得到了一個非常簡單的網頁應用程式,這是一個不錯的開始。**請瀏覽 GitHub 上的[文件](https://drogonframework.github.io/drogon-docs/#/CHN/CHN-01-%E6%A6%82%E8%BF%B0)**
## 貢獻方式 ## 貢獻方式
歡迎您的貢獻。請閱讀[貢獻指南](CONTRIBUTING.md)以獲取更多的信息 歡迎您的貢獻。請閱讀[貢獻指南](CONTRIBUTING.md)以取得更多資訊
<a href="https://github.com/drogonframework/drogon/graphs/contributors"><img src="https://contributors-svg.opencollective.com/drogon/contributors.svg?width=890&button=false" alt="Code contributors" /></a> <a href="https://github.com/drogonframework/drogon/graphs/contributors"><img src="https://contributors-svg.opencollective.com/drogon/contributors.svg?width=890&button=false" alt="Code contributors" /></a>
## QQ交流群1137909452 ## QQ 交流群1137909452
歡迎交流討。 歡迎交流討

View File

@ -17,7 +17,7 @@
# ParseAndAddDrogonTests(${PROJECT_NAME}) # # ParseAndAddDrogonTests(${PROJECT_NAME}) #
#==================================================================================================# #==================================================================================================#
cmake_minimum_required(VERSION 3.5) cmake_minimum_required(VERSION 3.5...3.31)
# This removes the contents between # This removes the contents between
# - block comments (i.e. /* ... */) # - block comments (i.e. /* ... */)

View File

@ -212,7 +212,7 @@ if(CXX_FILESYSTEM_HAVE_FS)
]] code @ONLY) ]] code @ONLY)
# HACK: Needed to compile correctly on Yocto Linux # HACK: Needed to compile correctly on Yocto Linux
if(CMAKE_CXX_COMPILER_ID STREQUAL "GCC" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang" if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang"
OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
set(CMAKE_REQUIRED_FLAGS ${prev_req_flags} -std=c++17) set(CMAKE_REQUIRED_FLAGS ${prev_req_flags} -std=c++17)
endif () endif ()

View File

@ -51,7 +51,9 @@ if(Jsoncpp_FOUND)
COMMAND awk "{ printf \$3 }" COMMAND awk "{ printf \$3 }"
COMMAND sed -e "s/\"//g" COMMAND sed -e "s/\"//g"
OUTPUT_VARIABLE jsoncpp_ver) OUTPUT_VARIABLE jsoncpp_ver)
if(NOT Jsoncpp_FIND_QUIETLY)
message(STATUS "jsoncpp version:" ${jsoncpp_ver}) message(STATUS "jsoncpp version:" ${jsoncpp_ver})
endif()
if(jsoncpp_ver LESS 1.7) if(jsoncpp_ver LESS 1.7)
message( message(
FATAL_ERROR FATAL_ERROR

View File

@ -108,7 +108,7 @@
"session_timeout": 0, "session_timeout": 0,
//string value of SameSite attribute of the Set-Cookie HTTP response header //string value of SameSite attribute of the Set-Cookie HTTP response header
//valid value is either 'Null' (default), 'Lax', 'Strict' or 'None' //valid value is either 'Null' (default), 'Lax', 'Strict' or 'None'
"session_same_site" : "Null", "session_same_site": "Null",
//session_cookie_key: The cookie key of the session, "JSESSIONID" by default //session_cookie_key: The cookie key of the session, "JSESSIONID" by default
"session_cookie_key": "JSESSIONID", "session_cookie_key": "JSESSIONID",
//session_max_age: The max age of the session cookie, -1 by default //session_max_age: The max age of the session cookie, -1 by default
@ -310,7 +310,10 @@
// Currently only gzip and br are supported. Note: max_memory_body_size and max_body_size applies twice for compressed requests. // Currently only gzip and br are supported. Note: max_memory_body_size and max_body_size applies twice for compressed requests.
// Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request // Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request
// will be rejected. // will be rejected.
"enabled_compressed_request": false "enabled_compressed_request": false,
// enable_request_stream: Defaults to false. If true the server will enable stream mode for http requests.
// See the wiki for more details.
"enable_request_stream": false,
}, },
//plugins: Define all plugins running in the application //plugins: Define all plugins running in the application
"plugins": [ "plugins": [

View File

@ -283,6 +283,9 @@ app:
# Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request # Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request
# will be rejected. # will be rejected.
enabled_compressed_request: false enabled_compressed_request: false
# enable_request_stream: Defaults to false. If true the server will enable stream mode for http requests.
# See the wiki for more details.
enable_request_stream: false
# plugins: Define all plugins running in the application # plugins: Define all plugins running in the application
plugins: plugins:
# name: The class name of the plugin # name: The class name of the plugin

View File

@ -42,7 +42,8 @@ std::string create::detail()
"create a plugin named class_name\n\n" "create a plugin named class_name\n\n"
"drogon_ctl create project <project_name> //" "drogon_ctl create project <project_name> //"
"create a project named project_name\n\n" "create a project named project_name\n\n"
"drogon_ctl create model <model_path> [--table=<table_name>] [-f]//" "drogon_ctl create model <model_path> [-o <output path>] "
"[--table=<table_name>] [-f]//"
"create model classes in model_path\n"; "create model classes in model_path\n";
} }

View File

@ -66,6 +66,17 @@ static std::string escapeConnString(const std::string &str)
return escaped; return escaped;
} }
std::string drogon_ctl::escapeIdentifier(const std::string &identifier,
const std::string &rdbms)
{
if (rdbms != "postgresql")
{
return identifier;
}
return "\\\"" + identifier + "\\\"";
}
static std::map<std::string, std::vector<ConvertMethod>> getConvertMethods( static std::map<std::string, std::vector<ConvertMethod>> getConvertMethods(
const Json::Value &convertColumns) const Json::Value &convertColumns)
{ {
@ -166,7 +177,7 @@ void create_model::createModelClassFromPG(
auto className = nameTransform(tableName, true); auto className = nameTransform(tableName, true);
HttpViewData data; HttpViewData data;
data["className"] = className; data["className"] = className;
data["tableName"] = toLower(tableName); data["tableName"] = tableName;
data["hasPrimaryKey"] = (int)0; data["hasPrimaryKey"] = (int)0;
data["primaryKeyName"] = ""; data["primaryKeyName"] = "";
data["dbName"] = dbname_; data["dbName"] = dbname_;
@ -460,7 +471,7 @@ void create_model::createModelClassFromMysql(
data["convertMethods"] = convertMethods; data["convertMethods"] = convertMethods;
std::vector<ColumnInfo> cols; std::vector<ColumnInfo> cols;
int i = 0; int i = 0;
*client << "desc " + tableName << Mode::Blocking >> *client << "desc `" + tableName + "`" << Mode::Blocking >>
[&i, &cols](bool isNull, [&i, &cols](bool isNull,
const std::string &field, const std::string &field,
const std::string &type, const std::string &type,
@ -815,6 +826,7 @@ void create_model::createModel(const std::string &path,
auto restfulApiConfig = config["restful_api_controllers"]; auto restfulApiConfig = config["restful_api_controllers"];
auto relationships = getRelationships(config["relationships"]); auto relationships = getRelationships(config["relationships"]);
auto convertMethods = getConvertMethods(config["convert"]); auto convertMethods = getConvertMethods(config["convert"]);
drogon::utils::createPath(path);
if (dbType == "postgresql") if (dbType == "postgresql")
{ {
#if USE_POSTGRESQL #if USE_POSTGRESQL
@ -1162,7 +1174,9 @@ void create_model::createModel(const std::string &path,
try try
{ {
infile >> configJsonRoot; infile >> configJsonRoot;
createModel(path, configJsonRoot, singleModelName); createModel(outputPath_.empty() ? path : outputPath_,
configJsonRoot,
singleModelName);
} }
catch (const std::exception &exception) catch (const std::exception &exception)
{ {
@ -1200,6 +1214,22 @@ void create_model::handleCommand(std::vector<std::string> &parameters)
break; break;
} }
} }
for (auto iter = parameters.begin(); iter != parameters.end();)
{
auto &file = *iter;
if (file == "-o" || file == "--output")
{
iter = parameters.erase(iter);
if (iter != parameters.end())
{
outputPath_ = *iter;
iter = parameters.erase(iter);
}
continue;
}
++iter;
}
for (auto const &path : parameters) for (auto const &path : parameters)
{ {
createModel(path, singleModelName); createModel(path, singleModelName);

View File

@ -78,6 +78,9 @@ inline std::string nameTransform(const std::string &origName, bool isType)
return ret; return ret;
} }
std::string escapeIdentifier(const std::string &identifier,
const std::string &rdbms);
class PivotTable class PivotTable
{ {
public: public:
@ -426,5 +429,6 @@ class create_model : public DrObject<create_model>, public CommandHandler
const Json::Value &restfulApiConfig); const Json::Value &restfulApiConfig);
std::string dbname_; std::string dbname_;
bool forceOverwrite_{false}; bool forceOverwrite_{false};
std::string outputPath_;
}; };
} // namespace drogon_ctl } // namespace drogon_ctl

View File

@ -411,6 +411,7 @@ void create_view::newViewSourceFile(std::ofstream &file,
"automatically,don't modify it!\n"; "automatically,don't modify it!\n";
file << "#include \"" << namespacePrefix << className << ".h\"\n"; file << "#include \"" << namespacePrefix << className << ".h\"\n";
file << "#include <drogon/utils/OStringStream.h>\n"; file << "#include <drogon/utils/OStringStream.h>\n";
file << "#include <drogon/utils/Utilities.h>\n";
file << "#include <string>\n"; file << "#include <string>\n";
file << "#include <map>\n"; file << "#include <map>\n";
file << "#include <vector>\n"; file << "#include <vector>\n";

View File

@ -19,6 +19,10 @@
#include <memory> #include <memory>
#include <iomanip> #include <iomanip>
#include <cstdlib> #include <cstdlib>
#include <json/json.h>
#include <fstream>
#include <string>
#include <unordered_map>
#ifndef _WIN32 #ifndef _WIN32
#include <unistd.h> #include <unistd.h>
#endif #endif
@ -33,9 +37,10 @@ std::string press::detail()
" -t num number of threads(default : 1)\n" " -t num number of threads(default : 1)\n"
" -c num concurrent connections(default : 1)\n" " -c num concurrent connections(default : 1)\n"
" -k disable SSL certificate validation(default: enable)\n" " -k disable SSL certificate validation(default: enable)\n"
" -f customize http request json file(default: disenable)\n"
" -q no progress indication(default: show)\n\n" " -q no progress indication(default: show)\n\n"
"example: drogon_ctl press -n 10000 -c 100 -t 4 -q " "example: drogon_ctl press -n 10000 -c 100 -t 4 -q "
"http://localhost:8080/index.html\n"; "http://localhost:8080/index.html -f ./http_request.json\n";
} }
void outputErrorAndExit(const std::string_view &err) void outputErrorAndExit(const std::string_view &err)
@ -151,6 +156,24 @@ void press::handleCommand(std::vector<std::string> &parameters)
continue; continue;
} }
} }
else if (param.find("-f") == 0)
{
if (param == "-f")
{
++iter;
if (iter == parameters.end())
{
outputErrorAndExit("No http request json file!");
}
httpRequestJsonFile_ = *iter;
continue;
}
else
{
httpRequestJsonFile_ = param.substr(2);
continue;
}
}
else if (param == "-k") else if (param == "-k")
{ {
certValidation_ = false; certValidation_ = false;
@ -190,6 +213,118 @@ void press::handleCommand(std::vector<std::string> &parameters)
path_ = url_.substr(posOfPath); path_ = url_.substr(posOfPath);
} }
} }
/*
http_request.json
{
"method": "POST",
"header": {
"token": "e2e9d0fe-dd14-4eaf-8ac1-0997730a805d"
},
"body": {
"passwd": "123456",
"account": "10001"
}
}
*/
if (!httpRequestJsonFile_.empty())
{
Json::Value httpRequestJson;
std::ifstream httpRequestFile(httpRequestJsonFile_,
std::ifstream::binary);
if (!httpRequestFile.is_open())
{
outputErrorAndExit(std::string{"No "} + httpRequestJsonFile_);
}
httpRequestFile >> httpRequestJson;
if (!httpRequestJson.isMember("method"))
{
outputErrorAndExit("No contain method");
}
auto methodStr = httpRequestJson["method"].asString();
std::transform(methodStr.begin(),
methodStr.end(),
methodStr.begin(),
::toupper);
auto toHttpMethod = [&]() -> drogon::HttpMethod {
if (methodStr == "GET")
{
return drogon::HttpMethod::Get;
}
else if (methodStr == "POST")
{
return drogon::HttpMethod::Post;
}
else if (methodStr == "HEAD")
{
return drogon::HttpMethod::Head;
}
else if (methodStr == "PUT")
{
return drogon::HttpMethod::Put;
}
else if (methodStr == "DELETE")
{
return drogon::HttpMethod::Delete;
}
else if (methodStr == "OPTIONS")
{
return drogon::HttpMethod::Options;
}
else if (methodStr == "PATCH")
{
return drogon::HttpMethod::Patch;
}
else
{
outputErrorAndExit("invalid method");
}
return drogon::HttpMethod::Get;
};
std::unordered_map<std::string, std::string> header;
if (httpRequestJson.isMember("header"))
{
auto &jsonValue = httpRequestJson["header"];
for (const auto &key : jsonValue.getMemberNames())
{
if (jsonValue[key].isString())
{
header[key] = jsonValue[key].asString();
}
else
{
header[key] = jsonValue[key].toStyledString();
}
}
}
std::string body;
if (httpRequestJson.isMember("body"))
{
Json::FastWriter fastWriter;
body = fastWriter.write(httpRequestJson["body"]);
}
createHttpRequestFunc_ = [this,
method = toHttpMethod(),
body = std::move(body),
header =
std::move(header)]() -> HttpRequestPtr {
auto request = HttpRequest::newHttpRequest();
request->setPath(path_);
request->setMethod(method);
for (const auto &[field, val] : header)
request->addHeader(field, val);
if (!body.empty())
request->setBody(body);
return request;
};
}
// std::cout << "host=" << host_ << std::endl; // std::cout << "host=" << host_ << std::endl;
// std::cout << "path=" << path_ << std::endl; // std::cout << "path=" << path_ << std::endl;
doTesting(); doTesting();
@ -232,9 +367,19 @@ void press::sendRequest(const HttpClientPtr &client)
{ {
return; return;
} }
auto request = HttpRequest::newHttpRequest();
HttpRequestPtr request;
if (createHttpRequestFunc_)
{
request = createHttpRequestFunc_();
}
else
{
request = HttpRequest::newHttpRequest();
request->setPath(path_); request->setPath(path_);
request->setMethod(Get); request->setMethod(Get);
}
// std::cout << "send!" << std::endl; // std::cout << "send!" << std::endl;
client->sendRequest( client->sendRequest(
request, request,

View File

@ -20,6 +20,7 @@
#include <drogon/HttpClient.h> #include <drogon/HttpClient.h>
#include <trantor/utils/Date.h> #include <trantor/utils/Date.h>
#include <trantor/net/EventLoopThreadPool.h> #include <trantor/net/EventLoopThreadPool.h>
#include <functional>
#include <string> #include <string>
#include <atomic> #include <atomic>
#include <memory> #include <memory>
@ -62,6 +63,8 @@ class press : public DrObject<press>, public CommandHandler
size_t numOfThreads_{1}; size_t numOfThreads_{1};
size_t numOfRequests_{1}; size_t numOfRequests_{1};
size_t numOfConnections_{1}; size_t numOfConnections_{1};
std::string httpRequestJsonFile_;
std::function<HttpRequestPtr()> createHttpRequestFunc_;
bool certValidation_{true}; bool certValidation_{true};
bool processIndication_{true}; bool processIndication_{true};
std::string url_; std::string url_;

View File

@ -310,7 +310,10 @@
// Currently only gzip and br are supported. Note: max_memory_body_size and max_body_size applies twice for compressed requests. // Currently only gzip and br are supported. Note: max_memory_body_size and max_body_size applies twice for compressed requests.
// Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request // Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request
// will be rejected. // will be rejected.
"enabled_compressed_request": false "enabled_compressed_request": false,
// enable_request_stream: Defaults to false. If true the server will enable stream mode for http requests.
// See the wiki for more details.
"enable_request_stream": false,
}, },
//plugins: Define all plugins running in the application //plugins: Define all plugins running in the application
"plugins": [ "plugins": [

View File

@ -283,6 +283,9 @@ app:
# Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request # Once when receiving and once when decompressing. i.e. if the decompressed body is larger than max_body_size, the request
# will be rejected. # will be rejected.
enabled_compressed_request: false enabled_compressed_request: false
# enable_request_stream: Defaults to false. If true the server will enable stream mode for http requests.
# See the wiki for more details.
enable_request_stream: false
# plugins: Define all plugins running in the application # plugins: Define all plugins running in the application
plugins: plugins:
# name: The class name of the plugin # name: The class name of the plugin

View File

@ -76,7 +76,7 @@ else
<%c++for(auto col:cols){ <%c++for(auto col:cols){
%> %>
const std::string [[className]]::Cols::_{%col.colName_%} = "{%col.colName_%}"; const std::string [[className]]::Cols::_{%col.colName_%} = "{%escapeIdentifier(col.colName_, rdbms)%}";
<%c++ <%c++
}%> }%>
<%c++if(@@.get<int>("hasPrimaryKey")<=1){%> <%c++if(@@.get<int>("hasPrimaryKey")<=1){%>
@ -102,7 +102,7 @@ if(!schema.empty())
{ {
$$<<schema<<"."; $$<<schema<<".";
} }
%>[[tableName]]"; %>{%escapeIdentifier(@@.get<std::string>("tableName"), rdbms)%}";
const std::vector<typename [[className]]::MetaData> [[className]]::metaData_={ const std::vector<typename [[className]]::MetaData> [[className]]::metaData_={
<%c++for(size_t i=0;i<cols.size();i++){ <%c++for(size_t i=0;i<cols.size();i++){

View File

@ -31,7 +31,10 @@ add_executable(redis_simple redis/main.cc
add_executable(redis_chat redis_chat/main.cc add_executable(redis_chat redis_chat/main.cc
redis_chat/controllers/Chat.cc) redis_chat/controllers/Chat.cc)
add_executable(async_stream async_stream/main.cc) add_executable(async_stream async_stream/main.cc
async_stream/RequestStreamExampleCtrl.cc)
add_executable(cors cors/main.cc)
set(example_targets set(example_targets
benchmark benchmark
@ -44,7 +47,8 @@ set(example_targets
jsonstore jsonstore
redis_simple redis_simple
redis_chat redis_chat
async_stream) async_stream
cors)
# Add warnings for our example targets--some warnings (such as -Wunused-parameter) only appear # Add warnings for our example targets--some warnings (such as -Wunused-parameter) only appear
# when the templated functions are instantiated at their point of use. # when the templated functions are instantiated at their point of use.

View File

@ -2,19 +2,21 @@
The following examples can help you understand how to use Drogon: The following examples can help you understand how to use Drogon:
1. [helloworld](https://github.com/an-tao/drogon/tree/master/examples/helloworld) - The multiple ways of "Hello, World!" 1. [helloworld](https://github.com/drogonframework/drogon/tree/master/examples/helloworld) - The multiple ways of "Hello, World!"
2. [client_example](https://github.com/an-tao/drogon/tree/master/examples/client_example/main.cc) - A client example. 2. [client_example](https://github.com/drogonframework/drogon/tree/master/examples/client_example/main.cc) - A client example
3. [websocket_client](https://github.com/an-tao/drogon/tree/master/examples/websocket_client/WebSocketClient.cc) - An example on how to use the WebSocket client 3. [websocket_client](https://github.com/drogonframework/drogon/tree/master/examples/websocket_client/WebSocketClient.cc) - An example on how to use the WebSocket client
4. [login_session](https://github.com/an-tao/drogon/tree/master/examples/login_session) - How to use the built-in session system to handle login and out 4. [login_session](https://github.com/drogonframework/drogon/tree/master/examples/login_session) - How to use the built-in session system to handle login and out
5. [file_upload](https://github.com/an-tao/drogon/tree/master/examples/file_upload) - How to handle file uploads in Drogon 5. [file_upload](https://github.com/drogonframework/drogon/tree/master/examples/file_upload) - How to handle file uploads in Drogon
6. [simple_reverse_proxy](https://github.com/an-tao/drogon/tree/master/examples/simple_reverse_proxy) - A Example showing how to use drogon as a http reverse 6. [simple_reverse_proxy](https://github.com/drogonframework/drogon/tree/master/examples/simple_reverse_proxy) - An example showing how to use Drogon as a HTTP reverse
proxy with a simple round robin. proxy with a simple round robin
7. [benchmark](https://github.com/an-tao/drogon/tree/master/examples/benchmark) - Basic benchmark example. see [wiki benchmarks](https://github.com/an-tao/drogon/wiki/13-Benchmarks) 7. [benchmark](https://github.com/drogonframework/drogon/tree/master/examples/benchmark) - Basic benchmark(https://github.com/drogonframework/drogon/wiki/13-Benchmarks) example
8. [jsonstore](https://github.com/an-tao/drogon/tree/master/examples/jsonstore) - Implementation of a [jsonstore](https://github.com/bluzi/jsonstore)-like storage service that is concurrent and stores in memory. Serving as a showcase on how to build a minimally useful RESTful APIs in Drogon. 8. [jsonstore](https://github.com/drogonframework/drogon/tree/master/examples/jsonstore) - Implementation of a [jsonstore](https://github.com/bluzi/jsonstore)-like storage service that is concurrent and stores in memory. Serving as a showcase on how to build a minimally useful RESTful APIs in Drogon
9. [redis](https://github.com/an-tao/drogon/tree/master/examples/redis) - A simple example of Redis 9. [redis](https://github.com/drogonframework/drogon/tree/master/examples/redis) - A simple example of Redis
10. [websocket_server](https://github.com/drogonframework/drogon/tree/master/examples/websocket_server) - Example WebSocker chat room server 10. [websocket_server](https://github.com/drogonframework/drogon/tree/master/examples/websocket_server) - A example websocket chat room server
11. [redis_cache](https://github.com/an-tao/drogon/tree/master/examples/redis_cache) - An example for using coroutines of redis clients 11. [redis_cache](https://github.com/drogonframework/drogon/tree/master/examples/redis_cache) - An example for using coroutines of Redis clients
12. [redis_chat](https://github.com/an-tao/drogon/tree/master/examples/redis_chat) - A chatroom server built with websocket and redis pub/sub service. 12. [redis_chat](https://github.com/drogonframework/drogon/tree/master/examples/redis_chat) - A chatroom server built with websocket and Redis pub/sub service
13. [prometheus_example](https://github.com/drogonframework/drogon/tree/master/examples/prometheus_example) - An example of how to use the Prometheus exporter in Drogon
14. [cors](https://github.com/drogonframework/drogon/tree/master/examples/cors) - An example demonstrating how to implement CORS (Cross-Origin Resource Sharing) support in Drogon
### [TechEmpower Framework Benchmarks](https://github.com/TechEmpower/FrameworkBenchmarks) test suite ### [TechEmpower Framework Benchmarks](https://github.com/TechEmpower/FrameworkBenchmarks) test suite
@ -22,4 +24,4 @@ I created a benchmark suite for the `tfb`, see [here](https://github.com/TechEmp
### Another test suite ### Another test suite
I also created a test suite for another web frameworks benchmark repository, see [here](https://github.com/the-benchmarker/web-frameworks/tree/master/cpp/drogon), in this project, drogon is used as a sub-module (locally include in the project). I also created a test suite for another web frameworks benchmark repository, see [here](https://github.com/the-benchmarker/web-frameworks/tree/master/cpp/drogon). In this project, Drogon is used as a sub-module (locally include in the project).

View File

@ -0,0 +1,167 @@
#include <drogon/drogon.h>
#include <drogon/HttpController.h>
#include <drogon/HttpRequest.h>
#include <fstream>
using namespace drogon;
class StreamEchoReader : public RequestStreamReader
{
public:
StreamEchoReader(ResponseStreamPtr respStream)
: respStream_(std::move(respStream))
{
}
void onStreamData(const char *data, size_t length) override
{
LOG_INFO << "onStreamData[" << length << "]";
respStream_->send({data, length});
}
void onStreamFinish(std::exception_ptr ptr) override
{
if (ptr)
{
try
{
std::rethrow_exception(ptr);
}
catch (const std::exception &e)
{
LOG_ERROR << "onStreamError: " << e.what();
}
}
else
{
LOG_INFO << "onStreamFinish";
}
respStream_->close();
}
private:
ResponseStreamPtr respStream_;
};
class RequestStreamExampleCtrl : public HttpController<RequestStreamExampleCtrl>
{
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(RequestStreamExampleCtrl::stream_echo, "/stream_echo", Post);
ADD_METHOD_TO(RequestStreamExampleCtrl::stream_upload,
"/stream_upload",
Post);
METHOD_LIST_END
void stream_echo(
const HttpRequestPtr &,
RequestStreamPtr &&stream,
std::function<void(const HttpResponsePtr &)> &&callback) const
{
auto resp = drogon::HttpResponse::newAsyncStreamResponse(
[stream](ResponseStreamPtr respStream) {
stream->setStreamReader(
std::make_shared<StreamEchoReader>(std::move(respStream)));
});
callback(resp);
}
void stream_upload(
const HttpRequestPtr &req,
RequestStreamPtr &&stream,
std::function<void(const HttpResponsePtr &)> &&callback) const
{
struct Entry
{
MultipartHeader header;
std::string tmpName;
std::ofstream file;
};
auto files = std::make_shared<std::vector<Entry>>();
auto reader = RequestStreamReader::newMultipartReader(
req,
[files](MultipartHeader &&header) {
LOG_INFO << "Multipart name: " << header.name
<< ", filename:" << header.filename
<< ", contentType:" << header.contentType;
files->push_back({std::move(header)});
auto tmpName = drogon::utils::genRandomString(40);
if (!files->back().header.filename.empty())
{
files->back().tmpName = tmpName;
files->back().file.open("uploads/" + tmpName,
std::ios::trunc);
}
},
[files](const char *data, size_t length) {
if (files->back().tmpName.empty())
{
return;
}
auto &currentFile = files->back().file;
if (length == 0)
{
LOG_INFO << "file finish";
if (currentFile.is_open())
{
currentFile.flush();
currentFile.close();
}
return;
}
LOG_INFO << "data[" << length << "]: ";
if (currentFile.is_open())
{
LOG_INFO << "write file";
currentFile.write(data, length);
}
else
{
LOG_ERROR << "file not open";
}
},
[files, callback = std::move(callback)](std::exception_ptr ex) {
if (ex)
{
try
{
std::rethrow_exception(std::move(ex));
}
catch (const StreamError &e)
{
LOG_ERROR << "stream error: " << e.what();
}
catch (const std::exception &e)
{
LOG_ERROR << "multipart error: " << e.what();
}
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("error\n");
callback(resp);
}
else
{
LOG_INFO << "stream finish, received " << files->size()
<< " files";
Json::Value respJson;
for (const auto &item : *files)
{
if (item.tmpName.empty())
continue;
Json::Value entry;
entry["name"] = item.header.name;
entry["filename"] = item.header.filename;
entry["tmpName"] = item.tmpName;
respJson.append(entry);
}
auto resp = HttpResponse::newHttpJsonResponse(respJson);
callback(resp);
}
});
stream->setStreamReader(std::move(reader));
}
};

View File

@ -1,15 +1,33 @@
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <chrono> #include <chrono>
#include <memory> #include <functional>
#include <mutex>
#include <unordered_map>
#include <trantor/utils/Logger.h>
#include <trantor/net/callbacks.h>
#include <trantor/net/TcpConnection.h>
using namespace drogon; using namespace drogon;
using namespace std::chrono_literals; using namespace std::chrono_literals;
std::mutex mutex;
std::unordered_map<trantor::TcpConnectionPtr, std::function<void()>>
connMapping;
int main() int main()
{ {
app().registerHandler( app().registerHandler(
"/stream", "/stream",
[](const HttpRequestPtr &, [](const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) { std::function<void(const HttpResponsePtr &)> &&callback) {
const auto &weakConnPtr = req->getConnectionPtr();
if (auto connPtr = weakConnPtr.lock())
{
std::lock_guard lk(mutex);
connMapping.emplace(std::move(connPtr), [] {
LOG_INFO << "call stop or other options!!!!";
});
}
auto resp = drogon::HttpResponse::newAsyncStreamResponse( auto resp = drogon::HttpResponse::newAsyncStreamResponse(
[](drogon::ResponseStreamPtr stream) { [](drogon::ResponseStreamPtr stream) {
std::thread([stream = std::thread([stream =
@ -28,6 +46,68 @@ int main()
callback(resp); callback(resp);
}); });
// Example: register a stream-mode function handler
app().registerHandler(
"/stream_req",
[](const HttpRequestPtr &req,
RequestStreamPtr &&stream,
std::function<void(const HttpResponsePtr &)> &&callback) {
if (!stream)
{
LOG_INFO << "stream mode is not enabled";
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("no stream");
callback(resp);
return;
}
auto reader = RequestStreamReader::newReader(
[](const char *data, size_t length) {
LOG_INFO << "piece[" << length
<< "]: " << std::string_view{data, length};
},
[callback = std::move(callback)](std::exception_ptr ex) {
auto resp = HttpResponse::newHttpResponse();
if (ex)
{
try
{
std::rethrow_exception(std::move(ex));
}
catch (const std::exception &e)
{
LOG_ERROR << "stream error: " << e.what();
}
resp->setStatusCode(k400BadRequest);
resp->setBody("error\n");
callback(resp);
}
else
{
LOG_INFO << "stream finish";
resp->setBody("success\n");
callback(resp);
}
});
stream->setStreamReader(std::move(reader));
},
{Post});
LOG_INFO << "Server running on 127.0.0.1:8848"; LOG_INFO << "Server running on 127.0.0.1:8848";
app().enableRequestStream(); // This is for request stream.
app().setConnectionCallback([](const trantor::TcpConnectionPtr &conn) {
if (conn->disconnected())
{
std::lock_guard lk(mutex);
if (auto it = connMapping.find(conn); it != connMapping.end())
{
LOG_INFO << "disconnect";
connMapping[conn]();
connMapping.erase(conn);
}
}
});
app().addListener("127.0.0.1", 8848).run(); app().addListener("127.0.0.1", 8848).run();
} }

View File

@ -72,6 +72,8 @@ int main()
std::cout << "count=" << nth_resp << std::endl; std::cout << "count=" << nth_resp << std::endl;
}); });
} }
std::cout << "requestsBufferSize:" << client->requestsBufferSize()
<< std::endl;
} }
app().run(); app().run();

153
examples/cors/main.cc Normal file
View File

@ -0,0 +1,153 @@
#include <drogon/HttpAppFramework.h>
#include <drogon/HttpResponse.h>
#include <drogon/drogon.h>
#include "trantor/utils/Logger.h"
using namespace drogon;
/// Configure Cross-Origin Resource Sharing (CORS) support.
///
/// This function registers both synchronous pre-processing advice for handling
/// OPTIONS preflight requests and post-handling advice to inject CORS headers
/// into all responses dynamically based on the incoming request headers.
void setupCors()
{
// Register sync advice to handle CORS preflight (OPTIONS) requests
drogon::app().registerSyncAdvice([](const drogon::HttpRequestPtr &req)
-> drogon::HttpResponsePtr {
if (req->method() == drogon::HttpMethod::Options)
{
auto resp = drogon::HttpResponse::newHttpResponse();
// Set Access-Control-Allow-Origin header based on the Origin
// request header
const auto &origin = req->getHeader("Origin");
if (!origin.empty())
{
resp->addHeader("Access-Control-Allow-Origin", origin);
}
// Set Access-Control-Allow-Methods based on the requested method
const auto &requestMethod =
req->getHeader("Access-Control-Request-Method");
if (!requestMethod.empty())
{
resp->addHeader("Access-Control-Allow-Methods", requestMethod);
}
// Allow credentials to be included in cross-origin requests
resp->addHeader("Access-Control-Allow-Credentials", "true");
// Set allowed headers from the Access-Control-Request-Headers
// header
const auto &requestHeaders =
req->getHeader("Access-Control-Request-Headers");
if (!requestHeaders.empty())
{
resp->addHeader("Access-Control-Allow-Headers", requestHeaders);
}
return std::move(resp);
}
return {};
});
// Register post-handling advice to add CORS headers to all responses
drogon::app().registerPostHandlingAdvice(
[](const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp) -> void {
// Set Access-Control-Allow-Origin based on the Origin request
// header
const auto &origin = req->getHeader("Origin");
if (!origin.empty())
{
resp->addHeader("Access-Control-Allow-Origin", origin);
}
// Reflect the requested Access-Control-Request-Method back in the
// response
const auto &requestMethod =
req->getHeader("Access-Control-Request-Method");
if (!requestMethod.empty())
{
resp->addHeader("Access-Control-Allow-Methods", requestMethod);
}
// Allow credentials to be included in cross-origin requests
resp->addHeader("Access-Control-Allow-Credentials", "true");
// Reflect the requested Access-Control-Request-Headers back
const auto &requestHeaders =
req->getHeader("Access-Control-Request-Headers");
if (!requestHeaders.empty())
{
resp->addHeader("Access-Control-Allow-Headers", requestHeaders);
}
});
}
/**
* Main function to start the Drogon application with CORS-enabled routes.
* This example includes:
* - A simple GET endpoint `/hello` that returns a greeting message.
* - A POST endpoint `/echo` that echoes back the request body.
* You can test with curl to test the CORS support:
*
```
curl -i -X OPTIONS http://localhost:8000/echo \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"
```
or
```
curl -i -X POST http://localhost:8000/echo \
-H "Origin: http://localhost:3000" \
-H "Content-Type: application/json" \
-d '{"key":"value"}'
```
*/
int main()
{
// Listen on port 8000 for all interfaces
app().addListener("0.0.0.0", 8000);
// Setup CORS support
setupCors();
// Register /hello route for GET and OPTIONS methods
app().registerHandler(
"/hello",
[](const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
auto resp = HttpResponse::newHttpResponse();
resp->setBody("Hello from Drogon!");
// Log client IP address
LOG_INFO << "Request to /hello from " << req->getPeerAddr().toIp();
callback(resp);
},
{Get, Options});
// Register /echo route for POST and OPTIONS methods
app().registerHandler(
"/echo",
[](const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) {
auto resp = HttpResponse::newHttpResponse();
resp->setBody(std::string("Echo: ").append(req->getBody()));
// Log client IP and request body
LOG_INFO << "Request to /echo from " << req->getPeerAddr().toIp();
LOG_INFO << "Echo content: " << req->getBody();
callback(resp);
},
{Post, Options});
// Start the application main loop
app().run();
return 0;
}

View File

@ -1,3 +1,11 @@
#include <trantor/utils/Logger.h>
#ifdef _WIN32
#include <ws2tcpip.h>
#else
#include <netinet/tcp.h>
#include <sys/socket.h>
#endif
#include <drogon/drogon.h> #include <drogon/drogon.h>
using namespace drogon; using namespace drogon;
@ -8,8 +16,10 @@ int main()
// sent to Drogon // sent to Drogon
app().registerHandler( app().registerHandler(
"/", "/",
[](const HttpRequestPtr &, [](const HttpRequestPtr &request,
std::function<void(const HttpResponsePtr &)> &&callback) { std::function<void(const HttpResponsePtr &)> &&callback) {
LOG_INFO << "connected:"
<< (request->connected() ? "true" : "false");
auto resp = HttpResponse::newHttpResponse(); auto resp = HttpResponse::newHttpResponse();
resp->setBody("Hello, World!"); resp->setBody("Hello, World!");
callback(resp); callback(resp);
@ -61,6 +71,23 @@ int main()
}, },
{Get}); {Get});
app()
.setBeforeListenSockOptCallback([](int fd) {
LOG_INFO << "setBeforeListenSockOptCallback:" << fd;
#ifdef _WIN32
#elif __linux__
int enable = 1;
if (setsockopt(
fd, IPPROTO_TCP, TCP_FASTOPEN, &enable, sizeof(enable)) ==
-1)
{
LOG_INFO << "setsockopt TCP_FASTOPEN failed";
}
#else
#endif
})
.setAfterAcceptSockOptCallback([](int) {});
// Ask Drogon to listen on 127.0.0.1 port 8848. Drogon supports listening // Ask Drogon to listen on 127.0.0.1 port 8848. Drogon supports listening
// on multiple IP addresses by adding multiple listeners. For example, if // on multiple IP addresses by adding multiple listeners. For example, if
// you want the server also listen on 127.0.0.1 port 5555. Just add another // you want the server also listen on 127.0.0.1 port 5555. Just add another

561
examples/prometheus_example/.gitignore vendored Normal file
View File

@ -0,0 +1,561 @@
# Created by https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++
# Edit at https://www.toptal.com/developers/gitignore?templates=intellij+all,visualstudio,visualstudiocode,cmake,c,c++
### C ###
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
### C++ ###
# Prerequisites
# Compiled Object files
*.slo
# Precompiled Headers
# Linker files
# Debugger Files
# Compiled Dynamic libraries
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
# Executables
### CMake ###
CMakeLists.txt.user
CMakeCache.txt
CMakeFiles
CMakeScripts
Testing
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps
CMakeUserPresets.json
### CMake Patch ###
# External projects
*-prefix/
### Intellij+all ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Intellij+all Patch ###
# Ignores the whole .idea folder and all .iml files
# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360
.idea/
# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023
*.iml
modules.xml
.idea/misc.xml
*.ipr
# Sonarlint plugin
.idea/sonarlint
### VisualStudioCode ###
.vscode/*
!.vscode/tasks.json
!.vscode/launch.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
### VisualStudio ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.meta
*.iobj
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*[.json, .xml, .info]
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
### VisualStudio Patch ###
# Additional files built by Visual Studio
*.tlog
# End of https://www.toptal.com/developers/gitignore/api/intellij+all,visualstudio,visualstudiocode,cmake,c,c++

View File

@ -0,0 +1,75 @@
cmake_minimum_required(VERSION 3.5)
project(prometheus_example CXX)
include(CheckIncludeFileCXX)
check_include_file_cxx(any HAS_ANY)
check_include_file_cxx(string_view HAS_STRING_VIEW)
check_include_file_cxx(coroutine HAS_COROUTINE)
if (NOT "${CMAKE_CXX_STANDARD}" STREQUAL "")
# Do nothing
elseif (HAS_ANY AND HAS_STRING_VIEW AND HAS_COROUTINE)
set(CMAKE_CXX_STANDARD 20)
elseif (HAS_ANY AND HAS_STRING_VIEW)
set(CMAKE_CXX_STANDARD 17)
else ()
set(CMAKE_CXX_STANDARD 14)
endif ()
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_executable(${PROJECT_NAME} main.cc)
# ##############################################################################
# If you include the drogon source code locally in your project, use this method
# to add drogon
# add_subdirectory(drogon)
# target_link_libraries(${PROJECT_NAME} PRIVATE drogon)
#
# and comment out the following lines
find_package(Drogon CONFIG REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
# ##############################################################################
if (CMAKE_CXX_STANDARD LESS 17)
message(FATAL_ERROR "c++17 or higher is required")
elseif (CMAKE_CXX_STANDARD LESS 20)
message(STATUS "use c++17")
else ()
message(STATUS "use c++20")
endif ()
aux_source_directory(controllers CTL_SRC)
aux_source_directory(filters FILTER_SRC)
aux_source_directory(plugins PLUGIN_SRC)
aux_source_directory(models MODEL_SRC)
drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
${CMAKE_CURRENT_BINARY_DIR})
# use the following line to create views with namespaces.
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
# ${CMAKE_CURRENT_BINARY_DIR} TRUE)
# use the following line to create views with namespace CHANGE_ME prefixed
# and path namespaces.
# drogon_create_views(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/views
# ${CMAKE_CURRENT_BINARY_DIR} TRUE CHANGE_ME)
target_include_directories(${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/models)
target_sources(${PROJECT_NAME}
PRIVATE
${SRC_DIR}
${CTL_SRC}
${FILTER_SRC}
${PLUGIN_SRC}
${MODEL_SRC})
# ##############################################################################
# uncomment the following line for dynamically loading views
# set_property(TARGET ${PROJECT_NAME} PROPERTY ENABLE_EXPORTS ON)
# ##############################################################################
add_subdirectory(test)

View File

@ -0,0 +1,33 @@
{
"listeners": [
{
"address": "0.0.0.0",
"port": 5555,
"https": false
}
],
"plugins": [
{
"name": "drogon::plugin::PromExporter",
"dependencies": [],
"config": {
"path": "/metrics",
"collectors":[
{
"name": "http_requests_total",
"help": "The total number of http requests",
"type": "counter",
"labels": ["method", "path"]
},
{
"name": "http_request_duration_seconds",
"help": "The processing time of http requests, in seconds",
"type": "histogram",
"labels": ["method", "path"]
}
]
}
}
]
}

View File

@ -0,0 +1,27 @@
#include "PromTestCtrl.h"
using namespace drogon;
void PromTestCtrl::fast(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback)
{
auto resp = HttpResponse::newHttpResponse();
resp->setBody("Hello, world!");
callback(resp);
}
drogon::AsyncTask PromTestCtrl::slow(
const HttpRequestPtr req,
std::function<void(const HttpResponsePtr &)> callback)
{
// sleep for a random time between 1 and 3 seconds
static std::once_flag flag;
std::call_once(flag, []() { srand(time(nullptr)); });
auto duration = 1 + (rand() % 3);
auto loop = trantor::EventLoop::getEventLoopOfCurrentThread();
co_await drogon::sleepCoro(loop, std::chrono::seconds(duration));
auto resp = HttpResponse::newHttpResponse();
resp->setBody("Hello, world!");
callback(resp);
co_return;
}

View File

@ -0,0 +1,21 @@
#pragma once
#include <drogon/HttpController.h>
#include <drogon/utils/coroutine.h>
using namespace drogon;
class PromTestCtrl : public drogon::HttpController<PromTestCtrl>
{
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(PromTestCtrl::fast, "/fast", "PromStat");
ADD_METHOD_TO(PromTestCtrl::slow, "/slow", "PromStat");
METHOD_LIST_END
void fast(const HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback);
drogon::AsyncTask slow(
const HttpRequestPtr req,
std::function<void(const HttpResponsePtr &)> callback);
};

View File

@ -0,0 +1,52 @@
/**
*
* PromStat.cc
*
*/
#include "PromStat.h"
#include <drogon/plugins/PromExporter.h>
#include <drogon/utils/monitoring/Counter.h>
#include <drogon/utils/monitoring/Histogram.h>
#include <drogon/HttpAppFramework.h>
#include <chrono>
using namespace std::literals::chrono_literals;
using namespace drogon;
Task<HttpResponsePtr> PromStat::invoke(const HttpRequestPtr &req,
MiddlewareNextAwaiter &&next)
{
std::string path{req->matchedPathPattern()};
auto method = req->methodString();
auto promExporter = app().getPlugin<drogon::plugin::PromExporter>();
if (promExporter)
{
auto collector =
promExporter->getCollector<drogon::monitoring::Counter>(
"http_requests_total");
if (collector)
{
collector->metric({method, path})->increment();
}
}
auto start = trantor::Date::date();
auto resp = co_await next;
if (promExporter)
{
auto collector =
promExporter->getCollector<drogon::monitoring::Histogram>(
"http_request_duration_seconds");
if (collector)
{
static const std::vector<double> boundaries{
0.0001, 0.001, 0.01, 0.1, 0.5, 1, 2, 3};
auto end = trantor::Date::date();
auto duration =
end.microSecondsSinceEpoch() - start.microSecondsSinceEpoch();
collector->metric({method, path}, boundaries, 1h, 6)
->observe((double)duration / 1000000);
}
}
co_return resp;
}

View File

@ -0,0 +1,27 @@
/**
*
* PromStat.h
*
*/
#pragma once
#include <drogon/HttpMiddleware.h>
using namespace drogon;
class PromStat : public HttpCoroMiddleware<PromStat>
{
public:
PromStat()
{
void(0);
}
virtual ~PromStat()
{
}
Task<HttpResponsePtr> invoke(const HttpRequestPtr &req,
MiddlewareNextAwaiter &&next) override;
};

View File

@ -0,0 +1,7 @@
#include <drogon/drogon.h>
int main()
{
drogon::app().loadConfigFile("../config.json").run();
return 0;
}

View File

@ -0,0 +1,14 @@
cmake_minimum_required(VERSION 3.5)
project(prometheus_example_test CXX)
add_executable(${PROJECT_NAME} test_main.cc)
# ##############################################################################
# If you include the drogon source code locally in your project, use this method
# to add drogon
# target_link_libraries(${PROJECT_NAME} PRIVATE drogon)
#
# and comment out the following lines
target_link_libraries(${PROJECT_NAME} PRIVATE Drogon::Drogon)
ParseAndAddDrogonTests(${PROJECT_NAME})

View File

@ -0,0 +1,32 @@
#define DROGON_TEST_MAIN
#include <drogon/drogon_test.h>
#include <drogon/drogon.h>
DROGON_TEST(BasicTest)
{
// Add your tests here
}
int main(int argc, char **argv)
{
using namespace drogon;
std::promise<void> p1;
std::future<void> f1 = p1.get_future();
// Start the main loop on another thread
std::thread thr([&]() {
// Queues the promise to be fulfilled after starting the loop
app().getLoop()->queueInLoop([&p1]() { p1.set_value(); });
app().run();
});
// The future is only satisfied after the event loop started
f1.get();
int status = test::run(argc, argv);
// Ask the event loop to shutdown and wait
app().getLoop()->queueInLoop([]() { app().quit(); });
thr.join();
return status;
}

View File

@ -36,12 +36,7 @@ class DROGON_EXPORT Cookie
* @param key key of the cookie * @param key key of the cookie
* @param value value of the cookie * @param value value of the cookie
*/ */
Cookie(const std::string &key, const std::string &value) Cookie(std::string key, std::string value)
: key_(key), value_(value)
{
}
Cookie(std::string &&key, std::string &&value)
: key_(std::move(key)), value_(std::move(value)) : key_(std::move(key)), value_(std::move(value))
{ {
} }
@ -89,6 +84,9 @@ class DROGON_EXPORT Cookie
domain_ = domain; domain_ = domain;
} }
/**
* @brief Set the domain of the cookie.
*/
void setDomain(std::string &&domain) void setDomain(std::string &&domain)
{ {
domain_ = std::move(domain); domain_ = std::move(domain);
@ -102,6 +100,9 @@ class DROGON_EXPORT Cookie
path_ = path; path_ = path;
} }
/**
* @brief Set the path of the cookie.
*/
void setPath(std::string &&path) void setPath(std::string &&path)
{ {
path_ = std::move(path); path_ = std::move(path);
@ -115,6 +116,9 @@ class DROGON_EXPORT Cookie
key_ = key; key_ = key;
} }
/**
* @brief Set the key of the cookie.
*/
void setKey(std::string &&key) void setKey(std::string &&key)
{ {
key_ = std::move(key); key_ = std::move(key);
@ -128,6 +132,9 @@ class DROGON_EXPORT Cookie
value_ = value; value_ = value;
} }
/**
* @brief Set the value of the cookie.
*/
void setValue(std::string &&value) void setValue(std::string &&value)
{ {
value_ = std::move(value); value_ = std::move(value);
@ -149,6 +156,18 @@ class DROGON_EXPORT Cookie
sameSite_ = sameSite; sameSite_ = sameSite;
} }
/**
* @brief Set the partitioned status of the cookie
*/
void setPartitioned(bool partitioned)
{
partitioned_ = partitioned;
if (partitioned)
{
setSecure(true);
}
}
/** /**
* @brief Get the string value of the cookie * @brief Get the string value of the cookie
*/ */
@ -275,6 +294,17 @@ class DROGON_EXPORT Cookie
return secure_; return secure_;
} }
/**
* @brief Check if the cookie is partitioned.
*
* @return true means the cookie is partitioned.
* @return false means the cookie is not partitioned.
*/
bool isPartitioned() const
{
return partitioned_;
}
/** /**
* @brief Get the max-age of the cookie * @brief Get the max-age of the cookie
*/ */
@ -343,21 +373,15 @@ class DROGON_EXPORT Cookie
* @brief Converts a string value to its associated enum class SameSite * @brief Converts a string value to its associated enum class SameSite
* value * value
*/ */
static SameSite convertString2SameSite(const std::string_view &sameSite) static SameSite convertString2SameSite(std::string_view sameSite)
{ {
if (stricmp(sameSite, "lax")) if (stricmp(sameSite, "lax"))
{
return Cookie::SameSite::kLax; return Cookie::SameSite::kLax;
} if (stricmp(sameSite, "strict"))
else if (stricmp(sameSite, "strict"))
{
return Cookie::SameSite::kStrict; return Cookie::SameSite::kStrict;
} if (stricmp(sameSite, "none"))
else if (stricmp(sameSite, "none"))
{
return Cookie::SameSite::kNone; return Cookie::SameSite::kNone;
} if (!stricmp(sameSite, "null"))
else if (!stricmp(sameSite, "null"))
{ {
LOG_WARN LOG_WARN
<< "'" << sameSite << "'" << sameSite
@ -372,34 +396,20 @@ class DROGON_EXPORT Cookie
* @brief Converts an enum class SameSite value to its associated string * @brief Converts an enum class SameSite value to its associated string
* value * value
*/ */
static const std::string_view &convertSameSite2String(SameSite sameSite) static std::string_view convertSameSite2String(SameSite sameSite)
{ {
switch (sameSite) switch (sameSite)
{ {
case SameSite::kLax: case SameSite::kLax:
{ return "Lax";
static std::string_view sv{"Lax"};
return sv;
}
case SameSite::kStrict: case SameSite::kStrict:
{ return "Strict";
static std::string_view sv{"Strict"};
return sv;
}
case SameSite::kNone: case SameSite::kNone:
{ return "None";
static std::string_view sv{"None"};
return sv;
}
case SameSite::kNull: case SameSite::kNull:
{ return "Null";
static std::string_view sv{"Null"}; default:
return sv; return "UNDEFINED";
}
}
{
static std::string_view sv{"UNDEFINED"};
return sv;
} }
} }
@ -407,6 +417,7 @@ class DROGON_EXPORT Cookie
trantor::Date expiresDate_{(std::numeric_limits<int64_t>::max)()}; trantor::Date expiresDate_{(std::numeric_limits<int64_t>::max)()};
bool httpOnly_{true}; bool httpOnly_{true};
bool secure_{false}; bool secure_{false};
bool partitioned_{false};
std::string domain_; std::string domain_;
std::string path_; std::string path_;
std::string key_; std::string key_;

View File

@ -61,8 +61,8 @@ template <typename T>
struct isAutoCreationClass struct isAutoCreationClass
{ {
template <class C> template <class C>
static constexpr auto check(C *) static constexpr auto check(C *) -> std::enable_if_t<
-> std::enable_if_t<std::is_same_v<decltype(C::isAutoCreation), bool>, std::is_same_v<decltype(C::isAutoCreation), const bool>,
bool> bool>
{ {
return C::isAutoCreation; return C::isAutoCreation;

View File

@ -43,6 +43,16 @@
#include <vector> #include <vector>
#include <chrono> #include <chrono>
#if defined(__APPLE__) && defined(__MACH__) && \
(defined(__ENVIRONMENT_IPHONE_OS__) || \
defined(__IPHONE_OS_VERSION_MIN_REQUIRED))
// iOS
#define TARGET_OS_IOS 1
#else
// not iOS
#define TARGET_OS_IOS 0
#endif
namespace drogon namespace drogon
{ {
// the drogon banner // the drogon banner
@ -349,7 +359,7 @@ class DROGON_EXPORT HttpAppFramework : public trantor::NonCopyable
/// Register an advice called before routing /// Register an advice called before routing
/** /**
* @param advice is called after all the synchronous advices return * @param advice is called after all the synchronous advice return
* nullptr and before the request is routed to any handler. The parameters * nullptr and before the request is routed to any handler. The parameters
* of the advice are same as those of the doFilter method of the Filter * of the advice are same as those of the doFilter method of the Filter
* class. * class.
@ -806,6 +816,15 @@ class DROGON_EXPORT HttpAppFramework : public trantor::NonCopyable
const std::vector<std::pair<std::string, std::string>> const std::vector<std::pair<std::string, std::string>>
&sslConfCmds) = 0; &sslConfCmds) = 0;
/// Reload the global cert file and private key file for https server
/// Note: The goal of this method is not to make the framework
/// use the new SSL path, but rather to reload the new content
/// from the old path while the framework is still running.
/// Typically, when our SSL is about to expire,
/// we need to reload the SSL. The purpose of this function
/// is to use the new SSL certificate without stopping the framework.
virtual HttpAppFramework &reloadSSLFiles() = 0;
/// Add plugins /// Add plugins
/** /**
* @param configs The plugins array * @param configs The plugins array
@ -988,7 +1007,7 @@ class DROGON_EXPORT HttpAppFramework : public trantor::NonCopyable
virtual HttpAppFramework &setFileTypes( virtual HttpAppFramework &setFileTypes(
const std::vector<std::string> &types) = 0; const std::vector<std::string> &types) = 0;
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
/// Enable supporting for dynamic views loading. /// Enable supporting for dynamic views loading.
/** /**
* *
@ -1590,6 +1609,34 @@ class DROGON_EXPORT HttpAppFramework : public trantor::NonCopyable
*/ */
virtual int64_t getConnectionCount() const = 0; virtual int64_t getConnectionCount() const = 0;
/**
* @brief Set the before listen setsockopt callback.
*
* @param cb This callback will be called before the listen
*/
virtual HttpAppFramework &setBeforeListenSockOptCallback(
std::function<void(int)> cb) = 0;
/**
* @brief Set the after accept setsockopt callback.
*
* @param cb This callback will be called after accept
*/
virtual HttpAppFramework &setAfterAcceptSockOptCallback(
std::function<void(int)> cb) = 0;
/**
* @brief Set the client disconnect or connect callback.
*
* @param cb This callback will be called, when the client disconnect or
* connect
*/
virtual HttpAppFramework &setConnectionCallback(
std::function<void(const trantor::TcpConnectionPtr &)> cb) = 0;
virtual HttpAppFramework &enableRequestStream(bool enable = true) = 0;
virtual bool isRequestStreamEnabled() const = 0;
private: private:
virtual void registerHttpController( virtual void registerHttpController(
const std::string &pathPattern, const std::string &pathPattern,

View File

@ -164,6 +164,7 @@ class HttpBinderBase
std::function<void(const HttpResponsePtr &)> &&callback) = 0; std::function<void(const HttpResponsePtr &)> &&callback) = 0;
virtual size_t paramCount() = 0; virtual size_t paramCount() = 0;
virtual const std::string &handlerName() const = 0; virtual const std::string &handlerName() const = 0;
virtual bool isStreamHandler() = 0;
virtual ~HttpBinderBase() virtual ~HttpBinderBase()
{ {
@ -218,6 +219,11 @@ class HttpBinder : public HttpBinderBase
return traits::arity; return traits::arity;
} }
bool isStreamHandler() override
{
return traits::isStreamHandler;
}
HttpBinder(FUNCTION &&func) : func_(std::forward<FUNCTION>(func)) HttpBinder(FUNCTION &&func) : func_(std::forward<FUNCTION>(func))
{ {
static_assert(traits::isHTTPFunction, static_assert(traits::isHTTPFunction,
@ -266,6 +272,7 @@ class HttpBinder : public HttpBinderBase
template <typename... Values, template <typename... Values,
std::size_t Boundary = argument_count, std::size_t Boundary = argument_count,
bool isStreamHandler = traits::isStreamHandler,
bool isCoroutine = traits::isCoroutine> bool isCoroutine = traits::isCoroutine>
void run(std::deque<std::string> &pathArguments, void run(std::deque<std::string> &pathArguments,
const HttpRequestPtr &req, const HttpRequestPtr &req,
@ -344,8 +351,18 @@ class HttpBinder : public HttpBinderBase
{ {
// Explicit copy because `callFunction` moves it // Explicit copy because `callFunction` moves it
auto cb = callback; auto cb = callback;
if constexpr (isStreamHandler)
{
callFunction(req,
createRequestStream(req),
cb,
std::move(values)...);
}
else
{
callFunction(req, cb, std::move(values)...); callFunction(req, cb, std::move(values)...);
} }
}
catch (const std::exception &except) catch (const std::exception &except)
{ {
handleException(except, req, std::move(callback)); handleException(except, req, std::move(callback));
@ -359,6 +376,7 @@ class HttpBinder : public HttpBinderBase
#ifdef __cpp_impl_coroutine #ifdef __cpp_impl_coroutine
else else
{ {
static_assert(!isStreamHandler);
[this](HttpRequestPtr req, [this](HttpRequestPtr req,
std::function<void(const HttpResponsePtr &)> callback, std::function<void(const HttpResponsePtr &)> callback,
Values &&...values) -> AsyncTask { Values &&...values) -> AsyncTask {

View File

@ -21,6 +21,7 @@
#include <drogon/HttpRequest.h> #include <drogon/HttpRequest.h>
#include <trantor/utils/NonCopyable.h> #include <trantor/utils/NonCopyable.h>
#include <trantor/net/EventLoop.h> #include <trantor/net/EventLoop.h>
#include <cstddef>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <future> #include <future>
@ -178,6 +179,12 @@ class DROGON_EXPORT HttpClient : public trantor::NonCopyable
*/ */
virtual void setSockOptCallback(std::function<void(int)> cb) = 0; virtual void setSockOptCallback(std::function<void(int)> cb) = 0;
/**
* @brief Return the number of unsent http requests in the current http
* client cache buffer
*/
virtual std::size_t requestsBufferSize() = 0;
/// Set the pipelining depth, which is the number of requests that are not /// Set the pipelining depth, which is the number of requests that are not
/// responding. /// responding.
/** /**

View File

@ -30,6 +30,7 @@
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>
#include <string_view> #include <string_view>
#include <trantor/net/TcpConnection.h>
namespace drogon namespace drogon
{ {
@ -179,6 +180,17 @@ class DROGON_EXPORT HttpRequest
return cookies(); return cookies();
} }
/**
* @brief Return content length parsed from the Content-Length header
* If no Content-Length header, return null.
*/
virtual size_t realContentLength() const = 0;
size_t getRealContentLength() const
{
return realContentLength();
}
/// Get the query string of the request. /// Get the query string of the request.
/** /**
* The query string is the substring after the '?' in the URL string. * The query string is the substring after the '?' in the URL string.
@ -438,8 +450,7 @@ class DROGON_EXPORT HttpRequest
virtual void setCustomContentTypeString(const std::string &type) = 0; virtual void setCustomContentTypeString(const std::string &type) = 0;
/// Add a cookie /// Add a cookie
virtual void addCookie(const std::string &key, virtual void addCookie(std::string key, std::string value) = 0;
const std::string &value) = 0;
/** /**
* @brief Set the request object to the pass-through mode or not. It's not * @brief Set the request object to the pass-through mode or not. It's not
@ -494,6 +505,11 @@ class DROGON_EXPORT HttpRequest
virtual void setContentTypeString(const char *typeString, virtual void setContentTypeString(const char *typeString,
size_t typeStringLength) = 0; size_t typeStringLength) = 0;
virtual bool connected() const noexcept = 0;
virtual const std::weak_ptr<trantor::TcpConnection> &getConnectionPtr()
const noexcept = 0;
virtual ~HttpRequest() virtual ~HttpRequest()
{ {
} }

View File

@ -123,6 +123,8 @@ class DROGON_EXPORT MultiPartParser
{ {
public: public:
MultiPartParser(){}; MultiPartParser(){};
MultiPartParser(const MultiPartParser &other) = default; // Copyable
MultiPartParser(MultiPartParser &&other) = default; // Movable
~MultiPartParser(){}; ~MultiPartParser(){};
/// Get files, This method should be called after calling the parse() /// Get files, This method should be called after calling the parse()
/// method. /// method.

View File

@ -0,0 +1,117 @@
/**
*
* @file RequestStream.h
* @author Nitromelon
*
* Copyright 2024, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <drogon/exports.h>
#include <string>
#include <functional>
#include <memory>
#include <exception>
namespace drogon
{
class HttpRequest;
using HttpRequestPtr = std::shared_ptr<HttpRequest>;
class RequestStreamReader;
using RequestStreamReaderPtr = std::shared_ptr<RequestStreamReader>;
struct MultipartHeader
{
std::string name;
std::string filename;
std::string contentType;
};
class DROGON_EXPORT RequestStream
{
public:
virtual ~RequestStream() = default;
virtual void setStreamReader(RequestStreamReaderPtr reader) = 0;
};
using RequestStreamPtr = std::shared_ptr<RequestStream>;
namespace internal
{
DROGON_EXPORT RequestStreamPtr createRequestStream(const HttpRequestPtr &req);
}
enum class StreamErrorCode
{
kNone = 0,
kBadRequest,
kConnectionBroken
};
class StreamError final : public std::exception
{
public:
const char *what() const noexcept override
{
return message_.data();
}
StreamErrorCode code() const
{
return code_;
}
StreamError(StreamErrorCode code, const std::string &message)
: message_(message), code_(code)
{
}
StreamError(StreamErrorCode code, std::string &&message)
: message_(std::move(message)), code_(code)
{
}
StreamError() = delete;
private:
std::string message_;
StreamErrorCode code_;
};
/**
* An interface for stream request reading.
* User should create an implementation class, or use built-in handlers
*/
class DROGON_EXPORT RequestStreamReader
{
public:
virtual ~RequestStreamReader() = default;
virtual void onStreamData(const char *, size_t) = 0;
virtual void onStreamFinish(std::exception_ptr) = 0;
using StreamDataCallback = std::function<void(const char *, size_t)>;
using StreamFinishCallback = std::function<void(std::exception_ptr)>;
// Create a handler with default implementation
static RequestStreamReaderPtr newReader(StreamDataCallback dataCb,
StreamFinishCallback finishCb);
// A handler that drops all data
static RequestStreamReaderPtr newNullReader();
using MultipartHeaderCallback = std::function<void(MultipartHeader header)>;
static RequestStreamReaderPtr newMultipartReader(
const HttpRequestPtr &req,
MultipartHeaderCallback headerCb,
StreamDataCallback dataCb,
StreamFinishCallback finishCb);
};
} // namespace drogon

View File

@ -36,6 +36,7 @@ namespace plugin
// "show_microseconds": true, // "show_microseconds": true,
// "custom_time_format": "", // "custom_time_format": "",
// "use_real_ip": false // "use_real_ip": false
// "path_exempt": ""
} }
} }
@endcode @endcode
@ -98,6 +99,10 @@ namespace plugin
* Enable the plugin by adding the configuration to the list of plugins in the * Enable the plugin by adding the configuration to the list of plugins in the
* configuration file. * configuration file.
* *
* path_exempt: must be a string or a string array, present a regular expression
* (for matching the path of a request) or a regular expression list for URLs
* that don't have to be logged.
*
*/ */
class DROGON_EXPORT AccessLogger : public drogon::Plugin<AccessLogger> class DROGON_EXPORT AccessLogger : public drogon::Plugin<AccessLogger>
{ {
@ -117,6 +122,8 @@ class DROGON_EXPORT AccessLogger : public drogon::Plugin<AccessLogger>
bool useCustomTimeFormat_{false}; bool useCustomTimeFormat_{false};
std::string timeFormat_; std::string timeFormat_;
static bool useRealIp_; static bool useRealIp_;
std::regex exemptRegex_;
bool regexFlag_{false};
using LogFunction = std::function<void(trantor::LogStream &, using LogFunction = std::function<void(trantor::LogStream &,
const drogon::HttpRequestPtr &, const drogon::HttpRequestPtr &,

View File

@ -71,7 +71,9 @@ IPs or users. the default value is 600.
"ip_capacity": 0, "ip_capacity": 0,
"user_capacity": 0 "user_capacity": 0
},... },...
] ],
// Trusted proxy ip or cidr
"trust_ips": ["127.0.0.1", "172.16.0.0/12"],
} }
} }
@endcode @endcode
@ -137,12 +139,14 @@ class DROGON_EXPORT Hodor : public drogon::Plugin<Hodor>
std::function<HttpResponsePtr(const drogon::HttpRequestPtr &)> std::function<HttpResponsePtr(const drogon::HttpRequestPtr &)>
rejectResponseFactory_; rejectResponseFactory_;
RealIpResolver::CIDRs trustCIDRs_;
void onHttpRequest(const drogon::HttpRequestPtr &, void onHttpRequest(const drogon::HttpRequestPtr &,
AdviceCallback &&, AdviceCallback &&,
AdviceChainCallback &&); AdviceChainCallback &&);
bool checkLimit(const drogon::HttpRequestPtr &req, bool checkLimit(const drogon::HttpRequestPtr &req,
const LimitStrategy &strategy, const LimitStrategy &strategy,
const std::string &ip, const trantor::InetAddress &ip,
const std::optional<std::string> &userId); const std::optional<std::string> &userId);
HttpResponsePtr rejectResponse_; HttpResponsePtr rejectResponse_;
}; };

View File

@ -57,7 +57,6 @@ class DROGON_EXPORT RealIpResolver : public drogon::Plugin<RealIpResolver>
private: private:
const trantor::InetAddress &getRealAddr( const trantor::InetAddress &getRealAddr(
const drogon::HttpRequestPtr &req) const; const drogon::HttpRequestPtr &req) const;
bool matchCidr(const trantor::InetAddress &addr) const;
struct CIDR struct CIDR
{ {
@ -66,7 +65,12 @@ class DROGON_EXPORT RealIpResolver : public drogon::Plugin<RealIpResolver>
in_addr_t mask_{32}; in_addr_t mask_{32};
}; };
std::vector<CIDR> trustCIDRs_; using CIDRs = std::vector<CIDR>;
static bool matchCidr(const trantor::InetAddress &addr,
const CIDRs &trustCIDRs);
friend class Hodor;
CIDRs trustCIDRs_;
std::string fromHeader_; std::string fromHeader_;
std::string attributeKey_; std::string attributeKey_;
bool useXForwardedFor_{false}; bool useXForwardedFor_{false};

View File

@ -15,6 +15,7 @@
#pragma once #pragma once
#include <drogon/DrObject.h> #include <drogon/DrObject.h>
#include <drogon/RequestStream.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <tuple> #include <tuple>
@ -46,53 +47,36 @@ struct resumable_type : std::false_type
template <typename> template <typename>
struct FunctionTraits; struct FunctionTraits;
// functor,lambda,std::function... //
template <typename Function> // Basic match, inherited by all other matches
struct FunctionTraits //
: public FunctionTraits< template <typename ReturnType, typename... Arguments>
decltype(&std::remove_reference_t<Function>::operator())> struct FunctionTraits<ReturnType (*)(Arguments...)>
{ {
static const bool isClassFunction = false; using result_type = ReturnType;
static const bool isDrObjectClass = false;
template <std::size_t Index>
using argument =
typename std::tuple_element_t<Index, std::tuple<Arguments...>>;
static const std::size_t arity = sizeof...(Arguments);
using class_type = void; using class_type = void;
using return_type = ReturnType;
static const bool isHTTPFunction = false;
static const bool isClassFunction = false;
static const bool isStreamHandler = false;
static const bool isDrObjectClass = false;
static const bool isCoroutine = false;
static const std::string name() static const std::string name()
{ {
return std::string("Functor"); return std::string("Normal or Static Function");
} }
}; };
// class instance method of const object //
template <typename ClassType, typename ReturnType, typename... Arguments> // Match normal functions
struct FunctionTraits<ReturnType (ClassType::*)(Arguments...) const> //
: FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isClassFunction = true;
static const bool isDrObjectClass =
std::is_base_of<DrObject<ClassType>, ClassType>::value;
using class_type = ClassType;
static const std::string name()
{
return std::string("Class Function");
}
};
// class instance method of non-const object
template <typename ClassType, typename ReturnType, typename... Arguments>
struct FunctionTraits<ReturnType (ClassType::*)(Arguments...)>
: FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isClassFunction = true;
static const bool isDrObjectClass =
std::is_base_of<DrObject<ClassType>, ClassType>::value;
using class_type = ClassType;
static const std::string name()
{
return std::string("Class Function");
}
};
// normal function for HTTP handling // normal function for HTTP handling
template <typename ReturnType, typename... Arguments> template <typename ReturnType, typename... Arguments>
@ -108,16 +92,93 @@ struct FunctionTraits<
using return_type = ReturnType; using return_type = ReturnType;
}; };
template <typename ReturnType, typename... Arguments> // normal function with custom request object
template <typename T, typename ReturnType, typename... Arguments>
struct FunctionTraits< struct FunctionTraits<
ReturnType (*)(HttpRequestPtr &req, ReturnType (*)(T &&customReq,
std::function<void(const HttpResponsePtr &)> &&callback, std::function<void(const HttpResponsePtr &)> &&callback,
Arguments...)> : FunctionTraits<ReturnType (*)(Arguments...)> Arguments...)> : FunctionTraits<ReturnType (*)(Arguments...)>
{ {
static const bool isHTTPFunction = false; static const bool isHTTPFunction = !resumable_type<ReturnType>::value;
static const bool isCoroutine = false;
using class_type = void; using class_type = void;
using first_param_type = T;
using return_type = ReturnType;
}; };
// normal function with stream handler
template <typename ReturnType, typename... Arguments>
struct FunctionTraits<
ReturnType (*)(const HttpRequestPtr &req,
RequestStreamPtr &&streamCtx,
std::function<void(const HttpResponsePtr &)> &&callback,
Arguments...)> : FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isHTTPFunction = !resumable_type<ReturnType>::value;
static const bool isCoroutine = false;
static const bool isStreamHandler = true;
using class_type = void;
using first_param_type = HttpRequestPtr;
using return_type = ReturnType;
};
//
// Match functor,lambda,std::function... inherits normal function matches
//
template <typename Function>
struct FunctionTraits
: public FunctionTraits<
decltype(&std::remove_reference_t<Function>::operator())>
{
static const bool isClassFunction = false;
static const bool isDrObjectClass = false;
using class_type = void;
static const std::string name()
{
return std::string("Functor");
}
};
//
// Match class functions, inherits normal function matches
//
// class const method
template <typename ClassType, typename ReturnType, typename... Arguments>
struct FunctionTraits<ReturnType (ClassType::*)(Arguments...) const>
: FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isClassFunction = true;
static const bool isDrObjectClass =
std::is_base_of<DrObject<ClassType>, ClassType>::value;
using class_type = ClassType;
static const std::string name()
{
return std::string("Class Function");
}
};
// class non-const method
template <typename ClassType, typename ReturnType, typename... Arguments>
struct FunctionTraits<ReturnType (ClassType::*)(Arguments...)>
: FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isClassFunction = true;
static const bool isDrObjectClass =
std::is_base_of<DrObject<ClassType>, ClassType>::value;
using class_type = ClassType;
static const std::string name()
{
return std::string("Class Function");
}
};
//
// Match coroutine functions
//
#ifdef __cpp_impl_coroutine #ifdef __cpp_impl_coroutine
template <typename... Arguments> template <typename... Arguments>
struct FunctionTraits< struct FunctionTraits<
@ -158,6 +219,20 @@ struct FunctionTraits<Task<HttpResponsePtr> (*)(HttpRequestPtr req,
}; };
#endif #endif
//
// Bad matches
//
template <typename ReturnType, typename... Arguments>
struct FunctionTraits<
ReturnType (*)(HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback,
Arguments...)> : FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isHTTPFunction = false;
using class_type = void;
};
template <typename ReturnType, typename... Arguments> template <typename ReturnType, typename... Arguments>
struct FunctionTraits< struct FunctionTraits<
ReturnType (*)(HttpRequestPtr &&req, ReturnType (*)(HttpRequestPtr &&req,
@ -168,43 +243,5 @@ struct FunctionTraits<
using class_type = void; using class_type = void;
}; };
// normal function for HTTP handling
template <typename T, typename ReturnType, typename... Arguments>
struct FunctionTraits<
ReturnType (*)(T &&customReq,
std::function<void(const HttpResponsePtr &)> &&callback,
Arguments...)> : FunctionTraits<ReturnType (*)(Arguments...)>
{
static const bool isHTTPFunction = !resumable_type<ReturnType>::value;
static const bool isCoroutine = false;
using class_type = void;
using first_param_type = T;
using return_type = ReturnType;
};
// normal function
template <typename ReturnType, typename... Arguments>
struct FunctionTraits<ReturnType (*)(Arguments...)>
{
using result_type = ReturnType;
template <std::size_t Index>
using argument =
typename std::tuple_element_t<Index, std::tuple<Arguments...>>;
static const std::size_t arity = sizeof...(Arguments);
using class_type = void;
using return_type = ReturnType;
static const bool isHTTPFunction = false;
static const bool isClassFunction = false;
static const bool isDrObjectClass = false;
static const bool isCoroutine = false;
static const std::string name()
{
return std::string("Normal or Static Function");
}
};
} // namespace internal } // namespace internal
} // namespace drogon } // namespace drogon

View File

@ -134,47 +134,85 @@ constexpr size_t base64EncodedLength(size_t in_len, bool padded = true)
} }
/// Encode the string to base64 format. /// Encode the string to base64 format.
DROGON_EXPORT std::string base64Encode(const unsigned char *bytes_to_encode, DROGON_EXPORT void base64Encode(const unsigned char *bytesToEncode,
size_t in_len, size_t inLen,
bool url_safe = false, unsigned char *outputBuffer,
bool urlSafe = false,
bool padded = true); bool padded = true);
/// Encode the string to base64 format.
inline std::string base64Encode(const unsigned char *bytesToEncode,
size_t inLen,
bool urlSafe = false,
bool padded = true)
{
std::string ret;
ret.resize(base64EncodedLength(inLen, padded));
base64Encode(
bytesToEncode, inLen, (unsigned char *)ret.data(), urlSafe, padded);
return ret;
}
/// Encode the string to base64 format. /// Encode the string to base64 format.
inline std::string base64Encode(std::string_view data, inline std::string base64Encode(std::string_view data,
bool url_safe = false, bool urlSafe = false,
bool padded = true) bool padded = true)
{ {
return base64Encode((unsigned char *)data.data(), return base64Encode((unsigned char *)data.data(),
data.size(), data.size(),
url_safe, urlSafe,
padded); padded);
} }
/// Encode the string to base64 format with no padding. /// Encode the string to base64 format with no padding.
inline std::string base64EncodeUnpadded(const unsigned char *bytes_to_encode, inline void base64EncodeUnpadded(const unsigned char *bytesToEncode,
size_t in_len, size_t inLen,
bool url_safe = false) unsigned char *outputBuffer,
bool urlSafe = false)
{ {
return base64Encode(bytes_to_encode, in_len, url_safe, false); base64Encode(bytesToEncode, inLen, outputBuffer, urlSafe, false);
}
/// Encode the string to base64 format with no padding.
inline std::string base64EncodeUnpadded(const unsigned char *bytesToEncode,
size_t inLen,
bool urlSafe = false)
{
return base64Encode(bytesToEncode, inLen, urlSafe, false);
} }
/// Encode the string to base64 format with no padding. /// Encode the string to base64 format with no padding.
inline std::string base64EncodeUnpadded(std::string_view data, inline std::string base64EncodeUnpadded(std::string_view data,
bool url_safe = false) bool urlSafe = false)
{ {
return base64Encode(data, url_safe, false); return base64Encode(data, urlSafe, false);
} }
/// Get the decoded length of base64. /// Get the decoded length of base64.
constexpr size_t base64DecodedLength(size_t in_len) constexpr size_t base64DecodedLength(size_t inLen)
{ {
return (in_len * 3) / 4; return (inLen * 3) / 4;
} }
/// Decode the base64 format string. /// Decode the base64 format string.
DROGON_EXPORT std::string base64Decode(std::string_view encoded_string); /// Return the number of bytes written.
DROGON_EXPORT size_t base64Decode(const char *encodedString,
size_t inLen,
unsigned char *outputBuffer);
/// Decode the base64 format string.
inline std::string base64Decode(std::string_view encodedString)
{
auto inLen = encodedString.size();
std::string ret;
ret.resize(base64DecodedLength(inLen));
ret.resize(
base64Decode(encodedString.data(), inLen, (unsigned char *)ret.data()));
return ret;
}
DROGON_EXPORT std::vector<char> base64DecodeToVector( DROGON_EXPORT std::vector<char> base64DecodeToVector(
std::string_view encoded_string); std::string_view encodedString);
/// Check if the string need decoding /// Check if the string need decoding
DROGON_EXPORT bool needUrlDecoding(const char *begin, const char *end); DROGON_EXPORT bool needUrlDecoding(const char *begin, const char *end);
@ -263,6 +301,12 @@ DROGON_EXPORT std::string brotliDecompress(const char *data,
DROGON_EXPORT char *getHttpFullDate( DROGON_EXPORT char *getHttpFullDate(
const trantor::Date &date = trantor::Date::now()); const trantor::Date &date = trantor::Date::now());
DROGON_EXPORT const std::string &getHttpFullDateStr(
const trantor::Date &date = trantor::Date::now());
DROGON_EXPORT void dateToCustomFormattedString(const std::string &fmtStr,
std::string &str,
const trantor::Date &date);
/// Get the trantor::Date object according to the http full date string /// Get the trantor::Date object according to the http full date string
/** /**
* Returns trantor::Date(std::numeric_limits<int64_t>::max()) upon failure. * Returns trantor::Date(std::numeric_limits<int64_t>::max()) upon failure.
@ -540,6 +584,7 @@ namespace trantor
{ {
inline LogStream &operator<<(LogStream &ls, const std::string_view &v) inline LogStream &operator<<(LogStream &ls, const std::string_view &v)
{ {
if (!v.empty())
ls.append(v.data(), v.length()); ls.append(v.data(), v.length());
return ls; return ls;
} }

View File

@ -245,7 +245,7 @@ struct [[nodiscard]] Task
std::optional<T> value; std::optional<T> value;
std::exception_ptr exception_; std::exception_ptr exception_;
std::coroutine_handle<> continuation_; std::coroutine_handle<> continuation_{std::noop_coroutine()};
}; };
auto operator co_await() const noexcept auto operator co_await() const noexcept
@ -332,7 +332,7 @@ struct [[nodiscard]] Task<void>
} }
std::exception_ptr exception_; std::exception_ptr exception_;
std::coroutine_handle<> continuation_; std::coroutine_handle<> continuation_{std::noop_coroutine()};
}; };
auto operator co_await() const noexcept auto operator co_await() const noexcept
@ -811,4 +811,180 @@ inline internal::EventLoopAwaiter<T> queueInLoopCoro(trantor::EventLoop *loop,
return internal::EventLoopAwaiter<T>(std::move(task), loop); return internal::EventLoopAwaiter<T>(std::move(task), loop);
} }
class Mutex final
{
class ScopedCoroMutexAwaiter;
class CoroMutexAwaiter;
public:
Mutex() noexcept : state_(unlockedValue()), waiters_(nullptr)
{
}
Mutex(const Mutex &) = delete;
Mutex(Mutex &&) = delete;
Mutex &operator=(const Mutex &) = delete;
Mutex &operator=(Mutex &&) = delete;
~Mutex()
{
[[maybe_unused]] auto state = state_.load(std::memory_order_relaxed);
assert(state == unlockedValue() || state == nullptr);
assert(waiters_ == nullptr);
}
bool try_lock() noexcept
{
void *oldValue = unlockedValue();
return state_.compare_exchange_strong(oldValue,
nullptr,
std::memory_order_acquire,
std::memory_order_relaxed);
}
[[nodiscard]] ScopedCoroMutexAwaiter scoped_lock(
trantor::EventLoop *loop =
trantor::EventLoop::getEventLoopOfCurrentThread()) noexcept
{
return ScopedCoroMutexAwaiter(*this, loop);
}
[[nodiscard]] CoroMutexAwaiter lock(
trantor::EventLoop *loop =
trantor::EventLoop::getEventLoopOfCurrentThread()) noexcept
{
return CoroMutexAwaiter(*this, loop);
}
void unlock() noexcept
{
assert(state_.load(std::memory_order_relaxed) != unlockedValue());
auto *waitersHead = waiters_;
if (waitersHead == nullptr)
{
void *currentState = state_.load(std::memory_order_relaxed);
if (currentState == nullptr)
{
const bool releasedLock =
state_.compare_exchange_strong(currentState,
unlockedValue(),
std::memory_order_release,
std::memory_order_relaxed);
if (releasedLock)
{
return;
}
}
currentState = state_.exchange(nullptr, std::memory_order_acquire);
assert(currentState != unlockedValue());
assert(currentState != nullptr);
auto *waiter = static_cast<CoroMutexAwaiter *>(currentState);
do
{
auto *temp = waiter->next_;
waiter->next_ = waitersHead;
waitersHead = waiter;
waiter = temp;
} while (waiter != nullptr);
}
assert(waitersHead != nullptr);
waiters_ = waitersHead->next_;
if (waitersHead->loop_)
{
auto handle = waitersHead->handle_;
waitersHead->loop_->runInLoop([handle] { handle.resume(); });
}
else
{
waitersHead->handle_.resume();
}
}
private:
class CoroMutexAwaiter
{
public:
CoroMutexAwaiter(Mutex &mutex, trantor::EventLoop *loop) noexcept
: mutex_(mutex), loop_(loop)
{
}
bool await_ready() noexcept
{
return mutex_.try_lock();
}
bool await_suspend(std::coroutine_handle<> handle) noexcept
{
handle_ = handle;
return mutex_.asynclockImpl(this);
}
void await_resume() noexcept
{
}
private:
friend class Mutex;
Mutex &mutex_;
trantor::EventLoop *loop_;
std::coroutine_handle<> handle_;
CoroMutexAwaiter *next_;
};
class ScopedCoroMutexAwaiter : public CoroMutexAwaiter
{
public:
ScopedCoroMutexAwaiter(Mutex &mutex, trantor::EventLoop *loop)
: CoroMutexAwaiter(mutex, loop)
{
}
[[nodiscard]] auto await_resume() noexcept
{
return std::unique_lock<Mutex>{mutex_, std::adopt_lock};
}
};
bool asynclockImpl(CoroMutexAwaiter *awaiter)
{
void *oldValue = state_.load(std::memory_order_relaxed);
while (true)
{
if (oldValue == unlockedValue())
{
void *newValue = nullptr;
if (state_.compare_exchange_weak(oldValue,
newValue,
std::memory_order_acquire,
std::memory_order_relaxed))
{
return false;
}
}
else
{
void *newValue = awaiter;
awaiter->next_ = static_cast<CoroMutexAwaiter *>(oldValue);
if (state_.compare_exchange_weak(oldValue,
newValue,
std::memory_order_release,
std::memory_order_relaxed))
{
return true;
}
}
}
}
void *unlockedValue() noexcept
{
return this;
}
std::atomic<void *> state_;
CoroMutexAwaiter *waiters_;
};
} // namespace drogon } // namespace drogon

View File

@ -60,8 +60,10 @@ class Collector : public CollectorBase
{ {
} }
template <typename... Arguments>
const std::shared_ptr<T> &metric( const std::shared_ptr<T> &metric(
const std::vector<std::string> &labelValues) noexcept(false) const std::vector<std::string> &labelValues,
Arguments... args) noexcept(false)
{ {
if (labelValues.size() != labelsNames_.size()) if (labelValues.size() != labelsNames_.size())
{ {
@ -75,7 +77,8 @@ class Collector : public CollectorBase
{ {
return iter->second; return iter->second;
} }
auto metric = std::make_shared<T>(name_, labelsNames_, labelValues); auto metric =
std::make_shared<T>(name_, labelsNames_, labelValues, args...);
metrics_[labelValues] = metric; metrics_[labelValues] = metric;
return metrics_[labelValues]; return metrics_[labelValues];
} }

View File

@ -91,7 +91,7 @@ class Gauge : public Metric
static std::string_view type() static std::string_view type()
{ {
return "counter"; return "gauge";
} }
void setToCurrentTime() void setToCurrentTime()

View File

@ -68,6 +68,7 @@ class DROGON_EXPORT Histogram : public Metric
} }
} }
timeBuckets_.emplace_back(); timeBuckets_.emplace_back();
timeBuckets_.back().buckets.resize(bucketBoundaries.size() + 1);
// check the bucket boundaries are sorted // check the bucket boundaries are sorted
for (size_t i = 1; i < bucketBoundaries.size(); i++) for (size_t i = 1; i < bucketBoundaries.size(); i++)
{ {

View File

@ -20,10 +20,10 @@
namespace drogon namespace drogon
{ {
static void doAdvicesChain( static void doAdviceChain(
const std::vector<std::function<void(const HttpRequestPtr &, const std::vector<std::function<void(const HttpRequestPtr &,
AdviceCallback &&, AdviceCallback &&,
AdviceChainCallback &&)>> &advices, AdviceChainCallback &&)>> &adviceChain,
size_t index, size_t index,
const HttpRequestImplPtr &req, const HttpRequestImplPtr &req,
std::shared_ptr<const std::function<void(const HttpResponsePtr &)>> std::shared_ptr<const std::function<void(const HttpResponsePtr &)>>
@ -88,7 +88,7 @@ void AopAdvice::passPreRoutingAdvices(
auto callbackPtr = auto callbackPtr =
std::make_shared<std::decay_t<decltype(callback)>>(std::move(callback)); std::make_shared<std::decay_t<decltype(callback)>>(std::move(callback));
doAdvicesChain(preRoutingAdvices_, 0, req, std::move(callbackPtr)); doAdviceChain(preRoutingAdvices_, 0, req, std::move(callbackPtr));
} }
void AopAdvice::passPostRoutingObservers(const HttpRequestImplPtr &req) const void AopAdvice::passPostRoutingObservers(const HttpRequestImplPtr &req) const
@ -114,7 +114,7 @@ void AopAdvice::passPostRoutingAdvices(
auto callbackPtr = auto callbackPtr =
std::make_shared<std::decay_t<decltype(callback)>>(std::move(callback)); std::make_shared<std::decay_t<decltype(callback)>>(std::move(callback));
doAdvicesChain(postRoutingAdvices_, 0, req, std::move(callbackPtr)); doAdviceChain(postRoutingAdvices_, 0, req, std::move(callbackPtr));
} }
void AopAdvice::passPreHandlingObservers(const HttpRequestImplPtr &req) const void AopAdvice::passPreHandlingObservers(const HttpRequestImplPtr &req) const
@ -140,7 +140,7 @@ void AopAdvice::passPreHandlingAdvices(
auto callbackPtr = auto callbackPtr =
std::make_shared<std::decay_t<decltype(callback)>>(std::move(callback)); std::make_shared<std::decay_t<decltype(callback)>>(std::move(callback));
doAdvicesChain(preHandlingAdvices_, 0, req, std::move(callbackPtr)); doAdviceChain(preHandlingAdvices_, 0, req, std::move(callbackPtr));
} }
void AopAdvice::passPostHandlingAdvices(const HttpRequestImplPtr &req, void AopAdvice::passPostHandlingAdvices(const HttpRequestImplPtr &req,
@ -161,32 +161,32 @@ void AopAdvice::passPreSendingAdvices(const HttpRequestImplPtr &req,
} }
} }
static void doAdvicesChain( static void doAdviceChain(
const std::vector<std::function<void(const HttpRequestPtr &, const std::vector<std::function<void(const HttpRequestPtr &,
AdviceCallback &&, AdviceCallback &&,
AdviceChainCallback &&)>> &advices, AdviceChainCallback &&)>> &adviceChain,
size_t index, size_t index,
const HttpRequestImplPtr &req, const HttpRequestImplPtr &req,
std::shared_ptr<const std::function<void(const HttpResponsePtr &)>> std::shared_ptr<const std::function<void(const HttpResponsePtr &)>>
&&callbackPtr) &&callbackPtr)
{ {
if (index < advices.size()) if (index < adviceChain.size())
{ {
auto &advice = advices[index]; auto &advice = adviceChain[index];
advice( advice(
req, req,
[/*copy*/ callbackPtr](const HttpResponsePtr &resp) { [/*copy*/ callbackPtr](const HttpResponsePtr &resp) {
(*callbackPtr)(resp); (*callbackPtr)(resp);
}, },
[index, req, callbackPtr, &advices]() mutable { [index, req, callbackPtr, &adviceChain]() mutable {
auto ioLoop = req->getLoop(); auto ioLoop = req->getLoop();
if (ioLoop && !ioLoop->isInLoopThread()) if (ioLoop && !ioLoop->isInLoopThread())
{ {
ioLoop->queueInLoop([index, ioLoop->queueInLoop([index,
req, req,
callbackPtr = std::move(callbackPtr), callbackPtr = std::move(callbackPtr),
&advices]() mutable { &adviceChain]() mutable {
doAdvicesChain(advices, doAdviceChain(adviceChain,
index + 1, index + 1,
req, req,
std::move(callbackPtr)); std::move(callbackPtr));
@ -194,7 +194,7 @@ static void doAdvicesChain(
} }
else else
{ {
doAdvicesChain(advices, doAdviceChain(adviceChain,
index + 1, index + 1,
req, req,
std::move(callbackPtr)); std::move(callbackPtr));

View File

@ -113,6 +113,46 @@ void AccessLogger::initAndStart(const Json::Value &config)
} }
createLogFunctions(format); createLogFunctions(format);
auto logPath = config.get("log_path", "").asString(); auto logPath = config.get("log_path", "").asString();
if (config.isMember("path_exempt"))
{
if (config["path_exempt"].isArray())
{
const auto &exempts = config["path_exempt"];
size_t exemptsCount = exempts.size();
if (exemptsCount)
{
std::string regexString;
size_t len = 0;
for (const auto &exempt : exempts)
{
assert(exempt.isString());
len += exempt.size();
}
regexString.reserve((exemptsCount * (1 + 2)) - 1 + len);
const auto last = --exempts.end();
for (auto exempt = exempts.begin(); exempt != last; ++exempt)
regexString.append("(")
.append(exempt->asString())
.append(")|");
regexString.append("(").append(last->asString()).append(")");
exemptRegex_ = std::regex(regexString);
regexFlag_ = true;
}
}
else if (config["path_exempt"].isString())
{
exemptRegex_ = std::regex(config["path_exempt"].asString());
regexFlag_ = true;
}
else
{
LOG_ERROR << "path_exempt must be a string or string array!";
}
}
#ifdef DROGON_SPDLOG_SUPPORT #ifdef DROGON_SPDLOG_SUPPORT
auto logWithSpdlog = trantor::Logger::hasSpdLogSupport() && auto logWithSpdlog = trantor::Logger::hasSpdLogSupport() &&
config.get("use_spdlog", false).asBool(); config.get("use_spdlog", false).asBool();
@ -228,7 +268,17 @@ void AccessLogger::initAndStart(const Json::Value &config)
drogon::app().registerPreSendingAdvice( drogon::app().registerPreSendingAdvice(
[this](const drogon::HttpRequestPtr &req, [this](const drogon::HttpRequestPtr &req,
const drogon::HttpResponsePtr &resp) { const drogon::HttpResponsePtr &resp) {
if (regexFlag_)
{
if (!std::regex_match(req->path(), exemptRegex_))
{
logging(LOG_RAW_TO(logIndex_), req, resp); logging(LOG_RAW_TO(logIndex_), req, resp);
}
}
else
{
logging(LOG_RAW_TO(logIndex_), req, resp);
}
}); });
} }

View File

@ -383,7 +383,7 @@ static void loadApp(const Json::Value &app)
{ {
drogon::app().setMaxConnectionNumPerIP(maxConnsPerIP); drogon::app().setMaxConnectionNumPerIP(maxConnsPerIP);
} }
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
// dynamic views // dynamic views
auto enableDynamicViews = app.get("load_dynamic_views", false).asBool(); auto enableDynamicViews = app.get("load_dynamic_views", false).asBool();
if (enableDynamicViews) if (enableDynamicViews)
@ -524,6 +524,9 @@ static void loadApp(const Json::Value &app)
bool enableCompressedRequests = bool enableCompressedRequests =
app.get("enabled_compressed_request", false).asBool(); app.get("enabled_compressed_request", false).asBool();
drogon::app().enableCompressedRequest(enableCompressedRequests); drogon::app().enableCompressedRequest(enableCompressedRequests);
drogon::app().enableRequestStream(
app.get("enable_request_stream", false).asBool());
} }
static void loadDbClients(const Json::Value &dbClients) static void loadDbClients(const Json::Value &dbClients)

View File

@ -41,6 +41,11 @@ struct ControllerBinderBase
virtual void handleRequest( virtual void handleRequest(
const HttpRequestImplPtr &req, const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) const = 0; std::function<void(const HttpResponsePtr &)> &&callback) const = 0;
virtual bool isStreamHandler() const
{
return false;
}
}; };
struct RouteResult struct RouteResult

View File

@ -19,16 +19,18 @@ using namespace drogon;
std::string Cookie::cookieString() const std::string Cookie::cookieString() const
{ {
std::string ret = "Set-Cookie: "; constexpr std::string_view prefix = "Set-Cookie: ";
std::string ret;
// reserve space to reduce frequency allocation // reserve space to reduce frequency allocation
ret.reserve(ret.size() + key_.size() + value_.size() + 30); ret.reserve(prefix.size() + key_.size() + value_.size() + 30);
ret = prefix;
ret.append(key_).append("=").append(value_).append("; "); ret.append(key_).append("=").append(value_).append("; ");
if (expiresDate_.microSecondsSinceEpoch() != if (expiresDate_.microSecondsSinceEpoch() !=
(std::numeric_limits<int64_t>::max)() && (std::numeric_limits<int64_t>::max)() &&
expiresDate_.microSecondsSinceEpoch() >= 0) expiresDate_.microSecondsSinceEpoch() >= 0)
{ {
ret.append("Expires=") ret.append("Expires=")
.append(utils::getHttpFullDate(expiresDate_)) .append(utils::getHttpFullDateStr(expiresDate_))
.append("; "); .append("; ");
} }
if (maxAge_.has_value()) if (maxAge_.has_value())
@ -67,7 +69,7 @@ std::string Cookie::cookieString() const
ret.append("SameSite=Lax; "); ret.append("SameSite=Lax; ");
} }
} }
if (secure_ && sameSite_ != SameSite::kNone) if ((secure_ && sameSite_ != SameSite::kNone) || partitioned_)
{ {
ret.append("Secure; "); ret.append("Secure; ");
} }
@ -75,6 +77,10 @@ std::string Cookie::cookieString() const
{ {
ret.append("HttpOnly; "); ret.append("HttpOnly; ");
} }
if (partitioned_)
{
ret.append("Partitioned; ");
}
ret.resize(ret.length() - 2); // delete last semicolon ret.resize(ret.length() - 2); // delete last semicolon
ret.append("\r\n"); ret.append("\r\n");
return ret; return ret;

View File

@ -105,6 +105,17 @@ void Hodor::initAndStart(const Json::Value &config)
limitStrategies_.emplace_back(makeLimitStrategy(subLimit)); limitStrategies_.emplace_back(makeLimitStrategy(subLimit));
} }
} }
const Json::Value &trustIps = config["trust_ips"];
if (!trustIps.isNull() && !trustIps.isArray())
{
throw std::runtime_error("Invalid trusted_ips. Should be array.");
}
for (const auto &ipOrCidr : trustIps)
{
trustCIDRs_.emplace_back(ipOrCidr.asString());
}
app().registerPreHandlingAdvice([this](const drogon::HttpRequestPtr &req, app().registerPreHandlingAdvice([this](const drogon::HttpRequestPtr &req,
AdviceCallback &&acb, AdviceCallback &&acb,
AdviceChainCallback &&accb) { AdviceChainCallback &&accb) {
@ -119,9 +130,13 @@ void Hodor::shutdown()
bool Hodor::checkLimit(const drogon::HttpRequestPtr &req, bool Hodor::checkLimit(const drogon::HttpRequestPtr &req,
const LimitStrategy &strategy, const LimitStrategy &strategy,
const std::string &ip, const trantor::InetAddress &ip,
const std::optional<std::string> &userId) const std::optional<std::string> &userId)
{ {
if (RealIpResolver::matchCidr(ip, trustCIDRs_))
{
return true;
}
if (strategy.regexFlag) if (strategy.regexFlag)
{ {
if (!std::regex_match(req->path(), strategy.urlsRegex)) if (!std::regex_match(req->path(), strategy.urlsRegex))
@ -140,7 +155,7 @@ bool Hodor::checkLimit(const drogon::HttpRequestPtr &req,
{ {
RateLimiterPtr limiterPtr; RateLimiterPtr limiterPtr;
strategy.ipLimiterMapPtr->modify( strategy.ipLimiterMapPtr->modify(
ip, ip.toIpNetEndian(),
[this, &limiterPtr, &strategy](RateLimiterPtr &ptr) { [this, &limiterPtr, &strategy](RateLimiterPtr &ptr) {
if (!ptr) if (!ptr)
{ {
@ -207,10 +222,9 @@ void Hodor::onHttpRequest(const drogon::HttpRequestPtr &req,
drogon::AdviceCallback &&adviceCallback, drogon::AdviceCallback &&adviceCallback,
drogon::AdviceChainCallback &&chainCallback) drogon::AdviceChainCallback &&chainCallback)
{ {
auto ip = const trantor::InetAddress &ip =
(useRealIpResolver_ ? drogon::plugin::RealIpResolver::GetRealAddr(req) useRealIpResolver_ ? drogon::plugin::RealIpResolver::GetRealAddr(req)
: req->peerAddr()) : req->peerAddr();
.toIpNetEndian();
std::optional<std::string> userId; std::optional<std::string> userId;
if (userIdGetter_) if (userIdGetter_)
{ {

View File

@ -184,7 +184,7 @@ static void TERMFunction(int sig)
HttpAppFrameworkImpl::~HttpAppFrameworkImpl() noexcept HttpAppFrameworkImpl::~HttpAppFrameworkImpl() noexcept
{ {
// Destroy the following objects before the loop destruction // Destroy the following objects before the loop destruction
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
sharedLibManagerPtr_.reset(); sharedLibManagerPtr_.reset();
#endif #endif
sessionManagerPtr_.reset(); sessionManagerPtr_.reset();
@ -236,7 +236,7 @@ const std::string &HttpAppFrameworkImpl::getImplicitPage() const
{ {
return StaticFileRouter::instance().getImplicitPage(); return StaticFileRouter::instance().getImplicitPage();
} }
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
HttpAppFramework &HttpAppFrameworkImpl::enableDynamicViewsLoading( HttpAppFramework &HttpAppFrameworkImpl::enableDynamicViewsLoading(
const std::vector<std::string> &libPaths, const std::vector<std::string> &libPaths,
const std::string &outputPath) const std::string &outputPath)
@ -511,6 +511,12 @@ HttpAppFramework &HttpAppFrameworkImpl::setSSLFiles(const std::string &certPath,
return *this; return *this;
} }
HttpAppFramework &HttpAppFrameworkImpl::reloadSSLFiles()
{
listenerManagerPtr_->reloadSSLFiles();
return *this;
}
void HttpAppFrameworkImpl::run() void HttpAppFrameworkImpl::run()
{ {
if (!getLoop()->isInLoopThread()) if (!getLoop()->isInLoopThread())
@ -593,7 +599,7 @@ void HttpAppFrameworkImpl::run()
LOG_INFO << "Start child process"; LOG_INFO << "Start child process";
} }
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
if (!libFilePaths_.empty()) if (!libFilePaths_.empty())
{ {
sharedLibManagerPtr_ = sharedLibManagerPtr_ =
@ -1027,7 +1033,7 @@ HttpAppFramework &HttpAppFrameworkImpl::createRedisClient(
void HttpAppFrameworkImpl::quit() void HttpAppFrameworkImpl::quit()
{ {
if (getLoop()->isRunning()) if (getLoop()->isRunning() && running_.exchange(false))
{ {
getLoop()->queueInLoop([this]() { getLoop()->queueInLoop([this]() {
// Release members in the reverse order of initialization // Release members in the reverse order of initialization
@ -1038,7 +1044,6 @@ void HttpAppFrameworkImpl::quit()
pluginsManagerPtr_.reset(); pluginsManagerPtr_.reset();
redisClientManagerPtr_.reset(); redisClientManagerPtr_.reset();
dbClientManagerPtr_.reset(); dbClientManagerPtr_.reset();
running_ = false;
getLoop()->quit(); getLoop()->quit();
for (trantor::EventLoop *loop : ioLoopThreadPool_->getLoops()) for (trantor::EventLoop *loop : ioLoopThreadPool_->getLoops())
{ {
@ -1246,6 +1251,17 @@ int64_t HttpAppFrameworkImpl::getConnectionCount() const
return HttpConnectionLimit::instance().getConnectionNum(); return HttpConnectionLimit::instance().getConnectionNum();
} }
HttpAppFramework &HttpAppFrameworkImpl::enableRequestStream(bool enable)
{
enableRequestStream_ = enable;
return *this;
}
bool HttpAppFrameworkImpl::isRequestStreamEnabled() const
{
return enableRequestStream_;
}
// AOP registration methods // AOP registration methods
HttpAppFramework &HttpAppFrameworkImpl::registerNewConnectionAdvice( HttpAppFramework &HttpAppFrameworkImpl::registerNewConnectionAdvice(
@ -1336,3 +1352,24 @@ HttpAppFramework &HttpAppFrameworkImpl::registerPreSendingAdvice(
AopAdvice::instance().registerPreSendingAdvice(advice); AopAdvice::instance().registerPreSendingAdvice(advice);
return *this; return *this;
} }
HttpAppFramework &HttpAppFrameworkImpl::setBeforeListenSockOptCallback(
std::function<void(int)> cb)
{
listenerManagerPtr_->setBeforeListenSockOptCallback(std::move(cb));
return *this;
}
HttpAppFramework &HttpAppFrameworkImpl::setAfterAcceptSockOptCallback(
std::function<void(int)> cb)
{
listenerManagerPtr_->setAfterAcceptSockOptCallback(std::move(cb));
return *this;
}
HttpAppFramework &HttpAppFrameworkImpl::setConnectionCallback(
std::function<void(const trantor::TcpConnectionPtr &)> cb)
{
listenerManagerPtr_->setConnectionCallback(std::move(cb));
return *this;
}

View File

@ -84,6 +84,9 @@ class HttpAppFrameworkImpl final : public HttpAppFramework
override; override;
HttpAppFramework &setSSLFiles(const std::string &certPath, HttpAppFramework &setSSLFiles(const std::string &certPath,
const std::string &keyPath) override; const std::string &keyPath) override;
HttpAppFramework &reloadSSLFiles() override;
void run() override; void run() override;
HttpAppFramework &registerWebSocketController( HttpAppFramework &registerWebSocketController(
const std::string &pathName, const std::string &pathName,
@ -264,7 +267,7 @@ class HttpAppFrameworkImpl final : public HttpAppFramework
HttpAppFramework &setUploadPath(const std::string &uploadPath) override; HttpAppFramework &setUploadPath(const std::string &uploadPath) override;
HttpAppFramework &setFileTypes( HttpAppFramework &setFileTypes(
const std::vector<std::string> &types) override; const std::vector<std::string> &types) override;
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
HttpAppFramework &enableDynamicViewsLoading( HttpAppFramework &enableDynamicViewsLoading(
const std::vector<std::string> &libPaths, const std::vector<std::string> &libPaths,
const std::string &outputPath) override; const std::string &outputPath) override;
@ -658,6 +661,16 @@ class HttpAppFrameworkImpl final : public HttpAppFramework
HttpResponsePtr handleSessionForResponse(const HttpRequestImplPtr &req, HttpResponsePtr handleSessionForResponse(const HttpRequestImplPtr &req,
const HttpResponsePtr &resp); const HttpResponsePtr &resp);
HttpAppFramework &setBeforeListenSockOptCallback(
std::function<void(int)> cb) override;
HttpAppFramework &setAfterAcceptSockOptCallback(
std::function<void(int)> cb) override;
HttpAppFramework &setConnectionCallback(
std::function<void(const trantor::TcpConnectionPtr &)> cb) override;
HttpAppFramework &enableRequestStream(bool enable) override;
bool isRequestStreamEnabled() const override;
private: private:
void registerHttpController(const std::string &pathPattern, void registerHttpController(const std::string &pathPattern,
const internal::HttpBinderBasePtr &binder, const internal::HttpBinderBasePtr &binder,
@ -696,7 +709,7 @@ class HttpAppFrameworkImpl final : public HttpAppFramework
size_t threadNum_{1}; size_t threadNum_{1};
std::unique_ptr<trantor::EventLoopThreadPool> ioLoopThreadPool_; std::unique_ptr<trantor::EventLoopThreadPool> ioLoopThreadPool_;
#ifndef _WIN32 #if !defined(_WIN32) && !TARGET_OS_IOS
std::vector<std::string> libFilePaths_; std::vector<std::string> libFilePaths_;
std::string libFileOutputPath_; std::string libFileOutputPath_;
std::unique_ptr<SharedLibManager> sharedLibManagerPtr_; std::unique_ptr<SharedLibManager> sharedLibManagerPtr_;
@ -748,6 +761,8 @@ class HttpAppFrameworkImpl final : public HttpAppFramework
ExceptionHandler exceptionHandler_{defaultExceptionHandler}; ExceptionHandler exceptionHandler_{defaultExceptionHandler};
bool enableCompressedRequest_{false}; bool enableCompressedRequest_{false};
bool enableRequestStream_{false};
}; };
} // namespace drogon } // namespace drogon

View File

@ -561,10 +561,6 @@ void HttpClientImpl::handleResponse(
resp->brDecompress(); resp->brDecompress();
} }
#endif #endif
if (type.find("application/json") != std::string::npos)
{
resp->parseJson();
}
auto cb = std::move(reqAndCb); auto cb = std::move(reqAndCb);
pipeliningCallbacks_.pop(); pipeliningCallbacks_.pop();
handleCookies(resp); handleCookies(resp);

View File

@ -19,6 +19,9 @@
#include <trantor/net/EventLoop.h> #include <trantor/net/EventLoop.h>
#include <trantor/net/Resolver.h> #include <trantor/net/Resolver.h>
#include <trantor/net/TcpClient.h> #include <trantor/net/TcpClient.h>
#include <cstddef>
#include <functional>
#include <future>
#include <list> #include <list>
#include <mutex> #include <mutex>
#include <queue> #include <queue>
@ -115,6 +118,21 @@ class HttpClientImpl final : public HttpClient,
sockOptCallback_ = std::move(cb); sockOptCallback_ = std::move(cb);
} }
std::size_t requestsBufferSize() override
{
if (loop_->isInLoopThread())
{
return requestsBuffer_.size();
}
else
{
std::promise<std::size_t> bufferSize;
loop_->queueInLoop(
[&] { bufferSize.set_value(requestsBuffer_.size()); });
return bufferSize.get_future().get();
}
}
private: private:
std::shared_ptr<trantor::TcpClient> tcpClientPtr_; std::shared_ptr<trantor::TcpClient> tcpClientPtr_;
trantor::EventLoop *loop_; trantor::EventLoop *loop_;

View File

@ -39,6 +39,12 @@ class HttpControllerBinder : public ControllerBinderBase
const HttpRequestImplPtr &req, const HttpRequestImplPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) const override; std::function<void(const HttpResponsePtr &)> &&callback) const override;
bool isStreamHandler() const override
{
assert(binderPtr_);
return binderPtr_->isStreamHandler();
}
internal::HttpBinderBasePtr binderPtr_; internal::HttpBinderBasePtr binderPtr_;
std::vector<size_t> parameterPlaces_; std::vector<size_t> parameterPlaces_;
std::vector<std::pair<std::string, size_t>> queryParametersPlaces_; std::vector<std::pair<std::string, size_t>> queryParametersPlaces_;

View File

@ -96,13 +96,15 @@ void HttpRequestImpl::parseParameters() const
while (cpos < key.length() && while (cpos < key.length() &&
isspace(static_cast<unsigned char>(key[cpos]))) isspace(static_cast<unsigned char>(key[cpos])))
++cpos; ++cpos;
key = key.substr(cpos); key.remove_prefix(cpos);
auto pvalue = coo.substr(epos + 1); auto pvalue = coo.substr(epos + 1);
std::string pdecode = utils::urlDecode(pvalue); parameters_[utils::urlDecode(key)] = utils::urlDecode(pvalue);
std::string keydecode = utils::urlDecode(key);
parameters_[keydecode] = pdecode;
} }
value = value.substr(pos + 1); else
{
parameters_[utils::urlDecode(coo)];
}
value.remove_prefix(pos + 1);
} }
if (value.length() > 0) if (value.length() > 0)
{ {
@ -115,11 +117,13 @@ void HttpRequestImpl::parseParameters() const
while (cpos < key.length() && while (cpos < key.length() &&
isspace(static_cast<unsigned char>(key[cpos]))) isspace(static_cast<unsigned char>(key[cpos])))
++cpos; ++cpos;
key = key.substr(cpos); key.remove_prefix(cpos);
auto pvalue = coo.substr(epos + 1); auto pvalue = coo.substr(epos + 1);
std::string pdecode = utils::urlDecode(pvalue); parameters_[utils::urlDecode(key)] = utils::urlDecode(pvalue);
std::string keydecode = utils::urlDecode(key); }
parameters_[keydecode] = pdecode; else
{
parameters_[utils::urlDecode(coo)];
} }
} }
} }
@ -153,13 +157,15 @@ void HttpRequestImpl::parseParameters() const
while (cpos < key.length() && while (cpos < key.length() &&
isspace(static_cast<unsigned char>(key[cpos]))) isspace(static_cast<unsigned char>(key[cpos])))
++cpos; ++cpos;
key = key.substr(cpos); key.remove_prefix(cpos);
auto pvalue = coo.substr(epos + 1); auto pvalue = coo.substr(epos + 1);
std::string pdecode = utils::urlDecode(pvalue); parameters_[utils::urlDecode(key)] = utils::urlDecode(pvalue);
std::string keydecode = utils::urlDecode(key);
parameters_[keydecode] = pdecode;
} }
value = value.substr(pos + 1); else
{
parameters_[utils::urlDecode(coo)];
}
value.remove_prefix(pos + 1);
} }
if (value.length() > 0) if (value.length() > 0)
{ {
@ -172,11 +178,13 @@ void HttpRequestImpl::parseParameters() const
while (cpos < key.length() && while (cpos < key.length() &&
isspace(static_cast<unsigned char>(key[cpos]))) isspace(static_cast<unsigned char>(key[cpos])))
++cpos; ++cpos;
key = key.substr(cpos); key.remove_prefix(cpos);
auto pvalue = coo.substr(epos + 1); auto pvalue = coo.substr(epos + 1);
std::string pdecode = utils::urlDecode(pvalue); parameters_[utils::urlDecode(key)] = utils::urlDecode(pvalue);
std::string keydecode = utils::urlDecode(key); }
parameters_[keydecode] = pdecode; else
{
parameters_[utils::urlDecode(coo)];
} }
} }
} }
@ -567,6 +575,8 @@ void HttpRequestImpl::swap(HttpRequestImpl &that) noexcept
swap(query_, that.query_); swap(query_, that.query_);
swap(headers_, that.headers_); swap(headers_, that.headers_);
swap(cookies_, that.cookies_); swap(cookies_, that.cookies_);
swap(contentLengthHeaderValue_, that.contentLengthHeaderValue_);
swap(realContentLength_, that.realContentLength_);
swap(parameters_, that.parameters_); swap(parameters_, that.parameters_);
swap(jsonPtr_, that.jsonPtr_); swap(jsonPtr_, that.jsonPtr_);
swap(sessionPtr_, that.sessionPtr_); swap(sessionPtr_, that.sessionPtr_);
@ -584,6 +594,13 @@ void HttpRequestImpl::swap(HttpRequestImpl &that) noexcept
swap(flagForParsingContentType_, that.flagForParsingContentType_); swap(flagForParsingContentType_, that.flagForParsingContentType_);
swap(jsonParsingErrorPtr_, that.jsonParsingErrorPtr_); swap(jsonParsingErrorPtr_, that.jsonParsingErrorPtr_);
swap(routingParams_, that.routingParams_); swap(routingParams_, that.routingParams_);
// stream
swap(streamStatus_, that.streamStatus_);
swap(streamReaderPtr_, that.streamReaderPtr_);
swap(streamFinishCb_, that.streamFinishCb_);
swap(streamExceptionPtr_, that.streamExceptionPtr_);
swap(startProcessing_, that.startProcessing_);
swap(connPtr_, that.connPtr_);
} }
const char *HttpRequestImpl::versionString() const const char *HttpRequestImpl::versionString() const
@ -723,6 +740,11 @@ HttpRequestImpl::~HttpRequestImpl()
void HttpRequestImpl::reserveBodySize(size_t length) void HttpRequestImpl::reserveBodySize(size_t length)
{ {
assert(loop_->isInLoopThread());
if (cacheFilePtr_)
{
return;
}
if (length <= HttpAppFrameworkImpl::instance().getClientMaxMemoryBodySize()) if (length <= HttpAppFrameworkImpl::instance().getClientMaxMemoryBodySize())
{ {
content_.reserve(length); content_.reserve(length);
@ -736,7 +758,14 @@ void HttpRequestImpl::reserveBodySize(size_t length)
void HttpRequestImpl::appendToBody(const char *data, size_t length) void HttpRequestImpl::appendToBody(const char *data, size_t length)
{ {
if (cacheFilePtr_) assert(loop_->isInLoopThread());
realContentLength_ += length;
if (streamReaderPtr_)
{
assert(streamStatus_ == ReqStreamStatus::Open);
streamReaderPtr_->onStreamData(data, length);
}
else if (cacheFilePtr_)
{ {
cacheFilePtr_->append(data, length); cacheFilePtr_->append(data, length);
} }
@ -974,3 +1003,114 @@ StreamDecompressStatus HttpRequestImpl::decompressBodyGzip() noexcept
} }
return status; return status;
} }
void HttpRequestImpl::setStreamReader(RequestStreamReaderPtr reader)
{
assert(loop_->isInLoopThread());
assert(!streamReaderPtr_);
assert(streamStatus_ > ReqStreamStatus::None);
if (streamExceptionPtr_)
{
assert(streamStatus_ == ReqStreamStatus::Error);
reader->onStreamFinish(std::move(streamExceptionPtr_));
streamExceptionPtr_ = nullptr;
return;
}
// Consume already received body
if (cacheFilePtr_)
{
auto bodyPieceView = cacheFilePtr_->getStringView();
if (!bodyPieceView.empty())
reader->onStreamData(bodyPieceView.data(), bodyPieceView.length());
cacheFilePtr_.reset();
}
else if (!content_.empty())
{
reader->onStreamData(content_.data(), content_.length());
content_.clear();
}
if (streamStatus_ == ReqStreamStatus::Finish)
{
reader->onStreamFinish({});
}
else
{
streamReaderPtr_ = std::move(reader);
}
}
void HttpRequestImpl::streamStart()
{
assert(streamStatus_ == ReqStreamStatus::None);
streamStatus_ = ReqStreamStatus::Open;
}
void HttpRequestImpl::streamFinish()
{
assert(loop_->isInLoopThread());
assert(streamStatus_ == ReqStreamStatus::Open);
streamStatus_ = ReqStreamStatus::Finish;
if (streamFinishCb_)
{
auto cb = std::move(streamFinishCb_);
streamFinishCb_ = nullptr;
cb();
}
if (streamReaderPtr_)
{
streamReaderPtr_->onStreamFinish({});
streamReaderPtr_ = nullptr;
}
}
void HttpRequestImpl::streamError(std::exception_ptr ex)
{
// TODO: can we be sure that streamError() only be called once?
// If not, we could allow it to be called multiple times, and
// only handle the first one.
assert(loop_->isInLoopThread());
assert(streamStatus_ == ReqStreamStatus::Open);
streamStatus_ = ReqStreamStatus::Error;
if (streamReaderPtr_)
{
streamReaderPtr_->onStreamFinish(std::move(ex));
streamReaderPtr_ = nullptr;
}
else
{
streamExceptionPtr_ = std::move(ex);
}
if (streamFinishCb_)
{
auto cb = std::move(streamFinishCb_);
streamFinishCb_ = nullptr;
cb();
}
}
void HttpRequestImpl::waitForStreamFinish(std::function<void()> &&cb)
{
assert(loop_->isInLoopThread());
assert(streamStatus_ > ReqStreamStatus::None);
if (streamStatus_ <= ReqStreamStatus::Open)
{
assert(!streamFinishCb_); // should only be called once
streamFinishCb_ = std::move(cb);
}
else
{
cb();
}
}
void HttpRequestImpl::quitStreamMode()
{
assert(loop_->isInLoopThread());
assert(streamStatus_ >= ReqStreamStatus::Finish);
assert(!streamReaderPtr_);
streamStatus_ = ReqStreamStatus::None;
}

View File

@ -16,8 +16,10 @@
#include "HttpUtils.h" #include "HttpUtils.h"
#include "CacheFile.h" #include "CacheFile.h"
#include "impl_forwards.h"
#include <drogon/utils/Utilities.h> #include <drogon/utils/Utilities.h>
#include <drogon/HttpRequest.h> #include <drogon/HttpRequest.h>
#include <drogon/RequestStream.h>
#include <drogon/utils/Utilities.h> #include <drogon/utils/Utilities.h>
#include <trantor/net/EventLoop.h> #include <trantor/net/EventLoop.h>
#include <trantor/net/InetAddress.h> #include <trantor/net/InetAddress.h>
@ -25,9 +27,12 @@
#include <trantor/utils/Logger.h> #include <trantor/utils/Logger.h>
#include <trantor/utils/MsgBuffer.h> #include <trantor/utils/MsgBuffer.h>
#include <trantor/utils/NonCopyable.h> #include <trantor/utils/NonCopyable.h>
#include <trantor/net/TcpConnection.h>
#include <algorithm> #include <algorithm>
#include <functional>
#include <memory>
#include <string> #include <string>
#include <thread> #include <future>
#include <unordered_map> #include <unordered_map>
#include <assert.h> #include <assert.h>
#include <stdio.h> #include <stdio.h>
@ -42,6 +47,14 @@ enum class StreamDecompressStatus
Ok Ok
}; };
enum class ReqStreamStatus
{
None = 0,
Open = 1,
Finish = 2,
Error = 3
};
class HttpRequestImpl : public HttpRequest class HttpRequestImpl : public HttpRequest
{ {
public: public:
@ -60,6 +73,8 @@ class HttpRequestImpl : public HttpRequest
flagForParsingJson_ = false; flagForParsingJson_ = false;
headers_.clear(); headers_.clear();
cookies_.clear(); cookies_.clear();
contentLengthHeaderValue_.reset();
realContentLength_ = 0;
flagForParsingParameters_ = false; flagForParsingParameters_ = false;
path_.clear(); path_.clear();
originalPath_.clear(); originalPath_.clear();
@ -80,6 +95,13 @@ class HttpRequestImpl : public HttpRequest
jsonParsingErrorPtr_.reset(); jsonParsingErrorPtr_.reset();
peerCertificate_.reset(); peerCertificate_.reset();
routingParams_.clear(); routingParams_.clear();
// stream
streamStatus_ = ReqStreamStatus::None;
streamReaderPtr_.reset();
streamFinishCb_ = nullptr;
streamExceptionPtr_ = nullptr;
startProcessing_ = false;
connPtr_.reset();
} }
trantor::EventLoop *getLoop() trantor::EventLoop *getLoop()
@ -207,6 +229,10 @@ class HttpRequestImpl : public HttpRequest
std::string_view bodyView() const std::string_view bodyView() const
{ {
if (isStreamMode())
{
return emptySv_;
}
if (cacheFilePtr_) if (cacheFilePtr_)
{ {
return cacheFilePtr_->getStringView(); return cacheFilePtr_->getStringView();
@ -216,6 +242,10 @@ class HttpRequestImpl : public HttpRequest
const char *bodyData() const override const char *bodyData() const override
{ {
if (isStreamMode())
{
return emptySv_.data();
}
if (cacheFilePtr_) if (cacheFilePtr_)
{ {
return cacheFilePtr_->getStringView().data(); return cacheFilePtr_->getStringView().data();
@ -225,6 +255,10 @@ class HttpRequestImpl : public HttpRequest
size_t bodyLength() const override size_t bodyLength() const override
{ {
if (isStreamMode())
{
return emptySv_.length();
}
if (cacheFilePtr_) if (cacheFilePtr_)
{ {
return cacheFilePtr_->getStringView().length(); return cacheFilePtr_->getStringView().length();
@ -243,6 +277,10 @@ class HttpRequestImpl : public HttpRequest
std::string_view contentView() const std::string_view contentView() const
{ {
if (isStreamMode())
{
return emptySv_;
}
if (cacheFilePtr_) if (cacheFilePtr_)
return cacheFilePtr_->getStringView(); return cacheFilePtr_->getStringView();
return content_; return content_;
@ -293,6 +331,11 @@ class HttpRequestImpl : public HttpRequest
peerCertificate_ = cert; peerCertificate_ = cert;
} }
void setConnectionPtr(const std::shared_ptr<trantor::TcpConnection> &ptr)
{
connPtr_ = ptr;
}
void addHeader(const char *start, const char *colon, const char *end); void addHeader(const char *start, const char *colon, const char *end);
void removeHeader(std::string key) override void removeHeader(std::string key) override
@ -349,6 +392,16 @@ class HttpRequestImpl : public HttpRequest
return cookies_; return cookies_;
} }
std::optional<size_t> getContentLengthHeaderValue() const
{
return contentLengthHeaderValue_;
}
size_t realContentLength() const override
{
return realContentLength_;
}
void setParameter(const std::string &key, const std::string &value) override void setParameter(const std::string &key, const std::string &value) override
{ {
flagForParsingParameters_ = true; flagForParsingParameters_ = true;
@ -395,9 +448,9 @@ class HttpRequestImpl : public HttpRequest
headers_[std::move(field)] = std::move(value); headers_[std::move(field)] = std::move(value);
} }
void addCookie(const std::string &key, const std::string &value) override void addCookie(std::string key, std::string value) override
{ {
cookies_[key] = value; cookies_[std::move(key)] = std::move(value);
} }
void setPassThrough(bool flag) override void setPassThrough(bool flag) override
@ -511,6 +564,21 @@ class HttpRequestImpl : public HttpRequest
return keepAlive_; return keepAlive_;
} }
bool connected() const noexcept override
{
if (auto conn = connPtr_.lock())
{
return conn->connected();
}
return false;
}
const std::weak_ptr<trantor::TcpConnection> &getConnectionPtr()
const noexcept override
{
return connPtr_;
}
bool isOnSecureConnection() const noexcept override bool isOnSecureConnection() const noexcept override
{ {
return isOnSecureConnection_; return isOnSecureConnection_;
@ -526,7 +594,36 @@ class HttpRequestImpl : public HttpRequest
StreamDecompressStatus decompressBody(); StreamDecompressStatus decompressBody();
~HttpRequestImpl(); // Stream mode api
ReqStreamStatus streamStatus() const
{
return streamStatus_;
}
bool isStreamMode() const
{
return streamStatus_ > ReqStreamStatus::None;
}
void streamStart();
void streamFinish();
void streamError(std::exception_ptr ex);
void setStreamReader(RequestStreamReaderPtr reader);
void waitForStreamFinish(std::function<void()> &&cb);
void quitStreamMode();
void startProcessing()
{
startProcessing_ = true;
}
bool isProcessingStarted() const
{
return startProcessing_;
}
~HttpRequestImpl() override;
protected: protected:
friend class HttpRequest; friend class HttpRequest;
@ -592,18 +689,26 @@ class HttpRequestImpl : public HttpRequest
StreamDecompressStatus decompressBodyBrotli() noexcept; StreamDecompressStatus decompressBodyBrotli() noexcept;
#endif #endif
StreamDecompressStatus decompressBodyGzip() noexcept; StreamDecompressStatus decompressBodyGzip() noexcept;
static constexpr const std::string_view emptySv_{""};
mutable bool flagForParsingParameters_{false}; mutable bool flagForParsingParameters_{false};
mutable bool flagForParsingJson_{false}; mutable bool flagForParsingJson_{false};
HttpMethod method_{Invalid}; HttpMethod method_{Invalid};
HttpMethod previousMethod_{Invalid}; HttpMethod previousMethod_{Invalid};
Version version_{Version::kUnknown}; Version version_{Version::kUnknown};
std::string path_; std::string path_;
/// Contains the encoded `path_` if and only if `path_` is set in encoded
/// form. If path is in a normal form and needed no decoding, then this will
/// be empty, as we do not need to store a duplicate.
std::string originalPath_; std::string originalPath_;
bool pathEncode_{true}; bool pathEncode_{true};
std::string_view matchedPathPattern_{""}; std::string_view matchedPathPattern_{""};
std::string query_; std::string query_;
SafeStringMap<std::string> headers_; SafeStringMap<std::string> headers_;
SafeStringMap<std::string> cookies_; SafeStringMap<std::string> cookies_;
std::optional<size_t> contentLengthHeaderValue_;
size_t realContentLength_{0};
mutable SafeStringMap<std::string> parameters_; mutable SafeStringMap<std::string> parameters_;
mutable std::shared_ptr<Json::Value> jsonPtr_; mutable std::shared_ptr<Json::Value> jsonPtr_;
SessionPtr sessionPtr_; SessionPtr sessionPtr_;
@ -620,6 +725,13 @@ class HttpRequestImpl : public HttpRequest
bool passThrough_{false}; bool passThrough_{false};
std::vector<std::string> routingParams_; std::vector<std::string> routingParams_;
ReqStreamStatus streamStatus_{ReqStreamStatus::None};
std::function<void()> streamFinishCb_;
RequestStreamReaderPtr streamReaderPtr_;
std::exception_ptr streamExceptionPtr_;
bool startProcessing_{false};
std::weak_ptr<trantor::TcpConnection> connPtr_;
protected: protected:
std::string content_; std::string content_;
trantor::EventLoop *loop_; trantor::EventLoop *loop_;

View File

@ -36,19 +36,6 @@ HttpRequestParser::HttpRequestParser(const trantor::TcpConnectionPtr &connPtr)
{ {
} }
void HttpRequestParser::shutdownConnection(HttpStatusCode code)
{
auto connPtr = conn_.lock();
if (connPtr)
{
connPtr->send(utils::formattedString(
"HTTP/1.1 %d %s\r\nConnection: close\r\n\r\n",
code,
statusCodeToString(code).data()));
connPtr->shutdown();
}
}
bool HttpRequestParser::processRequestLine(const char *begin, const char *end) bool HttpRequestParser::processRequestLine(const char *begin, const char *end)
{ {
bool succeed = false; bool succeed = false;
@ -130,7 +117,7 @@ HttpRequestImplPtr HttpRequestParser::makeRequestForPool(HttpRequestImpl *ptr)
void HttpRequestParser::reset() void HttpRequestParser::reset()
{ {
assert(loop_->isInLoopThread()); assert(loop_->isInLoopThread());
currentContentLength_ = 0; remainContentLength_ = 0;
status_ = HttpRequestParseStatus::kExpectMethod; status_ = HttpRequestParseStatus::kExpectMethod;
if (requestsPool_.empty()) if (requestsPool_.empty())
{ {
@ -146,9 +133,12 @@ void HttpRequestParser::reset()
} }
/** /**
* @return return -1 if encounters any error in request * @return return -HttpStatusCode if encounters any http errors in request
* @return return -1 if encounters any other errors in request
* @return return 0 if request is not ready * @return return 0 if request is not ready
* @return return 1 if request is ready * @return return 1 if request is ready
* @return return 2 if request is ready and entering stream mode
* @return return 3 if request header is ready and entering stream mode
*/ */
int HttpRequestParser::parseRequest(MsgBuffer *buf) int HttpRequestParser::parseRequest(MsgBuffer *buf)
{ {
@ -166,18 +156,14 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
{ {
if (buf->readableBytes() > METHOD_MAX_LEN) if (buf->readableBytes() > METHOD_MAX_LEN)
{ {
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
return 0; return 0;
} }
// try read method // try read method
if (!request_->setMethod(buf->peek(), space)) if (!request_->setMethod(buf->peek(), space))
{ {
buf->retrieveAll(); return -k405MethodNotAllowed;
shutdownConnection(k405MethodNotAllowed);
return -1;
} }
status_ = HttpRequestParseStatus::kExpectRequestLine; status_ = HttpRequestParseStatus::kExpectRequestLine;
buf->retrieveUntil(space + 1); buf->retrieveUntil(space + 1);
@ -193,18 +179,14 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
/// The limit for request line is 64K bytes. response /// The limit for request line is 64K bytes. response
/// k414RequestURITooLarge /// k414RequestURITooLarge
/// TODO: Make this configurable? /// TODO: Make this configurable?
buf->retrieveAll(); return -k414RequestURITooLarge;
shutdownConnection(k414RequestURITooLarge);
return -1;
} }
return 0; return 0;
} }
if (!processRequestLine(buf->peek(), crlf)) if (!processRequestLine(buf->peek(), crlf))
{ {
// error // error
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
buf->retrieveUntil(crlf + CRLF_LEN); buf->retrieveUntil(crlf + CRLF_LEN);
status_ = HttpRequestParseStatus::kExpectHeaders; status_ = HttpRequestParseStatus::kExpectHeaders;
@ -219,9 +201,7 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
{ {
/// The limit for every request header is 64K bytes; /// The limit for every request header is 64K bytes;
/// TODO: Make this configurable? /// TODO: Make this configurable?
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
return 0; return 0;
} }
@ -246,21 +226,18 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
{ {
try try
{ {
currentContentLength_ = remainContentLength_ =
static_cast<size_t>(std::stoull(len)); static_cast<size_t>(std::stoull(len));
} }
catch (...) catch (...)
{ {
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
if (currentContentLength_ == 0) request_->contentLengthHeaderValue_ = remainContentLength_;
if (remainContentLength_ == 0)
{ {
// content-length = 0, request is over. // content-length = 0, request is over.
status_ = HttpRequestParseStatus::kGotAll; status_ = HttpRequestParseStatus::kGotAll;
++requestsCounter_;
return 1;
} }
else else
{ {
@ -276,8 +253,6 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
// no content-length and no transfer-encoding, // no content-length and no transfer-encoding,
// request is over. // request is over.
status_ = HttpRequestParseStatus::kGotAll; status_ = HttpRequestParseStatus::kGotAll;
++requestsCounter_;
return 1;
} }
else if (encode == "chunked") else if (encode == "chunked")
{ {
@ -285,43 +260,37 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
} }
else else
{ {
buf->retrieveAll(); return -k501NotImplemented;
shutdownConnection(k501NotImplemented);
return -1;
} }
} }
// Check max body size
if (remainContentLength_ >
HttpAppFrameworkImpl::instance().getClientMaxBodySize())
{
return -k413RequestEntityTooLarge;
}
// Check expect:100-continue
auto &expect = request_->expect(); auto &expect = request_->expect();
if (expect == "100-continue" && if (expect == "100-continue" &&
request_->getVersion() >= Version::kHttp11) request_->getVersion() >= Version::kHttp11)
{ {
if (currentContentLength_ == 0) if (remainContentLength_ == 0)
{ {
// error // error
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
else
{
// rfc2616-8.2.3 // rfc2616-8.2.3
auto connPtr = conn_.lock(); // TODO: consider adding an AOP for expect header
auto connPtr = conn_.lock(); // ugly
if (!connPtr) if (!connPtr)
{ {
return -1; return -1;
} }
auto resp = HttpResponse::newHttpResponse(); auto resp = HttpResponse::newHttpResponse();
if (currentContentLength_ >
HttpAppFrameworkImpl::instance().getClientMaxBodySize())
{
resp->setStatusCode(k413RequestEntityTooLarge);
auto httpString =
static_cast<HttpResponseImpl *>(resp.get())
->renderToBuffer();
reset();
connPtr->send(std::move(*httpString));
// TODO: missing logic here
}
else
{
resp->setStatusCode(k100Continue); resp->setStatusCode(k100Continue);
auto httpString = auto httpString =
static_cast<HttpResponseImpl *>(resp.get()) static_cast<HttpResponseImpl *>(resp.get())
@ -332,35 +301,50 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
else if (!expect.empty()) else if (!expect.empty())
{ {
LOG_WARN << "417ExpectationFailed for \"" << expect << "\""; LOG_WARN << "417ExpectationFailed for \"" << expect << "\"";
buf->retrieveAll(); return -k417ExpectationFailed;
shutdownConnection(k417ExpectationFailed);
return -1;
} }
else if (currentContentLength_ >
HttpAppFrameworkImpl::instance() assert(status_ == HttpRequestParseStatus::kGotAll ||
.getClientMaxBodySize()) status_ == HttpRequestParseStatus::kExpectBody ||
status_ == HttpRequestParseStatus::kExpectChunkLen);
if (app().isRequestStreamEnabled())
{ {
buf->retrieveAll(); request_->streamStart();
shutdownConnection(k413RequestEntityTooLarge); if (status_ == HttpRequestParseStatus::kGotAll)
return -1; {
++requestsCounter_;
return 2;
}
else
{
return 3;
}
}
// Reserve space for full body in non-stream mode.
// For stream mode requests that match a non-stream handler,
// we will reserve full body before waitForStreamFinish().
if (remainContentLength_)
{
request_->reserveBodySize(remainContentLength_);
} }
request_->reserveBodySize(currentContentLength_);
continue; continue;
} }
case HttpRequestParseStatus::kExpectBody: case HttpRequestParseStatus::kExpectBody:
{ {
size_t bytesToConsume = size_t bytesToConsume =
currentContentLength_ <= buf->readableBytes() remainContentLength_ <= buf->readableBytes()
? currentContentLength_ ? remainContentLength_
: buf->readableBytes(); : buf->readableBytes();
if (bytesToConsume) if (bytesToConsume)
{ {
request_->appendToBody(buf->peek(), bytesToConsume); request_->appendToBody(buf->peek(), bytesToConsume);
buf->retrieve(bytesToConsume); buf->retrieve(bytesToConsume);
currentContentLength_ -= bytesToConsume; remainContentLength_ -= bytesToConsume;
} }
if (currentContentLength_ == 0) if (remainContentLength_ == 0)
{ {
status_ = HttpRequestParseStatus::kGotAll; status_ = HttpRequestParseStatus::kGotAll;
++requestsCounter_; ++requestsCounter_;
@ -376,9 +360,7 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
{ {
if (buf->readableBytes() > TRUNK_LEN_MAX_LEN + CRLF_LEN) if (buf->readableBytes() > TRUNK_LEN_MAX_LEN + CRLF_LEN)
{ {
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
return 0; return 0;
} }
@ -388,12 +370,10 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
currentChunkLength_ = strtol(len.c_str(), &end, 16); currentChunkLength_ = strtol(len.c_str(), &end, 16);
if (currentChunkLength_ != 0) if (currentChunkLength_ != 0)
{ {
if (currentChunkLength_ + currentContentLength_ > if (currentChunkLength_ + remainContentLength_ >
HttpAppFrameworkImpl::instance().getClientMaxBodySize()) HttpAppFrameworkImpl::instance().getClientMaxBodySize())
{ {
buf->retrieveAll(); return -k413RequestEntityTooLarge;
shutdownConnection(k413RequestEntityTooLarge);
return -1;
} }
status_ = HttpRequestParseStatus::kExpectChunkBody; status_ = HttpRequestParseStatus::kExpectChunkBody;
} }
@ -414,13 +394,11 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
*(buf->peek() + currentChunkLength_ + 1) != '\n') *(buf->peek() + currentChunkLength_ + 1) != '\n')
{ {
// error! // error!
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
request_->appendToBody(buf->peek(), currentChunkLength_); request_->appendToBody(buf->peek(), currentChunkLength_);
buf->retrieve(currentChunkLength_ + CRLF_LEN); buf->retrieve(currentChunkLength_ + CRLF_LEN);
currentContentLength_ += currentChunkLength_; remainContentLength_ += currentChunkLength_;
currentChunkLength_ = 0; currentChunkLength_ = 0;
status_ = HttpRequestParseStatus::kExpectChunkLen; status_ = HttpRequestParseStatus::kExpectChunkLen;
continue; continue;
@ -435,25 +413,44 @@ int HttpRequestParser::parseRequest(MsgBuffer *buf)
if (*(buf->peek()) != '\r' || *(buf->peek() + 1) != '\n') if (*(buf->peek()) != '\r' || *(buf->peek() + 1) != '\n')
{ {
// error! // error!
buf->retrieveAll(); return -k400BadRequest;
shutdownConnection(k400BadRequest);
return -1;
} }
buf->retrieve(CRLF_LEN); buf->retrieve(CRLF_LEN);
status_ = HttpRequestParseStatus::kGotAll;
if (!request_->isStreamMode())
{
// Previously we only have non-stream mode, drogon handled
// chunked encoding internally, and give user a regular
// request as if it has a content-length header.
//
// We have to keep compatibility for non-stream mode.
//
// But I don't think it's a good implementation. We should
// instead add an api to access real content-length of
// requests.
// Now HttpRequest::realContentLength() is added, and user
// should no longer parse content-length header by
// themselves.
//
// NOTE: request forward behavior may be infected in stream
// mode, we should check it out.
request_->addHeader("content-length", request_->addHeader("content-length",
std::to_string(request_->bodyLength())); std::to_string(
request_->realContentLength()));
request_->removeHeaderBy("transfer-encoding"); request_->removeHeaderBy("transfer-encoding");
}
status_ = HttpRequestParseStatus::kGotAll;
++requestsCounter_; ++requestsCounter_;
return 1; return 1;
} }
case HttpRequestParseStatus::kGotAll: case HttpRequestParseStatus::kGotAll:
{ {
++requestsCounter_;
return 1; return 1;
} }
} }
} }
return -1; return -1; // won't reach here, just to make compiler happy
} }
void HttpRequestParser::pushRequestToPipelining(const HttpRequestPtr &req, void HttpRequestParser::pushRequestToPipelining(const HttpRequestPtr &req,

View File

@ -43,7 +43,6 @@ class HttpRequestParser : public trantor::NonCopyable,
explicit HttpRequestParser(const trantor::TcpConnectionPtr &connPtr); explicit HttpRequestParser(const trantor::TcpConnectionPtr &connPtr);
// return false if any error
int parseRequest(trantor::MsgBuffer *buf); int parseRequest(trantor::MsgBuffer *buf);
bool gotAll() const bool gotAll() const
@ -138,7 +137,6 @@ class HttpRequestParser : public trantor::NonCopyable,
private: private:
HttpRequestImplPtr makeRequestForPool(HttpRequestImpl *p); HttpRequestImplPtr makeRequestForPool(HttpRequestImpl *p);
void shutdownConnection(HttpStatusCode code);
bool processRequestLine(const char *begin, const char *end); bool processRequestLine(const char *begin, const char *end);
HttpRequestParseStatus status_; HttpRequestParseStatus status_;
trantor::EventLoop *loop_; trantor::EventLoop *loop_;
@ -156,7 +154,7 @@ class HttpRequestParser : public trantor::NonCopyable,
std::unique_ptr<std::vector<HttpRequestImplPtr>> requestBuffer_; std::unique_ptr<std::vector<HttpRequestImplPtr>> requestBuffer_;
std::vector<HttpRequestImplPtr> requestsPool_; std::vector<HttpRequestImplPtr> requestsPool_;
size_t currentChunkLength_{0}; size_t currentChunkLength_{0};
size_t currentContentLength_{0}; size_t remainContentLength_{0};
}; };
} // namespace drogon } // namespace drogon

View File

@ -516,6 +516,7 @@ void HttpResponseImpl::makeHeaderString(trantor::MsgBuffer &buffer)
statusCode_); statusCode_);
} }
} }
buffer.hasWritten(len); buffer.hasWritten(len);
if (!statusMessage_.empty()) if (!statusMessage_.empty())
@ -525,7 +526,18 @@ void HttpResponseImpl::makeHeaderString(trantor::MsgBuffer &buffer)
if (!passThrough_) if (!passThrough_)
{ {
buffer.ensureWritableBytes(64); buffer.ensureWritableBytes(64);
if (streamCallback_ || asyncStreamCallback_) if (!contentLengthIsAllowed())
{
len = 0;
if ((bodyPtr_ && bodyPtr_->length() > 0) ||
!sendfileName_.empty() || streamCallback_ ||
asyncStreamCallback_)
{
LOG_ERROR << "The body should be empty when the content-length "
"is not allowed!";
}
}
else if (streamCallback_ || asyncStreamCallback_)
{ {
// When the headers are created, it is time to set the transfer // When the headers are created, it is time to set the transfer
// encoding to chunked if the contents size is not specified // encoding to chunked if the contents size is not specified
@ -620,15 +632,14 @@ void HttpResponseImpl::renderToBuffer(trantor::MsgBuffer &buffer)
drogon::HttpAppFrameworkImpl::instance().sendDateHeader()) drogon::HttpAppFrameworkImpl::instance().sendDateHeader())
{ {
buffer.append("date: "); buffer.append("date: ");
buffer.append(utils::getHttpFullDate(trantor::Date::date()), buffer.append(utils::getHttpFullDateStr(trantor::Date::date()));
httpFullDateStringLength);
buffer.append("\r\n\r\n"); buffer.append("\r\n\r\n");
} }
else else
{ {
buffer.append("\r\n"); buffer.append("\r\n");
} }
if (bodyPtr_) if (bodyPtr_ && contentLengthIsAllowed())
buffer.append(bodyPtr_->data(), bodyPtr_->length()); buffer.append(bodyPtr_->data(), bodyPtr_->length());
} }
@ -694,8 +705,7 @@ std::shared_ptr<trantor::MsgBuffer> HttpResponseImpl::renderToBuffer()
{ {
httpString->append("date: "); httpString->append("date: ");
auto datePos = httpString->readableBytes(); auto datePos = httpString->readableBytes();
httpString->append(utils::getHttpFullDate(trantor::Date::date()), httpString->append(utils::getHttpFullDateStr(trantor::Date::date()));
httpFullDateStringLength);
httpString->append("\r\n\r\n"); httpString->append("\r\n\r\n");
datePos_ = datePos; datePos_ = datePos;
} }
@ -854,7 +864,7 @@ void HttpResponseImpl::addHeader(const char *start,
} }
if (!cookie.key().empty()) if (!cookie.key().empty())
{ {
cookies_[cookie.key()] = cookie; cookies_[cookie.key()] = std::move(cookie);
} }
} }
else else
@ -957,7 +967,8 @@ bool HttpResponseImpl::shouldBeCompressed() const
{ {
if (streamCallback_ || asyncStreamCallback_ || !sendfileName_.empty() || if (streamCallback_ || asyncStreamCallback_ || !sendfileName_.empty() ||
contentType() >= CT_APPLICATION_OCTET_STREAM || contentType() >= CT_APPLICATION_OCTET_STREAM ||
getBody().length() < 1024 || !(getHeaderBy("content-encoding").empty())) getBody().length() < 1024 ||
!(getHeaderBy("content-encoding").empty()) || !contentLengthIsAllowed())
{ {
return false; return false;
} }

View File

@ -402,6 +402,16 @@ class DROGON_EXPORT HttpResponseImpl : public HttpResponse
addHeader("content-length", std::to_string(bodyPtr_->length())); addHeader("content-length", std::to_string(bodyPtr_->length()));
} }
} }
bool contentLengthIsAllowed() const
{
int statusCode =
customStatusCode_ >= 0 ? customStatusCode_ : statusCode_;
// return false if status code is 1xx or 204
return (statusCode >= k200OK || statusCode < k100Continue) &&
statusCode != k204NoContent;
}
#ifdef USE_BROTLI #ifdef USE_BROTLI
void brDecompress() void brDecompress()
{ {

View File

@ -30,6 +30,7 @@
#include "HttpControllersRouter.h" #include "HttpControllersRouter.h"
#include "StaticFileRouter.h" #include "StaticFileRouter.h"
#include "WebSocketConnectionImpl.h" #include "WebSocketConnectionImpl.h"
#include "impl_forwards.h"
#if COZ_PROFILING #if COZ_PROFILING
#include <coz.h> #include <coz.h>
@ -75,7 +76,12 @@ HttpServer::HttpServer(EventLoop *loop,
: server_(loop, listenAddr, std::move(name), true, app().reusePort()) : server_(loop, listenAddr, std::move(name), true, app().reusePort())
#endif #endif
{ {
server_.setConnectionCallback(onConnection); server_.setConnectionCallback(
[this](const trantor::TcpConnectionPtr &conn) {
onConnection(conn);
if (connectionCallback_)
connectionCallback_(conn);
});
server_.setRecvMessageCallback(onMessage); server_.setRecvMessageCallback(onMessage);
server_.kickoffIdleConnections( server_.kickoffIdleConnections(
HttpAppFrameworkImpl::instance().getIdleConnectionTimeout()); HttpAppFrameworkImpl::instance().getIdleConnectionTimeout());
@ -85,6 +91,14 @@ HttpServer::~HttpServer() = default;
void HttpServer::start() void HttpServer::start()
{ {
if (beforeListenSetSockOptCallback_)
{
server_.setBeforeListenSockOptCallback(beforeListenSetSockOptCallback_);
}
if (afterAcceptSetSockOptCallback_)
{
server_.setAfterAcceptSockOptCallback(afterAcceptSetSockOptCallback_);
}
LOG_TRACE << "HttpServer[" << server_.name() << "] starts listening on " LOG_TRACE << "HttpServer[" << server_.name() << "] starts listening on "
<< server_.ipPort(); << server_.ipPort();
server_.start(); server_.start();
@ -124,6 +138,13 @@ void HttpServer::onConnection(const TcpConnectionPtr &conn)
{ {
requestParser->webSocketConn()->onClose(); requestParser->webSocketConn()->onClose();
} }
else if (requestParser->requestImpl()->isStreamMode())
{
requestParser->requestImpl()->streamError(
std::make_exception_ptr(
StreamError(StreamErrorCode::kConnectionBroken,
"Connection closed")));
}
conn->clearContext(); conn->clearContext();
} }
} }
@ -154,28 +175,76 @@ void HttpServer::onMessage(const TcpConnectionPtr &conn, MsgBuffer *buf)
buf->retrieveAll(); buf->retrieveAll();
return; return;
} }
auto &req = requestParser->requestImpl();
// if stream mode enabled, parseRequest() may return >0 multiple times
// for the same request
int parseRes = requestParser->parseRequest(buf); int parseRes = requestParser->parseRequest(buf);
if (parseRes < 0) if (parseRes < 0)
{ {
if (req->isStreamMode() && req->isProcessingStarted())
{
// After entering stream mode, if request matches a non-stream
// handler, stream error would be intercepted by the
// `waitForStreamFinish()` call.
// If request matches a stream handler, stream error should be
// captured by user provided StreamReader, and response should
// also be sent by user.
req->streamError(std::make_exception_ptr(
StreamError(StreamErrorCode::kBadRequest, "Bad request")));
}
else if (parseRes != -1)
{
// In non-stream mode, request won't be process until it's fully
// parsed. To keep the old behavior, we send response directly
// through conn. (This response won't go through pre-sending
// aop, maybe we should change this behavior).
auto code = static_cast<HttpStatusCode>(-parseRes);
conn->send(utils::formattedString(
"HTTP/1.1 %d %s\r\nConnection: close\r\n\r\n",
code,
statusCodeToString(code).data()));
}
buf->retrieveAll();
// NOTE: should we call conn->forceClose() instead?
// Calling shutdown() handles socket more elegantly.
conn->shutdown();
// We have to call clearContext() here in order to ignore following
// illegal data from client
conn->clearContext();
requestParser->reset(); requestParser->reset();
conn->forceClose();
return; return;
} }
if (parseRes == 0) if (parseRes == 0)
{ {
break; break;
} }
auto &req = requestParser->requestImpl(); if (parseRes >= 2 || parseRes == 1 && !req->isStreamMode())
{
req->setPeerAddr(conn->peerAddr()); req->setPeerAddr(conn->peerAddr());
req->setLocalAddr(conn->localAddr()); req->setLocalAddr(conn->localAddr());
req->setCreationDate(trantor::Date::date()); req->setCreationDate(trantor::Date::date());
req->setSecure(conn->isSSLConnection()); req->setSecure(conn->isSSLConnection());
req->setPeerCertificate(conn->peerCertificate()); req->setPeerCertificate(conn->peerCertificate());
req->setConnectionPtr(conn);
// TODO: maybe call onRequests() directly in stream mode
requests.push_back(req); requests.push_back(req);
}
if (parseRes == 1 || parseRes == 2)
{
assert(requestParser->gotAll());
if (req->isStreamMode())
{
req->streamFinish();
}
requestParser->reset(); requestParser->reset();
} }
}
if (!requests.empty())
{
onRequests(conn, requests, requestParser); onRequests(conn, requests, requestParser);
requests.clear(); requests.clear();
}
} }
struct CallbackParamPack struct CallbackParamPack
@ -206,14 +275,14 @@ void HttpServer::onRequests(
const std::vector<HttpRequestImplPtr> &requests, const std::vector<HttpRequestImplPtr> &requests,
const std::shared_ptr<HttpRequestParser> &requestParser) const std::shared_ptr<HttpRequestParser> &requestParser)
{ {
if (requests.empty()) assert(!requests.empty());
return;
// will only be checked for the first request // will only be checked for the first request
if (requestParser->firstReq() && requests.size() == 1 && if (requestParser->firstReq() && requests.size() == 1 &&
isWebSocket(requests[0])) isWebSocket(requests[0]))
{ {
auto &req = requests[0]; auto &req = requests[0];
req->startProcessing();
if (passSyncAdvices(req, if (passSyncAdvices(req,
requestParser, requestParser,
false /* Not pipelined */, false /* Not pipelined */,
@ -244,7 +313,7 @@ void HttpServer::onRequests(
return; return;
} }
// flush response for not passing sync advices // flush response for not passing sync advice
if (conn->connected() && !requestParser->getResponseBuffer().empty()) if (conn->connected() && !requestParser->getResponseBuffer().empty())
{ {
sendResponses(conn, sendResponses(conn,
@ -279,6 +348,7 @@ void HttpServer::onRequests(
for (auto &req : requests) for (auto &req : requests)
{ {
req->startProcessing();
bool isHeadMethod = (req->method() == Head); bool isHeadMethod = (req->method() == Head);
if (isHeadMethod) if (isHeadMethod)
{ {
@ -413,6 +483,47 @@ void HttpServer::httpRequestRouting(
template <typename Pack> template <typename Pack>
void HttpServer::requestPostRouting(const HttpRequestImplPtr &req, Pack &&pack) void HttpServer::requestPostRouting(const HttpRequestImplPtr &req, Pack &&pack)
{ {
// Handle stream mode for non-stream handlers
if (req->streamStatus() >= ReqStreamStatus::Open &&
!pack.binderPtr->isStreamHandler())
{
LOG_TRACE << "Wait for request stream finish";
if (req->streamStatus() == ReqStreamStatus::Finish)
{
req->quitStreamMode();
}
else
{
auto contentLength = req->getContentLengthHeaderValue();
if (contentLength.has_value())
{
req->reserveBodySize(contentLength.value());
}
req->waitForStreamFinish([weakReq = std::weak_ptr(req),
pack =
std::forward<Pack>(pack)]() mutable {
auto req = weakReq.lock();
if (!req)
return;
if (req->streamStatus() == ReqStreamStatus::Finish)
{
req->quitStreamMode();
// call requestPostRouting again
requestPostRouting(req, std::forward<Pack>(pack));
return;
}
else
{
req->quitStreamMode();
LOG_ERROR << "Stop processing request due to stream error";
pack.callback(
app().getCustomErrorHandler()(k400BadRequest, req));
}
});
return;
}
}
// post-routing aop // post-routing aop
auto &aop = AopAdvice::instance(); auto &aop = AopAdvice::instance();
aop.passPostRoutingObservers(req); aop.passPostRoutingObservers(req);
@ -862,6 +973,8 @@ void HttpServer::sendResponse(const TcpConnectionPtr &conn,
{ {
auto httpString = respImplPtr->renderToBuffer(); auto httpString = respImplPtr->renderToBuffer();
conn->send(httpString); conn->send(httpString);
if (!respImplPtr->contentLengthIsAllowed())
return;
auto &asyncStreamCallback = respImplPtr->asyncStreamCallback(); auto &asyncStreamCallback = respImplPtr->asyncStreamCallback();
if (asyncStreamCallback) if (asyncStreamCallback)
{ {
@ -942,6 +1055,8 @@ void HttpServer::sendResponses(
{ {
// Not HEAD method // Not HEAD method
respImplPtr->renderToBuffer(buffer); respImplPtr->renderToBuffer(buffer);
if (!respImplPtr->contentLengthIsAllowed())
continue;
auto &asyncStreamCallback = respImplPtr->asyncStreamCallback(); auto &asyncStreamCallback = respImplPtr->asyncStreamCallback();
if (asyncStreamCallback) if (asyncStreamCallback)
{ {
@ -1087,7 +1202,7 @@ static inline HttpResponsePtr tryDecompressRequest(
* @brief Check request against each sync advice, generate response if request * @brief Check request against each sync advice, generate response if request
* is rejected by any one of them. * is rejected by any one of them.
* *
* @return true if all sync advices are passed. * @return true if all sync advice are passed.
* @return false if rejected by any sync advice. * @return false if rejected by any sync advice.
*/ */
static inline bool passSyncAdvices( static inline bool passSyncAdvices(

View File

@ -49,11 +49,32 @@ class HttpServer : trantor::NonCopyable
server_.enableSSL(std::move(policy)); server_.enableSSL(std::move(policy));
} }
void reloadSSL()
{
server_.reloadSSL();
}
const trantor::InetAddress &address() const const trantor::InetAddress &address() const
{ {
return server_.address(); return server_.address();
} }
void setBeforeListenSockOptCallback(std::function<void(int)> cb)
{
beforeListenSetSockOptCallback_ = std::move(cb);
}
void setAfterAcceptSockOptCallback(std::function<void(int)> cb)
{
afterAcceptSetSockOptCallback_ = std::move(cb);
}
void setConnectionCallback(
std::function<void(const trantor::TcpConnectionPtr &)> cb)
{
connectionCallback_ = std::move(cb);
}
private: private:
friend class HttpInternalForwardHelper; friend class HttpInternalForwardHelper;
@ -126,6 +147,10 @@ class HttpServer : trantor::NonCopyable
trantor::MsgBuffer &buffer); trantor::MsgBuffer &buffer);
trantor::TcpServer server_; trantor::TcpServer server_;
std::function<void(int)> beforeListenSetSockOptCallback_;
std::function<void(int)> afterAcceptSetSockOptCallback_;
std::function<void(const trantor::TcpConnectionPtr &)> connectionCallback_;
}; };
class HttpInternalForwardHelper class HttpInternalForwardHelper

View File

@ -111,6 +111,20 @@ void ListenerManager::createListeners(
std::make_shared<HttpServer>(ioLoops[i], std::make_shared<HttpServer>(ioLoops[i],
listenAddress, listenAddress,
"drogon"); "drogon");
if (beforeListenSetSockOptCallback_)
{
serverPtr->setBeforeListenSockOptCallback(
beforeListenSetSockOptCallback_);
}
if (afterAcceptSetSockOptCallback_)
{
serverPtr->setAfterAcceptSockOptCallback(
afterAcceptSetSockOptCallback_);
}
if (connectionCallback_)
{
serverPtr->setConnectionCallback(connectionCallback_);
}
if (listener.useSSL_ && utils::supportsTls()) if (listener.useSSL_ && utils::supportsTls())
{ {
@ -204,3 +218,11 @@ void ListenerManager::stopListening()
listeningThread_->wait(); listeningThread_->wait();
} }
} }
void ListenerManager::reloadSSLFiles()
{
for (auto &server : servers_)
{
server->reloadSSL();
}
}

View File

@ -51,6 +51,24 @@ class ListenerManager : public trantor::NonCopyable
void startListening(); void startListening();
void stopListening(); void stopListening();
void setBeforeListenSockOptCallback(std::function<void(int)> cb)
{
beforeListenSetSockOptCallback_ = std::move(cb);
}
void setAfterAcceptSockOptCallback(std::function<void(int)> cb)
{
afterAcceptSetSockOptCallback_ = std::move(cb);
}
void setConnectionCallback(
std::function<void(const trantor::TcpConnectionPtr &)> cb)
{
connectionCallback_ = std::move(cb);
}
void reloadSSLFiles();
private: private:
struct ListenerInfo struct ListenerInfo
{ {
@ -87,6 +105,9 @@ class ListenerManager : public trantor::NonCopyable
// should have value when and only when on OS that one port can only be // should have value when and only when on OS that one port can only be
// listened by one thread // listened by one thread
std::unique_ptr<trantor::EventLoopThread> listeningThread_; std::unique_ptr<trantor::EventLoopThread> listeningThread_;
std::function<void(int)> beforeListenSetSockOptCallback_;
std::function<void(int)> afterAcceptSetSockOptCallback_;
std::function<void(const trantor::TcpConnectionPtr &)> connectionCallback_;
}; };
} // namespace drogon } // namespace drogon

View File

@ -0,0 +1,356 @@
/**
*
* @file MultipartStreamParser.h
* @author Nitromelon
*
* Copyright 2024, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "MultipartStreamParser.h"
#include <cassert>
using namespace drogon;
static bool startsWith(const std::string_view &a, const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (a[i] != b[i])
{
return false;
}
}
return true;
}
static bool startsWithIgnoreCase(const std::string_view &a,
const std::string_view &b)
{
if (a.size() < b.size())
{
return false;
}
for (size_t i = 0; i < b.size(); i++)
{
if (::tolower(a[i]) != ::tolower(b[i]))
{
return false;
}
}
return true;
}
MultipartStreamParser::MultipartStreamParser(const std::string &contentType)
{
static const std::string_view multipart = "multipart/form-data";
static const std::string_view boundaryEq = "boundary=";
if (!startsWithIgnoreCase(contentType, multipart))
{
isValid_ = false;
return;
}
auto pos = contentType.find(boundaryEq, multipart.size());
if (pos == std::string::npos)
{
isValid_ = false;
return;
}
pos += boundaryEq.size();
size_t pos2;
if (contentType[pos] == '"')
{
++pos;
pos2 = contentType.find('"', pos);
}
else
{
pos2 = contentType.find(';', pos);
}
if (pos2 == std::string::npos)
pos2 = contentType.size();
boundary_ = contentType.substr(pos, pos2 - pos);
dashBoundaryCrlf_ = dash_ + boundary_ + crlf_;
crlfDashBoundary_ = crlf_ + dash_ + boundary_;
}
// TODO: same function in HttpRequestParser.cc
static std::pair<std::string_view, std::string_view> parseLine(
const char *begin,
const char *end)
{
auto p = begin;
while (p != end)
{
if (*p == ':')
{
if (p + 1 != end && *(p + 1) == ' ')
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 2, end - p - 2));
}
else
{
return std::make_pair(std::string_view(begin, p - begin),
std::string_view(p + 1, end - p - 1));
}
}
++p;
}
return std::make_pair(std::string_view(), std::string_view());
}
void drogon::MultipartStreamParser::parse(
const char *data,
size_t length,
const drogon::RequestStreamReader::MultipartHeaderCallback &headerCb,
const drogon::RequestStreamReader::StreamDataCallback &dataCb)
{
buffer_.append(data, length);
while (buffer_.size() > 0)
{
switch (status_)
{
case Status::kExpectFirstBoundary:
{
if (buffer_.size() < dashBoundaryCrlf_.size())
{
return;
}
std::string_view v = buffer_.view();
auto pos = v.find(dashBoundaryCrlf_);
// ignore everything before the first boundary
if (pos == std::string::npos)
{
buffer_.eraseFront(buffer_.size() -
dashBoundaryCrlf_.size());
return;
}
// found
buffer_.eraseFront(pos + dashBoundaryCrlf_.size());
status_ = Status::kExpectNewEntry;
continue;
}
case Status::kExpectNewEntry:
{
currentHeader_.name.clear();
currentHeader_.filename.clear();
currentHeader_.contentType.clear();
status_ = Status::kExpectHeader;
continue;
}
case Status::kExpectHeader:
{
std::string_view v = buffer_.view();
auto pos = v.find(crlf_);
if (pos == std::string::npos)
{
// same magic number in HttpRequestParser::parseRequest()
if (buffer_.size() > 60 * 1024)
{
isValid_ = false;
}
return; // header incomplete, wait for more data
}
// empty line
if (pos == 0)
{
buffer_.eraseFront(crlf_.size());
status_ = Status::kExpectBody;
headerCb(currentHeader_);
continue;
}
// found header line
auto [keyView, valueView] = parseLine(v.data(), v.data() + pos);
if (keyView.empty() || valueView.empty())
{
// Bad header
isValid_ = false;
return;
}
if (startsWithIgnoreCase(keyView, "content-type"))
{
currentHeader_.contentType = valueView;
}
else if (startsWithIgnoreCase(keyView, "content-disposition"))
{
static const std::string_view nameKey = "name=";
static const std::string_view fileNameKey = "filename=";
// Extract name
auto namePos = valueView.find(nameKey);
if (namePos == std::string::npos)
{
// name absent
isValid_ = false;
return;
}
namePos += nameKey.size();
size_t nameEnd;
if (valueView[namePos] == '"')
{
++namePos;
nameEnd = valueView.find('"', namePos);
}
else
{
nameEnd = valueView.find(';', namePos);
}
if (nameEnd == std::string::npos)
{
// name end not found
isValid_ = false;
return;
}
currentHeader_.name =
valueView.substr(namePos, nameEnd - namePos);
// Extract filename
auto fileNamePos = valueView.find(fileNameKey, nameEnd);
if (fileNamePos != std::string::npos)
{
fileNamePos += fileNameKey.size();
size_t fileNameEnd;
if (valueView[fileNamePos] == '"')
{
++fileNamePos;
fileNameEnd = valueView.find('"', fileNamePos);
}
else
{
fileNameEnd = valueView.find(';', fileNamePos);
}
currentHeader_.filename =
valueView.substr(fileNamePos,
fileNameEnd - fileNamePos);
}
}
// ignore other headers
buffer_.eraseFront(pos + crlf_.size());
continue;
}
case Status::kExpectBody:
{
if (buffer_.size() < crlfDashBoundary_.size())
{
return; // not enough data to check boundary
}
std::string_view v = buffer_.view();
auto pos = v.find(crlfDashBoundary_);
if (pos == std::string::npos)
{
// boundary not found, leave potential partial boundary
size_t len = v.size() - crlfDashBoundary_.size();
if (len > 0)
{
dataCb(v.data(), len);
buffer_.eraseFront(len);
}
return;
}
// found boundary
dataCb(v.data(), pos);
if (pos > 0)
{
dataCb(v.data() + pos, 0); // notify end of file
}
buffer_.eraseFront(pos + crlfDashBoundary_.size());
status_ = Status::kExpectEndOrNewEntry;
continue;
}
case Status::kExpectEndOrNewEntry:
{
std::string_view v = buffer_.view();
// Check new entry
if (v.size() < crlf_.size())
{
return;
}
if (startsWith(v, crlf_))
{
buffer_.eraseFront(crlf_.size());
status_ = Status::kExpectNewEntry;
continue;
}
// Check end
if (v.size() < dash_.size())
{
return;
}
if (startsWith(v, dash_))
{
isFinished_ = true;
buffer_.clear(); // ignore epilogue
return;
}
isValid_ = false;
return;
}
}
}
}
std::string_view MultipartStreamParser::Buffer::view() const
{
return {buffer_.data() + bufHead_, size()};
}
void MultipartStreamParser::Buffer::append(const char *data, size_t length)
{
size_t remainSize = size();
// Move existing data to the front
if (remainSize > 0 && bufHead_ > 0)
{
for (size_t i = 0; i < remainSize; i++)
{
buffer_[i] = buffer_[bufHead_ + i];
}
}
bufHead_ = 0;
bufTail_ = remainSize;
if (remainSize + length > buffer_.size())
{
buffer_.resize(remainSize + length);
}
for (size_t i = 0; i < length; ++i)
{
buffer_[bufTail_ + i] = data[i];
}
bufTail_ += length;
}
size_t MultipartStreamParser::Buffer::size() const
{
return bufTail_ - bufHead_;
}
void MultipartStreamParser::Buffer::eraseFront(size_t length)
{
assert(length <= size());
bufHead_ += length;
}
void MultipartStreamParser::Buffer::clear()
{
buffer_.clear();
bufHead_ = 0;
bufTail_ = 0;
}

View File

@ -0,0 +1,77 @@
/**
*
* @file MultipartStreamParser.h
* @author Nitromelon
*
* Copyright 2024, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#pragma once
#include <drogon/exports.h>
#include <drogon/RequestStream.h>
#include <string>
namespace drogon
{
class DROGON_EXPORT MultipartStreamParser
{
public:
MultipartStreamParser(const std::string &contentType);
void parse(const char *data,
size_t length,
const RequestStreamReader::MultipartHeaderCallback &headerCb,
const RequestStreamReader::StreamDataCallback &dataCb);
bool isFinished() const
{
return isFinished_;
}
bool isValid() const
{
return isValid_;
}
private:
const std::string dash_ = "--";
const std::string crlf_ = "\r\n";
std::string boundary_;
std::string dashBoundaryCrlf_;
std::string crlfDashBoundary_;
struct Buffer
{
public:
std::string_view view() const;
void append(const char *data, size_t length);
size_t size() const;
void eraseFront(size_t length);
void clear();
private:
std::string buffer_;
size_t bufHead_{0};
size_t bufTail_{0};
} buffer_;
enum class Status
{
kExpectFirstBoundary = 0,
kExpectNewEntry = 1,
kExpectHeader = 2,
kExpectBody = 3,
kExpectEndOrNewEntry = 4,
} status_{Status::kExpectFirstBoundary};
MultipartHeader currentHeader_;
bool isValid_{true};
bool isFinished_{false};
};
} // namespace drogon

View File

@ -12,7 +12,7 @@ using namespace drogon::plugin;
void PromExporter::initAndStart(const Json::Value &config) void PromExporter::initAndStart(const Json::Value &config)
{ {
path_ = config.get("path", path_).asString(); path_ = config.get("path", path_).asString();
LOG_ERROR << path_; LOG_TRACE << path_;
auto &app = drogon::app(); auto &app = drogon::app();
std::weak_ptr<PromExporter> weakPtr = shared_from_this(); std::weak_ptr<PromExporter> weakPtr = shared_from_this();
app.registerHandler( app.registerHandler(
@ -28,6 +28,7 @@ void PromExporter::initAndStart(const Json::Value &config)
} }
auto resp = HttpResponse::newHttpResponse(); auto resp = HttpResponse::newHttpResponse();
resp->setBody(thisPtr->exportMetrics()); resp->setBody(thisPtr->exportMetrics());
resp->setContentTypeCode(CT_TEXT_PLAIN);
resp->setExpiredTime(5); resp->setExpiredTime(5);
callback(resp); callback(resp);
}, },
@ -118,19 +119,19 @@ static std::string exportCollector(
.append(collector->name()) .append(collector->name())
.append(" ") .append(" ")
.append(collector->help()) .append(collector->help())
.append("\r\n"); .append("\n");
res.append("# TYPE ") res.append("# TYPE ")
.append(collector->name()) .append(collector->name())
.append(" ") .append(" ")
.append(collector->type()) .append(collector->type())
.append("\r\n"); .append("\n");
for (auto const &sampleGroup : sampleGroups) for (auto const &sampleGroup : sampleGroups)
{ {
auto const &metricPtr = sampleGroup.metric; auto const &metricPtr = sampleGroup.metric;
auto const &samples = sampleGroup.samples; auto const &samples = sampleGroup.samples;
for (auto &sample : samples) for (auto &sample : samples)
{ {
res.append(metricPtr->name()); res.append(sample.name);
if (!sample.exLabels.empty() || !metricPtr->labels().empty()) if (!sample.exLabels.empty() || !metricPtr->labels().empty())
{ {
res.append("{"); res.append("{");
@ -157,11 +158,11 @@ static std::string exportCollector(
res.append(" ") res.append(" ")
.append(std::to_string( .append(std::to_string(
sample.timestamp.microSecondsSinceEpoch() / 1000)) sample.timestamp.microSecondsSinceEpoch() / 1000))
.append("\r\n"); .append("\n");
} }
else else
{ {
res.append("\r\n"); res.append("\n");
} }
} }
} }

View File

@ -96,21 +96,20 @@ void RealIpResolver::initAndStart(const Json::Value &config)
} }
const Json::Value &trustIps = config["trust_ips"]; const Json::Value &trustIps = config["trust_ips"];
if (!trustIps.isArray()) if (!trustIps.isNull() && !trustIps.isArray())
{ {
throw std::runtime_error("Invalid trusted_ips. Should be array."); throw std::runtime_error("Invalid trusted_ips. Should be array.");
} }
for (const auto &elem : trustIps) for (const auto &ipOrCidr : trustIps)
{ {
std::string ipOrCidr = elem.asString(); trustCIDRs_.emplace_back(ipOrCidr.asString());
trustCIDRs_.emplace_back(ipOrCidr);
} }
drogon::app().registerPreRoutingAdvice([this](const HttpRequestPtr &req) { drogon::app().registerPreRoutingAdvice([this](const HttpRequestPtr &req) {
const auto &headers = req->headers(); const auto &headers = req->headers();
auto ipHeaderFind = headers.find(fromHeader_); auto ipHeaderFind = headers.find(fromHeader_);
const trantor::InetAddress &peerAddr = req->getPeerAddr(); const trantor::InetAddress &peerAddr = req->getPeerAddr();
if (ipHeaderFind == headers.end() || !matchCidr(peerAddr)) if (ipHeaderFind == headers.end() || !matchCidr(peerAddr, trustCIDRs_))
{ {
// Target header is empty, or // Target header is empty, or
// direct peer is already a non-proxy // direct peer is already a non-proxy
@ -139,7 +138,7 @@ void RealIpResolver::initAndStart(const Json::Value &config)
while (!(ip = parser.getNext()).empty()) while (!(ip = parser.getNext()).empty())
{ {
trantor::InetAddress addr = parseAddress(ip); trantor::InetAddress addr = parseAddress(ip);
if (addr.isUnspecified() || matchCidr(addr)) if (addr.isUnspecified() || matchCidr(addr, trustCIDRs_))
{ {
continue; continue;
} }
@ -177,9 +176,10 @@ const trantor::InetAddress &RealIpResolver::getRealAddr(
return attributesPtr->get<trantor::InetAddress>(attributeKey_); return attributesPtr->get<trantor::InetAddress>(attributeKey_);
} }
bool RealIpResolver::matchCidr(const trantor::InetAddress &addr) const bool RealIpResolver::matchCidr(const trantor::InetAddress &addr,
const CIDRs &trustCIDRs)
{ {
for (auto &cidr : trustCIDRs_) for (const auto &cidr : trustCIDRs)
{ {
if ((addr.ipNetEndian() & cidr.mask_) == cidr.addr_) if ((addr.ipNetEndian() & cidr.mask_) == cidr.addr_)
{ {

View File

@ -30,6 +30,10 @@ void Redirector::initAndStart(const Json::Value &config)
} }
std::string protocol, host; std::string protocol, host;
bool pathChanged{false}; bool pathChanged{false};
for (auto &handler : thisPtr->pathRewriteHandlers_)
{
pathChanged |= handler(req);
}
for (auto &handler : thisPtr->handlers_) for (auto &handler : thisPtr->handlers_)
{ {
if (!handler(req, protocol, host, pathChanged)) if (!handler(req, protocol, host, pathChanged))
@ -37,10 +41,6 @@ void Redirector::initAndStart(const Json::Value &config)
return HttpResponse::newNotFoundResponse(req); return HttpResponse::newNotFoundResponse(req);
} }
} }
for (auto &handler : thisPtr->pathRewriteHandlers_)
{
pathChanged |= handler(req);
}
if (!protocol.empty() || !host.empty() || pathChanged) if (!protocol.empty() || !host.empty() || pathChanged)
{ {
std::string url; std::string url;

225
lib/src/RequestStream.cc Normal file
View File

@ -0,0 +1,225 @@
/**
*
* @file RequestStream.cc
* @author Nitromelon
*
* Copyright 2024, Nitromelon. All rights reserved.
* https://github.com/drogonframework/drogon
* Use of this source code is governed by a MIT license
* that can be found in the License file.
*
* Drogon
*
*/
#include "MultipartStreamParser.h"
#include "HttpRequestImpl.h"
#include <drogon/RequestStream.h>
#include <variant>
namespace drogon
{
class RequestStreamImpl : public RequestStream
{
public:
RequestStreamImpl(const HttpRequestImplPtr &req) : weakReq_(req)
{
}
~RequestStreamImpl() override
{
if (isSet_.exchange(true))
{
return;
}
// Drop all data if no reader is set
if (auto req = weakReq_.lock())
{
setHandlerInLoop(req, RequestStreamReader::newNullReader());
}
}
void setStreamReader(RequestStreamReaderPtr reader) override
{
if (isSet_.exchange(true))
{
return;
}
if (auto req = weakReq_.lock())
{
setHandlerInLoop(req, std::move(reader));
}
}
void setHandlerInLoop(const HttpRequestImplPtr &req,
RequestStreamReaderPtr reader)
{
if (!req->isStreamMode())
{
return;
}
auto loop = req->getLoop();
if (loop->isInLoopThread())
{
req->setStreamReader(std::move(reader));
}
else
{
loop->queueInLoop([req, reader = std::move(reader)]() mutable {
req->setStreamReader(std::move(reader));
});
}
}
private:
std::weak_ptr<HttpRequestImpl> weakReq_;
std::atomic_bool isSet_{false};
};
namespace internal
{
RequestStreamPtr createRequestStream(const HttpRequestPtr &req)
{
auto reqImpl = std::static_pointer_cast<HttpRequestImpl>(req);
if (!reqImpl->isStreamMode())
{
return nullptr;
}
return std::make_shared<RequestStreamImpl>(
std::static_pointer_cast<HttpRequestImpl>(req));
}
} // namespace internal
/**
* A default implementation for convenience
*/
class DefaultStreamReader : public RequestStreamReader
{
public:
DefaultStreamReader(StreamDataCallback dataCb,
StreamFinishCallback finishCb)
: dataCb_(std::move(dataCb)), finishCb_(std::move(finishCb))
{
}
void onStreamData(const char *data, size_t length) override
{
dataCb_(data, length);
}
void onStreamFinish(std::exception_ptr ex) override
{
finishCb_(std::move(ex));
}
private:
StreamDataCallback dataCb_;
StreamFinishCallback finishCb_;
};
/**
* Drops all data
*/
class NullStreamReader : public RequestStreamReader
{
public:
void onStreamData(const char *, size_t length) override
{
}
void onStreamFinish(std::exception_ptr) override
{
}
};
/**
* Parse multipart data and return actual content
*/
class MultipartStreamReader : public RequestStreamReader
{
public:
MultipartStreamReader(const std::string &contentType,
MultipartHeaderCallback headerCb,
StreamDataCallback dataCb,
StreamFinishCallback finishCb)
: parser_(contentType),
headerCb_(std::move(headerCb)),
dataCb_(std::move(dataCb)),
finishCb_(std::move(finishCb))
{
}
void onStreamData(const char *data, size_t length) override
{
if (!parser_.isValid() || parser_.isFinished())
{
return;
}
parser_.parse(data, length, headerCb_, dataCb_);
if (!parser_.isValid())
{
// TODO: should we mix stream error and user error?
finishCb_(std::make_exception_ptr(
std::runtime_error("invalid multipart data")));
}
else if (parser_.isFinished())
{
finishCb_({});
}
}
void onStreamFinish(std::exception_ptr ex) override
{
if (!parser_.isValid() || parser_.isFinished())
{
return;
}
if (!ex)
{
finishCb_(std::make_exception_ptr(
std::runtime_error("incomplete multipart data")));
}
else
{
finishCb_(std::move(ex));
}
}
private:
MultipartStreamParser parser_;
MultipartHeaderCallback headerCb_;
StreamDataCallback dataCb_;
StreamFinishCallback finishCb_;
};
RequestStreamReaderPtr RequestStreamReader::newReader(
StreamDataCallback dataCb,
StreamFinishCallback finishCb)
{
return std::make_shared<DefaultStreamReader>(std::move(dataCb),
std::move(finishCb));
}
RequestStreamReaderPtr RequestStreamReader::newNullReader()
{
return std::make_shared<NullStreamReader>();
}
RequestStreamReaderPtr RequestStreamReader::newMultipartReader(
const HttpRequestPtr &req,
MultipartHeaderCallback headerCb,
StreamDataCallback dataCb,
StreamFinishCallback finishCb)
{
return std::make_shared<MultipartStreamReader>(req->getHeader(
"content-type"),
std::move(headerCb),
std::move(dataCb),
std::move(finishCb));
}
} // namespace drogon

View File

@ -44,7 +44,7 @@ void StaticFileRouter::init(const std::vector<trantor::EventLoop *> &ioLoops)
size_t i) { size_t i) {
assert(i == ioLoops[i]->index()); assert(i == ioLoops[i]->index());
mapPtr = std::make_unique<CacheMap<std::string, char>>(ioLoops[i], mapPtr = std::make_unique<CacheMap<std::string, char>>(ioLoops[i],
1.0, 1.0f,
4, 4,
50); 50);
}); });

View File

@ -33,6 +33,7 @@
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <mutex> #include <mutex>
#include <random>
#include <algorithm> #include <algorithm>
#include <array> #include <array>
#include <locale> #include <locale>
@ -83,12 +84,12 @@ namespace drogon
{ {
namespace utils namespace utils
{ {
static const std::string base64Chars = static constexpr std::string_view base64Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz"
"0123456789+/"; "0123456789+/";
static const std::string urlBase64Chars = static constexpr std::string_view urlBase64Chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ" "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz" "abcdefghijklmnopqrstuvwxyz"
"0123456789-_"; "0123456789-_";
@ -162,28 +163,16 @@ bool isBase64(std::string_view str)
std::string genRandomString(int length) std::string genRandomString(int length)
{ {
static const char char_space[] = static const std::string_view char_space =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
static std::once_flag once; std::uniform_int_distribution<size_t> dist(0, char_space.size() - 1);
static const size_t len = strlen(char_space); thread_local std::mt19937 rng(std::random_device{}());
static const int randMax = RAND_MAX - (RAND_MAX % len);
std::call_once(once, []() {
std::srand(static_cast<unsigned int>(time(nullptr)));
});
int i;
std::string str; std::string str;
str.resize(length); str.resize(length);
for (char &ch : str)
for (i = 0; i < length; ++i)
{ {
int x = std::rand(); ch = char_space[dist(rng)];
while (x >= randMax)
{
x = std::rand();
}
x = (x % len);
str[i] = char_space[x];
} }
return str; return str;
@ -440,33 +429,33 @@ std::string getUuid(bool lowercase)
#endif #endif
} }
std::string base64Encode(const unsigned char *bytes_to_encode, void base64Encode(const unsigned char *bytesToEncode,
size_t in_len, size_t inLen,
bool url_safe, unsigned char *outputBuffer,
bool urlSafe,
bool padded) bool padded)
{ {
std::string ret;
ret.reserve(base64EncodedLength(in_len, padded));
int i = 0; int i = 0;
unsigned char char_array_3[3]; unsigned char charArray3[3];
unsigned char char_array_4[4]; unsigned char charArray4[4];
const std::string &charSet = url_safe ? urlBase64Chars : base64Chars; const std::string_view charSet = urlSafe ? urlBase64Chars : base64Chars;
while (in_len--) size_t a = 0;
while (inLen--)
{ {
char_array_3[i++] = *(bytes_to_encode++); charArray3[i++] = *(bytesToEncode++);
if (i == 3) if (i == 3)
{ {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; charArray4[0] = (charArray3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + charArray4[1] =
((char_array_3[1] & 0xf0) >> 4); ((charArray3[0] & 0x03) << 4) + ((charArray3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + charArray4[2] =
((char_array_3[2] & 0xc0) >> 6); ((charArray3[1] & 0x0f) << 2) + ((charArray3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f; charArray4[3] = charArray3[2] & 0x3f;
for (i = 0; (i < 4); ++i) for (i = 0; (i < 4); ++i, ++a)
ret += charSet[char_array_4[i]]; outputBuffer[a] = charSet[charArray4[i]];
i = 0; i = 0;
} }
} }
@ -474,59 +463,61 @@ std::string base64Encode(const unsigned char *bytes_to_encode,
if (i) if (i)
{ {
for (int j = i; j < 3; ++j) for (int j = i; j < 3; ++j)
char_array_3[j] = '\0'; charArray3[j] = '\0';
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; charArray4[0] = (charArray3[0] & 0xfc) >> 2;
char_array_4[1] = charArray4[1] =
((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); ((charArray3[0] & 0x03) << 4) + ((charArray3[1] & 0xf0) >> 4);
char_array_4[2] = charArray4[2] =
((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); ((charArray3[1] & 0x0f) << 2) + ((charArray3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f; charArray4[3] = charArray3[2] & 0x3f;
for (int j = 0; (j <= i); ++j) for (int j = 0; (j <= i); ++j, ++a)
ret += charSet[char_array_4[j]]; outputBuffer[a] = charSet[charArray4[j]];
if (padded) if (padded)
while ((++i < 4)) while ((++i < 4))
ret += '='; {
outputBuffer[a] = '=';
++a;
}
} }
return ret;
} }
std::vector<char> base64DecodeToVector(std::string_view encoded_string) std::vector<char> base64DecodeToVector(std::string_view encodedString)
{ {
auto in_len = encoded_string.size(); auto inLen = encodedString.size();
int i = 0; int i = 0;
int in_{0}; int in_{0};
char char_array_4[4], char_array_3[3]; char charArray4[4], charArray3[3];
std::vector<char> ret; std::vector<char> ret;
ret.reserve(base64DecodedLength(in_len)); ret.reserve(base64DecodedLength(inLen));
while (in_len-- && (encoded_string[in_] != '=')) while (inLen-- && (encodedString[in_] != '='))
{ {
if (!isBase64(encoded_string[in_])) if (!isBase64(encodedString[in_]))
{ {
++in_; ++in_;
continue; continue;
} }
char_array_4[i++] = encoded_string[in_]; charArray4[i++] = encodedString[in_];
++in_; ++in_;
if (i == 4) if (i == 4)
{ {
for (i = 0; i < 4; ++i) for (i = 0; i < 4; ++i)
{ {
char_array_4[i] = base64CharMap.getIndex(char_array_4[i]); charArray4[i] = base64CharMap.getIndex(charArray4[i]);
} }
char_array_3[0] = charArray3[0] =
(char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + charArray3[1] =
((char_array_4[2] & 0x3c) >> 2); ((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
for (i = 0; (i < 3); ++i) for (i = 0; (i < 3); ++i)
ret.push_back(char_array_3[i]); ret.push_back(charArray3[i]);
i = 0; i = 0;
} }
} }
@ -534,60 +525,59 @@ std::vector<char> base64DecodeToVector(std::string_view encoded_string)
if (i) if (i)
{ {
for (int j = i; j < 4; ++j) for (int j = i; j < 4; ++j)
char_array_4[j] = 0; charArray4[j] = 0;
for (int j = 0; j < 4; ++j) for (int j = 0; j < 4; ++j)
{ {
char_array_4[j] = base64CharMap.getIndex(char_array_4[j]); charArray4[j] = base64CharMap.getIndex(charArray4[j]);
} }
char_array_3[0] = charArray3[0] = (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
(char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); charArray3[1] =
char_array_3[1] = ((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
--i; --i;
for (int j = 0; (j < i); ++j) for (int j = 0; (j < i); ++j)
ret.push_back(char_array_3[j]); ret.push_back(charArray3[j]);
} }
return ret; return ret;
} }
std::string base64Decode(std::string_view encoded_string) size_t base64Decode(const char *encodedString,
size_t inLen,
unsigned char *outputBuffer)
{ {
auto in_len = encoded_string.size();
int i = 0; int i = 0;
int in_{0}; int in_{0};
unsigned char char_array_4[4], char_array_3[3]; unsigned char charArray4[4], charArray3[3];
std::string ret;
ret.reserve(base64DecodedLength(in_len));
while (in_len-- && (encoded_string[in_] != '=')) size_t a = 0;
while (inLen-- && (encodedString[in_] != '='))
{ {
if (!isBase64(encoded_string[in_])) if (!isBase64(encodedString[in_]))
{ {
++in_; ++in_;
continue; continue;
} }
char_array_4[i++] = encoded_string[in_]; charArray4[i++] = encodedString[in_];
++in_; ++in_;
if (i == 4) if (i == 4)
{ {
for (i = 0; i < 4; ++i) for (i = 0; i < 4; ++i)
{ {
char_array_4[i] = base64CharMap.getIndex(char_array_4[i]); charArray4[i] = base64CharMap.getIndex(charArray4[i]);
} }
char_array_3[0] = charArray3[0] =
(char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + charArray3[1] =
((char_array_4[2] & 0x3c) >> 2); ((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
for (i = 0; (i < 3); ++i) for (i = 0; (i < 3); ++i, ++a)
ret += char_array_3[i]; outputBuffer[a] = charArray3[i];
i = 0; i = 0;
} }
} }
@ -595,25 +585,24 @@ std::string base64Decode(std::string_view encoded_string)
if (i) if (i)
{ {
for (int j = i; j < 4; ++j) for (int j = i; j < 4; ++j)
char_array_4[j] = 0; charArray4[j] = 0;
for (int j = 0; j < 4; ++j) for (int j = 0; j < 4; ++j)
{ {
char_array_4[j] = base64CharMap.getIndex(char_array_4[j]); charArray4[j] = base64CharMap.getIndex(charArray4[j]);
} }
char_array_3[0] = charArray3[0] = (charArray4[0] << 2) + ((charArray4[1] & 0x30) >> 4);
(char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); charArray3[1] =
char_array_3[1] = ((charArray4[1] & 0xf) << 4) + ((charArray4[2] & 0x3c) >> 2);
((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); charArray3[2] = ((charArray4[2] & 0x3) << 6) + charArray4[3];
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
--i; --i;
for (int j = 0; (j < i); ++j) for (int j = 0; (j < i); ++j, ++a)
ret += char_array_3[j]; outputBuffer[a] = charArray3[j];
} }
return ret; return a;
} }
static std::string charToHex(char c) static std::string charToHex(char c)
@ -1045,6 +1034,35 @@ char *getHttpFullDate(const trantor::Date &date)
return lastTimeString; return lastTimeString;
} }
void dateToCustomFormattedString(const std::string &fmtStr,
std::string &str,
const trantor::Date &date)
{
auto nowSecond = date.microSecondsSinceEpoch() / MICRO_SECONDS_PRE_SEC;
time_t seconds = static_cast<time_t>(nowSecond);
struct tm tm_LValue = date.tmStruct();
std::stringstream Out;
Out.imbue(std::locale{"C"});
Out << std::put_time(&tm_LValue, fmtStr.c_str());
str = Out.str();
}
const std::string &getHttpFullDateStr(const trantor::Date &date)
{
static thread_local int64_t lastSecond = 0;
static thread_local std::string lastTimeString(128, 0);
auto nowSecond = date.microSecondsSinceEpoch() / MICRO_SECONDS_PRE_SEC;
if (nowSecond == lastSecond)
{
return lastTimeString;
}
lastSecond = nowSecond;
dateToCustomFormattedString("%a, %d %b %Y %H:%M:%S GMT",
lastTimeString,
date);
return lastTimeString;
}
trantor::Date getHttpDate(const std::string &httpFullDateString) trantor::Date getHttpDate(const std::string &httpFullDateString)
{ {
static const std::array<const char *, 4> formats = { static const std::array<const char *, 4> formats = {

View File

@ -17,6 +17,7 @@
#include <json/value.h> #include <json/value.h>
#include <json/writer.h> #include <json/writer.h>
#include <thread> #include <thread>
#include <limits>
using namespace drogon; using namespace drogon;
@ -268,14 +269,14 @@ bool WebSocketMessageParser::parse(trantor::MsgBuffer *buffer)
{ {
// According to the rfc6455 // According to the rfc6455
gotAll_ = false; gotAll_ = false;
if (buffer->readableBytes() >= 2) while (buffer->readableBytes() >= 2)
{ {
unsigned char opcode = (*buffer)[0] & 0x0f; unsigned char opcode = (*buffer)[0] & 0x0f;
bool isControlFrame = false; bool isControlFrame = false;
switch (opcode) switch (opcode)
{ {
case 0: case 0:
// continuation frame LOG_TRACE << "continuation frame";
break; break;
case 1: case 1:
type_ = WebSocketMessageType::Text; type_ = WebSocketMessageType::Text;
@ -327,8 +328,13 @@ bool WebSocketMessageParser::parse(trantor::MsgBuffer *buffer)
{ {
indexFirstMask = 10; indexFirstMask = 10;
} }
if (indexFirstMask > 2 && buffer->readableBytes() >= indexFirstMask) if (indexFirstMask > 2)
{ {
if (buffer->readableBytes() < indexFirstMask)
{
// Not enough data yet, wait for more.
return true;
}
if (isControlFrame) if (isControlFrame)
{ {
// rfc6455-5.5 // rfc6455-5.5
@ -344,14 +350,17 @@ bool WebSocketMessageParser::parse(trantor::MsgBuffer *buffer)
} }
else if (indexFirstMask == 10) else if (indexFirstMask == 10)
{ {
length = (unsigned char)(*buffer)[2]; length = 0;
length = (length << 8) + (unsigned char)(*buffer)[3]; for (int i = 2; i <= 9; ++i)
length = (length << 8) + (unsigned char)(*buffer)[4]; {
length = (length << 8) + (unsigned char)(*buffer)[5]; if (length > ((std::numeric_limits<size_t>::max)() >> 8))
length = (length << 8) + (unsigned char)(*buffer)[6]; {
length = (length << 8) + (unsigned char)(*buffer)[7]; LOG_ERROR
length = (length << 8) + (unsigned char)(*buffer)[8]; << "Payload length too large to handle safely";
length = (length << 8) + (unsigned char)(*buffer)[9]; return false;
}
length = (length << 8) + (unsigned char)(*buffer)[i];
}
} }
else else
{ {
@ -380,9 +389,16 @@ bool WebSocketMessageParser::parse(trantor::MsgBuffer *buffer)
{ {
message_[oldLen + i] = (rawData[i] ^ masks[i % 4]); message_[oldLen + i] = (rawData[i] ^ masks[i % 4]);
} }
if (isFin)
gotAll_ = true;
buffer->retrieve(indexFirstMask + 4 + length); buffer->retrieve(indexFirstMask + 4 + length);
if (isFin)
{
gotAll_ = true;
return true;
}
}
else
{
// Not enough data yet, wait for more.
return true; return true;
} }
} }
@ -392,9 +408,16 @@ bool WebSocketMessageParser::parse(trantor::MsgBuffer *buffer)
{ {
auto rawData = buffer->peek() + indexFirstMask; auto rawData = buffer->peek() + indexFirstMask;
message_.append(rawData, length); message_.append(rawData, length);
if (isFin)
gotAll_ = true;
buffer->retrieve(indexFirstMask + length); buffer->retrieve(indexFirstMask + length);
if (isFin)
{
gotAll_ = true;
return true;
}
}
else
{
// Not enough data yet, wait for more.
return true; return true;
} }
} }

View File

@ -42,7 +42,8 @@ if(CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND BUILD_SHARED_LIBS)
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpUtils.cc) set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpUtils.cc)
else() else()
set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpFileImpl.cc set(UNITTEST_SOURCES ${UNITTEST_SOURCES} ../src/HttpFileImpl.cc
unittests/HttpFileTest.cc) unittests/HttpFileTest.cc
unittests/WebsocketResponseTest.cc)
endif() endif()
add_executable(unittest ${UNITTEST_SOURCES}) add_executable(unittest ${UNITTEST_SOURCES})
@ -52,7 +53,8 @@ if (BUILD_CTL)
integration_test/client/main.cc integration_test/client/main.cc
integration_test/client/WebSocketTest.cc integration_test/client/WebSocketTest.cc
integration_test/client/MultipleWsTest.cc integration_test/client/MultipleWsTest.cc
integration_test/client/HttpPipeliningTest.cc) integration_test/client/HttpPipeliningTest.cc
integration_test/client/RequestStreamTest.cc)
add_executable(integration_test_client ${INTEGRATION_TEST_CLIENT_SOURCES}) add_executable(integration_test_client ${INTEGRATION_TEST_CLIENT_SOURCES})
set(INTEGRATION_TEST_SERVER_SOURCES set(INTEGRATION_TEST_SERVER_SOURCES
@ -75,6 +77,7 @@ if (BUILD_CTL)
integration_test/server/RangeTestController.cc integration_test/server/RangeTestController.cc
integration_test/server/BeginAdviceTest.cc integration_test/server/BeginAdviceTest.cc
integration_test/server/MiddlewareTest.cc integration_test/server/MiddlewareTest.cc
integration_test/server/RequestStreamTestCtrl.cc
integration_test/server/main.cc) integration_test/server/main.cc)
if(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE) if(DROGON_CXX_STANDARD GREATER_EQUAL 20 AND HAS_COROUTINE)

View File

@ -44,8 +44,7 @@ DROGON_TEST(CookieSameSite)
if (seq.sessionCookie.getValue() != "") if (seq.sessionCookie.getValue() != "")
{ {
// add session cookie // add session cookie
req->addCookie(seq.sessionCookie.getKey(), req->addCookie(seq.sessionCookie.key(), seq.sessionCookie.value());
seq.sessionCookie.getValue());
} // endif } // endif
client->sendRequest( client->sendRequest(

View File

@ -0,0 +1,143 @@
#include <drogon/HttpClient.h>
#include <drogon/drogon_test.h>
#include <trantor/net/TcpClient.h>
#include <chrono>
#include <string>
#include <iostream>
#include <fstream>
using namespace drogon;
template <typename T>
void checkStreamRequest(T &&TEST_CTX,
trantor::EventLoop *loop,
const trantor::InetAddress &addr,
const std::vector<std::string_view> &dataToSend,
std::string_view expectedResp)
{
auto tcpClient = std::make_shared<trantor::TcpClient>(loop, addr, "test");
std::promise<void> promise;
auto respString = std::make_shared<std::string>();
tcpClient->setMessageCallback(
[respString](const trantor::TcpConnectionPtr &conn,
trantor::MsgBuffer *buf) {
respString->append(buf->read(buf->readableBytes()));
});
tcpClient->setConnectionCallback(
[TEST_CTX, &promise, respString, dataToSend, expectedResp](
const trantor::TcpConnectionPtr &conn) {
if (conn->disconnected())
{
LOG_INFO << "Disconnected from server";
CHECK(respString->substr(0, expectedResp.size()) ==
expectedResp);
promise.set_value();
return;
}
LOG_INFO << "Connected to server";
CHECK(conn->connected());
for (auto &data : dataToSend)
{
conn->send(data.data(), data.size());
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
conn->shutdown();
});
tcpClient->connect();
promise.get_future().wait();
}
DROGON_TEST(RequestStreamTest)
{
const std::string ip = "127.0.0.1";
const uint16_t port = 8848;
auto client = HttpClient::newHttpClient(ip, port);
HttpRequestPtr req;
bool enabled = false;
req = HttpRequest::newHttpRequest();
req->setPath("/stream_status");
{
auto [res, resp] = client->sendRequest(req);
REQUIRE(res == ReqResult::Ok);
REQUIRE(resp->statusCode() == k200OK);
if (resp->body() == "enabled")
{
enabled = true;
}
else
{
LOG_INFO << "Server does not enable request stream.";
}
}
req = HttpRequest::newHttpRequest();
req->setPath("/stream_chunk");
req->setMethod(Post);
req->setBody("1234567890");
client->sendRequest(req,
[TEST_CTX, enabled](ReqResult r,
const HttpResponsePtr &resp) {
REQUIRE(r == ReqResult::Ok);
if (enabled)
{
CHECK(resp->statusCode() == k200OK);
CHECK(resp->body() == "1234567890");
}
else
{
CHECK(resp->statusCode() == k400BadRequest);
CHECK(resp->body() == "no stream");
}
});
if (!enabled)
{
return;
}
LOG_INFO << "Test request stream";
std::string filePath = "./中文.txt";
std::ifstream file(filePath);
std::stringstream content;
REQUIRE(file.is_open());
content << file.rdbuf();
req = HttpRequest::newFileUploadRequest({UploadFile{filePath}});
req->setPath("/stream_upload_echo");
req->setMethod(Post);
client->sendRequest(req,
[TEST_CTX,
content = content.str()](ReqResult r,
const HttpResponsePtr &resp) {
CHECK(r == ReqResult::Ok);
CHECK(resp->statusCode() == k200OK);
CHECK(resp->body() == content);
});
checkStreamRequest(TEST_CTX,
client->getLoop(),
trantor::InetAddress{ip, port},
// Good request
{"POST /stream_chunk HTTP/1.1\r\n"
"Transfer-Encoding: chunked\r\n\r\n",
"1\r\nz\r\n",
"2\r\nzz\r\n0\r\n\r\n"},
// Good response
"HTTP/1.1 200 OK\r\n");
checkStreamRequest(TEST_CTX,
client->getLoop(),
trantor::InetAddress{ip, port},
// Bad request
{"POST /stream_chunk HTTP/1.1\r\n"
"Transfer-Encoding: chunked\r\n\r\n",
"1\r\nz\r\n",
"1\r\nzz\r\n",
"0\r\n\r\n"},
// Bad response
"HTTP/1.1 400 Bad Request\r\n");
}

View File

@ -0,0 +1,150 @@
#include <fstream>
#include <drogon/HttpController.h>
#include <drogon/HttpRequest.h>
#include <drogon/RequestStream.h>
using namespace drogon;
class RequestStreamTestCtrl : public HttpController<RequestStreamTestCtrl>
{
public:
METHOD_LIST_BEGIN
ADD_METHOD_TO(RequestStreamTestCtrl::stream_status, "/stream_status", Get);
ADD_METHOD_TO(RequestStreamTestCtrl::stream_chunk, "/stream_chunk", Post);
ADD_METHOD_TO(RequestStreamTestCtrl::stream_upload_echo,
"/stream_upload_echo",
Post);
METHOD_LIST_END
void stream_status(
const HttpRequestPtr &,
std::function<void(const HttpResponsePtr &)> &&callback) const
{
auto resp = HttpResponse::newHttpResponse();
if (app().isRequestStreamEnabled())
{
resp->setBody("enabled");
}
else
{
resp->setBody("not enabled");
}
callback(resp);
}
void stream_chunk(
const HttpRequestPtr &,
RequestStreamPtr &&stream,
std::function<void(const HttpResponsePtr &)> &&callback) const
{
if (!stream)
{
LOG_INFO << "stream mode is not enabled";
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("no stream");
callback(resp);
return;
}
auto respBody = std::make_shared<std::string>();
auto reader = RequestStreamReader::newReader(
[respBody](const char *data, size_t length) {
respBody->append(data, length);
},
[respBody, callback = std::move(callback)](std::exception_ptr ex) {
auto resp = HttpResponse::newHttpResponse();
if (ex)
{
try
{
std::rethrow_exception(std::move(ex));
}
catch (const std::exception &e)
{
LOG_ERROR << "stream error: " << e.what();
}
resp->setStatusCode(k400BadRequest);
resp->setBody("stream error");
callback(resp);
}
else
{
resp->setBody(*respBody);
callback(resp);
}
});
stream->setStreamReader(std::move(reader));
}
void stream_upload_echo(
const HttpRequestPtr &req,
RequestStreamPtr &&stream,
std::function<void(const HttpResponsePtr &)> &&callback) const
{
assert(drogon::app().isRequestStreamEnabled() || !stream);
if (!stream)
{
LOG_INFO << "stream mode is not enabled";
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("no stream");
callback(resp);
return;
}
if (req->contentType() != CT_MULTIPART_FORM_DATA)
{
auto resp = HttpResponse::newHttpResponse();
resp->setStatusCode(k400BadRequest);
resp->setBody("should upload multipart");
callback(resp);
return;
}
struct Context
{
std::string firstFileContent;
size_t currentFileIndex_{0};
};
auto ctx = std::make_shared<Context>();
auto reader = RequestStreamReader::newMultipartReader(
req,
[ctx](MultipartHeader &&header) { ctx->currentFileIndex_++; },
[ctx](const char *data, size_t length) {
if (ctx->currentFileIndex_ == 1)
{
ctx->firstFileContent.append(data, length);
}
},
[ctx, callback = std::move(callback)](std::exception_ptr ex) {
auto resp = HttpResponse::newHttpResponse();
if (ex)
{
try
{
std::rethrow_exception(std::move(ex));
}
catch (const StreamError &e)
{
LOG_ERROR << "stream error: " << e.what();
}
catch (const std::exception &e)
{
LOG_ERROR << "multipart error: " << e.what();
}
resp->setStatusCode(k400BadRequest);
resp->setBody("error\n");
callback(resp);
}
else
{
resp->setBody(ctx->firstFileContent);
callback(resp);
}
});
stream->setStreamReader(std::move(reader));
}
};

View File

@ -408,8 +408,7 @@ int main()
app().registerCustomExtensionMime("md", "text/markdown"); app().registerCustomExtensionMime("md", "text/markdown");
app().setFileTypes({"md", "html", "jpg", "cc", "txt"}); app().setFileTypes({"md", "html", "jpg", "cc", "txt"});
std::cout << "Date: " std::cout << "Date: "
<< std::string{drogon::utils::getHttpFullDate( << drogon::utils::getHttpFullDateStr(trantor::Date::now())
trantor::Date::now())}
<< std::endl; << std::endl;
app().registerBeginningAdvice( app().registerBeginningAdvice(

View File

@ -35,4 +35,29 @@ DROGON_TEST(CookieTest)
drogon::Cookie::convertString2SameSite("Strict")); drogon::Cookie::convertString2SameSite("Strict"));
CHECK(drogon::Cookie::SameSite::kNone == CHECK(drogon::Cookie::SameSite::kNone ==
drogon::Cookie::convertString2SameSite("None")); drogon::Cookie::convertString2SameSite("None"));
// Test for Partitioned attribute
drogon::Cookie cookie6("test", "6");
cookie6.setPartitioned(true);
CHECK(cookie6.cookieString() ==
"Set-Cookie: test=6; Secure; HttpOnly; Partitioned\r\n");
// Test that partitioned attribute automatically sets secure
drogon::Cookie cookie7("test", "7");
cookie7.setPartitioned(true);
CHECK(cookie7.isSecure() == true);
// Test other attributes
drogon::Cookie cookie8("test", "8");
cookie8.setPartitioned(true);
cookie8.setDomain("drogon.org");
cookie8.setMaxAge(3600);
CHECK(cookie8.cookieString() ==
"Set-Cookie: test=8; Max-Age=3600; Domain=drogon.org; Secure; "
"HttpOnly; Partitioned\r\n");
// Teset Partitioned and SameSite can coexist
drogon::Cookie cookie9("test", "9");
cookie9.setPartitioned(true);
cookie9.setSameSite(drogon::Cookie::SameSite::kLax);
CHECK(
cookie9.cookieString() ==
"Set-Cookie: test=9; SameSite=Lax; Secure; HttpOnly; Partitioned\r\n");
} }

View File

@ -2,6 +2,10 @@
#include <drogon/utils/coroutine.h> #include <drogon/utils/coroutine.h>
#include <drogon/HttpAppFramework.h> #include <drogon/HttpAppFramework.h>
#include <trantor/net/EventLoopThread.h> #include <trantor/net/EventLoopThread.h>
#include <trantor/net/EventLoopThreadPool.h>
#include <chrono>
#include <cstdint>
#include <future>
#include <type_traits> #include <type_traits>
using namespace drogon; using namespace drogon;
@ -212,3 +216,32 @@ DROGON_TEST(SwitchThread)
sync_wait(switch_thread()); sync_wait(switch_thread());
thread.wait(); thread.wait();
} }
DROGON_TEST(Mutex)
{
trantor::EventLoopThreadPool pool{3};
pool.start();
Mutex mutex;
async_run([&]() -> Task<> {
co_await switchThreadCoro(pool.getLoop(0));
auto guard = co_await mutex.scoped_lock();
co_await sleepCoro(pool.getLoop(1), std::chrono::seconds(2));
co_return;
});
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::promise<void> done;
async_run([&]() -> Task<> {
co_await switchThreadCoro(pool.getLoop(2));
auto id = std::this_thread::get_id();
co_await mutex.lock();
CHECK(id == std::this_thread::get_id());
mutex.unlock();
CHECK(id == std::this_thread::get_id());
done.set_value();
co_return;
});
done.get_future().wait();
for (int16_t i = 0; i < 3; i++)
pool.getLoop(i)->quit();
pool.wait();
}

View File

@ -1,5 +1,6 @@
#include <drogon/DrObject.h> #include <drogon/DrObject.h>
#include <drogon/drogon_test.h> #include <drogon/drogon_test.h>
#include <drogon/HttpController.h>
using namespace drogon; using namespace drogon;
@ -41,3 +42,45 @@ DROGON_TEST(DrObjectNamespaceTest)
CHECK(objPtr2.get() != nullptr); CHECK(objPtr2.get() != nullptr);
CHECK(objPtr == objPtr2); CHECK(objPtr == objPtr2);
} }
class TestC : public DrObject<TestC>
{
public:
static constexpr bool isAutoCreation = true;
};
class TestD : public DrObject<TestD>
{
public:
static constexpr bool isAutoCreation = false;
};
class TestE : public DrObject<TestE>
{
public:
static constexpr double isAutoCreation = 3.0;
};
class CtrlA : public HttpController<CtrlA>
{
public:
METHOD_LIST_BEGIN
METHOD_LIST_END
};
class CtrlB : public HttpController<CtrlB, false>
{
public:
METHOD_LIST_BEGIN
METHOD_LIST_END
};
DROGON_TEST(IsAutoCreationClassTest)
{
STATIC_REQUIRE(isAutoCreationClass<TestA>::value == false);
STATIC_REQUIRE(isAutoCreationClass<TestC>::value == true);
STATIC_REQUIRE(isAutoCreationClass<TestD>::value == false);
STATIC_REQUIRE(isAutoCreationClass<TestE>::value == false);
STATIC_REQUIRE(isAutoCreationClass<CtrlA>::value == true);
STATIC_REQUIRE(isAutoCreationClass<CtrlB>::value == false);
}

View File

@ -7,7 +7,7 @@ using namespace drogon;
DROGON_TEST(HttpFullDateTest) DROGON_TEST(HttpFullDateTest)
{ {
auto str = utils::getHttpFullDate(); auto str = utils::getHttpFullDateStr();
auto date = utils::getHttpDate(str); auto date = utils::getHttpDate(str);
CHECK(utils::getHttpFullDate(date) == str); CHECK(utils::getHttpFullDate(date) == str);
} }

View File

@ -1,6 +1,7 @@
#include <drogon/MultiPart.h> #include <drogon/MultiPart.h>
#include <drogon/drogon_test.h> #include <drogon/drogon_test.h>
#include <drogon/HttpRequest.h> #include <drogon/HttpRequest.h>
#include "../../lib/src/MultipartStreamParser.h"
DROGON_TEST(MultiPartParser) DROGON_TEST(MultiPartParser)
{ {
@ -60,3 +61,72 @@ DROGON_TEST(MultiPartParser)
CHECK(parser4.getParameters().size() == 1); CHECK(parser4.getParameters().size() == 1);
CHECK(parser4.getParameters().at("some;key") == "Hello; World"); CHECK(parser4.getParameters().at("some;key") == "Hello; World");
} }
DROGON_TEST(MultiPartStreamParser)
{
static const std::string ct = "multipart/form-data; boundary=\"12345\"";
static const std::string_view data =
"--12345\r\n"
"Content-Disposition: form-data; name=\"key1\"; filename=\"file1\"\r\n"
"\r\n"
"Hello; World\r\n"
"--12345\r\n"
"Content-Disposition: form-data; name=\"key2\"\r\n"
"\r\n"
"value2\r\n"
"--12345--";
struct Entry
{
drogon::MultipartHeader header;
std::string value;
std::string fileContent;
};
auto check = [TEST_CTX](size_t step) {
drogon::MultipartStreamParser parser(ct);
auto entries = std::make_shared<std::vector<Entry>>();
auto headerCb = [TEST_CTX, entries](drogon::MultipartHeader hdr) {
entries->emplace_back(Entry{std::move(hdr)});
};
auto dataCb = [TEST_CTX, entries](const char *data, size_t length) {
MANDATE(!entries->empty());
if (length == 0)
{
// Field finished
return;
}
if (entries->back().header.filename.empty())
{
entries->back().value.append(data, length);
}
else
{
entries->back().fileContent.append(data, length);
}
};
size_t i = 0;
while (i < data.length() && parser.isValid())
{
size_t end = i + step < data.length() ? i + step : data.length();
parser.parse(data.data() + i, end - i, headerCb, dataCb);
CHECK(parser.isValid());
i = end;
}
MANDATE(i == data.length());
MANDATE(parser.isFinished());
MANDATE(entries->size() == 2);
CHECK(entries->at(0).header.name == "key1");
CHECK(entries->at(0).fileContent == "Hello; World");
CHECK(entries->at(1).header.name == "key2");
CHECK(entries->at(1).value == "value2");
};
check(1);
check(3);
check(7);
check(20);
}

View File

@ -0,0 +1,40 @@
#include <drogon/drogon_test.h>
#include "../../lib/src/HttpRequestImpl.h"
#include "../../lib/src/HttpResponseImpl.h"
#include "../../lib/src/HttpControllerBinder.h"
using namespace drogon;
DROGON_TEST(WebsocketReponseTest)
{
WebsocketControllerBinder binder;
auto reqPtr = std::make_shared<HttpRequestImpl>(nullptr);
// Value from rfc6455-1.3
reqPtr->addHeader("sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ==");
binder.handleRequest(reqPtr, [&](const HttpResponsePtr &resp) {
CHECK(resp->statusCode() == k101SwitchingProtocols);
CHECK(resp->headers().size() == 3);
CHECK(resp->getHeader("Upgrade") == "websocket");
CHECK(resp->getHeader("Connection") == "Upgrade");
// Value from rfc6455-1.3
CHECK(resp->getHeader("Sec-WebSocket-Accept") ==
"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
auto implPtr = std::dynamic_pointer_cast<HttpResponseImpl>(resp);
implPtr->makeHeaderString();
auto buffer = implPtr->renderToBuffer();
auto str = std::string{buffer->peek(), buffer->readableBytes()};
CHECK(str.find("upgrade: websocket") != std::string::npos);
CHECK(str.find("connection: Upgrade") != std::string::npos);
CHECK(str.find("sec-websocket-accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=") !=
std::string::npos);
CHECK(str.find("content-length:") == std::string::npos);
});
}

Some files were not shown because too many files have changed in this diff Show More