mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f1f8ff53d5 | |||
| 9b9bda6b6e | |||
| 2280f1d191 | |||
| 43cf1822c6 | |||
| 17f301877f | |||
| 4f17fbaa03 | |||
| 0d5bf55c73 | |||
| 718d7d92b9 | |||
| c41c5fb8a9 | |||
| ab96f72b96 | |||
| b2430249d2 | |||
| 464867a9ce | |||
| d4180e923f | |||
| 8d225afc8c | |||
| ed5c5d325b | |||
| c1ee85d89e | |||
| 14e37bd75b | |||
| c0adbb4b20 | |||
| f80864ca03 | |||
| 4e75a84b39 | |||
| 77d945d3b6 | |||
| f69737a838 | |||
| a188913b02 | |||
| 02e4c53685 | |||
| 8c4370247a | |||
| 1f1a799d13 | |||
| a875292153 | |||
| f0b7d4161d | |||
| 2867b74f13 | |||
| 4e14bc8948 | |||
| 5d717e6d91 | |||
| 8b4146324f | |||
| 94b5038eb3 | |||
| 9248ce3bfe | |||
| 4639b696ab | |||
| 5ead179b8e | |||
| 1942e0ef01 | |||
| 6be32a540d | |||
| dc6faf5c17 | |||
| 71bb17fb0e | |||
| 094bf112bb | |||
| fbec2a3466 | |||
| c3fa06112b | |||
| f73e694f0c | |||
| 191bfb2ea4 | |||
| ad5839f0d1 | |||
| 02dfb97fd6 | |||
| a38a076571 | |||
| 0e1b52b23e | |||
| c0469eba96 | |||
| 7dec57d1eb | |||
| b85aa76bd2 | |||
| cea018f2cd | |||
| 1111219f17 | |||
| a7e1d14b15 | |||
| 6eff49e1fb | |||
| bd95e67c23 | |||
| 2e2e47bab1 | |||
| 59905c7f0d | |||
| 8d03ef1615 | |||
| 23a1d79a66 |
+99
-12
@@ -75,21 +75,45 @@ jobs:
|
||||
(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 libraries
|
||||
- name: install common 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 EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}"
|
||||
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
|
||||
run: cd test && make EXTRA_CXXFLAGS="${{ matrix.config.arch_flags }}" fuzz_test
|
||||
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
|
||||
@@ -98,13 +122,38 @@ jobs:
|
||||
(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: build and run tests
|
||||
run: cd test && make
|
||||
- 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
|
||||
@@ -144,7 +193,14 @@ jobs:
|
||||
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 }}
|
||||
@@ -156,6 +212,7 @@ jobs:
|
||||
-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
|
||||
@@ -164,7 +221,37 @@ jobs:
|
||||
run: cmake --build build --config Release -- /v:m /clp:ShowCommandLine
|
||||
- name: Run tests ${{ matrix.config.name }}
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release
|
||||
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 || '*' }}-*BenchmarkTest*" `
|
||||
-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."
|
||||
- name: Run benchmark tests with retry ${{ matrix.config.name }}
|
||||
if: ${{ matrix.config.run_tests }}
|
||||
run: ctest --output-on-failure --test-dir build -C Release -R "BenchmarkTest" --repeat until-pass:5
|
||||
|
||||
env:
|
||||
VCPKG_ROOT: "C:/vcpkg"
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -6,15 +6,28 @@ 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@v3
|
||||
|
||||
- name: Install dependencies
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install common dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libssl-dev zlib1g-dev libcurl4-openssl-dev libbrotli-dev libzstd-dev netcat-openbsd
|
||||
|
||||
- name: Run proxy tests
|
||||
run: |
|
||||
cd test && make proxy
|
||||
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
|
||||
- name: Run proxy tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make proxy_mbedtls
|
||||
+13
@@ -29,18 +29,31 @@ 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.xcodeproj/xcuser*
|
||||
test/test.xcodeproj/*/xcuser*
|
||||
test/*.o
|
||||
test/*.pem
|
||||
test/*.srl
|
||||
test/*.log
|
||||
test/_build_*
|
||||
work/
|
||||
benchmark/server*
|
||||
|
||||
+53
-7
@@ -5,6 +5,7 @@
|
||||
* 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_ZLIB (default off)
|
||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||
@@ -110,6 +111,15 @@ option(HTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN "Enable feature to load system cer
|
||||
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()
|
||||
# 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."
|
||||
@@ -185,10 +195,11 @@ elseif(HTTPLIB_USE_BROTLI_IF_AVAILABLE)
|
||||
set(HTTPLIB_IS_USING_BROTLI ${Brotli_FOUND})
|
||||
endif()
|
||||
|
||||
# NOTE: When using cpp-httplib as a subproject (e.g., via FetchContent), the zstd::libzstd target may not be visible in the parent project scope.
|
||||
# If you encounter a "target not found" error, see https://github.com/yhirose/cpp-httplib/issues/2313 for a workaround.
|
||||
# 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)
|
||||
find_package(zstd 1.5.6 CONFIG)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(zstd REQUIRED IMPORTED_TARGET libzstd)
|
||||
@@ -196,7 +207,7 @@ if(HTTPLIB_REQUIRE_ZSTD)
|
||||
endif()
|
||||
set(HTTPLIB_IS_USING_ZSTD TRUE)
|
||||
elseif(HTTPLIB_USE_ZSTD_IF_AVAILABLE)
|
||||
find_package(zstd QUIET)
|
||||
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig QUIET)
|
||||
if(PKG_CONFIG_FOUND)
|
||||
@@ -239,6 +250,22 @@ if(HTTPLIB_COMPILE)
|
||||
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")
|
||||
@@ -247,6 +274,13 @@ if(HTTPLIB_COMPILE)
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}/httplib.h>
|
||||
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/httplib.h>
|
||||
)
|
||||
|
||||
# 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}
|
||||
@@ -263,8 +297,12 @@ endif()
|
||||
# Only useful if building in-tree, versus using it from an installation.
|
||||
add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME})
|
||||
|
||||
# Require C++11
|
||||
target_compile_features(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC} cxx_std_11)
|
||||
# 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}
|
||||
$<BUILD_INTERFACE:${_httplib_build_includedir}>
|
||||
@@ -336,7 +374,11 @@ if(HTTPLIB_INSTALL)
|
||||
# Creates the export httplibTargets.cmake
|
||||
# This is strictly what holds compilation requirements
|
||||
# and linkage information (doesn't find deps though).
|
||||
install(TARGETS ${PROJECT_NAME} EXPORT httplibTargets)
|
||||
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)
|
||||
|
||||
@@ -365,6 +407,10 @@ if(HTTPLIB_INSTALL)
|
||||
include(CPack)
|
||||
endif()
|
||||
|
||||
if(HTTPLIB_BUILD_MODULES AND NOT HTTPLIB_COMPILE)
|
||||
message(FATAL_ERROR "HTTPLIB_BUILD_MODULES requires HTTPLIB_COMPILE to be ON.")
|
||||
endif()
|
||||
|
||||
if(HTTPLIB_TEST)
|
||||
include(CTest)
|
||||
add_subdirectory(test)
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
# 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<void(const Request &, ws::WebSocket &)>;
|
||||
using SubProtocolSelector =
|
||||
std::function<std::string(const std::vector<std::string> &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<std::string> &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) |
|
||||
|
||||
## 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
|
||||
```
|
||||
@@ -1,5 +1,4 @@
|
||||
cpp-httplib
|
||||
===========
|
||||
# cpp-httplib
|
||||
|
||||
[](https://github.com/yhirose/cpp-httplib/actions)
|
||||
|
||||
@@ -10,10 +9,17 @@ It's extremely easy to set up. Just include the **httplib.h** file in your code!
|
||||
> [!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.
|
||||
|
||||
Simple examples
|
||||
---------------
|
||||
## Main Features
|
||||
|
||||
#### Server (Multi-threaded)
|
||||
- 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++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
@@ -32,7 +38,7 @@ svr.Get("/hi", [](const httplib::Request &, httplib::Response &res) {
|
||||
svr.listen("0.0.0.0", 8080);
|
||||
```
|
||||
|
||||
#### Client
|
||||
### Client
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
@@ -50,19 +56,34 @@ if (auto res = cli.Get("/hi")) {
|
||||
}
|
||||
```
|
||||
|
||||
SSL Support
|
||||
-----------
|
||||
## SSL/TLS Support
|
||||
|
||||
SSL support is available with `CPPHTTPLIB_OPENSSL_SUPPORT`. `libssl` and `libcrypto` should be linked.
|
||||
cpp-httplib supports multiple TLS backends through an abstraction layer:
|
||||
|
||||
| Backend | Define | Libraries |
|
||||
| :------ | :----- | :-------- |
|
||||
| OpenSSL | `CPPHTTPLIB_OPENSSL_SUPPORT` | `libssl`, `libcrypto` |
|
||||
| Mbed TLS | `CPPHTTPLIB_MBEDTLS_SUPPORT` | `libmbedtls`, `libmbedx509`, `libmbedcrypto` |
|
||||
| wolfSSL | `CPPHTTPLIB_WOLFSSL_SUPPORT` | `libwolfssl` |
|
||||
|
||||
> [!NOTE]
|
||||
> cpp-httplib currently supports only version 3.0 or later. Please see [this page](https://www.openssl.org/policies/releasestrat.html) to get more information.
|
||||
> OpenSSL 3.0 or later is required. Please see [this page](https://www.openssl.org/policies/releasestrat.html) for more information.
|
||||
|
||||
> [!NOTE]
|
||||
> Mbed TLS 2.x and 3.x are supported. The library automatically detects the version and uses the appropriate API.
|
||||
|
||||
> [!NOTE]
|
||||
> wolfSSL must be built with OpenSSL compatibility layer enabled (`--enable-opensslall`). wolfSSL 5.x is supported.
|
||||
|
||||
> [!NOTE]
|
||||
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()` or `load_ca_cert_store(pem, size)`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable with these backends.
|
||||
|
||||
> [!TIP]
|
||||
> For macOS: cpp-httplib now can use system certs with `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`. `CoreFoundation` and `Security` should be linked with `-framework`.
|
||||
> For macOS: cpp-httplib can use system certs with `CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN`. `CoreFoundation` and `Security` should be linked with `-framework`.
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
// 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
|
||||
@@ -83,15 +104,15 @@ cli.enable_server_certificate_verification(false);
|
||||
cli.enable_server_hostname_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.
|
||||
|
||||
### SSL Error Handling
|
||||
|
||||
When SSL operations fail, cpp-httplib provides detailed error information through two separate error fields:
|
||||
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
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
|
||||
#include "path/to/httplib.h"
|
||||
|
||||
httplib::Client cli("https://example.com");
|
||||
@@ -108,18 +129,18 @@ if (!res) {
|
||||
break;
|
||||
|
||||
case httplib::Error::SSLLoadingCerts:
|
||||
std::cout << "SSL cert loading failed, OpenSSL error: "
|
||||
<< std::hex << res.ssl_openssl_error() << std::endl;
|
||||
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, X509 error: "
|
||||
<< res.ssl_openssl_error() << std::endl;
|
||||
std::cout << "SSL verification failed, verify error: "
|
||||
<< res.ssl_backend_error() << std::endl;
|
||||
break;
|
||||
|
||||
case httplib::Error::SSLServerHostnameVerification:
|
||||
std::cout << "SSL hostname verification failed, X509 error: "
|
||||
<< res.ssl_openssl_error() << std::endl;
|
||||
std::cout << "SSL hostname verification failed, verify error: "
|
||||
<< res.ssl_backend_error() << std::endl;
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -128,8 +149,75 @@ if (!res) {
|
||||
}
|
||||
```
|
||||
|
||||
Server
|
||||
------
|
||||
### 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;
|
||||
});
|
||||
```
|
||||
|
||||
### Windows Certificate Verification
|
||||
|
||||
On Windows, cpp-httplib automatically performs additional certificate verification using the Windows certificate store via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`). This works with all TLS backends (OpenSSL, Mbed TLS, and wolfSSL), providing:
|
||||
|
||||
- Real-time certificate validation integrated with Windows Update
|
||||
- Certificate revocation checking
|
||||
- SSL/TLS policy verification using the system certificate store (ROOT and CA)
|
||||
|
||||
This feature is enabled by default and can be controlled at runtime:
|
||||
|
||||
```c++
|
||||
// Disable Windows certificate verification (use only OpenSSL/Mbed TLS/wolfSSL verification)
|
||||
cli.enable_windows_certificate_verification(false);
|
||||
```
|
||||
|
||||
To disable this feature at compile time, define:
|
||||
|
||||
```c++
|
||||
#define CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
```
|
||||
|
||||
> [!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 <httplib.h>
|
||||
@@ -380,6 +468,34 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
||||
});
|
||||
```
|
||||
|
||||
### Response user data
|
||||
|
||||
`res.user_data` is a `std::map<std::string, httplib::any>` 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<AuthContext>(&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')
|
||||
@@ -634,20 +750,24 @@ Please see [Server example](https://github.com/yhirose/cpp-httplib/blob/master/e
|
||||
|
||||
### Default thread pool support
|
||||
|
||||
`ThreadPool` is used as the **default** task queue, with a default thread count of 8 or `std::thread::hardware_concurrency() - 1`, whichever is greater. You can change it with `CPPHTTPLIB_THREAD_POOL_COUNT`.
|
||||
`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`.
|
||||
|
||||
If you want to set the thread count at runtime, there is no convenient way... But here is how.
|
||||
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
|
||||
svr.new_task_queue = [] { return new ThreadPool(12); };
|
||||
svr.new_task_queue = [] { return new ThreadPool(/*base_threads=*/8, /*max_threads=*/64); };
|
||||
```
|
||||
|
||||
#### 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
|
||||
svr.new_task_queue = [] { return new ThreadPool(/*num_threads=*/12, /*max_queued_requests=*/18); };
|
||||
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
|
||||
@@ -683,8 +803,7 @@ svr.new_task_queue = [] {
|
||||
};
|
||||
```
|
||||
|
||||
Client
|
||||
------
|
||||
## Client
|
||||
|
||||
```c++
|
||||
#include <httplib.h>
|
||||
@@ -813,11 +932,15 @@ httplib::Headers headers = {
|
||||
};
|
||||
auto res = cli.Get("/hi", headers);
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```c++
|
||||
auto res = cli.Get("/hi", {{"Hello", "World!"}});
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```c++
|
||||
cli.set_default_headers({
|
||||
{ "Hello", "World!" }
|
||||
@@ -866,6 +989,17 @@ httplib::UploadFormDataItems items = {
|
||||
auto res = cli.Post("/multipart", items);
|
||||
```
|
||||
|
||||
To upload files from disk without loading them entirely into memory, use `make_file_provider`. The file is read and sent in chunks with a correct `Content-Length` header.
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
### PUT
|
||||
|
||||
```c++
|
||||
@@ -902,6 +1036,12 @@ cli.set_write_timeout(5, 0); // 5 seconds
|
||||
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++
|
||||
@@ -1102,18 +1242,21 @@ httplib::Server svr;
|
||||
svr.listen("127.0.0.1", 8080);
|
||||
```
|
||||
|
||||
Compression
|
||||
-----------
|
||||
## 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
|
||||
- all text types except text/event-stream
|
||||
- image/svg+xml
|
||||
- application/javascript
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/protobuf
|
||||
- application/xhtml+xml
|
||||
|
||||
### Zlib Support
|
||||
|
||||
@@ -1206,8 +1349,7 @@ std::string decoded_component = httplib::decode_uri_component(encoded_component)
|
||||
|
||||
Use `encode_uri()` for full URLs and `encode_uri_component()` for individual query parameters or path segments.
|
||||
|
||||
Stream API
|
||||
----------
|
||||
## Stream API
|
||||
|
||||
Process large responses without loading everything into memory.
|
||||
|
||||
@@ -1234,8 +1376,7 @@ All HTTP methods are supported: `stream::Get`, `Post`, `Put`, `Patch`, `Delete`,
|
||||
|
||||
See [README-stream.md](README-stream.md) for more details.
|
||||
|
||||
SSE Client
|
||||
----------
|
||||
## SSE Client
|
||||
|
||||
```cpp
|
||||
#include <httplib.h>
|
||||
@@ -1256,8 +1397,47 @@ int main() {
|
||||
|
||||
See [README-sse.md](README-sse.md) for more details.
|
||||
|
||||
Split httplib.h into .h and .cc
|
||||
-------------------------------
|
||||
## 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
|
||||
@@ -1275,8 +1455,7 @@ $ ./split.py
|
||||
Wrote out/httplib.h and out/httplib.cc
|
||||
```
|
||||
|
||||
Dockerfile for Static HTTP Server
|
||||
---------------------------------
|
||||
## 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.
|
||||
|
||||
@@ -1347,12 +1526,10 @@ Include `httplib.h` before `Windows.h` or include `Windows.h` by defining `WIN32
|
||||
> [!NOTE]
|
||||
> Windows 8 or lower, Visual Studio 2015 or lower, and Cygwin and MSYS2 including MinGW are neither supported nor tested.
|
||||
|
||||
License
|
||||
-------
|
||||
## License
|
||||
|
||||
MIT license (© 2026 Yuji Hirose)
|
||||
|
||||
Special Thanks To
|
||||
-----------------
|
||||
## Special Thanks To
|
||||
|
||||
[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!
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# This file contains C++20 module support requiring CMake 3.28+
|
||||
# Included conditionally to prevent parse errors on older CMake versions
|
||||
|
||||
if(HTTPLIB_BUILD_MODULES)
|
||||
if(POLICY CMP0155)
|
||||
cmake_policy(SET CMP0155 NEW)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_SCAN_FOR_MODULES ON)
|
||||
|
||||
target_sources(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
FILE_SET CXX_MODULES FILES
|
||||
"${_httplib_build_includedir}/httplib.cppm"
|
||||
)
|
||||
endif()
|
||||
+5
-2
@@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = $(PREFIX)/opt/brotli
|
||||
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header
|
||||
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli wsecho benchmark one_time_request server_and_client accept_header
|
||||
|
||||
server : server.cc ../httplib.h Makefile
|
||||
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
@@ -47,6 +47,9 @@ ssesvr : ssesvr.cc ../httplib.h Makefile
|
||||
ssecli : ssecli.cc ../httplib.h Makefile
|
||||
$(CXX) -o ssecli $(CXXFLAGS) ssecli.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
|
||||
wsecho : wsecho.cc ../httplib.h Makefile
|
||||
$(CXX) -o wsecho $(CXXFLAGS) wsecho.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
|
||||
benchmark : benchmark.cc ../httplib.h Makefile
|
||||
$(CXX) -o benchmark $(CXXFLAGS) benchmark.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
|
||||
|
||||
@@ -64,4 +67,4 @@ pem:
|
||||
openssl req -new -key key.pem | openssl x509 -days 3650 -req -signkey key.pem > cert.pem
|
||||
|
||||
clean:
|
||||
rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header *.pem
|
||||
rm server client hello simplecli simplesvr upload redirect ssesvr ssecli wsecho benchmark one_time_request server_and_client accept_header *.pem
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ int main(void) {
|
||||
} else {
|
||||
cout << "error code: " << res.error() << std::endl;
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
auto result = cli.get_openssl_verify_result();
|
||||
auto result = cli.get_verify_result();
|
||||
if (result) {
|
||||
cout << "verify error: " << X509_verify_cert_error_string(result) << endl;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
#include <httplib.h>
|
||||
#include <iostream>
|
||||
|
||||
using namespace httplib;
|
||||
|
||||
const auto html = R"HTML(
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WebSocket Demo</title>
|
||||
<style>
|
||||
body { font-family: monospace; margin: 2em; }
|
||||
#log { height: 300px; overflow-y: scroll; border: 1px solid #ccc; padding: 8px; }
|
||||
.controls { margin: 8px 0; }
|
||||
button { margin-right: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Demo</h1>
|
||||
<p>Server accepts subprotocols: <b>echo</b>, <b>chat</b> (or none)</p>
|
||||
|
||||
<div class="controls">
|
||||
<label>Subprotocols: </label>
|
||||
<input id="protos" type="text" value="echo, chat" placeholder="leave empty for none" />
|
||||
<button onclick="doConnect()">Connect</button>
|
||||
<button onclick="doDisconnect()">Disconnect</button>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<input id="msg" type="text" placeholder="Type a message..." />
|
||||
<button onclick="doSend()">Send</button>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button onclick="startAuto()">Start Auto (1s)</button>
|
||||
<button onclick="stopAuto()">Stop Auto</button>
|
||||
<span id="auto-status"></span>
|
||||
</div>
|
||||
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script>
|
||||
var sock = null;
|
||||
var logEl = document.getElementById("log");
|
||||
var statusEl = document.getElementById("auto-status");
|
||||
var timer = null;
|
||||
var seq = 0;
|
||||
|
||||
function appendLog(text) {
|
||||
logEl.textContent += text + "\n";
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
|
||||
function doConnect() {
|
||||
if (sock && sock.readyState <= 1) { sock.close(); }
|
||||
var input = document.getElementById("protos").value.trim();
|
||||
var protocols = input ? input.split(/\s*,\s*/).filter(Boolean) : [];
|
||||
sock = new WebSocket("ws://" + location.host + "/ws", protocols);
|
||||
appendLog("[connecting] proposed: " + (protocols.length ? protocols.join(", ") : "(none)"));
|
||||
sock.onopen = function() { appendLog("[connected] subprotocol: " + (sock.protocol || "(none)")); };
|
||||
sock.onclose = function() { appendLog("[disconnected]"); stopAuto(); };
|
||||
sock.onmessage = function(e) { appendLog("< " + e.data); };
|
||||
}
|
||||
|
||||
function doDisconnect() {
|
||||
if (sock) { sock.close(); }
|
||||
}
|
||||
|
||||
function doSend() {
|
||||
var input = document.getElementById("msg");
|
||||
if (!sock || sock.readyState !== 1 || input.value === "") return;
|
||||
sock.send(input.value);
|
||||
appendLog("> " + input.value);
|
||||
input.value = "";
|
||||
}
|
||||
|
||||
function startAuto() {
|
||||
if (timer || !sock || sock.readyState !== 1) return;
|
||||
seq = 0;
|
||||
statusEl.textContent = "running...";
|
||||
timer = setInterval(function() {
|
||||
if (!sock || sock.readyState !== 1) { stopAuto(); return; }
|
||||
var msg = "auto #" + seq++;
|
||||
sock.send(msg);
|
||||
appendLog("> " + msg);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function stopAuto() {
|
||||
if (timer) { clearInterval(timer); timer = null; }
|
||||
statusEl.textContent = "";
|
||||
}
|
||||
|
||||
document.getElementById("msg").addEventListener("keydown", function(e) {
|
||||
if (e.key === "Enter") doSend();
|
||||
});
|
||||
|
||||
doConnect();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)HTML";
|
||||
|
||||
int main(void) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/", [&](const Request & /*req*/, Response &res) {
|
||||
res.set_content(html, "text/html");
|
||||
});
|
||||
|
||||
svr.WebSocket(
|
||||
"/ws",
|
||||
[](const Request &req, ws::WebSocket &ws) {
|
||||
std::cout << "WebSocket connected from " << req.remote_addr
|
||||
<< std::endl;
|
||||
|
||||
std::string msg;
|
||||
while (ws.read(msg)) {
|
||||
std::cout << "Received: " << msg << std::endl;
|
||||
ws.send("echo: " + msg);
|
||||
}
|
||||
|
||||
std::cout << "WebSocket disconnected" << std::endl;
|
||||
},
|
||||
[](const std::vector<std::string> &protocols) -> std::string {
|
||||
for (const auto &p : protocols) {
|
||||
if (p == "echo" || p == "chat") { return p; }
|
||||
}
|
||||
return "";
|
||||
});
|
||||
|
||||
std::cout << "Listening on http://localhost:8080" << std::endl;
|
||||
svr.listen("localhost", 8080);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""This script generates httplib.cppm module file from httplib.h."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from typing import List
|
||||
|
||||
def main() -> None:
|
||||
"""Main entry point for the script."""
|
||||
|
||||
args_parser: ArgumentParser = ArgumentParser(description=__doc__)
|
||||
args_parser.add_argument(
|
||||
"-o", "--out", help="where to write the files (default: out)", default="out"
|
||||
)
|
||||
args: Namespace = args_parser.parse_args()
|
||||
|
||||
cur_dir: str = os.path.dirname(sys.argv[0])
|
||||
if not cur_dir:
|
||||
cur_dir = '.'
|
||||
lib_name: str = "httplib"
|
||||
header_name: str = f"/{lib_name}.h"
|
||||
# get the input file
|
||||
in_file: str = f"{cur_dir}{header_name}"
|
||||
# get the output file
|
||||
cppm_out: str = f"{args.out}/{lib_name}.cppm"
|
||||
|
||||
# if the modification time of the out file is after the in file,
|
||||
# don't generate (as it is already finished)
|
||||
do_generate: bool = True
|
||||
|
||||
if os.path.exists(cppm_out):
|
||||
in_time: float = os.path.getmtime(in_file)
|
||||
out_time: float = os.path.getmtime(cppm_out)
|
||||
do_generate: bool = in_time > out_time
|
||||
|
||||
if do_generate:
|
||||
with open(in_file) as f:
|
||||
lines: List[str] = f.readlines()
|
||||
|
||||
os.makedirs(args.out, exist_ok=True)
|
||||
|
||||
# Find the Headers and Declaration comment markers
|
||||
headers_start: int = -1
|
||||
declaration_start: int = -1
|
||||
for i, line in enumerate(lines):
|
||||
if ' * Headers' in line:
|
||||
headers_start = i - 1 # Include the /* line
|
||||
elif ' * Declaration' in line:
|
||||
declaration_start = i - 1 # Stop before the /* line
|
||||
break
|
||||
|
||||
with open(cppm_out, 'w') as fm:
|
||||
# Write module file
|
||||
fm.write("module;\n\n")
|
||||
|
||||
# Write global module fragment (from Headers to Declaration comment)
|
||||
# Filter out 'using' declarations to avoid conflicts
|
||||
if headers_start >= 0 and declaration_start >= 0:
|
||||
for i in range(headers_start, declaration_start):
|
||||
line: str = lines[i]
|
||||
if 'using' not in line:
|
||||
fm.write(line)
|
||||
|
||||
fm.write("\nexport module httplib;\n\n")
|
||||
fm.write("export extern \"C++\" {\n")
|
||||
fm.write(f"{' ' * 4}#include \"httplib.h\"\n")
|
||||
fm.write("}\n")
|
||||
|
||||
print(f"Wrote {cppm_out}")
|
||||
else:
|
||||
print(f"{cppm_out} is up to date")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,46 @@
|
||||
set shell := ["bash", "-c"]
|
||||
|
||||
default: list
|
||||
|
||||
list:
|
||||
@just --list --unsorted
|
||||
|
||||
openssl:
|
||||
@(cd test && LSAN_OPTIONS=suppressions=lsan_suppressions.txt make)
|
||||
@(cd test && make proxy)
|
||||
|
||||
openssl_parallel:
|
||||
@(cd test && make test_openssl_parallel)
|
||||
|
||||
mbedtls:
|
||||
@(cd test && make test_mbedtls && LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test_mbedtls)
|
||||
@(cd test && make proxy_mbedtls)
|
||||
|
||||
mbedtls_parallel:
|
||||
@(cd test && make test_mbedtls_parallel)
|
||||
|
||||
wolfssl:
|
||||
@(cd test && make test_wolfssl && LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test_wolfssl)
|
||||
@(cd test && make proxy_wolfssl)
|
||||
|
||||
wolfssl_parallel:
|
||||
@(cd test && make test_wolfssl_parallel)
|
||||
|
||||
no_tls:
|
||||
@(cd test && make test_no_tls && ./test_no_tls)
|
||||
|
||||
no_tls_parallel:
|
||||
@(cd test && make test_no_tls_parallel)
|
||||
|
||||
others:
|
||||
@(cd test && make fuzz_test)
|
||||
@(cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat)
|
||||
@(cd test && make test_thread_pool && ./test_thread_pool)
|
||||
|
||||
build:
|
||||
@(cd test && make test_split)
|
||||
@(cd test && make test_split_mbedtls)
|
||||
@(cd test && make test_split_wolfssl)
|
||||
|
||||
bench:
|
||||
@(cd benchmark && make bench-all)
|
||||
+23
-5
@@ -39,11 +39,29 @@ endif
|
||||
deps = [dependency('threads')]
|
||||
args = []
|
||||
|
||||
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('openssl'))
|
||||
if openssl_dep.found()
|
||||
deps += openssl_dep
|
||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||
if host_machine.system() == 'darwin'
|
||||
if get_option('tls').allowed()
|
||||
tls_found = false
|
||||
if get_option('tls_backend') == 'openssl'
|
||||
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('tls'))
|
||||
if openssl_dep.found()
|
||||
deps += openssl_dep
|
||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||
tls_found = true
|
||||
endif
|
||||
else
|
||||
mbedtls_dep = dependency('mbedtls', required: get_option('tls'))
|
||||
mbedtlsx509_dep = dependency('mbedx509', required: get_option('tls'))
|
||||
mbedtlscrypto_dep = dependency('mbedcrypto', required: get_option('tls'))
|
||||
if mbedtls_dep.found() and mbedtlsx509_dep.found() and mbedtlscrypto_dep.found()
|
||||
deps += mbedtls_dep
|
||||
deps += mbedtlsx509_dep
|
||||
deps += mbedtlscrypto_dep
|
||||
args += '-DCPPHTTPLIB_MBEDTLS_SUPPORT'
|
||||
tls_found = true
|
||||
endif
|
||||
endif
|
||||
|
||||
if tls_found and host_machine.system() == 'darwin'
|
||||
macosx_keychain_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
|
||||
if macosx_keychain_dep.found()
|
||||
deps += macosx_keychain_dep
|
||||
|
||||
+3
-1
@@ -2,7 +2,8 @@
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
option('openssl', type: 'feature', value: 'auto', description: 'Enable OpenSSL support')
|
||||
option('tls', type: 'feature', description: 'Enable TLS support')
|
||||
option('tls_backend', type: 'combo', choices: ['openssl', 'mbedtls'], value: 'openssl', description: 'Which TLS library to use')
|
||||
option('zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
|
||||
option('brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
|
||||
option('zstd', type: 'feature', value: 'auto', description: 'Enable zstd support')
|
||||
@@ -12,6 +13,7 @@ option('compile', type: 'boolean', value: false, description: 'Split the header
|
||||
option('test', type: 'boolean', value: false, description: 'Build tests')
|
||||
|
||||
# Old option names
|
||||
option('openssl', type: 'feature', deprecated: 'tls')
|
||||
option('cpp-httplib_openssl', type: 'feature', deprecated: 'openssl')
|
||||
option('cpp-httplib_zlib', type: 'feature', deprecated: 'zlib')
|
||||
option('cpp-httplib_brotli', type: 'feature', deprecated: 'brotli')
|
||||
|
||||
@@ -7,15 +7,14 @@ import sys
|
||||
from argparse import ArgumentParser, Namespace
|
||||
from typing import List
|
||||
|
||||
BORDER: str = '// ----------------------------------------------------------------------------'
|
||||
|
||||
def main() -> None:
|
||||
"""Main entry point for the script."""
|
||||
BORDER: str = '// ----------------------------------------------------------------------------'
|
||||
|
||||
args_parser: ArgumentParser = ArgumentParser(description=__doc__)
|
||||
args_parser.add_argument(
|
||||
"-e", "--extension", help="extension of the implementation file (default: cc)",
|
||||
default="cc"
|
||||
"-e", "--extension", help="extension of the implementation file (default: cc)", default="cc"
|
||||
)
|
||||
args_parser.add_argument(
|
||||
"-o", "--out", help="where to write the files (default: out)", default="out"
|
||||
@@ -25,14 +24,14 @@ def main() -> None:
|
||||
cur_dir: str = os.path.dirname(sys.argv[0])
|
||||
if not cur_dir:
|
||||
cur_dir = '.'
|
||||
lib_name: str = 'httplib'
|
||||
lib_name: str = "httplib"
|
||||
header_name: str = f"/{lib_name}.h"
|
||||
source_name: str = f"/{lib_name}.{args.extension}"
|
||||
# get the input file
|
||||
in_file: str = cur_dir + header_name
|
||||
in_file: str = f"{cur_dir}{header_name}"
|
||||
# get the output file
|
||||
h_out: str = args.out + header_name
|
||||
cc_out: str = args.out + source_name
|
||||
h_out: str = f"{args.out}{header_name}"
|
||||
cc_out: str = f"{args.out}{source_name}"
|
||||
|
||||
# if the modification time of the out file is after the in file,
|
||||
# don't split (as it is already finished)
|
||||
@@ -51,18 +50,23 @@ def main() -> None:
|
||||
|
||||
in_implementation: bool = False
|
||||
cc_out: str = args.out + source_name
|
||||
|
||||
with open(h_out, 'w') as fh, open(cc_out, 'w') as fc:
|
||||
fc.write('#include "httplib.h"\n')
|
||||
fc.write('namespace httplib {\n')
|
||||
# Write source file
|
||||
fc.write("#include \"httplib.h\"\n")
|
||||
fc.write("namespace httplib {\n")
|
||||
|
||||
# Process lines for header and source split
|
||||
for line in lines:
|
||||
is_border_line: bool = BORDER in line
|
||||
if is_border_line:
|
||||
in_implementation: bool = not in_implementation
|
||||
elif in_implementation:
|
||||
fc.write(line.replace('inline ', ''))
|
||||
fc.write(line.replace("inline ", ""))
|
||||
else:
|
||||
fh.write(line)
|
||||
fc.write('} // namespace httplib\n')
|
||||
|
||||
fc.write("} // namespace httplib\n")
|
||||
|
||||
print(f"Wrote {h_out} and {cc_out}")
|
||||
else:
|
||||
|
||||
+145
-19
@@ -1,38 +1,49 @@
|
||||
CXX = clang++
|
||||
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow $(EXTRA_CXXFLAGS) -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS -fsanitize=address
|
||||
|
||||
PREFIX ?= $(shell brew --prefix)
|
||||
|
||||
OPENSSL_DIR = $(PREFIX)/opt/openssl@3
|
||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
|
||||
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow $(EXTRA_CXXFLAGS) -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO -fsanitize=address # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS
|
||||
|
||||
ifneq ($(OS), Windows_NT)
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S), Darwin)
|
||||
PREFIX ?= $(shell brew --prefix)
|
||||
OPENSSL_DIR = $(PREFIX)/opt/openssl@3
|
||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -I$(OPENSSL_DIR)/include -L$(OPENSSL_DIR)/lib -lssl -lcrypto
|
||||
OPENSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
|
||||
MBEDTLS_DIR ?= $(shell brew --prefix mbedtls@3)
|
||||
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -I$(MBEDTLS_DIR)/include -L$(MBEDTLS_DIR)/lib -lmbedtls -lmbedx509 -lmbedcrypto
|
||||
MBEDTLS_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
|
||||
WOLFSSL_DIR ?= $(shell brew --prefix wolfssl)
|
||||
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -I$(WOLFSSL_DIR)/include -I$(WOLFSSL_DIR)/include/wolfssl -L$(WOLFSSL_DIR)/lib -lwolfssl
|
||||
WOLFSSL_SUPPORT += -DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN -framework Security
|
||||
else
|
||||
OPENSSL_SUPPORT = -DCPPHTTPLIB_OPENSSL_SUPPORT -lssl -lcrypto
|
||||
MBEDTLS_SUPPORT = -DCPPHTTPLIB_MBEDTLS_SUPPORT -lmbedtls -lmbedx509 -lmbedcrypto
|
||||
WOLFSSL_SUPPORT = -DCPPHTTPLIB_WOLFSSL_SUPPORT -lwolfssl
|
||||
endif
|
||||
endif
|
||||
|
||||
ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
|
||||
BROTLI_DIR = $(PREFIX)/opt/brotli
|
||||
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
ZSTD_DIR = $(PREFIX)/opt/zstd
|
||||
ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -I$(ZSTD_DIR)/include -L$(ZSTD_DIR)/lib -lzstd
|
||||
|
||||
LIBS = -lpthread -lcurl
|
||||
ifneq ($(OS), Windows_NT)
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S), Darwin)
|
||||
LIBS += -framework CoreFoundation -framework CFNetwork
|
||||
endif
|
||||
ifneq ($(UNAME_S), Darwin)
|
||||
LIBS += -lanl
|
||||
# macOS: use Homebrew paths for brotli and zstd
|
||||
BROTLI_DIR = $(PREFIX)/opt/brotli
|
||||
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
ZSTD_DIR = $(PREFIX)/opt/zstd
|
||||
ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -I$(ZSTD_DIR)/include -L$(ZSTD_DIR)/lib -lzstd
|
||||
LIBS = -lpthread -lcurl -framework CoreFoundation -framework CFNetwork
|
||||
else
|
||||
# Linux: use system paths
|
||||
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
ZSTD_SUPPORT = -DCPPHTTPLIB_ZSTD_SUPPORT -lzstd
|
||||
LIBS = -lpthread -lcurl -lanl
|
||||
endif
|
||||
endif
|
||||
|
||||
TEST_ARGS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
|
||||
TEST_ARGS_MBEDTLS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(MBEDTLS_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
|
||||
TEST_ARGS_WOLFSSL = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(WOLFSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
|
||||
TEST_ARGS_NO_TLS = gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include $(ZLIB_SUPPORT) $(BROTLI_SUPPORT) $(ZSTD_SUPPORT) $(LIBS)
|
||||
|
||||
# By default, use standalone_fuzz_target_runner.
|
||||
# This runner does no fuzzing, but simply executes the inputs
|
||||
@@ -48,7 +59,46 @@ STYLE_CHECK_FILES = $(filter-out httplib.h httplib.cc, \
|
||||
$(wildcard example/*.h example/*.cc fuzzing/*.h fuzzing/*.cc *.h *.cc ../httplib.h))
|
||||
|
||||
all : test test_split
|
||||
./test
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test
|
||||
|
||||
SHARDS ?= 4
|
||||
|
||||
define run_parallel
|
||||
@echo "Running $(1) with $(SHARDS) shards in parallel..."
|
||||
@fail=0; \
|
||||
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
|
||||
GTEST_TOTAL_SHARDS=$(SHARDS) GTEST_SHARD_INDEX=$$i \
|
||||
LSAN_OPTIONS=suppressions=lsan_suppressions.txt \
|
||||
./$(1) --gtest_color=yes > $(1)_shard_$$i.log 2>&1 & \
|
||||
done; \
|
||||
wait; \
|
||||
for i in $$(seq 0 $$(($(SHARDS) - 1))); do \
|
||||
if ! grep -q "\[ PASSED \]" $(1)_shard_$$i.log; then \
|
||||
echo "=== Shard $$i FAILED ==="; \
|
||||
cat $(1)_shard_$$i.log; \
|
||||
fail=1; \
|
||||
else \
|
||||
passed=$$(grep "\[ PASSED \]" $(1)_shard_$$i.log); \
|
||||
echo "Shard $$i: $$passed"; \
|
||||
fi; \
|
||||
done; \
|
||||
if [ $$fail -ne 0 ]; then exit 1; fi; \
|
||||
echo "All shards passed."
|
||||
endef
|
||||
|
||||
.PHONY: test_openssl_parallel test_mbedtls_parallel test_wolfssl_parallel test_no_tls_parallel
|
||||
|
||||
test_openssl_parallel : test
|
||||
$(call run_parallel,test)
|
||||
|
||||
test_mbedtls_parallel : test_mbedtls
|
||||
$(call run_parallel,test_mbedtls)
|
||||
|
||||
test_wolfssl_parallel : test_wolfssl
|
||||
$(call run_parallel,test_wolfssl)
|
||||
|
||||
test_no_tls_parallel : test_no_tls
|
||||
$(call run_parallel,test_no_tls)
|
||||
|
||||
proxy : test_proxy
|
||||
@echo "Starting proxy server..."
|
||||
@@ -69,6 +119,44 @@ proxy : test_proxy
|
||||
cd proxy && docker compose down; \
|
||||
exit $$exit_code
|
||||
|
||||
proxy_mbedtls : test_proxy_mbedtls
|
||||
@echo "Starting proxy server..."
|
||||
cd proxy && \
|
||||
docker compose up -d
|
||||
@echo "Waiting for proxy to be ready..."
|
||||
@until nc -z localhost 3128 && nc -z localhost 3129; do sleep 1; done
|
||||
@echo "Proxy servers are ready, waiting additional 5 seconds for full startup..."
|
||||
@sleep 5
|
||||
@echo "Checking proxy server status..."
|
||||
@cd proxy && docker compose ps
|
||||
@echo "Checking proxy server logs..."
|
||||
@cd proxy && docker compose logs --tail=20
|
||||
@echo "Running proxy tests (Mbed TLS)..."
|
||||
./test_proxy_mbedtls; \
|
||||
exit_code=$$?; \
|
||||
echo "Stopping proxy server..."; \
|
||||
cd proxy && docker compose down; \
|
||||
exit $$exit_code
|
||||
|
||||
proxy_wolfssl : test_proxy_wolfssl
|
||||
@echo "Starting proxy server..."
|
||||
cd proxy && \
|
||||
docker compose up -d
|
||||
@echo "Waiting for proxy to be ready..."
|
||||
@until nc -z localhost 3128 && nc -z localhost 3129; do sleep 1; done
|
||||
@echo "Proxy servers are ready, waiting additional 5 seconds for full startup..."
|
||||
@sleep 5
|
||||
@echo "Checking proxy server status..."
|
||||
@cd proxy && docker compose ps
|
||||
@echo "Checking proxy server logs..."
|
||||
@cd proxy && docker compose logs --tail=20
|
||||
@echo "Running proxy tests (wolfSSL)..."
|
||||
./test_proxy_wolfssl; \
|
||||
exit_code=$$?; \
|
||||
echo "Stopping proxy server..."; \
|
||||
cd proxy && docker compose down; \
|
||||
exit $$exit_code
|
||||
|
||||
test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS)
|
||||
@file $@
|
||||
@@ -78,6 +166,34 @@ test : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
|
||||
test_split : test.cc ../httplib.h httplib.cc Makefile cert.pem
|
||||
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS)
|
||||
|
||||
# Mbed TLS backend targets
|
||||
test_mbedtls : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS_MBEDTLS)
|
||||
@file $@
|
||||
|
||||
test_split_mbedtls : test.cc ../httplib.h httplib.cc Makefile cert.pem
|
||||
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_MBEDTLS)
|
||||
|
||||
# wolfSSL backend targets
|
||||
test_wolfssl : test.cc include_httplib.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS_WOLFSSL)
|
||||
@file $@
|
||||
|
||||
test_split_wolfssl : test.cc ../httplib.h httplib.cc Makefile cert.pem
|
||||
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_WOLFSSL)
|
||||
|
||||
# No TLS
|
||||
test_no_tls : test.cc include_httplib.cc ../httplib.h Makefile
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test.cc include_httplib.cc $(TEST_ARGS_NO_TLS)
|
||||
@file $@
|
||||
|
||||
test_split_no_tls : test.cc ../httplib.h httplib.cc Makefile
|
||||
$(CXX) -o $@ $(CXXFLAGS) test.cc httplib.cc $(TEST_ARGS_NO_TLS)
|
||||
|
||||
# ThreadPool unit tests (no TLS, no compression needed)
|
||||
test_thread_pool : test_thread_pool.cc ../httplib.h Makefile
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_thread_pool.cc gtest/src/gtest-all.cc gtest/src/gtest_main.cc -Igtest -Igtest/include -lpthread
|
||||
|
||||
check_abi:
|
||||
@./check-shared-library-abi-compatibility.sh
|
||||
|
||||
@@ -103,9 +219,19 @@ style_check: $(STYLE_CHECK_FILES)
|
||||
echo "All files are properly formatted."; \
|
||||
fi
|
||||
|
||||
test_websocket_heartbeat : test_websocket_heartbeat.cc ../httplib.h Makefile
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
|
||||
@file $@
|
||||
|
||||
test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS)
|
||||
|
||||
test_proxy_mbedtls : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_MBEDTLS)
|
||||
|
||||
test_proxy_wolfssl : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS_WOLFSSL)
|
||||
|
||||
# Runs server_fuzzer.cc based on value of $(LIB_FUZZING_ENGINE).
|
||||
# Usage: make fuzz_test LIB_FUZZING_ENGINE=/path/to/libFuzzer
|
||||
fuzz_test: server_fuzzer
|
||||
@@ -128,5 +254,5 @@ cert.pem:
|
||||
./gen-certs.sh
|
||||
|
||||
clean:
|
||||
rm -rf test test_split test_proxy server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM
|
||||
rm -rf test test_split test_mbedtls test_split_mbedtls test_wolfssl test_split_wolfssl test_no_tls, test_split_no_tls test_proxy test_proxy_mbedtls test_proxy_wolfssl server_fuzzer *.pem *.0 *.o *.1 *.srl httplib.h httplib.cc _build* *.dSYM *_shard_*.log
|
||||
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@ openssl genrsa 2048 > client.key.pem
|
||||
openssl req -new -batch -config test.conf -key client.key.pem | openssl x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial > client.cert.pem
|
||||
openssl genrsa -passout pass:test123! 2048 > key_encrypted.pem
|
||||
openssl req -new -batch -config test.conf -key key_encrypted.pem | openssl x509 -days 3650 -req -signkey key_encrypted.pem > cert_encrypted.pem
|
||||
openssl genrsa -aes256 -passout pass:test012! 2048 > client_encrypted.key.pem
|
||||
openssl genrsa 2048 | openssl pkcs8 -topk8 -v1 PBE-SHA1-3DES -passout pass:test012! -out client_encrypted.key.pem
|
||||
openssl req -new -batch -config test.conf -key client_encrypted.key.pem -passin pass:test012! | openssl x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial > client_encrypted.cert.pem
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# OpenSSL 3.x internal caches (provider, cipher, keymgmt) are allocated
|
||||
# lazily and intentionally kept until process exit. These are not real leaks.
|
||||
leak:libcrypto
|
||||
+3056
-470
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -26,7 +26,7 @@ TEST(ProxyTest, NoSSLBasic) {
|
||||
ProxyTest(cli, true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(ProxyTest, SSLBasic) {
|
||||
SSLClient cli("nghttp2.org");
|
||||
ProxyTest(cli, true);
|
||||
@@ -51,7 +51,7 @@ void RedirectProxyText(T &cli, const char *path, bool basic) {
|
||||
if (basic) {
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
} else {
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
cli.set_proxy_digest_auth("hello", "world");
|
||||
#endif
|
||||
}
|
||||
@@ -67,7 +67,7 @@ TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
||||
RedirectProxyText(cli, "/httpbin/redirect/2", true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
||||
Client cli("nghttp2.org");
|
||||
RedirectProxyText(cli, "/httpbin/redirect/2", false);
|
||||
@@ -84,7 +84,7 @@ TEST(RedirectTest, HTTPBinSSLDigest) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, YouTubeNoSSLBasic) {
|
||||
Client cli("youtube.com");
|
||||
RedirectProxyText(cli, "/", true);
|
||||
@@ -157,7 +157,7 @@ TEST(BaseAuthTest, NoSSL) {
|
||||
BaseAuthTestFromHTTPWatch(cli);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(BaseAuthTest, SSL) {
|
||||
SSLClient cli("httpcan.org");
|
||||
BaseAuthTestFromHTTPWatch(cli);
|
||||
@@ -166,7 +166,7 @@ TEST(BaseAuthTest, SSL) {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_proxy("localhost", 3129);
|
||||
cli.set_proxy_digest_auth("hello", "world");
|
||||
@@ -230,13 +230,13 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
if (basic) {
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
} else {
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
cli.set_proxy_digest_auth("hello", "world");
|
||||
#endif
|
||||
}
|
||||
|
||||
cli.set_follow_location(true);
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
cli.set_digest_auth("hello", "world");
|
||||
#endif
|
||||
|
||||
@@ -274,7 +274,7 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(KeepAliveTest, NoSSLWithBasic) {
|
||||
Client cli("nghttp2.org");
|
||||
KeepAliveTest(cli, true);
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
// ThreadPool unit tests
|
||||
// Set a short idle timeout for faster shrink tests
|
||||
#define CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT 1
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
using namespace httplib;
|
||||
|
||||
TEST(ThreadPoolTest, BasicTaskExecution) {
|
||||
ThreadPool pool(4);
|
||||
std::atomic<int> count(0);
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
pool.enqueue([&count]() { count++; });
|
||||
}
|
||||
|
||||
pool.shutdown();
|
||||
EXPECT_EQ(10, count.load());
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, FixedPoolWhenMaxEqualsBase) {
|
||||
// max_n == 0 means max = base (fixed pool behavior)
|
||||
ThreadPool pool(4);
|
||||
std::atomic<int> count(0);
|
||||
|
||||
for (int i = 0; i < 100; i++) {
|
||||
pool.enqueue([&count]() { count++; });
|
||||
}
|
||||
|
||||
pool.shutdown();
|
||||
EXPECT_EQ(100, count.load());
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, DynamicScaleUp) {
|
||||
// base=2, max=8: block 2 base threads, then enqueue more tasks
|
||||
ThreadPool pool(2, 8);
|
||||
|
||||
std::atomic<int> active(0);
|
||||
std::atomic<int> max_active(0);
|
||||
std::atomic<int> completed(0);
|
||||
std::mutex barrier_mutex;
|
||||
std::condition_variable barrier_cv;
|
||||
bool release = false;
|
||||
|
||||
// Occupy all base threads with blocking tasks
|
||||
for (int i = 0; i < 2; i++) {
|
||||
pool.enqueue([&]() {
|
||||
active++;
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(barrier_mutex);
|
||||
barrier_cv.wait(lock, [&] { return release; });
|
||||
}
|
||||
active--;
|
||||
completed++;
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for base threads to be occupied
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// These should trigger dynamic thread creation
|
||||
for (int i = 0; i < 4; i++) {
|
||||
pool.enqueue([&]() {
|
||||
int cur = ++active;
|
||||
// Track peak active count
|
||||
int prev = max_active.load();
|
||||
while (cur > prev && !max_active.compare_exchange_weak(prev, cur)) {}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
active--;
|
||||
completed++;
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for dynamic tasks to complete
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
// Release the blocking tasks
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(barrier_mutex);
|
||||
release = true;
|
||||
}
|
||||
barrier_cv.notify_all();
|
||||
|
||||
pool.shutdown();
|
||||
EXPECT_EQ(6, completed.load());
|
||||
// More than 2 threads were active simultaneously
|
||||
EXPECT_GT(max_active.load(), 2);
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, DynamicShrinkAfterIdle) {
|
||||
// CPPHTTPLIB_THREAD_POOL_IDLE_TIMEOUT is set to 1 second
|
||||
ThreadPool pool(2, 8);
|
||||
|
||||
std::atomic<int> completed(0);
|
||||
|
||||
// Enqueue tasks that require dynamic threads
|
||||
for (int i = 0; i < 8; i++) {
|
||||
pool.enqueue([&]() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
completed++;
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for all tasks to complete + idle timeout + margin
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2500));
|
||||
|
||||
// Now enqueue a simple task to verify the pool still works
|
||||
// (base threads are still alive)
|
||||
std::atomic<bool> final_task_done(false);
|
||||
pool.enqueue([&]() { final_task_done = true; });
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
pool.shutdown();
|
||||
EXPECT_EQ(8, completed.load());
|
||||
EXPECT_TRUE(final_task_done.load());
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, ShutdownWithActiveDynamicThreads) {
|
||||
ThreadPool pool(2, 8);
|
||||
|
||||
std::atomic<int> started(0);
|
||||
|
||||
std::mutex block_mutex;
|
||||
std::condition_variable block_cv;
|
||||
bool release = false;
|
||||
|
||||
// Start tasks on dynamic threads that block until released
|
||||
for (int i = 0; i < 6; i++) {
|
||||
pool.enqueue([&]() {
|
||||
started++;
|
||||
std::unique_lock<std::mutex> lock(block_mutex);
|
||||
block_cv.wait(lock, [&] { return release; });
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for tasks to start
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
EXPECT_GE(started.load(), 2);
|
||||
|
||||
// Release all blocked threads, then shutdown
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(block_mutex);
|
||||
release = true;
|
||||
}
|
||||
block_cv.notify_all();
|
||||
|
||||
pool.shutdown();
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, MaxQueuedRequests) {
|
||||
// base=2, max=2 (fixed), mqr=3
|
||||
ThreadPool pool(2, 2, 3);
|
||||
|
||||
std::mutex block_mutex;
|
||||
std::condition_variable block_cv;
|
||||
bool release = false;
|
||||
|
||||
// Block both threads
|
||||
for (int i = 0; i < 2; i++) {
|
||||
EXPECT_TRUE(pool.enqueue([&]() {
|
||||
std::unique_lock<std::mutex> lock(block_mutex);
|
||||
block_cv.wait(lock, [&] { return release; });
|
||||
}));
|
||||
}
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
// Fill the queue up to max_queued_requests
|
||||
EXPECT_TRUE(pool.enqueue([]() {}));
|
||||
EXPECT_TRUE(pool.enqueue([]() {}));
|
||||
EXPECT_TRUE(pool.enqueue([]() {}));
|
||||
|
||||
// This should fail - queue is full
|
||||
EXPECT_FALSE(pool.enqueue([]() {}));
|
||||
|
||||
// Release blocked threads
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(block_mutex);
|
||||
release = true;
|
||||
}
|
||||
block_cv.notify_all();
|
||||
|
||||
pool.shutdown();
|
||||
}
|
||||
|
||||
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
||||
TEST(ThreadPoolTest, InvalidMaxThreadsThrows) {
|
||||
// max_n < n should throw
|
||||
EXPECT_THROW(ThreadPool(8, 4), std::invalid_argument);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(ThreadPoolTest, EnqueueAfterShutdownReturnsFalse) {
|
||||
ThreadPool pool(2);
|
||||
pool.shutdown();
|
||||
EXPECT_FALSE(pool.enqueue([]() {}));
|
||||
}
|
||||
|
||||
TEST(ThreadPoolTest, ConcurrentEnqueue) {
|
||||
ThreadPool pool(4, 16);
|
||||
std::atomic<int> count(0);
|
||||
const int num_producers = 4;
|
||||
const int tasks_per_producer = 100;
|
||||
|
||||
std::vector<std::thread> producers;
|
||||
for (int p = 0; p < num_producers; p++) {
|
||||
producers.emplace_back([&]() {
|
||||
for (int i = 0; i < tasks_per_producer; i++) {
|
||||
pool.enqueue([&count]() { count++; });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (auto &t : producers) {
|
||||
t.join();
|
||||
}
|
||||
|
||||
pool.shutdown();
|
||||
EXPECT_EQ(num_producers * tasks_per_producer, count.load());
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Standalone test for WebSocket automatic heartbeat.
|
||||
// Compiled with a 1-second ping interval so we can verify heartbeat behavior
|
||||
// without waiting 30 seconds.
|
||||
|
||||
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
||||
#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 3
|
||||
#include <httplib.h>
|
||||
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
using namespace httplib;
|
||||
|
||||
class WebSocketHeartbeatTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
|
||||
std::string msg;
|
||||
while (ws.read(msg)) {
|
||||
ws.send(msg);
|
||||
}
|
||||
});
|
||||
|
||||
port_ = svr_.bind_to_any_port("localhost");
|
||||
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
|
||||
svr_.wait_until_ready();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
svr_.stop();
|
||||
thread_.join();
|
||||
}
|
||||
|
||||
Server svr_;
|
||||
int port_;
|
||||
std::thread thread_;
|
||||
};
|
||||
|
||||
// Verify that an idle connection stays alive beyond the read timeout
|
||||
// thanks to automatic heartbeat pings.
|
||||
TEST_F(WebSocketHeartbeatTest, IdleConnectionStaysAlive) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Sleep longer than read timeout (3s). Without heartbeat, the connection
|
||||
// would time out. With heartbeat pings every 1s, it stays alive.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(5));
|
||||
|
||||
// Connection should still be open
|
||||
ASSERT_TRUE(client.is_open());
|
||||
|
||||
// Verify we can still exchange messages
|
||||
ASSERT_TRUE(client.send("hello after idle"));
|
||||
std::string msg;
|
||||
ASSERT_TRUE(client.read(msg));
|
||||
EXPECT_EQ("hello after idle", msg);
|
||||
|
||||
client.close();
|
||||
}
|
||||
|
||||
// Verify that multiple heartbeat cycles work
|
||||
TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
|
||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Wait through several heartbeat cycles
|
||||
for (int i = 0; i < 3; i++) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
|
||||
ASSERT_TRUE(client.is_open());
|
||||
std::string text = "msg" + std::to_string(i);
|
||||
ASSERT_TRUE(client.send(text));
|
||||
std::string msg;
|
||||
ASSERT_TRUE(client.read(msg));
|
||||
EXPECT_EQ(text, msg);
|
||||
}
|
||||
|
||||
client.close();
|
||||
}
|
||||
Reference in New Issue
Block a user