├── .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 | [![build & test (clang, gcc, MSVC)](https://github.com/kamchatka-volcano/asyncgi/actions/workflows/build_and_test.yml/badge.svg?branch=master)](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 += "
" 201 | "" 202 | "" 203 | "" 204 | "
"; 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 += "
" 328 | "" 329 | "" 330 | "" 331 | "
"; 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 += "
" 472 | "" 473 | "" 474 | "" 475 | "
"; 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 |
588 | 589 | 590 | 591 | 592 | 593 |
)"}; 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 |
684 | 685 | 686 | 687 | 688 | 689 |
)"}; 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 |
790 | 791 | 792 | 793 | 794 | 795 |
)"}; 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 |
886 | 887 | 888 | 889 |
890 | 891 |
892 |
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 | 40 | 41 | 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 |
133 | %ERROR_MSG% 134 |
135 | 136 | 137 | 138 |
139 | 140 |
141 |
142 |
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 | 35 | 36 | 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 | 34 | 35 | 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 | "" 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 | "" 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 | "" 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


8 | --- 9 | 10 | -Expect status from "/" with form param "msg=Hello world!": 302 11 | 12 | -Expect response from "/": 13 |

Guest book

Hello world!


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!


20 | --- 21 | 22 | -Expect status from post request "/delete/0": 302 23 | -Expect response from "/": 24 |

Guest book

Hello moon!


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


8 | --- 9 | 10 | -Expect status from "/" with form param "msg=Hello world!": 302 11 | 12 | -Expect response from "/": 13 |

Guest book

Hello world!


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!


20 | --- 21 | 22 | -Expect status from post request "/delete/0": 302 23 | -Expect response from "/": 24 |

Guest book

Hello moon!


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


8 | --- 9 | 10 | -Expect status from "/" with form param "msg=Hello world!": 302 11 | 12 | -Expect response from "/": 13 |

Guest book

