├── .clang-format ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ └── CI.yml ├── .gitignore ├── .gitmodules ├── CMakeLists.txt ├── README.md ├── docs ├── README.md ├── assets │ ├── project_banner.png │ └── request-response-diagram.png ├── examples │ ├── DIRECTORY_LISTING.md │ ├── LOGGER.md │ └── REDIRECTION.md ├── general │ ├── CONTRIBUTING.md │ ├── GETTING_STARTED.md │ ├── MODULES.md │ ├── STANDARDS.md │ └── WHY_US.md └── guides │ ├── CONFIG.md │ ├── IMPLEMENT_HANDLER.md │ ├── IMPLEMENT_NETWORK.md │ ├── IMPLEMENT_POSTPROCESSOR.md │ ├── IMPLEMENT_PREPROCESSOR.md │ ├── INSTALL_AND_BUILD.md │ └── MODULES_101.md ├── examples ├── dylib │ └── Module.cpp └── modules │ ├── compressor │ └── CompressorModule.hpp │ ├── decompressor │ └── DecompressorModule.hpp │ ├── directory-listing │ └── DirectoryListingModule.hpp │ ├── logger │ └── LoggerModule.hpp │ └── redirection │ └── Module.hpp ├── include └── ziapi │ ├── Color.hpp │ ├── Config.hpp │ ├── Http.hpp │ ├── HttpConstants.hpp │ ├── Logger.hpp │ ├── Module.hpp │ └── Version.hpp └── tests ├── Compressor.cpp ├── Config.cpp ├── Decompressor.cpp ├── Logger.cpp ├── Module.cpp └── Version.cpp /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | IndentWidth: 4 3 | AccessModifierOffset: -4 4 | BreakBeforeBraces: Custom 5 | BraceWrapping: 6 | AfterClass: false 7 | AfterFunction: true 8 | AfterControlStatement: false 9 | AllowShortIfStatementsOnASingleLine: Never 10 | ColumnLimit: 120 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: 'My Bug Report' 5 | labels: 'bug' 6 | 7 | --- 8 | 9 | ### Description 10 | 11 | Describe your issue in as much detail as possible here. 12 | 13 | ### Your environment 14 | 15 | * OS and version 16 | * branch that causes this issue 17 | 18 | ### Steps to reproduce 19 | 20 | * Tell us how to reproduce this issue
21 | * Where the issue is, if you know
22 | * Which commands triggered the issue, if any 23 | 24 | ### Expected behaviour 25 | 26 | Tell us what should happen 27 | 28 | ### Actual behaviour 29 | 30 | Tell us what happens instead 31 | 32 | ### Logs 33 | 34 | Please paste any logs here that demonstrate the issue, if they exist 35 | 36 | ### Proposed solution 37 | 38 | If you have an idea of how to fix this issue, please write it down here, so we can begin discussing it 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: 'My Feature' 5 | labels: 'enhancement' 6 | 7 | --- 8 | 9 | ### Description 10 | 11 | Describe your issue in as much detail as possible here. 12 | 13 | ### Is your feature request related to a problem? Please describe. 14 | 15 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 16 | 17 | ### Describe the solution you'd like 18 | 19 | A clear and concise description of what you want to happen. 20 | 21 | ### Additional context 22 | 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please provide a detailed description of what was done in this PR. 4 | 5 | # Breaking changes 6 | 7 | Please complete this section if any breaking changes have been made, otherwise delete it. 8 | 9 | # Additional Changes 10 | 11 | # Checklist 12 | 13 | - [ ] I have assigned this PR to myself 14 | - [ ] I have added at least 1 reviewer 15 | - [ ] I have tested this code 16 | - [ ] I have added sufficient documentation and/or updated documentation to reflect my changes 17 | 18 | # Additional comments 19 | 20 | Please post additional comments in this section if you have them, otherwise delete it. 21 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | defaults: 6 | run: 7 | shell: bash 8 | 9 | jobs: 10 | build_and_test: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [windows-latest, ubuntu-latest, macos-latest] 15 | include: 16 | - os: windows-latest 17 | bin: unit_tests.exe 18 | dir: ./Debug/ 19 | - os: ubuntu-latest 20 | bin: unit_tests 21 | dir: ./ 22 | - os: macos-latest 23 | bin: unit_tests 24 | dir: ./ 25 | 26 | name: ${{ matrix.os }} 27 | runs-on: ${{ matrix.os }} 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v2 32 | 33 | - name: Checkout submodules 34 | run: git submodule update --init --recursive 35 | 36 | - name: Generate Tests Build File 37 | run: cmake . -B build -DUNIT_TESTS=ON 38 | 39 | - name: Build Unit Tests 40 | run: cmake --build build 41 | 42 | - name: Run Unit Tests 43 | run: ${{ matrix.dir }}${{ matrix.bin }} 44 | 45 | memory_check: 46 | name: memory check 47 | runs-on: ubuntu-latest 48 | 49 | steps: 50 | - uses: actions/checkout@v2 51 | 52 | - name: Update Packages 53 | run: sudo apt update 54 | 55 | - name: Install Valgrind 56 | run: sudo apt install -y valgrind 57 | 58 | - name: Checkout submodules 59 | run: git submodule update --init --recursive 60 | 61 | - name: Generate Tests Build File 62 | run: cmake . -B build_tests -DUNIT_TESTS=ON -DCMAKE_BUILD_TYPE=Debug 63 | 64 | - name: Build Unit Tests 65 | run: cmake --build build_tests 66 | 67 | - name: Run Unit Tests with Valgrind 68 | run: valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 ./unit_tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Editor Files and Folders 2 | 3 | .idea/ 4 | .vscode/ 5 | .DS_Store 6 | *~ 7 | \#*# 8 | 9 | # Build Files and Binaries 10 | 11 | *.log 12 | *.o 13 | *.so 14 | *.dll 15 | *.dylib 16 | cmake-build-*/ 17 | *build/ 18 | unit_tests 19 | 20 | # Documentation Generation Files 21 | 22 | doc/latex/ 23 | doc/html/ -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "include/dylib"] 2 | path = include/dylib 3 | url = https://github.com/martin-olivier/dylib 4 | branch = v1.8.1 5 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.17) 2 | 3 | # The name of the CMake project 4 | project(ziapi) 5 | 6 | # The C++ standard you want to use for your project 7 | set(CMAKE_CXX_STANDARD 17) 8 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 9 | 10 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) 11 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}) 12 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}) 13 | 14 | option(UNIT_TESTS "When sets to ON, build the unit tests" OFF) 15 | 16 | if(UNIT_TESTS) 17 | 18 | # Fetch GoogleTest 19 | find_package(googletest QUIET) 20 | include(FetchContent) 21 | FetchContent_Declare( 22 | googletest 23 | URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip 24 | ) 25 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 26 | FetchContent_MakeAvailable(googletest) 27 | 28 | # The bin name for your unit tests 29 | set(BIN unit_tests) 30 | 31 | # The build flags 32 | if(UNIX) 33 | add_compile_options(-Wall -Wextra -Weffc++) 34 | elseif(WIN32) 35 | add_compile_options(/W4) 36 | endif() 37 | 38 | # The list of tests source files 39 | set(TEST_SOURCES 40 | tests/Logger.cpp 41 | tests/Compressor.cpp 42 | tests/Decompressor.cpp 43 | tests/Config.cpp 44 | tests/Module.cpp 45 | tests/Version.cpp 46 | ) 47 | 48 | add_executable(${BIN} 49 | ${SOURCES} 50 | ${TEST_SOURCES} 51 | ) 52 | enable_testing() 53 | include(GoogleTest) 54 | gtest_discover_tests(${BIN}) 55 | target_link_libraries(${BIN} PRIVATE gtest_main) 56 | 57 | if(UNIX) 58 | target_link_libraries(${BIN} PRIVATE dl) 59 | endif() 60 | 61 | # The include path 62 | include_directories( 63 | ${PROJECT_SOURCE_DIR}/include 64 | ${PROJECT_SOURCE_DIR}/examples/modules 65 | ${PROJECT_SOURCE_DIR}/examples/utils 66 | ) 67 | 68 | add_library(module SHARED examples/dylib/Module.cpp) 69 | set_target_properties(module PROPERTIES PREFIX "") 70 | 71 | add_dependencies(${BIN} module) 72 | 73 | endif() 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZIAPI 2 | 3 | ![Banner](docs/assets/project_banner.png) 4 | 5 | [![ZiAPI](https://img.shields.io/badge/ZiAPI-v5.0.0-blue.svg)](https://github.com/martin-olivier/ZiAPI/releases/tag/v5.0.0) 6 | [![CPP Version](https://img.shields.io/badge/C++-17_and_above-darkgreen.svg)](https://isocpp.org/) 7 | [![Discord](https://img.shields.io/discord/934852777136513075)](https://discord.gg/CzKv6dGXmf) 8 | [![workflow](https://github.com/martin-olivier/ZiAPI/actions/workflows/CI.yml/badge.svg)](https://github.com/martin-olivier/ZiAPI/actions/workflows/CI.yml) 9 | 10 | Welcome to the **ZIAPI** repository which contains the interfaces and concrete implementations that make up our Epitech Zia project API proposal. 11 | 12 | **ZIAPI** was elected as the city-wide API for the **Paris and Marseille Regions** in **2022**. 13 | 14 | [**ZIAPI Official Website**](https://ziapi.vercel.app) 15 | [**ZIAPI Official Discord**](https://discord.gg/CzKv6dGXmf) 16 | 17 | ## Documentation 18 | 19 | You can find the documentation for the **ZIAPI** in our [documentation section](docs/README.md). 20 | 21 | Here are the most useful doc pages for starting out: 22 | - [Getting started](docs/general/GETTING_STARTED.md) 23 | - [Install and build guide](docs/guides/INSTALL_AND_BUILD.md) 24 | - [Module interfaces](docs/general/MODULES.md) 25 | 26 | Here is a quick overview of how the request / response cycle is handled with the ZIAPI. 27 | 28 | ![Request / Response flow](docs/assets/request-response-diagram.png) 29 | 30 | ## Usage 31 | 32 | ### Fetch ZiAPI using CMake 33 | 34 | Add the following content to your CMakeLists to fetch the API and include its header files in your project: 35 | ```cmake 36 | include(ExternalProject) 37 | 38 | ExternalProject_Add( 39 | ziapi 40 | GIT_REPOSITORY https://github.com/martin-olivier/ZiAPI.git 41 | GIT_TAG v5.0.0 42 | INSTALL_COMMAND "" 43 | TEST_COMMAND "" 44 | ) 45 | 46 | add_dependencies(zia ziapi) 47 | ExternalProject_Get_Property(ziapi SOURCE_DIR) 48 | include_directories(${SOURCE_DIR}/include) 49 | ``` 50 | 51 | > :bulb: Don't forget to link with `libdl` on unix if you use `dylib`: 52 | ```cmake 53 | if(UNIX) 54 | target_link_libraries(zia PRIVATE dl) 55 | endif() 56 | ``` 57 | 58 | ## Contact 59 | 60 | Feel free to submit any issues on our GitHub repository or ask questions on our [Discord](https://discord.gg/CzKv6dGXmf) 61 | 62 | ## Authors 63 | 64 | - [Martin Olivier](https://github.com/martin-olivier) 65 | - [Diego Rojas](https://github.com/rojasdiegopro) 66 | - [Edouard Sengeissen](https://github.com/edouard-sn) 67 | - [Nicolas Allain](https://github.com/Nirasak) 68 | - [Romain Minguet](https://github.com/Romain-1) 69 | - [Allan Debeve](https://github.com/Gfaim) 70 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ZIAPI Documentation 2 | 3 | Welcome to the main documentation page of the **ZIAPI**. The **ZIAPI** is a set of interfaces and concrete implementations to help you manage module implementation for the Zia project. 4 | 5 | This document lists all the documentation pages for the API separated by category. 6 | 7 | ## General Documentation 8 | 9 | General documentation pages will help you understand the general concepts of the ZIAPI better. 10 | 11 | - [Getting started](general/GETTING_STARTED.md) 12 | - [Why choose ZIAPI](general/WHY_US.md) 13 | - [Modules](general/MODULES.md) 14 | - [ZIAPI Standards](general/STANDARDS.md) 15 | - [Contributing](general/CONTRIBUTING.md) 16 | 17 | ## Guides 18 | 19 | Guides are step by step instructions on how to do pretty much anything with the ZIAPI. 20 | 21 | - [Install and build](guides/INSTALL_AND_BUILD.md) 22 | - [Module 101](guides/MODULES_101.md) 23 | - [The config object](guides/CONFIG.md) 24 | - [Implementing a handler module](guides/IMPLEMENT_HANDLER.md) 25 | - [Implementing a pre-processor module](guides/IMPLEMENT_PREPROCESSOR.md) 26 | - [Implementing a post-processor module](guides/IMPLEMENT_POSTPROCESSOR.md) 27 | - [Implementing a network module](guides/IMPLEMENT_NETWORK.md) 28 | 29 | ## Module Examples 30 | 31 | Module examples are code tutorials on how to build a variety of modules. 32 | 33 | - [Logger Module Tutorial](examples/LOGGER.md) 34 | - [Directory Listing Module Tutorial](examples/DIRECTORY_LISTING.md) 35 | - [Redirection Module Tutorial](examples/REDIRECTION.md) 36 | -------------------------------------------------------------------------------- /docs/assets/project_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martin-olivier/ZiAPI/838e46d1674c3e2d67a2eda790bcc3f15ff98906/docs/assets/project_banner.png -------------------------------------------------------------------------------- /docs/assets/request-response-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martin-olivier/ZiAPI/838e46d1674c3e2d67a2eda790bcc3f15ff98906/docs/assets/request-response-diagram.png -------------------------------------------------------------------------------- /docs/examples/DIRECTORY_LISTING.md: -------------------------------------------------------------------------------- 1 | # Directory Listing Module Tutorial 2 | 3 | Let's implement a simple directory listing module. 4 | 5 | ## Purpose 6 | 7 | This module will handle incoming http requests and serve the file contained in the request's target field. 8 | 9 | For example, for the following request: 10 | ``` 11 | GET /index.html HTTP/1.1 12 | ``` 13 | 14 | Our module will open the `index.html` and serve it back to the user. 15 | 16 | ## Tutorial 17 | 18 | First, let's implement the `IHandlerModule` interface. We give it a basic priority and we specify that it only handles GET requests. 19 | 20 | ```c++ 21 | #include 22 | 23 | #include "ziapi/Module.hpp" 24 | 25 | class DirectoryListingModule : public ziapi::IHandlerModule { 26 | public: 27 | void Init(const ziapi::config::Node &cfg) override {} 28 | 29 | [[nodiscard]] ziapi::Version GetVersion() const noexcept override { return {4, 0, 0}; } 30 | 31 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept override { return {4, 0, 0}; } 32 | 33 | [[nodiscard]] const char *GetName() const noexcept override { return "DirectoryListing"; } 34 | 35 | [[nodiscard]] const char *GetDescription() const noexcept override { return "Give access to a filesystem over HTTP"; } 36 | 37 | [[nodiscard]] double GetHandlerPriority() const noexcept override 38 | { 39 | /// Our module doesn't have any specific priority requirements. 40 | return 0.5f; 41 | } 42 | 43 | [[nodiscard]] bool ShouldHandle(const ziapi::http::Context &ctx, const ziapi::http::Request &req) const override 44 | { 45 | /// We only want to handle GET requests. 46 | return req.method == ziapi::http::method::kGet; 47 | } 48 | 49 | void Handle(ziapi::http::Context &ctx, const ziapi::http::Request &req, ziapi::http::Response &res) override {} 50 | }; 51 | ``` 52 | 53 | First thing we want to do is make our module configurable. Imagine we want to serve a specific directory on our computer. Like let's say we want to make the contents of the `/var/www` directory available. 54 | 55 | Well, we can add the path to this directory as a variable of our config file and fetch it in the `Init()` function. We will store it in a `root_` member variable of our class. 56 | 57 | ```c++ 58 | ... 59 | 60 | void Init(const ziapi::config::Node &cfg) override 61 | { 62 | /// In our config, we can specify which folder our module serves. 63 | root_ = cfg["modules"]["directoryListing"]["root"].AsString(); 64 | } 65 | 66 | ... 67 | ``` 68 | 69 | Let's now code the `Handle()` method. We first obtain the path of the file we want to serve, then we check if it's a directory or a file and we either send back the contents of the file or the contents of the directory using `std::filesystem`. 70 | 71 | ```c++ 72 | ... 73 | 74 | void Handle(ziapi::http::Context &ctx, const ziapi::http::Request &req, ziapi::http::Response &res) override 75 | { 76 | auto filepath = std::filesystem::path(root_) / std::filesystem::path(req.target); 77 | std::error_code ec; 78 | 79 | res.status_code = ziapi::http::code::OK; 80 | res.reason = ziapi::http::reason::OK; 81 | if (std::filesystem::is_directory(filepath, ec)) { 82 | std::ostringstream ss; 83 | for (const auto &entry : std::filesystem::directory_iterator(filepath)) { 84 | ss << entry.path().string() << "\n"; 85 | } 86 | res.body = ss.str(); 87 | return; 88 | } else { 89 | std::ifstream file_stream(filepath.filename()); 90 | std::ostringstream ss; 91 | ss << file_stream.rdbuf(); 92 | res.body = ss.str(); 93 | return; 94 | } 95 | res.status_code = ziapi::http::code::NOT_FOUND; 96 | res.reason = ziapi::http::reason::NOT_FOUND; 97 | } 98 | 99 | ... 100 | ``` 101 | 102 | You can check the full source code for this example [here](/examples/modules/directory-listing/DirectoryListingModule.hpp). 103 | -------------------------------------------------------------------------------- /docs/examples/LOGGER.md: -------------------------------------------------------------------------------- 1 | # Logger Module Tutorial 2 | 3 | Let's implement a simple logger module. 4 | 5 | ## Purpose 6 | 7 | This module will log every requests that goes through our API and their state. 8 | 9 | For example, for the following request: 10 | ``` 11 | GET /users/notfound HTTP/1.1 12 | ``` 13 | 14 | Our module will log the status code, the time the request took, the reason and the target, so we could have an output like: 15 | ``` 16 | [X] 404: Not found exception (GET /users/notfound HTTP/1.1, 4.02s) 17 | ``` 18 | 19 | ## Tutorial 20 | 21 | This module is going to be a little special. It's going to have **two types**, it's going to be a post-processor **and** a pre-processor. We'll use diamond heritage for this! 22 | 23 | Let's implement the `IPreProcessorModule` and the `IPostProcessorModule` interfaces. 24 | We want it to log every request, so we'll return `true` in `ShouldPreProcess` and `ShouldPostProcess`. 25 | We set the pre-processing priority to 0 so it is the first module called. 26 | We set the post-processing priority to 1 so it is also the last module called. 27 | ```cpp 28 | 29 | class LoggerModule : virtual public ziapi::IPreProcessorModule, public ziapi::IPostProcessorModule { 30 | public: 31 | void Init(const ziapi::config::Node &cfg) {} 32 | 33 | [[nodiscard]] ziapi::Version GetVersion() const noexcept override { return {1, 0}; } 34 | 35 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept override { return {1, 0}; } 36 | 37 | [[nodiscard]] const char *GetName() const noexcept override { return "LoggerModule"; } 38 | 39 | [[nodiscard]] const char *GetDescription() const noexcept override 40 | { 41 | return "Log all responses from HTTP requests"; 42 | } 43 | 44 | [[nodiscard]] double GetPostProcessorPriority() const noexcept override { return 1; } 45 | 46 | [[nodiscard]] bool ShouldPostProcess(const ziapi::http::Context &ctx, const ziapi::http::Request &req, const ziapi::http::Response &res) const override 47 | { 48 | return true; 49 | } 50 | 51 | [[nodiscard]] double GetPreProcessorPriority() const noexcept override { return 0; } 52 | 53 | [[nodiscard]] bool ShouldPreProcess(const ziapi::http::Context &ctx, const ziapi::http::Request &req) const override 54 | { 55 | return true; 56 | } 57 | 58 | // ... 59 | } 60 | ``` 61 | Then, first thing we want is to store the timestamp when the request is received so we can time it. We'll store that timestamp in the request context to fetch it later. 62 | 63 | ```cpp 64 | ... 65 | 66 | void PreProcess(ziapi::http::Context &ctx, ziapi::http::Request &req) override 67 | { 68 | ctx["timestamp"] = std::time(nullptr); 69 | // This example is deprecated. The `PostProcess` method has access to the request since version 5.0.0. 70 | ctx["target"] = req.target; 71 | ctx["method"] = req.method; 72 | } 73 | 74 | ... 75 | ``` 76 | And now in the post-process we can just simply access our variables and use them :smile: 77 | ```cpp 78 | ... 79 | 80 | void PostProcess(ziapi::http::Context &ctx, const ziapi::http::Request &, ziapi::http::Response &res) override 81 | { 82 | std::stringstream ss; 83 | 84 | // Example: ` [X] 404: Not found (GET /test, 2.02s)` 85 | ss << std::to_string(res.status_code) << ": " << res.reason << " (" << std::any_cast(ctx["method"]) 86 | << " " << std::any_cast(ctx["target"]) << ", " << std::setprecision(2) 87 | << difftime(std::time(nullptr), std::any_cast(ctx["timestamp"])) << "s)"; 88 | if (res.status_code < 300) { 89 | ziapi::Logger::Info(ss.str()); 90 | } else if (res.status_code < 400) { 91 | ziapi::Logger::Warning(ss.str()); 92 | } else { 93 | ziapi::Logger::Error(ss.str()); 94 | } 95 | } 96 | 97 | ... 98 | ``` 99 | You can check the full source code for this example [here](/examples/modules/logger/LoggerModule.hpp). 100 | -------------------------------------------------------------------------------- /docs/examples/REDIRECTION.md: -------------------------------------------------------------------------------- 1 | # Redirection Module Tutorial 2 | 3 | Let's implement a simple redirection module. 4 | 5 | ## Purpose 6 | 7 | This module will handle incoming http requests and send back a redirection response. 8 | For example we might want to redirect all requests to `google.com`. 9 | 10 | So for the following incoming request: 11 | ``` 12 | GET /google HTTP/1.1 13 | ``` 14 | 15 | We'll then get the following response: 16 | ``` 17 | HTTP/1.1 301 Moved Permanently 18 | Location: www.google.com 19 | ``` 20 | 21 | ## Tutorial 22 | 23 | First, let's implement the `IHandlerModule` interface base. 24 | 25 | ```c++ 26 | #include "ziapi/Module.hpp" 27 | 28 | class PhpCgiModule : public ziapi::IHandlerModule { 29 | public: 30 | void Init(const ziapi::config::Node &cfg) override {} 31 | 32 | [[nodiscard]] Version GetVersion() const noexcept { return {4, 0, 0}; } 33 | 34 | [[nodiscard]] Version GetCompatibleApiVersion() const noexcept { return {4, 0, 0}; } 35 | 36 | [[nodiscard]] const char *GetName() const noexcept { return "Redirection Module"; } 37 | 38 | [[nodiscard]] const char *GetDescription() const noexcept { return "Redirects the request to another location."; } 39 | 40 | [[nodiscard]] double GetHandlerPriority() const noexcept { 41 | // Directions should be treated in priority 42 | return 0.9; 43 | } 44 | 45 | void Handle(ziapi::http::Context &ctx, const ziapi::http::Request &req, ziapi::http::Response &res) override {} 46 | }; 47 | ``` 48 | 49 | Let's give our module a configuration. 50 | Let's load from the config the route to which we will redirect requests. We'll store the value in a member variable of our module called `redirection_route_`. 51 | 52 | ```c++ 53 | ... 54 | 55 | void Init(const Config &cfg) 56 | { 57 | /// We'll load from the configuration where to redirect to! 58 | redirection_route_ = cfg["modules"]["redirection"]["route"]; 59 | } 60 | 61 | ... 62 | ``` 63 | 64 | We want to redirect all requests so we just return `true` in our `ShouldHandle()`. 65 | 66 | ```cpp 67 | ... 68 | 69 | [[nodiscard]] bool ShouldHandle(const http::Context &, const http::Request &req) const 70 | { 71 | return true; 72 | } 73 | 74 | ... 75 | ``` 76 | 77 | Let's now implement the `Handle()` method. We simply redirect each request to the `redirection_route_` by changing the `Location` header on the response. We also set the appropriate status code. 78 | 79 | ```c++ 80 | ... 81 | 82 | void Handle(http::Context &, const http::Request &, http::Response &res) 83 | { 84 | res.headers[ziapi::http::header::kLocation] = redirection_route_; 85 | res.status_code = ziapi::http::code::kMovedPermanently; 86 | } 87 | 88 | ... 89 | ``` 90 | 91 | You can check the full source code for this example [here](/examples/modules/redirection/Module.hpp). 92 | -------------------------------------------------------------------------------- /docs/general/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to ZIAPI 2 | 3 | ## Issues 4 | 5 | Issues are more than welcome. When submitting an issue make you sure to select the right issue template for your need. 6 | 7 | ### Bug Reports 8 | 9 | Submit a bug report when an implemented feature isn't working properly (e.g. the CMake intergation refuses to build). 10 | 11 | ### Feature Requests 12 | 13 | If you feel like something is missing from the ZIAPI, like a new feature or a tweak to an existing one, submit a feature request describing your feature and some justification on why it should be implemented. 14 | 15 | ## Pull Requests 16 | 17 | Pull requests are more than welcome. Usually it's best to start by opening an issue and to follow it up with a pull request once everyone agrees on how to fix the bug / implement the feature. 18 | 19 | ## Conventions 20 | 21 | **Git Commits** 22 | 23 | We use the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) specification for git commits. 24 | 25 | **Git Branches** 26 | 27 | We use the following syntax for naming git branches 28 | 29 | ``` 30 | /- 31 | 32 | feat/billing-module-76 33 | fix/http-header-parser-167 34 | fix/module-loader-12 35 | ``` 36 | 37 | Some branches like `main`, `dev`, `hotfix` do not follow that convention. 38 | 39 | **C++** 40 | 41 | We use the [Google C++ style guide](https://google.github.io/styleguide/cppguide.html). 42 | 43 | **Issues and Pull Requests** 44 | 45 | Make sure your pull request and issue names are human readable titles such as `Missing const qualifier on method` for an issue or `Support for HTTP 3` for a pull request. 46 | -------------------------------------------------------------------------------- /docs/general/GETTING_STARTED.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Welcome to the main documentation page of the **ZIAPI**. The **ZIAPI** is a set of interfaces and concrete implementations to help you manage module implementation for the Zia project. 4 | 5 | ## Introduction 6 | 7 | **ZIAPI** aims at making your life easier as a developper. It features different type of modules that you can implement to customize parts of your Zia project. 8 | 9 | Here is an overview of what you can do using the **ZIAPI** module interfaces: 10 | - Switch between HTTP versions 1.0, 1.1, 2 and 3 11 | - Change the underlying network implementation (UDP, TCP, Quic, ...) 12 | - Change the behaviour of your HTTP server (Directory Listing, PHP CGI) 13 | - Manage multiple sites and conditional module execution (e.g. only execute a module on a request if it is a `GET` request) 14 | 15 | ## Architecture 16 | 17 | To understand how the ZiAPI works, lets take a look at how requests are handled from start to finish. 18 | 19 | ### Request handling process 20 | 21 | The request / response cycle is handled in 5 steps. Before we go into detail about how each of the steps work let's look at a quick illustration to help us understand better. 22 | 23 | ![Request / Response flow](/docs/assets/request-response-diagram.png) 24 | 25 | Okay now, let's study each step in detail. 26 | 27 | #### Step 1. Receive 28 | 29 | Like any HTTP message, the request is first handled through the network layer (TCP sockets, etc...). This is where you'll setup your sockets and listeners, your hypothetical SSL / TLS encryption logic, your HTTP parser and any additional serialization / deserialization code. When a request arrives, the networking layer must **read it, decrypt it, parse it** and forward it to the next layer: the pre-processing layer! 30 | 31 | Check the module associated with this step [here](../guides/IMPLEMENT_NETWORK.md) 32 | 33 | #### Step 2. Pre-process 34 | 35 | Okay, so our request has been received and parsed by the network layer. Once it lands in the pre-processing layer, a pipeline of request pre-processing modules will be invoked. This is where you can route the request, log it, rewrite its URL, add / remove some headers, etc. 36 | 37 | Check the module associated with this step [here](../guides/IMPLEMENT_PREPROCESSOR.md) 38 | 39 | #### Step 3. Handle 40 | 41 | Once the whole pre-processing pipeline has been applied on the request, the corresponding handler for this request is invoked. A handler will generate the HTTP response for a request. So that might be the contents of a file (Directory Listing module), the output of a PHP script (PHP CGI) or some JSON output gathered from a database, you choose! 42 | 43 | Check the module associated with this step [here](../guides/IMPLEMENT_HANDLER.md) 44 | 45 | #### Step 4. Post-process 46 | 47 | Once the HTTP response has been generated by the handling layer, it is forwarded to the post-processing layer. This is where a pipeline of response post-processing modules will be invoked. This is where you can serialize the contents of your response body, log the response, add additional headers such as CORS, etc. 48 | 49 | Check the module associated with this step [here](../guides/IMPLEMENT_POSTPROCESSOR.md) 50 | 51 | #### Step 5. Send 52 | 53 | Once the post-processing pipeline has been applied on the response, it is forwarded back to the network layer to be sent back over the network. This is where you will serialize your HTTP message, and possibly encode it using TLS before writing it to your socket. 54 | 55 | Check the module associated with this step [here](../guides/IMPLEMENT_NETWORK.md) 56 | 57 | And boom! Request handled! Using 5 easy steps: **Receive**, **pre-process**, **handle**, **post process** and **send**. 58 | 59 | For more information checkout the [documentation entrypoint](../README.md). 60 | -------------------------------------------------------------------------------- /docs/general/MODULES.md: -------------------------------------------------------------------------------- 1 | # Modules Documentation 2 | 3 | The ZIAPI features 4 different types of modules: 4 | - `INetworkModule` 5 | - `IPreProcessorModule` 6 | - `IHandlerModule` 7 | - `IPostProcessorModule` 8 | 9 | Each module is invoked at a different stage in the request lifecycle. If you remember from the [Getting started](GETTING_STARTED.md) page of the documentation, a request goes through 5 different steps when received by our HTTP server. Let's see which steps correspond to which module. 10 | - **1. Receive** step is handled by `INetworkModule`. 11 | - **2. Pre-process** step is handled by `IPreProcessorModule`. 12 | - **3. Handle** step is handled by `IHandlerModule`. 13 | - **4. Post-process** step is handled by `IPostProcessorModule`. 14 | - **5. Send** step is handled by `INetworkModule`. 15 | 16 | We'll see each of these modules types detail, but first, let's look at the `IModule` interface. 17 | 18 | ## The `IModule` interface 19 | 20 | Each **ZIAPI** module type inherits from `IModule`. It features very basic methods to make it easier to manage modules. 21 | 22 | ```c++ 23 | virtual void Init(const Config &cfg) = 0; 24 | 25 | [[nodiscard]] virtual Version GetVersion() const noexcept = 0; 26 | 27 | [[nodiscard]] virtual Version GetCompatibleApiVersion() const noexcept = 0; 28 | 29 | [[nodiscard]] virtual const char *GetName() const noexcept = 0; 30 | 31 | [[nodiscard]] virtual const char *GetDescription() const noexcept = 0; 32 | ``` 33 | 34 | There's not much to say about this interface. Every module must implement it so we can have access to its version, name, description... 35 | 36 | ## `INetworkModule` 37 | 38 | The `Run()` method starts the module, providing it with an output queue in which it shall push incoming requests and an input queue from which it should receive incoming responses and send them over the network. So basically, inside the run method, you should create your sockets, spin up your http parser and wait for incoming requests! This method is intended to run forever! 39 | 40 | ```c++ 41 | virtual void Run(http::IRequestOutputQueue &requests, http::IResponseInputQueue &responses); 42 | ``` 43 | 44 | The `Terminate()` method will be invoked upon reloading or termination of the server, it notifies the module that it needs to stops running altogether and release every resource it has created. So this is the time where you should **close** all your sockets and return the flow of control to the caller of your `Run` method. 45 | 46 | ```c++ 47 | virtual void Terminate() = 0; 48 | ``` 49 | 50 | ## `IPreProcessorModule` 51 | 52 | The `PreProcess()` is the method invoked on a request before the handler is called. You can use pre-processor to modify the request's data (add / remove headers, etc...) 53 | 54 | ```c++ 55 | virtual void PreProcess(http::Context &ctx, http::Request &req) = 0; 56 | ``` 57 | 58 | The `GetPreProcessorPriority()` method returns the priority of the module between zero and one. Pre-processors with a higher priority are executed first! 59 | 60 | ```c++ 61 | [[nodiscard]] virtual double GetPreProcessorPriority() const noexcept = 0; 62 | ``` 63 | 64 | The `ShouldPreProcess()` method returns `true` if this module's PreProcess method should be called on the request. For example you can choose that your module will only pre-process requests with a `Content-Type: application/json` header. 65 | 66 | ```c++ 67 | [[nodiscard]] virtual bool ShouldPreProcess(const http::Context &ctx, const http::Request &req) const = 0; 68 | ``` 69 | 70 | ## `IPostProcesserModule` 71 | 72 | The `PostProcess()` is the method invoked on a response after the handler is called. You can use post-processors to modify the response after it is generated (serializing the body, logging the response, etc...). 73 | 74 | ```c++ 75 | virtual void PostProcess(http::Context &ctx, const http::Request &, http::Response &res) = 0; 76 | ``` 77 | 78 | The `GetPostProcessorPriority()` method returns the priority of the module between zero and one. Post-processors with a higher priority are executed first! 79 | 80 | ```c++ 81 | [[nodiscard]] virtual double GetPostProcessorPriority() const noexcept = 0; 82 | ``` 83 | 84 | The `ShouldPostProcess()` method returns `true` if this module's PostProcess should be called on the response. For example you can choose that your module will only post-process responses with a status code over `399` to log them to the console! 85 | 86 | ```c++ 87 | [[nodiscard]] virtual bool ShouldPostProcess(const http::Context &ctx, const http::Request &req, const http::Response &res) const = 0; 88 | ``` 89 | 90 | ## `IHandlerModule` 91 | 92 | The `Handle()` method is invoked on a request in order to generate a response. It's like any typical web framework you can find: you get access to the incoming request and you're in charge of generating a response! 93 | 94 | ```c++ 95 | virtual void Handle(http::Context &ctx, const http::Request &req, http::Response &res) = 0; 96 | ``` 97 | 98 | The `GetHandlerPriority()` sets the module's priority of execution. If you have two handlers in your API which both want to handle a particular request, only the handler with the highest priority will be called. 99 | 100 | ```c++ 101 | [[nodiscard]] virtual double GetHandlerPriority() const noexcept = 0; 102 | ``` 103 | 104 | The `ShouldHandle()` is used to determin if your module **wants** to handle a specific request. For example, you may have a module which only handles `GET` requests, or requests with a specific header, etc... 105 | 106 | ```c++ 107 | [[nodiscard]] virtual bool ShouldHandle(const http::Context &ctx, const http::Request &req) const = 0; 108 | ``` 109 | -------------------------------------------------------------------------------- /docs/general/STANDARDS.md: -------------------------------------------------------------------------------- 1 | # ZIAPI Standards 2 | 3 | A set of rules that define a standard implementation of a ZIAPI module to make it easier to share modules between groups. 4 | 5 | The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in [RFC 2119](https://datatracker.ietf.org/doc/html/rfc2119). 6 | 7 | ## `Z0` - Module Documentation 8 | 9 | Each module **MUST** be shipped with a `README.md` providing basic documentation for the module. 10 | 11 | ## `Z1` - Module Configuration Documentation 12 | 13 | ZIAPI doesn't impose a norm for configuration files. However modules **MUST** be shipped with sufficient documentation regarding the configuration fields they expect. 14 | 15 | #### Example 16 | 17 | If the module `tls` depends on the following configuration: 18 | 19 | ```c++ 20 | void Init(const ziapi::config::Node &cfg) override { 21 | auto enable_tls = cfg["modules"]["tls"]["enableTls"].AsBool(); 22 | auto certificate_path = cfg["modules"]["tls"]["certficiatePath"].AsString(); 23 | } 24 | ``` 25 | 26 | Then the `tls` module's documentation should explicitly state that the module expects: 27 | - `modules.tls.enableTls` to be defined as a boolean 28 | - `modules.tls.certificatePath` to be defined as a string 29 | 30 | ## `Z2` - Configuration File Layout 31 | 32 | To limit friction when sharing modules, all groups **MAY** implement the following configuration file layout where: 33 | - The top level node is a `ziapi::config::Dict` 34 | - It contains a `modules` key which is a `ziapi::config::Dict` too. 35 | - Each module which requires a configuration fetches its configuration information from `modules.*` 36 | 37 | #### Example 38 | 39 | To illustrate this, let's look at a YAML configuration file example. 40 | 41 | Let's say we have two modules respectively named `tls` and `directoryListing`. By applying rule Z2 we get the following configuration. 42 | 43 | ```yaml 44 | modules: 45 | tls: 46 | foo: bar 47 | directoryListing: 48 | foo: bar 49 | ``` 50 | 51 | Therefore it is **NOT RECOMMENDED** to depend on configuration fields outside of the `modules.` scope like so: 52 | 53 | ```c++ 54 | void MyModule::Init(const ziapi::config::Node &cfg) override { 55 | /// Not C2 compliant, the module depends on a node outside of "modules.". 56 | auto enable_tls = cfg["enableTls"].AsBool(); 57 | auto certificate_path = cfg["certficiatePath"].AsString(); 58 | } 59 | ``` 60 | 61 | ## `Z3` - Request/Response Context Documentation 62 | 63 | ZIAPI doesn't impose a norm for the request/response context. However, modules **MUST** be shipped with sufficient documentation regarding the context fields they write to and read from. 64 | 65 | Modules **SHOULD** try to populate the context with primitive types only or basic `std` types (Like `std::string`). 66 | 67 | #### Example 68 | 69 | If the `MyPreProcessor` module depends on the following context: 70 | 71 | ```c++ 72 | void MyPreProcessor::PreProcess(http::Context &ctx, http::Request &req) 73 | { 74 | auto client_address = std::any_cast(ctx["client.socket.address"]); 75 | ctx["php_cgi.user.is_authenticated"] = (client_address == "127.0.0.1"); 76 | } 77 | ``` 78 | 79 | Then the `MyPreProcessor` module's documentation must explicitly state that it expects: 80 | - `ctx["client.socket.address"]` to be defined as a `std::string` 81 | 82 | The documentation must also state that the module plans to: 83 | - Write a boolean value inside `ctx["user.is_authenticated"]` 84 | 85 | ## `Z4` - Standard Request/Response Context Fields 86 | 87 | Each request/response is associated with a `ziapi::http::Context` to allow modules to communicate data. 88 | 89 | The following fields are standard fields and **SHOULD** be populated according to the following specification. 90 | 91 | | Key | Type | Description | 92 | |-------------------------|-----------------|----------------------------------------------------------------------------| 93 | | `client.socket.address` | `std::string` | The IP address of the client. May only be mutated by `INetworkModule`s | 94 | | `client.socket.port` | `std::uint16_t` | The port of the client's socket. May only be mutated by `INetworkModule`s | 95 | -------------------------------------------------------------------------------- /docs/general/WHY_US.md: -------------------------------------------------------------------------------- 1 | # Why choose ZIAPI 2 | 3 | ## Modules 4 | 5 | First of all **ZIAPI** offers a great deal of simplicity. Modules are separated into clearly defined groups (**network**, **pre-processor**, **post-processor**, **network**) and feature a wide variety of use cases. 6 | 7 | Our best selling point is the long list of **example modules** that we have already implemented to help you understand our API better. 8 | 9 | Here is a list of non-hexaustive list of modules you can very easily implement using **ZIAPI**. 10 | - [Directory Listing](../examples/DIRECTORY_LISTING.md) 11 | - Authentication 12 | - Automatic MIME type detection 13 | - Routing 14 | - [Redirection](../examples/REDIRECTION.md) 15 | - Secure connection module 16 | - PHP CGI 17 | - SSL / TLS 18 | - HTTP Handler 19 | - Database 20 | - Benchmark 21 | - Dashboard (frequency of requests, CPU load) 22 | - [Logger](../examples/LOGGER.md) 23 | - Compression / decompression 24 | 25 | ## Documentation 26 | 27 | We have documented each and every one of our source files and added additional info on how to 28 | - [Build and compile the project](../guides/INSTALL_AND_BUILD.md) 29 | - Create modules (Numerous [code examples](../examples) of different common module and one [in-depth guide per module type](../guides)) 30 | - [Contributing](CONTRIBUTING.md) 31 | 32 | ## Additional tools 33 | 34 | We've built an amazing [website](https://ziapi.vercel.app) for you to navigate the docs more easily. 35 | 36 | With the API, we also included our own Open-Source and unit-tested [dynamic library loader](https://github.com/martin-olivier/dylib) developped by [Martin Olivier](https://github.com/martin-olivier). 37 | -------------------------------------------------------------------------------- /docs/guides/CONFIG.md: -------------------------------------------------------------------------------- 1 | # Managing your configuration 2 | 3 | In this guide we will learn how to use our configuration, to setup all of your modules easily. 4 | 5 | ## The `Node` object 6 | 7 | First, let's take a look at the definition of `ziapi::config::Node`. 8 | 9 | ```cpp 10 | using NodeVariant = std::variant; 11 | 12 | struct Node : public NodeVariant { /* ... */ } 13 | ``` 14 | 15 | So here we can see that our `struct Node` inherits from a `std::variant` of a lot of types. 16 | The `std::variant` declines all the possible data types available in JSON, YAML, XML, etc. 17 | 18 | Let's look at a simple example: 19 | 20 | Imagine you have a configuration file of the following form: 21 | 22 | ```json 23 | { 24 | "sources": ["/bin/ls", 10, null, 10.5], 25 | "modules": { 26 | "directoryListing": { 27 | "root": "/var/www" 28 | } 29 | } 30 | } 31 | ``` 32 | 33 | Or its YAML equivalent 34 | 35 | ```yaml 36 | sources: 37 | - "/bin/ls" 38 | - 10 39 | - null 40 | - 10.5 41 | modules: 42 | directoryListing: 43 | root: /var/www 44 | ``` 45 | 46 | You could represent it using a `ziapi::config::Node` like so 47 | 48 | ```cpp 49 | using Node = ziapi::config::Node; 50 | using Dict = ziapi::config::Dict; 51 | using Array = ziapi::config::Array; 52 | 53 | Node obj( 54 | { 55 | {"sources", std::make_shared({ 56 | std::make_shared("/bin/ls"), 57 | std::make_shared(10), 58 | std::make_shared(nullptr), 59 | std::make_shared(10.5) 60 | }) 61 | }, 62 | {"modules", std::make_shared({ 63 | {"directoryListing", std::make_shared({ 64 | {"root", std::make_shared("/var/www")} 65 | })} 66 | })} 67 | } 68 | ); 69 | 70 | // Or using helper static methods 71 | 72 | auto obj = Node::MakeDict({ 73 | {"sources", Node::MakeArray({ "/bin/ls", 10, nullptr, 10.5 })}, 74 | {"modules", Node::MakeDict({ 75 | {"directoryListing", Node::MakeDict({ 76 | {"root", "/var/www"} 77 | })} 78 | })} 79 | }) 80 | ``` 81 | ### More informations about Dict and Array helper functions 82 | ```cpp 83 | // You can also give the helper methods a vector representing your array or dictionnary 84 | // The helper functions will automatically transform your contained Nodes into shared_ptr 85 | 86 | std::unordered_map dictionnary_vector; 87 | // ... Fill the dictionnary vector 88 | Node dictionnary_node = Node::MakeDict(dictionnary_vector) 89 | 90 | std::vector array_vector; 91 | // ... Fill the array vector 92 | Node array_node = Node::MakeArray(array_vector) 93 | 94 | 95 | // You can of course build them from the Node base types 96 | // In this case, you're responsible of giving the values as shared_ptr and converting the final variable as a Node 97 | 98 | Dict dictionnary; 99 | dictionnary["modules_count"] = std::make_shared(10); 100 | Node dictionnary_node(dictionnary); 101 | 102 | Array arr; 103 | arr.emplace_back(std::make_shared("/bin/ls")); 104 | Node array_node(arr); 105 | ``` 106 | 107 | ## Data Types 108 | 109 | ### `Undefined` 110 | 111 | The `Undefined` data type represents an empty configuration, much like an empty file. 112 | 113 | ```cpp 114 | ziapi::config::Node(ziapi::config::Undefined{}) 115 | ``` 116 | 117 | ### `Null` 118 | 119 | The `Null` data type represents a boolean value. So the following JSON: 120 | 121 | ```json 122 | null 123 | ``` 124 | 125 | Would translate to 126 | 127 | ```cpp 128 | ziapi::config::Node(nullptr) 129 | ``` 130 | 131 | ### `bool` 132 | 133 | The `bool` data type represents a boolean value. So the following JSON: 134 | 135 | ```json 136 | true 137 | ``` 138 | 139 | Would translate to 140 | 141 | ```cpp 142 | ziapi::config::Node(true) 143 | ``` 144 | 145 | ### `int` 146 | 147 | The `int` data type represents an integer value. So the following JSON: 148 | 149 | ```json 150 | 5 151 | ``` 152 | 153 | Would translate to 154 | 155 | ```cpp 156 | ziapi::config::Node(5) 157 | ``` 158 | 159 | ### `double` 160 | 161 | The `double` data type represents a floating point value. So the following JSON: 162 | 163 | ```json 164 | 4.2 165 | ``` 166 | 167 | Would translate to 168 | 169 | ```cpp 170 | ziapi::config::Node(4.2) 171 | ``` 172 | 173 | ### `String` 174 | 175 | The `String` data type represents a string value. So the following JSON: 176 | 177 | ```json 178 | "Hello World!" 179 | ``` 180 | 181 | Would translate to 182 | 183 | ```cpp 184 | ziapi::config::Node("Hello World!") 185 | ``` 186 | 187 | 188 | ### `Array` 189 | 190 | The `Array` data type represents an array of values. So the following JSON: 191 | 192 | ```json 193 | ["Hello", "World", "!"] 194 | ``` 195 | 196 | Would translate to 197 | 198 | ```cpp 199 | ziapi::config::Array arr({ 200 | std::make_shared("Hello"), 201 | std::make_shared("World"), 202 | std::make_shared("!") 203 | }); 204 | 205 | ziapi::config::Node arr_node(arr); 206 | ``` 207 | 208 | Which is equivalent to 209 | 210 | ```c++ 211 | auto array_node = ziapi::config::Node::MakeArray({ "Hello", "World", "!" }); 212 | // vector can also be passed as a parameter 213 | ``` 214 | 215 | ### `Dict` 216 | 217 | The `Dict` data type represents a dictionary of values. So the following JSON: 218 | 219 | ```json 220 | { 221 | "age": 20, 222 | "first_name": "Charlie", 223 | "last_name": "Chou", 224 | "is_sexy": true 225 | } 226 | ``` 227 | 228 | Would translate to 229 | 230 | ```cpp 231 | ziapi::config::Dict dict({ 232 | {"age", std::make_shared(20)}, 233 | {"first_name", std::make_shared("Charlie")}, 234 | {"last_name", std::make_shared("Chou")}, 235 | {"is_sexy", std::make_shared(true)} 236 | }) 237 | 238 | ziapi::config::Node node_dict(dict); 239 | ``` 240 | 241 | Which is equivalent to 242 | 243 | ```c++ 244 | auto node_dict = ziapi::config::Node::MakeDict({ 245 | {"age", 20}, 246 | {"first_name", "Charlie"}, 247 | {"last_name", "Chou"}, 248 | {"is_sexy", true}, 249 | }) // vector can also be passed as a parameter 250 | ``` -------------------------------------------------------------------------------- /docs/guides/IMPLEMENT_HANDLER.md: -------------------------------------------------------------------------------- 1 | # Implement a Handler module 2 | 3 | In this guide we will learn how to implement a Handler module which is a type of module that allows us to generate an HTTP response from a request. 4 | 5 | ## `IHandlerModule` interface 6 | 7 | Let's look at the definition of the `IHandlerModule` interface. 8 | 9 | ```c++ 10 | class IHandlerModule : public IModule { 11 | public: 12 | virtual void Handle(http::Request &req, http::Response &res) = 0; 13 | 14 | [[nodiscard]] virtual double GetHandlerPriority() const noexcept = 0; 15 | 16 | [[nodiscard]] virtual bool ShouldHandle(const http::Context &ctx, const http::Request &req) const = 0; 17 | }; 18 | ``` 19 | 20 | We have three methods to implement, let's go! 21 | 22 | > :exclamation: Don't forget to implement the `IModule` methods 23 | 24 | ## Create a handler 25 | 26 | Okay, let's create our own class which inherits from `IHandlerModule`. 27 | 28 | ```c++ 29 | #include "ziapi/Module.hpp" 30 | 31 | class MyHandler : public ziapi::IHandlerModule { 32 | public: 33 | void Handle(http::Context &ctx, const http::Request &req, http::Response &res) override; 34 | 35 | [[nodiscard]] double GetHandlerPriority() const noexcept override; 36 | 37 | [[nodiscard]] bool ShouldHandle(const http::Context &ctx, const http::Request &req) const override; 38 | }; 39 | ``` 40 | 41 | Then, let's implement the `GetHandlerPriority()`. Our module doesn't have specific priority requirements so we'll put it at `0.5f`. 42 | 43 | ```c++ 44 | [[nodiscard]] double MyHandler::GetHandlerPriority() const noexcept 45 | { 46 | return 0.5f; 47 | } 48 | ``` 49 | 50 | Then, let's implement the `ShouldHandle()`. This method is invoked to know if our handler should be called for a specific request. We can return `true` if we want all requests to go through this handler but let's just say our handler only handles `GET` request for the sake of the example. 51 | 52 | ```c++ 53 | [[nodiscard]] bool MyHandler::ShouldHandle(const http::Context &ctx, const http::Request &req) const 54 | { 55 | return req.method == http::method::kGet; 56 | } 57 | ``` 58 | 59 | Great! Now our handler will be called on all GET requests! Now let's add the `Handle()`. 60 | 61 | ```c++ 62 | void MyHandler::Handle(http::Context &ctx, const http::Request &req, http::Response &res) 63 | { 64 | res.status_code = 200; 65 | res.body = "Hello guys!"; 66 | } 67 | ``` 68 | 69 | And that's it! You now have a module which displays `"Hello guys!"` in your browser upon all `GET` requests. 70 | -------------------------------------------------------------------------------- /docs/guides/IMPLEMENT_NETWORK.md: -------------------------------------------------------------------------------- 1 | # Implement a Network module 2 | 3 | In this guide we will learn how to implement a Network module which is a type of module that allows us to handle incoming requests and send back responses. 4 | 5 | ## `INetworkModule` interface 6 | 7 | Let's look at the definition of the `INetworkModule` interface. 8 | 9 | ```c++ 10 | class INetworkModule { 11 | public: 12 | virtual void Run(http::IRequestOutputQueue &requests, http::IResponseInputQueue &responses) = 0; 13 | 14 | virtual void Terminate() = 0; 15 | }; 16 | ``` 17 | 18 | We have two methods to implement, let's go! 19 | 20 | ## Create a network module 21 | 22 | Okay, let's create our own class which inherits from `INetworkModule`. 23 | 24 | ```c++ 25 | #include "ziapi/Module.hpp" 26 | 27 | class MyNetwork : public ziapi::INetworkModule { 28 | public: 29 | void Run(http::IRequestOutputQueue &requests, http::IResponseInputQueue &responses) override; 30 | 31 | void Terminate() override; 32 | 33 | private: 34 | Array clients; 35 | bool mustStop = false; // do a constructor, it's pseudo-code 36 | }; 37 | ``` 38 | 39 | First, let's implement the `Run()` method. It should contain the main loop on all the clients and handle incoming data, and / or send back responses back. It's pseudo-code, just to understand the purpose of the method. 40 | 41 | ```c++ 42 | void MyNetwork::Run(http::IRequestOutputQueue &requests, http::IResponseInputQueue &responses) 43 | { 44 | Socket mainSocket = CreateSocket(); 45 | 46 | while (true) { 47 | // Check for a new client 48 | if (mainSocket.HasNewConnection()) { 49 | clients.append(mainSocket.Accept()); 50 | } 51 | if (mustStop) { 52 | mainSocket.destroy(); 53 | return; 54 | } 55 | // For each client, read incoming data 56 | for (int i = 0; i < clients.Size(); ++i) { 57 | if (clients[i].HasData()) { 58 | requests.Push(clients[i].GetData()); 59 | } 60 | } 61 | // For each response, send data 62 | for (int i = 0; i < responses.Size(); ++i) { 63 | auto response = responses.Pop(); 64 | 65 | // send the response to the right client 66 | } 67 | sleep(1); 68 | } 69 | } 70 | ``` 71 | 72 | To end, let's implement `Terminate`. It should stop the running loop and may be useful in order to join threads. 73 | 74 | ```c++ 75 | void MyNetwork::Terminate() 76 | { 77 | shouldStop = true; 78 | // join threads if there is 79 | } 80 | ``` 81 | 82 | The network module is now complete ! 83 | -------------------------------------------------------------------------------- /docs/guides/IMPLEMENT_POSTPROCESSOR.md: -------------------------------------------------------------------------------- 1 | # Implement a PostProcessor module 2 | 3 | In this guide we will learn how to implement a PostProcessor module which is a type of module that allows us to modify an HTTP response after it was handled. 4 | 5 | ## `IPostProcessorModule` interface 6 | 7 | Let's look at the definition of the `IPostProcessorModule` interface. 8 | 9 | ```c++ 10 | class IPostProcessorModule : public IModule { 11 | public: 12 | virtual void PostProcess(http::Context &ctx, http::Response &res) = 0; 13 | 14 | [[nodiscard]] virtual double GetPostProcessorPriority() const noexcept = 0; 15 | 16 | [[nodiscard]] virtual bool ShouldPostProcess(const http::Context &ctx, const http::Request &req, const http::Response &res) const = 0; 17 | }; 18 | ``` 19 | 20 | We have three methods to implement, let's go! 21 | 22 | > :exclamation: Don't forget to implement the `IModule` methods 23 | 24 | ## Create a post-processor 25 | 26 | Okay, let's create our own class which inherits from `IPostProcessorModule`. 27 | 28 | ```c++ 29 | #include "ziapi/Module.hpp" 30 | 31 | class MyPostProcessor : public ziapi::IPostProcessorModule { 32 | public: 33 | void PostProcess(http::Context &ctx, http::Response &res) override; 34 | 35 | [[nodiscard]] double GetPostProcessorPriority() const noexcept override; 36 | 37 | [[nodiscard]] bool ShouldPostProcess(const http::Context &ctx, const http::Request &req, const http::Response &res) const override; 38 | }; 39 | ``` 40 | 41 | Then, let's implement the `GetPostProcessorPriority()`. Our module doesn't have specific priority requirements so we'll put it at `0.5f`. 42 | 43 | ```c++ 44 | [[nodiscard]] double MyPostProcessor::GetPostProcessorPriority() const noexcept 45 | { 46 | return 0.5f; 47 | } 48 | ``` 49 | 50 | Then, let's implement the `ShouldPostProcess()`. This method is invoked to know if our post-processor should be called for a specific request. We can return `true` if we want all requests to go through this post-processor but let's just say our post-processor only handles `status code` inferior to 400 for the sake of the example. 51 | 52 | ```c++ 53 | [[nodiscard]] bool ShouldPostProcess(const http::Context &ctx, const http::Request &req, const http::Response &res) const 54 | { 55 | return res.status_code < 400; 56 | } 57 | ``` 58 | 59 | Great! Now our post-processor will be called on all responses with a status code lower than `400`! Now let's add the `PostProcess()`. 60 | 61 | ```c++ 62 | void MyPostProcessor::PostProcess(http::Context &ctx, http::Response &res) 63 | { 64 | std::cout << "New " << res.status_code << " response!" << std::endl; 65 | } 66 | ``` 67 | 68 | And that's it! You now have a module which logs all responses with a status code < `400`. 69 | -------------------------------------------------------------------------------- /docs/guides/IMPLEMENT_PREPROCESSOR.md: -------------------------------------------------------------------------------- 1 | # Implement a PreProcessor module 2 | 3 | In this guide we will learn how to implement a PreProcessor module which is a type of module that allows us to modify an HTTP request before it is handled. 4 | 5 | ## `IPreProcessorModule` interface 6 | 7 | Let's look at the definition of the `IPreProcessorModule` interface. 8 | 9 | ```c++ 10 | class IPreProcessorModule : public IModule { 11 | public: 12 | virtual void PreProcess(http::Context &ctx, http::Request &req) = 0; 13 | 14 | [[nodiscard]] virtual double GetPreProcessorPriority() const noexcept = 0; 15 | 16 | [[nodiscard]] virtual bool ShouldPreProcess(const http::Context &ctx, const http::Request &req) const = 0; 17 | }; 18 | ``` 19 | 20 | We have three methods to implement, let's go! 21 | 22 | > :exclamation: Don't forget to implement the `IModule` methods 23 | 24 | ## Create a pre-processor 25 | 26 | Okay, let's create our own class which inherits from `IPreProcessorModule`. 27 | 28 | ```c++ 29 | #include "ziapi/Module.hpp" 30 | 31 | class MyPreProcessor : public ziapi::IPreProcessorModule { 32 | public: 33 | void PreProcess(http::Context &ctx, http::Request &req) override; 34 | 35 | [[nodiscard]] double GetPreProcessorPriority() const noexcept override; 36 | 37 | [[nodiscard]] bool ShouldPreProcess(const http::Context &ctx, const http::Request &req) const override; 38 | }; 39 | ``` 40 | 41 | Then, let's implement the `GetPreProcessorPriority()`. Our module doesn't have specific priority requirements so we'll put it at `0.5f`. 42 | 43 | ```c++ 44 | [[nodiscard]] double MyPreProcessor::GetPreProcessorPriority() const noexcept 45 | { 46 | return 0.5f; 47 | } 48 | ``` 49 | 50 | Then, let's implement the `ShouldPreProcess()`. This method is invoked to know if our pre-processor should be called for a specific request. We can return `true` if we want all requests to go through this pre-processor but let's just say our pre-processor only handles `GET` request for the sake of the example. 51 | 52 | ```c++ 53 | [[nodiscard]] bool ShouldPreProcess(const http::Context &ctx, const http::Request &req) const 54 | { 55 | return req.method == http::method::kGet; 56 | } 57 | ``` 58 | 59 | Great! Now our pre-processor will be called on all GET requests! Now let's add the `PreProcess()`. 60 | 61 | ```c++ 62 | void MyPreProcessor::PreProcess(http::Context &ctx, http::Request &req) 63 | { 64 | req.method = http::method::kPost; 65 | } 66 | ``` 67 | 68 | And that's it! You now have a module which transforms all `GET` requests to `POST` requests in order to completely break your API ^^. 69 | -------------------------------------------------------------------------------- /docs/guides/INSTALL_AND_BUILD.md: -------------------------------------------------------------------------------- 1 | # Build and install guide 2 | 3 | Let's take a look at how to build the project. 4 | 5 | ## Fetch ZiAPI using CMake 6 | 7 | Add the following content to your `CMakeLists` to fetch the `ZiAPI` and include its header files in your project: 8 | ```cmake 9 | include(ExternalProject) 10 | 11 | ExternalProject_Add( 12 | ziapi 13 | GIT_REPOSITORY https://github.com/martin-olivier/ZiAPI.git 14 | GIT_TAG v5.0.0 15 | INSTALL_COMMAND "" 16 | TEST_COMMAND "" 17 | ) 18 | 19 | add_dependencies(zia ziapi) 20 | ExternalProject_Get_Property(ziapi SOURCE_DIR) 21 | include_directories(${SOURCE_DIR}/include) 22 | ``` 23 | 24 | > :bulb: Don't forget to link with `libdl` on unix if you use `dylib`: 25 | ```cmake 26 | if(UNIX) 27 | target_link_libraries(zia PRIVATE dl) 28 | endif() 29 | ``` 30 | 31 | ## Build and run unit tests 32 | 33 | If you want to run unit tests on `ZiAPI`, do the following steps: 34 | - Clone the ZiAPI repository 35 | - Execute the following commands: 36 | ``` 37 | cmake . -B build -DUNIT_TESTS=ON 38 | cmake --build build 39 | ./unit_tests 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/guides/MODULES_101.md: -------------------------------------------------------------------------------- 1 | # Modules 101 2 | 3 | The goal of this guide is to build a module as a dynamic library and load it at runtime 4 | 5 | ## Documentation 6 | 7 | - [modules](https://github.com/martin-olivier/ZiAPI/blob/main/docs/general/MODULES.md) 8 | - [dylib](https://github.com/martin-olivier/dylib) 9 | 10 | ## Setup Build 11 | 12 | Here is the repo architecture of this example: 13 | ``` 14 | . 15 | ├── CMakeLists.txt 16 | └── src 17 | ├── main.cpp 18 | └── module.cpp 19 | ``` 20 | 21 | Let's create a `CMakeLists` to build our binary `zia` and our dynamic library `module`: 22 | 23 | ```cmake 24 | cmake_minimum_required(VERSION 3.17) 25 | 26 | # The name of the CMake project 27 | project(TestZia) 28 | 29 | # The C++ standard you want to use for your project 30 | set(CMAKE_CXX_STANDARD 17) 31 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 32 | 33 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}) 34 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_BINARY_DIR}) 35 | 36 | add_executable(zia src/main.cpp) 37 | 38 | # Fetch ZiAPI 39 | 40 | include(ExternalProject) 41 | 42 | ExternalProject_Add( 43 | ziapi 44 | GIT_REPOSITORY https://github.com/martin-olivier/ZiAPI.git 45 | GIT_TAG v5.0.0 46 | INSTALL_COMMAND "" 47 | TEST_COMMAND "" 48 | ) 49 | 50 | add_dependencies(zia ziapi) 51 | ExternalProject_Get_Property(ziapi SOURCE_DIR) 52 | include_directories(${SOURCE_DIR}/include) 53 | 54 | if(UNIX) 55 | target_link_libraries(zia PRIVATE dl) 56 | endif() 57 | 58 | # Build Our Dynamic Lib 59 | 60 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}) 61 | 62 | add_library(module SHARED src/module.cpp) 63 | 64 | # running the below CMake rule will allow you to remove the prefix `lib` for macOS and linux, ensuring 65 | # that the library shares the same name on all the different OS: 66 | # https://github.com/martin-olivier/dylib#remove-the-lib-prefix 67 | set_target_properties(module PROPERTIES PREFIX "") 68 | 69 | add_dependencies(module ziapi) 70 | ``` 71 | 72 | After that lets implement `module.cpp` that will be build into a dynamic lib: 73 | ```c++ 74 | #include "dylib/dylib.hpp" 75 | #include "ziapi/Module.hpp" 76 | 77 | class Module : public ziapi::IModule { 78 | public: 79 | Module() = default; 80 | 81 | ~Module() override = default; 82 | 83 | void Init(const ziapi::config::Node &) override {} 84 | 85 | ziapi::Version GetVersion() const noexcept override { return {4, 0, 0}; } 86 | 87 | ziapi::Version GetCompatibleApiVersion() const noexcept override { return {4, 0, 0}; } 88 | 89 | [[nodiscard]] virtual const char *GetName() const noexcept override { return "module_name"; } 90 | 91 | [[nodiscard]] virtual const char *GetDescription() const noexcept override 92 | { 93 | return "A module implementation example"; 94 | } 95 | }; 96 | 97 | DYLIB_API ziapi::IModule *LoadZiaModule() { return new Module; } 98 | ``` 99 | 100 | > :warning: The function that returns a new module from a dynamic library **MUST** have this prototype: 101 | ```c++ 102 | DYLIB_API ziapi::IModule *LoadZiaModule() 103 | ``` 104 | 105 | And then let's implement our `main.cpp` that will load the dynamic lib: 106 | 107 | ```c++ 108 | #include "ziapi/Logger.hpp" 109 | #include "ziapi/Module.hpp" 110 | #include "dylib/dylib.hpp" 111 | 112 | int main() 113 | { 114 | try { 115 | // Create a dynamic lib object that will load the module 116 | dylib lib("./module", dylib::extension); 117 | // Get the function that will generate our module when called 118 | auto entry_point_fn = lib.get_function("LoadZiaModule"); 119 | // Call the function to get a module instance 120 | std::unique_ptr mod(entry_point_fn()); 121 | // Print information about the module using the logger 122 | ziapi::Logger::Info("Module loaded: ", mod->GetName(), " - ", mod->GetDescription()); 123 | } 124 | catch (const dylib::exception &e) { 125 | // Catch exceptions around a dynamic lib (handle or symbol errors) and print them using the logger 126 | ziapi::Logger::Error(e.what()); 127 | } 128 | return 0; 129 | } 130 | ``` 131 | 132 | Let's run our binary: 133 | ```sh 134 | > ./zia 135 | Sun Jan 23 16:07:41 2022 [i] Module loaded: module_name - A module implementation example 136 | ``` 137 | -------------------------------------------------------------------------------- /examples/dylib/Module.cpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Module.hpp" 2 | 3 | #include "dylib/dylib.hpp" 4 | 5 | class Module : public ziapi::IModule { 6 | public: 7 | Module() = default; 8 | 9 | ~Module() override = default; 10 | 11 | void Init(const ziapi::config::Node &) override {} 12 | 13 | ziapi::Version GetVersion() const noexcept override { return {4, 0, 0}; } 14 | 15 | ziapi::Version GetCompatibleApiVersion() const noexcept override { return {4, 0, 0}; } 16 | 17 | [[nodiscard]] virtual const char *GetName() const noexcept override { return "module_name"; } 18 | 19 | [[nodiscard]] virtual const char *GetDescription() const noexcept override 20 | { 21 | return "A module implementation example"; 22 | } 23 | }; 24 | 25 | DYLIB_API ziapi::IModule *LoadZiaModule() { return new Module; } 26 | -------------------------------------------------------------------------------- /examples/modules/compressor/CompressorModule.hpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Module.hpp" 2 | 3 | class CompressorModule : public ziapi::IPostProcessorModule { 4 | public: 5 | void Init(const ziapi::config::Node &) override 6 | { 7 | // Don't need anything to configure in this implementation 8 | } 9 | 10 | [[nodiscard]] ziapi::Version GetVersion() const noexcept override { return ziapi::Version{4, 0, 0}; } 11 | 12 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept override { return ziapi::Version{4, 0, 0}; } 13 | 14 | [[nodiscard]] const char *GetName() const noexcept override { return "CompressorModule"; } 15 | 16 | [[nodiscard]] const char *GetDescription() const noexcept override 17 | { 18 | return "Compress the response body before sending it back to the network"; 19 | } 20 | 21 | void PostProcess(ziapi::http::Context &, const ziapi::http::Request &, ziapi::http::Response &res) override 22 | { 23 | res.body = CompressBody(res.body); 24 | } 25 | 26 | [[nodiscard]] double GetPostProcessorPriority() const noexcept override 27 | { 28 | // Compressor needs to be ran last, just before sending data back 29 | return 1.0f; 30 | } 31 | 32 | [[nodiscard]] bool ShouldPostProcess(const ziapi::http::Context &, const ziapi::http::Request &, 33 | const ziapi::http::Response &) const override 34 | { 35 | // Compressor will always be used as it's always useful 36 | return true; 37 | } 38 | 39 | private: 40 | std::string CompressBody(const std::string &body) 41 | { 42 | // Algorithm of the hell 43 | return body.substr(0, body.length() / 2); 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /examples/modules/decompressor/DecompressorModule.hpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Module.hpp" 2 | 3 | class DecompressorModule : public ziapi::IPreProcessorModule { 4 | public: 5 | ~DecompressorModule() = default; 6 | 7 | void Init(const ziapi::config::Node &) override 8 | { 9 | // Don't need anything to configure in this implementation 10 | } 11 | 12 | [[nodiscard]] ziapi::Version GetVersion() const noexcept override { return ziapi::Version{4, 0, 0}; } 13 | 14 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept override { return ziapi::Version{4, 0, 0}; } 15 | 16 | [[nodiscard]] const char *GetName() const noexcept override { return "DecompressorModule"; } 17 | 18 | [[nodiscard]] const char *GetDescription() const noexcept override 19 | { 20 | return "Decompress the response body before sending it to the module pipeline"; 21 | } 22 | 23 | void PreProcess(ziapi::http::Context &, ziapi::http::Request &req) override { req.body = DecompressBody(req.body); } 24 | 25 | [[nodiscard]] double GetPreProcessorPriority() const noexcept override 26 | { 27 | // Decompressor needs to be ran first, before any pre-processor. 28 | return 0.0f; 29 | } 30 | 31 | [[nodiscard]] bool ShouldPreProcess(const ziapi::http::Context &, const ziapi::http::Request &req) const override 32 | { 33 | // Only use if compressed header is set 34 | return req.headers.at("compressed") == std::string("true"); 35 | } 36 | 37 | private: 38 | std::string DecompressBody(const std::string &body) 39 | { 40 | // algorithm of the hell 41 | return std::string(body + std::string(" omg i am now decompressed thx algorithm")); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /examples/modules/directory-listing/DirectoryListingModule.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ziapi/Config.hpp" 5 | #include "ziapi/Module.hpp" 6 | 7 | class DirectoryListingModule : public ziapi::IHandlerModule { 8 | public: 9 | void Init(const ziapi::config::Node &cfg) override 10 | { 11 | /// In our config, we can specify which folder our module serves. 12 | /// We fetch the "modules.directoryListing.root" variable from the config 13 | /// as a string. 14 | root_ = cfg["modules"]["directoryListing"]["root"].AsString(); 15 | } 16 | 17 | [[nodiscard]] ziapi::Version GetVersion() const noexcept override { return {4, 0, 0}; } 18 | 19 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept override { return {4, 0, 0}; } 20 | 21 | [[nodiscard]] const char *GetName() const noexcept override { return "DirectoryListing"; } 22 | 23 | [[nodiscard]] const char *GetDescription() const noexcept override 24 | { 25 | return "Give access to a filesystem over HTTP"; 26 | } 27 | 28 | [[nodiscard]] double GetHandlerPriority() const noexcept override 29 | { 30 | /// Our module doesn't have any specific priority requirements. 31 | return 0.5f; 32 | } 33 | 34 | [[nodiscard]] bool ShouldHandle(const ziapi::http::Context &ctx, const ziapi::http::Request &req) const override 35 | { 36 | /// We only want to handle GET requests. 37 | return req.method == ziapi::http::method::kGet; 38 | } 39 | 40 | void Handle(ziapi::http::Context &ctx, const ziapi::http::Request &req, ziapi::http::Response &res) override 41 | { 42 | /// Here we concat the root path from our config (e.g /var/www/) with the target from our request (e.g. 43 | /// index.html) to get the full path of our file. 44 | auto filepath = std::filesystem::path(root_) / std::filesystem::path(req.target); 45 | std::error_code ec; 46 | 47 | /// If the file is a directory let's provide a list of all the files as a response. 48 | if (std::filesystem::is_directory(filepath, ec)) { 49 | std::ostringstream ss; 50 | for (const auto &entry : std::filesystem::directory_iterator(filepath)) { 51 | ss << entry.path().string() << "\n"; 52 | } 53 | return; 54 | } else { 55 | /// If the file is not a directory, we assume it's a regular file and we just send its contents with the 56 | /// response. 57 | std::ifstream file_stream(filepath.filename()); 58 | std::ostringstream ss; 59 | ss << file_stream.rdbuf(); 60 | res.body = ss.str(); 61 | return; 62 | } 63 | res.status_code = ziapi::http::Code::kNotFound; 64 | res.reason = ziapi::http::reason::kNotFound; 65 | } 66 | 67 | private: 68 | std::string root_; 69 | }; 70 | -------------------------------------------------------------------------------- /examples/modules/logger/LoggerModule.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "ziapi/Logger.hpp" 5 | #include "ziapi/Module.hpp" 6 | 7 | class LoggerModule : virtual public ziapi::IPreProcessorModule, public ziapi::IPostProcessorModule { 8 | public: 9 | void Init(const ziapi::config::Node &config) {} 10 | 11 | [[nodiscard]] ziapi::Version GetVersion() const noexcept override { return {4, 0, 0}; } 12 | 13 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept override { return {4, 0, 0}; } 14 | 15 | [[nodiscard]] const char *GetName() const noexcept override { return "LoggerModule"; } 16 | 17 | [[nodiscard]] const char *GetDescription() const noexcept override 18 | { 19 | return "Log all responses from HTTP requests"; 20 | } 21 | 22 | [[nodiscard]] double GetPostProcessorPriority() const noexcept override { return 1; } 23 | 24 | [[nodiscard]] bool ShouldPostProcess(const ziapi::http::Context &ctx, const ziapi::http::Request &req, 25 | const ziapi::http::Response &res) const override 26 | { 27 | return true; 28 | } 29 | 30 | [[nodiscard]] double GetPreProcessorPriority() const noexcept override { return 0; } 31 | 32 | [[nodiscard]] bool ShouldPreProcess(const ziapi::http::Context &ctx, const ziapi::http::Request &req) const override 33 | { 34 | return true; 35 | } 36 | 37 | void PostProcess(ziapi::http::Context &ctx, const ziapi::http::Request &, ziapi::http::Response &res) override 38 | { 39 | std::stringstream ss; 40 | 41 | // Exemple: ` [X] 404: Not found (GET /test, 2s)` 42 | ss << std::to_string((int)res.status_code) << ": " << res.reason << " (" 43 | << std::any_cast(ctx["method"]) << " " << std::any_cast(ctx["target"]) << ", " 44 | << std::setprecision(2) << difftime(std::time(nullptr), std::any_cast(ctx["timestamp"])) << "s)"; 45 | if ((int)res.status_code < 300) { 46 | ziapi::Logger::Info(ss.str()); 47 | } else if ((int)res.status_code < 400) { 48 | ziapi::Logger::Warning(ss.str()); 49 | } else { 50 | ziapi::Logger::Error(ss.str()); 51 | } 52 | } 53 | 54 | void PreProcess(ziapi::http::Context &ctx, ziapi::http::Request &req) override 55 | { 56 | ctx["timestamp"] = std::time(nullptr); 57 | /// This example is deprecrated. Storing the target and method is useless because the `PostProcess` method now 58 | /// has access to the request object. 59 | ctx["target"] = req.target; 60 | ctx["method"] = req.method; 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /examples/modules/redirection/Module.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "ziapi/Module.hpp" 8 | 9 | class RedirectionModule : public ziapi::IHandlerModule { 10 | public: 11 | void Init(const ziapi::config::Node &cfg) override 12 | { 13 | /// We'll load from the configuration where to redirect to! 14 | redirection_route_ = cfg["modules"]["redirection"]["route"]; 15 | } 16 | 17 | void Handle(ziapi::http::Context &ctx, const ziapi::http::Request &req, ziapi::http::Response &res) override 18 | { 19 | /// Now we just say that the resource was moved permenatly and we indicate the new 20 | /// location with the redirection route. 21 | res.headers[ziapi::http::header::kLocation] = redirection_route_; 22 | res.status_code = ziapi::http::Code::kMovedPermanently; 23 | } 24 | 25 | [[nodiscard]] bool ShouldHandle(const ziapi::http::Request &req) 26 | { 27 | /// We wish to handle all requests. 28 | return true; 29 | } 30 | 31 | [[nodiscard]] double GetHandlerPriority() const noexcept 32 | { 33 | /// Our handler is really important! 34 | return 0.9f; 35 | } 36 | 37 | [[nodiscard]] ziapi::Version GetVersion() const noexcept { return {4, 0, 0}; } 38 | 39 | [[nodiscard]] ziapi::Version GetCompatibleApiVersion() const noexcept { return {4, 0, 0}; } 40 | 41 | [[nodiscard]] const char *GetName() const noexcept { return "Redirection Module"; } 42 | 43 | [[nodiscard]] const char *GetDescription() const noexcept { return "Redirects the request to another location."; } 44 | 45 | private: 46 | std::string redirection_route_; 47 | }; 48 | -------------------------------------------------------------------------------- /include/ziapi/Color.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * This namespace contains every color that you can display on the shell 5 | */ 6 | namespace ziapi::color { 7 | 8 | constexpr auto DEFAULT = "\x1B[0m"; 9 | constexpr auto RED = "\x1B[31m"; 10 | constexpr auto GREEN = "\x1B[32m"; 11 | constexpr auto YELLOW = "\x1B[33m"; 12 | constexpr auto BLUE = "\x1B[34m"; 13 | constexpr auto MAGENTA = "\x1B[35m"; 14 | constexpr auto CYAN = "\x1B[36m"; 15 | 16 | } // namespace ziapi::color 17 | -------------------------------------------------------------------------------- /include/ziapi/Config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace ziapi::config { 11 | 12 | enum Type { 13 | kUndefined = 0, 14 | kNull, 15 | kBool, 16 | kInt, 17 | kDouble, 18 | kString, 19 | kArray, 20 | kDict, 21 | }; 22 | 23 | struct Node; 24 | 25 | struct Undefined { 26 | }; 27 | 28 | using Null = std::nullptr_t; 29 | 30 | using String = std::string; 31 | 32 | using Array = std::vector>; 33 | 34 | /// Datatype for json/yaml-like data 35 | using Dict = std::unordered_map>; 36 | 37 | using NodeVariant = std::variant; 38 | 39 | struct Node : public NodeVariant { 40 | public: 41 | using NodeVariant::NodeVariant; 42 | 43 | /// Simpler way to construct an Array node, it instantiates an std::shared_ptr for each value 44 | static Node MakeArray(const std::initializer_list &values) 45 | { 46 | Array arr; 47 | 48 | for (auto &value : values) { 49 | arr.push_back(std::make_shared(value)); 50 | } 51 | return arr; 52 | } 53 | 54 | /// Constructs an Array node from a vector, it instantiates an std::shared_ptr for each value 55 | static Node MakeArray(const std::vector &values) 56 | { 57 | Array arr; 58 | 59 | for (auto &value : values) { 60 | arr.push_back(std::make_shared(value)); 61 | } 62 | return arr; 63 | } 64 | 65 | /// Simpler way to construct a Dict node, it instantiates an std::shared_ptr for each value 66 | static Node MakeDict(const std::initializer_list> &values) 67 | { 68 | Dict dict; 69 | 70 | for (auto &value : values) { 71 | dict[value.first] = std::make_shared(value.second); 72 | } 73 | return dict; 74 | } 75 | 76 | /// Constructs a Dict node from a vector, it instantiates an std::shared_ptr for each value 77 | static Node MakeDict(const std::unordered_map &values) 78 | { 79 | Dict dict; 80 | 81 | for (auto &[key, value] : values) { 82 | dict[key] = std::make_shared(value); 83 | } 84 | return dict; 85 | } 86 | 87 | /// Used to construct a Node from a string 88 | Node(const char *str) : NodeVariant(std::string(str)){}; 89 | 90 | /// Shorthand, `node->AsDict()["something"]` become `node["something"]` 91 | Node &operator[](const char *key) const { return *AsDict().at(key); } 92 | 93 | /// Shorthand, `node->AsDict()["something"]` become `node["something"]` 94 | Node &operator[](const std::string &key) const { return *AsDict().at(key); } 95 | 96 | /// Shorthand, `node->AsArray()[5]` become `node[5]` 97 | Node &operator[](std::size_t index) const { return *AsArray().at(index); } 98 | 99 | /// Casts the variant as a bool. Will throw if actual type differs. 100 | bool AsBool() const { return std::get(*this); } 101 | 102 | /// Casts the variant as a Dict. Will throw if actual type differs. 103 | const Dict &AsDict() const { return std::get(*this); } 104 | 105 | /// Casts the variant as a String. Will throw if actual type differs. 106 | const String &AsString() const { return std::get(*this); } 107 | 108 | /// Casts the variant as a Array. Will throw if actual type differs. 109 | const Array &AsArray() const { return std::get(*this); } 110 | 111 | /// Casts the variant as a int. Will throw if actual type differs. 112 | int AsInt() const { return std::get(*this); } 113 | 114 | /// Casts the variant as a double. Will throw if actual type differs. 115 | double AsDouble() const { return std::get(*this); } 116 | 117 | /// Check if your Node value is null 118 | bool IsNull() const { return index() == kNull; } 119 | 120 | /// Check if your Node value is undefined 121 | bool IsUndefined() const { return index() == kUndefined; } 122 | 123 | /// Check if your Node is not empty 124 | operator bool() const { return !IsNull() && !IsUndefined(); } 125 | }; 126 | 127 | } // namespace ziapi::config 128 | -------------------------------------------------------------------------------- /include/ziapi/Http.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "HttpConstants.hpp" 10 | 11 | namespace ziapi::http { 12 | 13 | /** 14 | * Struct that represents an HTTP request message 15 | */ 16 | struct Request { 17 | /// For possible values of version checkout ziapi::http::Version. 18 | Version version; 19 | 20 | /// The request target contains the route and the query parameters. 21 | /// The route is simply the path of the request, like `/users/profile`. 22 | /// The query parameters are the parameters of the request, like `?username=toto&age=18`. 23 | std::string target; 24 | 25 | /// For possible values of method checkout ziapi::http::method. 26 | std::string method; 27 | 28 | std::map headers; 29 | 30 | std::string body; 31 | }; 32 | 33 | /** 34 | * Struct that represents an HTTP response message 35 | */ 36 | struct Response { 37 | /// For possible values of version checkout ziapi::http::Version. 38 | Version version; 39 | 40 | /// For possible values of version checkout ziapi::http::code. 41 | Code status_code; 42 | 43 | /// For possible values of version checkout ziapi::http::reason. 44 | std::string reason; 45 | 46 | std::map headers; 47 | 48 | std::string body; 49 | 50 | void Bootstrap(Code status_code_ = Code::kOK, std::string reason_ = reason::kOK, Version version_ = Version::kV1_1) 51 | { 52 | status_code = status_code_; 53 | reason = reason_; 54 | version = version_; 55 | } 56 | }; 57 | 58 | /** 59 | * Context stores the context associated with an HTTP request. It acts like 60 | * a key value store to allow inter-module communication 61 | */ 62 | using Context = std::unordered_map; 63 | 64 | /** 65 | * IResponseInputQueue is a consumer-only container for HTTP responses. 66 | */ 67 | class IResponseInputQueue { 68 | public: 69 | using ValueType = std::pair; 70 | 71 | virtual ~IResponseInputQueue() = default; 72 | 73 | [[nodiscard]] virtual std::optional Pop() = 0; 74 | 75 | [[nodiscard]] virtual std::size_t Size() const noexcept = 0; 76 | 77 | // Wait is used to wait until something is available in the queue and return once that's the case. 78 | // It is a blocking call 79 | virtual void Wait() noexcept = 0; 80 | }; 81 | 82 | /** 83 | * IRequestOutputQueue is a consumer-only container for HTTP requests. 84 | */ 85 | class IRequestOutputQueue { 86 | public: 87 | using ValueType = std::pair; 88 | 89 | virtual ~IRequestOutputQueue() = default; 90 | 91 | virtual void Push(ValueType &&req) = 0; 92 | 93 | [[nodiscard]] virtual std::size_t Size() const noexcept = 0; 94 | }; 95 | 96 | } // namespace ziapi::http 97 | -------------------------------------------------------------------------------- /include/ziapi/HttpConstants.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ziapi::http { 4 | 5 | namespace method { 6 | 7 | constexpr auto kGet = "GET"; 8 | constexpr auto kPost = "POST"; 9 | constexpr auto kPut = "PUT"; 10 | constexpr auto kDelete = "DELETE"; 11 | constexpr auto kPatch = "PATCH"; 12 | constexpr auto kOptions = "OPTIONS"; 13 | constexpr auto kHead = "HEAD"; 14 | 15 | } // namespace method 16 | 17 | enum class Version { 18 | kV1 = 10, 19 | kV1_1 = 11, 20 | kV2 = 20, 21 | kV3 = 30, 22 | }; 23 | 24 | namespace header { 25 | 26 | constexpr auto kAIM = "A-IM"; 27 | constexpr auto kAccept = "Accept"; 28 | constexpr auto kAcceptCharset = "Accept-Charset"; 29 | constexpr auto kAcceptDatetime = "Accept-Datetime"; 30 | constexpr auto kAcceptEncoding = "Accept-Encoding"; 31 | constexpr auto kAcceptLanguage = "Accept-Language"; 32 | constexpr auto kAccessControlRequestMethod = "Access-Control-Request-Method"; 33 | constexpr auto kAccessControlRequestHeaders = "Access-Control-Request-Headers"; 34 | constexpr auto kAuthorization = "Authorization"; 35 | constexpr auto kCacheControl = "Cache-Control"; 36 | constexpr auto kConnection = "Connection"; 37 | constexpr auto kContentLength = "Content-Length"; 38 | constexpr auto kContentMD5 = "Content-MD5"; 39 | constexpr auto kContentType = "Content-Type"; 40 | constexpr auto kCookie = "Cookie"; 41 | constexpr auto kDate = "Date"; 42 | constexpr auto kExpect = "Expect"; 43 | constexpr auto kForwarded = "Forwarded"; 44 | constexpr auto kFrom = "From"; 45 | constexpr auto kHost = "Host"; 46 | constexpr auto kHTTP2Settings = "HTTP2-Settings"; 47 | constexpr auto kIfMatch = "If-Match"; 48 | constexpr auto kIfModifiedSince = "If-Modified-Since"; 49 | constexpr auto kIfNoneMatch = "If-None-Match"; 50 | constexpr auto kIfRange = "If-Range"; 51 | constexpr auto kIfUnmodifiedSince = "If-Unmodified-Since"; 52 | constexpr auto kMaxForwards = "Max-Forwards"; 53 | constexpr auto kOrigin = "Origin"; 54 | constexpr auto kPragma = "Pragma"; 55 | constexpr auto kPrefer = "Prefer"; 56 | constexpr auto kProxyAuthorization = "Proxy-Authorization"; 57 | constexpr auto kRange = "Range"; 58 | constexpr auto kReferer = "Referer"; 59 | constexpr auto kTE = "TE"; 60 | constexpr auto kUserAgent = "User-Agent"; 61 | constexpr auto kUpgrade = "Upgrade"; 62 | constexpr auto kVia = "Via"; 63 | constexpr auto kAcceptCH = "Accept-CH"; 64 | constexpr auto kAccessControlAllowOrigin = "Access-Control-Allow-Origin"; 65 | constexpr auto kAccessControlAllowCredentials = "Access-Control-Allow-Credentials"; 66 | constexpr auto kAccessControlExposeHeaders = "Access-Control-Expose-Headers"; 67 | constexpr auto kAccessControlMaxAge = "Access-Control-Max-Age"; 68 | constexpr auto kAccessControlAllowMethods = "Access-Control-Allow-Methods"; 69 | constexpr auto kAccessControlAllowHeaders = "Access-Control-Allow-Headers"; 70 | constexpr auto kAcceptPatch = "Accept-Patch"; 71 | constexpr auto kAcceptRanges = "Accept-Ranges"; 72 | constexpr auto kAge = "Age"; 73 | constexpr auto kAllow = "Allow"; 74 | constexpr auto kAltSvc = "Alt-Svc"; 75 | constexpr auto kContentDisposition = "Content-Disposition"; 76 | constexpr auto kContentEncoding = "Content-Encoding"; 77 | constexpr auto kContentLanguage = "Content-Language"; 78 | constexpr auto kContentLocation = "Content-Location"; 79 | constexpr auto kContentRange = "Content-Range"; 80 | constexpr auto kDeltaBase = "Delta-Base"; 81 | constexpr auto kETag = "ETag"; 82 | constexpr auto kExpires = "Expires"; 83 | constexpr auto kIM = "IM"; 84 | constexpr auto kLastModified = "Last-Modified"; 85 | constexpr auto kLink = "Link"; 86 | constexpr auto kLocation = "Location"; 87 | constexpr auto kP3P = "P3P"; 88 | constexpr auto kPreferenceApplied = "Preference-Applied"; 89 | constexpr auto kProxyAuthenticate = "Proxy-Authenticate"; 90 | constexpr auto kPublicKeyPins = "Public-Key-Pins"; 91 | constexpr auto kRetryAfter = "Retry-After"; 92 | constexpr auto kServer = "Server"; 93 | constexpr auto kSetCookie = "Set-Cookie"; 94 | constexpr auto kStrictTransportSecurity = "Strict-Transport-Security"; 95 | constexpr auto kTrailer = "Trailer"; 96 | constexpr auto kTransferEncoding = "Transfer-Encoding"; 97 | constexpr auto kTk = "Tk"; 98 | constexpr auto kVary = "Vary"; 99 | constexpr auto kWarning = "Warning"; 100 | constexpr auto kWWWAuthenticate = "WWW-Authenticate"; 101 | constexpr auto kXFrameOptions = "X-Frame-Options"; 102 | constexpr auto kKeepAlive= "Keep-Alive"; 103 | 104 | } // namespace header 105 | 106 | namespace reason { 107 | 108 | constexpr auto kContinue = "Continue"; 109 | constexpr auto kSwitchingProtocols = "Switching Protocols"; 110 | constexpr auto kOK = "OK"; 111 | constexpr auto kCreated = "Created"; 112 | constexpr auto kAccepted = "Accepted"; 113 | constexpr auto kNonAuthoritativeInformation = "Non-Authoritative Information"; 114 | constexpr auto kNoContent = "No Content"; 115 | constexpr auto kResetContent = "Reset Content"; 116 | constexpr auto kPartialContent = "Partial Content"; 117 | constexpr auto kMultipleChoices = "Multiple Choices"; 118 | constexpr auto kMovedPermanently = "Moved Permanently"; 119 | constexpr auto kFound = "Found"; 120 | constexpr auto kSeeOther = "See Other"; 121 | constexpr auto kNotModified = "Not Modified"; 122 | constexpr auto kUseProxy = "Use Proxy"; 123 | constexpr auto kTemporaryRedirect = "Temporary Redirect"; 124 | constexpr auto kBadRequest = "Bad Request"; 125 | constexpr auto kUnauthorized = "Unauthorized"; 126 | constexpr auto kPaymentRequired = "Payment Required"; 127 | constexpr auto kForbidden = "Forbidden"; 128 | constexpr auto kNotFound = "Not Found"; 129 | constexpr auto kMethodNotAllowed = "Method Not Allowed"; 130 | constexpr auto kNotAcceptable = "Not Acceptable"; 131 | constexpr auto kProxyAuthenticationRequired = "Proxy Authentication Required"; 132 | constexpr auto kRequesTtimOut = "Request Time-out"; 133 | constexpr auto kConflicT = " Conflict"; 134 | constexpr auto kGone = "Gone"; 135 | constexpr auto kLengthRequired = " Length Required"; 136 | constexpr auto kPreconditionFailed = " Precondition Failed"; 137 | constexpr auto kRequestEntityTooLarge = " Request Entity Too Large"; 138 | constexpr auto kRequestURITooLarge = " Request-URI Too Large"; 139 | constexpr auto kUnsupportedMediaType = " Unsupported Media Type"; 140 | constexpr auto kRequestedRangeNotSatisfiable = " Requested range not satisfiable"; 141 | constexpr auto kExpectationFailed = " Expectation Failed"; 142 | constexpr auto kInternalServerError = "Internal Server Error"; 143 | constexpr auto kNotImplemented = "Not Implemented"; 144 | constexpr auto kBadGateway = "Bad Gateway"; 145 | constexpr auto kServiceUnavailable = "Service Unavailable"; 146 | constexpr auto kGatewayTimeout = "Gateway Time-out"; 147 | constexpr auto kHttpVersionNotSupported = "HTTP Version not supported"; 148 | 149 | } // namespace reason 150 | 151 | enum class Code { 152 | kContinue = 100, 153 | kSwitchingProtocols = 101, 154 | kOK = 200, 155 | kCreated = 201, 156 | kAccepted = 202, 157 | kNonAuthoritativeInformation = 203, 158 | kNoContent = 204, 159 | kResetContent = 205, 160 | kPartialContent = 206, 161 | kMultipleChoices = 300, 162 | kMovedPermanently = 301, 163 | kFound = 302, 164 | kSeeOther = 303, 165 | kNotModified = 304, 166 | kUseProxy = 305, 167 | kTemporaryRedirect = 307, 168 | kBadRequest = 400, 169 | kUnauthorized = 401, 170 | kPaymentRequired = 402, 171 | kForbidden = 403, 172 | kNotFound = 404, 173 | kMethodNotAllowed = 405, 174 | kNotAcceptable = 406, 175 | kProxyAuthenticationRequired = 407, 176 | kRequestTimeOut = 408, 177 | kConflict = 409, 178 | kGone = 410, 179 | kLengthRequired = 411, 180 | kPreconditionFailed = 412, 181 | kRequestEntityTooLarge = 413, 182 | kRequestURITooLarge = 414, 183 | kUnsupportedMediaType = 415, 184 | kRequestedRangeNotSatisfiable = 416, 185 | kExpectationFailed = 417, 186 | kInternalServerError = 500, 187 | kNotImplemented = 501, 188 | kBadGateway = 502, 189 | kServiceUnavailable = 503, 190 | kGatewayTime_out = 504, 191 | kHttpVersionNotSupported = 505, 192 | }; 193 | 194 | } // namespace ziapi::http 195 | -------------------------------------------------------------------------------- /include/ziapi/Logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "Color.hpp" 10 | 11 | namespace ziapi { 12 | 13 | /** 14 | * Log a message in a stream 15 | * @param args message to be logged as variadic template arguments 16 | */ 17 | class Logger { 18 | private: 19 | enum LogType { debug, info, warning, error }; 20 | template 21 | static void Log(std::ostream &stream, LogType log_type, Args &&...args) 22 | { 23 | static const std::map log_type_map{ 24 | {LogType::info, color::BLUE + std::string(" [i] ") + color::DEFAULT}, 25 | {LogType::warning, color::YELLOW + std::string(" [!] ") + color::DEFAULT}, 26 | {LogType::error, color::RED + std::string(" [X] ") + color::DEFAULT}, 27 | {LogType::debug, color::GREEN + std::string(" [&] ") + color::DEFAULT}, 28 | }; 29 | time_t actual_time = std::time(nullptr); 30 | std::string time_str = std::ctime(&actual_time); 31 | time_str.erase(std::remove_if(time_str.begin(), time_str.end(), [](char a) { return a == '\n'; }), 32 | time_str.end()); 33 | 34 | stream << color::CYAN << time_str << color::DEFAULT; 35 | stream << log_type_map.at(log_type); 36 | ((stream << args), ...) << std::endl; 37 | } 38 | 39 | public: 40 | template 41 | static void Debug(Args &&...args) 42 | { 43 | Log(std::cout, LogType::debug, std::forward(args)...); 44 | } 45 | template 46 | static void Info(Args &&...args) 47 | { 48 | Log(std::cout, LogType::info, std::forward(args)...); 49 | } 50 | template 51 | static void Warning(Args &&...args) 52 | { 53 | Log(std::cout, LogType::warning, std::forward(args)...); 54 | } 55 | template 56 | static void Error(Args &&...args) 57 | { 58 | Log(std::cerr, LogType::error, std::forward(args)...); 59 | } 60 | }; 61 | 62 | } // namespace ziapi 63 | -------------------------------------------------------------------------------- /include/ziapi/Module.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "Config.hpp" 7 | #include "Http.hpp" 8 | #include "Version.hpp" 9 | 10 | namespace ziapi { 11 | 12 | class IModule { 13 | public: 14 | virtual ~IModule() = default; 15 | 16 | virtual void Init(const config::Node &cfg) = 0; 17 | 18 | [[nodiscard]] virtual Version GetVersion() const noexcept = 0; 19 | 20 | [[nodiscard]] virtual Version GetCompatibleApiVersion() const noexcept = 0; 21 | 22 | [[nodiscard]] virtual const char *GetName() const noexcept = 0; 23 | 24 | [[nodiscard]] virtual const char *GetDescription() const noexcept = 0; 25 | }; 26 | 27 | /** 28 | * Handler modules handle HTTP requests and are responsible for generating 29 | * the contents of the response such as a directory listing module, a web 30 | * server module, etc... 31 | */ 32 | class IHandlerModule : virtual public IModule { 33 | public: 34 | virtual ~IHandlerModule() = default; 35 | 36 | /** 37 | * Handler invoked as the response-generation step for the request. For a 38 | * single HTTP request, only one handler will be invoked. If multiple 39 | * handler modules try to handle the same request through the ShouldHandle 40 | * method, only one handler with the highest priority will be invoked 41 | */ 42 | virtual void Handle(http::Context &ctx, const http::Request &req, http::Response &res) = 0; 43 | 44 | /** 45 | * Value between zero and one which states the module's priority. Higher 46 | * values are prioritized 47 | */ 48 | [[nodiscard]] virtual double GetHandlerPriority() const noexcept = 0; 49 | 50 | /** 51 | * Whether this module's Handle method should be called on the request 52 | */ 53 | [[nodiscard]] virtual bool ShouldHandle(const http::Context &ctx, const http::Request &req) const = 0; 54 | }; 55 | 56 | /** 57 | * Post processor modules are invoked after the generation of the response 58 | * by the handler module. They can be used for logging, cors, compression, etc... 59 | */ 60 | class IPostProcessorModule : virtual public IModule { 61 | public: 62 | virtual ~IPostProcessorModule() = default; 63 | /** 64 | * Handler invoked during the post-processing pipeline after the handler. 65 | */ 66 | virtual void PostProcess(http::Context &ctx, const http::Request &req, http::Response &res) = 0; 67 | 68 | /** 69 | * Value between zero and one which states the module's priority of 70 | * execution in the pipeline. Higher values are prioritized 71 | */ 72 | [[nodiscard]] virtual double GetPostProcessorPriority() const noexcept = 0; 73 | 74 | /** 75 | * Whether this module's PostProcess should be called on the response 76 | */ 77 | [[nodiscard]] virtual bool ShouldPostProcess(const http::Context &ctx, const http::Request &req, 78 | const http::Response &res) const = 0; 79 | }; 80 | 81 | /** 82 | * Pre processor modules are invoked before the generation of the response by 83 | * the handler module. They can be used for url rewriting, authentication, logging, etc... 84 | */ 85 | class IPreProcessorModule : virtual public IModule { 86 | public: 87 | virtual ~IPreProcessorModule() = default; 88 | /** 89 | * Handler invoked during the pre-processing pipeline before the handler 90 | */ 91 | virtual void PreProcess(http::Context &ctx, http::Request &req) = 0; 92 | 93 | /** 94 | * Value between zero and one which states the module's priority of 95 | * execution in the pipeline. Higher values are prioritized 96 | */ 97 | [[nodiscard]] virtual double GetPreProcessorPriority() const noexcept = 0; 98 | 99 | /** 100 | * Whether this module's PreProcess method should be called on the request 101 | */ 102 | [[nodiscard]] virtual bool ShouldPreProcess(const http::Context &ctx, const http::Request &req) const = 0; 103 | }; 104 | 105 | class INetworkModule : public IModule { 106 | public: 107 | virtual ~INetworkModule() = default; 108 | /** 109 | * Runs the module providing it with an output queue in which it 110 | * shall push incoming requests and an input queue from which it should 111 | * receive incoming responses and send them over the network. 112 | * A call to Run must be blocking till a call to Terminate is issued. 113 | */ 114 | virtual void Run(http::IRequestOutputQueue &requests, http::IResponseInputQueue &responses) = 0; 115 | 116 | /** 117 | * Terminate will be invoked upon reloading or termination of the server, 118 | * it notifies the module that it needs to stops running altogether and 119 | * release every resource it has created 120 | */ 121 | virtual void Terminate() = 0; 122 | }; 123 | 124 | } // namespace ziapi 125 | -------------------------------------------------------------------------------- /include/ziapi/Version.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace ziapi { 4 | 5 | /** 6 | * Semantic versioning structure with a major and a minor version 7 | */ 8 | struct Version { 9 | Version(int major_, int minor_, int patch_) : major(major_), minor(minor_), patch(patch_) {} 10 | 11 | inline bool operator==(const Version &other) const noexcept 12 | { 13 | return major == other.major && minor == other.minor && patch == other.patch; 14 | } 15 | 16 | inline bool operator!=(const Version &other) const noexcept { return !(*this == other); } 17 | 18 | inline bool operator>(const Version &other) const noexcept { return (*this >= other) && (*this != other); } 19 | 20 | inline bool operator<(const Version &other) const noexcept { return !(*this > other) && other != *this; } 21 | 22 | inline bool operator>=(const Version &other) const noexcept 23 | { 24 | return (major == other.major 25 | ? minor == other.minor ? patch == other.patch ? true : patch > other.patch : minor > other.minor 26 | : major > other.major); 27 | } 28 | 29 | inline bool operator<=(const Version &other) const noexcept { return (*this < other) || (*this == other); } 30 | 31 | unsigned int major; 32 | unsigned int minor; 33 | unsigned int patch; 34 | }; 35 | 36 | } // namespace ziapi 37 | -------------------------------------------------------------------------------- /tests/Compressor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "compressor/CompressorModule.hpp" 4 | 5 | TEST(Compressor, UtilsInfo) 6 | { 7 | CompressorModule compressor; 8 | 9 | ASSERT_EQ(std::string(compressor.GetName()), std::string("CompressorModule")); 10 | ASSERT_EQ(std::string(compressor.GetDescription()), 11 | std::string("Compress the response body before sending it back to the network")); 12 | ASSERT_EQ(compressor.GetCompatibleApiVersion(), (ziapi::Version{4, 0, 0})); 13 | ASSERT_EQ(compressor.GetVersion(), (ziapi::Version{4, 0, 0})); 14 | } 15 | 16 | TEST(Compressor, compressionRate) 17 | { 18 | CompressorModule compressor; 19 | ziapi::http::Context ctx{}; 20 | ziapi::http::Request req{}; 21 | ziapi::http::Response res = { 22 | ziapi::http::Version::kV1_1, // Version 23 | ziapi::http::Code::kOK, // Status code 24 | std::string("OK"), // reason 25 | std::map({}), // headers 26 | std::string("not compressed stuff blabla omg so long"), // body 27 | }; 28 | res.headers.insert(std::make_pair("Content-Type", "application/json")); 29 | 30 | if (compressor.ShouldPostProcess(ctx, req, res)) { 31 | compressor.PostProcess(ctx, req, res); 32 | } 33 | ASSERT_EQ(res.body, "not compressed stuf"); 34 | } 35 | -------------------------------------------------------------------------------- /tests/Config.cpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Config.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | 8 | using namespace ziapi::config; 9 | 10 | TEST(Config, SimpleInt) 11 | { 12 | Node node(10); 13 | 14 | ASSERT_EQ(node.AsInt(), 10); 15 | } 16 | 17 | TEST(Config, SimpleString) 18 | { 19 | Node node("Hello world"); 20 | 21 | ASSERT_EQ(node.AsString(), "Hello world"); 22 | } 23 | 24 | TEST(Config, SimpleDouble) 25 | { 26 | Node node(45.647f); 27 | 28 | ASSERT_EQ(node.AsDouble(), 45.647f); 29 | } 30 | 31 | TEST(Config, SimpleBool) 32 | { 33 | Node node(true); 34 | 35 | ASSERT_EQ(node.AsBool(), true); 36 | } 37 | 38 | TEST(Config, SimpleArray) 39 | { 40 | auto array = Node::MakeArray({10, "Hello world", 14.5f}); 41 | 42 | ASSERT_EQ(array[(std::size_t)0].AsInt(), 10); 43 | ASSERT_EQ(array[1].AsString(), "Hello world"); 44 | ASSERT_EQ(array[2].AsDouble(), 14.5f); 45 | } 46 | 47 | TEST(Config, SimpleDict) 48 | { 49 | auto dict = Node::MakeDict({{"modules_count", 10}}); 50 | 51 | ASSERT_EQ(dict["modules_count"].AsInt(), 10); 52 | } 53 | 54 | TEST(Config, NestedAccess) 55 | { 56 | auto cfg = Node::MakeDict({ 57 | {"modules", Node::MakeDict({{"directoryListing", Node::MakeDict({{"root", "/var/www"}})}})}, 58 | }); 59 | 60 | ASSERT_EQ(cfg["modules"]["directoryListing"]["root"].AsString(), "/var/www"); 61 | } 62 | 63 | TEST(Config, OperatorBool) 64 | { 65 | Node undefined(Undefined{}); 66 | Node null(nullptr); 67 | Node string("Hello"); 68 | 69 | ASSERT_EQ(undefined.operator bool(), false); 70 | ASSERT_EQ(null.operator bool(), false); 71 | ASSERT_EQ(string.operator bool(), true); 72 | } 73 | 74 | TEST(Config, VectorConstructArray) 75 | { 76 | std::vector vec{10, "value", 1.f}; 77 | Node n = Node::MakeArray(vec); 78 | 79 | ASSERT_EQ(n[(std::size_t)0].AsInt(), 10); 80 | ASSERT_EQ(n[1].AsString(), "value"); 81 | ASSERT_EQ(n[2].AsDouble(), 1.f); 82 | } 83 | 84 | TEST(Config, MapConstructDict) 85 | { 86 | std::unordered_map map{{"ten", 10}, {"string", "value"}, {"float", 1.f}, {"null", nullptr}}; 87 | Node n = Node::MakeDict(map); 88 | 89 | ASSERT_EQ(n["ten"].AsInt(), 10); 90 | ASSERT_EQ(n["string"].AsString(), "value"); 91 | ASSERT_EQ(n["float"].AsDouble(), 1.f); 92 | ASSERT_EQ(n["null"].IsNull(), true); 93 | } 94 | -------------------------------------------------------------------------------- /tests/Decompressor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "decompressor/DecompressorModule.hpp" 4 | 5 | TEST(Decompressor, UtilsInfo) 6 | { 7 | DecompressorModule decompressor; 8 | 9 | ASSERT_EQ(std::string(decompressor.GetName()), std::string("DecompressorModule")); 10 | ASSERT_EQ(std::string(decompressor.GetDescription()), 11 | std::string("Decompress the response body before sending it to the module pipeline")); 12 | ASSERT_EQ(decompressor.GetCompatibleApiVersion(), (ziapi::Version{4, 0, 0})); 13 | ASSERT_EQ(decompressor.GetVersion(), (ziapi::Version{4, 0, 0})); 14 | } 15 | 16 | TEST(Decompressor, Decompression) 17 | { 18 | DecompressorModule decompressor; 19 | ziapi::http::Context ctx; 20 | ziapi::http::Request req = { 21 | ziapi::http::Version::kV1_1, // version 22 | ziapi::http::method::kPost, // method 23 | "/zipper", // target 24 | std::map({}), // hedears 25 | std::string("0101010110101"), // body 26 | }; 27 | 28 | req.headers.insert(std::make_pair("compressed", "true")); 29 | 30 | if (decompressor.ShouldPreProcess(ctx, req)) { 31 | decompressor.PreProcess(ctx, req); 32 | } 33 | ASSERT_EQ(req.body, "0101010110101 omg i am now decompressed thx algorithm"); 34 | } 35 | -------------------------------------------------------------------------------- /tests/Logger.cpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Logger.hpp" 2 | 3 | #include 4 | 5 | class OSRedirector { 6 | private: 7 | std::ostringstream _oss{}; 8 | std::streambuf *_backup{}; 9 | std::ostream &_c; 10 | 11 | public: 12 | OSRedirector(OSRedirector &) = delete; 13 | OSRedirector &operator=(OSRedirector &) = delete; 14 | 15 | OSRedirector(std::ostream &c) : _c(c) 16 | { 17 | _backup = _c.rdbuf(); 18 | _c.rdbuf(_oss.rdbuf()); 19 | } 20 | 21 | ~OSRedirector() { _c.rdbuf(_backup); } 22 | 23 | const std::string getContent() 24 | { 25 | _oss << std::flush; 26 | return _oss.str(); 27 | } 28 | }; 29 | 30 | TEST(Logger, Info) 31 | { 32 | OSRedirector os(std::cout); 33 | 34 | ziapi::Logger::Info("infos"); 35 | auto out = os.getContent(); 36 | EXPECT_TRUE(out.find(ziapi::color::BLUE + std::string(" [i] ") + ziapi::color::DEFAULT + "infos") != 37 | std::string::npos); 38 | } 39 | 40 | TEST(Logger, Warning) 41 | { 42 | OSRedirector os(std::cout); 43 | 44 | ziapi::Logger::Warning("warning"); 45 | auto out = os.getContent(); 46 | EXPECT_TRUE(out.find(ziapi::color::YELLOW + std::string(" [!] ") + ziapi::color::DEFAULT + "warning") != 47 | std::string::npos); 48 | } 49 | 50 | TEST(Logger, Error) 51 | { 52 | OSRedirector os(std::cerr); 53 | 54 | ziapi::Logger::Error("error"); 55 | auto out = os.getContent(); 56 | EXPECT_TRUE(out.find(ziapi::color::RED + std::string(" [X] ") + ziapi::color::DEFAULT + "error") != 57 | std::string::npos); 58 | } 59 | 60 | TEST(Logger, Debug) 61 | { 62 | OSRedirector os(std::cout); 63 | 64 | ziapi::Logger::Debug("debug"); 65 | auto out = os.getContent(); 66 | EXPECT_TRUE(out.find(ziapi::color::GREEN + std::string(" [&] ") + ziapi::color::DEFAULT + "debug") != 67 | std::string::npos); 68 | } 69 | 70 | TEST(Logger, Variadic) 71 | { 72 | OSRedirector os(std::cout); 73 | 74 | ziapi::Logger::Debug("debug", ' ', 1.1); 75 | auto out = os.getContent(); 76 | EXPECT_TRUE(out.find(ziapi::color::GREEN + std::string(" [&] ") + ziapi::color::DEFAULT + "debug 1.1") != 77 | std::string::npos); 78 | } 79 | -------------------------------------------------------------------------------- /tests/Module.cpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Module.hpp" 2 | 3 | #include 4 | 5 | #include "dylib/dylib.hpp" 6 | #include "ziapi/Logger.hpp" 7 | 8 | TEST(Module, Example) 9 | { 10 | try { 11 | dylib lib("./module", dylib::extension); 12 | auto entry_point_fn = lib.get_function("LoadZiaModule"); 13 | std::unique_ptr mod(entry_point_fn()); 14 | ziapi::Logger::Info("Module loaded: ", mod->GetName(), " - ", mod->GetDescription()); 15 | } catch (const dylib::exception &e) { 16 | EXPECT_TRUE(false); 17 | ziapi::Logger::Error(e.what()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/Version.cpp: -------------------------------------------------------------------------------- 1 | #include "ziapi/Version.hpp" 2 | 3 | #include 4 | 5 | TEST(Version, Equal) 6 | { 7 | ziapi::Version a{1, 0, 3}; 8 | ziapi::Version b{1, 0, 3}; 9 | 10 | ASSERT_TRUE(a == b); 11 | 12 | a = ziapi::Version{1, 1, 3}; 13 | b = ziapi::Version{1, 0, 3}; 14 | 15 | ASSERT_FALSE(a == b); 16 | 17 | a = ziapi::Version{2, 0, 3}; 18 | b = ziapi::Version{1, 1, 3}; 19 | 20 | ASSERT_FALSE(a == b); 21 | } 22 | 23 | TEST(Version, NotEqual) 24 | { 25 | ziapi::Version a{1, 0, 0}; 26 | ziapi::Version b{1, 0, 0}; 27 | 28 | ASSERT_FALSE(a != b); 29 | 30 | a = ziapi::Version{1, 1, 0}; 31 | b = ziapi::Version{1, 0, 0}; 32 | 33 | ASSERT_TRUE(a != b); 34 | 35 | a = ziapi::Version{2, 0, 0}; 36 | b = ziapi::Version{1, 1, 0}; 37 | 38 | ASSERT_TRUE(a != b); 39 | } 40 | 41 | TEST(Version, Greater) 42 | { 43 | ziapi::Version a{1, 0, 3}; 44 | ziapi::Version b{1, 0, 3}; 45 | 46 | ASSERT_FALSE(a > b); 47 | 48 | a = ziapi::Version{1, 1, 2}; 49 | b = ziapi::Version{1, 1, 0}; 50 | 51 | ASSERT_TRUE(a > b); 52 | 53 | a = ziapi::Version{2, 0, 2}; 54 | b = ziapi::Version{3, 1, 2}; 55 | 56 | ASSERT_FALSE(a > b); 57 | } 58 | 59 | TEST(Version, Less) 60 | { 61 | ziapi::Version a{1, 0, 1}; 62 | ziapi::Version b{1, 0, 0}; 63 | 64 | ASSERT_FALSE(a < b); 65 | 66 | a = ziapi::Version{1, 1, 0}; 67 | b = ziapi::Version{3, 4, 0}; 68 | 69 | ASSERT_TRUE(a < b); 70 | 71 | a = ziapi::Version{2, 0, 1}; 72 | b = ziapi::Version{1, 0, 0}; 73 | 74 | ASSERT_FALSE(a < b); 75 | } 76 | 77 | TEST(Version, GreaterOrEqual) 78 | { 79 | ziapi::Version a{1, 0, 0}; 80 | ziapi::Version b{1, 0, 0}; 81 | 82 | ASSERT_TRUE(a >= b); 83 | 84 | a = ziapi::Version{4, 0, 0}; 85 | b = ziapi::Version{1, 4, 0}; 86 | 87 | ASSERT_TRUE(a >= b); 88 | 89 | a = ziapi::Version{1, 6, 0}; 90 | b = ziapi::Version{3, 6, 1}; 91 | 92 | ASSERT_FALSE(a >= b); 93 | } 94 | 95 | TEST(Version, LessOrEqual) 96 | { 97 | ziapi::Version a{1, 0, 0}; 98 | ziapi::Version b{1, 0, 0}; 99 | 100 | ASSERT_TRUE(a <= b); 101 | 102 | a = ziapi::Version{1, 1, 2}; 103 | b = ziapi::Version{4, 0, 0}; 104 | 105 | ASSERT_TRUE(a <= b); 106 | 107 | a = ziapi::Version{2, 4, 42}; 108 | b = ziapi::Version{1, 7, 0}; 109 | 110 | ASSERT_FALSE(a < b); 111 | ASSERT_FALSE(a <= b); 112 | } 113 | --------------------------------------------------------------------------------