├── .clang-format
├── .github
└── workflows
│ └── build_and_test.yml
├── CMakeLists.txt
├── CMakePresets.json
├── LICENSE.md
├── README.md
├── doc
└── logo.png
├── examples
├── CMakeLists.txt
├── example_asio_dispatcher.cpp
├── example_client.cpp
├── example_client_in_processor.cpp
├── example_guestbook.cpp
├── example_hello_world.cpp
├── example_request_processor.cpp
├── example_response_dispatching_asio_task.cpp
├── example_response_wait_future.cpp
├── example_route_context.cpp
├── example_route_matcher.cpp
├── example_route_params.cpp
├── example_route_params_user_defined_types.cpp
├── example_router.cpp
└── example_timer.cpp
├── external
├── asio
└── seal_lake
├── functional_tests
├── asio_dispatcher
│ └── test.toast
├── client
│ └── test.toast
├── client_in_processor
│ └── test.toast
├── exception_in_request_handler
│ └── test.toast
├── fastcgi_params
├── hello_world
│ └── test.toast
├── lunchtoast.cfg
├── nginx_linux.conf
├── nginx_windows.conf
├── request_processor
│ └── test.toast
├── response_dispatching_asio_task
│ └── test.toast
├── response_wait_future
│ └── test.toast
├── route_context
│ └── test.toast
├── route_matchers
│ └── test.toast
├── route_params
│ └── test.toast
├── route_params_user_defined_types
│ └── test.toast
├── router
│ └── test.toast
└── timer
│ └── test.toast
├── include
└── asyncgi
│ ├── asiodispatcher.h
│ ├── asyncgi.h
│ ├── asyncgi_fwd.h
│ ├── client.h
│ ├── detail
│ ├── asio_namespace.h
│ ├── eventhandlerproxy.h
│ ├── lazyinitialized.h
│ ├── routeresponsecontextaccessor.h
│ ├── serviceholder.h
│ └── utils.h
│ ├── errors.h
│ ├── events.h
│ ├── io.h
│ ├── request.h
│ ├── requestprocessor.h
│ ├── responder.h
│ ├── router.h
│ ├── server.h
│ ├── taskcontext.h
│ ├── timer.h
│ └── types.h
├── src
├── asio_error.h
├── asiodispatcher.cpp
├── asiodispatcherservice.cpp
├── asiodispatcherservice.h
├── client.cpp
├── clientconnection.cpp
├── clientconnection.h
├── clientservice.cpp
├── clientservice.h
├── connection.cpp
├── connection.h
├── connectionfactory.cpp
├── connectionfactory.h
├── connectionlistener.cpp
├── connectionlistener.h
├── connectionlistenerfactory.cpp
├── connectionlistenerfactory.h
├── eventhandlerproxy.cpp
├── io.cpp
├── ioservice.cpp
├── ioservice.h
├── request.cpp
├── response.cpp
├── responsecontext.cpp
├── responsecontext.h
├── responsesender.cpp
├── responsesender.h
├── routeresponsecontextaccessor.cpp
├── server.cpp
├── serverservice.cpp
├── serverservice.h
├── serviceholder.cpp
├── taskcontext.cpp
├── timer.cpp
├── timerprovider.cpp
├── timerprovider.h
├── timerservice.cpp
└── timerservice.h
├── test_examples
├── CMakeLists.txt
└── test_exception_in_request_handler.cpp
└── vcpkg.json
/.clang-format:
--------------------------------------------------------------------------------
1 | ---
2 | BasedOnStyle: WebKit
3 | AlignAfterOpenBracket: AlwaysBreak
4 | AllowAllArgumentsOnNextLine: false
5 | AllowAllConstructorInitializersOnNextLine: false
6 | AllowAllParametersOfDeclarationOnNextLine: false
7 | AllowShortFunctionsOnASingleLine: Empty
8 | AllowShortLambdasOnASingleLine: Empty
9 | AllowShortEnumsOnASingleLine: false
10 | AllowShortIfStatementsOnASingleLine: Never
11 | AllowShortLoopsOnASingleLine: false
12 | AllowShortBlocksOnASingleLine: Empty
13 | AlwaysBreakTemplateDeclarations: Yes
14 | BinPackArguments: false
15 | BinPackParameters: false
16 | BraceWrapping:
17 | AfterFunction: true
18 | BeforeElse: true
19 | BeforeLambdaBody: true
20 | BeforeWhile: true
21 | BeforeCatch: true
22 | BreakBeforeBraces: Custom
23 | BreakBeforeBinaryOperators: None
24 | BreakInheritanceList: AfterComma
25 | ColumnLimit: 120
26 | ContinuationIndentWidth: 8
27 | Cpp11BracedListStyle: true
28 | NamespaceIndentation: None
29 | PenaltyBreakBeforeFirstCallParameter: 0
30 | PenaltyReturnTypeOnItsOwnLine: 1000
31 | PenaltyBreakAssignment: 10
32 | SpaceBeforeCpp11BracedList: false
33 | SpaceInEmptyBlock: false
34 | SpaceInEmptyParentheses: false
35 | SpaceAfterTemplateKeyword: false
36 | SpacesInLineCommentPrefix:
37 | Minimum: 0
38 | Maximum: -1
39 | FixNamespaceComments: true
40 | UseCRLF: false
41 | IncludeCategories:
42 | # Headers in <> without extension.
43 | - Regex: '<[[:alnum:]\-_]+>'
44 | Priority: 6
45 | # Headers in <> from specific external libraries.
46 | - Regex: '<(gtest|gmock|boost|gsl)\/'
47 | Priority: 5
48 | # Headers in <> with subdirectory.
49 | - Regex: '<[[:alnum:]\-_]+\/'
50 | Priority: 4
51 | # Headers in <> with extension.
52 | - Regex: '<[[:alnum:].\-_]+>'
53 | Priority: 3
54 | # Headers in "" with subdirectory.
55 | - Regex: '"[[:alnum:]\-_]+\/'
56 | Priority: 2
57 | # Headers in "" with extension.
58 | - Regex: '"[[:alnum:].\-_]+"'
59 | Priority: 1
60 | ...
61 |
--------------------------------------------------------------------------------
/.github/workflows/build_and_test.yml:
--------------------------------------------------------------------------------
1 | name: build & test (clang, gcc, MSVC)
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | branches: [ "master", "dev" ]
7 | paths-ignore:
8 | - '**.md'
9 | pull_request:
10 | branches: [ "master", "dev" ]
11 | paths-ignore:
12 | - '**.md'
13 |
14 | jobs:
15 | build:
16 | name: ${{ matrix.config.name }}
17 | runs-on: ${{ matrix.config.os }}
18 | env:
19 | CC: ${{ matrix.config.cc }}
20 | CXX: ${{ matrix.config.cxx }}
21 |
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | config:
26 | - {
27 | name: "Ubuntu Latest gcc",
28 | os: ubuntu-latest,
29 | cmake-preset: gcc-release,
30 | cmake_vars: "",
31 | asio_type: "standalone"
32 | }
33 | - {
34 | name: "Ubuntu Latest clang",
35 | os: ubuntu-latest,
36 | cmake-preset: clang-release,
37 | cmake_vars: "",
38 | asio_type: "standalone"
39 | }
40 | - {
41 | name: "Windows Latest MSVC",
42 | os: windows-latest,
43 | cmake-preset: msvc-release,
44 | cmake_vars: "",
45 | asio_type: "standalone"
46 | }
47 | - {
48 | name: "Ubuntu Latest clang (Boost.Asio)",
49 | os: ubuntu-latest,
50 | cmake-preset: clang-release,
51 | asio_type: "boost",
52 | cmake_vars: "-DASYNCGI_USE_BOOST_ASIO=ON -DBoost_USE_STATIC_LIBS=ON -DBoost_USE_STATIC_RUNTIME=ON -DCMAKE_TOOLCHAIN_FILE=vcpkg/scripts/buildsystems/vcpkg.cmake",
53 | }
54 | - {
55 | name: "Windows Latest MSVC (Boost.Asio)",
56 | os: windows-latest,
57 | cmake-preset: msvc-release,
58 | asio_type: "boost",
59 | cmake_vars: "-DASYNCGI_USE_BOOST_ASIO=ON -DVCPKG_TARGET_TRIPLET=x64-windows-static-md -DCMAKE_TOOLCHAIN_FILE='vcpkg/scripts/buildsystems/vcpkg.cmake'"
60 | }
61 |
62 | steps:
63 | - name: Install ninja (Windows)
64 | if: matrix.config.os == 'windows-latest'
65 | run: choco install ninja
66 | - name: Install ninja (Linux)
67 | if: matrix.config.os == 'ubuntu-latest'
68 | run: sudo apt install ninja-build
69 | - uses: actions/checkout@v4
70 |
71 | - uses: rui314/setup-mold@v1
72 | - uses: hendrikmuhs/ccache-action@v1.2
73 | - uses: ilammy/msvc-dev-cmd@v1
74 |
75 | - name: Install vcpkg
76 | uses: lukka/run-vcpkg@v10
77 | with:
78 | vcpkgGitCommitId: 31a159c1cae2bf905299085d9ef01bdfea0ca7b8
79 |
80 | - name: Configure CMake
81 | run: cmake -B ${{github.workspace}}/build -DENABLE_EXAMPLES=ON ${{ matrix.config.cmake_vars }} --preset="${{ matrix.config.cmake-preset }}"
82 |
83 | - name: Build
84 | run: cmake --build ${{github.workspace}}/build --config Release
85 |
86 | - name: Upload build artifact
87 | uses: actions/upload-artifact@v3
88 | with:
89 | name: asyncgi-examples-${{ matrix.config.os }}-${{ matrix.config.asio_type }}-asio
90 | path: build/examples/
91 |
92 | - name: Upload build test artifact
93 | uses: actions/upload-artifact@v3
94 | with:
95 | name: asyncgi-test-examples-${{ matrix.config.os }}-${{ matrix.config.asio_type }}-asio
96 | path: build/test_examples/
97 |
98 | functional_tests:
99 | name: Functional testing (${{ matrix.config.name }} ${{ matrix.asio_type }}-asio)
100 | needs: build
101 | runs-on: ${{ matrix.config.os }}
102 | strategy:
103 | fail-fast: false
104 | matrix:
105 | asio_type: [ "standalone", "boost" ]
106 | config:
107 | - {
108 | name: "Windows",
109 | os: "windows-latest",
110 | lunchtoast_exec: "lunchtoast.exe",
111 | shell_command: -shell="msys2 -c",
112 | tags: -skip=linux,
113 | nginx_exec: "c:/tools/nginx-1.27.0/nginx.exe",
114 | nginx_cfg: "nginx_windows.conf"
115 | }
116 | - {
117 | name: "Linux",
118 | os: "ubuntu-latest",
119 | lunchtoast_exec: "lunchtoast",
120 | shell_command: "",
121 | tags: "",
122 | nginx_exec: "nginx",
123 | nginx_cfg: "nginx_linux.conf"
124 | }
125 | steps:
126 | - if: matrix.config.name == 'Windows'
127 | name: Disable Windows Defender real monitoring
128 | run: Set-MpPreference -DisableRealtimeMonitoring $true
129 | shell: powershell
130 | - name: Git checkout
131 | uses: actions/checkout@v3
132 |
133 | - if: matrix.config.name == 'Windows'
134 | name: Install MSYS2
135 | uses: msys2/setup-msys2@v2
136 | with:
137 | path-type: inherit
138 |
139 | - name: Download lunchtoast
140 | uses: robinraju/release-downloader@v1.7
141 | with:
142 | repository: "kamchatka-volcano/lunchtoast"
143 | latest: true
144 | filename: ${{ matrix.config.lunchtoast_exec }}
145 | out-file-path: "lunchtoast/"
146 |
147 | - name: Set lunchtoast execute permissions
148 | if: matrix.config.name == 'Linux'
149 | shell: sh
150 | working-directory: ${{github.workspace}}/lunchtoast
151 | run: chmod +x lunchtoast
152 |
153 | - name: Download asyncgi examples build
154 | uses: actions/download-artifact@v3
155 | with:
156 | name: asyncgi-examples-${{ matrix.config.os }}-${{ matrix.asio_type }}-asio
157 | path: build/examples
158 |
159 | - name: Download asyncgi test_examples build
160 | id: pre_launch_tests
161 | uses: actions/download-artifact@v3
162 | with:
163 | name: asyncgi-test-examples-${{ matrix.config.os }}-${{ matrix.asio_type }}-asio
164 | path: build/test_examples
165 |
166 | - name: Set artifacts execute permissions
167 | if: matrix.config.name == 'Linux'
168 | shell: sh
169 | working-directory: ${{github.workspace}}/build/
170 | run: chmod +x examples/example_* && chmod +x test_examples/test_*
171 |
172 | - name: Make temp dir for NGINX
173 | run: mkdir ${{github.workspace}}/temp
174 |
175 | - name: Launch tests
176 | uses: BerniWittmann/background-server-action@v1
177 | with:
178 | command: lunchtoast/lunchtoast functional_tests ${{ matrix.config.shell_command }} ${{ matrix.config.tags }} -collectFailedTests=failed_tests
179 | start: ${{ matrix.config.nginx_exec }} -c '${{github.workspace}}'/functional_tests/${{ matrix.config.nginx_cfg }}
180 |
181 | - name: Upload failed tests
182 | if: failure() && steps.launch_tests.outcome != 'success' && steps.pre_launch_tests.outcome == 'success'
183 | uses: actions/upload-artifact@v3
184 | with:
185 | name: asyncgi-failed-tests-${{ matrix.config.os }}
186 | path: failed_tests
187 |
--------------------------------------------------------------------------------
/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.18)
2 | project(asyncgi VERSION 0.6.0 DESCRIPTION "asyncgi - asynchronous FastCGI web application microframework")
3 |
4 | include(external/seal_lake)
5 | if (ASYNCGI_USE_BOOST_ASIO)
6 | find_package(Boost REQUIRED COMPONENTS system)
7 | else()
8 | include(external/asio)
9 | endif()
10 |
11 | SealLake_Bundle(
12 | NAME asyncgi_sfun
13 | GIT_REPOSITORY https://github.com/kamchatka-volcano/sfun.git
14 | GIT_TAG v5.1.0
15 | DESTINATION include/asyncgi/detail/external
16 | DIRECTORIES include/sfun
17 | TEXT_REPLACEMENTS
18 | "namespace sfun" "namespace asyncgi::sfun"
19 | "SFUN_" "ASYNCGI_SFUN_"
20 | )
21 |
22 | set(ASYNCGI_FCGI_RESPONDER_OBJECT_LIB ON)
23 | SealLake_Bundle(
24 | NAME asyncgi_fcgi_responder
25 | GIT_REPOSITORY https://github.com/kamchatka-volcano/fcgi_responder.git
26 | GIT_TAG v1.7.0
27 | TEXT_REPLACEMENTS
28 | "namespace fcgi" "namespace asyncgi::fcgi"
29 | )
30 |
31 | SealLake_Bundle(
32 | NAME asyncgi_whaleroute
33 | GIT_REPOSITORY https://github.com/kamchatka-volcano/whaleroute.git
34 | GIT_TAG v3.1.0
35 | DESTINATION include/asyncgi/detail/external
36 | DIRECTORIES include/whaleroute
37 | TEXT_REPLACEMENTS
38 | "namespace whaleroute" "namespace asyncgi::whaleroute"
39 | "WHALEROUTE_" "ASYNCGI_WHALEROUTE_"
40 | )
41 |
42 | set(ASYNCGI_HOT_TEACUP_OBJECT_LIB ON)
43 | SealLake_Bundle(
44 | NAME asyncgi_hot_teacup
45 | GIT_REPOSITORY https://github.com/kamchatka-volcano/hot_teacup.git
46 | GIT_TAG v3.3.0
47 | WILDCARDS
48 | include/hot_teacup/*.h
49 | DESTINATION include/asyncgi/http
50 | TEXT_REPLACEMENTS
51 | "namespace http" "namespace asyncgi::http"
52 | "HOT_TEACUP_" "ASYNCGI_HOT_TEACUP_"
53 | )
54 |
55 | set(SRC
56 | src/asiodispatcher.cpp
57 | src/io.cpp
58 | src/client.cpp
59 | src/server.cpp
60 | src/timer.cpp
61 | src/asiodispatcherservice.cpp
62 | src/connection.cpp
63 | src/connectionfactory.cpp
64 | src/eventhandlerproxy.cpp
65 | src/responsecontext.cpp
66 | src/responsesender.cpp
67 | src/ioservice.cpp
68 | src/serverservice.cpp
69 | src/clientservice.cpp
70 | src/taskcontext.cpp
71 | src/timerservice.cpp
72 | src/timerprovider.cpp
73 | src/connectionlistener.cpp
74 | src/connectionlistenerfactory.cpp
75 | src/clientconnection.cpp
76 | src/request.cpp
77 | src/response.cpp
78 | src/routeresponsecontextaccessor.cpp
79 | src/serviceholder.cpp
80 | )
81 |
82 | SealLake_StaticLibrary(
83 | SOURCES ${SRC}
84 | COMPILE_FEATURES cxx_std_17
85 | PROPERTIES
86 | CXX_EXTENSIONS OFF
87 | LIBRARIES
88 | asyncgi_fcgi_responder::asyncgi_fcgi_responder
89 | asyncgi_sfun::asyncgi_sfun
90 | asyncgi_hot_teacup::asyncgi_hot_teacup
91 | )
92 |
93 | if (ASYNCGI_USE_BOOST_ASIO)
94 | SealLake_AddLibraries(Boost::boost)
95 | else()
96 | SealLake_AddLibraries(asio)
97 | endif()
98 |
99 | SealLake_OptionalSubProjects(examples)
100 | if (ENABLE_EXAMPLES)
101 | add_subdirectory(test_examples)
102 | endif()
103 |
104 | if (ASYNCGI_USE_BOOST_ASIO)
105 | target_compile_definitions(asyncgi PUBLIC "ASYNCGI_USE_BOOST_ASIO")
106 | endif()
--------------------------------------------------------------------------------
/CMakePresets.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 6,
3 | "configurePresets": [
4 | {
5 | "name": "base-linux",
6 | "hidden": true,
7 | "displayName": "linux base preset",
8 | "generator": "Ninja",
9 | "binaryDir": "build-${presetName}",
10 | "cacheVariables": {
11 | "CMAKE_EXE_LINKER_FLAGS": "-fuse-ld=mold",
12 | "CMAKE_CXX_COMPILER_LAUNCHER": "ccache",
13 | "CPM_SOURCE_CACHE": "cpm_cache"
14 | }
15 | },
16 | {
17 | "name": "clang-base",
18 | "hidden": true,
19 | "displayName": "clang base preset",
20 | "inherits": "base-linux",
21 | "cacheVariables": {
22 | "CMAKE_CXX_COMPILER": "clang++",
23 | "CMAKE_C_COMPILER": "clang",
24 | "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Werror -Wcast-align -Wnon-virtual-dtor -Woverloaded-virtual -Wunused"
25 | }
26 | },
27 | {
28 | "name": "clang-debug",
29 | "displayName": "clang (Debug)",
30 | "inherits": "clang-base",
31 | "cacheVariables": {
32 | "CMAKE_BUILD_TYPE": "Debug"
33 | }
34 | },
35 | {
36 | "name": "clang-release",
37 | "displayName": "clang (Release)",
38 | "inherits": "clang-base",
39 | "cacheVariables": {
40 | "CMAKE_BUILD_TYPE": "Release"
41 | }
42 | },
43 | {
44 | "name": "gcc-base",
45 | "hidden": true,
46 | "displayName": "gcc base preset",
47 | "inherits": "base-linux",
48 | "cacheVariables": {
49 | "CMAKE_CXX_COMPILER": "g++",
50 | "CMAKE_C_COMPILER": "gcc",
51 | "CMAKE_CXX_FLAGS": "-Wall -Wextra -Wpedantic -Werror -Wcast-align -Wnon-virtual-dtor -Woverloaded-virtual -Wunused"
52 | }
53 | },
54 | {
55 | "name": "gcc-debug",
56 | "displayName": "gcc (Debug)",
57 | "inherits": "gcc-base",
58 | "cacheVariables": {
59 | "CMAKE_BUILD_TYPE": "Debug"
60 | }
61 | },
62 | {
63 | "name": "gcc-release",
64 | "displayName": "gcc (Release)",
65 | "inherits": "gcc-base",
66 | "cacheVariables": {
67 | "CMAKE_BUILD_TYPE": "Release"
68 | }
69 | },
70 | {
71 | "name": "base-windows",
72 | "displayName": "windows base preset",
73 | "hidden": true,
74 | "generator": "Ninja",
75 | "binaryDir": "build-${presetName}",
76 | "architecture": {
77 | "value": "x64",
78 | "strategy": "external"
79 | },
80 | "cacheVariables": {
81 | "CPM_SOURCE_CACHE": "cpm_cache",
82 | "CMAKE_CXX_COMPILER_LAUNCHER": "ccache"
83 | },
84 | "vendor": {
85 | "microsoft.com/VisualStudioSettings/CMake/1.0": {
86 | "hostOS": [
87 | "Windows"
88 | ]
89 | },
90 | "jetbrains.com/clion": {
91 | "toolchain": "Visual Studio"
92 | }
93 | }
94 | },
95 | {
96 | "name": "msvc-base",
97 | "hidden": true,
98 | "displayName": "msvc base preset",
99 | "inherits": "base-windows",
100 | "cacheVariables": {
101 | "CMAKE_CXX_COMPILER": "cl.exe",
102 | "CMAKE_C_COMPILER": "cl.exe",
103 | "CMAKE_CXX_FLAGS": "/EHsc /W4 /WX /wd4267"
104 | }
105 | },
106 | {
107 | "name": "msvc-debug",
108 | "displayName": "msvc (Debug)",
109 | "inherits": "msvc-base",
110 | "cacheVariables": {
111 | "CMAKE_BUILD_TYPE": "Debug"
112 | }
113 | },
114 | {
115 | "name": "msvc-release",
116 | "displayName": "msvc (Release)",
117 | "inherits": "msvc-base",
118 | "cacheVariables": {
119 | "CMAKE_BUILD_TYPE": "Release"
120 | }
121 | }
122 | ]
123 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Microsoft Public License (Ms-PL)
2 | Copyright 2022 Gorelyy PA
3 |
4 | This license governs use of the accompanying software. If you use the software,
5 | you accept this license. If you do not accept the license, do not use the
6 | software.
7 |
8 | 1. Definitions
9 |
10 | The terms "reproduce," "reproduction," "derivative works," and "distribution"
11 | have the same meaning here as under U.S. copyright law.
12 |
13 | A "contribution" is the original software, or any additions or changes to the
14 | software.
15 |
16 | A "contributor" is any person that distributes its contribution under this
17 | license.
18 |
19 | "Licensed patents" are a contributor's patent claims that read directly on its
20 | contribution.
21 |
22 | 2. Grant of Rights
23 |
24 | (A) Copyright Grant- Subject to the terms of this license, including the
25 | license conditions and limitations in section 3, each contributor grants you a
26 | non-exclusive, worldwide, royalty-free copyright license to reproduce its
27 | contribution, prepare derivative works of its contribution, and distribute its
28 | contribution or any derivative works that you create.
29 |
30 | (B) Patent Grant- Subject to the terms of this license, including the license
31 | conditions and limitations in section 3, each contributor grants you a
32 | non-exclusive, worldwide, royalty-free license under its licensed patents to
33 | make, have made, use, sell, offer for sale, import, and/or otherwise dispose of
34 | its contribution in the software or derivative works of the contribution in the
35 | software.
36 |
37 | 3. Conditions and Limitations
38 |
39 | (A) No Trademark License- This license does not grant you rights to use any
40 | contributors' name, logo, or trademarks.
41 |
42 | (B) If you bring a patent claim against any contributor over patents that you
43 | claim are infringed by the software, your patent license from such contributor
44 | to the software ends automatically.
45 |
46 | (C) If you distribute any portion of the software, you must retain all
47 | copyright, patent, trademark, and attribution notices that are present in the
48 | software.
49 |
50 | (D) If you distribute any portion of the software in source code form, you may
51 | do so only under this license by including a complete copy of this license with
52 | your distribution. If you distribute any portion of the software in compiled or
53 | object code form, you may only do so under a license that complies with this
54 | license.
55 |
56 | (E) The software is licensed "as-is." You bear the risk of using it. The
57 | contributors give no express warranties, guarantees or conditions. You may have
58 | additional consumer rights under your local laws which this license cannot
59 | change. To the extent permitted under your local laws, the contributors exclude
60 | the implied warranties of merchantability, fitness for a particular purpose and
61 | non-infringement.
62 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://github.com/kamchatka-volcano/asyncgi/actions/workflows/build_and_test.yml)
6 |
7 | **asyncgi** - is a C++17 asynchronous microframework for creating web applications interfacing with any HTTP server
8 | supporting [FastCGI](https://en.wikipedia.org/wiki/FastCGI) protocol. It aims to provide a modern way of
9 | using [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface), with a custom performant FastCGI implementation in
10 | C++, multithreading support and a clean and simple API:
11 |
12 | ```c++
13 | #include
14 |
15 | namespace http = asyncgi::http;
16 |
17 | int main()
18 | {
19 | auto io = asyncgi::IO{};
20 | auto router = asyncgi::Router{io};
21 | router.route("/", http::RequestMethod::Get).process(
22 | [](const asyncgi::Request&)
23 | {
24 | return http::Response{"Hello world"};
25 | });
26 |
27 | auto server = asyncgi::Server{io, router};
28 | server.listen("/tmp/fcgi.sock");
29 | io.run();
30 | }
31 | ```
32 |
33 | ## Table of Contents
34 | * [Usage](#usage)
35 | * [Connection](#connection)
36 | * [Request processor](#request-processor)
37 | * [Router](#router)
38 | * [Route parameters](#route-parameters)
39 | * [Route context](#route-context)
40 | * [Route matchers](#route-matchers)
41 | * [Complete guest book example](#complete-guest-book-example)
42 | * [Timer](#timer)
43 | * [Client](#client)
44 | * [Executing an asio task](#executing-an-asio-task)
45 | * [Showcase](#showcase)
46 | * [Development status](#development-status)
47 | * [Installation](#installation)
48 | * [Building examples](#building-examples)
49 | * [Running functional tests](#running-functional-tests)
50 | * [License](#license)
51 |
52 | ## Usage
53 |
54 | ### Connection
55 | Web applications developed with `asyncgi` require to establish a FastCGI connection with a web server handling HTTP requests. Most popular servers provide this functionality, for example `NGINX` can be used with a following configuration:
56 |
57 | ```
58 | server {
59 | listen 8088;
60 | server_name localhost;
61 |
62 | location / {
63 | try_files $uri @fcgi;
64 | }
65 |
66 | location @fcgi {
67 | fastcgi_pass unix:/tmp/fcgi.sock;
68 | #or using a TCP socket
69 | #fastcgi_pass localhost:9000;
70 | include fastcgi_params;
71 | }
72 | }
73 |
74 | ```
75 |
76 | `asyncgi` supports both `UNIX domain` and `TCP` sockets for opening `FastCGI` connections.
77 |
78 | ### Request processor
79 |
80 | In order to process requests, it's necessary to provide a function or function object that fulfills
81 | the `RequestProcessor` requirement. This means that the function must be invocable with one of the following signatures:
82 |
83 | * `http::Response (const asyncgi::Request&)`
84 | * `void (const asyncgi::Request&, asyncgi::Responder&)`.
85 |
86 |
87 | Example
88 |
89 | ```c++
90 | ///examples/example_request_processor.cpp
91 | ///
92 | #include
93 |
94 | namespace http = asyncgi::http;
95 |
96 | http::Response guestBookPage(const asyncgi::Request& request)
97 | {
98 | if (request.path() == "/")
99 | return {R"(
100 | Guest book
101 | No messages
102 | )"};
103 |
104 | return http::ResponseStatus::_404_Not_Found;
105 | }
106 |
107 | int main()
108 | {
109 | auto io = asyncgi::IO{};
110 | auto server = asyncgi::Server{io, guestBookPage};
111 | //Listen for FastCGI connections on UNIX domain socket
112 | server.listen("/tmp/fcgi.sock");
113 | //or over TCP
114 | //server.listen("127.0.0.1", 9088);
115 | io.run();
116 | return 0;
117 | }
118 | ```
119 |
120 | Here, the `guestBookPage` function serves as the request processor. Another way to implement it is by accepting a
121 | reference to the `asyncgi::Responder` object, which can be used for sending responses manually:
122 |
123 | ```c++
124 | void guestBookPage(const asyncgi::Request& request, asyncgi::Responder& responder)
125 | {
126 | if (request.path() == "/")
127 | responder.send(R"(
128 | Guest book
129 | No messages
130 | )");
131 |
132 | return responder.send(http::ResponseStatus::_404_Not_Found);
133 | }
134 | ```
135 |
136 | This approach tends to be more verbose and error-prone, therefore it is preferable to use it only when access to
137 | asyncgi::Responder is required for initiating asynchronous operations from the request processor. These cases are
138 | covered in the later parts of this document.
139 |
140 |
141 |
142 | ### Router
143 |
144 | Multiple request processors can be registered in the `asyncgi::Router` object, where they are matched to the paths
145 | specified in requests. The `asyncgi::Router` itself satisfies the `RequestProcessor` requirement.
146 |
147 | If multiple threads are required for request processing, the desired number of workers can be passed to
148 | the `asyncgi::IO` object's constructor. In such cases, the user must ensure that any shared data in the request
149 | processors is protected from concurrent read/write access.
150 |
151 |
152 | Example
153 |
154 | ```c++
155 | ///examples/example_router.cpp
156 | ///
157 | #include
158 | #include
159 |
160 | namespace http = asyncgi::http;
161 | using namespace std::string_literals;
162 |
163 | class GuestBookState {
164 | public:
165 | std::vector messages()
166 | {
167 | auto lock = std::scoped_lock{mutex_};
168 | return messages_;
169 | }
170 |
171 | void addMessage(const std::string& msg)
172 | {
173 | auto lock = std::scoped_lock{mutex_};
174 | messages_.emplace_back(msg);
175 | }
176 |
177 | private:
178 | std::vector messages_;
179 | std::mutex mutex_;
180 | };
181 |
182 | class GuestBookPage {
183 | public:
184 | GuestBookPage(GuestBookState& state)
185 | : state_(&state)
186 | {
187 | }
188 |
189 | http::Response operator()(const asyncgi::Request&)
190 | {
191 | auto messages = state_->messages();
192 | auto page = "Guest book "s;
193 | if (messages.empty())
194 | page += "No messages
";
195 | else
196 | for (const auto& msg : messages)
197 | page += "" + msg + "
";
198 |
199 | page += " ";
200 | page += "";
205 | return page;
206 | }
207 |
208 | private:
209 | GuestBookState* state_;
210 | };
211 |
212 | class GuestBookAddMessage {
213 | public:
214 | GuestBookAddMessage(GuestBookState& state)
215 | : state_(&state)
216 | {
217 | }
218 |
219 | http::Response operator()(const asyncgi::Request& request)
220 | {
221 | state_->addMessage(std::string{request.formField("msg")});
222 | return http::Redirect{"/"};
223 | }
224 |
225 | private:
226 | GuestBookState* state_;
227 | };
228 |
229 | int main()
230 | {
231 | auto io = asyncgi::IO{4}; //4 threads processing requests
232 | auto state = GuestBookState{};
233 | auto router = asyncgi::Router{io};
234 | router.route("/", http::RequestMethod::Get).process(state);
235 | router.route("/", http::RequestMethod::Post).process(state);
236 | router.route().set(http::Response{http::ResponseStatus::_404_Not_Found, "Page not found"});
237 | //Alternatively, it's possible to pass arguments for creation of http::Response object to the set() method.
238 | //router.route().set(http::ResponseStatus::Code_404_Not_Found, "Page not found");
239 |
240 | auto server = asyncgi::Server{io, router};
241 | server.listen("/tmp/fcgi.sock");
242 | io.run();
243 | }
244 | ```
245 |
246 |
247 |
248 | ### Route parameters
249 |
250 | When using `asyncgi::Router` with regular expressions, request processors must satisfy
251 | the `ParametrizedRequestProcessor` requirement. That means that a function object must be invocable with one of the
252 | following signatures:
253 |
254 | * `http::Response void(const TRouteParams&..., const asyncgi::Request&)`
255 | * `void (const TRouteParams&..., const asyncgi::Request&, asyncgi::Responder&)`
256 |
257 | The `TRouteParams` represents zero or more parameters generated from the capturing groups of the regular expression. For
258 | example, `http::Response (int age, string name, const asyncgi::Request&)` signature can be used to process
259 | requests matched by `asyncgi::rx{"/person/(\\w+)/age/(\\d+)"}`.
260 |
261 | In the following example a `ParametrizedRequestProcessor` named `GuestBookRemoveMessage` is added to remove the stored
262 | guest book messages:
263 |
264 |
265 | Example
266 |
267 | ```c++
268 | ///examples/example_route_params.cpp
269 | ///
270 | #include
271 | #include
272 |
273 | using namespace asyncgi;
274 | using namespace std::string_literals;
275 |
276 | class GuestBookState {
277 | public:
278 | std::vector messages()
279 | {
280 | auto lock = std::scoped_lock{mutex_};
281 | return messages_;
282 | }
283 |
284 | void addMessage(const std::string& msg)
285 | {
286 | auto lock = std::scoped_lock{mutex_};
287 | messages_.emplace_back(msg);
288 | }
289 |
290 | void removeMessage(int index)
291 | {
292 | auto lock = std::scoped_lock{mutex_};
293 | if (index < 0 || index >= static_cast(messages_.size()))
294 | return;
295 | messages_.erase(std::next(messages_.begin(), index));
296 | }
297 |
298 | private:
299 | std::vector messages_;
300 | std::mutex mutex_;
301 | };
302 |
303 | std::string makeMessage(int index, const std::string& msg)
304 | {
305 | return msg + R"()";
307 | }
308 |
309 | class GuestBookPage {
310 | public:
311 | explicit GuestBookPage(GuestBookState& state)
312 | : state_{&state}
313 | {
314 | }
315 |
316 | http::Response operator()(const asyncgi::Request&)
317 | {
318 | auto messages = state_->messages();
319 | auto page = "Guest book "s;
320 | if (messages.empty())
321 | page += "No messages
";
322 | else
323 | for (auto i = 0; i < static_cast(messages.size()); ++i)
324 | page += "" + makeMessage(i, messages.at(i)) + "
";
325 |
326 | page += " ";
327 | page += "";
332 | return page;
333 | }
334 |
335 | private:
336 | GuestBookState* state_;
337 | };
338 |
339 | class GuestBookAddMessage {
340 | public:
341 | explicit GuestBookAddMessage(GuestBookState& state)
342 | : state_{&state}
343 | {
344 | }
345 |
346 | http::Response operator()(const asyncgi::Request& request)
347 | {
348 | state_->addMessage(std::string{request.formField("msg")});
349 | return http::Redirect{"/"};
350 | }
351 |
352 | private:
353 | GuestBookState* state_;
354 | };
355 |
356 | class GuestBookRemoveMessage {
357 | public:
358 | explicit GuestBookRemoveMessage(GuestBookState& state)
359 | : state_{&state}
360 | {
361 | }
362 |
363 | http::Response operator()(int index, const asyncgi::Request&)
364 | {
365 | state_->removeMessage(index);
366 | return http::Redirect{"/"};
367 | }
368 |
369 | private:
370 | GuestBookState* state_;
371 | };
372 |
373 | int main()
374 | {
375 | auto io = asyncgi::IO{4};
376 | auto state = GuestBookState{};
377 | auto router = asyncgi::Router{io};
378 | router.route("/", http::RequestMethod::Get).process(state);
379 | router.route("/", http::RequestMethod::Post).process(state);
380 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process(state);
381 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
382 |
383 | auto server = asyncgi::Server{io, router};
384 | server.listen("/tmp/fcgi.sock");
385 | io.run();
386 | }
387 | ```
388 |
389 |
390 |
391 | Regular expression capture groups are transformed into request processor arguments using `std::stringstream`. In order
392 | to support request processors with user-defined parameter types, it is necessary to provide a specialization
393 | of `asyncgi::config::StringConverter` class template. The previous example has been modified to reformat
394 | the `GuestBookRemoveMessage` request processor to use the `MessageNumber` structure as a request processor argument:
395 |
396 |
397 | Example
398 |
399 | ```c++
400 | ///examples/example_route_params_user_defined_types.cpp
401 | ///
402 | #include
403 | #include
404 |
405 | using namespace asyncgi;
406 | using namespace std::string_literals;
407 |
408 | struct MessageNumber {
409 | int value;
410 | };
411 |
412 | template<>
413 | struct asyncgi::config::StringConverter {
414 | static std::optional fromString(const std::string& data)
415 | {
416 | return MessageNumber{std::stoi(data)};
417 | }
418 | };
419 |
420 | class GuestBookState {
421 | public:
422 | std::vector messages()
423 | {
424 | auto lock = std::scoped_lock{mutex_};
425 | return messages_;
426 | }
427 |
428 | void addMessage(const std::string& msg)
429 | {
430 | auto lock = std::scoped_lock{mutex_};
431 | messages_.emplace_back(msg);
432 | }
433 |
434 | void removeMessage(int index)
435 | {
436 | auto lock = std::scoped_lock{mutex_};
437 | if (index < 0 || index >= static_cast(messages_.size()))
438 | return;
439 | messages_.erase(std::next(messages_.begin(), index));
440 | }
441 |
442 | private:
443 | std::vector messages_;
444 | std::mutex mutex_;
445 | };
446 |
447 | std::string makeMessage(int index, const std::string& msg)
448 | {
449 | return msg + R"()";
451 | }
452 |
453 | class GuestBookPage {
454 | public:
455 | explicit GuestBookPage(GuestBookState& state)
456 | : state_{&state}
457 | {
458 | }
459 |
460 | http::Response operator()(const asyncgi::Request&)
461 | {
462 | auto messages = state_->messages();
463 | auto page = "Guest book "s;
464 | if (messages.empty())
465 | page += "No messages
";
466 | else
467 | for (auto i = 0; i < static_cast(messages.size()); ++i)
468 | page += "" + makeMessage(i, messages.at(i)) + "
";
469 |
470 | page += " ";
471 | page += "";
476 | return page;
477 | }
478 |
479 | private:
480 | GuestBookState* state_;
481 | };
482 |
483 | class GuestBookAddMessage {
484 | public:
485 | explicit GuestBookAddMessage(GuestBookState& state)
486 | : state_{&state}
487 | {
488 | }
489 |
490 | http::Response operator()(const asyncgi::Request& request)
491 | {
492 | state_->addMessage(std::string{request.formField("msg")});
493 | return http::Redirect{"/"};
494 | }
495 |
496 | private:
497 | GuestBookState* state_;
498 | };
499 |
500 | class GuestBookRemoveMessage {
501 | public:
502 | explicit GuestBookRemoveMessage(GuestBookState& state)
503 | : state_{&state}
504 | {
505 | }
506 |
507 | http::Response operator()(MessageNumber msgNumber, const asyncgi::Request&)
508 | {
509 | state_->removeMessage(msgNumber.value);
510 | return http::Redirect{"/"};
511 | }
512 |
513 | private:
514 | GuestBookState* state_;
515 | };
516 |
517 | int main()
518 | {
519 | auto io = asyncgi::IO{4};
520 | auto state = GuestBookState{};
521 | auto router = asyncgi::Router{io};
522 | router.route("/", http::RequestMethod::Get).process(state);
523 | router.route("/", http::RequestMethod::Post).process(state);
524 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process(state);
525 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
526 |
527 | auto server = asyncgi::Server{io, router};
528 | server.listen("/tmp/fcgi.sock");
529 | io.run();
530 | }
531 | ```
532 |
533 |
534 |
535 | ### Route context
536 |
537 | When using `asyncgi::Router`, it is possible to specify a template argument for a context structure type. This
538 | structure is then passed to the `ContextualRequestProcessor` functions and can be accessed and modified throughout the
539 | request processing for multiple routes. The `ContextualRequestProcessor` is a `RequestProcessor` that takes an
540 | additional argument referring to the context object.
541 | A single request can match multiple routes, as long as all preceding request processors do not send any response. To
542 | avoid sending responses request processors can use the `std::optional` signature and return empty
543 | values. This
544 | allows using `asyncgi::Router` to register middleware-like processors, which primarily modify the route context for
545 | subsequent processors.
546 |
547 | The next example demonstrates how a route context can be used for storing authorization information:
548 |
549 |
550 | Example
551 |
552 | ```c++
553 | ///examples/example_route_context.cpp
554 | ///
555 | #include
556 | #include
557 | #include
558 |
559 | namespace http = asyncgi::http;
560 | using namespace std::string_literals;
561 |
562 | enum class AccessRole {
563 | Admin,
564 | Guest
565 | };
566 |
567 | struct RouteContext {
568 | AccessRole role = AccessRole::Guest;
569 | };
570 |
571 | struct AdminAuthorizer {
572 | std::optional operator()(const asyncgi::Request& request, RouteContext& context)
573 | {
574 | if (request.cookie("admin_id") == "ADMIN_SECRET")
575 | context.role = AccessRole::Admin;
576 |
577 | return std::nullopt;
578 | }
579 | };
580 |
581 | struct LoginPage {
582 | http::Response operator()(const asyncgi::Request&, RouteContext& context)
583 | {
584 | if (context.role == AccessRole::Guest)
585 | return {R"(
586 |
587 | )"};
594 | else //We are already logged in as the administrator
595 | return http::Redirect{"/"};
596 | }
597 | };
598 |
599 | struct LoginPageAuthorize {
600 | http::Response operator()(const asyncgi::Request& request, RouteContext& context)
601 | {
602 | if (context.role == AccessRole::Guest) {
603 | if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
604 | return {http::Redirect{"/"}, {asyncgi::http::Cookie("admin_id", "ADMIN_SECRET")}};
605 | else
606 | return http::Redirect{"/login"};
607 | }
608 | else //We are already logged in as the administrator
609 | return http::Redirect{"/"};
610 | }
611 | };
612 |
613 | int main()
614 | {
615 | auto io = asyncgi::IO{4}; //4 threads processing requests
616 | auto router = asyncgi::Router{io};
617 | router.route(asyncgi::rx{".*"}).process();
618 | router.route("/").process(
619 | [](const asyncgi::Request&, asyncgi::Responder& response, RouteContext& context)
620 | {
621 | if (context.role == AccessRole::Admin)
622 | response.send("Hello admin
");
623 | else
624 | response.send(R"(Hello guest
login )");
625 | });
626 |
627 | router.route("/login", http::RequestMethod::Get).process();
628 | router.route("/login", http::RequestMethod::Post).process();
629 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
630 |
631 | auto server = asyncgi::Server{io, router};
632 | server.listen("/tmp/fcgi.sock");
633 | io.run();
634 | }
635 | ```
636 |
637 |
638 |
639 | ### Route matchers
640 |
641 | Any parameter of request or context objects can be registered for route matching
642 | in `asyncgi::Router::route()` method. To achieve this, it is required to provide a specialization of
643 | the `asyncgi::config::RouteMatcher` class template and implement a comparator bool operator() within it. Let's see how
644 | to register the enum class `Access` from the previous example as a route matcher:
645 |
646 |
647 | Example
648 |
649 | ```c++
650 | ///examples/example_route_matcher.cpp
651 | ///
652 | #include
653 | #include
654 | #include
655 |
656 | namespace http = asyncgi::http;
657 | using namespace std::string_literals;
658 |
659 | enum class AccessRole {
660 | Admin,
661 | Guest
662 | };
663 |
664 | struct RouteContext {
665 | AccessRole role = AccessRole::Guest;
666 | };
667 |
668 | struct AdminAuthorizer {
669 | std::optional operator()(const asyncgi::Request& request, RouteContext& context)
670 | {
671 | if (request.cookie("admin_id") == "ADMIN_SECRET")
672 | context.role = AccessRole::Admin;
673 |
674 | return std::nullopt;
675 | }
676 | };
677 |
678 | struct LoginPage {
679 | http::Response operator()(const asyncgi::Request&)
680 | {
681 | return {R"(
682 |
683 | )"};
690 | }
691 | };
692 |
693 | struct LoginPageAuthorize {
694 | http::Response operator()(const asyncgi::Request& request)
695 | {
696 | if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
697 | return {http::Redirect{"/"}, {asyncgi::http::Cookie("admin_id", "ADMIN_SECRET")}};
698 |
699 | return http::Redirect{"/login"};
700 | }
701 | };
702 |
703 | template<>
704 | struct asyncgi::config::RouteMatcher {
705 | bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
706 | {
707 | return value == context.role;
708 | }
709 | };
710 |
711 | int main()
712 | {
713 | auto io = asyncgi::IO{4};
714 | auto router = asyncgi::Router{io};
715 | router.route(asyncgi::rx{".*"}).process();
716 | router.route("/").process(
717 | [](const asyncgi::Request&, RouteContext& context) -> http::Response
718 | {
719 | if (context.role == AccessRole::Admin)
720 | return {"Hello admin
"};
721 | else
722 | return {R"(Hello guest
login )"};
723 | });
724 |
725 | router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process();
726 | router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process();
727 | router.route("/login", http::RequestMethod::Get, AccessRole::Admin).set("/", http::RedirectType::Found);
728 | router.route("/login", http::RequestMethod::Post, AccessRole::Admin).set("/", http::RedirectType::Found);
729 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
730 |
731 | auto server = asyncgi::Server{io, router};
732 | server.listen("/tmp/fcgi.sock");
733 | io.run();
734 | }
735 | ```
736 |
737 |
738 |
739 | ### Complete guest book example
740 |
741 | Let's combine all the previous examples to create a simple guest book.
742 | The messages in the guest book will only persist during the application runtime, as they are stored in a `std::vector`.
743 | The admin credentials for logging in are as follows: login: `admin`, password: `12345`. The admin account has the
744 | ability to delete posts.
745 |
746 |
747 | Example
748 |
749 | ```c++
750 | ///examples/example_guestbook.cpp
751 | ///
752 | #include
753 | #include
754 | #include
755 | #include
756 |
757 | namespace http = asyncgi::http;
758 | using namespace std::string_literals;
759 |
760 | enum class AccessRole {
761 | Admin,
762 | Guest
763 | };
764 |
765 | struct RouteContext {
766 | AccessRole role = AccessRole::Guest;
767 | };
768 |
769 | template<>
770 | struct asyncgi::config::RouteMatcher {
771 | bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
772 | {
773 | return value == context.role;
774 | }
775 | };
776 |
777 | std::optional authorizeAdmin(const asyncgi::Request& request, RouteContext& context)
778 | {
779 | if (request.cookie("admin_id") == "ADMIN_SECRET")
780 | context.role = AccessRole::Admin;
781 |
782 | return std::nullopt;
783 | }
784 |
785 | http::Response showLoginPage(const asyncgi::Request&)
786 | {
787 | return {R"(
788 |
789 | )"};
796 | }
797 |
798 | http::Response loginAdmin(const asyncgi::Request& request)
799 | {
800 | if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
801 | return {http::Redirect{"/"}, {http::Cookie("admin_id", "ADMIN_SECRET")}};
802 | else
803 | return http::Redirect{"/login"};
804 | }
805 |
806 | http::Response logoutAdmin(const asyncgi::Request&)
807 | {
808 | return {http::Redirect{"/"}, {http::Cookie("admin_id", "")}};
809 | }
810 |
811 | struct GuestBookMessage {
812 | std::string name;
813 | std::string text;
814 | };
815 |
816 | class GuestBookState {
817 | public:
818 | std::vector messages()
819 | {
820 | auto lock = std::scoped_lock{mutex_};
821 | return messages_;
822 | }
823 |
824 | void addMessage(std::string name, std::string msg)
825 | {
826 | name = std::regex_replace(name, std::regex{"<"}, "<");
827 | name = std::regex_replace(name, std::regex{">"}, ">");
828 | msg = std::regex_replace(msg, std::regex{"<"}, "<");
829 | msg = std::regex_replace(msg, std::regex{">"}, ">");
830 | auto lock = std::scoped_lock{mutex_};
831 | messages_.emplace_back(GuestBookMessage{name.empty() ? "Guest" : name, msg});
832 | }
833 |
834 | void removeMessage(int index)
835 | {
836 | auto lock = std::scoped_lock{mutex_};
837 | if (index < 0 || index >= static_cast(messages_.size()))
838 | return;
839 | messages_.erase(std::next(messages_.begin(), index));
840 | }
841 |
842 | private:
843 | std::vector messages_;
844 | std::mutex mutex_;
845 | };
846 |
847 | std::string makeMessagesDiv(const std::vector& messages, AccessRole role)
848 | {
849 | if (messages.empty())
850 | return "No messages
";
851 |
852 | auto messagesDiv = std::string{};
853 | for (auto i = 0; i < static_cast(messages.size()); ++i) {
854 | messagesDiv += "" + messages.at(i).name + " says:" + messages.at(i).text + " ";
855 | if (role == AccessRole::Admin)
856 | messagesDiv += R"( )";
858 | }
859 | return messagesDiv;
860 | }
861 |
862 | std::string makeLinksDiv(AccessRole role)
863 | {
864 | return (role == AccessRole::Admin ? R"(logout )"s
865 | : R"(login )"s) +
866 | R"(source )"s;
867 | }
868 |
869 | auto showGuestBookPage(GuestBookState& state)
870 | {
871 | return [&state](const asyncgi::Request& request, RouteContext& context) -> http::Response
872 | {
873 | auto page = R"(
874 | %LINKS%
875 |
876 |
asyncgi guest book
877 |
878 |
879 | %MESSAGES%
880 |
881 |
882 |
883 |
884 | %ERROR_MSG%
885 |
893 |
894 |
)"s;
895 |
896 | page = std::regex_replace(page, std::regex{"%MESSAGES%"}, makeMessagesDiv(state.messages(), context.role));
897 | page = std::regex_replace(page, std::regex{"%LINKS%"}, makeLinksDiv(context.role));
898 | if (request.hasQuery("error")) {
899 | if (request.query("error") == "urls_in_msg")
900 | page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "Messages can't contain urls ");
901 | if (request.query("error") == "empty_msg")
902 | page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "Messages can't be empty ");
903 | }
904 | else
905 | page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "");
906 |
907 | return page;
908 | };
909 | }
910 |
911 | auto addMessage(GuestBookState& state)
912 | {
913 | return [&state](const asyncgi::Request& request) -> http::Response
914 | {
915 | if (std::all_of(
916 | request.formField("msg").begin(),
917 | request.formField("msg").end(),
918 | [](char ch)
919 | {
920 | return std::isspace(static_cast(ch));
921 | }))
922 | return http::Redirect{"/?error=empty_msg"};
923 | else if (
924 | request.formField("msg").find("http://") != std::string_view::npos ||
925 | request.formField("msg").find("https://") != std::string_view::npos)
926 | return http::Redirect{"/?error=urls_in_msg"};
927 | else {
928 | state.addMessage(std::string{request.formField("name")}, std::string{request.formField("msg")});
929 | return http::Redirect{"/"};
930 | }
931 | };
932 | }
933 |
934 | auto removeMessage(GuestBookState& state)
935 | {
936 | return [&state](int index, const asyncgi::Request&) -> http::Response
937 | {
938 | state.removeMessage(index);
939 | return http::Redirect{"/"};
940 | };
941 | }
942 |
943 | int main()
944 | {
945 | auto io = asyncgi::IO{4};
946 | auto state = GuestBookState{};
947 | auto router = asyncgi::Router{io};
948 | router.route(asyncgi::rx{".*"}).process(authorizeAdmin);
949 | router.route("/", http::RequestMethod::Get).process(showGuestBookPage(state));
950 | router.route("/", http::RequestMethod::Post).process(addMessage(state));
951 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post, AccessRole::Admin)
952 | .process(removeMessage(state));
953 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post, AccessRole::Guest)
954 | .set(http::ResponseStatus::_401_Unauthorized);
955 | router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process(showLoginPage);
956 | router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process(loginAdmin);
957 | router.route("/logout").process(logoutAdmin);
958 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
959 |
960 | auto server = asyncgi::Server{io, router};
961 | server.listen("/tmp/fcgi.sock");
962 | io.run();
963 | }
964 | ```
965 |
966 |
967 |
968 | The live demo can be accessed [here](https://asyncgi-guestbook.eelnet.org).
969 |
970 | ### Timer
971 |
972 | A timer object `asyncgi::Timer` can be created to change or check some state periodically.
973 |
974 |
975 | Example
976 |
977 | ```c++
978 | ///examples/example_timer.cpp
979 | ///
980 | #include
981 |
982 | namespace http = asyncgi::http;
983 |
984 | struct Greeter{
985 | Greeter(const int& secondsCounter)
986 | : secondsCounter_{&secondsCounter}
987 | {
988 | }
989 |
990 | http::Response operator()(const asyncgi::Request&)
991 | {
992 | return "Hello world\n(alive for " + std::to_string(*secondsCounter_) + " seconds)";
993 | }
994 |
995 | private:
996 | const int* secondsCounter_;
997 | };
998 |
999 | int main()
1000 | {
1001 | auto io = asyncgi::IO{};
1002 | int secondsCounter = 0;
1003 |
1004 | auto timer = asyncgi::Timer{io};
1005 | timer.startPeriodic(
1006 | std::chrono::seconds(1),
1007 | [&secondsCounter]()
1008 | {
1009 | ++secondsCounter;
1010 | });
1011 |
1012 | auto router = asyncgi::Router{io};
1013 | router.route("/").process(secondsCounter);
1014 | router.route().set(http::ResponseStatus::_404_Not_Found);
1015 |
1016 | auto server = asyncgi::Server{io, router};
1017 | server.listen("/tmp/fcgi.sock");
1018 | io.run();
1019 | }
1020 | ```
1021 |
1022 |
1023 |
1024 |
1025 | The `asyncgi::Timer::waitFuture` method can accept an `std::future` object and invoke a provided callable object with
1026 | its result when the future object becomes ready. This function does not block while waiting and uses an internal timer
1027 | to periodically check the state of the future. To use it during request processing, a timer object created from
1028 | an `asyncgi::Responder` reference must be used. It is important to avoid using this timer after the response has already
1029 | been sent.
1030 |
1031 |
1032 | Example
1033 |
1034 | ```c++
1035 | ///examples/response_wait_future.cpp
1036 | ///
1037 | #include
1038 | #include
1039 |
1040 | using namespace asyncgi;
1041 |
1042 | struct DelayedPage{
1043 | void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
1044 | {
1045 | auto timer = asyncgi::Timer{responder};
1046 | timer.waitFuture(
1047 | std::async(
1048 | std::launch::async,
1049 | []
1050 | {
1051 | std::this_thread::sleep_for(std::chrono::seconds(3));
1052 | return "world";
1053 | }),
1054 | [responder](const std::string& result) mutable
1055 | {
1056 | responder.send(http::Response{"Hello " + result});
1057 | });
1058 | }
1059 | };
1060 |
1061 | int main()
1062 | {
1063 | auto io = asyncgi::IO{};
1064 | auto router = asyncgi::Router{io};
1065 | auto delayedPage = DelayedPage{};
1066 | router.route("/", http::RequestMethod::Get).process(delayedPage);
1067 | router.route().set(http::ResponseStatus::_404_Not_Found);
1068 | auto server = asyncgi::Server{io, router};
1069 | server.listen("/tmp/fcgi.sock");
1070 | io.run();
1071 | }
1072 | ```
1073 |
1074 |
1075 |
1076 | ### Client
1077 |
1078 | With `asyncgi::Client` it's possible to make direct requests to `FastCGI` applications. This
1079 | enables multiple `asyncgi`-based applications to communicate with each other without the need for other inter-process
1080 | communication solutions.
1081 |
1082 |
1083 | Example
1084 |
1085 | ```c++
1086 | ///examples/example_client.cpp
1087 | ///
1088 | #include
1089 | #include
1090 |
1091 | using namespace asyncgi;
1092 |
1093 | int main()
1094 | {
1095 | auto io = asyncgi::IO{};
1096 | auto client = asyncgi::Client{io};
1097 | client.makeRequest(
1098 | "/tmp/fcgi.sock",
1099 | http::Request{http::RequestMethod::Get, "/"},
1100 | [&io](const std::optional& response)
1101 | {
1102 | if (response)
1103 | std::cout << response->body() << std::endl;
1104 | else
1105 | std::cout << "No response" << std::endl;
1106 | io.stop();
1107 | });
1108 | io.run();
1109 | }
1110 | ```
1111 |
1112 |
1113 | To make FastCGI requests during request processing, a client object created from an `asyncgi::Responder` reference must
1114 | be used. It is important to avoid using this client object after the response has already been sent.
1115 |
1116 |
1117 | Example
1118 |
1119 | ```c++
1120 | ///examples/example_client_in_processor.cpp
1121 | ///
1122 | #include
1123 |
1124 | namespace http = asyncgi::http;
1125 |
1126 | struct RequestPage{
1127 | void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
1128 | {
1129 | // making request to FastCgi application listening on /tmp/fcgi.sock and showing the received response
1130 | auto client = asyncgi::Client{responder};
1131 | client.makeRequest(
1132 | "/tmp/fcgi.sock",
1133 | http::Request{http::RequestMethod::Get, "/"},
1134 | [responder](const std::optional& reqResponse) mutable
1135 | {
1136 | if (reqResponse)
1137 | responder.send(std::string{reqResponse->body()});
1138 | else
1139 | responder.send("No response");
1140 | });
1141 | }
1142 | };
1143 |
1144 | int main()
1145 | {
1146 | auto io = asyncgi::IO{};
1147 | auto router = asyncgi::Router{io};
1148 | router.route("/", http::RequestMethod::Get).process();
1149 | router.route().set(http::ResponseStatus::_404_Not_Found);
1150 | auto server = asyncgi::Server{io, router};
1151 | server.listen("/tmp/fcgi_client.sock");
1152 | io.run();
1153 | }
1154 | ```
1155 |
1156 |
1157 |
1158 |
1159 | ### Executing an asio task
1160 |
1161 | `asyncgi` internally uses the `asio` library. A dispatcher object `asyncgi::AsioDispatcher` can be created to invoke
1162 | callable objects that require access to the `asio::io_context` object.
1163 |
1164 |
1165 | Example
1166 |
1167 | ```c++
1168 | ///examples/example_asio_dispatcher.cpp
1169 | ///
1170 | #include
1171 | #include
1172 | #include
1173 |
1174 | int main()
1175 | {
1176 | auto io = asyncgi::IO{};
1177 | auto disp = asyncgi::AsioDispatcher{io};
1178 | disp.postTask(
1179 | [&io](const asyncgi::TaskContext& ctx) mutable
1180 | {
1181 | auto timer = std::make_shared(ctx.io());
1182 | timer->expires_after(std::chrono::seconds{3});
1183 | timer->async_wait(
1184 | [timer, ctx, &io](auto&) mutable
1185 | {
1186 | std::cout << "Hello world" << std::endl;
1187 | io.stop();
1188 | });
1189 | });
1190 | io.run();
1191 | }
1192 | ```
1193 |
1194 |
1195 |
1196 | To invoke such a callable object during request processing, a dispatcher created from an `asyncgi::Responder` reference
1197 | must be used. It is important to avoid using this dispatcher after the response has already been sent.
1198 |
1199 |
1200 | Example
1201 |
1202 | ```c++
1203 | ///examples/example_response_dispatching_asio_task.cpp
1204 | ///
1205 | #include
1206 | #include
1207 |
1208 | namespace http = asyncgi::http;
1209 |
1210 | struct DelayedPage {
1211 | void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
1212 | {
1213 | auto disp = asyncgi::AsioDispatcher{responder};
1214 | disp.postTask(
1215 | [responder](const asyncgi::TaskContext& ctx) mutable
1216 | {
1217 | auto timer = std::make_shared(ctx.io());
1218 | timer->expires_after(std::chrono::seconds{3});
1219 | timer->async_wait([timer, responder, ctx](auto&) mutable { // Note how we capture ctx object here,
1220 | responder.send("Hello world"); // it's necessary to keep it (or its copy) alive
1221 | }); // before the end of request processing
1222 | });
1223 | }
1224 | };
1225 |
1226 | int main()
1227 | {
1228 | auto io = asyncgi::IO{};
1229 | auto router = asyncgi::Router{io};
1230 | router.route("/", http::RequestMethod::Get).process();
1231 | router.route().set(http::ResponseStatus::_404_Not_Found);
1232 | auto server = asyncgi::Server{io, router};
1233 | server.listen("/tmp/fcgi.sock");
1234 | io.run();
1235 | }
1236 | ```
1237 |
1238 |
1239 |
1240 | To use `asyncgi` with the `Boost.Asio` library, set the `ASYNCGI_USE_BOOST_ASIO` CMake variable .
1241 |
1242 | ## Showcase
1243 |
1244 | * [`stone_skipper`](https://github.com/kamchatka-volcano/stone_skipper)
1245 |
1246 | ## Development status
1247 |
1248 | `asyncgi` is currently in the open beta stage, with all planned features complete. Until it reaches a non-zero major
1249 | version number, there may be frequent introductions of backward-incompatible changes.
1250 |
1251 | Unit tests are not included because most of the functionality in `asyncgi` is derived from the following libraries,
1252 | which already have their own test coverage:
1253 |
1254 | * [asio](https://github.com/chriskohlhoff/asio) - used for establishing connections, sending and receiving data.
1255 | * [fcgi_responder](https://github.com/kamchatka-volcano/fcgi_responder/) - implementation of the `FastCGI` protocol.
1256 | * [whaleroute](https://github.com/kamchatka-volcano/whaleroute/) - implementation of the request router.
1257 | * [hot_teacup](https://github.com/kamchatka-volcano/hot_teacup/) - parsing of HTTP data received over `FastCGI`
1258 | connections, forming HTTP responses.
1259 |
1260 | Instead of mocking code that integrates the functionality of these libraries, `asyncgi` is tested using functional
1261 | tests. These tests check the behavior of the executables found in the `examples/` directory when running with the NGINX
1262 | server. You can find these tests in the `functional_tests/` directory.
1263 |
1264 | ## Installation
1265 |
1266 | Download and link the library from your project's CMakeLists.txt:
1267 | ```
1268 | cmake_minimum_required(VERSION 3.14)
1269 |
1270 | include(FetchContent)
1271 |
1272 | FetchContent_Declare(cmdlime
1273 | GIT_REPOSITORY "https://github.com/kamchatka-volcano/asyncgi.git"
1274 | GIT_TAG "origin/master"
1275 | )
1276 | #uncomment if you need to install cmdlime with your target
1277 | #set(INSTALL_ASYNCGI ON)
1278 | FetchContent_MakeAvailable(asyncgi)
1279 |
1280 | add_executable(${PROJECT_NAME})
1281 | target_link_libraries(${PROJECT_NAME} PRIVATE asyncgi::asyncgi)
1282 | ```
1283 |
1284 | To install the library system-wide, use the following commands:
1285 | ```
1286 | git clone https://github.com/kamchatka-volcano/asyncgi.git
1287 | cd asyncgi
1288 | cmake -S . -B build
1289 | cmake --build build
1290 | cmake --install build
1291 | ```
1292 |
1293 | After installation, you can use the find_package() command to make the installed library available inside your project:
1294 |
1295 | ```
1296 | find_package(asyncgi 0.1.0 REQUIRED)
1297 | target_link_libraries(${PROJECT_NAME} PRIVATE asyncgi::asyncgi)
1298 | ```
1299 |
1300 | If you want to use the `Boost.Asio` library, `Boost` dependencies can be resolved
1301 | using [vcpkg](https://vcpkg.io/en/getting-started.html) by running the build with this command:
1302 |
1303 | ```
1304 | cmake -S . -B build -DASYNCGI_USE_BOOST_ASIO=ON -DCMAKE_TOOLCHAIN_FILE=/scripts/buildsystems/vcpkg.cmake
1305 | ```
1306 |
1307 | ## Building examples
1308 |
1309 | ```
1310 | cd asyncgi
1311 | cmake -S . -B build -DENABLE_EXAMPLES=ON
1312 | cmake --build build
1313 | cd build/examples
1314 | ```
1315 |
1316 | ## Running functional tests
1317 |
1318 | Download [`lunchtoast`](https://github.com/kamchatka-volcano/lunchtoast/releases) executable, build `asyncgi` examples
1319 | and start NGINX with `functional_tests/nginx_*.conf` config file.
1320 | Launch tests with the following command:
1321 |
1322 | * Linux command:
1323 |
1324 | ```
1325 | lunchtoast functional_tests
1326 | ```
1327 |
1328 | * Windows command:
1329 |
1330 | ```
1331 | lunchtoast.exe functional_tests -shell="msys2 -c" -skip=linux
1332 | ```
1333 |
1334 | To run functional tests on Windows, it's recommended to use the bash shell from the `msys2` project. After installing
1335 | it, add the following script `msys2.cmd` to your system `PATH`:
1336 |
1337 | ```bat
1338 | @echo off
1339 | setlocal
1340 | IF NOT DEFINED MSYS2_PATH_TYPE set MSYS2_PATH_TYPE=inherit
1341 | set CHERE_INVOKING=1
1342 | C:\\msys64\\usr\\bin\\bash.exe -leo pipefail %*
1343 | ```
1344 |
1345 | ## License
1346 |
1347 | **asyncgi** is licensed under the [MS-PL license](/LICENSE.md)
1348 |
1349 |
1350 |
1351 |
1352 |
1353 |
1354 |
--------------------------------------------------------------------------------
/doc/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kamchatka-volcano/asyncgi/f37bfa01f866ba16a06d510b566630485d0ddcb9/doc/logo.png
--------------------------------------------------------------------------------
/examples/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required(VERSION 3.18)
2 |
3 | file(GLOB SRC_FILES "*.cpp")
4 | foreach (SRC_FILE ${SRC_FILES})
5 | SealLake_StringAfterLast(${SRC_FILE} "/" EXAMPLE_NAME)
6 | SealLake_StringBeforeLast(${EXAMPLE_NAME} "." EXAMPLE_NAME)
7 |
8 | SealLake_Executable(
9 | NAME ${EXAMPLE_NAME}
10 | SOURCES ${SRC_FILE}
11 | COMPILE_FEATURES cxx_std_17
12 | PROPERTIES
13 | CXX_EXTENSIONS OFF
14 | LIBRARIES
15 | asyncgi::asyncgi Threads::Threads
16 | )
17 | if (EXAMPLE_NAME STREQUAL example_response_dispatching_asio_task OR
18 | EXAMPLE_NAME STREQUAL example_asio_dispatcher)
19 | if (ASYNCGI_USE_BOOST_ASIO)
20 | target_link_libraries(${EXAMPLE_NAME} PRIVATE Boost::boost)
21 | else()
22 | target_link_libraries(${EXAMPLE_NAME} PRIVATE asio)
23 | endif()
24 | endif ()
25 | endforeach ()
--------------------------------------------------------------------------------
/examples/example_asio_dispatcher.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #ifdef ASYNCGI_USE_BOOST_ASIO
3 | namespace asio = boost::asio;
4 | #include
5 | #else
6 | #include
7 | #endif
8 | #include
9 |
10 | int main()
11 | {
12 | auto io = asyncgi::IO{};
13 | auto disp = asyncgi::AsioDispatcher{io};
14 | disp.postTask(
15 | [&io](const asyncgi::TaskContext& ctx) mutable
16 | {
17 | auto timer = std::make_shared(ctx.io());
18 | timer->expires_after(std::chrono::seconds{3});
19 | timer->async_wait(
20 | [timer, ctx, &io](auto&) mutable
21 | {
22 | std::cout << "Hello world" << std::endl;
23 | io.stop();
24 | });
25 | });
26 | io.run();
27 | return 0;
28 | }
--------------------------------------------------------------------------------
/examples/example_client.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | using namespace asyncgi;
5 |
6 | int main()
7 | {
8 | auto io = asyncgi::IO{};
9 | auto client = asyncgi::Client{io};
10 | client.makeRequest(
11 | #ifndef _WIN32
12 | "/tmp/fcgi.sock",
13 | #else
14 | "127.0.0.1",
15 | 9088,
16 | #endif
17 | http::Request{http::RequestMethod::Get, "/"},
18 | [&io](const std::optional& response)
19 | {
20 | if (response)
21 | std::cout << response->body() << std::endl;
22 | else
23 | std::cout << "No response" << std::endl;
24 | io.stop();
25 | });
26 | io.run();
27 | return 0;
28 | }
--------------------------------------------------------------------------------
/examples/example_client_in_processor.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | namespace http = asyncgi::http;
4 |
5 | struct RequestPage{
6 | void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
7 | {
8 | // making request to FastCgi application listening on /tmp/fcgi.sock and showing the received response
9 | auto client = asyncgi::Client{responder};
10 | client.makeRequest(
11 | #ifndef _WIN32
12 | "/tmp/fcgi.sock",
13 | #else
14 | "127.0.0.1",
15 | 9088,
16 | #endif
17 | http::Request{http::RequestMethod::Get, "/"},
18 | [responder](const std::optional& reqResponse) mutable
19 | {
20 | if (reqResponse)
21 | responder.send(std::string{reqResponse->body()});
22 | else
23 | responder.send("No response");
24 | });
25 | }
26 | };
27 |
28 | int main()
29 | {
30 | auto io = asyncgi::IO{};
31 | auto router = asyncgi::Router{io};
32 | router.route("/", http::RequestMethod::Get).process();
33 | router.route().set(http::ResponseStatus::_404_Not_Found);
34 | auto server = asyncgi::Server{io, router};
35 | #ifndef _WIN32
36 | server.listen("/tmp/fcgi_client.sock");
37 | #else
38 | server.listen("127.0.0.1", 9089);
39 | #endif
40 | io.run();
41 | return 0;
42 | }
--------------------------------------------------------------------------------
/examples/example_guestbook.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | namespace http = asyncgi::http;
7 | using namespace std::string_literals;
8 |
9 | enum class AccessRole {
10 | Admin,
11 | Guest
12 | };
13 |
14 | struct RouteContext {
15 | AccessRole role = AccessRole::Guest;
16 | };
17 |
18 | template<>
19 | struct asyncgi::config::RouteMatcher {
20 | bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
21 | {
22 | return value == context.role;
23 | }
24 | };
25 |
26 | std::optional authorizeAdmin(const asyncgi::Request& request, RouteContext& context)
27 | {
28 | if (request.cookie("admin_id") == "ADMIN_SECRET")
29 | context.role = AccessRole::Admin;
30 |
31 | return std::nullopt;
32 | }
33 |
34 | http::Response showLoginPage(const asyncgi::Request&)
35 | {
36 | return {R"(
37 |
38 |
39 | Login:
40 |
41 | Password:
42 |
43 |
44 | )"};
45 | }
46 |
47 | http::Response loginAdmin(const asyncgi::Request& request)
48 | {
49 | if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
50 | return {http::Redirect{"/"}, {http::Cookie("admin_id", "ADMIN_SECRET")}};
51 | else
52 | return http::Redirect{"/login"};
53 | }
54 |
55 | http::Response logoutAdmin(const asyncgi::Request&)
56 | {
57 | return {http::Redirect{"/"}, {http::Cookie("admin_id", "")}};
58 | }
59 |
60 | struct GuestBookMessage {
61 | std::string name;
62 | std::string text;
63 | };
64 |
65 | class GuestBookState {
66 | public:
67 | std::vector messages()
68 | {
69 | auto lock = std::scoped_lock{mutex_};
70 | return messages_;
71 | }
72 |
73 | void addMessage(std::string name, std::string msg)
74 | {
75 | name = std::regex_replace(name, std::regex{"<"}, "<");
76 | name = std::regex_replace(name, std::regex{">"}, ">");
77 | msg = std::regex_replace(msg, std::regex{"<"}, "<");
78 | msg = std::regex_replace(msg, std::regex{">"}, ">");
79 | auto lock = std::scoped_lock{mutex_};
80 | messages_.emplace_back(GuestBookMessage{name.empty() ? "Guest" : name, msg});
81 | }
82 |
83 | void removeMessage(int index)
84 | {
85 | auto lock = std::scoped_lock{mutex_};
86 | if (index < 0 || index >= static_cast(messages_.size()))
87 | return;
88 | messages_.erase(std::next(messages_.begin(), index));
89 | }
90 |
91 | private:
92 | std::vector messages_;
93 | std::mutex mutex_;
94 | };
95 |
96 | std::string makeMessagesDiv(const std::vector& messages, AccessRole role)
97 | {
98 | if (messages.empty())
99 | return "No messages
";
100 |
101 | auto messagesDiv = std::string{};
102 | for (auto i = 0; i < static_cast(messages.size()); ++i) {
103 | messagesDiv += "" + messages.at(i).name + " says:" + messages.at(i).text + " ";
104 | if (role == AccessRole::Admin)
105 | messagesDiv += R"( )";
107 | }
108 | return messagesDiv;
109 | }
110 |
111 | std::string makeLinksDiv(AccessRole role)
112 | {
113 | return (role == AccessRole::Admin ? R"(logout )"s
114 | : R"(login )"s) +
115 | R"(source )"s;
116 | }
117 |
118 | auto showGuestBookPage(GuestBookState& state)
119 | {
120 | return [&state](const asyncgi::Request& request, RouteContext& context) -> http::Response
121 | {
122 | auto page = R"(
123 | %LINKS%
124 |
125 |
asyncgi guest book
126 |
127 |
128 | %MESSAGES%
129 |
130 |
131 |
132 |
143 |
)"s;
144 |
145 | page = std::regex_replace(page, std::regex{"%MESSAGES%"}, makeMessagesDiv(state.messages(), context.role));
146 | page = std::regex_replace(page, std::regex{"%LINKS%"}, makeLinksDiv(context.role));
147 | if (request.hasQuery("error")) {
148 | if (request.query("error") == "urls_in_msg")
149 | page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "Messages can't contain urls ");
150 | if (request.query("error") == "empty_msg")
151 | page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "Messages can't be empty ");
152 | }
153 | else
154 | page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "");
155 |
156 | return page;
157 | };
158 | }
159 |
160 | auto addMessage(GuestBookState& state)
161 | {
162 | return [&state](const asyncgi::Request& request) -> http::Response
163 | {
164 | if (std::all_of(
165 | request.formField("msg").begin(),
166 | request.formField("msg").end(),
167 | [](char ch)
168 | {
169 | return std::isspace(static_cast(ch));
170 | }))
171 | return http::Redirect{"/?error=empty_msg"};
172 | else if (
173 | request.formField("msg").find("http://") != std::string_view::npos ||
174 | request.formField("msg").find("https://") != std::string_view::npos)
175 | return http::Redirect{"/?error=urls_in_msg"};
176 | else {
177 | state.addMessage(std::string{request.formField("name")}, std::string{request.formField("msg")});
178 | return http::Redirect{"/"};
179 | }
180 | };
181 | }
182 |
183 | auto removeMessage(GuestBookState& state)
184 | {
185 | return [&state](int index, const asyncgi::Request&) -> http::Response
186 | {
187 | state.removeMessage(index);
188 | return http::Redirect{"/"};
189 | };
190 | }
191 |
192 | int main()
193 | {
194 | auto io = asyncgi::IO{4};
195 | auto state = GuestBookState{};
196 | auto router = asyncgi::Router{io};
197 | router.route(asyncgi::rx{".*"}).process(authorizeAdmin);
198 | router.route("/", http::RequestMethod::Get).process(showGuestBookPage(state));
199 | router.route("/", http::RequestMethod::Post).process(addMessage(state));
200 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post, AccessRole::Admin)
201 | .process(removeMessage(state));
202 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post, AccessRole::Guest)
203 | .set(http::ResponseStatus::_401_Unauthorized);
204 | router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process(showLoginPage);
205 | router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process(loginAdmin);
206 | router.route("/logout").process(logoutAdmin);
207 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
208 |
209 | auto server = asyncgi::Server{io, router};
210 | #ifndef _WIN32
211 | server.listen("/tmp/fcgi.sock");
212 | #else
213 | server.listen("127.0.0.1", 9088);
214 | #endif
215 | io.run();
216 | return 0;
217 | }
--------------------------------------------------------------------------------
/examples/example_hello_world.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | namespace http = asyncgi::http;
4 |
5 | int main()
6 | {
7 | auto io = asyncgi::IO{};
8 | auto router = asyncgi::Router{io};
9 | router.route("/", http::RequestMethod::Get)
10 | .process(
11 | [](const asyncgi::Request&)
12 | {
13 | return http::Response{"Hello world"};
14 | });
15 |
16 | auto server = asyncgi::Server{io, router};
17 | #ifndef _WIN32
18 | server.listen("/tmp/fcgi.sock");
19 | #else
20 | server.listen("127.0.0.1", 9088);
21 | #endif
22 | io.run();
23 | return 0;
24 | }
--------------------------------------------------------------------------------
/examples/example_request_processor.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | namespace http = asyncgi::http;
4 |
5 | http::Response guestBookPage(const asyncgi::Request& request)
6 | {
7 | if (request.path() == "/")
8 | return {R"(
9 | Guest book
10 | No messages
11 | )"};
12 |
13 | return http::ResponseStatus::_404_Not_Found;
14 | }
15 |
16 | int main()
17 | {
18 | auto io = asyncgi::IO{};
19 | auto server = asyncgi::Server{io, guestBookPage};
20 | #ifndef _WIN32
21 | server.listen("/tmp/fcgi.sock");
22 | #else
23 | server.listen("127.0.0.1", 9088);
24 | #endif
25 | io.run();
26 | return 0;
27 | }
--------------------------------------------------------------------------------
/examples/example_response_dispatching_asio_task.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #ifdef ASYNCGI_USE_BOOST_ASIO
3 | namespace asio = boost::asio;
4 | #include
5 | #else
6 | #include
7 | #endif
8 |
9 | namespace http = asyncgi::http;
10 |
11 | struct DelayedPage {
12 | void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
13 | {
14 | auto disp = asyncgi::AsioDispatcher{responder};
15 | disp.postTask(
16 | [responder](const asyncgi::TaskContext& ctx) mutable
17 | {
18 | auto timer = std::make_shared(ctx.io());
19 | timer->expires_after(std::chrono::seconds{3});
20 | timer->async_wait([timer, responder, ctx](auto&) mutable { // Note how we capture ctx object here,
21 | responder.send("Hello world"); // it's necessary to keep it (or its copy) alive
22 | }); // before the end of request processing
23 | });
24 | }
25 | };
26 |
27 | int main()
28 | {
29 | auto io = asyncgi::IO{};
30 | auto router = asyncgi::Router{io};
31 | router.route("/", http::RequestMethod::Get).process();
32 | router.route().set(http::ResponseStatus::_404_Not_Found);
33 | auto server = asyncgi::Server{io, router};
34 | #ifndef _WIN32
35 | server.listen("/tmp/fcgi.sock");
36 | #else
37 | server.listen("127.0.0.1", 9088);
38 | #endif
39 | io.run();
40 | return 0;
41 | }
--------------------------------------------------------------------------------
/examples/example_response_wait_future.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | using namespace asyncgi;
5 |
6 | struct DelayedPage{
7 | void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
8 | {
9 | auto timer = asyncgi::Timer{responder};
10 | timer.waitFuture(
11 | std::async(
12 | std::launch::async,
13 | []
14 | {
15 | std::this_thread::sleep_for(std::chrono::seconds(3));
16 | return "world";
17 | }),
18 | [responder](const std::string& result) mutable
19 | {
20 | responder.send(http::Response{"Hello " + result});
21 | });
22 | }
23 | };
24 |
25 | int main()
26 | {
27 | auto io = asyncgi::IO{};
28 | auto router = asyncgi::Router{io};
29 | auto delayedPage = DelayedPage{};
30 | router.route("/", http::RequestMethod::Get).process(delayedPage);
31 | router.route().set(http::ResponseStatus::_404_Not_Found);
32 | auto server = asyncgi::Server{io, router};
33 | #ifndef _WIN32
34 | server.listen("/tmp/fcgi.sock");
35 | #else
36 | server.listen("127.0.0.1", 9088);
37 | #endif
38 | io.run();
39 | return 0;
40 | }
--------------------------------------------------------------------------------
/examples/example_route_context.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | namespace http = asyncgi::http;
6 | using namespace std::string_literals;
7 |
8 | enum class AccessRole {
9 | Admin,
10 | Guest
11 | };
12 |
13 | struct RouteContext {
14 | AccessRole role = AccessRole::Guest;
15 | };
16 |
17 | struct AdminAuthorizer {
18 | std::optional operator()(const asyncgi::Request& request, RouteContext& context)
19 | {
20 | if (request.cookie("admin_id") == "ADMIN_SECRET")
21 | context.role = AccessRole::Admin;
22 |
23 | return std::nullopt;
24 | }
25 | };
26 |
27 | struct LoginPage {
28 | http::Response operator()(const asyncgi::Request&, RouteContext& context)
29 | {
30 | if (context.role == AccessRole::Guest)
31 | return {R"(
32 |
33 |
34 | Login:
35 |
36 | Password:
37 |
38 |
39 | )"};
40 | else //We are already logged in as the administrator
41 | return http::Redirect{"/"};
42 | }
43 | };
44 |
45 | struct LoginPageAuthorize {
46 | http::Response operator()(const asyncgi::Request& request, RouteContext& context)
47 | {
48 | if (context.role == AccessRole::Guest) {
49 | if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
50 | return {http::Redirect{"/"}, {asyncgi::http::Cookie("admin_id", "ADMIN_SECRET")}};
51 | else
52 | return http::Redirect{"/login"};
53 | }
54 | else //We are already logged in as the administrator
55 | return http::Redirect{"/"};
56 | }
57 | };
58 |
59 | int main()
60 | {
61 | auto io = asyncgi::IO{4}; //4 threads processing requests
62 | auto router = asyncgi::Router{io};
63 | router.route(asyncgi::rx{".*"}).process();
64 | router.route("/").process(
65 | [](const asyncgi::Request&, asyncgi::Responder& response, RouteContext& context)
66 | {
67 | if (context.role == AccessRole::Admin)
68 | response.send("Hello admin
");
69 | else
70 | response.send(R"(Hello guest
login )");
71 | });
72 |
73 | router.route("/login", http::RequestMethod::Get).process();
74 | router.route("/login", http::RequestMethod::Post).process();
75 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
76 |
77 | auto server = asyncgi::Server{io, router};
78 | #ifndef _WIN32
79 | server.listen("/tmp/fcgi.sock");
80 | #else
81 | server.listen("127.0.0.1", 9088);
82 | #endif
83 | io.run();
84 | return 0;
85 | }
--------------------------------------------------------------------------------
/examples/example_route_matcher.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | namespace http = asyncgi::http;
6 | using namespace std::string_literals;
7 |
8 | enum class AccessRole {
9 | Admin,
10 | Guest
11 | };
12 |
13 | struct RouteContext {
14 | AccessRole role = AccessRole::Guest;
15 | };
16 |
17 | struct AdminAuthorizer {
18 | std::optional operator()(const asyncgi::Request& request, RouteContext& context)
19 | {
20 | if (request.cookie("admin_id") == "ADMIN_SECRET")
21 | context.role = AccessRole::Admin;
22 |
23 | return std::nullopt;
24 | }
25 | };
26 |
27 | struct LoginPage {
28 | http::Response operator()(const asyncgi::Request&)
29 | {
30 | return {R"(
31 |
32 |
33 | Login:
34 |
35 | Password:
36 |
37 |
38 | )"};
39 | }
40 | };
41 |
42 | struct LoginPageAuthorize {
43 | http::Response operator()(const asyncgi::Request& request)
44 | {
45 | if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
46 | return {http::Redirect{"/"}, {asyncgi::http::Cookie("admin_id", "ADMIN_SECRET")}};
47 |
48 | return http::Redirect{"/login"};
49 | }
50 | };
51 |
52 | template<>
53 | struct asyncgi::config::RouteMatcher {
54 | bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
55 | {
56 | return value == context.role;
57 | }
58 | };
59 |
60 | int main()
61 | {
62 | auto io = asyncgi::IO{4};
63 | auto router = asyncgi::Router{io};
64 | router.route(asyncgi::rx{".*"}).process();
65 | router.route("/").process(
66 | [](const asyncgi::Request&, RouteContext& context) -> http::Response
67 | {
68 | if (context.role == AccessRole::Admin)
69 | return {"Hello admin
"};
70 | else
71 | return {R"(Hello guest
login )"};
72 | });
73 |
74 | router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process();
75 | router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process();
76 | router.route("/login", http::RequestMethod::Get, AccessRole::Admin).set("/", http::RedirectType::Found);
77 | router.route("/login", http::RequestMethod::Post, AccessRole::Admin).set("/", http::RedirectType::Found);
78 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
79 |
80 | auto server = asyncgi::Server{io, router};
81 | #ifndef _WIN32
82 | server.listen("/tmp/fcgi.sock");
83 | #else
84 | server.listen("127.0.0.1", 9088);
85 | #endif
86 | io.run();
87 | return 0;
88 | }
--------------------------------------------------------------------------------
/examples/example_route_params.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | using namespace asyncgi;
5 | using namespace std::string_literals;
6 |
7 | class GuestBookState {
8 | public:
9 | std::vector messages()
10 | {
11 | auto lock = std::scoped_lock{mutex_};
12 | return messages_;
13 | }
14 |
15 | void addMessage(const std::string& msg)
16 | {
17 | auto lock = std::scoped_lock{mutex_};
18 | messages_.emplace_back(msg);
19 | }
20 |
21 | void removeMessage(int index)
22 | {
23 | auto lock = std::scoped_lock{mutex_};
24 | if (index < 0 || index >= static_cast(messages_.size()))
25 | return;
26 | messages_.erase(std::next(messages_.begin(), index));
27 | }
28 |
29 | private:
30 | std::vector messages_;
31 | std::mutex mutex_;
32 | };
33 |
34 | std::string makeMessage(int index, const std::string& msg)
35 | {
36 | return msg + R"( )";
38 | }
39 |
40 | class GuestBookPage {
41 | public:
42 | explicit GuestBookPage(GuestBookState& state)
43 | : state_{&state}
44 | {
45 | }
46 |
47 | http::Response operator()(const asyncgi::Request&)
48 | {
49 | auto messages = state_->messages();
50 | auto page = "Guest book "s;
51 | if (messages.empty())
52 | page += "No messages
";
53 | else
54 | for (auto i = 0; i < static_cast(messages.size()); ++i)
55 | page += "" + makeMessage(i, messages.at(i)) + "
";
56 |
57 | page += " ";
58 | page += ""
59 | "Message: "
60 | " "
61 | " "
62 | " ";
63 | return page;
64 | }
65 |
66 | private:
67 | GuestBookState* state_;
68 | };
69 |
70 | class GuestBookAddMessage {
71 | public:
72 | explicit GuestBookAddMessage(GuestBookState& state)
73 | : state_{&state}
74 | {
75 | }
76 |
77 | http::Response operator()(const asyncgi::Request& request)
78 | {
79 | state_->addMessage(std::string{request.formField("msg")});
80 | return http::Redirect{"/"};
81 | }
82 |
83 | private:
84 | GuestBookState* state_;
85 | };
86 |
87 | class GuestBookRemoveMessage {
88 | public:
89 | explicit GuestBookRemoveMessage(GuestBookState& state)
90 | : state_{&state}
91 | {
92 | }
93 |
94 | http::Response operator()(int index, const asyncgi::Request&)
95 | {
96 | state_->removeMessage(index);
97 | return http::Redirect{"/"};
98 | }
99 |
100 | private:
101 | GuestBookState* state_;
102 | };
103 |
104 | int main()
105 | {
106 | auto io = asyncgi::IO{4};
107 | auto state = GuestBookState{};
108 | auto router = asyncgi::Router{io};
109 | router.route("/", http::RequestMethod::Get).process(state);
110 | router.route("/", http::RequestMethod::Post).process(state);
111 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process(state);
112 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
113 |
114 | auto server = asyncgi::Server{io, router};
115 | #ifndef _WIN32
116 | server.listen("/tmp/fcgi.sock");
117 | #else
118 | server.listen("127.0.0.1", 9088);
119 | #endif
120 | io.run();
121 | return 0;
122 | }
--------------------------------------------------------------------------------
/examples/example_route_params_user_defined_types.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | using namespace asyncgi;
5 | using namespace std::string_literals;
6 |
7 | struct MessageNumber {
8 | int value;
9 | };
10 |
11 | template<>
12 | struct asyncgi::config::StringConverter {
13 | static std::optional fromString(const std::string& data)
14 | {
15 | return MessageNumber{std::stoi(data)};
16 | }
17 | };
18 |
19 | class GuestBookState {
20 | public:
21 | std::vector messages()
22 | {
23 | auto lock = std::scoped_lock{mutex_};
24 | return messages_;
25 | }
26 |
27 | void addMessage(const std::string& msg)
28 | {
29 | auto lock = std::scoped_lock{mutex_};
30 | messages_.emplace_back(msg);
31 | }
32 |
33 | void removeMessage(int index)
34 | {
35 | auto lock = std::scoped_lock{mutex_};
36 | if (index < 0 || index >= static_cast(messages_.size()))
37 | return;
38 | messages_.erase(std::next(messages_.begin(), index));
39 | }
40 |
41 | private:
42 | std::vector messages_;
43 | std::mutex mutex_;
44 | };
45 |
46 | std::string makeMessage(int index, const std::string& msg)
47 | {
48 | return msg + R"( )";
50 | }
51 |
52 | class GuestBookPage {
53 | public:
54 | explicit GuestBookPage(GuestBookState& state)
55 | : state_{&state}
56 | {
57 | }
58 |
59 | http::Response operator()(const asyncgi::Request&)
60 | {
61 | auto messages = state_->messages();
62 | auto page = "Guest book "s;
63 | if (messages.empty())
64 | page += "No messages
";
65 | else
66 | for (auto i = 0; i < static_cast(messages.size()); ++i)
67 | page += "" + makeMessage(i, messages.at(i)) + "
";
68 |
69 | page += " ";
70 | page += ""
71 | "Message: "
72 | " "
73 | " "
74 | " ";
75 | return page;
76 | }
77 |
78 | private:
79 | GuestBookState* state_;
80 | };
81 |
82 | class GuestBookAddMessage {
83 | public:
84 | explicit GuestBookAddMessage(GuestBookState& state)
85 | : state_{&state}
86 | {
87 | }
88 |
89 | http::Response operator()(const asyncgi::Request& request)
90 | {
91 | state_->addMessage(std::string{request.formField("msg")});
92 | return http::Redirect{"/"};
93 | }
94 |
95 | private:
96 | GuestBookState* state_;
97 | };
98 |
99 | class GuestBookRemoveMessage {
100 | public:
101 | explicit GuestBookRemoveMessage(GuestBookState& state)
102 | : state_{&state}
103 | {
104 | }
105 |
106 | http::Response operator()(MessageNumber msgNumber, const asyncgi::Request&)
107 | {
108 | state_->removeMessage(msgNumber.value);
109 | return http::Redirect{"/"};
110 | }
111 |
112 | private:
113 | GuestBookState* state_;
114 | };
115 |
116 | int main()
117 | {
118 | auto io = asyncgi::IO{4};
119 | auto state = GuestBookState{};
120 | auto router = asyncgi::Router{io};
121 | router.route("/", http::RequestMethod::Get).process(state);
122 | router.route("/", http::RequestMethod::Post).process(state);
123 | router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process(state);
124 | router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
125 |
126 | auto server = asyncgi::Server{io, router};
127 | #ifndef _WIN32
128 | server.listen("/tmp/fcgi.sock");
129 | #else
130 | server.listen("127.0.0.1", 9088);
131 | #endif
132 | io.run();
133 | return 0;
134 | }
--------------------------------------------------------------------------------
/examples/example_router.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | namespace http = asyncgi::http;
5 | using namespace std::string_literals;
6 |
7 | class GuestBookState {
8 | public:
9 | std::vector messages()
10 | {
11 | auto lock = std::scoped_lock{mutex_};
12 | return messages_;
13 | }
14 |
15 | void addMessage(const std::string& msg)
16 | {
17 | auto lock = std::scoped_lock{mutex_};
18 | messages_.emplace_back(msg);
19 | }
20 |
21 | private:
22 | std::vector messages_;
23 | std::mutex mutex_;
24 | };
25 |
26 | class GuestBookPage {
27 | public:
28 | GuestBookPage(GuestBookState& state)
29 | : state_(&state)
30 | {
31 | }
32 |
33 | http::Response operator()(const asyncgi::Request&)
34 | {
35 | auto messages = state_->messages();
36 | auto page = "Guest book "s;
37 | if (messages.empty())
38 | page += "No messages
";
39 | else
40 | for (const auto& msg : messages)
41 | page += "" + msg + "
";
42 |
43 | page += " ";
44 | page += ""
45 | "Message: "
46 | " "
47 | " "
48 | " ";
49 | return page;
50 | }
51 |
52 | private:
53 | GuestBookState* state_;
54 | };
55 |
56 | class GuestBookAddMessage {
57 | public:
58 | GuestBookAddMessage(GuestBookState& state)
59 | : state_(&state)
60 | {
61 | }
62 |
63 | http::Response operator()(const asyncgi::Request& request)
64 | {
65 | state_->addMessage(std::string{request.formField("msg")});
66 | return http::Redirect{"/"};
67 | }
68 |
69 | private:
70 | GuestBookState* state_;
71 | };
72 |
73 | int main()
74 | {
75 | auto io = asyncgi::IO{4}; //4 threads processing requests
76 | auto state = GuestBookState{};
77 | auto router = asyncgi::Router{io};
78 | router.route("/", http::RequestMethod::Get).process(state);
79 | router.route("/", http::RequestMethod::Post).process(state);
80 | router.route().set(http::Response{http::ResponseStatus::_404_Not_Found, "Page not found"});
81 | //Alternatively, it's possible to pass arguments for creation of http::Response object to the set() method.
82 | //router.route().set(http::ResponseStatus::Code_404_Not_Found, "Page not found");
83 |
84 | auto server = asyncgi::Server{io, router};
85 | #ifndef _WIN32
86 | server.listen("/tmp/fcgi.sock");
87 | #else
88 | server.listen("127.0.0.1", 9088);
89 | #endif
90 | io.run();
91 | return 0;
92 | }
--------------------------------------------------------------------------------
/examples/example_timer.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | namespace http = asyncgi::http;
4 |
5 | struct Greeter{
6 | Greeter(const int& secondsCounter)
7 | : secondsCounter_{&secondsCounter}
8 | {
9 | }
10 |
11 | http::Response operator()(const asyncgi::Request&)
12 | {
13 | return "Hello world\n(alive for " + std::to_string(*secondsCounter_) + " seconds)";
14 | }
15 |
16 | private:
17 | const int* secondsCounter_;
18 | };
19 |
20 | int main()
21 | {
22 | auto io = asyncgi::IO{};
23 | int secondsCounter = 0;
24 |
25 | auto timer = asyncgi::Timer{io};
26 | timer.startPeriodic(
27 | std::chrono::seconds(1),
28 | [&secondsCounter]()
29 | {
30 | ++secondsCounter;
31 | });
32 |
33 | auto router = asyncgi::Router{io};
34 | router.route("/").process(secondsCounter);
35 | router.route().set(http::ResponseStatus::_404_Not_Found);
36 |
37 | auto server = asyncgi::Server{io, router};
38 | #ifndef _WIN32
39 | server.listen("/tmp/fcgi.sock");
40 | #else
41 | server.listen("127.0.0.1", 9088);
42 | #endif
43 | io.run();
44 | return 0;
45 | }
--------------------------------------------------------------------------------
/external/asio:
--------------------------------------------------------------------------------
1 | set(THREADS_PREFER_PTHREAD_FLAG ON)
2 | find_package(Threads REQUIRED)
3 |
4 | include(FetchContent)
5 | Set(FETCHCONTENT_QUIET FALSE)
6 |
7 | if (NOT TARGET asio)
8 | FetchContent_Declare(asio
9 | GIT_REPOSITORY https://github.com/chriskohlhoff/asio.git
10 | GIT_TAG asio-1-24-0
11 | GIT_SHALLOW ON
12 | GIT_PROGRESS TRUE
13 | CONFIGURE_COMMAND ""
14 | BUILD_COMMAND ""
15 | )
16 |
17 | FetchContent_GetProperties(asio)
18 | if(NOT asio_POPULATED)
19 | FetchContent_Populate(asio)
20 | endif()
21 |
22 | add_library(asio INTERFACE)
23 | target_include_directories(asio INTERFACE ${asio_SOURCE_DIR}/asio/include)
24 | target_link_libraries(asio INTERFACE Threads::Threads)
25 | endif()
--------------------------------------------------------------------------------
/external/seal_lake:
--------------------------------------------------------------------------------
1 | include(FetchContent)
2 | set(SEAL_LAKE_VERSION v0.2.0)
3 | set(FETCHCONTENT_QUIET FALSE)
4 | FetchContent_Declare(seal_lake_${SEAL_LAKE_VERSION}
5 | SOURCE_DIR seal_lake_${SEAL_LAKE_VERSION}
6 | GIT_REPOSITORY "https://github.com/kamchatka-volcano/seal_lake.git"
7 | GIT_TAG ${SEAL_LAKE_VERSION}
8 | )
9 | FetchContent_MakeAvailable(seal_lake_${SEAL_LAKE_VERSION})
10 | include(${seal_lake_${SEAL_LAKE_VERSION}_SOURCE_DIR}/seal_lake.cmake)
--------------------------------------------------------------------------------
/functional_tests/asio_dispatcher/test.toast:
--------------------------------------------------------------------------------
1 | -Launch: ../../build/examples/example_asio_dispatcher
2 | -Expect output:
3 | Hello world
4 |
5 | ---
--------------------------------------------------------------------------------
/functional_tests/client/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_hello_world
2 | -Wait: 1 sec
3 | -Launch: ../../build/examples/example_client
4 | -Expect output:
5 | Hello world
6 |
7 | ---
8 |
--------------------------------------------------------------------------------
/functional_tests/client_in_processor/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_hello_world
2 | -Launch detached: ../../build/examples/example_client_in_processor
3 | -Wait: 1 sec
4 |
5 | -Expect response from "/" on port 8089:
6 | Hello world
7 | ---
8 |
9 | -Expect status from "/foo" on port 8089: 404
--------------------------------------------------------------------------------
/functional_tests/exception_in_request_handler/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/test_examples/test_exception_in_request_handler
2 | -Wait: 1 sec
3 |
4 | -Expect status from "/": 500
5 | -Expect status from "/foo": 404
--------------------------------------------------------------------------------
/functional_tests/fastcgi_params:
--------------------------------------------------------------------------------
1 |
2 | fastcgi_param QUERY_STRING $query_string;
3 | fastcgi_param REQUEST_METHOD $request_method;
4 | fastcgi_param CONTENT_TYPE $content_type;
5 | fastcgi_param CONTENT_LENGTH $content_length;
6 |
7 | fastcgi_param SCRIPT_NAME $fastcgi_script_name;
8 | fastcgi_param REQUEST_URI $request_uri;
9 | fastcgi_param DOCUMENT_URI $document_uri;
10 | fastcgi_param DOCUMENT_ROOT $document_root;
11 | fastcgi_param SERVER_PROTOCOL $server_protocol;
12 | fastcgi_param REQUEST_SCHEME $scheme;
13 | fastcgi_param HTTPS $https if_not_empty;
14 |
15 | fastcgi_param GATEWAY_INTERFACE CGI/1.1;
16 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
17 |
18 | fastcgi_param REMOTE_ADDR $remote_addr;
19 | fastcgi_param REMOTE_PORT $remote_port;
20 | fastcgi_param SERVER_ADDR $server_addr;
21 | fastcgi_param SERVER_PORT $server_port;
22 | fastcgi_param SERVER_NAME $server_name;
23 |
24 | # PHP only, required if PHP was built with --enable-force-cgi-redirect
25 | fastcgi_param REDIRECT_STATUS 200;
26 |
--------------------------------------------------------------------------------
/functional_tests/hello_world/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_hello_world
2 | -Wait: 1 sec
3 |
4 | -Expect response from "/":
5 | Hello world
6 | ---
7 |
8 | -Expect status from "/foo": 404
--------------------------------------------------------------------------------
/functional_tests/lunchtoast.cfg:
--------------------------------------------------------------------------------
1 | #actions:
2 | ###
3 | format = Expect response from "%1"
4 | command = `curl -b cookies.txt --silent http://localhost:8088%1 | awk '{$1=$1};NF' | grep "\S" | head -c -1`
5 | checkOutput = %input
6 | ###
7 | format = Expect status from "%1"
8 | command = `curl -c cookies.txt --silent -i http://localhost:8088%1 | head -n 1 | cut -d ' ' -f 2 | head -c -1`
9 | checkOutput = %input
10 | ###
11 | format = Expect response from "%1" on port %2
12 | command = `curl -b cookies.txt --silent http://localhost:%2%1 | awk '{$1=$1};NF' | grep "\S" | head -c -1`
13 | checkOutput = %input
14 | ###
15 | format = Expect status from "%1" on port %2
16 | command = `curl -c cookies.txt --silent -i http://localhost:%2%1 | head -n 1 | cut -d ' ' -f 2 | head -c -1`
17 | checkOutput = %input
18 | ###
19 | format = Expect status from post request "%1"
20 | command = `curl -c cookies.txt --silent -i -X POST http://localhost:8088%1 | head -n 1 | cut -d ' ' -f 2 | head -c -1`
21 | checkOutput = %input
22 | ###
23 | format = Expect status from "%1" with form param "%2"
24 | command = `curl -c cookies.txt --silent -i -F "%2" http://localhost:8088%1 | head -n 1 | cut -d ' ' -f 2 | head -c -1`
25 | checkOutput = %input
26 | ###
27 | format = Expect status from "%1" with form param "%2" and form param "%3"
28 | command = `curl -c cookies.txt --silent -i -F "%2" -F "%3" http://localhost:8088%1 | head -n 1 | cut -d ' ' -f 2 | head -c -1`
29 | checkOutput = %input
30 | ###
31 | format = Expect cookies
32 | command = cat cookies.txt | head -c -1
33 | checkOutput = %input
34 |
--------------------------------------------------------------------------------
/functional_tests/nginx_linux.conf:
--------------------------------------------------------------------------------
1 | #user nobody;
2 | worker_processes 1;
3 | error_log error.log;
4 | error_log error.log notice;
5 | error_log error.log info;
6 | pid nginx.pid;
7 |
8 | events {
9 | worker_connections 1024;
10 | }
11 |
12 |
13 | http {
14 | default_type application/octet-stream;
15 | sendfile on;
16 | #tcp_nopush on;
17 |
18 | #keepalive_timeout 0;
19 | keepalive_timeout 65;
20 |
21 | server {
22 | listen 8088;
23 | server_name localhost;
24 | index /~;
25 | location / {
26 | try_files $uri $uri/ @fcgi;
27 | }
28 | access_log access.log;
29 |
30 | location @fcgi {
31 | fastcgi_pass unix:/tmp/fcgi.sock;
32 | #or using a TCP socket
33 | #fastcgi_pass localhost:9000;
34 | include fastcgi_params;
35 | fastcgi_keep_conn off;
36 | }
37 | }
38 |
39 | server {
40 | listen 8089;
41 | server_name localhost;
42 | index /~;
43 | location / {
44 | try_files $uri $uri/ @fcgi;
45 | }
46 | access_log access.log;
47 |
48 | location @fcgi {
49 | fastcgi_pass unix:/tmp/fcgi_client.sock;
50 | #or using a TCP socket
51 | #fastcgi_pass localhost:9000;
52 | include fastcgi_params;
53 | fastcgi_keep_conn off;
54 | }
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/functional_tests/nginx_windows.conf:
--------------------------------------------------------------------------------
1 | #user nobody;
2 | worker_processes 1;
3 | error_log error.log;
4 | error_log error.log notice;
5 | error_log error.log info;
6 | pid nginx.pid;
7 |
8 | events {
9 | worker_connections 1024;
10 | }
11 |
12 |
13 | http {
14 | default_type application/octet-stream;
15 | sendfile on;
16 | #tcp_nopush on;
17 |
18 | #keepalive_timeout 0;
19 | keepalive_timeout 65;
20 |
21 | server {
22 | listen 8088;
23 | server_name localhost;
24 | index /~;
25 | location / {
26 | try_files $uri $uri/ @fcgi;
27 | }
28 | access_log access.log;
29 |
30 | location @fcgi {
31 | fastcgi_pass localhost:9088;
32 | include fastcgi_params;
33 | fastcgi_keep_conn off;
34 | }
35 | }
36 |
37 | server {
38 | listen 8089;
39 | server_name localhost;
40 | index /~;
41 | location / {
42 | try_files $uri $uri/ @fcgi;
43 | }
44 | access_log access.log;
45 |
46 | location @fcgi {
47 | fastcgi_pass localhost:9089;
48 | include fastcgi_params;
49 | fastcgi_keep_conn off;
50 | }
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/functional_tests/request_processor/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_request_processor
2 | -Wait: 1 sec
3 |
4 | -Expect response from "/":
5 | Guest book
6 | No messages
7 | ---
8 |
9 | -Expect status from "/foo": 404
--------------------------------------------------------------------------------
/functional_tests/response_dispatching_asio_task/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_response_dispatching_asio_task
2 | -Wait: 1 sec
3 |
4 | -Expect response from "/":
5 | Hello world
6 | ---
7 |
8 | -Expect status from "/foo": 404
--------------------------------------------------------------------------------
/functional_tests/response_wait_future/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_response_wait_future
2 | -Wait: 1 sec
3 |
4 | -Expect response from "/":
5 | Hello world
6 | ---
7 |
8 | -Expect status from "/foo": 404
--------------------------------------------------------------------------------
/functional_tests/route_context/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_route_context
2 | -Wait: 1 sec
3 |
4 | -Expect status from "/foo": 404
5 |
6 | -Expect response from "/":
7 | Hello guest
login
8 | ---
9 |
10 | -Expect status from "/login" with form param "login=admin" and form param "passwd=12345": 302
11 | -Expect cookies:
12 | # Netscape HTTP Cookie File
13 | # https://curl.se/docs/http-cookies.html
14 | # This file was generated by libcurl! Edit at your own risk.
15 |
16 | localhost FALSE / FALSE 0 admin_id ADMIN_SECRET
17 | ---
18 |
19 | -Expect response from "/":
20 |
Hello admin
21 | ---
--------------------------------------------------------------------------------
/functional_tests/route_matchers/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_route_matcher
2 | -Wait: 1 sec
3 |
4 | -Expect status from "/foo": 404
5 |
6 | -Expect response from "/":
7 | Hello guest
login
8 | ---
9 |
10 | -Expect status from "/login" with form param "login=admin" and form param "passwd=12345": 302
11 | -Expect cookies:
12 | # Netscape HTTP Cookie File
13 | # https://curl.se/docs/http-cookies.html
14 | # This file was generated by libcurl! Edit at your own risk.
15 |
16 | localhost FALSE / FALSE 0 admin_id ADMIN_SECRET
17 | ---
18 |
19 | -Expect response from "/":
20 |
Hello admin
21 | ---
--------------------------------------------------------------------------------
/functional_tests/route_params/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_route_params_user_defined_types
2 | -Wait: 1 sec
3 |
4 | -Expect status from "/foo": 404
5 |
6 | -Expect response from "/":
7 | Guest book No messages
Message:
8 | ---
9 |
10 | -Expect status from "/" with form param "msg=Hello world!": 302
11 |
12 | -Expect response from "/":
13 | Guest book Hello world!
Message:
14 | ---
15 |
16 | -Expect status from "/" with form param "msg=Hello moon!": 302
17 |
18 | -Expect response from "/":
19 | Guest book Hello world!
Hello moon!
Message:
20 | ---
21 |
22 | -Expect status from post request "/delete/0": 302
23 | -Expect response from "/":
24 | Guest book Hello moon!
Message:
25 | ---
26 |
27 |
--------------------------------------------------------------------------------
/functional_tests/route_params_user_defined_types/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_route_params
2 | -Wait: 1 sec
3 |
4 | -Expect status from "/foo": 404
5 |
6 | -Expect response from "/":
7 | Guest book No messages
Message:
8 | ---
9 |
10 | -Expect status from "/" with form param "msg=Hello world!": 302
11 |
12 | -Expect response from "/":
13 | Guest book Hello world!
Message:
14 | ---
15 |
16 | -Expect status from "/" with form param "msg=Hello moon!": 302
17 |
18 | -Expect response from "/":
19 | Guest book Hello world!
Hello moon!
Message:
20 | ---
21 |
22 | -Expect status from post request "/delete/0": 302
23 | -Expect response from "/":
24 | Guest book Hello moon!
Message:
25 | ---
26 |
27 |
--------------------------------------------------------------------------------
/functional_tests/router/test.toast:
--------------------------------------------------------------------------------
1 | -Launch detached: ../../build/examples/example_router
2 | -Wait: 1 sec
3 |
4 | -Expect status from "/foo": 404
5 |
6 | -Expect response from "/":
7 | Guest book No messages
Message:
8 | ---
9 |
10 | -Expect status from "/" with form param "msg=Hello world!": 302
11 |
12 | -Expect response from "/":
13 | Guest book Hello world!
Message:
14 | ---
15 |
16 | -Expect status from "/" with form param "msg=Hello moon!": 302
17 |
18 | -Expect response from "/":
19 | Guest book Hello world!
Hello moon!
Message:
20 | ---
--------------------------------------------------------------------------------
/functional_tests/timer/test.toast:
--------------------------------------------------------------------------------
1 | -Tags: linux
2 | -Launch detached: ../../build/examples/example_timer
3 | -Wait: 1100 msec
4 |
5 | -Expect response from "/":
6 | Hello world
7 | (alive for 1 seconds)
8 | ---
9 |
10 | -Wait: 1 sec
11 |
12 | -Expect response from "/":
13 | Hello world
14 | (alive for 2 seconds)
15 | ---
16 |
17 | -Expect status from "/foo": 404
--------------------------------------------------------------------------------
/include/asyncgi/asiodispatcher.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_ASIODISPATCHER_H
2 | #define ASYNCGI_ASIODISPATCHER_H
3 |
4 | #include "taskcontext.h"
5 | #include "detail/serviceholder.h"
6 | #include
7 | #include
8 |
9 | namespace asyncgi {
10 | namespace detail {
11 | class AsioDispatcherService;
12 | }
13 |
14 | class IO;
15 | class Responder;
16 |
17 | class AsioDispatcher {
18 | public:
19 | explicit AsioDispatcher(IO&);
20 | explicit AsioDispatcher(Responder&);
21 |
22 | void postTask(std::function task);
23 |
24 | private:
25 | detail::ServiceHolder asioDispatcherService_;
26 | };
27 |
28 | } //namespace asyncgi
29 |
30 | #endif //ASYNCGI_ASIODISPATCHER_H
31 |
--------------------------------------------------------------------------------
/include/asyncgi/asyncgi.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_H
2 | #define ASYNCGI_H
3 |
4 | #include "asiodispatcher.h"
5 | #include "client.h"
6 | #include "errors.h"
7 | #include "events.h"
8 | #include "io.h"
9 | #include "router.h"
10 | #include "server.h"
11 | #include "timer.h"
12 |
13 | #endif //ASYNCGI_H
--------------------------------------------------------------------------------
/include/asyncgi/asyncgi_fwd.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_FWD_H
2 | #define ASYNCGI_FWD_H
3 |
4 | namespace asyncgi {
5 | class IO;
6 | class Request;
7 | class Responder;
8 | class Server;
9 | class Client;
10 | class Error;
11 | class AsioDispatcher;
12 | class TaskContext;
13 | class Timer;
14 |
15 | namespace http {
16 | class Response;
17 | };
18 | namespace fastcgi {
19 | struct Request;
20 | struct Response;
21 | } //namespace fastcgi
22 |
23 | } //namespace asyncgi
24 |
25 | #endif //ASYNCGI_FWD_H
--------------------------------------------------------------------------------
/include/asyncgi/client.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_CLIENT_H
2 | #define ASYNCGI_CLIENT_H
3 |
4 | #include "errors.h"
5 | #include "types.h"
6 | #include "detail/serviceholder.h"
7 | #include "http/request.h"
8 | #include "http/response_view.h"
9 | #include
10 | #include
11 | #include
12 | #include
13 | #include
14 |
15 | namespace asyncgi {
16 | class IO;
17 | class Responder;
18 |
19 | namespace detail {
20 | class ClientService;
21 | }
22 |
23 | class Client {
24 | public:
25 | explicit Client(IO&);
26 | explicit Client(Responder&);
27 |
28 | void makeRequest(
29 | const std::filesystem::path& socketPath,
30 | fastcgi::Request request,
31 | std::function)> responseHandler,
32 | std::chrono::milliseconds timeout = std::chrono::seconds{3});
33 | void makeRequest(
34 | const std::filesystem::path& socketPath,
35 | const http::Request& request,
36 | const std::function)>& responseHandler,
37 | std::chrono::milliseconds timeout = std::chrono::seconds{3});
38 |
39 | void makeRequest(
40 | std::string_view ipAddress,
41 | uint16_t port,
42 | fastcgi::Request request,
43 | std::function)> responseHandler,
44 | std::chrono::milliseconds timeout = std::chrono::seconds{3});
45 | void makeRequest(
46 | std::string_view ipAddress,
47 | uint16_t port,
48 | const http::Request& request,
49 | const std::function)>& responseHandler,
50 | std::chrono::milliseconds timeout = std::chrono::seconds{3});
51 |
52 | void disconnect();
53 |
54 | private:
55 | detail::ServiceHolder clientService_;
56 | };
57 |
58 | } //namespace asyncgi
59 |
60 | #endif //ASYNCGI_CLIENT_H
61 |
--------------------------------------------------------------------------------
/include/asyncgi/detail/asio_namespace.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_ASIO_NAMESPACE_H
2 | #define ASYNCGI_ASIO_NAMESPACE_H
3 |
4 | #ifdef ASYNCGI_USE_BOOST_ASIO
5 | #define ASYNCGI_ASIO boost::asio
6 | namespace boost::asio {
7 | }
8 | namespace asyncgi {
9 | namespace asio = boost::asio;
10 | }
11 | #else
12 | #define ASYNCGI_ASIO asio
13 | namespace asio {
14 | }
15 | namespace asyncgi {
16 | namespace asio = ::asio;
17 | }
18 | #endif
19 |
20 | #endif //ASYNCGI_ASIO_NAMESPACE_H
21 |
--------------------------------------------------------------------------------
/include/asyncgi/detail/eventhandlerproxy.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_EVENTHANDLERPROXY_H
2 | #define ASYNCGI_EVENTHANDLERPROXY_H
3 | #include "utils.h"
4 | #include "external/sfun/utility.h"
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | namespace asyncgi::detail {
11 |
12 | class EventHandlerProxy {
13 | public:
14 | EventHandlerProxy();
15 | template<
16 | typename TEventHandler,
17 | std::enable_if_t, EventHandlerProxy>>* = nullptr>
18 | EventHandlerProxy(TEventHandler&& eventHandler)
19 | {
20 | if constexpr (std::is_invocable_v)
21 | errorHandler_ = refWrapperOrRValue(std::forward(eventHandler));
22 | else
23 | static_assert(sfun::dependent_false, "TEventHandler has an incompatible signature");
24 | }
25 |
26 | void operator()(ErrorEvent, std::string_view);
27 |
28 | private:
29 | std::function errorHandler_ = [](ErrorEvent, std::string_view) {};
30 | };
31 |
32 | } //namespace asyncgi::detail
33 |
34 | #endif //ASYNCGI_EVENTHANDLERPROXY_H
35 |
--------------------------------------------------------------------------------
/include/asyncgi/detail/lazyinitialized.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 |
5 | namespace asyncgi::detail {
6 |
7 | template
8 | class LazyInitialized {
9 | public:
10 | explicit LazyInitialized(std::function initializer)
11 | : initializer_{std::move(initializer)}
12 | {
13 | }
14 | operator const T&() const
15 | {
16 | return get();
17 | }
18 | const T& get() const
19 | {
20 | if (!value)
21 | value = initializer_();
22 | return *value;
23 | }
24 |
25 | operator T&()
26 | {
27 | return get();
28 | }
29 | T& get()
30 | {
31 | if (!value)
32 | value = initializer_();
33 | return *value;
34 | }
35 |
36 | private:
37 | mutable std::optional value;
38 | std::function initializer_;
39 | };
40 |
41 | } // namespace asyncgi::detail
--------------------------------------------------------------------------------
/include/asyncgi/detail/routeresponsecontextaccessor.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_ROUTERESPONSECONTEXTACCESSOR_H
2 | #define ASYNCGI_ROUTERESPONSECONTEXTACCESSOR_H
3 |
4 | #include
5 |
6 | namespace asyncgi {
7 | class Responder;
8 |
9 | namespace whaleroute {
10 | class RequestProcessorQueue;
11 | }
12 |
13 | namespace detail {
14 |
15 | struct RouterResponseContextAccessor {
16 | static void setRequestProcessorQueue(Responder&, const std::shared_ptr&);
17 | };
18 |
19 | } //namespace detail
20 |
21 | } //namespace asyncgi
22 |
23 | #endif //ASYNCGI_ROUTERESPONSECONTEXTACCESSOR_H
24 |
--------------------------------------------------------------------------------
/include/asyncgi/detail/serviceholder.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_SERVICE_HOLDER_H
2 | #define ASYNCGI_SERVICE_HOLDER_H
3 |
4 | #include
5 | #include
6 |
7 | namespace asyncgi::detail {
8 |
9 | template
10 | class ServiceHolder {
11 | public:
12 | explicit ServiceHolder(std::unique_ptr);
13 | explicit ServiceHolder(sfun::optional_ref);
14 | ~ServiceHolder();
15 | ServiceHolder(const ServiceHolder&) = delete;
16 | ServiceHolder& operator=(const ServiceHolder&) = delete;
17 | ServiceHolder(ServiceHolder&&) noexcept = default;
18 | ServiceHolder& operator=(ServiceHolder&&) noexcept = default;
19 |
20 | bool has_value() const;
21 | T& get();
22 |
23 | private:
24 | std::unique_ptr service_;
25 | sfun::optional_ref serviceOptionalRef_;
26 | };
27 |
28 | } //namespace asyncgi::detail
29 |
30 | #endif //ASYNCGI_SERVICE_HOLDER_H
31 |
--------------------------------------------------------------------------------
/include/asyncgi/detail/utils.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_UTILS_H
2 | #define ASYNCGI_UTILS_H
3 |
4 | #include
5 |
6 | namespace asyncgi::detail {
7 |
8 | template
9 | auto makeCopyableLambda(TFunc&& f)
10 | {
11 | auto funcPtr = std::make_shared>(std::forward(f));
12 | return [funcPtr](auto&&... args) -> decltype(auto)
13 | {
14 | return (*funcPtr)(decltype(args)(args)...);
15 | };
16 | }
17 |
18 | template
19 | decltype(auto) refWrapperOrRValue(T&& obj)
20 | {
21 | if constexpr (std::is_lvalue_reference_v)
22 | return std::ref(std::forward(obj));
23 | else
24 | return std::forward(obj);
25 | }
26 |
27 | } //namespace asyncgi::detail
28 |
29 | #endif //ASYNCGI_UTILS_H
30 |
--------------------------------------------------------------------------------
/include/asyncgi/errors.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_ERRORS_H
2 | #define ASYNCGI_ERRORS_H
3 | #include
4 |
5 | namespace asyncgi {
6 |
7 | class Error : public std::runtime_error {
8 | using std::runtime_error::runtime_error;
9 | };
10 |
11 | } // namespace asyncgi
12 |
13 | #endif //ASYNCGI_ERRORS_H
--------------------------------------------------------------------------------
/include/asyncgi/events.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_EVENTS_H
2 | #define ASYNCGI_EVENTS_H
3 |
4 | namespace asyncgi {
5 |
6 | enum ErrorEvent {
7 | ConnectionError,
8 | SocketReadError,
9 | SocketWriteError,
10 | SocketCloseError,
11 | RequestProcessingError,
12 | RouteParametersError
13 | };
14 |
15 | } //namespace asyncgi
16 |
17 | #endif //ASYNCGI_EVENTS_H
18 |
--------------------------------------------------------------------------------
/include/asyncgi/io.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_IO_H
2 | #define ASYNCGI_IO_H
3 |
4 | #include "errors.h"
5 | #include "detail/eventhandlerproxy.h"
6 | #include "detail/external/sfun/interface.h"
7 | #include
8 |
9 | namespace asyncgi {
10 | class Server;
11 | class Client;
12 | class Timer;
13 | class AsioDispatcher;
14 | class RouterIOAccess;
15 |
16 | namespace detail {
17 | class IOService;
18 | using IOAccessPermission = sfun::access_permission;
19 | } //namespace detail
20 |
21 | class IO {
22 |
23 | public:
24 | template
25 | explicit IO(int threadsNumber, TEventHandler&& eventHandler)
26 | : IO(threadsNumber)
27 | {
28 | eventHandler_ = std::forward(eventHandler);
29 | }
30 |
31 | template
32 | explicit IO(TEventHandler&& eventHandler)
33 | : IO(1)
34 | {
35 | eventHandler_ = std::forward(eventHandler);
36 | }
37 | explicit IO(int threadsNumber = 1);
38 |
39 | ~IO();
40 | IO(const IO&) = delete;
41 | IO& operator=(const IO&) = delete;
42 | IO(IO&&) = delete;
43 | IO& operator=(IO&&) = delete;
44 |
45 | void run();
46 | void stop();
47 |
48 | /// detail
49 | detail::EventHandlerProxy& eventHandler(detail::IOAccessPermission);
50 | detail::IOService& ioService(detail::IOAccessPermission);
51 |
52 | private:
53 | std::unique_ptr ioService_;
54 | detail::EventHandlerProxy eventHandler_;
55 | };
56 |
57 | } //namespace asyncgi
58 |
59 | #endif //ASYNCGI_IO_H
60 |
--------------------------------------------------------------------------------
/include/asyncgi/request.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_REQUEST_H
2 | #define ASYNCGI_REQUEST_H
3 |
4 | #include "detail/external/sfun/member.h"
5 | #include "detail/lazyinitialized.h"
6 | #include "http/cookie_view.h"
7 | #include "http/query_view.h"
8 | #include "http/request_view.h"
9 | #include "http/types.h"
10 | #include
11 | #include
12 | #include
13 | #include
14 | #include
15 | #include
16 |
17 | namespace asyncgi {
18 |
19 | namespace fcgi {
20 | class Request;
21 | }
22 |
23 | class Request {
24 | public:
25 | // as http::Request
26 | std::string_view ipAddress() const;
27 | std::string_view domainName() const;
28 | std::string_view path() const;
29 | http::RequestMethod method() const;
30 | const std::vector& queries() const;
31 | std::string_view query(std::string_view name) const;
32 | bool hasQuery(std::string_view name) const;
33 |
34 | const std::vector& cookies() const;
35 | std::string_view cookie(std::string_view name) const;
36 | bool hasCookie(std::string_view name) const;
37 |
38 | std::string_view formField(std::string_view name, int index = 0) const;
39 | std::vector formFieldList() const;
40 | std::vector fileList() const;
41 | int formFieldCount(std::string_view name) const;
42 | bool hasFormField(std::string_view name) const;
43 |
44 | std::string_view fileData(std::string_view name, int index = 0) const;
45 | int fileCount(std::string_view name) const;
46 | bool hasFile(std::string_view name) const;
47 | std::string_view fileName(std::string_view name, int index = 0) const;
48 | std::string_view fileType(std::string_view name, int index = 0) const;
49 | bool hasFiles() const;
50 |
51 | // as FCGIRequest
52 | const std::string& fcgiParam(const std::string& name) const;
53 | bool hasFcgiParam(const std::string& name) const;
54 | const std::vector>& fcgiParams() const;
55 | const std::string& fcgiStdIn() const;
56 |
57 | explicit Request(const fcgi::Request& fcgiRequest);
58 |
59 | private:
60 | const fcgi::Request& fcgiRequest() const;
61 | const http::RequestView& httpRequest() const;
62 |
63 | private:
64 | sfun::member fcgiRequest_;
65 | detail::LazyInitialized httpRequest_;
66 | };
67 |
68 | } // namespace asyncgi
69 |
70 | #endif //ASYNCGI_REQUEST_H
--------------------------------------------------------------------------------
/include/asyncgi/requestprocessor.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_REQUESTPROCESSOR_H
2 | #define ASYNCGI_REQUESTPROCESSOR_H
3 |
4 | #include "request.h"
5 | #include "responder.h"
6 | #include "detail/external/sfun/functional.h"
7 | #include
8 |
9 | namespace asyncgi {
10 |
11 | namespace detail {
12 | class ResponseContext;
13 |
14 | template
15 | constexpr void checkRequestProcessorSignature()
16 | {
17 | if constexpr (std::is_same_v) {
18 | constexpr auto args = TRequestProcessorArgs{};
19 | static_assert(args.size() == 2);
20 | static_assert(std::is_same_v(args))::type>);
21 | static_assert(std::is_same_v(args))::type>);
22 | }
23 | else {
24 | static_assert(
25 | std::is_same_v ||
26 | std::is_same_v);
27 | constexpr auto args = TRequestProcessorArgs{};
28 | static_assert(args.size() == 1);
29 | static_assert(std::is_same_v(args))::type>);
30 | }
31 | }
32 | } // namespace detail
33 |
34 | class RequestProcessor {
35 | public:
36 | template<
37 | typename TRequestProcessorFunc,
38 | typename std::enable_if_t<
39 | !std::is_same_v, RequestProcessor>>* = nullptr>
40 | RequestProcessor(TRequestProcessorFunc&& requestProcessor)
41 | {
42 | detail::checkRequestProcessorSignature<
43 | sfun::callable_return_type,
44 | sfun::callable_args>();
45 |
46 | if constexpr (std::is_lvalue_reference_v) {
47 | requestProcessorInvoker_ = [&requestProcessor](
48 | const Request& request,
49 | std::shared_ptr responseContext)
50 | {
51 | if constexpr (std::is_same_v, void>) {
52 | auto response = Responder{std::move(responseContext)};
53 | requestProcessor(request, response);
54 | }
55 | else {
56 | auto response = requestProcessor(request);
57 | Responder{std::move(responseContext)}.send(response);
58 | }
59 | };
60 | }
61 | else {
62 | requestProcessorInvoker_ = [requestProcessor = std::forward(requestProcessor)](
63 | const Request& request,
64 | std::shared_ptr responseContext)
65 | {
66 | if constexpr (std::is_same_v, void>) {
67 | auto response = Responder{std::move(responseContext)};
68 | requestProcessor(request, response);
69 | }
70 | else {
71 | auto response = requestProcessor(request);
72 | Responder{std::move(responseContext)}.send(response);
73 | }
74 | };
75 | }
76 | }
77 |
78 | void operator()(const Request& request, std::shared_ptr response)
79 | {
80 | requestProcessorInvoker_(request, response);
81 | }
82 |
83 | private:
84 | std::function response)>
85 | requestProcessorInvoker_;
86 | };
87 |
88 | } // namespace asyncgi
89 |
90 | #endif //ASYNCGI_REQUESTPROCESSOR_H
--------------------------------------------------------------------------------
/include/asyncgi/responder.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_RESPONDER_H
2 | #define ASYNCGI_RESPONDER_H
3 |
4 | #include "types.h"
5 | #include "detail/external/sfun/interface.h"
6 | #include "http/request.h"
7 | #include "http/response.h"
8 | #include
9 | #include
10 | #include
11 | #include
12 |
13 | namespace asyncgi {
14 |
15 | namespace whaleroute {
16 | class RequestProcessorQueue;
17 | }
18 |
19 | template
20 | class Router;
21 | class Client;
22 | class Timer;
23 | class AsioDispatcher;
24 |
25 | namespace detail {
26 | class ResponseContext;
27 | struct RouterResponseContextAccessor;
28 | using ResponseContextAccessPermission =
29 | sfun::access_permission;
30 | } //namespace detail
31 |
32 | class Responder {
33 | public:
34 | explicit Responder(std::shared_ptr responseContext);
35 |
36 | template
37 | void send(TArgs... args)
38 | {
39 | auto response = http::Response{std::forward(args)...};
40 | send(response);
41 | }
42 |
43 | void send(const http::Response& response);
44 | void send(fastcgi::Response response);
45 | bool isSent() const;
46 |
47 | /// detail
48 | std::weak_ptr context(detail::ResponseContextAccessPermission);
49 |
50 | private:
51 | std::weak_ptr responseContext_;
52 | };
53 |
54 | } // namespace asyncgi
55 |
56 | #endif //ASYNCGI_RESPONDER_H
--------------------------------------------------------------------------------
/include/asyncgi/router.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_ROUTER_H
2 | #define ASYNCGI_ROUTER_H
3 |
4 | #include "errors.h"
5 | #include "io.h"
6 | #include "request.h"
7 | #include "responder.h"
8 | #include "types.h"
9 | #include "detail/external/sfun/functional.h"
10 | #include "detail/external/sfun/interface.h"
11 | #include "detail/external/sfun/type_traits.h"
12 | #include "detail/external/whaleroute/requestrouter.h"
13 | #include "detail/routeresponsecontextaccessor.h"
14 | #include "http/response.h"
15 |
16 | namespace asyncgi {
17 | namespace config = whaleroute::config;
18 |
19 | template
20 | class Router;
21 |
22 | class RouterIOAccess {
23 | public:
24 | template
25 | static auto makeToken(sfun::access_permission>)
26 | {
27 | return sfun::access_token{};
28 | }
29 | };
30 |
31 | namespace detail {
32 | struct ResponseSender {
33 | template
34 | void operator()(Responder& responseSender, const TResponse& response)
35 | {
36 | if constexpr (sfun::is_optional_v) {
37 | if (response.has_value())
38 | responseSender.send(response.value());
39 | }
40 | else {
41 | responseSender.send(response);
42 | }
43 | }
44 |
45 | template
46 | auto operator()(Responder& responseSender, TResponse&&... response) -> std::enable_if_t<(sizeof...(TResponse) > 1)>
47 | {
48 | responseSender.send(response...);
49 | }
50 | };
51 | } //namespace detail
52 |
53 | template
54 | class Router : public whaleroute::RequestRouter {
55 | public:
56 | explicit Router(IO& io)
57 | : eventHandler_{io.eventHandler(RouterIOAccess::makeToken(sfun::access_token{*this}))}
58 | {
59 | }
60 |
61 | void operator()(const Request& request, Responder& response)
62 | {
63 | auto requestProcessorQueuePtr = std::make_shared();
64 | detail::RouterResponseContextAccessor::setRequestProcessorQueue(response, requestProcessorQueuePtr);
65 | auto requestProcessorQueue =
66 | whaleroute::RequestRouter::
67 | makeRequestProcessorQueue(request, response);
68 |
69 | *requestProcessorQueuePtr = requestProcessorQueue;
70 | requestProcessorQueuePtr->launch();
71 | }
72 |
73 | private:
74 | std::string getRequestPath(const Request& request) final
75 | {
76 | return std::string{request.path()};
77 | }
78 |
79 | void processUnmatchedRequest(const Request&, Responder& response) final
80 | {
81 | response.send(http::ResponseStatus::_404_Not_Found);
82 | }
83 |
84 | bool isRouteProcessingFinished(const Request&, Responder& response) const final
85 | {
86 | return response.isSent();
87 | }
88 |
89 | void onRouteParametersError(const Request&, Responder& response, const whaleroute::RouteParameterError& error)
90 | override
91 | {
92 | auto errorMessageVisitor = sfun::overloaded{
93 | [](const whaleroute::RouteParameterCountMismatch& countMismatchError) -> std::string
94 | {
95 | return "RouteParameterError: Parameter count mismatch, expected: " +
96 | std::to_string(countMismatchError.expectedNumber) +
97 | ", actual:" + std::to_string(countMismatchError.actualNumber);
98 | },
99 | [](const whaleroute::RouteParameterReadError& readError) -> std::string
100 | {
101 | return "RouteParameterError: Couldn't read parameter#" + std::to_string(readError.index) +
102 | ", value:" + readError.value;
103 | },
104 |
105 | };
106 | eventHandler_(RouteParametersError, std::visit(errorMessageVisitor, error));
107 | response.send(http::ResponseStatus::_500_Internal_Server_Error);
108 | };
109 |
110 | private:
111 | detail::EventHandlerProxy eventHandler_;
112 | };
113 |
114 | template<>
115 | struct config::RouteMatcher {
116 | bool operator()(asyncgi::http::RequestMethod value, const asyncgi::Request& request) const
117 | {
118 | return value == request.method();
119 | }
120 | };
121 |
122 | template
123 | struct config::RouteMatcher {
124 | bool operator()(asyncgi::http::RequestMethod value, const asyncgi::Request& request, const TContext&) const
125 | {
126 | return value == request.method();
127 | }
128 | };
129 |
130 | } // namespace asyncgi
131 |
132 | #endif //ASYNCGI_ROUTER_H
--------------------------------------------------------------------------------
/include/asyncgi/server.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_SERVER_H
2 | #define ASYNCGI_SERVER_H
3 |
4 | #include "requestprocessor.h"
5 | #include
6 | #include
7 | #include
8 |
9 | namespace asyncgi {
10 | class IO;
11 |
12 | namespace detail {
13 | class ServerService;
14 | }
15 |
16 | class Server {
17 | public:
18 | Server(IO&, RequestProcessor);
19 | ~Server();
20 | Server(const Server&) = delete;
21 | Server& operator=(const Server&) = delete;
22 | Server(Server&&) = default;
23 | Server& operator=(Server&&) = default;
24 |
25 | void listen(std::string_view ipAddress, int port);
26 | void listen(const std::filesystem::path& unixDomainSocket);
27 |
28 | private:
29 | std::unique_ptr serverService_;
30 | };
31 |
32 | } //namespace asyncgi
33 |
34 | #endif //ASYNCGI_SERVER_H
35 |
--------------------------------------------------------------------------------
/include/asyncgi/taskcontext.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_TASKCONTEXT_H
2 | #define ASYNCGI_TASKCONTEXT_H
3 | #include "detail/asio_namespace.h"
4 | #include "detail/external/sfun/member.h"
5 | #include
6 | #include
7 |
8 | namespace ASYNCGI_ASIO {
9 | class io_context;
10 | }
11 |
12 | namespace asyncgi {
13 |
14 | class TaskContext {
15 | class PostAction {
16 | public:
17 | explicit PostAction(std::function action);
18 | ~PostAction();
19 |
20 | private:
21 | std::function action_;
22 | };
23 |
24 | public:
25 | TaskContext(asio::io_context& io, std::function postTaskAction);
26 | asio::io_context& io() const;
27 |
28 | private:
29 | sfun::member io_;
30 | std::shared_ptr postTaskAction_;
31 | };
32 |
33 | } // namespace asyncgi
34 |
35 | #endif //ASYNCGI_TASKCONTEXT_H
--------------------------------------------------------------------------------
/include/asyncgi/timer.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_TIMER_H
2 | #define ASYNCGI_TIMER_H
3 |
4 | #include "detail/serviceholder.h"
5 | #include "detail/utils.h"
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | namespace asyncgi {
12 | class IO;
13 | class Responder;
14 |
15 | namespace detail {
16 | class TimerService;
17 | }
18 |
19 | class Timer {
20 | public:
21 | explicit Timer(IO&);
22 | explicit Timer(Responder&);
23 |
24 | void start(std::chrono::milliseconds time, std::function callback);
25 | void startPeriodic(std::chrono::milliseconds time, std::function callback);
26 | void stop();
27 |
28 | template
29 | void waitFuture(
30 | std::future&& future,
31 | TCallable callback,
32 | [[maybe_unused]] std::chrono::milliseconds checkPeriod = std::chrono::milliseconds{1})
33 | {
34 | static_assert(std::is_invocable_v, "TCallable must be invokable with argument of type T");
35 | auto timerCallback = [fut = std::move(future), func = std::move(callback), stopTimer = stopSignal()]() mutable
36 | {
37 | if (fut.wait_for(std::chrono::seconds{0}) == std::future_status::ready) {
38 | func(fut.get());
39 | stopTimer();
40 | }
41 | };
42 |
43 | startPeriodic(checkPeriod, detail::makeCopyableLambda(std::move(timerCallback)));
44 | }
45 |
46 | private:
47 | std::function stopSignal();
48 |
49 | private:
50 | detail::ServiceHolder timerService_;
51 | };
52 |
53 | } //namespace asyncgi
54 |
55 | #endif //ASYNCGI_TIMER_H
56 |
--------------------------------------------------------------------------------
/include/asyncgi/types.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_TYPES_H
2 | #define ASYNCGI_TYPES_H
3 |
4 | #include "detail/external/whaleroute/types.h"
5 | #include "http/types.h"
6 | #include
7 | #include
8 |
9 | namespace asyncgi {
10 | using _ = whaleroute::_;
11 |
12 | using rx = whaleroute::rx;
13 | namespace string_literals = whaleroute::string_literals;
14 | using TrailingSlashMode = whaleroute::TrailingSlashMode;
15 | template
16 | using RouteParameters = whaleroute::RouteParameters;
17 |
18 | namespace fastcgi {
19 | struct Response {
20 | std::string data;
21 | std::string errorMsg;
22 | };
23 |
24 | struct Request {
25 | std::map params;
26 | std::string stdIn;
27 | };
28 | } // namespace fastcgi
29 |
30 | } //namespace asyncgi
31 |
32 | #endif //ASYNCGI_TYPES_H
--------------------------------------------------------------------------------
/src/asio_error.h:
--------------------------------------------------------------------------------
1 | #ifndef ASYNCGI_ASIO_ERROR_H
2 | #define ASYNCGI_ASIO_ERROR_H
3 |
4 | #ifdef ASYNCGI_USE_BOOST_ASIO
5 | #include
6 | namespace asyncgi {
7 | using asio_error = boost::system::error_code;
8 | }
9 | #else
10 | #include
11 | namespace asyncgi {
12 | using asio_error = std::error_code;
13 | }
14 | #endif
15 |
16 | #endif //ASYNCGI_ASIO_ERRORCODE_H
17 |
--------------------------------------------------------------------------------
/src/asiodispatcher.cpp:
--------------------------------------------------------------------------------
1 | #include "asiodispatcherservice.h"
2 | #include "ioservice.h"
3 | #include "responsecontext.h"
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | namespace asyncgi {
11 |
12 | AsioDispatcher::AsioDispatcher(IO& io)
13 | : asioDispatcherService_{
14 | std::make_unique(io.ioService(sfun::access_token{}).io())}
15 | {
16 | }
17 |
18 | namespace {
19 | sfun::optional_ref getAsioDispatcherService(
20 | Responder& response,
21 | sfun::access_token accessToken)
22 | {
23 | if (auto context = response.context(accessToken).lock())
24 | return context->asioDispatcher();
25 | else
26 | return std::nullopt;
27 | }
28 | } //namespace
29 |
30 | AsioDispatcher::AsioDispatcher(Responder& response)
31 | : asioDispatcherService_{getAsioDispatcherService(response, sfun::access_token{})}
32 | {
33 | }
34 |
35 | void AsioDispatcher::postTask(std::function task)
36 | {
37 | if (!asioDispatcherService_.has_value())
38 | return;
39 | asioDispatcherService_.get().postTask(std::move(task));
40 | }
41 |
42 | } //namespace asyncgi
--------------------------------------------------------------------------------
/src/asiodispatcherservice.cpp:
--------------------------------------------------------------------------------
1 | #include "asiodispatcherservice.h"
2 | #ifdef ASYNCGI_USE_BOOST_ASIO
3 | #include
4 | #else
5 | #include
6 | #endif
7 | #include
8 | #include
9 |
10 | namespace asyncgi::detail {
11 |
12 | AsioDispatcherService::AsioDispatcherService(asio::io_context& io)
13 | : io_{io}
14 | {
15 | }
16 |
17 | void AsioDispatcherService::postTask(std::function task)
18 | {
19 | if (auto queue = requestProcessorQueue_.lock())
20 | queue->stop();
21 |
22 | auto postTaskAction = [queueObserver = requestProcessorQueue_]
23 | {
24 | if (auto queue = queueObserver.lock())
25 | queue->launch();
26 | };
27 |
28 | auto taskContext = TaskContext{io_, std::move(postTaskAction)};
29 | io_.get().post(
30 | [task = std::move(task), taskContext = std::move(taskContext)]
31 | {
32 | task(taskContext);
33 | });
34 | }
35 |
36 | void AsioDispatcherService::setRequestProcessorQueue(std::shared_ptr queue)
37 | {
38 | requestProcessorQueue_ = queue;
39 | }
40 |
41 | } // namespace asyncgi::detail
--------------------------------------------------------------------------------
/src/asiodispatcherservice.h:
--------------------------------------------------------------------------------
1 | #pragma once
2 | #include
3 | #include
4 | #include
5 | #include
6 |
7 | namespace ASYNCGI_ASIO {
8 | class io_context;
9 | }
10 |
11 | namespace asyncgi::whaleroute {
12 | class RequestProcessorQueue;
13 | }
14 |
15 | namespace asyncgi::detail {
16 |
17 | class AsioDispatcherService {
18 | public:
19 | explicit AsioDispatcherService(asio::io_context& io);
20 | void postTask(std::function task);
21 | void setRequestProcessorQueue(std::shared_ptr queue);
22 |
23 | private:
24 | sfun::member io_;
25 | std::weak_ptr requestProcessorQueue_;
26 | };
27 |
28 | } // namespace asyncgi::detail
29 |
--------------------------------------------------------------------------------
/src/client.cpp:
--------------------------------------------------------------------------------
1 | #include "clientservice.h"
2 | #include "ioservice.h"
3 | #include "responsecontext.h"
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 |
10 | namespace asyncgi {
11 |
12 | Client::Client(IO& io)
13 | : clientService_{std::make_unique(
14 | io.ioService(sfun::access_token{}).io(),
15 | io.eventHandler(sfun::access_token{}))}
16 | {
17 | }
18 |
19 | namespace {
20 | sfun::optional_ref getClientService(Responder& response, sfun::access_token accessToken)
21 | {
22 | if (auto context = response.context(accessToken).lock())
23 | return context->client();
24 | else
25 | return std::nullopt;
26 | }
27 | } //namespace
28 |
29 | Client::Client(Responder& response)
30 | : clientService_{getClientService(response, sfun::access_token{})}
31 | {
32 | }
33 |
34 | void Client::makeRequest(
35 | const std::filesystem::path& socketPath,
36 | fastcgi::Request request,
37 | std::function)> responseHandler,
38 | std::chrono::milliseconds timeout)
39 | {
40 | if (!clientService_.has_value())
41 | return;
42 |
43 | clientService_.get().makeRequest(socketPath, std::move(request), std::move(responseHandler), timeout);
44 | }
45 |
46 | void Client::makeRequest(
47 | const std::filesystem::path& socketPath,
48 | const http::Request& request,
49 | const std::function)>& responseHandler,
50 | std::chrono::milliseconds timeout)
51 | {
52 | if (!clientService_.has_value())
53 | return;
54 |
55 | clientService_.get().makeRequest(socketPath, request, responseHandler, timeout);
56 | }
57 |
58 | void Client::makeRequest(
59 | std::string_view ipAddress,
60 | uint16_t port,
61 | fastcgi::Request request,
62 | std::function)> responseHandler,
63 | std::chrono::milliseconds timeout)
64 | {
65 | if (!clientService_.has_value())
66 | return;
67 |
68 | clientService_.get().makeRequest(ipAddress, port, std::move(request), std::move(responseHandler), timeout);
69 | }
70 |
71 | void Client::makeRequest(
72 | std::string_view ipAddress,
73 | uint16_t port,
74 | const http::Request& request,
75 | const std::function)>& responseHandler,
76 | std::chrono::milliseconds timeout)
77 | {
78 | if (!clientService_.has_value())
79 | return;
80 |
81 | clientService_.get().makeRequest(ipAddress, port, request, responseHandler, timeout);
82 | }
83 |
84 | void Client::disconnect()
85 | {
86 | if (!clientService_.has_value())
87 | return;
88 |
89 | clientService_.get().disconnect();
90 | }
91 |
92 | } //namespace asyncgi
93 |
--------------------------------------------------------------------------------
/src/clientconnection.cpp:
--------------------------------------------------------------------------------
1 | #include "clientconnection.h"
2 | #include "asio_error.h"
3 | #include "clientservice.h"
4 | #include "timerprovider.h"
5 | #include
6 | #ifdef ASYNCGI_USE_BOOST_ASIO
7 | #include
8 | #include
9 | #include
10 | #else
11 | #include
12 | #include
13 | #include
14 | #endif
15 |
16 | namespace asyncgi::detail {
17 |
18 | template
19 | ClientConnection::ClientConnection(asio::io_context& io, EventHandlerProxy& eventHandler)
20 | : socket_{io}
21 | , eventHandler_{eventHandler}
22 | {
23 | }
24 |
25 | template
26 | ClientConnection::~ClientConnection()
27 | {
28 | close();
29 | }
30 |
31 | template
32 | void ClientConnection::makeRequest(
33 | const typename TProtocol::endpoint& socketPath,
34 | fastcgi::Request request,
35 | std::function)> responseHandler,
36 | const std::shared_ptr>& cancelRequestOnTimeout)
37 | {
38 | socket_.async_connect(
39 | socketPath,
40 | [this,
41 | cancelRequestOnTimeout,
42 | fcgiParams = std::move(request.params),
43 | fcgiStdIn = std::move(request.stdIn),
44 | responseHandler = std::move(responseHandler)](auto error_code) mutable
45 | {
46 | if (error_code) {
47 | responseHandler(std::nullopt);
48 | return;
49 | }
50 | processReading();
51 | auto requestHandle = sendRequest(std::move(fcgiParams), std::move(fcgiStdIn), responseHandler);
52 | if (requestHandle)
53 | *cancelRequestOnTimeout = [=]() mutable
54 | {
55 | requestHandle->cancelRequest();
56 | };
57 | else
58 | responseHandler(std::nullopt);
59 | });
60 | }
61 |
62 | template
63 | void ClientConnection::disconnect()
64 | {
65 | disconnectRequested_ = true;
66 | if (bytesToWrite_ > 0)
67 | return;
68 | close();
69 | }
70 |
71 | template
72 | void ClientConnection::processReading()
73 | {
74 | if (!socket_.is_open())
75 | return;
76 | socket_.async_read_some(
77 | asio::buffer(buffer_),
78 | [this](const auto& error, auto bytesRead)
79 | {
80 | if (error) {
81 | if (error.value() != asio::error::operation_aborted)
82 | eventHandler_(ErrorEvent::SocketReadError, error.message());
83 | return;
84 | }
85 | readData(bytesRead);
86 | });
87 | }
88 |
89 | template
90 | void ClientConnection::readData(std::size_t bytesRead)
91 | {
92 | fcgi::Requester::receiveData(buffer_.data(), bytesRead);
93 | processReading();
94 | }
95 |
96 | template
97 | void ClientConnection::sendData(const std::string& data)
98 | {
99 | if (bytesToWrite_) {
100 | nextWriteBuffer_ += data;
101 | return;
102 | }
103 |
104 | if (!nextWriteBuffer_.empty()) {
105 | writeBuffer_ = nextWriteBuffer_ + data;
106 | nextWriteBuffer_.clear();
107 | }
108 | else
109 | writeBuffer_ = data;
110 |
111 | bytesToWrite_ += writeBuffer_.size();
112 | asio::async_write(
113 | socket_,
114 | asio::buffer(writeBuffer_),
115 | [this](const auto& error, auto bytesWritten)
116 | {
117 | if (error) {
118 | eventHandler_(ErrorEvent::SocketWriteError, error.message());
119 | return;
120 | }
121 | onBytesWritten(bytesWritten);
122 | });
123 | }
124 |
125 | template
126 | void ClientConnection::onBytesWritten(std::size_t numOfBytes)
127 | {
128 | bytesToWrite_ -= numOfBytes;
129 | if (bytesToWrite_)
130 | return;
131 |
132 | if (!nextWriteBuffer_.empty())
133 | sendData({});
134 | else if (disconnectRequested_)
135 | close();
136 | }
137 |
138 | template