Hello world!


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!


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 139 | void ClientConnection::close() 140 | { 141 | auto error = asio_error{}; 142 | socket_.shutdown(asio::basic_stream_socket::shutdown_both, error); 143 | socket_.close(error); 144 | if (error) { 145 | eventHandler_(ErrorEvent::SocketCloseError, error.message()); 146 | } 147 | disconnectRequested_ = false; 148 | } 149 | 150 | template class ClientConnection; 151 | template class ClientConnection; 152 | 153 | } // namespace asyncgi::detail -------------------------------------------------------------------------------- /src/clientconnection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifdef ASYNCGI_USE_BOOST_ASIO 4 | #include 5 | #else 6 | #include 7 | #endif 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | namespace asyncgi::detail { 19 | namespace fs = std::filesystem; 20 | class TimerProvider; 21 | 22 | template 23 | class ClientConnection : public fcgi::Requester { 24 | public: 25 | ClientConnection(asio::io_context&, EventHandlerProxy&); 26 | ~ClientConnection() override; 27 | 28 | void makeRequest( 29 | const typename TProtocol::endpoint& socketPath, 30 | fastcgi::Request request, 31 | std::function)> responseHandler, 32 | const std::shared_ptr>& cancelRequestOnTimeout); 33 | void disconnect() override; 34 | 35 | private: 36 | void processReading(); 37 | void readData(std::size_t bytesRead); 38 | void sendData(const std::string& data) override; 39 | void onBytesWritten(std::size_t numOfBytes); 40 | void close(); 41 | 42 | private: 43 | asio::basic_stream_socket socket_; 44 | sfun::member eventHandler_; 45 | std::array buffer_; 46 | std::string writeBuffer_; 47 | std::string nextWriteBuffer_; 48 | std::size_t bytesToWrite_ = 0; 49 | bool disconnectRequested_ = false; 50 | }; 51 | 52 | } // namespace asyncgi::detail 53 | -------------------------------------------------------------------------------- /src/clientservice.cpp: -------------------------------------------------------------------------------- 1 | #include "clientservice.h" 2 | #include "ioservice.h" 3 | #include "timerprovider.h" 4 | #include 5 | #ifdef ASYNCGI_USE_BOOST_ASIO 6 | #include 7 | #else 8 | #include 9 | #endif 10 | #include 11 | #include 12 | 13 | namespace asyncgi::detail { 14 | 15 | ClientService::ClientService(asio::io_context& io, EventHandlerProxy& eventHandler) 16 | : io_{io} 17 | , eventHandler_{eventHandler} 18 | , timerProvider_{io_} 19 | { 20 | } 21 | 22 | void ClientService::makeRequest( 23 | const fs::path& socketPath, 24 | fastcgi::Request request, 25 | std::function)> responseHandler, 26 | std::chrono::milliseconds timeout) 27 | { 28 | if (requestProcessorQueue_) 29 | requestProcessorQueue_->stop(); 30 | 31 | auto cancelRequestOnTimeout = std::make_shared>([] {}); 32 | auto& responseTimeoutTimer = timerProvider_.emplaceTimer(); 33 | responseTimeoutTimer.start( 34 | timeout, 35 | [=] 36 | { 37 | (*cancelRequestOnTimeout)(); 38 | }); 39 | 40 | auto onResponseReceived = [=, &responseTimeoutTimer](std::optional fcgiResponse) 41 | { 42 | responseTimeoutTimer.stop(); 43 | if (fcgiResponse) { 44 | if (!fcgiResponse->errorMsg.empty()) 45 | eventHandler_(ErrorEvent::RequestProcessingError, fcgiResponse->errorMsg); 46 | responseHandler(fastcgi::Response{std::move(fcgiResponse->data), std::move(fcgiResponse->errorMsg)}); 47 | } 48 | else 49 | responseHandler(std::nullopt); 50 | 51 | if (requestProcessorQueue_) 52 | requestProcessorQueue_->launch(); 53 | }; 54 | auto& clientConnection = localClientConnections_.emplace_back( 55 | std::make_unique>(io_, eventHandler_)); 56 | clientConnection->makeRequest( 57 | asio::local::stream_protocol::endpoint{socketPath.string()}, 58 | std::move(request), 59 | onResponseReceived, 60 | cancelRequestOnTimeout); 61 | } 62 | 63 | void ClientService::makeRequest( 64 | const fs::path& socketPath, 65 | const http::Request& request, 66 | const std::function)>& responseHandler, 67 | std::chrono::milliseconds timeout) 68 | { 69 | if (requestProcessorQueue_) 70 | requestProcessorQueue_->stop(); 71 | auto cancelRequestOnTimeout = std::make_shared>([] {}); 72 | auto& responseTimeoutTimer = timerProvider_.emplaceTimer(); 73 | responseTimeoutTimer.start( 74 | timeout, 75 | [=] 76 | { 77 | (*cancelRequestOnTimeout)(); 78 | }); 79 | auto onResponseReceived = [=, &responseTimeoutTimer](std::optional fcgiResponse) 80 | { 81 | responseTimeoutTimer.stop(); 82 | if (fcgiResponse) { 83 | if (!fcgiResponse->errorMsg.empty()) 84 | eventHandler_(ErrorEvent::RequestProcessingError, fcgiResponse->errorMsg); 85 | responseHandler(http::responseFromString(fcgiResponse->data, http::ResponseMode::Cgi)); 86 | } 87 | else 88 | responseHandler(std::nullopt); 89 | if (requestProcessorQueue_) 90 | requestProcessorQueue_->launch(); 91 | }; 92 | auto& clientConnection = localClientConnections_.emplace_back( 93 | std::make_unique>(io_, eventHandler_)); 94 | auto [params, stdIn] = request.toFcgiData(http::FormType::Multipart); 95 | auto fcgiRequest = fastcgi::Request{std::move(params), std::move(stdIn)}; 96 | clientConnection->makeRequest( 97 | asio::local::stream_protocol::endpoint{socketPath.string()}, 98 | std::move(fcgiRequest), 99 | onResponseReceived, 100 | cancelRequestOnTimeout); 101 | } 102 | 103 | void ClientService::makeRequest( 104 | std::string_view ipAddress, 105 | uint16_t port, 106 | fastcgi::Request request, 107 | std::function)> responseHandler, 108 | std::chrono::milliseconds timeout) 109 | { 110 | if (requestProcessorQueue_) 111 | requestProcessorQueue_->stop(); 112 | auto cancelRequestOnTimeout = std::make_shared>([] {}); 113 | auto& responseTimeoutTimer = timerProvider_.emplaceTimer(); 114 | responseTimeoutTimer.start( 115 | timeout, 116 | [=] 117 | { 118 | (*cancelRequestOnTimeout)(); 119 | }); 120 | auto onResponseReceived = [=, &responseTimeoutTimer](std::optional fcgiResponse) 121 | { 122 | responseTimeoutTimer.stop(); 123 | if (fcgiResponse) { 124 | if (!fcgiResponse->errorMsg.empty()) 125 | eventHandler_(ErrorEvent::RequestProcessingError, fcgiResponse->errorMsg); 126 | responseHandler(fastcgi::Response{std::move(fcgiResponse->data), std::move(fcgiResponse->errorMsg)}); 127 | } 128 | else 129 | responseHandler(std::nullopt); 130 | if (requestProcessorQueue_) 131 | requestProcessorQueue_->launch(); 132 | }; 133 | auto& clientConnection = 134 | tcpClientConnections_.emplace_back(std::make_unique>(io_, eventHandler_)); 135 | auto address = asio::ip::make_address(ipAddress.data()); 136 | clientConnection->makeRequest( 137 | asio::ip::tcp::endpoint{address, port}, 138 | std::move(request), 139 | onResponseReceived, 140 | cancelRequestOnTimeout); 141 | } 142 | 143 | void ClientService::makeRequest( 144 | std::string_view ipAddress, 145 | uint16_t port, 146 | const http::Request& request, 147 | const std::function)>& responseHandler, 148 | std::chrono::milliseconds timeout) 149 | { 150 | if (requestProcessorQueue_) 151 | requestProcessorQueue_->stop(); 152 | auto cancelRequestOnTimeout = std::make_shared>([] {}); 153 | auto& responseTimeoutTimer = timerProvider_.emplaceTimer(); 154 | responseTimeoutTimer.start( 155 | timeout, 156 | [=] 157 | { 158 | (*cancelRequestOnTimeout)(); 159 | }); 160 | 161 | auto onResponseReceived = [=, &responseTimeoutTimer](std::optional fcgiResponse) 162 | { 163 | responseTimeoutTimer.stop(); 164 | if (fcgiResponse) { 165 | if (!fcgiResponse->errorMsg.empty()) 166 | eventHandler_(ErrorEvent::RequestProcessingError, fcgiResponse->errorMsg); 167 | responseHandler(http::responseFromString(fcgiResponse->data, http::ResponseMode::Cgi)); 168 | } 169 | else 170 | responseHandler(std::nullopt); 171 | if (requestProcessorQueue_) 172 | requestProcessorQueue_->launch(); 173 | }; 174 | auto& clientConnection = 175 | tcpClientConnections_.emplace_back(std::make_unique>(io_, eventHandler_)); 176 | auto [params, stdIn] = request.toFcgiData(http::FormType::Multipart); 177 | auto fcgiRequest = fastcgi::Request{std::move(params), std::move(stdIn)}; 178 | auto address = asio::ip::make_address(ipAddress); 179 | clientConnection->makeRequest( 180 | asio::ip::tcp::endpoint{address, port}, 181 | std::move(fcgiRequest), 182 | onResponseReceived, 183 | cancelRequestOnTimeout); 184 | } 185 | 186 | void ClientService::disconnect() 187 | { 188 | localClientConnections_.clear(); 189 | } 190 | 191 | void ClientService::setRequestProcessorQueue(whaleroute::RequestProcessorQueue& queue) 192 | { 193 | requestProcessorQueue_.get().emplace(queue); 194 | } 195 | 196 | } // namespace asyncgi::detail -------------------------------------------------------------------------------- /src/clientservice.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "clientconnection.h" 3 | #include "timerprovider.h" 4 | #include 5 | #ifdef ASYNCGI_USE_BOOST_ASIO 6 | #include 7 | #include 8 | #else 9 | #include 10 | #include 11 | #endif 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | namespace asyncgi::whaleroute { 23 | class RequestProcessorQueue; 24 | } 25 | 26 | namespace asyncgi::detail { 27 | 28 | class IOService; 29 | 30 | class ClientService { 31 | public: 32 | ClientService(asio::io_context&, EventHandlerProxy&); 33 | void makeRequest( 34 | const std::filesystem::path& socketPath, 35 | fastcgi::Request request, 36 | std::function)> responseHandler, 37 | std::chrono::milliseconds timeout = std::chrono::seconds{3}); 38 | void makeRequest( 39 | const std::filesystem::path& socketPath, 40 | const http::Request& request, 41 | const std::function)>& responseHandler, 42 | std::chrono::milliseconds timeout = std::chrono::seconds{3}); 43 | 44 | void makeRequest( 45 | std::string_view ipAddress, 46 | uint16_t port, 47 | fastcgi::Request request, 48 | std::function)> responseHandler, 49 | std::chrono::milliseconds timeout = std::chrono::seconds{3}); 50 | void makeRequest( 51 | std::string_view ipAddress, 52 | uint16_t port, 53 | const http::Request& request, 54 | const std::function)>& responseHandler, 55 | std::chrono::milliseconds timeout = std::chrono::seconds{3}); 56 | 57 | void disconnect(); 58 | 59 | void setRequestProcessorQueue(whaleroute::RequestProcessorQueue& queue); 60 | 61 | private: 62 | sfun::member io_; 63 | sfun::member eventHandler_; 64 | TimerProvider timerProvider_; 65 | std::vector>> localClientConnections_; 66 | std::vector>> tcpClientConnections_; 67 | sfun::member> requestProcessorQueue_; 68 | }; 69 | 70 | } // namespace asyncgi::detail 71 | -------------------------------------------------------------------------------- /src/connection.cpp: -------------------------------------------------------------------------------- 1 | #include "connection.h" 2 | #include "asio_error.h" 3 | #include "responsecontext.h" 4 | #include 5 | #ifdef ASYNCGI_USE_BOOST_ASIO 6 | #include 7 | #include 8 | #include 9 | #else 10 | #include 11 | #include 12 | #include 13 | #endif 14 | #include 15 | #include 16 | #include 17 | 18 | namespace asyncgi::detail { 19 | 20 | template 21 | Connection::Connection( 22 | RequestProcessor requestProcessor, 23 | asio::io_context& io, 24 | EventHandlerProxy& eventHandler, 25 | sfun::access_permission) 26 | : requestProcessor_{std::move(requestProcessor)} 27 | , asioDispatcher_{io} 28 | , timerProvider_{io} 29 | , client_{io, eventHandler} 30 | , socket_{io} 31 | , eventHandler_{eventHandler} 32 | { 33 | } 34 | 35 | template 36 | Connection::~Connection() = default; 37 | 38 | template 39 | asio::basic_socket& Connection::socket() 40 | { 41 | return socket_; 42 | } 43 | 44 | template 45 | void Connection::process() 46 | { 47 | socket_.async_read_some( 48 | asio::buffer(buffer_), 49 | [self = this->shared_from_this(), this](const auto& error, auto bytesRead) 50 | { 51 | if (error) { 52 | if (error.value() != asio::error::operation_aborted) { 53 | eventHandler_(ErrorEvent::SocketReadError, error.message()); 54 | close(); 55 | } 56 | return; 57 | } 58 | self->readData(bytesRead); 59 | }); 60 | } 61 | 62 | template 63 | void Connection::readData(std::size_t bytesRead) 64 | { 65 | fcgi::Responder::receiveData(buffer_.data(), bytesRead); 66 | process(); 67 | } 68 | 69 | template 70 | void Connection::sendData(const std::string& data) 71 | { 72 | if (bytesToWrite_) { 73 | nextWriteBuffer_ += data; 74 | return; 75 | } 76 | 77 | if (!nextWriteBuffer_.empty()) { 78 | writeBuffer_ = nextWriteBuffer_ + data; 79 | nextWriteBuffer_.clear(); 80 | } 81 | else 82 | writeBuffer_ = data; 83 | 84 | bytesToWrite_ += writeBuffer_.size(); 85 | asio::async_write( 86 | socket_, 87 | asio::buffer(writeBuffer_), 88 | [self = this->shared_from_this(), this](const auto& error, auto bytesWritten) 89 | { 90 | if (error) { 91 | eventHandler_(ErrorEvent::SocketWriteError, error.message()); 92 | close(); 93 | return; 94 | } 95 | self->onBytesWritten(bytesWritten); 96 | }); 97 | } 98 | 99 | template 100 | void Connection::onBytesWritten(std::size_t numOfBytes) 101 | { 102 | bytesToWrite_ -= numOfBytes; 103 | if (bytesToWrite_) 104 | return; 105 | 106 | if (!nextWriteBuffer_.empty()) 107 | sendData({}); 108 | else if (disconnectRequested_) 109 | close(); 110 | } 111 | 112 | template 113 | void Connection::disconnect() 114 | { 115 | disconnectRequested_ = true; 116 | if (bytesToWrite_ > 0) 117 | return; 118 | close(); 119 | } 120 | 121 | template 122 | void Connection::processRequest(fcgi::Request&& fcgiRequest, fcgi::Response&& fcgiResponse) 123 | { 124 | fcgiRequest_ = std::move(fcgiRequest); 125 | responseSender_.emplace(std::move(fcgiResponse)); 126 | const auto request = Request{*fcgiRequest_}; 127 | responseContext_ = std::make_shared(*responseSender_, timerProvider_, client_, asioDispatcher_); 128 | try { 129 | requestProcessor_(request, responseContext_); 130 | } 131 | catch (const std::exception& e) { 132 | auto response = asyncgi::Responder{responseContext_}; 133 | response.send(http::ResponseStatus::_500_Internal_Server_Error); 134 | eventHandler_(ErrorEvent::RequestProcessingError, e.what()); 135 | } 136 | catch (...) { 137 | auto response = asyncgi::Responder{responseContext_}; 138 | response.send(http::ResponseStatus::_500_Internal_Server_Error); 139 | eventHandler_(ErrorEvent::RequestProcessingError, "Unknown error"); 140 | } 141 | } 142 | 143 | template 144 | void Connection::close() 145 | { 146 | auto error = asio_error{}; 147 | socket_.shutdown(TProtocol::socket::shutdown_both, error); 148 | socket_.close(error); 149 | 150 | if (error) 151 | eventHandler_(ErrorEvent::SocketCloseError, error.message()); 152 | disconnectRequested_ = false; 153 | } 154 | 155 | template class Connection; 156 | template class Connection; 157 | 158 | } // namespace asyncgi::detail 159 | -------------------------------------------------------------------------------- /src/connection.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "asiodispatcherservice.h" 3 | #include "clientservice.h" 4 | #include "responsesender.h" 5 | #include "timerprovider.h" 6 | #include 7 | #ifdef ASYNCGI_USE_BOOST_ASIO 8 | #include 9 | #else 10 | #include 11 | #endif 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace ASYNCGI_ASIO { 25 | class io_context; 26 | } 27 | 28 | namespace asyncgi::detail { 29 | class ConnectionFactory; 30 | class ResponseContext; 31 | 32 | template 33 | class Connection : public std::enable_shared_from_this>, 34 | public fcgi::Responder { 35 | public: 36 | Connection(RequestProcessor, asio::io_context&, EventHandlerProxy&, sfun::access_permission); 37 | ~Connection() override; 38 | Connection(const Connection&) = delete; 39 | Connection& operator=(const Connection&) = delete; 40 | Connection(Connection&&) noexcept = default; 41 | Connection& operator=(Connection&&) noexcept = default; 42 | 43 | asio::basic_socket& socket(); 44 | void process(); 45 | void readData(std::size_t bytesRead); 46 | void sendData(const std::string& data) final; 47 | void disconnect() final; 48 | void processRequest(fcgi::Request&& request, fcgi::Response&& response) final; 49 | 50 | private: 51 | void onBytesWritten(std::size_t numOfBytes); 52 | void close(); 53 | 54 | private: 55 | std::shared_ptr responseContext_; 56 | std::optional fcgiRequest_; 57 | std::optional responseSender_; 58 | RequestProcessor requestProcessor_; 59 | AsioDispatcherService asioDispatcher_; 60 | TimerProvider timerProvider_; 61 | ClientService client_; 62 | asio::basic_stream_socket socket_; 63 | std::array buffer_; 64 | std::string writeBuffer_; 65 | std::string nextWriteBuffer_; 66 | std::size_t bytesToWrite_ = 0; 67 | bool disconnectRequested_ = false; 68 | sfun::member eventHandler_; 69 | }; 70 | 71 | } // namespace asyncgi::detail 72 | -------------------------------------------------------------------------------- /src/connectionfactory.cpp: -------------------------------------------------------------------------------- 1 | #include "connectionfactory.h" 2 | #include "connection.h" 3 | #include 4 | #ifdef ASYNCGI_USE_BOOST_ASIO 5 | #include 6 | #include 7 | #else 8 | #include 9 | #include 10 | #endif 11 | #include 12 | #include 13 | 14 | namespace asyncgi::detail { 15 | 16 | ConnectionFactory::ConnectionFactory( 17 | RequestProcessor requestProcessor, 18 | IOService& ioService, 19 | EventHandlerProxy& eventHandler) 20 | : requestProcessor_{std::move(requestProcessor)} 21 | , ioService_{ioService} 22 | , eventHandler_{eventHandler} 23 | { 24 | } 25 | 26 | template 27 | std::shared_ptr> ConnectionFactory::makeConnection() 28 | { 29 | return std::make_shared>( 30 | requestProcessor_, 31 | ioService_.get().nextIO(), 32 | eventHandler_, 33 | sfun::access_token{}); 34 | } 35 | 36 | template class std::shared_ptr> ConnectionFactory::makeConnection< 37 | asio::local::stream_protocol>(); 38 | template class std::shared_ptr> ConnectionFactory::makeConnection(); 39 | 40 | } // namespace asyncgi::detail 41 | -------------------------------------------------------------------------------- /src/connectionfactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "ioservice.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace asyncgi::detail { 10 | template 11 | class Connection; 12 | 13 | class ConnectionFactory { 14 | public: 15 | ConnectionFactory(RequestProcessor requestProcessor, IOService& ioService, EventHandlerProxy& eventHandler); 16 | template 17 | std::shared_ptr> makeConnection(); 18 | 19 | private: 20 | RequestProcessor requestProcessor_; 21 | sfun::member ioService_; 22 | sfun::member eventHandler_; 23 | }; 24 | } // namespace asyncgi::detail 25 | -------------------------------------------------------------------------------- /src/connectionlistener.cpp: -------------------------------------------------------------------------------- 1 | #include "connectionlistener.h" 2 | #include "connection.h" 3 | #include "connectionfactory.h" 4 | #include 5 | #ifdef ASYNCGI_USE_BOOST_ASIO 6 | #include 7 | #include 8 | #else 9 | #include 10 | #include 11 | #endif 12 | 13 | namespace asyncgi::detail { 14 | 15 | template 16 | ConnectionListener::ConnectionListener( 17 | std::unique_ptr> socketAcceptor, 18 | ConnectionFactory& connectionFactory, 19 | EventHandlerProxy& eventHandler) 20 | : socketAcceptor_{std::move(socketAcceptor)} 21 | , connectionFactory_{connectionFactory} 22 | , eventHandler_{eventHandler} 23 | 24 | { 25 | waitForConnection(); 26 | } 27 | 28 | template 29 | void ConnectionListener::waitForConnection() 30 | { 31 | auto connection = connectionFactory_.get().makeConnection(); 32 | socketAcceptor_->async_accept( 33 | connection->socket(), 34 | [this, connection](auto error_code) 35 | { 36 | onConnected(*connection, error_code); 37 | }); 38 | } 39 | 40 | template 41 | void ConnectionListener::onConnected(Connection& connection, const std::error_code& error) 42 | { 43 | if (error) { 44 | eventHandler_(ErrorEvent::ConnectionError, error.message()); 45 | return; 46 | } 47 | connection.process(); 48 | waitForConnection(); 49 | } 50 | 51 | template class ConnectionListener; 52 | template class ConnectionListener; 53 | 54 | } // namespace asyncgi::detail 55 | -------------------------------------------------------------------------------- /src/connectionlistener.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifdef ASYNCGI_USE_BOOST_ASIO 4 | #include 5 | #else 6 | #include 7 | #endif 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace asyncgi::detail { 14 | template 15 | class Connection; 16 | class ConnectionFactory; 17 | 18 | template 19 | class ConnectionListener { 20 | public: 21 | ConnectionListener( 22 | std::unique_ptr> socketAcceptor, 23 | ConnectionFactory& connectionFactory, 24 | EventHandlerProxy& eventHandler); 25 | 26 | private: 27 | void waitForConnection(); 28 | void onConnected(Connection& connection, const std::error_code& error); 29 | 30 | private: 31 | std::unique_ptr> socketAcceptor_; 32 | sfun::member connectionFactory_; 33 | sfun::member eventHandler_; 34 | }; 35 | 36 | } // namespace asyncgi::detail 37 | -------------------------------------------------------------------------------- /src/connectionlistenerfactory.cpp: -------------------------------------------------------------------------------- 1 | #include "connectionlistenerfactory.h" 2 | #include "connectionfactory.h" 3 | #include 4 | #ifdef ASYNCGI_USE_BOOST_ASIO 5 | #include 6 | #include 7 | #else 8 | #include 9 | #include 10 | #endif 11 | 12 | namespace asyncgi::detail { 13 | 14 | ConnectionListenerFactory::ConnectionListenerFactory( 15 | asio::io_context& io, 16 | std::unique_ptr connectionFactory, 17 | EventHandlerProxy& eventHandler) 18 | : io_{io} 19 | , connectionFactory_{std::move(connectionFactory)} 20 | , eventHandler_{eventHandler} 21 | { 22 | } 23 | 24 | ConnectionListenerFactory::~ConnectionListenerFactory() = default; 25 | 26 | template 27 | std::unique_ptr> ConnectionListenerFactory::makeConnectionListener( 28 | const typename TProtocol::endpoint& address) 29 | { 30 | return std::make_unique>( 31 | std::make_unique(io_.get(), address), 32 | *connectionFactory_, 33 | eventHandler_); 34 | } 35 | 36 | template std::unique_ptr> ConnectionListenerFactory:: 37 | makeConnectionListener(const asio::local::stream_protocol::endpoint& address); 38 | template std::unique_ptr> ConnectionListenerFactory::makeConnectionListener( 39 | const asio::ip::tcp::endpoint& address); 40 | } // namespace asyncgi::detail -------------------------------------------------------------------------------- /src/connectionlistenerfactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "connectionlistener.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace ASYNCGI_ASIO { 10 | class io_context; 11 | } 12 | 13 | namespace asyncgi::detail { 14 | class ConnectionFactory; 15 | 16 | class ConnectionListenerFactory { 17 | public: 18 | ConnectionListenerFactory(asio::io_context&, std::unique_ptr, EventHandlerProxy&); 19 | ~ConnectionListenerFactory(); 20 | ConnectionListenerFactory(const ConnectionListenerFactory&) = delete; 21 | ConnectionListenerFactory& operator=(const ConnectionListenerFactory&) = delete; 22 | ConnectionListenerFactory(ConnectionListenerFactory&&) noexcept = default; 23 | ConnectionListenerFactory& operator=(ConnectionListenerFactory&&) noexcept = default; 24 | 25 | template 26 | std::unique_ptr> makeConnectionListener(const typename TProtocol::endpoint& address); 27 | 28 | private: 29 | sfun::member io_; 30 | std::unique_ptr connectionFactory_; 31 | sfun::member eventHandler_; 32 | }; 33 | 34 | } // namespace asyncgi::detail 35 | -------------------------------------------------------------------------------- /src/eventhandlerproxy.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | namespace asyncgi::detail { 4 | 5 | EventHandlerProxy::EventHandlerProxy() = default; 6 | 7 | void EventHandlerProxy::operator()(ErrorEvent event, std::string_view message) 8 | { 9 | errorHandler_(event, message); 10 | } 11 | 12 | } //namespace asyncgi::detail 13 | -------------------------------------------------------------------------------- /src/io.cpp: -------------------------------------------------------------------------------- 1 | #include "ioservice.h" 2 | #include 3 | 4 | namespace asyncgi { 5 | 6 | IO::IO(int threadsNumber) 7 | : ioService_{std::make_unique(threadsNumber)} 8 | { 9 | } 10 | 11 | IO::~IO() = default; 12 | 13 | detail::IOService& IO::ioService(detail::IOAccessPermission) 14 | { 15 | return *ioService_; 16 | } 17 | 18 | detail::EventHandlerProxy& IO::eventHandler(detail::IOAccessPermission) 19 | { 20 | return eventHandler_; 21 | } 22 | 23 | void IO::run() 24 | { 25 | ioService_->run(); 26 | } 27 | 28 | void IO::stop() 29 | { 30 | ioService_->stop(); 31 | } 32 | 33 | } //namespace asyncgi -------------------------------------------------------------------------------- /src/ioservice.cpp: -------------------------------------------------------------------------------- 1 | #include "ioservice.h" 2 | #include 3 | #ifdef ASYNCGI_USE_BOOST_ASIO 4 | #include 5 | #else 6 | #include 7 | #endif 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | namespace asyncgi::detail { 14 | 15 | namespace { 16 | std::vector> makeIoPool(int threadCount) 17 | { 18 | auto res = std::vector>{}; 19 | for (auto i = 0; i < threadCount; ++i) 20 | res.emplace_back(std::make_unique()); 21 | return res; 22 | } 23 | 24 | std::vector> makeGuardPool( 25 | const std::vector>& ioPool) 26 | { 27 | auto result = std::vector>{}; 28 | for (auto& io : ioPool) 29 | result.emplace_back(std::make_unique(io->get_executor())); 30 | return result; 31 | } 32 | 33 | } //namespace 34 | 35 | IOService::IOService(int threadCount) 36 | : ioPool_{makeIoPool(threadCount)} 37 | , ioGuardPool_{makeGuardPool(ioPool_)} 38 | , signals_{ 39 | *ioPool_.at(0), 40 | SIGINT, 41 | #ifndef _WIN32 42 | SIGQUIT, 43 | #endif 44 | SIGTERM} 45 | { 46 | handleStopSignals(); 47 | } 48 | 49 | asio::io_context& IOService::io() const 50 | { 51 | return *ioPool_.at(0); 52 | } 53 | 54 | asio::io_context& IOService::nextIO() 55 | { 56 | return *ioPool_[ioIndex_++ % ioPool_.size()]; 57 | } 58 | 59 | void IOService::run() 60 | { 61 | for (auto i = 1; i < sfun::ssize(ioPool_); ++i) { 62 | threadPool_.emplace_back( 63 | [this, i] 64 | { 65 | ioPool_.at(i)->run(); 66 | }); 67 | } 68 | 69 | ioPool_.at(0)->run(); 70 | for (auto& thread : threadPool_) 71 | thread.join(); 72 | } 73 | 74 | void IOService::stop() 75 | { 76 | for (auto& io : ioPool_) 77 | io->stop(); 78 | } 79 | 80 | void IOService::handleStopSignals() 81 | { 82 | signals_.async_wait( 83 | [&](auto error, auto) 84 | { 85 | if (!error) { 86 | stop(); 87 | return; 88 | } 89 | handleStopSignals(); 90 | }); 91 | } 92 | 93 | } // namespace asyncgi::detail 94 | -------------------------------------------------------------------------------- /src/ioservice.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #ifdef ASYNCGI_USE_BOOST_ASIO 3 | #include 4 | #include 5 | #include 6 | #else 7 | #include 8 | #include 9 | #include 10 | #endif 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace asyncgi::detail { 18 | 19 | using asio_work_guard = asio::executor_work_guard; 20 | 21 | class IOService { 22 | public: 23 | explicit IOService(int threadCount); 24 | asio::io_context& io() const; 25 | asio::io_context& nextIO(); 26 | void run(); 27 | void stop(); 28 | 29 | private: 30 | void handleStopSignals(); 31 | 32 | private: 33 | std::vector> ioPool_; 34 | std::vector> ioGuardPool_; 35 | asio::signal_set signals_; 36 | std::vector threadPool_; 37 | std::atomic ioIndex_ = 0; 38 | }; 39 | 40 | } // namespace asyncgi::detail 41 | -------------------------------------------------------------------------------- /src/request.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace asyncgi { 6 | 7 | Request::Request(const fcgi::Request& request) 8 | : fcgiRequest_{request} 9 | , httpRequest_{[this] 10 | { 11 | return http::RequestView{ 12 | fcgiRequest().hasParam("REQUEST_METHOD") ? fcgiRequest().param("REQUEST_METHOD") 13 | : std::string_view{}, 14 | fcgiRequest().hasParam("REMOTE_ADDR") ? fcgiRequest().param("REMOTE_ADDR") 15 | : std::string_view{}, 16 | fcgiRequest().hasParam("HTTP_HOST") ? fcgiRequest().param("HTTP_HOST") 17 | : std::string_view{}, 18 | fcgiRequest().hasParam("REQUEST_URI") ? fcgiRequest().param("REQUEST_URI") 19 | : std::string_view{}, 20 | fcgiRequest().hasParam("QUERY_STRING") ? fcgiRequest().param("QUERY_STRING") 21 | : std::string_view{}, 22 | fcgiRequest().hasParam("HTTP_COOKIE") ? fcgiRequest().param("HTTP_COOKIE") 23 | : std::string_view{}, 24 | fcgiRequest().hasParam("CONTENT_TYPE") ? fcgiRequest().param("CONTENT_TYPE") 25 | : std::string_view{}, 26 | fcgiRequest().stdIn()}; 27 | }} 28 | { 29 | } 30 | 31 | const fcgi::Request& Request::fcgiRequest() const 32 | { 33 | return fcgiRequest_; 34 | } 35 | 36 | const http::RequestView& Request::httpRequest() const 37 | { 38 | return httpRequest_; 39 | } 40 | 41 | const std::string& Request::fcgiParam(const std::string& name) const 42 | { 43 | return fcgiRequest().param(name); 44 | } 45 | 46 | bool Request::hasFcgiParam(const std::string& name) const 47 | { 48 | return fcgiRequest().hasParam(name); 49 | } 50 | 51 | const std::vector>& Request::fcgiParams() const 52 | { 53 | return fcgiRequest().params(); 54 | } 55 | 56 | const std::string& Request::fcgiStdIn() const 57 | { 58 | return fcgiRequest().stdIn(); 59 | } 60 | 61 | std::string_view Request::ipAddress() const 62 | { 63 | return httpRequest().ipAddress(); 64 | } 65 | 66 | std::string_view Request::domainName() const 67 | { 68 | return httpRequest().domainName(); 69 | } 70 | 71 | std::string_view Request::path() const 72 | { 73 | return httpRequest().path(); 74 | } 75 | 76 | http::RequestMethod Request::method() const 77 | { 78 | return httpRequest().method(); 79 | } 80 | 81 | std::string_view Request::query(std::string_view name) const 82 | { 83 | return httpRequest().query(name); 84 | } 85 | 86 | bool Request::hasQuery(std::string_view name) const 87 | { 88 | return httpRequest().hasQuery(name); 89 | } 90 | 91 | std::string_view Request::cookie(std::string_view name) const 92 | { 93 | return httpRequest().cookie(name); 94 | } 95 | 96 | bool Request::hasCookie(std::string_view name) const 97 | { 98 | return httpRequest().hasCookie(name); 99 | } 100 | 101 | std::string_view Request::formField(std::string_view name, int index) const 102 | { 103 | return httpRequest().formField(name, index); 104 | } 105 | 106 | int Request::formFieldCount(std::string_view name) const 107 | { 108 | return httpRequest().formFieldCount(name); 109 | } 110 | 111 | bool Request::hasFormField(std::string_view name) const 112 | { 113 | return httpRequest().hasFormField(name); 114 | } 115 | 116 | std::string_view Request::fileData(std::string_view name, int index) const 117 | { 118 | return httpRequest().fileData(name, index); 119 | } 120 | 121 | int Request::fileCount(std::string_view name) const 122 | { 123 | return httpRequest().fileCount(name); 124 | } 125 | 126 | bool Request::hasFile(std::string_view name) const 127 | { 128 | return httpRequest().hasFile(name); 129 | } 130 | 131 | std::string_view Request::fileName(std::string_view name, int index) const 132 | { 133 | return httpRequest().fileName(name, index); 134 | } 135 | 136 | std::string_view Request::fileType(std::string_view name, int index) const 137 | { 138 | return httpRequest().fileType(name, index); 139 | } 140 | 141 | const std::vector& Request::queries() const 142 | { 143 | return httpRequest().queries(); 144 | } 145 | 146 | const std::vector& Request::cookies() const 147 | { 148 | return httpRequest().cookies(); 149 | } 150 | 151 | std::vector Request::formFieldList() const 152 | { 153 | return httpRequest().formFieldList(); 154 | } 155 | 156 | std::vector Request::fileList() const 157 | { 158 | return httpRequest().fileList(); 159 | } 160 | 161 | bool Request::hasFiles() const 162 | { 163 | return httpRequest().hasFiles(); 164 | } 165 | 166 | } // namespace asyncgi 167 | -------------------------------------------------------------------------------- /src/response.cpp: -------------------------------------------------------------------------------- 1 | #include "asiodispatcherservice.h" 2 | #include "clientservice.h" 3 | #include "responsecontext.h" 4 | #include "responsesender.h" 5 | #include "timerprovider.h" 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | namespace asyncgi { 15 | 16 | namespace detail { 17 | class TimerProvider; 18 | } // namespace detail 19 | 20 | Responder::Responder(std::shared_ptr responseContext) 21 | : responseContext_{responseContext} 22 | { 23 | } 24 | 25 | void Responder::send(const http::Response& response) 26 | { 27 | if (auto context = responseContext_.lock()) 28 | context->responseSender().send(response.data(http::ResponseMode::Cgi)); 29 | } 30 | 31 | void Responder::send(fastcgi::Response response) 32 | { 33 | if (auto context = responseContext_.lock()) 34 | context->responseSender().send(std::move(response.data), std::move(response.errorMsg)); 35 | } 36 | 37 | bool Responder::isSent() const 38 | { 39 | if (auto context = responseContext_.lock()) 40 | return context->responseSender().isSent(); 41 | 42 | return true; 43 | } 44 | 45 | std::weak_ptr Responder::context(detail::ResponseContextAccessPermission) 46 | { 47 | return responseContext_; 48 | } 49 | 50 | } // namespace asyncgi 51 | -------------------------------------------------------------------------------- /src/responsecontext.cpp: -------------------------------------------------------------------------------- 1 | #include "responsecontext.h" 2 | #include "asiodispatcherservice.h" 3 | #include "clientservice.h" 4 | #include "timerprovider.h" 5 | 6 | namespace asyncgi::detail { 7 | 8 | ResponseContext::ResponseContext( 9 | ResponseSender& responseSender, 10 | TimerProvider& timerProvider, 11 | ClientService& client, 12 | AsioDispatcherService& asioDispatcher) 13 | : responseSender_{responseSender} 14 | , timerProvider_{timerProvider} 15 | , client_{client} 16 | , asioDispatcher_{asioDispatcher} 17 | { 18 | } 19 | 20 | ResponseSender& ResponseContext::responseSender() const 21 | { 22 | return responseSender_; 23 | } 24 | 25 | ClientService& ResponseContext::client() const 26 | { 27 | return client_; 28 | } 29 | 30 | AsioDispatcherService& ResponseContext::asioDispatcher() const 31 | { 32 | return asioDispatcher_; 33 | } 34 | 35 | TimerProvider& ResponseContext::timerProvider() const 36 | { 37 | return timerProvider_; 38 | } 39 | 40 | void ResponseContext::setRequestProcessorQueue(const std::shared_ptr& queue) 41 | { 42 | requestProcessorQueue_ = queue; 43 | client_.get().setRequestProcessorQueue(*requestProcessorQueue_); 44 | asioDispatcher_.get().setRequestProcessorQueue(requestProcessorQueue_); 45 | timerProvider_.get().setRequestProcessorQueue(*requestProcessorQueue_); 46 | } 47 | 48 | } // namespace asyncgi::detail 49 | -------------------------------------------------------------------------------- /src/responsecontext.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace asyncgi::whaleroute { 9 | class RequestProcessorQueue; 10 | } 11 | 12 | namespace asyncgi::detail { 13 | class ResponseSender; 14 | class TimerProvider; 15 | class ClientService; 16 | class AsioDispatcherService; 17 | 18 | class ResponseContext { 19 | public: 20 | ResponseContext(ResponseSender&, TimerProvider&, ClientService&, AsioDispatcherService&); 21 | 22 | ResponseSender& responseSender() const; 23 | ClientService& client() const; 24 | AsioDispatcherService& asioDispatcher() const; 25 | TimerProvider& timerProvider() const; 26 | 27 | void setRequestProcessorQueue(const std::shared_ptr& queue); 28 | 29 | private: 30 | sfun::member responseSender_; 31 | sfun::member timerProvider_; 32 | sfun::member client_; 33 | sfun::member asioDispatcher_; 34 | std::shared_ptr requestProcessorQueue_; 35 | }; 36 | 37 | } // namespace asyncgi::detail -------------------------------------------------------------------------------- /src/responsesender.cpp: -------------------------------------------------------------------------------- 1 | #include "responsesender.h" 2 | 3 | namespace asyncgi::detail { 4 | 5 | ResponseSender::ResponseSender(fcgi::Response response) 6 | : response_{std::move(response)} 7 | { 8 | } 9 | 10 | void ResponseSender::send(std::string data) 11 | { 12 | send(data, {}); 13 | } 14 | 15 | void ResponseSender::send(std::string data, std::string errorMsg) 16 | { 17 | if (!response_.isValid()) 18 | return; 19 | 20 | response_.setData(std::move(data)); 21 | response_.setErrorMsg(std::move(errorMsg)); 22 | response_.send(); 23 | } 24 | 25 | bool ResponseSender::isSent() const 26 | { 27 | return !response_.isValid(); 28 | } 29 | 30 | } // namespace asyncgi::detail -------------------------------------------------------------------------------- /src/responsesender.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | namespace fcgi { 5 | class Response; 6 | } 7 | 8 | namespace asyncgi::detail { 9 | 10 | class ResponseSender { 11 | public: 12 | explicit ResponseSender(fcgi::Response response); 13 | void send(std::string data); 14 | void send(std::string data, std::string errorMsg); 15 | bool isSent() const; 16 | 17 | private: 18 | fcgi::Response response_; 19 | }; 20 | 21 | } // namespace asyncgi::detail 22 | -------------------------------------------------------------------------------- /src/routeresponsecontextaccessor.cpp: -------------------------------------------------------------------------------- 1 | #include "responsecontext.h" 2 | #include 3 | #include 4 | #include 5 | 6 | namespace asyncgi::detail { 7 | 8 | void RouterResponseContextAccessor::setRequestProcessorQueue( 9 | Responder& response, 10 | const std::shared_ptr& queue) 11 | { 12 | if (auto context = response.context(sfun::access_token{}).lock()) 13 | context->setRequestProcessorQueue(queue); 14 | } 15 | 16 | } //namespace asyncgi::detail -------------------------------------------------------------------------------- /src/server.cpp: -------------------------------------------------------------------------------- 1 | #include "connectionfactory.h" 2 | #include "connectionlistenerfactory.h" 3 | #include "serverservice.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace asyncgi { 10 | 11 | namespace detail { 12 | auto makeConnectionListenerFactory( 13 | RequestProcessor requestProcessor, 14 | IO& io, 15 | const sfun::access_token& accessToken) 16 | { 17 | auto connectionFactory = std::make_unique( 18 | std::move(requestProcessor), 19 | io.ioService(accessToken), 20 | io.eventHandler(accessToken)); 21 | 22 | return std::make_unique( 23 | io.ioService(accessToken).io(), 24 | std::move(connectionFactory), 25 | io.eventHandler(accessToken)); 26 | } 27 | } //namespace detail 28 | 29 | Server::Server(IO& io, RequestProcessor requestProcessor) 30 | : serverService_{std::make_unique( 31 | detail::makeConnectionListenerFactory(std::move(requestProcessor), io, sfun::access_token{}))} 32 | { 33 | } 34 | 35 | Server::~Server() = default; 36 | 37 | void Server::listen(std::string_view ipAddress, int port) 38 | { 39 | if (port < 0 || port > 65535) 40 | throw Error{"port's value must be in range [0, 65535]"}; 41 | 42 | serverService_->listen(ipAddress, port); 43 | } 44 | 45 | void Server::listen(const std::filesystem::path& unixDomainSocket) 46 | { 47 | serverService_->listen(unixDomainSocket); 48 | } 49 | 50 | } //namespace asyncgi -------------------------------------------------------------------------------- /src/serverservice.cpp: -------------------------------------------------------------------------------- 1 | #include "serverservice.h" 2 | #include "connectionlistener.h" 3 | #include "connectionlistenerfactory.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace asyncgi::detail { 10 | 11 | namespace fs = std::filesystem; 12 | 13 | ServerService::ServerService(std::unique_ptr connectionListenerFactory) 14 | : connectionListenerFactory_{std::move(connectionListenerFactory)} 15 | { 16 | } 17 | 18 | ServerService::~ServerService() = default; 19 | 20 | #ifndef _WIN32 21 | namespace { 22 | void initUnixDomainSocket(const fs::path& path) 23 | { 24 | umask(0); 25 | chmod(path.c_str(), 0777); 26 | unlink(path.c_str()); 27 | } 28 | } // namespace 29 | #endif 30 | 31 | void ServerService::listen(const fs::path& socketPath) 32 | { 33 | #ifndef _WIN32 34 | initUnixDomainSocket(socketPath); 35 | localConnectionProcessors_.emplace_back( 36 | connectionListenerFactory_->makeConnectionListener( 37 | asio::local::stream_protocol::endpoint{socketPath})); 38 | #else 39 | [[maybe_unused]] auto& unused = socketPath; 40 | throw Error{"Unix domain hosts aren't supported on Windows. Use a TCP host with Server::listen(std::string_view " 41 | "ipAddress, uint16_t portNumber)."}; 42 | #endif 43 | } 44 | 45 | void ServerService::listen(std::string_view ipAddress, int portNumber) 46 | { 47 | sfun_precondition(portNumber >= 0 && portNumber < 65536); 48 | 49 | auto address = asio::ip::make_address(ipAddress.data()); 50 | tcpConnectionProcessors_.emplace_back(connectionListenerFactory_->makeConnectionListener( 51 | asio::ip::tcp::endpoint{address, static_cast(portNumber)})); 52 | } 53 | 54 | } // namespace asyncgi::detail 55 | -------------------------------------------------------------------------------- /src/serverservice.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifdef ASYNCGI_USE_BOOST_ASIO 4 | #include 5 | #include 6 | #else 7 | #include 8 | #include 9 | #endif 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace asyncgi::detail { 18 | class ConnectionListenerFactory; 19 | template 20 | class ConnectionListener; 21 | 22 | class ServerService { 23 | public: 24 | explicit ServerService(std::unique_ptr); 25 | ~ServerService(); 26 | ServerService(const ServerService&) = delete; 27 | ServerService& operator=(const ServerService&) = delete; 28 | ServerService(ServerService&&) noexcept = default; 29 | ServerService& operator=(ServerService&&) noexcept = default; 30 | 31 | void listen(const std::filesystem::path& socketPath); 32 | void listen(std::string_view ipAddress, int portNumber); 33 | 34 | private: 35 | std::unique_ptr connectionListenerFactory_; 36 | std::vector>> localConnectionProcessors_; 37 | std::vector>> tcpConnectionProcessors_; 38 | }; 39 | 40 | } // namespace asyncgi::detail 41 | -------------------------------------------------------------------------------- /src/serviceholder.cpp: -------------------------------------------------------------------------------- 1 | #include "asiodispatcherservice.h" 2 | #include "clientservice.h" 3 | #include "timerservice.h" 4 | #include 5 | 6 | namespace asyncgi::detail { 7 | 8 | template 9 | ServiceHolder::ServiceHolder(std::unique_ptr service) 10 | : service_{std::move(service)} 11 | { 12 | } 13 | 14 | template 15 | ServiceHolder::ServiceHolder(sfun::optional_ref service) 16 | : serviceOptionalRef_{service} 17 | { 18 | } 19 | 20 | template 21 | ServiceHolder::~ServiceHolder() = default; 22 | 23 | template 24 | T& ServiceHolder::get() 25 | { 26 | if (service_) 27 | return *service_; 28 | return *serviceOptionalRef_; 29 | } 30 | 31 | template 32 | bool ServiceHolder::has_value() const 33 | { 34 | return service_ || serviceOptionalRef_; 35 | } 36 | 37 | template class ServiceHolder; 38 | template class ServiceHolder; 39 | template class ServiceHolder; 40 | 41 | } //namespace asyncgi::detail -------------------------------------------------------------------------------- /src/taskcontext.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace asyncgi { 5 | 6 | TaskContext::PostAction::PostAction(std::function action) 7 | : action_{std::move(action)} 8 | { 9 | } 10 | 11 | TaskContext::PostAction::~PostAction() 12 | { 13 | if (action_) 14 | action_(); 15 | } 16 | 17 | TaskContext::TaskContext(asio::io_context& io, std::function postTaskAction) 18 | : io_{io} 19 | , postTaskAction_{std::make_shared(std::move(postTaskAction))} 20 | { 21 | } 22 | 23 | asio::io_context& TaskContext::io() const 24 | { 25 | return io_; 26 | } 27 | 28 | } // namespace asyncgi 29 | -------------------------------------------------------------------------------- /src/timer.cpp: -------------------------------------------------------------------------------- 1 | #include "ioservice.h" 2 | #include "responsecontext.h" 3 | #include "timerprovider.h" 4 | #include "timerservice.h" 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace asyncgi { 12 | 13 | Timer::Timer(IO& io) 14 | : timerService_{std::make_unique(io.ioService(sfun::access_token{}).io())} 15 | { 16 | } 17 | 18 | namespace { 19 | sfun::optional_ref getTimerService(Responder& response, sfun::access_token accessToken) 20 | { 21 | if (auto context = response.context(accessToken).lock()) 22 | return context->timerProvider().emplaceTimer(); 23 | else 24 | return std::nullopt; 25 | } 26 | } //namespace 27 | 28 | Timer::Timer(Responder& response) 29 | : timerService_{getTimerService(response, sfun::access_token{})} 30 | { 31 | } 32 | 33 | void Timer::start(std::chrono::milliseconds time, std::function callback) 34 | { 35 | if (!timerService_.has_value()) 36 | return; 37 | 38 | timerService_.get().start(time, callback); 39 | } 40 | 41 | void Timer::startPeriodic(std::chrono::milliseconds time, std::function callback) 42 | { 43 | if (!timerService_.has_value()) 44 | return; 45 | 46 | timerService_.get().startPeriodic(time, callback); 47 | } 48 | 49 | void Timer::stop() 50 | { 51 | if (!timerService_.has_value()) 52 | return; 53 | 54 | timerService_.get().stop(); 55 | } 56 | 57 | std::function Timer::stopSignal() 58 | { 59 | if (!timerService_.has_value()) 60 | return [] {}; 61 | 62 | return [timerService = &timerService_.get()] 63 | { 64 | timerService->stop(); 65 | }; 66 | } 67 | 68 | } //namespace asyncgi 69 | -------------------------------------------------------------------------------- /src/timerprovider.cpp: -------------------------------------------------------------------------------- 1 | #include "timerprovider.h" 2 | #include "timerservice.h" 3 | #include 4 | #ifdef ASYNCGI_USE_BOOST_ASIO 5 | #include 6 | #else 7 | #include 8 | #endif 9 | 10 | namespace asyncgi::detail { 11 | 12 | TimerProvider::TimerProvider(asio::io_context& io) 13 | : io_{io} 14 | { 15 | } 16 | 17 | TimerService& TimerProvider::emplaceTimer() 18 | { 19 | return timers_.emplace_back(io_, requestProcessorQueue_); 20 | } 21 | 22 | void TimerProvider::setRequestProcessorQueue(whaleroute::RequestProcessorQueue& queue) 23 | { 24 | requestProcessorQueue_ = queue; 25 | } 26 | 27 | } // namespace asyncgi::detail 28 | -------------------------------------------------------------------------------- /src/timerprovider.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "timerservice.h" 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace ASYNCGI_ASIO { 9 | class io_context; 10 | } 11 | 12 | namespace asyncgi::whaleroute { 13 | class RequestProcessorQueue; 14 | } 15 | 16 | namespace asyncgi::detail { 17 | 18 | class TimerProvider { 19 | public: 20 | explicit TimerProvider(asio::io_context& io); 21 | TimerService& emplaceTimer(); 22 | void setRequestProcessorQueue(whaleroute::RequestProcessorQueue& queue); 23 | 24 | private: 25 | sfun::member io_; 26 | std::deque timers_; 27 | sfun::member> requestProcessorQueue_; 28 | }; 29 | 30 | } // namespace asyncgi::detail 31 | -------------------------------------------------------------------------------- /src/timerservice.cpp: -------------------------------------------------------------------------------- 1 | #include "timerservice.h" 2 | #include 3 | #include 4 | #ifdef ASYNCGI_USE_BOOST_ASIO 5 | #include 6 | #else 7 | #include 8 | #endif 9 | 10 | namespace asyncgi::detail { 11 | 12 | TimerService::TimerService( 13 | asio::io_context& io, 14 | sfun::optional_ref requestProcessorQueue) 15 | : io_{io} 16 | , timer_{io_.get()} 17 | , requestProcessorQueue_{requestProcessorQueue} 18 | { 19 | } 20 | 21 | void TimerService::start(std::chrono::milliseconds time, std::function callback) 22 | { 23 | if (requestProcessorQueue_) 24 | requestProcessorQueue_->stop(); 25 | timer_.expires_after(time); 26 | timer_.async_wait( 27 | [this, task = std::move(callback)](auto& ec) mutable 28 | { 29 | if (ec) 30 | return; 31 | task(); 32 | stop(); 33 | }); 34 | } 35 | 36 | void TimerService::startPeriodic(std::chrono::milliseconds time, std::function callback) 37 | { 38 | if (requestProcessorQueue_) 39 | requestProcessorQueue_->stop(); 40 | 41 | timer_.expires_after(time); 42 | timer_.async_wait( 43 | [this, time, task = std::move(callback)](auto& ec) mutable 44 | { 45 | if (ec) 46 | return; 47 | task(); 48 | startPeriodic(time, std::move(task)); 49 | }); 50 | } 51 | 52 | void TimerService::stop() 53 | { 54 | timer_.cancel(); 55 | 56 | if (requestProcessorQueue_) 57 | requestProcessorQueue_->launch(); 58 | } 59 | 60 | } // namespace asyncgi::detail 61 | -------------------------------------------------------------------------------- /src/timerservice.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #ifdef ASYNCGI_USE_BOOST_ASIO 4 | #include 5 | #else 6 | #include 7 | #endif 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace ASYNCGI_ASIO { 16 | class io_context; 17 | } 18 | 19 | namespace asyncgi::whaleroute { 20 | class RequestProcessorQueue; 21 | } 22 | 23 | namespace asyncgi::detail { 24 | 25 | class TimerService { 26 | public: 27 | explicit TimerService( 28 | asio::io_context& io, 29 | sfun::optional_ref requestProcessorQueue_ = {}); 30 | void start(std::chrono::milliseconds time, std::function callback); 31 | void startPeriodic(std::chrono::milliseconds time, std::function callback); 32 | void stop(); 33 | 34 | private: 35 | sfun::member io_; 36 | asio::steady_timer timer_; 37 | sfun::member> requestProcessorQueue_; 38 | }; 39 | 40 | } // namespace asyncgi::detail 41 | -------------------------------------------------------------------------------- /test_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 | endforeach () -------------------------------------------------------------------------------- /test_examples/test_exception_in_request_handler.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace http = asyncgi::http; 6 | 7 | void errorHandler(asyncgi::ErrorEvent, std::string_view message) 8 | { 9 | std::cerr << message << std::endl; 10 | } 11 | 12 | int main() 13 | { 14 | auto io = asyncgi::IO{errorHandler}; 15 | auto router = asyncgi::Router{io}; 16 | router.route("/", http::RequestMethod::Get) 17 | .process( 18 | [](const asyncgi::Request&, asyncgi::Responder&) 19 | { 20 | throw std::runtime_error{"Can't send a response"}; 21 | }); 22 | 23 | auto server = asyncgi::Server{io, router}; 24 | #ifndef _WIN32 25 | server.listen("/tmp/fcgi.sock"); 26 | #else 27 | server.listen("127.0.0.1", 9088); 28 | #endif 29 | io.run(); 30 | return 0; 31 | } -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lunchtoast", 3 | "version-string": "0.1.0", 4 | "dependencies": [ 5 | "boost-asio" 6 | ] 7 | } 8 | --------------------------------------------------------------------------------