diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000..37e8a2b66c --- /dev/null +++ b/.clang-format @@ -0,0 +1,5 @@ +BasedOnStyle: LLVM +AllowShortBlocksOnASingleLine: true +AllowShortCaseLabelsOnASingleLine: true +AllowShortIfStatementsOnASingleLine: true +Cpp11BracedListStyle: true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..f8184834e9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/test/www*/dir/*.html text eol=lf +/test/www*/dir/*.txt text eol=lf \ No newline at end of file diff --git a/.github/workflows/abidiff.yaml b/.github/workflows/abidiff.yaml new file mode 100644 index 0000000000..84ead93d3c --- /dev/null +++ b/.github/workflows/abidiff.yaml @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: 2025 Andrea Pappacoda +# SPDX-License-Identifier: MIT + +name: abidiff + +on: [push, pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +defaults: + run: + shell: sh + +jobs: + abi: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + container: + image: debian:testing + + steps: + - name: Install dependencies + run: apt -y --update install --no-install-recommends + abigail-tools + ca-certificates + g++ + git + libbrotli-dev + libssl-dev + libzstd-dev + meson + pkg-config + python3 + zlib1g-dev + + - uses: actions/checkout@v4 + with: + path: current + + - uses: actions/checkout@v4 + with: + path: previous + fetch-depth: 0 + + - name: Checkout previous + working-directory: previous + run: | + git switch master + git describe --tags --abbrev=0 master | xargs git checkout + + - name: Build current + working-directory: current + run: | + meson setup --buildtype=debug -Dcpp-httplib_compile=true build + ninja -C build + + - name: Build previous + working-directory: previous + run: | + meson setup --buildtype=debug -Dcpp-httplib_compile=true build + ninja -C build + + - name: Run abidiff + run: abidiff + --headers-dir1 previous/build + --headers-dir2 current/build + previous/build/libcpp-httplib.so + current/build/libcpp-httplib.so diff --git a/.github/workflows/cifuzz.yaml b/.github/workflows/cifuzz.yaml new file mode 100644 index 0000000000..422b58da7a --- /dev/null +++ b/.github/workflows/cifuzz.yaml @@ -0,0 +1,32 @@ +name: CIFuzz + +on: [pull_request] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + Fuzzing: + runs-on: ubuntu-latest + steps: + - name: Build Fuzzers + id: build + uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master + with: + oss-fuzz-project-name: 'cpp-httplib' + dry-run: false + language: c++ + - name: Run Fuzzers + uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master + with: + oss-fuzz-project-name: 'cpp-httplib' + fuzz-seconds: 600 + dry-run: false + language: c++ + - name: Upload Crash + uses: actions/upload-artifact@v4 + if: failure() && steps.build.outcome == 'success' + with: + name: artifacts + path: ./out/artifacts diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000000..bf553c7048 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,33 @@ +name: docs +on: + push: + branches: [master] + paths: + - 'docs-src/**' +permissions: + contents: read + pages: write + id-token: write +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Install docs-gen + run: cargo install docs-gen + - name: Build + run: docs-gen build docs-src docs + - uses: actions/configure-pages@v5 + - uses: actions/upload-pages-artifact@v3 + with: + path: docs + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release-docker.yml b/.github/workflows/release-docker.yml new file mode 100644 index 0000000000..179ab82fae --- /dev/null +++ b/.github/workflows/release-docker.yml @@ -0,0 +1,51 @@ +name: Release Docker Image + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history and tags + + - name: Extract tag (manual) + if: github.event_name == 'workflow_dispatch' + id: set_tag_manual + run: | + # Checkout the latest tag and set output + git fetch --tags + LATEST_TAG=$(git describe --tags --abbrev=0) + git checkout $LATEST_TAG + echo "tag=${LATEST_TAG#v}" >> $GITHUB_OUTPUT + + - name: Extract tag (release) + if: github.event_name == 'release' + id: set_tag_release + run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64,linux/arm64 # Build for both amd64 and arm64 + # Use extracted tag without leading 'v' + tags: | + yhirose4dockerhub/cpp-httplib-server:latest + yhirose4dockerhub/cpp-httplib-server:${{ steps.set_tag_manual.outputs.tag || steps.set_tag_release.outputs.tag }} diff --git a/.github/workflows/test-32bit.yml b/.github/workflows/test-32bit.yml new file mode 100644 index 0000000000..2fa0337152 --- /dev/null +++ b/.github/workflows/test-32bit.yml @@ -0,0 +1,36 @@ +name: 32-bit Build Test + +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + test-win32: + name: Windows 32-bit (MSVC x86) + runs-on: windows-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Build (Win32) + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x86 + cl /std:c++14 /EHsc /W4 /WX /c /Fo:NUL test\test_32bit_build.cpp + + test-arm32: + name: ARM 32-bit (cross-compile) + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Install cross compiler + run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf + - name: Build (ARM 32-bit) + run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000000..69cc8d7463 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,254 @@ +name: test + +on: + push: + pull_request: + workflow_dispatch: + inputs: + gtest_filter: + description: 'Google Test filter' + test_linux: + description: 'Test on Linux' + type: boolean + default: true + test_macos: + description: 'Test on MacOS' + type: boolean + default: true + test_windows: + description: 'Test on Windows' + type: boolean + default: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +env: + GTEST_FILTER: ${{ github.event.inputs.gtest_filter || '*' }} + +jobs: + style-check: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + continue-on-error: true + steps: + - name: checkout + uses: actions/checkout@v4 + - name: run style check + run: | + clang-format --version + cd test && make style_check + + build-and-test-on-32bit: + runs-on: ubuntu-latest + if: > + (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') + strategy: + matrix: + config: + - arch_flags: -m32 + arch_suffix: :i386 + name: (32-bit) + steps: + - name: checkout + uses: actions/checkout@v4 + - name: install libraries + run: | + sudo dpkg --add-architecture i386 + sudo apt-get update + sudo apt-get install -y libc6-dev${{ matrix.config.arch_suffix }} libstdc++-13-dev${{ matrix.config.arch_suffix }} \ + libssl-dev${{ matrix.config.arch_suffix }} libcurl4-openssl-dev${{ matrix.config.arch_suffix }} \ + zlib1g-dev${{ matrix.config.arch_suffix }} libbrotli-dev${{ matrix.config.arch_suffix }} \ + libzstd-dev${{ matrix.config.arch_suffix }} + - name: build and run tests + run: cd test && make test EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}" + + ubuntu: + runs-on: ubuntu-latest + if: > + (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') + strategy: + matrix: + tls_backend: [openssl, mbedtls, wolfssl] + name: ubuntu (${{ matrix.tls_backend }}) + steps: + - name: checkout + uses: actions/checkout@v4 + - name: install common libraries + run: | + sudo apt-get update + sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev + - name: install OpenSSL + if: matrix.tls_backend == 'openssl' + run: sudo apt-get install -y libssl-dev + - name: install Mbed TLS + if: matrix.tls_backend == 'mbedtls' + run: sudo apt-get install -y libmbedtls-dev + - name: install wolfSSL + if: matrix.tls_backend == 'wolfssl' + run: sudo apt-get install -y libwolfssl-dev + - name: build and run tests (OpenSSL) + if: matrix.tls_backend == 'openssl' + run: cd test && make test_split && make test_openssl_parallel + env: + LSAN_OPTIONS: suppressions=lsan_suppressions.txt + - name: build and run tests (Mbed TLS) + if: matrix.tls_backend == 'mbedtls' + run: cd test && make test_split_mbedtls && make test_mbedtls_parallel + - name: build and run tests (wolfSSL) + if: matrix.tls_backend == 'wolfssl' + run: cd test && make test_split_wolfssl && make test_wolfssl_parallel + - name: run fuzz test target + if: matrix.tls_backend == 'openssl' + run: cd test && make fuzz_test + - name: build and run WebSocket heartbeat test + if: matrix.tls_backend == 'openssl' + run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat + - name: build and run ThreadPool test + run: cd test && make test_thread_pool && ./test_thread_pool + + macos: + runs-on: macos-latest + if: > + (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true') + strategy: + matrix: + tls_backend: [openssl, mbedtls, wolfssl] + name: macos (${{ matrix.tls_backend }}) + steps: + - name: checkout + uses: actions/checkout@v4 + - name: install Mbed TLS + if: matrix.tls_backend == 'mbedtls' + run: brew install mbedtls@3 + - name: install wolfSSL + if: matrix.tls_backend == 'wolfssl' + run: brew install wolfssl + - name: build and run tests (OpenSSL) + if: matrix.tls_backend == 'openssl' + run: cd test && make test_split && make test_openssl_parallel + env: + LSAN_OPTIONS: suppressions=lsan_suppressions.txt + - name: build and run tests (Mbed TLS) + if: matrix.tls_backend == 'mbedtls' + run: cd test && make test_split_mbedtls && make test_mbedtls_parallel + - name: build and run tests (wolfSSL) + if: matrix.tls_backend == 'wolfssl' + run: cd test && make test_split_wolfssl && make test_wolfssl_parallel + - name: run fuzz test target + if: matrix.tls_backend == 'openssl' + run: cd test && make fuzz_test + - name: build and run WebSocket heartbeat test + if: matrix.tls_backend == 'openssl' + run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat + - name: build and run ThreadPool test + run: cd test && make test_thread_pool && ./test_thread_pool + + windows: + runs-on: windows-latest + if: > + (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.test_windows == 'true') + strategy: + matrix: + config: + - with_ssl: false + compiled: false + run_tests: true + name: without SSL + - with_ssl: true + compiled: false + run_tests: true + name: with SSL + - with_ssl: false + compiled: true + run_tests: false + name: compiled + name: windows ${{ matrix.config.name }} + steps: + - name: Prepare Git for Checkout on Windows + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: Checkout + uses: actions/checkout@v4 + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Setup msbuild on windows + uses: microsoft/setup-msbuild@v2 + - name: Cache vcpkg packages + id: vcpkg-cache + uses: actions/cache@v4 + with: + path: C:/vcpkg/installed + key: vcpkg-installed-windows-gtest-curl-zlib-brotli-zstd + - name: Install vcpkg dependencies + if: steps.vcpkg-cache.outputs.cache-hit != 'true' + run: vcpkg install gtest curl zlib brotli zstd + - name: Install OpenSSL + if: ${{ matrix.config.with_ssl }} + run: choco install openssl + - name: Configure CMake ${{ matrix.config.name }} + run: > + cmake -B build -S . + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake + -DHTTPLIB_TEST=ON + -DHTTPLIB_COMPILE=${{ matrix.config.compiled && 'ON' || 'OFF' }} + -DHTTPLIB_USE_OPENSSL_IF_AVAILABLE=${{ matrix.config.with_ssl && 'ON' || 'OFF' }} + -DHTTPLIB_REQUIRE_ZLIB=ON + -DHTTPLIB_REQUIRE_BROTLI=ON + -DHTTPLIB_REQUIRE_ZSTD=ON + -DHTTPLIB_REQUIRE_OPENSSL=${{ matrix.config.with_ssl && 'ON' || 'OFF' }} + - name: Build ${{ matrix.config.name }} + run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine + - name: Run tests ${{ matrix.config.name }} + if: ${{ matrix.config.run_tests }} + shell: pwsh + working-directory: build/test + run: | + $shards = 4 + $procs = @() + for ($i = 0; $i -lt $shards; $i++) { + $log = "shard_${i}.log" + $procs += Start-Process -FilePath ./Release/httplib-test.exe ` + -ArgumentList "--gtest_color=yes","--gtest_filter=${{ github.event.inputs.gtest_filter || '*' }}" ` + -NoNewWindow -PassThru -RedirectStandardOutput $log -RedirectStandardError "${log}.err" ` + -Environment @{ GTEST_TOTAL_SHARDS="$shards"; GTEST_SHARD_INDEX="$i" } + } + $procs | Wait-Process + $failed = $false + for ($i = 0; $i -lt $shards; $i++) { + $log = "shard_${i}.log" + if (Select-String -Path $log -Pattern "\[ PASSED \]" -Quiet) { + $passed = (Select-String -Path $log -Pattern "\[ PASSED \]").Line + Write-Host "Shard ${i}: $passed" + } else { + Write-Host "=== Shard $i FAILED ===" + Get-Content $log + if (Test-Path "${log}.err") { Get-Content "${log}.err" } + $failed = $true + } + } + if ($failed) { exit 1 } + Write-Host "All shards passed." + + env: + VCPKG_ROOT: "C:/vcpkg" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" diff --git a/.github/workflows/test_benchmark.yaml b/.github/workflows/test_benchmark.yaml new file mode 100644 index 0000000000..e2c0b01c1a --- /dev/null +++ b/.github/workflows/test_benchmark.yaml @@ -0,0 +1,79 @@ +name: benchmark + +on: + push: + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + ubuntu: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + steps: + - name: checkout + uses: actions/checkout@v4 + - name: build and run + run: cd test && make test_benchmark && ./test_benchmark + + macos: + runs-on: macos-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + steps: + - name: checkout + uses: actions/checkout@v4 + - name: build and run + run: cd test && make test_benchmark && ./test_benchmark + + windows: + runs-on: windows-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + steps: + - name: Prepare Git for Checkout on Windows + run: | + git config --global core.autocrlf false + git config --global core.eol lf + - name: checkout + uses: actions/checkout@v4 + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + - name: Cache vcpkg packages + id: vcpkg-cache + uses: actions/cache@v4 + with: + path: C:/vcpkg/installed + key: vcpkg-installed-windows-gtest + - name: Install vcpkg dependencies + if: steps.vcpkg-cache.outputs.cache-hit != 'true' + run: vcpkg install gtest + - name: Configure and build + shell: pwsh + run: | + $cmake_content = @" + cmake_minimum_required(VERSION 3.14) + project(httplib-benchmark CXX) + find_package(GTest REQUIRED) + add_executable(httplib-benchmark test/test_benchmark.cc) + target_include_directories(httplib-benchmark PRIVATE .) + target_link_libraries(httplib-benchmark PRIVATE GTest::gtest_main) + target_compile_options(httplib-benchmark PRIVATE "$<$:/utf-8>") + "@ + New-Item -ItemType Directory -Force -Path build_bench/test | Out-Null + Set-Content -Path build_bench/CMakeLists.txt -Value $cmake_content + Copy-Item -Path httplib.h -Destination build_bench/ + Copy-Item -Path test/test_benchmark.cc -Destination build_bench/test/ + cmake -B build_bench/build -S build_bench ` + -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + cmake --build build_bench/build --config Release + - name: Run with retry + run: ctest --output-on-failure --test-dir build_bench/build -C Release --repeat until-pass:5 + env: + VCPKG_ROOT: "C:/vcpkg" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" diff --git a/.github/workflows/test_no_exceptions.yaml b/.github/workflows/test_no_exceptions.yaml new file mode 100644 index 0000000000..f525297158 --- /dev/null +++ b/.github/workflows/test_no_exceptions.yaml @@ -0,0 +1,20 @@ +name: No Exceptions Test + +on: [push, pull_request] + +jobs: + test-no-exceptions: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + + steps: + - uses: actions/checkout@v3 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev + + - name: Run tests with CPPHTTPLIB_NO_EXCEPTIONS + run: | + cd test && make test_split EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS" && make test_openssl_parallel EXTRA_CXXFLAGS="-fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS" diff --git a/.github/workflows/test_offline.yaml b/.github/workflows/test_offline.yaml new file mode 100644 index 0000000000..42c4495911 --- /dev/null +++ b/.github/workflows/test_offline.yaml @@ -0,0 +1,62 @@ +name: test_offline + +on: + push: + pull_request: + workflow_dispatch: + inputs: + test_linux: + description: 'Test on Linux' + type: boolean + default: true + +concurrency: + group: ${{ github.workflow }}-${{ github.ref || github.run_id }} + cancel-in-progress: true + +env: + GTEST_FILTER: "-*.*_Online" + +jobs: + ubuntu: + runs-on: ubuntu-latest + if: > + (github.event_name == 'push') || + (github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) || + (github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true') + strategy: + matrix: + tls_backend: [openssl, no-tls] + name: ubuntu (${{ matrix.tls_backend }}) + steps: + - name: checkout + uses: actions/checkout@v4 + - name: install common libraries + run: | + sudo apt-get update + sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev + - name: install OpenSSL + if: matrix.tls_backend == 'openssl' + run: sudo apt-get install -y libssl-dev + - name: disable network + run: | + sudo iptables -A OUTPUT -o lo -j ACCEPT + sudo iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + sudo iptables -A OUTPUT -j REJECT + sudo ip6tables -A OUTPUT -o lo -j ACCEPT + sudo ip6tables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT + sudo ip6tables -A OUTPUT -j REJECT + - name: build and run tests (OpenSSL) + if: matrix.tls_backend == 'openssl' + run: cd test && make test_split && make test_openssl_parallel + env: + LSAN_OPTIONS: suppressions=lsan_suppressions.txt + - name: build and run tests (No TLS) + if: matrix.tls_backend == 'no-tls' + run: cd test && make test_no_tls_parallel + - name: restore network + if: always() + run: | + sudo iptables -F OUTPUT + sudo ip6tables -F OUTPUT diff --git a/.github/workflows/test_proxy.yaml b/.github/workflows/test_proxy.yaml new file mode 100644 index 0000000000..cf09432a02 --- /dev/null +++ b/.github/workflows/test_proxy.yaml @@ -0,0 +1,37 @@ +name: Proxy Test + +on: [push, pull_request] + +jobs: + test-proxy: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name + strategy: + matrix: + tls_backend: [openssl, mbedtls] + name: proxy (${{ matrix.tls_backend }}) + + steps: + - uses: actions/checkout@v4 + + - name: Install common dependencies + run: | + sudo apt-get update + sudo apt-get install -y build-essential zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev netcat-openbsd + - name: Install OpenSSL + if: matrix.tls_backend == 'openssl' + run: sudo apt-get install -y libssl-dev + - name: Install Mbed TLS + if: matrix.tls_backend == 'mbedtls' + run: sudo apt-get install -y libmbedtls-dev + + - name: Run proxy tests (OpenSSL) + if: matrix.tls_backend == 'openssl' + run: cd test && make proxy + env: + COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml + - name: Run proxy tests (Mbed TLS) + if: matrix.tls_backend == 'mbedtls' + run: cd test && make proxy_mbedtls + env: + COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4a6580bd5b..9dca1dacf3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,71 @@ tags +AGENTS.md +docs-src/pages/AGENTS.md +plans/ +# Ignore executables (no extension) but not source files example/server +!example/server.* example/client +!example/client.* example/hello +!example/hello.* +example/simplecli +!example/simplecli.* example/simplesvr +!example/simplesvr.* example/benchmark +!example/benchmark.* example/redirect +!example/redirect.* +example/ssecli +!example/ssecli.* +example/ssecli-stream +!example/ssecli-stream.* +example/ssesvr +!example/ssesvr.* +example/upload +!example/upload.* +example/one_time_request +!example/one_time_request.* +example/server_and_client +!example/server_and_client.* +example/accept_header +!example/accept_header.* +example/wsecho +!example/wsecho.* example/*.pem +test/httplib.cc +test/httplib.h test/test +test/test_mbedtls +test/test_wolfssl +test/test_no_tls +test/server_fuzzer +test/test_proxy +test/test_proxy_mbedtls +test/test_proxy_wolfssl +test/test_split +test/test_split_mbedtls +test/test_split_wolfssl +test/test_split_no_tls +test/test_websocket_heartbeat +test/test_thread_pool +test/test_benchmark test/test.xcodeproj/xcuser* test/test.xcodeproj/*/xcuser* +test/*.o test/*.pem test/*.srl +test/*.log +test/_build_* +work/ +benchmark/server* +docs-gen/target/ *.swp +build/ Debug Release *.vcxproj.user @@ -24,5 +75,7 @@ Release *.db ipch *.dSYM +*.pyc .* -!/.travis.yml +!/.gitattributes +!/.github diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..0c3e54346f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.8 # 最新バージョンを使用 + hooks: + - id: clang-format + files: \.(cpp|cc|h)$ + args: [-i] # インプレースで修正 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3727d8aef0..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Environment -language: cpp -os: - - osx - -# Compiler selection -compiler: - - clang - -# Build/test steps -script: - - cd ${TRAVIS_BUILD_DIR}/test - - make all diff --git a/CMakeLists.txt b/CMakeLists.txt index a06cf358d8..1874e36be0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,29 +1,464 @@ -cmake_minimum_required(VERSION 3.7.0) -project(httplib) +#[[ + Build options: + * Standard BUILD_SHARED_LIBS is supported and sets HTTPLIB_SHARED default value. + * HTTPLIB_USE_OPENSSL_IF_AVAILABLE (default on) + * HTTPLIB_USE_WOLFSSL_IF_AVAILABLE (default off) + * HTTPLIB_USE_MBEDTLS_IF_AVAILABLE (default off) + * HTTPLIB_USE_ZLIB_IF_AVAILABLE (default on) + * HTTPLIB_USE_BROTLI_IF_AVAILABLE (default on) + * HTTPLIB_USE_ZSTD_IF_AVAILABLE (default on) + * HTTPLIB_BUILD_MODULES (default off) + * HTTPLIB_REQUIRE_OPENSSL (default off) + * HTTPLIB_REQUIRE_WOLFSSL (default off) + * HTTPLIB_REQUIRE_MBEDTLS (default off) + * HTTPLIB_REQUIRE_ZLIB (default off) + * HTTPLIB_REQUIRE_BROTLI (default off) + * HTTPLIB_REQUIRE_ZSTD (default off) + * HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off) + * HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on) + * HTTPLIB_COMPILE (default off) + * HTTPLIB_INSTALL (default on) + * HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON) + * HTTPLIB_TEST (default off) + * BROTLI_USE_STATIC_LIBS - tells Cmake to use the static Brotli libs (only works if you have them installed). + * OPENSSL_USE_STATIC_LIBS - tells Cmake to use the static OpenSSL libs (only works if you have them installed). -set(CMAKE_CXX_STANDARD 11) + ------------------------------------------------------------------------------- -# Include + After installation with Cmake, a find_package(httplib COMPONENTS OpenSSL wolfssl MbedTLS ZLIB Brotli zstd) is available. + This creates a httplib::httplib target (if found and if listed components are supported). + It can be linked like so: + + target_link_libraries(your_exe httplib::httplib) + + The following will build & install for later use. + + Linux/macOS: + + mkdir -p build + cd build + cmake -DCMAKE_BUILD_TYPE=Release .. + sudo cmake --build . --target install + + Windows: + + mkdir build + cd build + cmake .. + runas /user:Administrator "cmake --build . --config Release --target install" + + ------------------------------------------------------------------------------- + + These variables are available after you run find_package(httplib) + * HTTPLIB_HEADER_PATH - this is the full path to the installed header (e.g. /usr/include/httplib.h). + * HTTPLIB_IS_USING_OPENSSL - a bool for if OpenSSL support is enabled. + * HTTPLIB_IS_USING_WOLFSSL - a bool for if wolfSSL support is enabled. + * HTTPLIB_IS_USING_MBEDTLS - a bool for if MbedTLS support is enabled. + * HTTPLIB_IS_USING_ZLIB - a bool for if ZLIB support is enabled. + * HTTPLIB_IS_USING_BROTLI - a bool for if Brotli support is enabled. + * HTTPLIB_IS_USING_ZSTD - a bool for if ZSTD support is enabled. + * HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES - a bool for if support of loading system certs from the Apple Keychain is enabled. + * HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO - a bool for if nonblocking getaddrinfo is enabled. + * HTTPLIB_IS_COMPILED - a bool for if the library is compiled, or otherwise header-only. + * HTTPLIB_INCLUDE_DIR - the root path to httplib's header (e.g. /usr/include). + * HTTPLIB_LIBRARY - the full path to the library if compiled (e.g. /usr/lib/libhttplib.so). + * httplib_VERSION or HTTPLIB_VERSION - the project's version string. + * HTTPLIB_FOUND - a bool for if the target was found. + + Want to use precompiled headers (Cmake feature since v3.16)? + It's as simple as doing the following (before linking): + + target_precompile_headers(httplib::httplib INTERFACE "${HTTPLIB_HEADER_PATH}") + + ------------------------------------------------------------------------------- + + ARCH_INDEPENDENT option of write_basic_package_version_file() requires Cmake v3.14 +]] +cmake_minimum_required(VERSION 3.14.0 FATAL_ERROR) + +# Get the CPPHTTPLIB_VERSION value and use it as a version +# This gets the string with the CPPHTTPLIB_VERSION value from the header. +# This is so the maintainer doesn't actually need to update this manually. +file(STRINGS httplib.h _raw_version_string REGEX "CPPHTTPLIB_VERSION \"([0-9]+\\.[0-9]+\\.[0-9]+)\"") + +# Extracts just the version string itself from the whole string contained in _raw_version_string +# since _raw_version_string would contain the entire line of code where it found the version string +string(REGEX MATCH "([0-9]+\\.?)+" _httplib_version "${_raw_version_string}") + +project(httplib + VERSION ${_httplib_version} + LANGUAGES CXX + DESCRIPTION "A C++ header-only HTTP/HTTPS server and client library." + HOMEPAGE_URL "https://github.com/yhirose/cpp-httplib" +) + +# Change as needed to set an OpenSSL minimum version. +# This is used in the installed Cmake config file. +set(_HTTPLIB_OPENSSL_MIN_VER "3.0.0") + +# Lets you disable C++ exception during CMake configure time. +# The value is used in the install CMake config file. +option(HTTPLIB_NO_EXCEPTIONS "Disable the use of C++ exceptions" OFF) +# Allow for a build to require OpenSSL to pass, instead of just being optional +option(HTTPLIB_REQUIRE_OPENSSL "Requires OpenSSL to be found & linked, or fails build." OFF) +option(HTTPLIB_REQUIRE_WOLFSSL "Requires wolfSSL to be found & linked, or fails build." OFF) +option(HTTPLIB_REQUIRE_MBEDTLS "Requires MbedTLS to be found & linked, or fails build." OFF) +option(HTTPLIB_REQUIRE_ZLIB "Requires ZLIB to be found & linked, or fails build." OFF) +# Allow for a build to casually enable OpenSSL/ZLIB support, but silently continue if not found. +# Make these options so their automatic use can be specifically disabled (as needed) +option(HTTPLIB_USE_OPENSSL_IF_AVAILABLE "Uses OpenSSL (if available) to enable HTTPS support." ON) +option(HTTPLIB_USE_WOLFSSL_IF_AVAILABLE "Uses wolfSSL (if available) to enable HTTPS support." OFF) +option(HTTPLIB_USE_MBEDTLS_IF_AVAILABLE "Uses MbedTLS (if available) to enable HTTPS support." OFF) +option(HTTPLIB_USE_ZLIB_IF_AVAILABLE "Uses ZLIB (if available) to enable Zlib compression support." ON) +# Lets you compile the program as a regular library instead of header-only +option(HTTPLIB_COMPILE "If ON, uses a Python script to split the header into a compilable header & source file (requires Python v3)." OFF) +# Lets you disable the installation (useful when fetched from another CMake project) +option(HTTPLIB_INSTALL "Enables the installation target" ON) +option(HTTPLIB_TEST "Enables testing and builds tests" OFF) +option(HTTPLIB_REQUIRE_BROTLI "Requires Brotli to be found & linked, or fails build." OFF) +option(HTTPLIB_USE_BROTLI_IF_AVAILABLE "Uses Brotli (if available) to enable Brotli decompression support." ON) +option(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES "Disable loading system certs from the Apple Keychain on macOS." OFF) +option(HTTPLIB_USE_NON_BLOCKING_GETADDRINFO "Enables the non-blocking alternatives for getaddrinfo." ON) +option(HTTPLIB_REQUIRE_ZSTD "Requires ZSTD to be found & linked, or fails build." OFF) +option(HTTPLIB_USE_ZSTD_IF_AVAILABLE "Uses ZSTD (if available) to enable zstd support." ON) +# C++20 modules support requires CMake 3.28 or later +if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28") + option(HTTPLIB_BUILD_MODULES "Build httplib modules (requires HTTPLIB_COMPILE to be ON)." OFF) +else() + set(HTTPLIB_BUILD_MODULES OFF CACHE INTERNAL "Build httplib modules disabled (requires CMake 3.28+)" FORCE) + if(DEFINED CACHE{HTTPLIB_BUILD_MODULES} AND HTTPLIB_BUILD_MODULES) + message(WARNING "HTTPLIB_BUILD_MODULES requires CMake 3.28 or later. Current version is ${CMAKE_VERSION}. Modules support has been disabled.") + endif() +endif() + +# Incompatibility between TLS libraries +set(TLS_LIBRARIES_USED_TMP 0) + +foreach(tls_library OPENSSL WOLFSSL MBEDTLS) + set(TLS_REQUIRED ${HTTPLIB_REQUIRE_${tls_library}}) + set(TLS_IF_AVAILABLE ${HTTPLIB_USE_${tls_library}_IF_AVAILABLE}) + + if(TLS_REQUIRED OR TLS_IF_AVAILABLE) + math(EXPR TLS_LIBRARIES_USED_TMP "${TLS_LIBRARIES_USED_TMP} + 1") + endif() +endforeach() + +if(TLS_LIBRARIES_USED_TMP GREATER 1) + message(FATAL_ERROR "TLS libraries are mutually exclusive.") +endif() + +# Defaults to static library but respects standard BUILD_SHARED_LIBS if set +include(CMakeDependentOption) +cmake_dependent_option(HTTPLIB_SHARED "Build the library as a shared library instead of static. Has no effect if using header-only." + "${BUILD_SHARED_LIBS}" HTTPLIB_COMPILE OFF +) +if(HTTPLIB_SHARED) + set(HTTPLIB_LIB_TYPE SHARED) + if(WIN32) + # Necessary for Windows if building shared libs + # See https://stackoverflow.com/a/40743080 + set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) + endif() +else() + set(HTTPLIB_LIB_TYPE STATIC) +endif() + +if(CMAKE_SYSTEM_NAME MATCHES "Windows") + if(CMAKE_SYSTEM_VERSION) + if(${CMAKE_SYSTEM_VERSION} VERSION_LESS "10.0.0") + message(WARNING "Windows ${CMAKE_SYSTEM_VERSION} or lower is not supported. Please use Windows 10 or later.") + endif() + else() + set(CMAKE_SYSTEM_VERSION "10.0.19041.0") + message(WARNING "The target is Windows but CMAKE_SYSTEM_VERSION is not set, the default system version is set to Windows 10.") + endif() +endif() + +# Set some variables that are used in-tree and while building based on our options +set(HTTPLIB_IS_COMPILED ${HTTPLIB_COMPILE}) +set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES TRUE) +if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) + set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE) +endif() +set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}) + +# Threads needed for on some systems, and for on Linux +set(THREADS_PREFER_PTHREAD_FLAG TRUE) +find_package(Threads REQUIRED) +# Since Cmake v3.11, Crypto & SSL became optional when not specified as COMPONENTS. +if(HTTPLIB_REQUIRE_OPENSSL) + find_package(OpenSSL ${_HTTPLIB_OPENSSL_MIN_VER} COMPONENTS Crypto SSL REQUIRED) + set(HTTPLIB_IS_USING_OPENSSL TRUE) +elseif(HTTPLIB_USE_OPENSSL_IF_AVAILABLE) + find_package(OpenSSL ${_HTTPLIB_OPENSSL_MIN_VER} COMPONENTS Crypto SSL QUIET) + # Avoid a rare circumstance of not finding all components but the end-user did their + # own call for OpenSSL, which might trick us into thinking we'd otherwise have what we wanted + if (TARGET OpenSSL::SSL AND TARGET OpenSSL::Crypto) + set(HTTPLIB_IS_USING_OPENSSL ${OPENSSL_FOUND}) + else() + set(HTTPLIB_IS_USING_OPENSSL FALSE) + endif() +endif() + +if(HTTPLIB_REQUIRE_WOLFSSL) + find_package(wolfssl REQUIRED) + set(HTTPLIB_IS_USING_WOLFSSL TRUE) +elseif(HTTPLIB_USE_WOLFSSL_IF_AVAILABLE) + find_package(wolfssl QUIET) + set(HTTPLIB_IS_USING_WOLFSSL ${wolfssl_FOUND}) +endif() + +if(HTTPLIB_REQUIRE_MBEDTLS) + find_package(MbedTLS REQUIRED) + set(HTTPLIB_IS_USING_MBEDTLS TRUE) +elseif(HTTPLIB_USE_MBEDTLS_IF_AVAILABLE) + find_package(MbedTLS QUIET) + set(HTTPLIB_IS_USING_MBEDTLS ${MbedTLS_FOUND}) +endif() + +if(HTTPLIB_REQUIRE_ZLIB) + find_package(ZLIB REQUIRED) + set(HTTPLIB_IS_USING_ZLIB TRUE) +elseif(HTTPLIB_USE_ZLIB_IF_AVAILABLE) + find_package(ZLIB QUIET) + # FindZLIB doesn't have a ZLIB_FOUND variable, so check the target. + if(TARGET ZLIB::ZLIB) + set(HTTPLIB_IS_USING_ZLIB TRUE) + endif() +endif() + +# Adds our cmake folder to the search path for find_package +# This is so we can use our custom FindBrotli.cmake +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +if(HTTPLIB_REQUIRE_BROTLI) + find_package(Brotli COMPONENTS encoder decoder common REQUIRED) + set(HTTPLIB_IS_USING_BROTLI TRUE) +elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE) + find_package(Brotli COMPONENTS encoder decoder common QUIET) + set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND}) +endif() + +# NOTE: +# zstd < 1.5.6 does not provide the CMake imported target `zstd::libzstd`. +# Older versions must be consumed via their pkg-config file. +if(HTTPLIB_REQUIRE_ZSTD) + find_package(zstd 1.5.6 CONFIG) + if(NOT zstd_FOUND) + find_package(PkgConfig REQUIRED) + pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd) + add_library(zstd::libzstd ALIAS PkgConfig::zstd) + endif() + set(HTTPLIB_IS_USING_ZSTD TRUE) +elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE) + find_package(zstd 1.5.6 CONFIG QUIET) + if(NOT zstd_FOUND) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(zstd QUIET IMPORTED_TARGET libzstd) + + if(TARGET PkgConfig::zstd) + add_library(zstd::libzstd ALIAS PkgConfig::zstd) + endif() + endif() + endif() + # Both find_package and PkgConf set a XXX_FOUND var + set(HTTPLIB_IS_USING_ZSTD ${zstd_FOUND}) +endif() + +# Used for default, common dirs that the end-user can change (if needed) +# like CMAKE_INSTALL_INCLUDEDIR or CMAKE_INSTALL_DATADIR include(GNUInstallDirs) -include(ExternalProject) -add_library(${PROJECT_NAME} INTERFACE) -target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_11) +if(HTTPLIB_COMPILE) + # Put the split script into the build dir + configure_file(split.py "${CMAKE_CURRENT_BINARY_DIR}/split.py" + COPYONLY + ) + # Needs to be in the same dir as the python script + configure_file(httplib.h "${CMAKE_CURRENT_BINARY_DIR}/httplib.h" + COPYONLY + ) + + # Used outside of this if-else + set(_INTERFACE_OR_PUBLIC PUBLIC) + # Brings in the Python3_EXECUTABLE path we can use. + find_package(Python3 REQUIRED) + # Actually split the file + # Keeps the output in the build dir to not pollute the main dir + execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/split.py" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ERROR_VARIABLE _httplib_split_error + ) + if(_httplib_split_error) + message(FATAL_ERROR "Failed when trying to split cpp-httplib with the Python script.\n${_httplib_split_error}") + endif() + + # If building modules, also generate the module file + if(HTTPLIB_BUILD_MODULES) + # Put the generate_module script into the build dir + configure_file(generate_module.py "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py" + COPYONLY + ) + # Generate the module file + execute_process(COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_BINARY_DIR}/generate_module.py" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ERROR_VARIABLE _httplib_module_error + ) + if(_httplib_module_error) + message(FATAL_ERROR "Failed when trying to generate cpp-httplib module with the Python script.\n${_httplib_module_error}") + endif() + endif() + + # split.py puts output in "out" + set(_httplib_build_includedir "${CMAKE_CURRENT_BINARY_DIR}/out") + add_library(${PROJECT_NAME} ${HTTPLIB_LIB_TYPE} "${_httplib_build_includedir}/httplib.cc") + target_sources(${PROJECT_NAME} + PUBLIC + $ + $ + ) + + # Add C++20 module support if requested + # Include from separate file to prevent parse errors on older CMake versions + if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.28") + include(cmake/modules.cmake) + endif() + + set_target_properties(${PROJECT_NAME} + PROPERTIES + VERSION ${${PROJECT_NAME}_VERSION} + SOVERSION "${${PROJECT_NAME}_VERSION_MAJOR}.${${PROJECT_NAME}_VERSION_MINOR}" + OUTPUT_NAME cpp-httplib + ) +else() + # This is for header-only. + set(_INTERFACE_OR_PUBLIC INTERFACE) + add_library(${PROJECT_NAME} INTERFACE) + set(_httplib_build_includedir "${CMAKE_CURRENT_SOURCE_DIR}") +endif() +# Lets you address the target with httplib::httplib +# Only useful if building in-tree, versus using it from an installation. +add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +# Require C++11, or C++20 if modules are enabled +if(HTTPLIB_BUILD_MODULES) + target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_20) +else() + target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11) +endif() + +target_include_directories(${PROJECT_NAME} SYSTEM ${_INTERFACE_OR_PUBLIC} + $ + $ +) + +target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} + # Always require threads + Threads::Threads + # Needed for Windows libs on Mingw, as the pragma comment(lib, "xyz") aren't triggered. + $<$:ws2_32> + $<$:crypt32> + # Needed for API from MacOS Security framework + "$<$,$,$>:-framework CFNetwork -framework CoreFoundation -framework Security>" + # Needed for non-blocking getaddrinfo on MacOS + "$<$,$>:-framework CFNetwork -framework CoreFoundation>" + # Can't put multiple targets in a single generator expression or it bugs out. + $<$:Brotli::common> + $<$:Brotli::encoder> + $<$:Brotli::decoder> + $<$:ZLIB::ZLIB> + $<$:zstd::libzstd> + $<$:OpenSSL::SSL> + $<$:OpenSSL::Crypto> + $<$:wolfssl::wolfssl> + $<$:MbedTLS::mbedtls> +) + +# Set the definitions to enable optional features +target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} + $<$:CPPHTTPLIB_NO_EXCEPTIONS> + $<$:CPPHTTPLIB_BROTLI_SUPPORT> + $<$:CPPHTTPLIB_ZLIB_SUPPORT> + $<$:CPPHTTPLIB_ZSTD_SUPPORT> + $<$:CPPHTTPLIB_OPENSSL_SUPPORT> + $<$:CPPHTTPLIB_WOLFSSL_SUPPORT> + $<$:CPPHTTPLIB_MBEDTLS_SUPPORT> + $<$,$>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES> + $<$:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO> +) + +# CMake configuration files installation directory +set(_TARGET_INSTALL_CMAKEDIR "${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}") + +include(CMakePackageConfigHelpers) + +# Configures the meta-file httplibConfig.cmake.in to replace variables with paths/values/etc. +configure_package_config_file("cmake/${PROJECT_NAME}Config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + INSTALL_DESTINATION "${_TARGET_INSTALL_CMAKEDIR}" + # Passes the includedir install path + PATH_VARS CMAKE_INSTALL_FULL_INCLUDEDIR +) + +if(HTTPLIB_COMPILE) + write_basic_package_version_file("${PROJECT_NAME}ConfigVersion.cmake" + # Example: if you find_package(httplib 0.5.4) + # then anything >= 0.5.4 and < 0.6 is accepted + COMPATIBILITY SameMinorVersion + ) +else() + write_basic_package_version_file("${PROJECT_NAME}ConfigVersion.cmake" + # Example: if you find_package(httplib 0.5.4) + # then anything >= 0.5.4 and < 0.6 is accepted + COMPATIBILITY SameMinorVersion + # Tells Cmake that it's a header-only lib + # Mildly useful for end-users :) + ARCH_INDEPENDENT + ) +endif() + +if(HTTPLIB_INSTALL) + # Creates the export httplibTargets.cmake + # This is strictly what holds compilation requirements + # and linkage information (doesn't find deps though). + if(HTTPLIB_BUILD_MODULES) + install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets FILE_SET CXX_MODULES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/httplib/modules CXX_MODULES_BMI DESTINATION ${CMAKE_INSTALL_LIBDIR}/httplib/modules) + else() + install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets) + endif() + + install(FILES "${_httplib_build_includedir}/httplib.h" TYPE INCLUDE) -target_include_directories(${PROJECT_NAME} INTERFACE - $ - $) + install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake" + # Install it so it can be used later by the httplibConfig.cmake file. + # Put it in the same dir as our config file instead of a global path so we don't potentially stomp on other packages. + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/FindBrotli.cmake" + DESTINATION ${_TARGET_INSTALL_CMAKEDIR} + ) -install(TARGETS ${PROJECT_NAME} EXPORT httplibConfig - ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) + # NOTE: This path changes depending on if it's on Windows or Linux + install(EXPORT httplibTargets + # Puts the targets into the httplib namespace + # So this makes httplib::httplib linkable after doing find_package(httplib) + NAMESPACE ${PROJECT_NAME}:: + DESTINATION ${_TARGET_INSTALL_CMAKEDIR} + ) -install(FILES httplib.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME}) + # Install documentation & license + # ex: /usr/share/doc/httplib/README.md and /usr/share/licenses/httplib/LICENSE + install(FILES "README.md" DESTINATION "${CMAKE_INSTALL_DOCDIR}") + install(FILES "LICENSE" DESTINATION "${CMAKE_INSTALL_DATADIR}/licenses/${PROJECT_NAME}") -install(EXPORT httplibConfig DESTINATION share/httplib/cmake) + include(CPack) +endif() -export(TARGETS ${PROJECT_NAME} FILE httplibConfig.cmake) +if(HTTPLIB_BUILD_MODULES AND NOT HTTPLIB_COMPILE) + message(FATAL_ERROR "HTTPLIB_BUILD_MODULES requires HTTPLIB_COMPILE to be ON.") +endif() -#add_subdirectory(example) -#add_subdirectory(test) \ No newline at end of file +if(HTTPLIB_TEST) + include(CTest) + add_subdirectory(test) +endif() diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..3495b42f24 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,13 @@ +FROM yhirose4dockerhub/ubuntu-builder AS builder +WORKDIR /build +COPY httplib.h . +COPY docker/main.cc . +RUN g++ -std=c++23 -static -o server -O2 -I. main.cc && strip server + +FROM scratch +COPY --from=builder /build/server /server +COPY docker/html/index.html /html/index.html +EXPOSE 80 + +ENTRYPOINT ["/server"] +CMD ["--host", "0.0.0.0", "--port", "80", "--mount", "/:./html"] diff --git a/README-sse.md b/README-sse.md new file mode 100644 index 0000000000..223dcc32e6 --- /dev/null +++ b/README-sse.md @@ -0,0 +1,204 @@ +# SSEClient - Server-Sent Events Client + +A simple, EventSource-like SSE client for C++11. + +## Features + +- **Auto-reconnect**: Automatically reconnects on connection loss +- **Last-Event-ID**: Sends last received ID on reconnect for resumption +- **retry field**: Respects server's reconnect interval +- **Event types**: Supports custom event types via `on_event()` +- **Async support**: Run in background thread with `start_async()` +- **C++11 compatible**: No C++14/17/20 features required + +## Quick Start + +```cpp +httplib::Client cli("http://localhost:8080"); +httplib::sse::SSEClient sse(cli, "/events"); + +sse.on_message([](const httplib::sse::SSEMessage &msg) { + std::cout << "Event: " << msg.event << std::endl; + std::cout << "Data: " << msg.data << std::endl; +}); + +sse.start(); // Blocking, with auto-reconnect +``` + +## API Reference + +### SSEMessage + +```cpp +struct SSEMessage { + std::string event; // Event type (default: "message") + std::string data; // Event payload + std::string id; // Event ID +}; +``` + +### SSEClient + +#### Constructor + +```cpp +// Basic +SSEClient(Client &client, const std::string &path); + +// With custom headers +SSEClient(Client &client, const std::string &path, const Headers &headers); +``` + +#### Event Handlers + +```cpp +// Called for all events (or events without a specific handler) +sse.on_message([](const SSEMessage &msg) { }); + +// Called for specific event types +sse.on_event("update", [](const SSEMessage &msg) { }); +sse.on_event("delete", [](const SSEMessage &msg) { }); + +// Called when connection is established +sse.on_open([]() { }); + +// Called on connection errors +sse.on_error([](httplib::Error err) { }); +``` + +#### Configuration + +```cpp +// Set reconnect interval (default: 3000ms) +sse.set_reconnect_interval(5000); + +// Set max reconnect attempts (default: 0 = unlimited) +sse.set_max_reconnect_attempts(10); + +// Update headers at any time (thread-safe) +sse.set_headers({{"Authorization", "Bearer new_token"}}); +``` + +#### Control + +```cpp +// Blocking start with auto-reconnect +sse.start(); + +// Non-blocking start (runs in background thread) +sse.start_async(); + +// Stop the client (thread-safe) +sse.stop(); +``` + +#### State + +```cpp +bool connected = sse.is_connected(); +const std::string &id = sse.last_event_id(); +``` + +## Examples + +### Basic Usage + +```cpp +httplib::Client cli("http://localhost:8080"); +httplib::sse::SSEClient sse(cli, "/events"); + +sse.on_message([](const httplib::sse::SSEMessage &msg) { + std::cout << msg.data << std::endl; +}); + +sse.start(); +``` + +### With Custom Event Types + +```cpp +httplib::sse::SSEClient sse(cli, "/events"); + +sse.on_event("notification", [](const httplib::sse::SSEMessage &msg) { + std::cout << "Notification: " << msg.data << std::endl; +}); + +sse.on_event("update", [](const httplib::sse::SSEMessage &msg) { + std::cout << "Update: " << msg.data << std::endl; +}); + +sse.start(); +``` + +### Async with Stop + +```cpp +httplib::sse::SSEClient sse(cli, "/events"); + +sse.on_message([](const httplib::sse::SSEMessage &msg) { + std::cout << msg.data << std::endl; +}); + +sse.start_async(); // Returns immediately + +// ... do other work ... + +sse.stop(); // Stop when done +``` + +### With Custom Headers (e.g., Authentication) + +```cpp +httplib::Headers headers = { + {"Authorization", "Bearer token123"} +}; + +httplib::sse::SSEClient sse(cli, "/events", headers); +sse.start(); +``` + +### Refreshing Auth Token on Reconnect + +```cpp +httplib::sse::SSEClient sse(cli, "/events", + {{"Authorization", "Bearer " + get_token()}}); + +// Preemptively refresh token on each successful connection +sse.on_open([&sse]() { + sse.set_headers({{"Authorization", "Bearer " + get_token()}}); +}); + +// Or reactively refresh on auth failure (401 triggers reconnect) +sse.on_error([&sse](httplib::Error) { + sse.set_headers({{"Authorization", "Bearer " + refresh_token()}}); +}); + +sse.start(); +``` + +### Error Handling + +```cpp +sse.on_error([](httplib::Error err) { + std::cerr << "Error: " << httplib::to_string(err) << std::endl; +}); + +sse.set_reconnect_interval(1000); +sse.set_max_reconnect_attempts(5); + +sse.start(); +``` + +## SSE Protocol + +The client parses SSE format according to the [W3C specification](https://html.spec.whatwg.org/multipage/server-sent-events.html): + +``` +event: custom-type +id: 123 +data: {"message": "hello"} + +data: simple message + +: this is a comment (ignored) +``` diff --git a/README-stream.md b/README-stream.md new file mode 100644 index 0000000000..b7f41620d8 --- /dev/null +++ b/README-stream.md @@ -0,0 +1,317 @@ +# cpp-httplib Streaming API + +This document describes the streaming extensions for cpp-httplib, providing an iterator-style API for handling HTTP responses incrementally with **true socket-level streaming**. + +> **Important Notes**: +> +> - **No Keep-Alive**: Each `stream::Get()` call uses a dedicated connection that is closed after the response is fully read. For connection reuse, use `Client::Get()`. +> - **Single iteration only**: The `next()` method can only iterate through the body once. +> - **Result is not thread-safe**: While `stream::Get()` can be called from multiple threads simultaneously, the returned `stream::Result` must be used from a single thread only. + +## Overview + +The streaming API allows you to process HTTP response bodies chunk by chunk using an iterator-style pattern. Data is read directly from the network socket, enabling low-memory processing of large responses. This is particularly useful for: + +- **LLM/AI streaming responses** (e.g., ChatGPT, Claude, Ollama) +- **Server-Sent Events (SSE)** +- **Large file downloads** with progress tracking +- **Reverse proxy implementations** + +## Quick Start + +```cpp +#include "httplib.h" + +int main() { + httplib::Client cli("http://localhost:8080"); + + // Get streaming response + auto result = httplib::stream::Get(cli, "/stream"); + + if (result) { + // Process response body in chunks + while (result.next()) { + std::cout.write(result.data(), result.size()); + } + } + + return 0; +} +``` + +## API Layers + +cpp-httplib provides multiple API layers for different use cases: + +```text +┌─────────────────────────────────────────────┐ +│ SSEClient │ ← SSE-specific, parsed events +│ - on_message(), on_event() │ +│ - Auto-reconnect, Last-Event-ID │ +├─────────────────────────────────────────────┤ +│ stream::Get() / stream::Result │ ← Iterator-based streaming +│ - while (result.next()) { ... } │ +├─────────────────────────────────────────────┤ +│ open_stream() / StreamHandle │ ← General-purpose streaming +│ - handle.read(buf, len) │ +├─────────────────────────────────────────────┤ +│ Client::Get() │ ← Traditional, full buffering +└─────────────────────────────────────────────┘ +``` + +| Use Case | Recommended API | +|----------|----------------| +| SSE with auto-reconnect | `SSEClient` (see [README-sse.md](README-sse.md)) | +| LLM streaming (JSON Lines) | `stream::Get()` | +| Large file download | `stream::Get()` or `open_stream()` | +| Reverse proxy | `open_stream()` | +| Small responses with Keep-Alive | `Client::Get()` | + +## API Reference + +### Low-Level API: `StreamHandle` + +The `StreamHandle` struct provides direct control over streaming responses. It takes ownership of the socket connection and reads data directly from the network. + +> **Note:** When using `open_stream()`, the connection is dedicated to streaming and **Keep-Alive is not supported**. For Keep-Alive connections, use `client.Get()` instead. + +```cpp +// Open a stream (takes ownership of socket) +httplib::Client cli("http://localhost:8080"); +auto handle = cli.open_stream("GET", "/path"); + +// Check validity +if (handle.is_valid()) { + // Access response headers immediately + int status = handle.response->status; + auto content_type = handle.response->get_header_value("Content-Type"); + + // Read body incrementally + char buf[4096]; + ssize_t n; + while ((n = handle.read(buf, sizeof(buf))) > 0) { + process(buf, n); + } +} +``` + +#### StreamHandle Members + +| Member | Type | Description | +|--------|------|-------------| +| `response` | `std::unique_ptr` | HTTP response with headers | +| `error` | `Error` | Error code if request failed | +| `is_valid()` | `bool` | Returns true if response is valid | +| `read(buf, len)` | `ssize_t` | Read up to `len` bytes directly from socket | +| `get_read_error()` | `Error` | Get the last read error | +| `has_read_error()` | `bool` | Check if a read error occurred | + +### High-Level API: `stream::Get()` and `stream::Result` + +The `httplib.h` header provides a more ergonomic iterator-style API. + +```cpp +#include "httplib.h" + +httplib::Client cli("http://localhost:8080"); +cli.set_follow_location(true); +... + +// Simple GET +auto result = httplib::stream::Get(cli, "/path"); + +// GET with custom headers +httplib::Headers headers = {{"Authorization", "Bearer token"}}; +auto result = httplib::stream::Get(cli, "/path", headers); + +// Process the response +if (result) { + while (result.next()) { + process(result.data(), result.size()); + } +} + +// Or read entire body at once +auto result2 = httplib::stream::Get(cli, "/path"); +if (result2) { + std::string body = result2.read_all(); +} +``` + +#### stream::Result Members + +| Member | Type | Description | +|--------|------|-------------| +| `operator bool()` | `bool` | Returns true if response is valid | +| `is_valid()` | `bool` | Same as `operator bool()` | +| `status()` | `int` | HTTP status code | +| `headers()` | `const Headers&` | Response headers | +| `get_header_value(key, def)` | `std::string` | Get header value (with optional default) | +| `has_header(key)` | `bool` | Check if header exists | +| `next()` | `bool` | Read next chunk, returns false when done | +| `data()` | `const char*` | Pointer to current chunk data | +| `size()` | `size_t` | Size of current chunk | +| `read_all()` | `std::string` | Read entire remaining body into string | +| `error()` | `Error` | Get the connection/request error | +| `read_error()` | `Error` | Get the last read error | +| `has_read_error()` | `bool` | Check if a read error occurred | + +## Usage Examples + +### Example 1: SSE (Server-Sent Events) Client + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Client cli("http://localhost:1234"); + + auto result = httplib::stream::Get(cli, "/events"); + if (!result) { return 1; } + + while (result.next()) { + std::cout.write(result.data(), result.size()); + std::cout.flush(); + } + + return 0; +} +``` + +For a complete SSE client with auto-reconnection and event parsing, see `example/ssecli-stream.cc`. + +### Example 2: LLM Streaming Response + +```cpp +#include "httplib.h" +#include + +int main() { + httplib::Client cli("http://localhost:11434"); // Ollama + + auto result = httplib::stream::Get(cli, "/api/generate"); + + if (result && result.status() == 200) { + while (result.next()) { + std::cout.write(result.data(), result.size()); + std::cout.flush(); + } + } + + // Check for connection errors + if (result.read_error() != httplib::Error::Success) { + std::cerr << "Connection lost\n"; + } + + return 0; +} +``` + +### Example 3: Large File Download with Progress + +```cpp +#include "httplib.h" +#include +#include + +int main() { + httplib::Client cli("http://example.com"); + auto result = httplib::stream::Get(cli, "/large-file.zip"); + + if (!result || result.status() != 200) { + std::cerr << "Download failed\n"; + return 1; + } + + std::ofstream file("download.zip", std::ios::binary); + size_t total = 0; + + while (result.next()) { + file.write(result.data(), result.size()); + total += result.size(); + std::cout << "\rDownloaded: " << (total / 1024) << " KB" << std::flush; + } + + std::cout << "\nComplete!\n"; + return 0; +} +``` + +### Example 4: Reverse Proxy Streaming + +```cpp +#include "httplib.h" + +httplib::Server svr; + +svr.Get("/proxy/(.*)", [](const httplib::Request& req, httplib::Response& res) { + httplib::Client upstream("http://backend:8080"); + auto handle = upstream.open_stream("/" + req.matches[1].str()); + + if (!handle.is_valid()) { + res.status = 502; + return; + } + + res.status = handle.response->status; + res.set_chunked_content_provider( + handle.response->get_header_value("Content-Type"), + [handle = std::move(handle)](size_t, httplib::DataSink& sink) mutable { + char buf[8192]; + auto n = handle.read(buf, sizeof(buf)); + if (n > 0) { + sink.write(buf, static_cast(n)); + return true; + } + sink.done(); + return true; + } + ); +}); + +svr.listen("0.0.0.0", 3000); +``` + +## Comparison with Existing APIs + +| Feature | `Client::Get()` | `open_stream()` | `stream::Get()` | +|---------|----------------|-----------------|----------------| +| Headers available | After complete | Immediately | Immediately | +| Body reading | All at once | Direct from socket | Iterator-based | +| Memory usage | Full body in RAM | Minimal (controlled) | Minimal (controlled) | +| Keep-Alive support | ✅ Yes | ❌ No | ❌ No | +| Compression | Auto-handled | Auto-handled | Auto-handled | +| Best for | Small responses, Keep-Alive | Low-level streaming | Easy streaming | + +## Features + +- **True socket-level streaming**: Data is read directly from the network socket +- **Low memory footprint**: Only the current chunk is held in memory +- **Compression support**: Automatic decompression for gzip, brotli, and zstd +- **Chunked transfer**: Full support for chunked transfer encoding +- **SSL/TLS support**: Works with HTTPS connections + +## Important Notes + +### Keep-Alive Behavior + +The streaming API (`stream::Get()` / `open_stream()`) takes ownership of the socket connection for the duration of the stream. This means: + +- **Keep-Alive is not supported** for streaming connections +- The socket is closed when `StreamHandle` is destroyed +- For Keep-Alive scenarios, use the standard `client.Get()` API instead + +```cpp +// Use for streaming (no Keep-Alive) +auto result = httplib::stream::Get(cli, "/large-stream"); +while (result.next()) { /* ... */ } + +// Use for Keep-Alive connections +auto res = cli.Get("/api/data"); // Connection can be reused +``` + +## Related + +- [Issue #2269](https://github.com/yhirose/cpp-httplib/issues/2269) - Original feature request +- [example/ssecli-stream.cc](./example/ssecli-stream.cc) - SSE client with auto-reconnection diff --git a/README-websocket.md b/README-websocket.md new file mode 100644 index 0000000000..9ca5a6b8d2 --- /dev/null +++ b/README-websocket.md @@ -0,0 +1,411 @@ +# WebSocket - RFC 6455 WebSocket Support + +A simple, blocking WebSocket implementation for C++11. + +> [!IMPORTANT] +> This is a blocking I/O WebSocket implementation using a thread-per-connection model. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want. + +## Features + +- **RFC 6455 compliant**: Full WebSocket protocol support +- **Server and Client**: Both sides included +- **SSL/TLS support**: `wss://` scheme for secure connections +- **Text and Binary**: Both message types supported +- **Automatic heartbeat**: Periodic Ping/Pong keeps connections alive +- **Subprotocol negotiation**: `Sec-WebSocket-Protocol` support for GraphQL, MQTT, etc. + +## Quick Start + +### Server + +```cpp +httplib::Server svr; + +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send("echo: " + msg); + } +}); + +svr.listen("localhost", 8080); +``` + +### Client + +```cpp +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws"); + +if (ws.connect()) { + ws.send("hello"); + + std::string msg; + if (ws.read(msg)) { + std::cout << msg << std::endl; // "echo: hello" + } + ws.close(); +} +``` + +## API Reference + +### ReadResult + +```cpp +enum ReadResult : int { + Fail = 0, // Connection closed or error + Text = 1, // UTF-8 text message + Binary = 2, // Binary message +}; +``` + +Returned by `read()`. Since `Fail` is `0`, the result works naturally in boolean contexts — `while (ws.read(msg))` continues until the connection closes. When you need to distinguish text from binary, check the return value directly. + +### CloseStatus + +```cpp +enum class CloseStatus : uint16_t { + Normal = 1000, + GoingAway = 1001, + ProtocolError = 1002, + UnsupportedData = 1003, + NoStatus = 1005, + Abnormal = 1006, + InvalidPayload = 1007, + PolicyViolation = 1008, + MessageTooBig = 1009, + MandatoryExtension = 1010, + InternalError = 1011, +}; +``` + +### Server Registration + +```cpp +// Basic handler +Server &WebSocket(const std::string &pattern, WebSocketHandler handler); + +// With subprotocol negotiation +Server &WebSocket(const std::string &pattern, WebSocketHandler handler, + SubProtocolSelector sub_protocol_selector); +``` + +**Type aliases:** + +```cpp +using WebSocketHandler = + std::function; +using SubProtocolSelector = + std::function &protocols)>; +``` + +The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols. + +### WebSocket (Server-side) + +Passed to the handler registered with `Server::WebSocket()`. The handler runs in a dedicated thread per connection. + +```cpp +// Read next message (blocks until received, returns Fail/Text/Binary) +ReadResult read(std::string &msg); + +// Send messages +bool send(const std::string &data); // Text +bool send(const char *data, size_t len); // Binary + +// Close the connection +void close(CloseStatus status = CloseStatus::Normal, + const std::string &reason = ""); + +// Access the original HTTP upgrade request +const Request &request() const; + +// Check if the connection is still open +bool is_open() const; +``` + +### WebSocketClient + +```cpp +// Constructor - accepts ws:// or wss:// URL +explicit WebSocketClient(const std::string &scheme_host_port_path, + const Headers &headers = {}); + +// Check if the URL was parsed successfully +bool is_valid() const; + +// Connect (performs HTTP upgrade handshake) +bool connect(); + +// Get the subprotocol selected by the server (empty if none) +const std::string &subprotocol() const; + +// Read/Send/Close (same as server-side WebSocket) +ReadResult read(std::string &msg); +bool send(const std::string &data); +bool send(const char *data, size_t len); +void close(CloseStatus status = CloseStatus::Normal, + const std::string &reason = ""); +bool is_open() const; + +// Timeouts +void set_read_timeout(time_t sec, time_t usec = 0); +void set_write_timeout(time_t sec, time_t usec = 0); + +// SSL configuration (wss:// only, requires CPPHTTPLIB_OPENSSL_SUPPORT) +void set_ca_cert_path(const std::string &path); +void set_ca_cert_store(tls::ca_store_t store); +void enable_server_certificate_verification(bool enabled); +``` + +## Examples + +### Echo Server with Connection Logging + +```cpp +httplib::Server svr; + +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + std::cout << "Connected from " << req.remote_addr << std::endl; + + std::string msg; + while (ws.read(msg)) { + ws.send("echo: " + msg); + } + + std::cout << "Disconnected" << std::endl; +}); + +svr.listen("localhost", 8080); +``` + +### Client: Continuous Read Loop + +```cpp +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws"); + +if (ws.connect()) { + ws.send("hello"); + ws.send("world"); + + std::string msg; + while (ws.read(msg)) { // blocks until a message arrives + std::cout << msg << std::endl; // "echo: hello", "echo: world" + } + // read() returns false when the server closes the connection +} +``` + +### Text and Binary Messages + +Check the `ReadResult` return value to distinguish between text and binary: + +```cpp +// Server +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + std::string msg; + httplib::ws::ReadResult ret; + while ((ret = ws.read(msg))) { + if (ret == httplib::ws::Text) { + ws.send("echo: " + msg); + } else { + ws.send(msg.data(), msg.size()); // Binary echo + } + } +}); + +// Client +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws"); +if (ws.connect()) { + // Send binary data + const char binary[] = {0x00, 0x01, 0x02, 0x03}; + ws.send(binary, sizeof(binary)); + + // Receive and check the type + std::string msg; + if (ws.read(msg) == httplib::ws::Binary) { + // Process binary data in msg + } + ws.close(); +} +``` + +### SSL Client + +```cpp +httplib::ws::WebSocketClient ws("wss://echo.example.com/ws"); + +if (ws.connect()) { + ws.send("hello over TLS"); + + std::string msg; + if (ws.read(msg)) { + std::cout << msg << std::endl; + } + ws.close(); +} +``` + +### Close with Status + +```cpp +// Client-side: close with a specific status code and reason +ws.close(httplib::ws::CloseStatus::GoingAway, "shutting down"); + +// Server-side: close with a policy violation status +ws.close(httplib::ws::CloseStatus::PolicyViolation, "forbidden"); +``` + +### Accessing the Upgrade Request + +```cpp +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + // Access headers from the original HTTP upgrade request + auto auth = req.get_header_value("Authorization"); + if (auth.empty()) { + ws.close(httplib::ws::CloseStatus::PolicyViolation, "unauthorized"); + return; + } + + std::string msg; + while (ws.read(msg)) { + ws.send("echo: " + msg); + } +}); +``` + +### Custom Headers and Timeouts + +```cpp +httplib::Headers headers = { + {"Authorization", "Bearer token123"} +}; + +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws", headers); +ws.set_read_timeout(30, 0); // 30 seconds +ws.set_write_timeout(10, 0); // 10 seconds + +if (ws.connect()) { + std::string msg; + while (ws.read(msg)) { + std::cout << msg << std::endl; + } +} +``` + +### Subprotocol Negotiation + +The server can negotiate a subprotocol with the client using `Sec-WebSocket-Protocol`. This is required for protocols like GraphQL over WebSocket (`graphql-ws`) and MQTT. + +```cpp +// Server: register a handler with a subprotocol selector +svr.WebSocket( + "/ws", + [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + std::string msg; + while (ws.read(msg)) { + ws.send("echo: " + msg); + } + }, + [](const std::vector &protocols) -> std::string { + // The client proposed a list of subprotocols; pick one + for (const auto &p : protocols) { + if (p == "graphql-ws" || p == "graphql-transport-ws") { + return p; + } + } + return ""; // Decline all + }); + +// Client: propose subprotocols via Sec-WebSocket-Protocol header +httplib::Headers headers = { + {"Sec-WebSocket-Protocol", "graphql-ws, graphql-transport-ws"} +}; +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws", headers); + +if (ws.connect()) { + // Check which subprotocol the server selected + std::cout << "Subprotocol: " << ws.subprotocol() << std::endl; + // => "graphql-ws" + ws.close(); +} +``` + +### SSL Client with Certificate Configuration + +```cpp +httplib::ws::WebSocketClient ws("wss://example.com/ws"); +ws.set_ca_cert_path("/path/to/ca-bundle.crt"); +ws.enable_server_certificate_verification(true); + +if (ws.connect()) { + ws.send("secure message"); + ws.close(); +} +``` + +## Configuration + +| Macro | Default | Description | +|---------------------------------------------|-------------------|----------------------------------------------------------| +| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message | +| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) | +| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) | +| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) | + +### Runtime Ping Interval + +You can override the ping interval at runtime instead of changing the compile-time macro. Set it to `0` to disable automatic pings entirely. + +```cpp +// Server side +httplib::Server svr; +svr.set_websocket_ping_interval(10); // 10 seconds + +// Or using std::chrono +svr.set_websocket_ping_interval(std::chrono::seconds(10)); + +// Client side +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws"); +ws.set_websocket_ping_interval(10); // 10 seconds + +// Disable automatic pings +ws.set_websocket_ping_interval(0); +``` + +## Threading Model + +WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime. + +The default thread pool uses dynamic scaling: it maintains a base thread count of `CPPHTTPLIB_THREAD_POOL_COUNT` (8 or `std::thread::hardware_concurrency() - 1`, whichever is greater) and can scale up to 4x that count under load (`CPPHTTPLIB_THREAD_POOL_MAX_COUNT`). When all base threads are busy, temporary threads are spawned automatically up to the maximum. These dynamic threads exit after an idle timeout (`CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT`, default 3 seconds). + +This dynamic scaling helps accommodate WebSocket connections alongside HTTP requests. However, if you expect many simultaneous WebSocket connections, you should configure the thread pool accordingly: + +```cpp +httplib::Server svr; + +svr.new_task_queue = [] { + return new httplib::ThreadPool(/*base_threads=*/8, /*max_threads=*/128); +}; +``` + +Choose sizes that account for both your expected HTTP load and the maximum number of simultaneous WebSocket connections. + +## Protocol + +The implementation follows [RFC 6455](https://tools.ietf.org/html/rfc6455): + +- Handshake via HTTP Upgrade with `Sec-WebSocket-Key` / `Sec-WebSocket-Accept` +- Subprotocol negotiation via `Sec-WebSocket-Protocol` +- Frame masking (client-to-server) +- Control frames: Close, Ping, Pong +- Message fragmentation and reassembly +- Close handshake with status codes + +## Browser Test + +Run the echo server example and open `http://localhost:8080` in a browser: + +```bash +cd example && make wsecho && ./wsecho +``` diff --git a/README.md b/README.md index bb1ea04ab2..ebde4b8b9e 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,271 @@ -cpp-httplib -=========== +# cpp-httplib -[![Build Status](https://travis-ci.org/yhirose/cpp-httplib.svg?branch=master)](https://travis-ci.org/yhirose/cpp-httplib) -[![Bulid Status](https://ci.appveyor.com/api/projects/status/github/yhirose/cpp-httplib?branch=master&svg=true)](https://ci.appveyor.com/project/yhirose/cpp-httplib) +[![](https://github.com/yhirose/cpp-httplib/workflows/test/badge.svg)](https://github.com/yhirose/cpp-httplib/actions) -A C++ single-file header-only cross platform HTTP/HTTPS library. +A C++11 single-file header-only cross platform HTTP/HTTPS library.
+It's extremely easy to set up. Just include the **[httplib.h](https://raw.githubusercontent.com/yhirose/cpp-httplib/refs/heads/master/httplib.h)** file in your code! -It's extremely easy to setup. Just include **httplib.h** file in your code! +Learn more in the [official documentation](https://yhirose.github.io/cpp-httplib/) (built with [docs-gen](https://github.com/yhirose/docs-gen)). -Server Example --------------- +> [!IMPORTANT] +> This library uses 'blocking' socket I/O. If you are looking for a library with 'non-blocking' socket I/O, this is not the one that you want. + +> [!WARNING] +> 32-bit platforms are **NOT supported**. Use at your own risk. The library may compile on 32-bit targets, but no security review has been conducted for 32-bit environments. Integer truncation and other 32-bit-specific issues may exist. **Security reports that only affect 32-bit platforms will be closed without action.** The maintainer does not have access to 32-bit environments for testing or fixing issues. CI includes basic compile checks only, not functional or security testing. + +## Main Features + +- HTTP Server/Client +- SSL/TLS support (OpenSSL, MbedTLS, wolfSSL) +- [Stream API](README-stream.md) +- [Server-Sent Events](README-sse.md) +- [WebSocket](README-websocket.md) + +## Simple examples + +### Server ```c++ -#include +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "path/to/httplib.h" -int main(void) -{ - using namespace httplib; +// HTTP +httplib::Server svr; - Server svr; +// HTTPS +httplib::SSLServer svr; - svr.Get("/hi", [](const Request& req, Response& res) { - res.set_content("Hello World!", "text/plain"); - }); +svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) { + res.set_content("Hello World!", "text/plain"); +}); - svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) { - auto numbers = req.matches[1]; - res.set_content(numbers, "text/plain"); - }); +svr.listen("0.0.0.0", 8080); +``` + +### Client + +```c++ +#define CPPHTTPLIB_OPENSSL_SUPPORT +#include "path/to/httplib.h" + +// HTTP +httplib::Client cli("http://yhirose.github.io"); + +// HTTPS +httplib::Client cli("https://yhirose.github.io"); + +if (auto res = cli.Get("/hi")) { + res->status; + res->body; +} +``` + +## SSL/TLS Support + +cpp-httplib supports multiple TLS backends through an abstraction layer: + +| Backend | Define | Libraries | Notes | +| :------ | :----- | :-------- | :---- | +| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` | [3.0 or later](https://www.openssl.org/policies/releasestrat.html) required | +| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` | 2.x and 3.x supported (auto-detected) | +| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` | 5.x supported; must build with `--enable-opensslall` | + +> [!NOTE] +> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable. + +```c++ +// Use either OpenSSL, Mbed TLS, or wolfSSL +#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT +#include "path/to/httplib.h" + +// Server +httplib::SSLServer svr("./cert.pem", "./key.pem"); - svr.Get("/stop", [&](const Request& req, Response& res) { - svr.stop(); +// Client +httplib::Client cli("https://localhost:1234"); // scheme + host +httplib::SSLClient cli("localhost:1234"); // host +httplib::SSLClient cli("localhost", 1234); // host, port + +// Use your CA bundle +cli.set_ca_cert_path("./ca-bundle.crt"); + +// Disable cert verification +cli.enable_server_certificate_verification(false); + +// Disable host verification +cli.enable_server_hostname_verification(false); +``` + +### SSL Error Handling + +When SSL operations fail, cpp-httplib provides detailed error information through `ssl_error()` and `ssl_backend_error()`: + +- `ssl_error()` - Returns the TLS-level error code (e.g., `SSL_ERROR_SSL` for OpenSSL) +- `ssl_backend_error()` - Returns the backend-specific error code (e.g., `ERR_get_error()` for OpenSSL/wolfSSL, return value for Mbed TLS) + +```c++ +#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT +#include "path/to/httplib.h" + +httplib::Client cli("https://example.com"); + +auto res = cli.Get("/"); +if (!res) { + // Check the error type + const auto err = res.error(); + + switch (err) { + case httplib::Error::SSLConnection: + std::cout << "SSL connection failed, SSL error: " + << res.ssl_error() << std::endl; + break; + + case httplib::Error::SSLLoadingCerts: + std::cout << "SSL cert loading failed, backend error: " + << std::hex << res.ssl_backend_error() << std::endl; + break; + + case httplib::Error::SSLServerVerification: + std::cout << "SSL verification failed, verify error: " + << res.ssl_backend_error() << std::endl; + break; + + case httplib::Error::SSLServerHostnameVerification: + std::cout << "SSL hostname verification failed, verify error: " + << res.ssl_backend_error() << std::endl; + break; + + default: + std::cout << "HTTP error: " << httplib::to_string(err) << std::endl; + } +} +``` + +### Custom Certificate Verification + +You can set a custom verification callback using `tls::VerifyCallback`: + +```c++ +httplib::Client cli("https://example.com"); + +cli.set_server_certificate_verifier( + [](const httplib::tls::VerifyContext &ctx) -> bool { + std::cout << "Subject CN: " << ctx.subject_cn() << std::endl; + std::cout << "Issuer: " << ctx.issuer_name() << std::endl; + std::cout << "Depth: " << ctx.depth << std::endl; + std::cout << "Pre-verified: " << ctx.preverify_ok << std::endl; + + // Inspect SANs (Subject Alternative Names) + for (const auto &san : ctx.sans()) { + std::cout << "SAN: " << san.value << std::endl; + } + + // Return true to accept, false to reject + return ctx.preverify_ok; }); +``` + +### Peer Certificate Inspection + +On the server side, you can inspect the client's peer certificate from a request handler: + +```c++ +httplib::SSLServer svr("./cert.pem", "./key.pem", + "./client-ca-cert.pem"); + +svr.Get("/", [](const httplib::Request &req, httplib::Response &res) { + auto cert = req.peer_cert(); + if (cert) { + std::cout << "Client CN: " << cert.subject_cn() << std::endl; + std::cout << "Serial: " << cert.serial() << std::endl; + } + + auto sni = req.sni(); + std::cout << "SNI: " << sni << std::endl; +}); +``` + +### Platform-specific Certificate Handling + +cpp-httplib automatically integrates with the OS certificate store on macOS and Windows. This works with all TLS backends. - svr.listen("localhost", 1234); +| Platform | Behavior | Disable (compile time) | +| :------- | :------- | :--------------------- | +| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`) | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` | +| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` | + +On Windows, verification can also be disabled at runtime: + +```c++ +cli.enable_windows_certificate_verification(false); +``` + +> [!NOTE] +> When using SSL, it seems impossible to avoid SIGPIPE in all cases, since on some operating systems, SIGPIPE can only be suppressed on a per-message basis, but there is no way to make the OpenSSL library do so for its internal communications. If your program needs to avoid being terminated on SIGPIPE, the only fully general way might be to set up a signal handler for SIGPIPE to handle or ignore it yourself. + +## Server + +```c++ +#include + +int main(void) +{ + using namespace httplib; + + Server svr; + + svr.Get("/hi", [](const Request& req, Response& res) { + res.set_content("Hello World!", "text/plain"); + }); + + // Match the request path against a regular expression + // and extract its captures + svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) { + auto numbers = req.matches[1]; + res.set_content(numbers, "text/plain"); + }); + + // Capture the second segment of the request path as "id" path param + svr.Get("/users/:id", [&](const Request& req, Response& res) { + auto user_id = req.path_params.at("id"); + res.set_content(user_id, "text/plain"); + }); + + // Extract values from HTTP headers and URL query params + svr.Get("/body-header-param", [](const Request& req, Response& res) { + if (req.has_header("Content-Length")) { + auto val = req.get_header_value("Content-Length"); + } + if (req.has_param("key")) { + auto val = req.get_param_value("key"); + } + res.set_content(req.body, "text/plain"); + }); + + // If the handler takes time to finish, you can also poll the connection state + svr.Get("/task", [&](const Request& req, Response& res) { + const char * result = nullptr; + process.run(); // for example, starting an external process + while (result == nullptr) { + sleep(1); + if (req.is_connection_closed()) { + process.kill(); // kill the process + return; + } + result = process.stdout(); // != nullptr if the process finishes + } + res.set_content(result, "text/plain"); + }); + + svr.Get("/stop", [&](const Request& req, Response& res) { + svr.stop(); + }); + + svr.listen("localhost", 1234); } ``` -`Post`, `Put`, `Delete` and `Options` methods are also supported. +`Post`, `Put`, `Patch`, `Delete` and `Options` methods are also supported. ### Bind a socket to multiple interfaces and any available port @@ -49,56 +277,388 @@ svr.listen_after_bind(); ### Static File Server ```cpp -svr.set_base_dir("./www"); +// Mount / to ./www directory +auto ret = svr.set_mount_point("/", "./www"); +if (!ret) { + // The specified base directory doesn't exist... +} + +// Mount /public to ./www directory +ret = svr.set_mount_point("/public", "./www"); + +// Mount /public to ./www1 and ./www2 directories +ret = svr.set_mount_point("/public", "./www1"); // 1st order to search +ret = svr.set_mount_point("/public", "./www2"); // 2nd order to search + +// Remove mount / +ret = svr.remove_mount_point("/"); + +// Remove mount /public +ret = svr.remove_mount_point("/public"); +``` + +```cpp +// User defined file extension and MIME type mappings +svr.set_file_extension_and_mimetype_mapping("cc", "text/x-c"); +svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c"); +svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h"); +``` + +The following are built-in mappings: + +| Extension | MIME Type | Extension | MIME Type | +| :--------- | :-------------------------- | :--------- | :-------------------------- | +| css | text/css | mpga | audio/mpeg | +| csv | text/csv | weba | audio/webm | +| txt | text/plain | wav | audio/wave | +| vtt | text/vtt | otf | font/otf | +| html, htm | text/html | ttf | font/ttf | +| apng | image/apng | woff | font/woff | +| avif | image/avif | woff2 | font/woff2 | +| bmp | image/bmp | 7z | application/x-7z-compressed | +| gif | image/gif | atom | application/atom+xml | +| png | image/png | pdf | application/pdf | +| svg | image/svg+xml | mjs, js | text/javascript | +| webp | image/webp | json | application/json | +| ico | image/x-icon | rss | application/rss+xml | +| tif | image/tiff | tar | application/x-tar | +| tiff | image/tiff | xhtml, xht | application/xhtml+xml | +| jpeg, jpg | image/jpeg | xslt | application/xslt+xml | +| mp4 | video/mp4 | xml | application/xml | +| mpeg | video/mpeg | gz | application/gzip | +| webm | video/webm | zip | application/zip | +| mp3 | audio/mp3 | wasm | application/wasm | + +> [!WARNING] +> These static file server methods are not thread-safe. + + + +> [!NOTE] +> On POSIX systems, the static file server rejects requests that resolve (via symlinks) to a path outside the mounted base directory. Ensure that the served directory has appropriate permissions, as managing access to the served directory is the application developer's responsibility. + +### File request handler + +```cpp +// The handler is called right before the response is sent to a client +svr.set_file_request_handler([](const Request &req, Response &res) { + ... +}); ``` ### Logging +cpp-httplib provides separate logging capabilities for access logs and error logs, similar to web servers like Nginx and Apache. + +#### Access Logging + +Access loggers capture successful HTTP requests and responses: + +```cpp +svr.set_logger([](const httplib::Request& req, const httplib::Response& res) { + std::cout << req.method << " " << req.path << " -> " << res.status << std::endl; +}); +``` + +#### Pre-compression Logging + +You can also set a pre-compression logger to capture request/response data before compression is applied: + +```cpp +svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Response& res) { + // Log before compression - res.body contains uncompressed content + // Content-Encoding header is not yet set + your_pre_compression_logger(req, res); +}); +``` + +The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called. + +#### Error Logging + +Error loggers capture failed requests and connection issues. Unlike access loggers, error loggers only receive the Error and Request information, as errors typically occur before a meaningful Response can be generated. + ```cpp -svr.set_logger([](const auto& req, const auto& res) { - your_logger(req, res); +svr.set_error_logger([](const httplib::Error& err, const httplib::Request* req) { + std::cerr << httplib::to_string(err) << " while processing request"; + if (req) { + std::cerr << ", client: " << req->get_header_value("X-Forwarded-For") + << ", request: '" << req->method << " " << req->path << " " << req->version << "'" + << ", host: " << req->get_header_value("Host"); + } + std::cerr << std::endl; }); ``` -### Error Handler +### Error handler ```cpp svr.set_error_handler([](const auto& req, auto& res) { - const char* fmt = "

Error Status: %d

"; - char buf[BUFSIZ]; - snprintf(buf, sizeof(buf), fmt, res.status); - res.set_content(buf, "text/html"); + auto fmt = "

Error Status: %d

"; + char buf[BUFSIZ]; + snprintf(buf, sizeof(buf), fmt, res.status); + res.set_content(buf, "text/html"); }); ``` -### 'multipart/form-data' POST data +### Exception handler +The exception handler gets called if a user routing handler throws an error. ```cpp -svr.Post("/multipart", [&](const auto& req, auto& res) { - auto size = req.files.size(); - auto ret = req.has_file("name1")); - const auto& file = req.get_file_value("name1"); - // file.filename; - // file.content_type; - auto body = req.body.substr(file.offset, file.length)); -}) +svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) { + auto fmt = "

Error 500

%s

"; + char buf[BUFSIZ]; + try { + std::rethrow_exception(ep); + } catch (std::exception &e) { + snprintf(buf, sizeof(buf), fmt, e.what()); + } catch (...) { // See the following NOTE + snprintf(buf, sizeof(buf), fmt, "Unknown Exception"); + } + res.set_content(buf, "text/html"); + res.status = StatusCode::InternalServerError_500; +}); ``` -### Stream content with Content provider +> [!CAUTION] +> if you don't provide the `catch (...)` block for a rethrown exception pointer, an uncaught exception will end up causing the server crash. Be careful! + +### Pre routing handler ```cpp -const uint64_t DATA_CHUNK_SIZE = 4; +svr.set_pre_routing_handler([](const auto& req, auto& res) { + if (req.path == "/hello") { + res.set_content("world", "text/html"); + return Server::HandlerResponse::Handled; + } + return Server::HandlerResponse::Unhandled; +}); +``` + +### Post routing handler + +```cpp +svr.set_post_routing_handler([](const auto& req, auto& res) { + res.set_header("ADDITIONAL_HEADER", "value"); +}); +``` + +### Pre request handler + +```cpp +svr.set_pre_request_handler([](const auto& req, auto& res) { + if (req.matched_route == "/user/:user") { + auto user = req.path_params.at("user"); + if (user != "john") { + res.status = StatusCode::Forbidden_403; + res.set_content("error", "text/html"); + return Server::HandlerResponse::Handled; + } + } + return Server::HandlerResponse::Unhandled; +}); +``` + +### Response user data + +`res.user_data` is a `std::map` that lets pre-routing or pre-request handlers pass arbitrary data to route handlers. + +```cpp +struct AuthContext { + std::string user_id; + std::string role; +}; + +svr.set_pre_routing_handler([](const auto& req, auto& res) { + auto token = req.get_header_value("Authorization"); + res.user_data["auth"] = AuthContext{decode_token(token)}; + return Server::HandlerResponse::Unhandled; +}); + +svr.Get("/me", [](const auto& /*req*/, auto& res) { + auto* ctx = httplib::any_cast(&res.user_data["auth"]); + if (!ctx) { + res.status = StatusCode::Unauthorized_401; + return; + } + res.set_content("Hello " + ctx->user_id, "text/plain"); +}); +``` + +`httplib::any` mirrors the C++17 `std::any` API. On C++17 and later it is an alias for `std::any`; on C++11/14 a compatible implementation is provided. + +### Form data handling + +#### URL-encoded form data ('application/x-www-form-urlencoded') + +```cpp +svr.Post("/form", [&](const auto& req, auto& res) { + // URL query parameters and form-encoded data are accessible via req.params + std::string username = req.get_param_value("username"); + std::string password = req.get_param_value("password"); + + // Handle multiple values with same name + auto interests = req.get_param_values("interests"); + + // Check existence + if (req.has_param("newsletter")) { + // Handle newsletter subscription + } +}); +``` + +#### 'multipart/form-data' POST data + +```cpp +svr.Post("/multipart", [&](const Request& req, Response& res) { + // Access text fields (from form inputs without files) + std::string username = req.form.get_field("username"); + std::string bio = req.form.get_field("bio"); + + // Access uploaded files + if (req.form.has_file("avatar")) { + const auto& file = req.form.get_file("avatar"); + std::cout << "Uploaded file: " << file.filename + << " (" << file.content_type << ") - " + << file.content.size() << " bytes" << std::endl; + + // Access additional headers if needed + for (const auto& header : file.headers) { + std::cout << "Header: " << header.first << " = " << header.second << std::endl; + } + + // IMPORTANT: file.filename is an untrusted value from the client. + // Always sanitize to prevent path traversal attacks. + auto safe_name = httplib::sanitize_filename(file.filename); + if (safe_name.empty()) { + res.status = StatusCode::BadRequest_400; + res.set_content("Invalid filename", "text/plain"); + return; + } + + // Save to disk + std::ofstream ofs(upload_dir + "/" + safe_name, std::ios::binary); + ofs << file.content; + } + + // Handle multiple values with same name + auto tags = req.form.get_fields("tags"); // e.g., multiple checkboxes + for (const auto& tag : tags) { + std::cout << "Tag: " << tag << std::endl; + } + + auto documents = req.form.get_files("documents"); // multiple file upload + for (const auto& doc : documents) { + std::cout << "Document: " << doc.filename + << " (" << doc.content.size() << " bytes)" << std::endl; + } + + // Check existence before accessing + if (req.form.has_field("newsletter")) { + std::cout << "Newsletter subscription: " << req.form.get_field("newsletter") << std::endl; + } + + // Get counts for validation + if (req.form.get_field_count("tags") > 5) { + res.status = StatusCode::BadRequest_400; + res.set_content("Too many tags", "text/plain"); + return; + } + + // Summary + std::cout << "Received " << req.form.fields.size() << " text fields and " + << req.form.files.size() << " files" << std::endl; + + res.set_content("Upload successful", "text/plain"); +}); +``` + +#### Filename Sanitization + +`file.filename` in multipart uploads is an untrusted value from the client. Always sanitize before using it in file paths: + +```cpp +auto safe = httplib::sanitize_filename(file.filename); +``` + +This function strips path separators (`/`, `\`), null bytes, leading/trailing whitespace, and rejects `.` and `..`. Returns an empty string if the filename is unsafe. + +### Receive content with a content receiver + +```cpp +svr.Post("/content_receiver", + [&](const Request &req, Response &res, const ContentReader &content_reader) { + if (req.is_multipart_form_data()) { + // NOTE: `content_reader` is blocking until every form data field is read + // This approach allows streaming processing of large files + std::vector items; + content_reader( + [&](const FormData &item) { + items.push_back(item); + return true; + }, + [&](const char *data, size_t data_length) { + items.back().content.append(data, data_length); + return true; + }); + + // Process the received items + for (const auto& item : items) { + if (item.filename.empty()) { + // Text field + std::cout << "Field: " << item.name << " = " << item.content << std::endl; + } else { + // File + std::cout << "File: " << item.name << " (" << item.filename << ") - " + << item.content.size() << " bytes" << std::endl; + } + } + } else { + std::string body; + content_reader([&](const char *data, size_t data_length) { + body.append(data, data_length); + return true; + }); + } + }); +``` + +### Send content with the content provider + +```cpp +const size_t DATA_CHUNK_SIZE = 4; svr.Get("/stream", [&](const Request &req, Response &res) { auto data = new std::string("abcdefg"); res.set_content_provider( data->size(), // Content length - [data](uint64_t offset, uint64_t length, Out out) { + "text/plain", // Content type + [&, data](size_t offset, size_t length, DataSink &sink) { const auto &d = *data; - out(&d[offset], std::min(length, DATA_CHUNK_SIZE)); + sink.write(&d[offset], std::min(length, DATA_CHUNK_SIZE)); + return true; // return 'false' if you want to cancel the process. }, - [data] { delete data; }); + [data](bool success) { delete data; }); +}); +``` + +Without content length: + +```cpp +svr.Get("/stream", [&](const Request &req, Response &res) { + res.set_content_provider( + "text/plain", // Content type + [&](size_t offset, DataSink &sink) { + if (/* there is still data */) { + std::vector data; + // prepare data... + sink.write(data.data(), data.size()); + } else { + sink.done(); // No more data + } + return true; // return 'false' if you want to cancel the process. + }); }); ``` @@ -107,32 +667,126 @@ svr.Get("/stream", [&](const Request &req, Response &res) { ```cpp svr.Get("/chunked", [&](const Request& req, Response& res) { res.set_chunked_content_provider( - [](uint64_t offset, Out out, Done done) { - out("123", 3); - out("345", 3); - out("789", 3); - done(); + "text/plain", + [](size_t offset, DataSink &sink) { + sink.write("123", 3); + sink.write("345", 3); + sink.write("789", 3); + sink.done(); // No more data + return true; // return 'false' if you want to cancel the process. } ); }); ``` -### Default thread pool supporet +With trailer: + +```cpp +svr.Get("/chunked", [&](const Request& req, Response& res) { + res.set_header("Trailer", "Dummy1, Dummy2"); + res.set_chunked_content_provider( + "text/plain", + [](size_t offset, DataSink &sink) { + sink.write("123", 3); + sink.write("345", 3); + sink.write("789", 3); + sink.done_with_trailer({ + {"Dummy1", "DummyVal1"}, + {"Dummy2", "DummyVal2"} + }); + return true; + } + ); +}); +``` + +### Send file content + +```cpp +svr.Get("/content", [&](const Request &req, Response &res) { + res.set_file_content("./path/to/content.html"); +}); + +svr.Get("/content", [&](const Request &req, Response &res) { + res.set_file_content("./path/to/content", "text/html"); +}); +``` + +### 'Expect: 100-continue' handler + +By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header. + +```cpp +// Send a '417 Expectation Failed' response. +svr.set_expect_100_continue_handler([](const Request &req, Response &res) { + return StatusCode::ExpectationFailed_417; +}); +``` + +```cpp +// Send a final status without reading the message body. +svr.set_expect_100_continue_handler([](const Request &req, Response &res) { + return res.status = StatusCode::Unauthorized_401; +}); +``` + +### Keep-Alive connection + +```cpp +svr.set_keep_alive_max_count(2); // Default is 100 +svr.set_keep_alive_timeout(10); // Default is 5 +``` + +### Timeout + +```c++ +svr.set_read_timeout(5, 0); // 5 seconds +svr.set_write_timeout(5, 0); // 5 seconds +svr.set_idle_interval(0, 100000); // 100 milliseconds +``` + +### Set maximum payload length for reading a request body + +```c++ +svr.set_payload_max_length(1024 * 1024 * 512); // 512MB +``` + +> [!NOTE] +> When the request body content type is 'www-form-urlencoded', the actual payload length shouldn't exceed `CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH`. + +### Server-Sent Events + +Please see [Server example](https://github.com/yhirose/cpp-httplib/blob/master/example/ssesvr.cc) and [Client example](https://github.com/yhirose/cpp-httplib/blob/master/example/ssecli.cc). + +### Default thread pool support + +`ThreadPool` is used as the **default** task queue, with dynamic scaling support. By default, it maintains a base thread count of 8 or `std::thread::hardware_concurrency() - 1` (whichever is greater), and can scale up to 4x that count under load. You can change these with `CPPHTTPLIB_THREAD_POOL_COUNT` and `CPPHTTPLIB_THREAD_POOL_MAX_COUNT`. -Set thread count to 8: +When all threads are busy and a new task arrives, a temporary thread is spawned (up to the maximum). When a dynamic thread finishes its task and the queue is empty, or after an idle timeout, it exits automatically. The idle timeout defaults to 3 seconds, configurable via `CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT`. + +If you want to set the thread counts at runtime: ```cpp -#define CPPHTTPLIB_THREAD_POOL_COUNT 8 +svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/8, /*max_threads=*/64); }; ``` -Disable the default thread pool: +#### Max queued requests + +You can also provide an optional parameter to limit the maximum number +of pending requests, i.e. requests `accept()`ed by the listener but +still waiting to be serviced by worker threads. ```cpp -#define CPPHTTPLIB_THREAD_POOL_COUNT 0 +svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/12, /*max_threads=*/0, /*max_queued_requests=*/18); }; ``` +Default limit is 0 (unlimited). Once the limit is reached, the listener +will shutdown the client connection. + ### Override the default thread pool with yours +You can supply your own thread pool implementation according to your need. + ```cpp class YourThreadPoolTaskQueue : public TaskQueue { public: @@ -140,8 +794,10 @@ public: pool_.start_with_thread_count(n); } - virtual void enqueue(std::function fn) override { - pool_.enqueue(fn); + virtual bool enqueue(std::function fn) override { + /* Return true if the task was actually enqueued, or false + * if the caller must drop the corresponding connection. */ + return pool_.enqueue(fn); } virtual void shutdown() override { @@ -157,10 +813,7 @@ svr.new_task_queue = [] { }; ``` -Client Example --------------- - -### GET +## Client ```c++ #include @@ -168,35 +821,141 @@ Client Example int main(void) { - httplib::Client cli("localhost", 1234); + httplib::Client cli("localhost", 1234); - auto res = cli.Get("/hi"); - if (res && res->status == 200) { - std::cout << res->body << std::endl; + if (auto res = cli.Get("/hi")) { + if (res->status == StatusCode::OK_200) { + std::cout << res->body << std::endl; } + } else { + auto err = res.error(); + std::cout << "HTTP error: " << httplib::to_string(err) << std::endl; + } } ``` +> [!TIP] +> Constructor with scheme-host-port string is now supported! + +```c++ +httplib::Client cli("localhost"); +httplib::Client cli("localhost:8080"); +httplib::Client cli("http://localhost"); +httplib::Client cli("http://localhost:8080"); +httplib::Client cli("https://localhost"); +httplib::SSLClient cli("localhost"); +``` + +### Error code + +Here is the list of errors from `Result::error()`. + +```c++ +enum class Error { + Success = 0, + Unknown, + Connection, + BindIPAddress, + Read, + Write, + ExceedRedirectCount, + Canceled, + SSLConnection, + SSLLoadingCerts, + SSLServerVerification, + SSLServerHostnameVerification, + UnsupportedMultipartBoundaryChars, + Compression, + ConnectionTimeout, + ProxyConnection, + ConnectionClosed, + Timeout, + ResourceExhaustion, + TooManyFormDataFiles, + ExceedMaxPayloadSize, + ExceedUriMaxLength, + ExceedMaxSocketDescriptorCount, + InvalidRequestLine, + InvalidHTTPMethod, + InvalidHTTPVersion, + InvalidHeaders, + MultipartParsing, + OpenFile, + Listen, + GetSockName, + UnsupportedAddressFamily, + HTTPParsing, + InvalidRangeHeader, +}; +``` + +### Client Logging + +#### Access Logging + +```cpp +cli.set_logger([](const httplib::Request& req, const httplib::Response& res) { + auto duration = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time).count(); + std::cout << "✓ " << req.method << " " << req.path + << " -> " << res.status << " (" << res.body.size() << " bytes, " + << duration << "ms)" << std::endl; +}); +``` + +#### Error Logging + +```cpp +cli.set_error_logger([](const httplib::Error& err, const httplib::Request* req) { + std::cerr << "✗ "; + if (req) { + std::cerr << req->method << " " << req->path << " "; + } + std::cerr << "failed: " << httplib::to_string(err); + + // Add specific guidance based on error type + switch (err) { + case httplib::Error::Connection: + std::cerr << " (verify server is running and reachable)"; + break; + case httplib::Error::SSLConnection: + std::cerr << " (check SSL certificate and TLS configuration)"; + break; + case httplib::Error::ConnectionTimeout: + std::cerr << " (increase timeout or check network latency)"; + break; + case httplib::Error::Read: + std::cerr << " (server may have closed connection prematurely)"; + break; + default: + break; + } + std::cerr << std::endl; +}); +``` + ### GET with HTTP headers ```c++ - httplib::Headers headers = { - { "Accept-Encoding", "gzip, deflate" } - }; - auto res = cli.Get("/hi", headers); +httplib::Headers headers = { + { "Hello", "World!" } +}; +auto res = cli.Get("/hi", headers); ``` -### GET with Content Receiver +or ```c++ - std::string body; +auto res = cli.Get("/hi", {{"Hello", "World!"}}); +``` - auto res = cli.Get("/large-data", - [&](const char *data, uint64_t data_length, uint64_t offset, uint64_t content_length) { - body.append(data, data_length); - }); +or - assert(res->body.empty()); +```c++ +cli.set_default_headers({ + { "Hello", "World!" } +}); +auto res = cli.Get("/hi"); ``` ### POST @@ -229,15 +988,35 @@ auto res = cli.Post("/post", params); ### POST with Multipart Form Data ```c++ - httplib::MultipartFormDataItems items = { - { "text1", "text default", "", "" }, - { "text2", "aωb", "", "" }, - { "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" }, - { "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" }, - { "file3", "", "", "application/octet-stream" }, - }; +httplib::UploadFormDataItems items = { + { "text1", "text default", "", "" }, + { "text2", "aωb", "", "" }, + { "file1", "h\ne\n\nl\nl\no\n", "hello.txt", "text/plain" }, + { "file2", "{\n \"world\", true\n}\n", "world.json", "application/json" }, + { "file3", "", "", "application/octet-stream" }, +}; + +auto res = cli.Post("/multipart", items); +``` + +To upload files from disk without loading them entirely into memory, use `make_file_provider`. The file is sent with chunked transfer encoding. + +```cpp +httplib::FormDataProviderItems providers = { + httplib::make_file_provider("file1", "/path/to/large.bin", "large.bin", "application/octet-stream"), + httplib::make_file_provider("avatar", "/path/to/photo.jpg", "photo.jpg", "image/jpeg"), +}; + +auto res = cli.Post("/upload", {}, {}, providers); +``` - auto res = cli.Post("/multipart", items); +### POST with a file body + +To POST a file as a raw binary body with `Content-Length`, use `make_file_body`. + +```cpp +auto [size, provider] = httplib::make_file_body("/path/to/data.bin"); +auto res = cli.Post("/upload", size, provider, "application/octet-stream"); ``` ### PUT @@ -246,6 +1025,12 @@ auto res = cli.Post("/post", params); res = cli.Put("/resource/foo", "text", "text/plain"); ``` +### PATCH + +```c++ +res = cli.Patch("/resource/foo", "text", "text/plain"); +``` + ### DELETE ```c++ @@ -259,47 +1044,134 @@ res = cli.Options("*"); res = cli.Options("/resource/foo"); ``` -### Connection Timeout +### Timeout + +```c++ +cli.set_connection_timeout(0, 300000); // 300 milliseconds +cli.set_read_timeout(5, 0); // 5 seconds +cli.set_write_timeout(5, 0); // 5 seconds + +// This method works the same as curl's `--max-time` option +cli.set_max_timeout(5000); // 5 seconds +``` + +### Set maximum payload length for reading a response body + +```c++ +cli.set_payload_max_length(1024 * 1024 * 512); // 512MB +``` + +### Receive content with a content receiver ```c++ -httplib::Client cli("localhost", 8080, 5); // timeouts in 5 seconds +std::string body; + +auto res = cli.Get("/large-data", + [&](const char *data, size_t data_length) { + body.append(data, data_length); + return true; + }); +``` + +```cpp +std::string body; + +auto res = cli.Get( + "/stream", Headers(), + [&](const Response &response) { + EXPECT_EQ(StatusCode::OK_200, response.status); + return true; // return 'false' if you want to cancel the request. + }, + [&](const char *data, size_t data_length) { + body.append(data, data_length); + return true; // return 'false' if you want to cancel the request. + }); +``` + +### Send content with a content provider + +```cpp +std::string body = ...; + +auto res = cli.Post( + "/stream", body.size(), + [](size_t offset, size_t length, DataSink &sink) { + sink.write(body.data() + offset, length); + return true; // return 'false' if you want to cancel the request. + }, + "text/plain"); +``` + +### Chunked transfer encoding + +```cpp +auto res = cli.Post( + "/stream", + [](size_t offset, DataSink &sink) { + sink.os << "chunked data 1"; + sink.os << "chunked data 2"; + sink.os << "chunked data 3"; + sink.done(); + return true; // return 'false' if you want to cancel the request. + }, + "text/plain"); ``` + ### With Progress Callback ```cpp -httplib::Client client(url, port); +httplib::Client cli(url, port); // prints: 0 / 000 bytes => 50% complete -std::shared_ptr res = - cli.Get("/", [](uint64_t len, uint64_t total) { - printf("%lld / %lld bytes => %d%% complete\n", - len, total, - (int)((len/total)*100)); - return true; // return 'false' if you want to cancel the request. - } +auto res = cli.Get("/", [](size_t len, size_t total) { + printf("%lld / %lld bytes => %d%% complete\n", + len, total, + (int)(len*100/total)); + return true; // return 'false' if you want to cancel the request. +} ); ``` ![progress](https://user-images.githubusercontent.com/236374/33138910-495c4ecc-cf86-11e7-8693-2fc6d09615c4.gif) -This feature was contributed by [underscorediscovery](https://github.com/yhirose/cpp-httplib/pull/23). +### Authentication + +```cpp +// Basic Authentication +cli.set_basic_auth("user", "pass"); + +// Digest Authentication +cli.set_digest_auth("user", "pass"); + +// Bearer Token Authentication +cli.set_bearer_token_auth("token"); +``` + +> [!NOTE] +> OpenSSL is required for Digest Authentication. -### Basic Authentication +### Proxy server support ```cpp -httplib::Client cli("httplib.org"); +cli.set_proxy("host", port); -auto res = cli.Get("/basic-auth/hello/world", { - httplib::make_basic_authentication_header("hello", "world") -}); -// res->status should be 200 -// res->body should be "{\n \"authenticated\": true, \n \"user\": \"hello\"\n}\n". +// Basic Authentication +cli.set_proxy_basic_auth("user", "pass"); + +// Digest Authentication +cli.set_proxy_digest_auth("user", "pass"); + +// Bearer Token Authentication +cli.set_proxy_bearer_token_auth("pass"); ``` +> [!NOTE] +> OpenSSL is required for Digest Authentication. + ### Range ```cpp -httplib::Client cli("httpbin.org"); +httplib::Client cli("httpcan.org"); auto res = cli.Get("/range/32", { httplib::make_range_header({{1, 10}}) // 'Range: bytes=1-10' @@ -314,41 +1186,369 @@ httplib::make_range_header({{100, 199}, {500, 599}}) // 'Range: bytes=100-199, 5 httplib::make_range_header({{0, 0}, {-1, 1}}) // 'Range: bytes=0-0, -1' ``` -OpenSSL Support ---------------- +### Keep-Alive connection -SSL support is available with `CPPHTTPLIB_OPENSSL_SUPPORT`. `libssl` and `libcrypto` should be linked. +```cpp +httplib::Client cli("localhost", 1234); + +cli.Get("/hello"); // with "Connection: close" + +cli.set_keep_alive(true); +cli.Get("/world"); + +cli.set_keep_alive(false); +cli.Get("/last-request"); // with "Connection: close" +``` + +### Redirect + +```cpp +httplib::Client cli("yahoo.com"); + +auto res = cli.Get("/"); +res->status; // 301 + +cli.set_follow_location(true); +res = cli.Get("/"); +res->status; // 200 +``` + +### Use a specific network interface + +> [!NOTE] +> This feature is not available on Windows, yet. + +```cpp +cli.set_interface("eth0"); // Interface name, IP address or host name +``` + +### Automatic Path Encoding + +The client automatically encodes special characters in URL paths by default: + +```cpp +httplib::Client cli("https://example.com"); + +// Automatic path encoding (default behavior) +cli.set_path_encode(true); +auto res = cli.Get("/path with spaces/file.txt"); // Automatically encodes spaces + +// Disable automatic path encoding +cli.set_path_encode(false); +auto res = cli.Get("/already%20encoded/path"); // Use pre-encoded paths +``` + +- `set_path_encode(bool on)` - Controls automatic encoding of special characters in URL paths + - `true` (default): Automatically encodes spaces, plus signs, newlines, and other special characters + - `false`: Sends paths as-is without encoding (useful for pre-encoded URLs) + +### Performance Note for Local Connections + +> [!WARNING] +> On Windows systems with improperly configured IPv6 settings, using "localhost" as the hostname may cause significant connection delays (up to 2 seconds per request) due to DNS resolution issues. This affects both client and server operations. For better performance when connecting to local services, use "127.0.0.1" instead of "localhost". +> +> See: https://github.com/yhirose/cpp-httplib/issues/366#issuecomment-593004264 + +```cpp +// May be slower on Windows due to DNS resolution delays +httplib::Client cli("localhost", 8080); +httplib::Server svr; +svr.listen("localhost", 8080); + +// Faster alternative for local connections +httplib::Client cli("127.0.0.1", 8080); +httplib::Server svr; +svr.listen("127.0.0.1", 8080); +``` + +## Payload Limit + +The maximum payload body size is limited to 100MB by default for both server and client. You can change it with `set_payload_max_length()` or by defining `CPPHTTPLIB_PAYLOAD_MAX_LENGTH` at compile time. Setting it to `0` disables the limit entirely. + +## Compression + +The server can apply compression to the following MIME type contents: + +- all text types except text/event-stream +- image/svg+xml +- application/javascript +- application/json +- application/xml +- application/protobuf +- application/xhtml+xml + +### Zlib Support + +'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. `libz` should be linked. + +### Brotli Support + +Brotli compression is available with `CPPHTTPLIB_BROTLI_SUPPORT`. Necessary libraries should be linked. +Please see https://github.com/google/brotli for more detail. + +### Zstd Support + +Zstd compression is available with `CPPHTTPLIB_ZSTD_SUPPORT`. Necessary libraries should be linked. +Please see https://github.com/facebook/zstd for more detail. + +### Default `Accept-Encoding` value + +The default `Accept-Encoding` value contains all possible compression types. So, the following two examples are same. ```c++ -#define CPPHTTPLIB_OPENSSL_SUPPORT +res = cli.Get("/resource/foo"); +res = cli.Get("/resource/foo", {{"Accept-Encoding", "br, gzip, deflate, zstd"}}); +``` -SSLServer svr("./cert.pem", "./key.pem"); +If we don't want a response without compression, we have to set `Accept-Encoding` to an empty string. This behavior is similar to curl. + +```c++ +res = cli.Get("/resource/foo", {{"Accept-Encoding", ""}}); +``` + +### Compress request body on client + +```c++ +cli.set_compress(true); +res = cli.Post("/resource/foo", "...", "text/plain"); +``` + +### Compress response body on client + +```c++ +cli.set_decompress(false); +res = cli.Get("/resource/foo"); +res->body; // Compressed data -SSLClient cli("localhost", 8080); -cli.set_ca_cert_path("./ca-bundle.crt"); -cli.enable_server_certificate_verification(true); ``` -Zlib Support ------------- +Unix Domain Socket Support +-------------------------- -'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. +Unix Domain Socket support is available on Linux and macOS. -The server applies gzip compression to the following MIME type contents: +```c++ +// Server +httplib::Server svr; +svr.set_address_family(AF_UNIX).listen("./my-socket.sock", 80); + +// Client +httplib::Client cli("./my-socket.sock"); +cli.set_address_family(AF_UNIX); +``` - * all text types - * image/svg+xml - * application/javascript - * application/json - * application/xml - * application/xhtml+xml +"my-socket.sock" can be a relative path or an absolute path. Your application must have the appropriate permissions for the path. You can also use an abstract socket address on Linux. To use an abstract socket address, prepend a null byte ('\x00') to the path. + +This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior: + + +URI Encoding/Decoding Utilities +------------------------------- + +cpp-httplib provides utility functions for URI encoding and decoding: + +```cpp +#include + +std::string url = "https://example.com/search?q=hello world"; +std::string encoded = httplib::encode_uri(url); +std::string decoded = httplib::decode_uri(encoded); + +std::string param = "hello world"; +std::string encoded_component = httplib::encode_uri_component(param); +std::string decoded_component = httplib::decode_uri_component(encoded_component); +``` + +### Functions + +- `encode_uri(const std::string &value)` - Encodes a full URI, preserving reserved characters like `://`, `?`, `&`, `=` +- `decode_uri(const std::string &value)` - Decodes a URI-encoded string +- `encode_uri_component(const std::string &value)` - Encodes a URI component (query parameter, path segment), encoding all reserved characters +- `decode_uri_component(const std::string &value)` - Decodes a URI component + +Use `encode_uri()` for full URLs and `encode_uri_component()` for individual query parameters or path segments. + +## Stream API + +Process large responses without loading everything into memory. + +```c++ +httplib::Client cli("localhost", 8080); +cli.set_follow_location(true); +... + +auto result = httplib::stream::Get(cli, "/large-file"); +if (result) { + while (result.next()) { + process(result.data(), result.size()); // Process each chunk as it arrives + } +} + +// Or read the entire body at once +auto result2 = httplib::stream::Get(cli, "/file"); +if (result2) { + std::string body = result2.read_all(); +} +``` + +All HTTP methods are supported: `stream::Get`, `Post`, `Put`, `Patch`, `Delete`, `Head`, `Options`. + +See [README-stream.md](README-stream.md) for more details. + +## SSE Client + +```cpp +#include + +int main() { + httplib::Client cli("http://localhost:8080"); + httplib::sse::SSEClient sse(cli, "/events"); + + sse.on_message([](const httplib::sse::SSEMessage &msg) { + std::cout << "Event: " << msg.event << std::endl; + std::cout << "Data: " << msg.data << std::endl; + }); + + sse.start(); // Blocking, with auto-reconnect + return 0; +} +``` + +See [README-sse.md](README-sse.md) for more details. + +## WebSocket + +```cpp +// Server +httplib::Server svr; + +svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws) { + httplib::ws::Message msg; + while (ws.read(msg)) { + if (msg.is_text()) { + ws.send("Echo: " + msg.data); + } + } +}); + +svr.listen("localhost", 8080); +``` + +```cpp +// Client +httplib::ws::WebSocketClient ws("ws://localhost:8080/ws"); + +if (ws.connect()) { + ws.send("Hello, WebSocket!"); + + std::string msg; + if (ws.read(msg)) { + std::cout << "Received: " << msg << std::endl; + } + + ws.close(); +} +``` + +SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.com/ws")`). Subprotocol negotiation (`Sec-WebSocket-Protocol`) is supported via `SubProtocolSelector` callback. + +> **Note:** WebSocket connections occupy a thread for their entire lifetime. If you plan to handle many simultaneous WebSocket connections, consider using a dynamic thread pool: `svr.new_task_queue = [] { return new ThreadPool(8, 64); };` + +See [README-websocket.md](README-websocket.md) for more details. + +## Split httplib.h into .h and .cc + +```console +$ ./split.py -h +usage: split.py [-h] [-e EXTENSION] [-o OUT] + +This script splits httplib.h into .h and .cc parts. + +optional arguments: + -h, --help show this help message and exit + -e EXTENSION, --extension EXTENSION + extension of the implementation file (default: cc) + -o OUT, --out OUT where to write the files (default: out) + +$ ./split.py +Wrote out/httplib.h and out/httplib.cc +``` + +## Dockerfile for Static HTTP Server + +Dockerfile for static HTTP server is available. Port number of this HTTP server is 80, and it serves static files from `/html` directory in the container. + +```bash +> docker build -t cpp-httplib-server . +... + +> docker run --rm -it -p 8080:80 -v ./docker/html:/html cpp-httplib-server +Serving HTTP on 0.0.0.0 port 80 ... +192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1" +192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..." +192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..." +``` + +From Docker Hub + +```bash +> docker run --rm -it -p 8080:80 -v ./docker/html:/html yhirose4dockerhub/cpp-httplib-server +Serving HTTP on 0.0.0.0 port 80 ... +192.168.65.1 - - [31/Aug/2024:21:33:56 +0000] "GET / HTTP/1.1" 200 599 "-" "curl/8.7.1" +192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET / HTTP/1.1" 200 599 "-" "Mozilla/5.0 ..." +192.168.65.1 - - [31/Aug/2024:21:34:26 +0000] "GET /favicon.ico HTTP/1.1" 404 152 "-" "Mozilla/5.0 ..." +``` NOTE ---- -g++ 4.8 cannot build this library since `` in g++4.8 is [broken](https://stackoverflow.com/questions/12530406/is-gcc-4-8-or-earlier-buggy-about-regular-expressions). +### Regular Expression Stack Overflow + +> [!CAUTION] +> When using complex regex patterns in route handlers, be aware that certain patterns may cause stack overflow during pattern matching. This is a known issue with `std::regex` implementations and affects the `dispatch_request()` method. +> +> ```cpp +> // This pattern can cause stack overflow with large input +> svr.Get(".*", handler); +> ``` +> +> Consider using simpler patterns or path parameters to avoid this issue: +> +> ```cpp +> // Safer alternatives +> svr.Get("/users/:id", handler); // Path parameters +> svr.Get(R"(/api/v\d+/.*)", handler); // More specific patterns +> ``` + +### g++ + +g++ 4.8 and below cannot build this library since `` in the versions are [broken](https://stackoverflow.com/questions/12530406/is-gcc-4-8-or-earlier-buggy-about-regular-expressions). + +### Windows + +Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32_LEAN_AND_MEAN` beforehand. + +```cpp +#include +#include +``` + +```cpp +#define WIN32_LEAN_AND_MEAN +#include +#include +``` + +> [!NOTE] +> cpp-httplib officially supports only the latest Visual Studio. It might work with former versions of Visual Studio, but I can no longer verify it. Pull requests are always welcome for the older versions of Visual Studio unless they break the C++11 conformance. + +> [!NOTE] +> Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested. + +## License + +MIT license (© 2026 Yuji Hirose) -License -------- +## Special Thanks To -MIT license (© 2019 Yuji Hirose) +[These folks](https://github.com/yhirose/cpp-httplib/graphs/contributors) made great contributions to polish this library to totally another level from a simple toy! diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 0b3bc9eade..0000000000 --- a/appveyor.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 1.0.{build} -image: Visual Studio 2017 -build_script: -- cmd: >- - cd test - - msbuild.exe test.sln /verbosity:minimal /t:Build /p:Configuration=Debug;Platform=Win32 -test_script: -- cmd: Debug\test.exe \ No newline at end of file diff --git a/benchmark/Makefile b/benchmark/Makefile new file mode 100644 index 0000000000..6fdd5281ea --- /dev/null +++ b/benchmark/Makefile @@ -0,0 +1,50 @@ +CXXFLAGS = -O2 -I.. + +CPPHTTPLIB_CXXFLAGS = -std=c++11 +CROW_CXXFLAGS = -std=c++17 + +CPPHTTPLIB_FLAGS = -DCPPHTTPLIB_THREAD_POOL_COUNT=16 + +BENCH = bombardier -c 10 -d 5s localhost:8080 +MONITOR = ali http://localhost:8080 + +# cpp-httplib +bench: server + @echo "--------------------\n cpp-httplib latest\n--------------------\n" + @./server & export PID=$$!; $(BENCH); kill $${PID} + @echo "" + +monitor: server + @./server & export PID=$$!; $(MONITOR); kill $${PID} + +run : server + @./server + +server : cpp-httplib/main.cpp ../httplib.h + @g++ -o $@ $(CXXFLAGS) $(CPPHTTPLIB_CXXFLAGS) $(CPPHTTPLIB_FLAGS) cpp-httplib/main.cpp + +# crow +bench-crow: server-crow + @echo "-------------\n Crow v1.3.1\n-------------\n" + @./server-crow & export PID=$$!; $(BENCH); kill $${PID} + @echo "" + +monitor-crow: server-crow + @./server-crow & export PID=$$!; $(MONITOR); kill $${PID} + +run-crow : server-crow + @./server-crow + +server-crow : crow/main.cpp crow/crow_all.h + @g++ -o $@ $(CXXFLAGS) $(CROW_CXXFLAGS) crow/main.cpp + +# misc +build: server server-crow + +bench-all: bench-crow bench + +issue: + bombardier -c 10 -d 30s localhost:8080 + +clean: + rm -rf server* diff --git a/benchmark/cpp-httplib/main.cpp b/benchmark/cpp-httplib/main.cpp new file mode 100644 index 0000000000..ab2e757b1b --- /dev/null +++ b/benchmark/cpp-httplib/main.cpp @@ -0,0 +1,12 @@ +#include "httplib.h" +using namespace httplib; + +int main() { + Server svr; + + svr.Get("/", [](const Request &, Response &res) { + res.set_content("Hello World!", "text/plain"); + }); + + svr.listen("0.0.0.0", 8080); +} diff --git a/benchmark/crow/crow_all.h b/benchmark/crow/crow_all.h new file mode 100644 index 0000000000..6de0dac422 --- /dev/null +++ b/benchmark/crow/crow_all.h @@ -0,0 +1,13796 @@ +// SPDX-License-Identifier: BSD-3-Clause AND ISC AND MIT +/*BSD 3-Clause License + +Copyright (c) 2014-2017, ipkn + 2020-2026, CrowCpp +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The Crow logo and other graphic material (excluding third party logos) used are +under exclusive Copyright (c) 2021-2022, Farook Al-Sammarraie (The-EDev), All +rights reserved. +*/ +#pragma once +#ifdef CROW_ENABLE_COMPRESSION + +#include +#include + +// http://zlib.net/manual.html +namespace crow // NOTE: Already documented in "crow/app.h" +{ +namespace compression { +// Values used in the 'windowBits' parameter for deflateInit2. +enum algorithm { + // 15 is the default value for deflate + DEFLATE = 15, + // windowBits can also be greater than 15 for optional gzip encoding. + // Add 16 to windowBits to write a simple gzip header and trailer around the + // compressed data instead of a zlib wrapper. + GZIP = 15 | 16, +}; + +inline std::string compress_string(std::string const &str, algorithm algo) { + std::string compressed_str; + z_stream stream{}; + // Initialize with the default values + if (::deflateInit2(&stream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, algo, 8, + Z_DEFAULT_STRATEGY) == Z_OK) { + char buffer[8192]; + + stream.avail_in = str.size(); + // zlib does not take a const pointer. The data is not altered. + stream.next_in = + const_cast(reinterpret_cast(str.c_str())); + + int code = Z_OK; + do { + stream.avail_out = sizeof(buffer); + stream.next_out = reinterpret_cast(&buffer[0]); + + code = ::deflate(&stream, Z_FINISH); + // Successful and non-fatal error code returned by deflate when used with + // Z_FINISH flush + if (code == Z_OK || code == Z_STREAM_END) { + std::copy(&buffer[0], &buffer[sizeof(buffer) - stream.avail_out], + std::back_inserter(compressed_str)); + } + + } while (code == Z_OK); + + if (code != Z_STREAM_END) compressed_str.clear(); + + ::deflateEnd(&stream); + } + + return compressed_str; +} + +inline std::string decompress_string(std::string const &deflated_string) { + std::string inflated_string; + Bytef tmp[8192]; + + z_stream zstream{}; + zstream.avail_in = deflated_string.size(); + // Nasty const_cast but zlib won't alter its contents + zstream.next_in = const_cast( + reinterpret_cast(deflated_string.c_str())); + // Initialize with automatic header detection, for gzip support + if (::inflateInit2(&zstream, MAX_WBITS | 32) == Z_OK) { + do { + zstream.avail_out = sizeof(tmp); + zstream.next_out = &tmp[0]; + + auto ret = ::inflate(&zstream, Z_NO_FLUSH); + if (ret == Z_OK || ret == Z_STREAM_END) { + std::copy(&tmp[0], &tmp[sizeof(tmp) - zstream.avail_out], + std::back_inserter(inflated_string)); + } else { + // Something went wrong with inflate; make sure we return an empty + // string + inflated_string.clear(); + break; + } + + } while (zstream.avail_out == 0); + + // Free zlib's internal memory + ::inflateEnd(&zstream); + } + + return inflated_string; +} +} // namespace compression +} // namespace crow + +#endif + +namespace crow { +constexpr const char VERSION[] = "master"; +} + +/* + * SHA1 Wikipedia Page: http://en.wikipedia.org/wiki/SHA-1 + * + * Copyright (c) 2012-22 SAURAV MOHAPATRA + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +/** + * \file TinySHA1.hpp + * \author SAURAV MOHAPATRA + * \date 2012-22 + * \brief TinySHA1 - a header only implementation of the SHA1 algorithm in C++. + * Based on the implementation in boost::uuid::details. + * + * In this file are defined: + * - sha1::SHA1 + */ +#ifndef _TINY_SHA1_HPP_ +#define _TINY_SHA1_HPP_ +#include +#include +#include +#include + +/** + * \namespace sha1 + * \brief Here is defined the SHA1 class + */ +namespace sha1 { +/** + * \class SHA1 + * \brief A tiny SHA1 algorithm implementation used internally in the + * Crow server (specifically in crow/websocket.h). + */ +class SHA1 { +public: + typedef uint32_t digest32_t[5]; + typedef uint8_t digest8_t[20]; + inline static uint32_t LeftRotate(uint32_t value, size_t count) { + return (value << count) ^ (value >> (32 - count)); + } + SHA1() { reset(); } + virtual ~SHA1() {} + SHA1(const SHA1 &s) { *this = s; } + const SHA1 &operator=(const SHA1 &s) { + memcpy(m_digest, s.m_digest, 5 * sizeof(uint32_t)); + memcpy(m_block, s.m_block, 64); + m_blockByteIndex = s.m_blockByteIndex; + m_byteCount = s.m_byteCount; + return *this; + } + SHA1 &reset() { + m_digest[0] = 0x67452301; + m_digest[1] = 0xEFCDAB89; + m_digest[2] = 0x98BADCFE; + m_digest[3] = 0x10325476; + m_digest[4] = 0xC3D2E1F0; + m_blockByteIndex = 0; + m_byteCount = 0; + return *this; + } + SHA1 &processByte(uint8_t octet) { + this->m_block[this->m_blockByteIndex++] = octet; + ++this->m_byteCount; + if (m_blockByteIndex == 64) { + this->m_blockByteIndex = 0; + processBlock(); + } + return *this; + } + SHA1 &processBlock(const void *const start, const void *const end) { + const uint8_t *begin = static_cast(start); + const uint8_t *finish = static_cast(end); + while (begin != finish) { + processByte(*begin); + begin++; + } + return *this; + } + SHA1 &processBytes(const void *const data, size_t len) { + const uint8_t *block = static_cast(data); + processBlock(block, block + len); + return *this; + } + const uint32_t *getDigest(digest32_t digest) { + size_t bitCount = this->m_byteCount * 8; + processByte(0x80); + if (this->m_blockByteIndex > 56) { + while (m_blockByteIndex != 0) { + processByte(0); + } + while (m_blockByteIndex < 56) { + processByte(0); + } + } else { + while (m_blockByteIndex < 56) { + processByte(0); + } + } + processByte(0); + processByte(0); + processByte(0); + processByte(0); + processByte(static_cast((bitCount >> 24) & 0xFF)); + processByte(static_cast((bitCount >> 16) & 0xFF)); + processByte(static_cast((bitCount >> 8) & 0xFF)); + processByte(static_cast((bitCount) & 0xFF)); + + memcpy(digest, m_digest, 5 * sizeof(uint32_t)); + return digest; + } + const uint8_t *getDigestBytes(digest8_t digest) { + digest32_t d32; + getDigest(d32); + size_t di = 0; + digest[di++] = ((d32[0] >> 24) & 0xFF); + digest[di++] = ((d32[0] >> 16) & 0xFF); + digest[di++] = ((d32[0] >> 8) & 0xFF); + digest[di++] = ((d32[0]) & 0xFF); + + digest[di++] = ((d32[1] >> 24) & 0xFF); + digest[di++] = ((d32[1] >> 16) & 0xFF); + digest[di++] = ((d32[1] >> 8) & 0xFF); + digest[di++] = ((d32[1]) & 0xFF); + + digest[di++] = ((d32[2] >> 24) & 0xFF); + digest[di++] = ((d32[2] >> 16) & 0xFF); + digest[di++] = ((d32[2] >> 8) & 0xFF); + digest[di++] = ((d32[2]) & 0xFF); + + digest[di++] = ((d32[3] >> 24) & 0xFF); + digest[di++] = ((d32[3] >> 16) & 0xFF); + digest[di++] = ((d32[3] >> 8) & 0xFF); + digest[di++] = ((d32[3]) & 0xFF); + + digest[di++] = ((d32[4] >> 24) & 0xFF); + digest[di++] = ((d32[4] >> 16) & 0xFF); + digest[di++] = ((d32[4] >> 8) & 0xFF); + digest[di++] = ((d32[4]) & 0xFF); + return digest; + } + +protected: + void processBlock() { + uint32_t w[80]; + for (size_t i = 0; i < 16; i++) { + w[i] = (m_block[i * 4 + 0] << 24); + w[i] |= (m_block[i * 4 + 1] << 16); + w[i] |= (m_block[i * 4 + 2] << 8); + w[i] |= (m_block[i * 4 + 3]); + } + for (size_t i = 16; i < 80; i++) { + w[i] = LeftRotate((w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]), 1); + } + + uint32_t a = m_digest[0]; + uint32_t b = m_digest[1]; + uint32_t c = m_digest[2]; + uint32_t d = m_digest[3]; + uint32_t e = m_digest[4]; + + for (std::size_t i = 0; i < 80; ++i) { + uint32_t f = 0; + uint32_t k = 0; + + if (i < 20) { + f = (b & c) | (~b & d); + k = 0x5A827999; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + uint32_t temp = LeftRotate(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = LeftRotate(b, 30); + b = a; + a = temp; + } + + m_digest[0] += a; + m_digest[1] += b; + m_digest[2] += c; + m_digest[3] += d; + m_digest[4] += e; + } + +private: + digest32_t m_digest; + uint8_t m_block[64]; + size_t m_blockByteIndex; + size_t m_byteCount; +}; +} // namespace sha1 +#endif + +#include +#include +#include +#include +#include +#include +#include + +namespace crow { + +// ---------------------------------------------------------------------------- +// qs_parse (modified) +// https://github.com/bartgrantham/qs_parse +// ---------------------------------------------------------------------------- +/* Similar to strncmp, but handles URL-encoding for either string */ +int qs_strncmp(const char *s, const char *qs, size_t n); + +/* Finds the beginning of each key/value pair and stores a pointer in qs_kv. + * Also decodes the value portion of the k/v pair *in-place*. In a future + * enhancement it will also have a compile-time option of sorting qs_kv + * alphabetically by key. */ +size_t qs_parse(char *qs, char *qs_kv[], size_t qs_kv_size, bool parse_url); + +/* Used by qs_parse to decode the value portion of a k/v pair */ +int qs_decode(char *qs); + +/* Looks up the value according to the key on a pre-processed query string + * A future enhancement will be a compile-time option to look up the key + * in a pre-sorted qs_kv array via a binary search. */ +// char * qs_k2v(const char * key, char * qs_kv[], int qs_kv_size); +char *qs_k2v(const char *key, char *const *qs_kv, size_t qs_kv_size, int nth); + +/* Non-destructive lookup of value, based on key. User provides the + * destinaton string and length. */ +char *qs_scanvalue(const char *key, const char *qs, char *val, size_t val_len); + +// TODO: implement sorting of the qs_kv array; for now ensure it's not compiled +#undef _qsSORTING + +// isxdigit _is_ available in , but let's avoid another header instead +#define CROW_QS_ISHEX(x) \ + ((((x) >= '0' && (x) <= '9') || ((x) >= 'A' && (x) <= 'F') || \ + ((x) >= 'a' && (x) <= 'f')) \ + ? 1 \ + : 0) +#define CROW_QS_HEX2DEC(x) \ + (((x) >= '0' && (x) <= '9') ? (x) - 48 \ + : ((x) >= 'A' && (x) <= 'F') ? (x) - 55 \ + : ((x) >= 'a' && (x) <= 'f') ? (x) - 87 \ + : 0) +#define CROW_QS_ISQSCHR(x) \ + ((((x) == '=') || ((x) == '#') || ((x) == '&') || ((x) == '\0')) ? 0 : 1) + +inline int qs_strncmp(const char *s, const char *qs, size_t n) { + unsigned char u1, u2, unyb, lnyb; + + while (n-- > 0) { + u1 = static_cast(*s++); + u2 = static_cast(*qs++); + + if (!CROW_QS_ISQSCHR(u1)) { u1 = '\0'; } + if (!CROW_QS_ISQSCHR(u2)) { u2 = '\0'; } + + if (u1 == '+') { u1 = ' '; } + if (u1 == '%') // easier/safer than scanf + { + unyb = static_cast(*s++); + lnyb = static_cast(*s++); + if (CROW_QS_ISHEX(unyb) && CROW_QS_ISHEX(lnyb)) + u1 = (CROW_QS_HEX2DEC(unyb) * 16) + CROW_QS_HEX2DEC(lnyb); + else + u1 = '\0'; + } + + if (u2 == '+') { u2 = ' '; } + if (u2 == '%') // easier/safer than scanf + { + unyb = static_cast(*qs++); + lnyb = static_cast(*qs++); + if (CROW_QS_ISHEX(unyb) && CROW_QS_ISHEX(lnyb)) + u2 = (CROW_QS_HEX2DEC(unyb) * 16) + CROW_QS_HEX2DEC(lnyb); + else + u2 = '\0'; + } + + if (u1 != u2) return u1 - u2; + if (u1 == '\0') return 0; + } + if (CROW_QS_ISQSCHR(*qs)) + return -1; + else + return 0; +} + +inline size_t qs_parse(char *qs, char *qs_kv[], size_t qs_kv_size, + bool parse_url = true) { + size_t i, j; + char *substr_ptr; + + for (i = 0; i < qs_kv_size; i++) + qs_kv[i] = NULL; + + // find the beginning of the k/v substrings or the fragment + substr_ptr = parse_url ? qs + strcspn(qs, "?#") : qs; + if (parse_url) { + if (substr_ptr[0] != '\0') + substr_ptr++; + else + return 0; // no query or fragment + } + + i = 0; + while (i < qs_kv_size) { + qs_kv[i] = substr_ptr; + j = strcspn(substr_ptr, "&"); + if (substr_ptr[j] == '\0') { + i++; + break; + } // x &'s -> means x iterations of this loop -> means *x+1* k/v pairs + substr_ptr += j + 1; + i++; + } + + // we only decode the values in place, the keys could have '='s in them + // which will hose our ability to distinguish keys from values later + for (j = 0; j < i; j++) { + substr_ptr = qs_kv[j] + strcspn(qs_kv[j], "=&#"); + if (substr_ptr[0] == '&' || + substr_ptr[0] == '\0') // blank value: skip decoding + substr_ptr[0] = '\0'; + else + qs_decode(++substr_ptr); + } + +#ifdef _qsSORTING +// TODO: qsort qs_kv, using qs_strncmp() for the comparison +#endif + + return i; +} + +inline int qs_decode(char *qs) { + int i = 0, j = 0; + + while (CROW_QS_ISQSCHR(qs[j])) { + if (qs[j] == '+') { + qs[i] = ' '; + } else if (qs[j] == '%') // easier/safer than scanf + { + if (!CROW_QS_ISHEX(qs[j + 1]) || !CROW_QS_ISHEX(qs[j + 2])) { + qs[i] = '\0'; + return i; + } + qs[i] = (CROW_QS_HEX2DEC(qs[j + 1]) * 16) + CROW_QS_HEX2DEC(qs[j + 2]); + j += 2; + } else { + qs[i] = qs[j]; + } + i++; + j++; + } + qs[i] = '\0'; + + return i; +} + +inline char *qs_k2v(const char *key, char *const *qs_kv, size_t qs_kv_size, + int nth = 0) { + size_t i; + size_t key_len, skip; + + key_len = strlen(key); + +#ifdef _qsSORTING +// TODO: binary search for key in the sorted qs_kv +#else // _qsSORTING + for (i = 0; i < qs_kv_size; i++) { + // we rely on the unambiguous '=' to find the value in our k/v pair + if (qs_strncmp(key, qs_kv[i], key_len) == 0) { + skip = strcspn(qs_kv[i], "="); + if (qs_kv[i][skip] == '=') skip++; + // return (zero-char value) ? ptr to trailing '\0' : ptr to value + if (nth == 0) + return qs_kv[i] + skip; + else + --nth; + } + } +#endif // _qsSORTING + + return nullptr; +} + +inline std::unique_ptr> +qs_dict_name2kv(const char *dict_name, char *const *qs_kv, size_t qs_kv_size, + int nth = 0) { + size_t i; + size_t name_len, skip_to_eq, skip_to_brace_open, skip_to_brace_close; + + name_len = strlen(dict_name); + +#ifdef _qsSORTING +// TODO: binary search for key in the sorted qs_kv +#else // _qsSORTING + for (i = 0; i < qs_kv_size; i++) { + if (strncmp(dict_name, qs_kv[i], name_len) == 0) { + skip_to_eq = strcspn(qs_kv[i], "="); + if (qs_kv[i][skip_to_eq] == '=') skip_to_eq++; + skip_to_brace_open = strcspn(qs_kv[i], "["); + if (qs_kv[i][skip_to_brace_open] == '[') skip_to_brace_open++; + skip_to_brace_close = strcspn(qs_kv[i], "]"); + + if (skip_to_brace_open <= skip_to_brace_close && skip_to_brace_open > 0 && + skip_to_brace_close > 0 && nth == 0) { + auto key = std::string(qs_kv[i] + skip_to_brace_open, + skip_to_brace_close - skip_to_brace_open); + auto value = std::string(qs_kv[i] + skip_to_eq); + return std::unique_ptr>( + new std::pair(key, value)); + } else { + --nth; + } + } + } +#endif // _qsSORTING + + return nullptr; +} + +inline char *qs_scanvalue(const char *key, const char *qs, char *val, + size_t val_len) { + const char *tmp = strchr(qs, '?'); + + // find the beginning of the k/v substrings + if (tmp != nullptr) qs = tmp + 1; + + const size_t key_len = strlen(key); + while (*qs != '#' && *qs != '\0') { + if (qs_strncmp(key, qs, key_len) == 0) break; + qs += strcspn(qs, "&"); + if (*qs == '&') qs++; + } + + if (qs[0] == '\0') return nullptr; + + qs += strcspn(qs, "=&#"); + if (qs[0] == '=') { + qs++; + size_t i = strcspn(qs, "&=#"); +#ifdef _MSC_VER + strncpy_s(val, val_len, qs, + (val_len - 1) < (i + 1) ? (val_len - 1) : (i + 1)); +#else + strncpy(val, qs, (val_len - 1) < (i + 1) ? (val_len - 1) : (i + 1)); +#endif + qs_decode(val); + } else { + if (val_len > 0) val[0] = '\0'; + } + + return val; +} +} // namespace crow +// ---------------------------------------------------------------------------- + +namespace crow { +struct request; +/// A class to represent any data coming after the `?` in the request URL into +/// key-value pairs. +class query_string { +public: + static const int MAX_KEY_VALUE_PAIRS_COUNT = 256; + + query_string() = default; + + query_string(const query_string &qs) : url_(qs.url_) { + for (auto p : qs.key_value_pairs_) { + key_value_pairs_.push_back((char *)(p - qs.url_.c_str() + url_.c_str())); + } + } + + query_string &operator=(const query_string &qs) { + url_ = qs.url_; + key_value_pairs_.clear(); + for (auto p : qs.key_value_pairs_) { + key_value_pairs_.push_back((char *)(p - qs.url_.c_str() + url_.c_str())); + } + return *this; + } + + query_string &operator=(query_string &&qs) noexcept { + key_value_pairs_ = std::move(qs.key_value_pairs_); + char *old_data = (char *)qs.url_.c_str(); + url_ = std::move(qs.url_); + for (auto &p : key_value_pairs_) { + p += (char *)url_.c_str() - old_data; + } + return *this; + } + + query_string(std::string params, bool url = true) : url_(std::move(params)) { + if (url_.empty()) return; + + key_value_pairs_.resize(MAX_KEY_VALUE_PAIRS_COUNT); + size_t count = qs_parse(&url_[0], &key_value_pairs_[0], + MAX_KEY_VALUE_PAIRS_COUNT, url); + + key_value_pairs_.resize(count); + key_value_pairs_.shrink_to_fit(); + } + + void clear() { + key_value_pairs_.clear(); + url_.clear(); + } + + friend std::ostream &operator<<(std::ostream &os, const query_string &qs) { + os << "[ "; + for (size_t i = 0; i < qs.key_value_pairs_.size(); ++i) { + if (i) os << ", "; + os << qs.key_value_pairs_[i]; + } + os << " ]"; + return os; + } + + /// Get a value from a name, used for `?name=value`. + + /// + /// Note: this method returns the value of the first occurrence of the key + /// only, to return all occurrences, see \ref get_list(). + char *get(const std::string &name) const { + char *ret = + qs_k2v(name.c_str(), key_value_pairs_.data(), key_value_pairs_.size()); + return ret; + } + + /// Works similar to \ref get() except it removes the item from the query + /// string. + char *pop(const std::string &name) { + char *ret = get(name); + if (ret != nullptr) { + const std::string key_name = name + '='; + for (unsigned int i = 0; i < key_value_pairs_.size(); i++) { + std::string str_item(key_value_pairs_[i]); + if (str_item.find(key_name) == 0) { + key_value_pairs_.erase(key_value_pairs_.begin() + i); + break; + } + } + } + return ret; + } + + /// Returns a list of values, passed as + /// `?name[]=value1&name[]=value2&...name[]=valuen` with n being the size of + /// the list. + + /// + /// Note: Square brackets in the above example are controlled by + /// `use_brackets` boolean (true by default). If set to false, the example + /// becomes `?name=value1,name=value2...name=valuen` + std::vector get_list(const std::string &name, + bool use_brackets = true) const { + std::vector ret; + std::string plus = name + (use_brackets ? "[]" : ""); + char *element = nullptr; + + int count = 0; + while (1) { + element = qs_k2v(plus.c_str(), key_value_pairs_.data(), + key_value_pairs_.size(), count++); + if (!element) break; + ret.push_back(element); + } + return ret; + } + + /// Similar to \ref get_list() but it removes the + std::vector pop_list(const std::string &name, + bool use_brackets = true) { + std::vector ret = get_list(name, use_brackets); + const size_t name_len = name.length(); + if (!ret.empty()) { + for (unsigned int i = 0; i < key_value_pairs_.size(); i++) { + std::string str_item(key_value_pairs_[i]); + if (str_item.find(name) == 0) { + if (use_brackets && str_item.find("[]=", name_len) == name_len) { + key_value_pairs_.erase(key_value_pairs_.begin() + i--); + } else if (!use_brackets && + str_item.find('=', name_len) == name_len) { + key_value_pairs_.erase(key_value_pairs_.begin() + i--); + } + } + } + } + return ret; + } + + /// Works similar to \ref get_list() except the brackets are mandatory must + /// not be empty. + + /// + /// For example calling `get_dict(yourname)` on + /// `?yourname[sub1]=42&yourname[sub2]=84` would give a map containing `{sub1 + /// : 42, sub2 : 84}`. + /// + /// if your query string has both empty brackets and ones with a key inside, + /// use pop_list() to get all the values without a key before running this + /// method. + std::unordered_map + get_dict(const std::string &name) const { + std::unordered_map ret; + + int count = 0; + while (1) { + if (auto element = qs_dict_name2kv(name.c_str(), key_value_pairs_.data(), + key_value_pairs_.size(), count++)) + ret.insert(*element); + else + break; + } + return ret; + } + + /// Works the same as \ref get_dict() but removes the values from the query + /// string. + std::unordered_map + pop_dict(const std::string &name) { + const std::string name_value = name + '['; + std::unordered_map ret = get_dict(name); + if (!ret.empty()) { + for (unsigned int i = 0; i < key_value_pairs_.size(); i++) { + std::string str_item(key_value_pairs_[i]); + if (str_item.find(name_value) == 0) { + key_value_pairs_.erase(key_value_pairs_.begin() + i--); + } + } + } + return ret; + } + + std::vector keys() const { + std::vector keys; + keys.reserve(key_value_pairs_.size()); + + for (const char *const element : key_value_pairs_) { + const char *delimiter = strchr(element, '='); + if (delimiter) + keys.emplace_back(element, delimiter); + else + keys.emplace_back(element); + } + + return keys; + } + +private: + std::string url_; + std::vector key_value_pairs_; +}; + +} // namespace crow + +// This file is generated from nginx/conf/mime.types using nginx_mime2cpp.py on +// 2021-12-03. +#include +#include + +namespace crow { +const std::unordered_map mime_types{ + {"gz", "application/gzip"}, + {"shtml", "text/html"}, + {"htm", "text/html"}, + {"html", "text/html"}, + {"css", "text/css"}, + {"xml", "text/xml"}, + {"gif", "image/gif"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"js", "application/javascript"}, + {"atom", "application/atom+xml"}, + {"rss", "application/rss+xml"}, + {"mml", "text/mathml"}, + {"txt", "text/plain"}, + {"jad", "text/vnd.sun.j2me.app-descriptor"}, + {"wml", "text/vnd.wap.wml"}, + {"htc", "text/x-component"}, + {"avif", "image/avif"}, + {"png", "image/png"}, + {"svgz", "image/svg+xml"}, + {"svg", "image/svg+xml"}, + {"tiff", "image/tiff"}, + {"tif", "image/tiff"}, + {"wbmp", "image/vnd.wap.wbmp"}, + {"webp", "image/webp"}, + {"ico", "image/x-icon"}, + {"jng", "image/x-jng"}, + {"bmp", "image/x-ms-bmp"}, + {"woff", "font/woff"}, + {"woff2", "font/woff2"}, + {"ear", "application/java-archive"}, + {"war", "application/java-archive"}, + {"jar", "application/java-archive"}, + {"json", "application/json"}, + {"hqx", "application/mac-binhex40"}, + {"doc", "application/msword"}, + {"pdf", "application/pdf"}, + {"ai", "application/postscript"}, + {"eps", "application/postscript"}, + {"ps", "application/postscript"}, + {"rtf", "application/rtf"}, + {"m3u8", "application/vnd.apple.mpegurl"}, + {"kml", "application/vnd.google-earth.kml+xml"}, + {"kmz", "application/vnd.google-earth.kmz"}, + {"xls", "application/vnd.ms-excel"}, + {"eot", "application/vnd.ms-fontobject"}, + {"ppt", "application/vnd.ms-powerpoint"}, + {"odg", "application/vnd.oasis.opendocument.graphics"}, + {"odp", "application/vnd.oasis.opendocument.presentation"}, + {"ods", "application/vnd.oasis.opendocument.spreadsheet"}, + {"odt", "application/vnd.oasis.opendocument.text"}, + {"pptx", "application/" + "vnd.openxmlformats-officedocument.presentationml.presentation"}, + {"xlsx", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}, + {"docx", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document"}, + {"wmlc", "application/vnd.wap.wmlc"}, + {"wasm", "application/wasm"}, + {"7z", "application/x-7z-compressed"}, + {"cco", "application/x-cocoa"}, + {"jardiff", "application/x-java-archive-diff"}, + {"jnlp", "application/x-java-jnlp-file"}, + {"run", "application/x-makeself"}, + {"pm", "application/x-perl"}, + {"pl", "application/x-perl"}, + {"pdb", "application/x-pilot"}, + {"prc", "application/x-pilot"}, + {"rar", "application/x-rar-compressed"}, + {"rpm", "application/x-redhat-package-manager"}, + {"sea", "application/x-sea"}, + {"swf", "application/x-shockwave-flash"}, + {"sit", "application/x-stuffit"}, + {"tk", "application/x-tcl"}, + {"tcl", "application/x-tcl"}, + {"crt", "application/x-x509-ca-cert"}, + {"pem", "application/x-x509-ca-cert"}, + {"der", "application/x-x509-ca-cert"}, + {"xpi", "application/x-xpinstall"}, + {"xhtml", "application/xhtml+xml"}, + {"xspf", "application/xspf+xml"}, + {"zip", "application/zip"}, + {"dll", "application/octet-stream"}, + {"exe", "application/octet-stream"}, + {"bin", "application/octet-stream"}, + {"deb", "application/octet-stream"}, + {"dmg", "application/octet-stream"}, + {"img", "application/octet-stream"}, + {"iso", "application/octet-stream"}, + {"msm", "application/octet-stream"}, + {"msp", "application/octet-stream"}, + {"msi", "application/octet-stream"}, + {"kar", "audio/midi"}, + {"midi", "audio/midi"}, + {"mid", "audio/midi"}, + {"mp3", "audio/mpeg"}, + {"ogg", "audio/ogg"}, + {"m4a", "audio/x-m4a"}, + {"ra", "audio/x-realaudio"}, + {"3gp", "video/3gpp"}, + {"3gpp", "video/3gpp"}, + {"ts", "video/mp2t"}, + {"mp4", "video/mp4"}, + {"mpg", "video/mpeg"}, + {"mpeg", "video/mpeg"}, + {"mov", "video/quicktime"}, + {"webm", "video/webm"}, + {"flv", "video/x-flv"}, + {"m4v", "video/x-m4v"}, + {"mng", "video/x-mng"}, + {"asf", "video/x-ms-asf"}, + {"asx", "video/x-ms-asf"}, + {"wmv", "video/x-ms-wmv"}, + {"avi", "video/x-msvideo"}}; +} + +// settings for crow +// TODO(ipkn) replace with runtime config. libucl? + +/* #ifdef - enables debug mode */ +// #define CROW_ENABLE_DEBUG + +/* #ifdef - enables logging */ +#define CROW_ENABLE_LOGGING + +/* #ifdef - enforces section 5.2 and 6.1 of RFC6455 (only accepting masked + * messages from clients) */ +// #define CROW_ENFORCE_WS_SPEC + +/* #define - specifies log level */ +/* + Debug = 0 + Info = 1 + Warning = 2 + Error = 3 + Critical = 4 + + default to INFO +*/ +#ifndef CROW_LOG_LEVEL +#define CROW_LOG_LEVEL 1 +#endif + +#ifndef CROW_STATIC_DIRECTORY +#define CROW_STATIC_DIRECTORY "static/" +#endif +#ifndef CROW_STATIC_ENDPOINT +#define CROW_STATIC_ENDPOINT "/static/" +#endif + +// compiler flags + +#if defined(_MSC_VER) +#if _MSC_VER < 1900 +#define CROW_MSVC_WORKAROUND +#define constexpr const +#define noexcept throw() +#endif +#endif + +#ifdef CROW_USE_BOOST +#include +#include +#ifdef CROW_ENABLE_SSL +#include +#endif +#else +#ifndef ASIO_STANDALONE +#define ASIO_STANDALONE +#endif +#include +#include +#ifdef CROW_ENABLE_SSL +#include +#endif +#endif + +#if (defined(CROW_USE_BOOST) && BOOST_VERSION >= 107000) || \ + (ASIO_VERSION >= 101008) +#define GET_IO_CONTEXT(s) ((asio::io_context &)(s).get_executor().context()) +#else +#define GET_IO_CONTEXT(s) ((s).get_io_service()) +#endif + +namespace crow { +#ifdef CROW_USE_BOOST +namespace asio = boost::asio; +using error_code = boost::system::error_code; +#else +using error_code = asio::error_code; +#endif +using tcp = asio::ip::tcp; +using stream_protocol = asio::local::stream_protocol; + +/// A wrapper for the asio::ip::tcp::socket and asio::ssl::stream +struct SocketAdaptor { + using context = void; + SocketAdaptor(asio::io_context &io_context, context *) + : socket_(io_context) {} + + asio::io_context &get_io_context() { return GET_IO_CONTEXT(socket_); } + + /// Get the TCP socket handling data transfers, regardless of what layer is + /// handling transfers on top of the socket. + tcp::socket &raw_socket() { return socket_; } + + /// Get the object handling data transfers, this can be either a TCP socket or + /// an SSL stream (if SSL is enabled). + tcp::socket &socket() { return socket_; } + + tcp::endpoint remote_endpoint() const { return socket_.remote_endpoint(); } + + std::string address() const { + return socket_.remote_endpoint().address().to_string(); + } + + bool is_open() const { return socket_.is_open(); } + + void close() { + error_code ec; + socket_.close(ec); + } + + void shutdown_readwrite() { + error_code ec; + socket_.shutdown(asio::socket_base::shutdown_type::shutdown_both, ec); + } + + void shutdown_write() { + error_code ec; + socket_.shutdown(asio::socket_base::shutdown_type::shutdown_send, ec); + } + + void shutdown_read() { + error_code ec; + socket_.shutdown(asio::socket_base::shutdown_type::shutdown_receive, ec); + } + + template void start(F f) { f(error_code()); } + + tcp::socket socket_; +}; + +struct UnixSocketAdaptor { + using context = void; + UnixSocketAdaptor(asio::io_context &io_context, context *) + : socket_(io_context) {} + + asio::io_context &get_io_context() { return GET_IO_CONTEXT(socket_); } + + stream_protocol::socket &raw_socket() { return socket_; } + + stream_protocol::socket &socket() { return socket_; } + + stream_protocol::endpoint remote_endpoint() { + return socket_.local_endpoint(); + } + + std::string address() const { return ""; } + + bool is_open() { return socket_.is_open(); } + + void close() { + error_code ec; + socket_.close(ec); + } + + void shutdown_readwrite() { + error_code ec; + socket_.shutdown(asio::socket_base::shutdown_type::shutdown_both, ec); + } + + void shutdown_write() { + error_code ec; + socket_.shutdown(asio::socket_base::shutdown_type::shutdown_send, ec); + } + + void shutdown_read() { + error_code ec; + socket_.shutdown(asio::socket_base::shutdown_type::shutdown_receive, ec); + } + + template void start(F f) { f(error_code()); } + + stream_protocol::socket socket_; +}; + +#ifdef CROW_ENABLE_SSL +struct SSLAdaptor { + using context = asio::ssl::context; + using ssl_socket_t = asio::ssl::stream; + SSLAdaptor(asio::io_context &io_context, context *ctx) + : ssl_socket_(new ssl_socket_t(io_context, *ctx)) {} + + asio::ssl::stream &socket() { return *ssl_socket_; } + + tcp::socket::lowest_layer_type &raw_socket() { + return ssl_socket_->lowest_layer(); + } + + tcp::endpoint remote_endpoint() { return raw_socket().remote_endpoint(); } + + std::string address() const { + return ssl_socket_->lowest_layer().remote_endpoint().address().to_string(); + } + + bool is_open() { return ssl_socket_ ? raw_socket().is_open() : false; } + + void close() { + if (is_open()) { + error_code ec; + raw_socket().close(ec); + } + } + + void shutdown_readwrite() { + if (is_open()) { + error_code ec; + raw_socket().shutdown(asio::socket_base::shutdown_type::shutdown_both, + ec); + } + } + + void shutdown_write() { + if (is_open()) { + error_code ec; + raw_socket().shutdown(asio::socket_base::shutdown_type::shutdown_send, + ec); + } + } + + void shutdown_read() { + if (is_open()) { + error_code ec; + raw_socket().shutdown(asio::socket_base::shutdown_type::shutdown_receive, + ec); + } + } + + asio::io_context &get_io_context() { return GET_IO_CONTEXT(raw_socket()); } + + template void start(F f) { + ssl_socket_->async_handshake(asio::ssl::stream_base::server, + [f](const error_code &ec) { f(ec); }); + } + + std::unique_ptr> ssl_socket_; +}; +#endif +} // namespace crow + +#include +#include +#include +#include +#include +#include + +namespace crow { +enum class LogLevel { +#ifndef ERROR +#ifndef DEBUG + DEBUG = 0, + INFO, + WARNING, + ERROR, + CRITICAL, +#endif +#endif + + Debug = 0, + Info, + Warning, + Error, + Critical, +}; + +class ILogHandler { +public: + virtual ~ILogHandler() = default; + + virtual void log(const std::string &message, LogLevel level) = 0; +}; + +class CerrLogHandler : public ILogHandler { +public: + void log(const std::string &message, LogLevel level) override { + std::string log_msg; + log_msg.reserve(message.length() + 1 + 32 + 3 + 8 + 2); + log_msg.append("(").append(timestamp()).append(") ["); + + switch (level) { + case LogLevel::Debug: log_msg.append("DEBUG "); break; + case LogLevel::Info: log_msg.append("INFO "); break; + case LogLevel::Warning: log_msg.append("WARNING "); break; + case LogLevel::Error: log_msg.append("ERROR "); break; + case LogLevel::Critical: log_msg.append("CRITICAL"); break; + } + + log_msg.append("] ").append(message); + + std::cerr << log_msg << std::endl; + } + +private: + static std::string timestamp() { + char date[32]; + time_t t = time(0); + + tm my_tm; + +#if defined(_MSC_VER) || defined(__MINGW32__) +#ifdef CROW_USE_LOCALTIMEZONE + localtime_s(&my_tm, &t); +#else + gmtime_s(&my_tm, &t); +#endif +#else +#ifdef CROW_USE_LOCALTIMEZONE + localtime_r(&t, &my_tm); +#else + gmtime_r(&t, &my_tm); +#endif +#endif + + size_t sz = strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S", &my_tm); + return std::string(date, date + sz); + } +}; + +class logger { +public: + logger(LogLevel level) : level_(level) {} + ~logger() { +#ifdef CROW_ENABLE_LOGGING + if (level_ >= get_current_log_level()) { + get_handler_ref()->log(stringstream_.str(), level_); + } +#endif + } + + // + template logger &operator<<(T const &value) { +#ifdef CROW_ENABLE_LOGGING + if (level_ >= get_current_log_level()) { stringstream_ << value; } +#endif + return *this; + } + + // + static void setLogLevel(LogLevel level) { get_log_level_ref() = level; } + + static void setHandler(ILogHandler *handler) { get_handler_ref() = handler; } + + static LogLevel get_current_log_level() { return get_log_level_ref(); } + +private: + // + static LogLevel &get_log_level_ref() { + static LogLevel current_level = static_cast(CROW_LOG_LEVEL); + return current_level; + } + static ILogHandler *&get_handler_ref() { + static CerrLogHandler default_handler; + static ILogHandler *current_handler = &default_handler; + return current_handler; + } + + // + std::ostringstream stringstream_; + LogLevel level_; +}; +} // namespace crow + +#define CROW_LOG_CRITICAL \ + if (crow::logger::get_current_log_level() <= crow::LogLevel::Critical) \ + crow::logger(crow::LogLevel::Critical) +#define CROW_LOG_ERROR \ + if (crow::logger::get_current_log_level() <= crow::LogLevel::Error) \ + crow::logger(crow::LogLevel::Error) +#define CROW_LOG_WARNING \ + if (crow::logger::get_current_log_level() <= crow::LogLevel::Warning) \ + crow::logger(crow::LogLevel::Warning) +#define CROW_LOG_INFO \ + if (crow::logger::get_current_log_level() <= crow::LogLevel::Info) \ + crow::logger(crow::LogLevel::Info) +#define CROW_LOG_DEBUG \ + if (crow::logger::get_current_log_level() <= crow::LogLevel::Debug) \ + crow::logger(crow::LogLevel::Debug) + +#include + +namespace crow { +/// An abstract class that allows any other class to be returned by a handler. +struct returnable { + std::string content_type; + virtual std::string dump() const = 0; + + returnable(std::string ctype) : content_type{ctype} {} + + virtual ~returnable() {} +}; +} // namespace crow + +#ifdef CROW_USE_BOOST +#include +#ifdef CROW_ENABLE_SSL +#include +#endif +#else +#ifndef ASIO_STANDALONE +#define ASIO_STANDALONE +#endif +#include +#ifdef CROW_ENABLE_SSL +#include +#endif +#endif + +namespace crow { +#ifdef CROW_USE_BOOST +namespace asio = boost::asio; +using error_code = boost::system::error_code; +#else +using error_code = asio::error_code; +#endif +using tcp = asio::ip::tcp; +using stream_protocol = asio::local::stream_protocol; + +struct TCPAcceptor { + using endpoint = tcp::endpoint; + tcp::acceptor acceptor_; + TCPAcceptor(asio::io_context &io_context) : acceptor_(io_context) {} + + int16_t port() const { return acceptor_.local_endpoint().port(); } + std::string address() const { + return acceptor_.local_endpoint().address().to_string(); + } + std::string url_display(bool ssl_used) const { + auto address = acceptor_.local_endpoint().address(); + return (ssl_used ? "https://" : "http://") + + (address.is_v4() ? address.to_string() + : "[" + address.to_string() + "]") + + ":" + std::to_string(acceptor_.local_endpoint().port()); + } + tcp::acceptor &raw_acceptor() { return acceptor_; } + endpoint local_endpoint() const { return acceptor_.local_endpoint(); } + inline static tcp::acceptor::reuse_address reuse_address_option() { + return tcp::acceptor::reuse_address(true); + } +}; + +struct UnixSocketAcceptor { + using endpoint = stream_protocol::endpoint; + stream_protocol::acceptor acceptor_; + UnixSocketAcceptor(asio::io_context &io_context) : acceptor_(io_context) {} + + int16_t port() const { return 0; } + std::string address() const { return acceptor_.local_endpoint().path(); } + std::string url_display(bool) const { + return acceptor_.local_endpoint().path(); + } + stream_protocol::acceptor &raw_acceptor() { return acceptor_; } + endpoint local_endpoint() const { return acceptor_.local_endpoint(); } + inline static stream_protocol::acceptor::reuse_address + reuse_address_option() { + // reuse addr must be false + // (https://github.com/chriskohlhoff/asio/issues/622) + return stream_protocol::acceptor::reuse_address(false); + } +}; +} // namespace crow + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +// TODO(EDev): Adding C++20's [[likely]] and [[unlikely]] attributes might be +// useful +#if defined(__GNUG__) || defined(__clang__) +#define CROW_LIKELY(X) __builtin_expect(!!(X), 1) +#define CROW_UNLIKELY(X) __builtin_expect(!!(X), 0) +#else +#define CROW_LIKELY(X) (X) +#define CROW_UNLIKELY(X) (X) +#endif + +namespace crow { +/// @cond SKIP +namespace black_magic { +#ifndef CROW_MSVC_WORKAROUND +/// Out of Range Exception for const_str +struct OutOfRange { + OutOfRange(unsigned /*pos*/, unsigned /*length*/) {} +}; +/// Helper function to throw an exception if i is larger than len +constexpr unsigned requires_in_range(unsigned i, unsigned len) { + return i >= len ? throw OutOfRange(i, len) : i; +} + +/// A constant string implementation. +class const_str { + const char *const begin_; + unsigned size_; + +public: + template + constexpr const_str(const char (&arr)[N]) : begin_(arr), size_(N - 1) { + static_assert(N >= 1, "not a string literal"); + } + constexpr char operator[](unsigned i) const { + return requires_in_range(i, size_), begin_[i]; + } + + constexpr operator const char *() const { return begin_; } + + constexpr const char *begin() const { return begin_; } + constexpr const char *end() const { return begin_ + size_; } + + constexpr unsigned size() const { return size_; } +}; + +constexpr unsigned find_closing_tag(const_str s, unsigned p) { + return s[p] == '>' ? p : find_closing_tag(s, p + 1); +} + +/// Check that the CROW_ROUTE string is valid +constexpr bool is_valid(const_str s, unsigned i = 0, int f = 0) { + return i == s.size() ? f == 0 + : f < 0 || f >= 2 ? false + : s[i] == '<' ? is_valid(s, i + 1, f + 1) + : s[i] == '>' ? is_valid(s, i + 1, f - 1) + : is_valid(s, i + 1, f); +} + +constexpr bool is_equ_p(const char *a, const char *b, unsigned n) { + return *a == 0 && *b == 0 && n == 0 ? true + : (*a == 0 || *b == 0) ? false + : n == 0 ? true + : *a != *b ? false + : is_equ_p(a + 1, b + 1, n - 1); +} + +constexpr bool is_equ_n(const_str a, unsigned ai, const_str b, unsigned bi, + unsigned n) { + return ai + n > a.size() || bi + n > b.size() ? false + : n == 0 ? true + : a[ai] != b[bi] ? false + : is_equ_n(a, ai + 1, b, bi + 1, n - 1); +} + +constexpr bool is_int(const_str s, unsigned i) { + return is_equ_n(s, i, "", 0, 5); +} + +constexpr bool is_uint(const_str s, unsigned i) { + return is_equ_n(s, i, "", 0, 6); +} + +constexpr bool is_float(const_str s, unsigned i) { + return is_equ_n(s, i, "", 0, 7) || is_equ_n(s, i, "", 0, 8); +} + +constexpr bool is_str(const_str s, unsigned i) { + return is_equ_n(s, i, "", 0, 5) || is_equ_n(s, i, "", 0, 8); +} + +constexpr bool is_path(const_str s, unsigned i) { + return is_equ_n(s, i, "", 0, 6); +} +#endif +template struct parameter_tag { + static const int value = 0; +}; +#define CROW_INTERNAL_PARAMETER_TAG(t, i) \ + template <> struct parameter_tag { \ + static const int value = i; \ + } +CROW_INTERNAL_PARAMETER_TAG(int, 1); +CROW_INTERNAL_PARAMETER_TAG(char, 1); +CROW_INTERNAL_PARAMETER_TAG(short, 1); +CROW_INTERNAL_PARAMETER_TAG(long, 1); +CROW_INTERNAL_PARAMETER_TAG(long long, 1); +CROW_INTERNAL_PARAMETER_TAG(unsigned int, 2); +CROW_INTERNAL_PARAMETER_TAG(unsigned char, 2); +CROW_INTERNAL_PARAMETER_TAG(unsigned short, 2); +CROW_INTERNAL_PARAMETER_TAG(unsigned long, 2); +CROW_INTERNAL_PARAMETER_TAG(unsigned long long, 2); +CROW_INTERNAL_PARAMETER_TAG(double, 3); +CROW_INTERNAL_PARAMETER_TAG(std::string, 4); +#undef CROW_INTERNAL_PARAMETER_TAG +template struct compute_parameter_tag_from_args_list; + +template <> struct compute_parameter_tag_from_args_list<> { + static const int value = 0; +}; + +template +struct compute_parameter_tag_from_args_list { + static const int sub_value = + compute_parameter_tag_from_args_list::value; + static const int value = + parameter_tag::type>::value + ? sub_value * 6 + parameter_tag::type>::value + : sub_value; +}; + +static inline bool is_parameter_tag_compatible(uint64_t a, uint64_t b) { + if (a == 0) return b == 0; + if (b == 0) return a == 0; + int sa = a % 6; + int sb = a % 6; + if (sa == 5) sa = 4; + if (sb == 5) sb = 4; + if (sa != sb) return false; + return is_parameter_tag_compatible(a / 6, b / 6); +} + +static inline unsigned find_closing_tag_runtime(const char *s, unsigned p) { + return s[p] == 0 ? throw std::runtime_error("unmatched tag <") + : s[p] == '>' ? p + : find_closing_tag_runtime(s, p + 1); +} + +static inline uint64_t get_parameter_tag_runtime(const char *s, + unsigned p = 0) { + return s[p] == 0 ? 0 + : s[p] == '<' + ? (std::strncmp(s + p, "", 5) == 0 + ? get_parameter_tag_runtime( + s, find_closing_tag_runtime(s, p)) * + 6 + + 1 + : std::strncmp(s + p, "", 6) == 0 + ? get_parameter_tag_runtime( + s, find_closing_tag_runtime(s, p)) * + 6 + + 2 + : (std::strncmp(s + p, "", 7) == 0 || + std::strncmp(s + p, "", 8) == 0) + ? get_parameter_tag_runtime( + s, find_closing_tag_runtime(s, p)) * + 6 + + 3 + : (std::strncmp(s + p, "", 5) == 0 || + std::strncmp(s + p, "", 8) == 0) + ? get_parameter_tag_runtime( + s, find_closing_tag_runtime(s, p)) * + 6 + + 4 + : std::strncmp(s + p, "", 6) == 0 + ? get_parameter_tag_runtime( + s, find_closing_tag_runtime(s, p)) * + 6 + + 5 + : throw std::runtime_error("invalid parameter type")) + : get_parameter_tag_runtime(s, p + 1); +} +#ifndef CROW_MSVC_WORKAROUND +constexpr uint64_t get_parameter_tag(const_str s, unsigned p = 0) { + return p == s.size() ? 0 + : s[p] == '<' + ? (is_int(s, p) + ? get_parameter_tag(s, find_closing_tag(s, p)) * 6 + 1 + : is_uint(s, p) + ? get_parameter_tag(s, find_closing_tag(s, p)) * 6 + 2 + : is_float(s, p) + ? get_parameter_tag(s, find_closing_tag(s, p)) * 6 + 3 + : is_str(s, p) + ? get_parameter_tag(s, find_closing_tag(s, p)) * 6 + 4 + : is_path(s, p) + ? get_parameter_tag(s, find_closing_tag(s, p)) * 6 + 5 + : throw std::runtime_error("invalid parameter type")) + : get_parameter_tag(s, p + 1); +} +#endif + +template struct S { + template using push = S; + template using push_back = S; + template