mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-12 10:22:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00afaaed6a | ||
|
|
e803f5e413 | ||
|
|
57b8aca6da | ||
|
|
213029685a | ||
|
|
17eeb18feb | ||
|
|
edc9760005 | ||
|
|
6d1049462d | ||
|
|
4fcbc08f2d | ||
|
|
d59f3e438c | ||
|
|
09fa6820fe | ||
|
|
438319cfcb | ||
|
|
eda9a10bfe | ||
|
|
cf3693cb5c | ||
|
|
4fd9ae8f42 | ||
|
|
3d40dfc727 | ||
|
|
dd71728110 | ||
|
|
7255a7e979 | ||
|
|
8a3abfb597 | ||
|
|
10aadd57f7 | ||
|
|
43863e1f67 | ||
|
|
0db1df7cf2 | ||
|
|
639391ad7f | ||
|
|
0715c2739e | ||
|
|
3330d0eb06 | ||
|
|
c1c2b1f4b4 | ||
|
|
e11dbec7b3 | ||
|
|
174bce5ccf | ||
|
|
57c4f7f385 | ||
|
|
2fb2dbbe1e | ||
|
|
8b6ab24159 | ||
|
|
5a202d3d5f | ||
|
|
4f3f9ef19b | ||
|
|
6d59d1e2df | ||
|
|
9386b25dd7 | ||
|
|
91c55a4385 | ||
|
|
ad88645a83 | ||
|
|
82722fcb13 | ||
|
|
8b872605e0 | ||
|
|
52f214bf2e | ||
|
|
09c02f1335 | ||
|
|
2e5480ad65 | ||
|
|
4cb363e3f2 | ||
|
|
deb520e26b | ||
|
|
f37a5b1407 | ||
|
|
f15992c7ed | ||
|
|
278c2979e8 | ||
|
|
6b8c3f5387 | ||
|
|
e640f8376b | ||
|
|
73a4092f8a | ||
|
|
3517f92e2f | ||
|
|
0480ff77b8 | ||
|
|
8d25b6a3ac | ||
|
|
515b8f84af | ||
|
|
88956ccad8 | ||
|
|
f83d06538b | ||
|
|
dc41dbd954 | ||
|
|
6303a99ce1 | ||
|
|
d5f8858731 | ||
|
|
199d7ee248 | ||
|
|
9e2e33da56 | ||
|
|
7d53a31d23 | ||
|
|
9d6a7ee2c1 | ||
|
|
9ce15a14e5 | ||
|
|
9185fdb6ae | ||
|
|
644cda7032 | ||
|
|
2adfc45838 | ||
|
|
c7db3da982 | ||
|
|
139f30e0f1 | ||
|
|
b4ec1bb1de | ||
|
|
c58061ea81 | ||
|
|
f2b338ae2e | ||
|
|
efe709c45f | ||
|
|
794a997d8c | ||
|
|
e96a52e9dd | ||
|
|
2addb41089 | ||
|
|
ae417b405a | ||
|
|
bc7e51dbb9 | ||
|
|
f9c205632d | ||
|
|
84f75185fe | ||
|
|
19352ae929 | ||
|
|
2afe933103 | ||
|
|
bc58e6e9ac | ||
|
|
254e576b50 | ||
|
|
af75a4160f | ||
|
|
f3e5a93a5b | ||
|
|
00d1f54267 | ||
|
|
f82d2d90b6 | ||
|
|
228af9033b | ||
|
|
6494edd8c0 | ||
|
|
70b49d50bd | ||
|
|
2a068def54 | ||
|
|
abf525d78c | ||
|
|
0151b3e23e | ||
|
|
8b19c288e8 | ||
|
|
f442226581 | ||
|
|
3e3e4863b0 | ||
|
|
e8887d98e9 | ||
|
|
881842cd72 | ||
|
|
2731b728f5 | ||
|
|
161f787fee | ||
|
|
7ae9ffad3e | ||
|
|
ffe2a1c1e9 | ||
|
|
2004668509 | ||
|
|
d3ff68d28d | ||
|
|
b8fe69e4e8 | ||
|
|
acd0640870 | ||
|
|
2d8e49dd9b | ||
|
|
1e9d6f0b0b | ||
|
|
88240172ea | ||
|
|
89e0c5c238 | ||
|
|
f00e476f1b | ||
|
|
8e702d3837 | ||
|
|
19333f80d4 | ||
|
|
86d0210391 | ||
|
|
6018c7feb3 | ||
|
|
8d5085df1b | ||
|
|
8f0ff32056 | ||
|
|
2dd44d0f52 | ||
|
|
86abc9a0ea | ||
|
|
bd02a50cbb | ||
|
|
1c2607cbb8 | ||
|
|
98be5fd3a6 | ||
|
|
d2ef193b9c | ||
|
|
a1aa2ad9cd | ||
|
|
b2b1d56d6d | ||
|
|
2b8658fa99 |
@@ -31,12 +31,19 @@ env:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
style-check:
|
style-check:
|
||||||
runs-on: ubuntu-latest
|
# Uses the macOS runner's pre-installed Homebrew so clang-format tracks
|
||||||
|
# whatever version `brew install clang-format` currently resolves to on
|
||||||
|
# the maintainer's own Mac, instead of a version pinned in this file.
|
||||||
|
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
|
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
|
continue-on-error: true
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
- name: install clang-format
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install clang-format coreutils
|
||||||
- name: run style check
|
- name: run style check
|
||||||
run: |
|
run: |
|
||||||
clang-format --version
|
clang-format --version
|
||||||
@@ -114,6 +121,9 @@ jobs:
|
|||||||
- name: build and run WebSocket heartbeat test
|
- name: build and run WebSocket heartbeat test
|
||||||
if: matrix.tls_backend == 'openssl'
|
if: matrix.tls_backend == 'openssl'
|
||||||
run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
|
run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
|
||||||
|
- name: build and run WebSocket TLS thread safety test
|
||||||
|
if: matrix.tls_backend == 'openssl'
|
||||||
|
run: cd test && make test_websocket_thread_safety && ./test_websocket_thread_safety
|
||||||
- name: build and run ThreadPool test
|
- name: build and run ThreadPool test
|
||||||
run: cd test && make test_thread_pool && ./test_thread_pool
|
run: cd test && make test_thread_pool && ./test_thread_pool
|
||||||
|
|
||||||
@@ -414,6 +424,9 @@ jobs:
|
|||||||
- name: build and run WebSocket heartbeat test
|
- name: build and run WebSocket heartbeat test
|
||||||
if: matrix.tls_backend == 'openssl'
|
if: matrix.tls_backend == 'openssl'
|
||||||
run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
|
run: cd test && make test_websocket_heartbeat && ./test_websocket_heartbeat
|
||||||
|
- name: build and run WebSocket TLS thread safety test
|
||||||
|
if: matrix.tls_backend == 'openssl'
|
||||||
|
run: cd test && make test_websocket_thread_safety && ./test_websocket_thread_safety
|
||||||
- name: build and run ThreadPool test
|
- name: build and run ThreadPool test
|
||||||
run: cd test && make test_thread_pool && ./test_thread_pool
|
run: cd test && make test_thread_pool && ./test_thread_pool
|
||||||
|
|
||||||
@@ -467,6 +480,8 @@ jobs:
|
|||||||
|
|
||||||
windows:
|
windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
if: >
|
if: >
|
||||||
(github.event_name == 'push') ||
|
(github.event_name == 'push') ||
|
||||||
(github.event_name == 'pull_request' &&
|
(github.event_name == 'pull_request' &&
|
||||||
@@ -515,7 +530,52 @@ jobs:
|
|||||||
run: vcpkg install gtest curl zlib brotli zstd
|
run: vcpkg install gtest curl zlib brotli zstd
|
||||||
- name: Install OpenSSL
|
- name: Install OpenSSL
|
||||||
if: ${{ matrix.config.with_ssl }}
|
if: ${{ matrix.config.with_ssl }}
|
||||||
run: choco install openssl
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
# Chocolatey's openssl package hardcodes a versioned slproweb URL, and
|
||||||
|
# slproweb keeps only the newest build of each branch. The package
|
||||||
|
# therefore 404s on every OpenSSL release until someone respins it.
|
||||||
|
# Read slproweb's own manifest instead: it is updated at the same time
|
||||||
|
# as the downloads it points at, so the URL is always live.
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue' # Invoke-WebRequest is slow with it
|
||||||
|
|
||||||
|
$manifest = 'https://raw.githubusercontent.com/slproweb/opensslhashes/master/win32_openssl_hashes.json'
|
||||||
|
$entry = (Invoke-RestMethod $manifest).files.PSObject.Properties.Value |
|
||||||
|
Where-Object {
|
||||||
|
$_.bits -eq 64 -and $_.arch -eq 'INTEL' -and
|
||||||
|
-not $_.light -and $_.installer -eq 'exe' -and $_.basever -like '4.*'
|
||||||
|
} |
|
||||||
|
Sort-Object { [version]$_.basever } | Select-Object -Last 1
|
||||||
|
if (-not $entry) { throw 'No 64-bit OpenSSL 4.x installer found in the manifest' }
|
||||||
|
Write-Host "Installing OpenSSL $($entry.basever) from $($entry.url)"
|
||||||
|
|
||||||
|
$installer = Join-Path $env:RUNNER_TEMP 'Win64OpenSSL.exe'
|
||||||
|
Invoke-WebRequest $entry.url -OutFile $installer
|
||||||
|
$actual = (Get-FileHash $installer -Algorithm SHA512).Hash.ToLower()
|
||||||
|
if ($actual -ne $entry.sha512.ToLower()) {
|
||||||
|
throw "SHA512 mismatch: expected $($entry.sha512), got $actual"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Same silent flags the Chocolatey package used. The installer is Inno
|
||||||
|
# Setup, so /DIR pins the location CMake already looks in. The inner
|
||||||
|
# quotes matter: ArgumentList joins on spaces, so an unquoted /DIR
|
||||||
|
# would install to C:\Program and only fail later, at load time.
|
||||||
|
$dir = 'C:\Program Files\OpenSSL'
|
||||||
|
$proc = Start-Process $installer -Wait -PassThru -ArgumentList `
|
||||||
|
'/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-', "/DIR=`"$dir`""
|
||||||
|
if ($proc.ExitCode -ne 0) { throw "Installer exited with $($proc.ExitCode)" }
|
||||||
|
|
||||||
|
# Catch a misplaced install here rather than at link or load time.
|
||||||
|
if (-not (Test-Path "$dir\lib\VC\x64\MD\libcrypto.lib")) {
|
||||||
|
throw "OpenSSL import libraries missing under $dir"
|
||||||
|
}
|
||||||
|
if (-not (Get-ChildItem "$dir\bin\libcrypto-*.dll" -ErrorAction SilentlyContinue)) {
|
||||||
|
throw "OpenSSL runtime DLLs missing under $dir\bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
"$dir\bin" | Out-File $env:GITHUB_PATH -Append -Encoding utf8
|
||||||
|
"OPENSSL_CONF=$dir\bin\openssl.cfg" | Out-File $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
- name: Configure CMake ${{ matrix.config.name }}
|
- name: Configure CMake ${{ matrix.config.name }}
|
||||||
run: >
|
run: >
|
||||||
cmake -B build -S .
|
cmake -B build -S .
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
tags
|
tags
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
docs-src/pages/AGENTS.md
|
docs-src/pages/AGENTS.md
|
||||||
plans/
|
plans/
|
||||||
work/
|
work/
|
||||||
@@ -54,6 +55,7 @@ test/test_split_mbedtls
|
|||||||
test/test_split_wolfssl
|
test/test_split_wolfssl
|
||||||
test/test_split_no_tls
|
test/test_split_no_tls
|
||||||
test/test_websocket_heartbeat
|
test/test_websocket_heartbeat
|
||||||
|
test/test_websocket_thread_safety
|
||||||
test/test_thread_pool
|
test/test_thread_pool
|
||||||
test/test_benchmark
|
test/test_benchmark
|
||||||
test/test.xcodeproj/xcuser*
|
test/test.xcodeproj/xcuser*
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: local
|
||||||
rev: v18.1.8 # 最新バージョンを使用
|
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
files: \.(cpp|cc|h)$
|
name: clang-format
|
||||||
args: [-i] # インプレースで修正
|
# Uses whatever `clang-format` resolves to on PATH (the Homebrew
|
||||||
|
# install on macOS) instead of a version pinned here, so it tracks
|
||||||
|
# the same version CI installs via `brew install clang-format`.
|
||||||
|
entry: clang-format
|
||||||
|
language: system
|
||||||
|
files: ^(httplib\.h|(example|fuzzing|test)/[^/]+\.(cpp|cc|h))$
|
||||||
|
args: [-i]
|
||||||
|
|||||||
+21
-3
@@ -15,7 +15,7 @@
|
|||||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||||
* HTTPLIB_REQUIRE_ZSTD (default off)
|
* HTTPLIB_REQUIRE_ZSTD (default off)
|
||||||
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
||||||
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
|
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on when supported)
|
||||||
* HTTPLIB_COMPILE (default off)
|
* HTTPLIB_COMPILE (default off)
|
||||||
* HTTPLIB_INSTALL (default on)
|
* HTTPLIB_INSTALL (default on)
|
||||||
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
||||||
@@ -181,6 +181,24 @@ if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
|
|||||||
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
||||||
endif()
|
endif()
|
||||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
||||||
|
if(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO AND WIN32)
|
||||||
|
include(CheckCXXSymbolExists)
|
||||||
|
|
||||||
|
set(_httplib_cmake_required_definitions ${CMAKE_REQUIRED_DEFINITIONS})
|
||||||
|
set(_httplib_cmake_required_libraries ${CMAKE_REQUIRED_LIBRARIES})
|
||||||
|
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_WIN32_WINNT=0x0A00)
|
||||||
|
list(APPEND CMAKE_REQUIRED_LIBRARIES ws2_32)
|
||||||
|
check_cxx_symbol_exists(GetAddrInfoExCancel "winsock2.h;ws2tcpip.h" HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||||
|
set(CMAKE_REQUIRED_DEFINITIONS ${_httplib_cmake_required_definitions})
|
||||||
|
set(CMAKE_REQUIRED_LIBRARIES ${_httplib_cmake_required_libraries})
|
||||||
|
unset(_httplib_cmake_required_definitions)
|
||||||
|
unset(_httplib_cmake_required_libraries)
|
||||||
|
|
||||||
|
if(NOT HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||||
|
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO FALSE)
|
||||||
|
message(WARNING "GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
||||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||||
@@ -367,7 +385,7 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
|||||||
# Needed for API from MacOS Security framework
|
# Needed for API from MacOS Security framework
|
||||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
||||||
# Needed for non-blocking getaddrinfo on MacOS
|
# Needed for non-blocking getaddrinfo on MacOS
|
||||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||||
# Can't put multiple targets in a single generator expression or it bugs out.
|
# Can't put multiple targets in a single generator expression or it bugs out.
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
|
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
||||||
@@ -390,7 +408,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
|||||||
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
||||||
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
||||||
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
$<$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
||||||
)
|
)
|
||||||
|
|
||||||
# CMake configuration files installation directory
|
# CMake configuration files installation directory
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ sse.on_error([](httplib::Error err) { });
|
|||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Set reconnect interval (default: 3000ms)
|
// Set reconnect interval (default: 3000ms, minimum: 100ms)
|
||||||
sse.set_reconnect_interval(5000);
|
sse.set_reconnect_interval(5000);
|
||||||
|
|
||||||
// Set max reconnect attempts (default: 0 = unlimited)
|
// Set max reconnect attempts (default: 0 = unlimited)
|
||||||
|
|||||||
+104
-11
@@ -57,14 +57,30 @@ if (ws.connect()) {
|
|||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
enum ReadResult : int {
|
enum ReadResult : int {
|
||||||
Fail = 0, // Connection closed or error
|
Fail = 0, // Connection closed or error
|
||||||
Text = 1, // UTF-8 text message
|
Text = 1, // UTF-8 text message
|
||||||
Binary = 2, // Binary message
|
Binary = 2, // Binary message
|
||||||
|
Timeout = 3, // Read timeout elapsed; connection still open
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
`Timeout` is only returned for a read timeout you set yourself with `set_read_timeout()`. It means the timeout elapsed on a message boundary: nothing was consumed and the connection is still open, so you can send on it and read again. The compile-time defaults (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`, 300 seconds on the server; a client waits forever) are a backstop against a peer that has gone quiet, not a request for control: when one of them elapses, `read()` returns `Fail` and closes the connection, so code that never calls `set_read_timeout()` can keep using `while (ws.read(msg))`.
|
||||||
|
|
||||||
|
**`msg` is left untouched on `Timeout`.** Because `Timeout` is non-zero, `while (ws.read(msg))` keeps looping — with the *previous* message still in `msg`. Once you set a read timeout, test the result instead:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ws.set_read_timeout(std::chrono::milliseconds(100));
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) { continue; } // nothing yet; send if you like
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### CloseStatus
|
### CloseStatus
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -103,7 +119,7 @@ using SubProtocolSelector =
|
|||||||
std::function<std::string(const std::vector<std::string> &protocols)>;
|
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.
|
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. A returned value that the client did not propose is ignored.
|
||||||
|
|
||||||
### WebSocket (Server-side)
|
### WebSocket (Server-side)
|
||||||
|
|
||||||
@@ -135,11 +151,31 @@ bool is_open() const;
|
|||||||
explicit WebSocketClient(const std::string &scheme_host_port_path,
|
explicit WebSocketClient(const std::string &scheme_host_port_path,
|
||||||
const Headers &headers = {});
|
const Headers &headers = {});
|
||||||
|
|
||||||
|
// Constructor with a client certificate for mutual TLS (wss:// only,
|
||||||
|
// requires CPPHTTPLIB_OPENSSL_SUPPORT). The certificate is ignored for
|
||||||
|
// ws:// URLs.
|
||||||
|
struct PemMemory {
|
||||||
|
const char *cert_pem;
|
||||||
|
size_t cert_pem_len;
|
||||||
|
const char *key_pem;
|
||||||
|
size_t key_pem_len;
|
||||||
|
const char *private_key_password;
|
||||||
|
};
|
||||||
|
explicit WebSocketClient(const std::string &scheme_host_port_path,
|
||||||
|
const PemMemory &pem, const Headers &headers = {});
|
||||||
|
|
||||||
// Check if the URL was parsed successfully
|
// Check if the URL was parsed successfully
|
||||||
bool is_valid() const;
|
bool is_valid() const;
|
||||||
|
|
||||||
// Connect (performs HTTP upgrade handshake)
|
// Connect (performs HTTP upgrade handshake). The returned Result is truthy
|
||||||
bool connect();
|
// only when the handshake fully succeeded; on failure it describes what went
|
||||||
|
// wrong:
|
||||||
|
// res.error() httplib::Error identifying the failing layer
|
||||||
|
// res.status() HTTP status of the upgrade response (-1 if none)
|
||||||
|
// res.headers() headers of the upgrade response
|
||||||
|
// res.ssl_error() TLS error detail (wss://, SSL builds only)
|
||||||
|
// res.ssl_backend_error() backend-specific TLS error code (SSL builds only)
|
||||||
|
Result connect();
|
||||||
|
|
||||||
// Get the subprotocol selected by the server (empty if none)
|
// Get the subprotocol selected by the server (empty if none)
|
||||||
const std::string &subprotocol() const;
|
const std::string &subprotocol() const;
|
||||||
@@ -155,11 +191,20 @@ bool is_open() const;
|
|||||||
// Timeouts
|
// Timeouts
|
||||||
void set_read_timeout(time_t sec, time_t usec = 0);
|
void set_read_timeout(time_t sec, time_t usec = 0);
|
||||||
void set_write_timeout(time_t sec, time_t usec = 0);
|
void set_write_timeout(time_t sec, time_t usec = 0);
|
||||||
|
void set_connection_timeout(time_t sec, time_t usec = 0);
|
||||||
|
template <class Rep, class Period>
|
||||||
|
void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
|
||||||
|
template <class Rep, class Period>
|
||||||
|
void set_write_timeout(const std::chrono::duration<Rep, Period> &duration);
|
||||||
|
template <class Rep, class Period>
|
||||||
|
void set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
|
||||||
|
|
||||||
// SSL configuration (wss:// only, requires CPPHTTPLIB_OPENSSL_SUPPORT)
|
// SSL configuration (wss:// only, requires CPPHTTPLIB_OPENSSL_SUPPORT)
|
||||||
void set_ca_cert_path(const std::string &path);
|
void set_ca_cert_path(const std::string &ca_cert_file_path,
|
||||||
|
const std::string &ca_cert_dir_path = std::string());
|
||||||
void set_ca_cert_store(tls::ca_store_t store);
|
void set_ca_cert_store(tls::ca_store_t store);
|
||||||
void enable_server_certificate_verification(bool enabled);
|
void enable_server_certificate_verification(bool enabled);
|
||||||
|
void enable_server_hostname_verification(bool enabled);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@@ -200,6 +245,26 @@ if (ws.connect()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Inspecting Connection Failures
|
||||||
|
|
||||||
|
`connect()` returns a `Result` that tells you why a connection attempt failed.
|
||||||
|
`error()` distinguishes network problems (`Connection`, `ConnectionTimeout`),
|
||||||
|
TLS problems (`SSLConnection`, `SSLServerVerification`,
|
||||||
|
`SSLServerHostnameVerification`), and upgrade rejections
|
||||||
|
(`WebSocketHandshake`). When the server answered with something other than
|
||||||
|
`101 Switching Protocols`, `status()` and `headers()` carry that response:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto res = ws.connect();
|
||||||
|
if (!res) {
|
||||||
|
std::cerr << "connect failed: " << httplib::to_string(res.error()) << std::endl;
|
||||||
|
if (res.status() != -1) {
|
||||||
|
// The server responded but refused the upgrade (e.g. 401, 404)
|
||||||
|
std::cerr << "HTTP status: " << res.status() << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Text and Binary Messages
|
### Text and Binary Messages
|
||||||
|
|
||||||
Check the `ReadResult` return value to distinguish between text and binary:
|
Check the `ReadResult` return value to distinguish between text and binary:
|
||||||
@@ -278,6 +343,18 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The check above runs after the handshake, so the client sees a successful upgrade followed by a close frame. To refuse the upgrade itself with an HTTP status, use a pre-routing or pre-request handler. Both run before the `101 Switching Protocols` response, and `req.matched_route` is available in the pre-request handler:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
if (req.matched_route == "/ws" && req.get_header_value("Authorization").empty()) {
|
||||||
|
res.status = httplib::StatusCode::Unauthorized_401;
|
||||||
|
return httplib::Server::HandlerResponse::Handled; // not upgraded
|
||||||
|
}
|
||||||
|
return httplib::Server::HandlerResponse::Unhandled;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Custom Headers and Timeouts
|
### Custom Headers and Timeouts
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -286,8 +363,14 @@ httplib::Headers headers = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws", headers);
|
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws", headers);
|
||||||
ws.set_read_timeout(30, 0); // 30 seconds
|
ws.set_connection_timeout(5, 0); // 5 seconds
|
||||||
ws.set_write_timeout(10, 0); // 10 seconds
|
ws.set_read_timeout(30, 0); // 30 seconds
|
||||||
|
ws.set_write_timeout(10, 0); // 10 seconds
|
||||||
|
|
||||||
|
// std::chrono is also supported
|
||||||
|
ws.set_connection_timeout(std::chrono::seconds(5));
|
||||||
|
ws.set_read_timeout(std::chrono::seconds(30));
|
||||||
|
ws.set_write_timeout(std::chrono::seconds(10));
|
||||||
|
|
||||||
if (ws.connect()) {
|
if (ws.connect()) {
|
||||||
std::string msg;
|
std::string msg;
|
||||||
@@ -341,6 +424,7 @@ if (ws.connect()) {
|
|||||||
httplib::ws::WebSocketClient ws("wss://example.com/ws");
|
httplib::ws::WebSocketClient ws("wss://example.com/ws");
|
||||||
ws.set_ca_cert_path("/path/to/ca-bundle.crt");
|
ws.set_ca_cert_path("/path/to/ca-bundle.crt");
|
||||||
ws.enable_server_certificate_verification(true);
|
ws.enable_server_certificate_verification(true);
|
||||||
|
ws.enable_server_hostname_verification(true); // default; false skips the identity check
|
||||||
|
|
||||||
if (ws.connect()) {
|
if (ws.connect()) {
|
||||||
ws.send("secure message");
|
ws.send("secure message");
|
||||||
@@ -353,7 +437,8 @@ if (ws.connect()) {
|
|||||||
| Macro | Default | Description |
|
| Macro | Default | Description |
|
||||||
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
||||||
| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message |
|
| `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_CLIENT_READ_TIMEOUT_SECOND` | `0` | Client read timeout (seconds); `0` waits forever |
|
||||||
|
| `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` | `300` | Server read timeout (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (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) |
|
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
||||||
@@ -390,7 +475,7 @@ The server side has the same `set_websocket_max_missed_pongs()`.
|
|||||||
|
|
||||||
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
|
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
|
||||||
|
|
||||||
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. Even so, a dead connection does not linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. `max_missed_pongs` is the knob for detecting an unresponsive peer faster than that 5-minute fallback.
|
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. On the server side a dead connection still does not linger: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no such backstop — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side it is also the knob for noticing one *faster* than the 5-minute fallback.
|
||||||
|
|
||||||
## Threading Model
|
## Threading Model
|
||||||
|
|
||||||
@@ -410,6 +495,14 @@ svr.new_task_queue = [] {
|
|||||||
|
|
||||||
Choose sizes that account for both your expected HTTP load and the maximum number of simultaneous WebSocket connections.
|
Choose sizes that account for both your expected HTTP load and the maximum number of simultaneous WebSocket connections.
|
||||||
|
|
||||||
|
### Calling from Multiple Threads
|
||||||
|
|
||||||
|
A single `WebSocket` (server-side) or `WebSocketClient` handle is shared by three potential callers: the thread running your handler (or holding the client), the heartbeat thread, and, if your code does its own thing, a separate thread calling `send()`/`close()` while another thread is blocked in `read()`.
|
||||||
|
|
||||||
|
**Supported**: calling `read()` from one thread while calling `send()`/`close()` from another. This is the common pattern for a client that reads incoming messages in a loop on one thread and sends from elsewhere (e.g. a UI thread). A message that is in flight when `close()` is called still arrives intact; `close()` sends the Close frame and returns, leaving the connection's read side to the thread that owns it, so it does not block waiting for the peer's Close reply in that case. The heartbeat thread's automatic pings use the same `send()` path internally, so they are safe to run concurrently with your `read()` loop too — for `wss://` this requires every TLS call on a connection to be serialized internally, which cpp-httplib does for you.
|
||||||
|
|
||||||
|
**Not supported**: calling `read()` from two threads at the same time on the same handle. The calls are serialized rather than left to corrupt each other, but which thread receives which message is unspecified, so there is nothing useful to build on it.
|
||||||
|
|
||||||
## Protocol
|
## Protocol
|
||||||
|
|
||||||
The implementation follows [RFC 6455](https://tools.ietf.org/html/rfc6455):
|
The implementation follows [RFC 6455](https://tools.ietf.org/html/rfc6455):
|
||||||
|
|||||||
@@ -168,6 +168,41 @@ cli.set_server_certificate_verifier(
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Mutual TLS (mTLS)
|
||||||
|
|
||||||
|
Regular TLS only verifies the server certificate. With mTLS, the client also presents a certificate that the server verifies.
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// Server: pass a CA to verify client certificates against
|
||||||
|
httplib::SSLServer svr("./cert.pem", "./key.pem", "./client-ca-cert.pem");
|
||||||
|
|
||||||
|
// Client: present a certificate
|
||||||
|
httplib::SSLClient cli("api.example.com", 443,
|
||||||
|
"./client-cert.pem", "./client-key.pem");
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `SSLServer` and `SSLClient` also accept an in-memory `PemMemory` struct instead of file paths — handy when certs come from an environment variable or a secrets manager:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
httplib::SSLServer::PemMemory server_pem{};
|
||||||
|
server_pem.cert_pem = server_cert.data();
|
||||||
|
server_pem.cert_pem_len = server_cert.size();
|
||||||
|
server_pem.key_pem = server_key.data();
|
||||||
|
server_pem.key_pem_len = server_key.size();
|
||||||
|
server_pem.client_ca_pem = client_ca.data();
|
||||||
|
server_pem.client_ca_pem_len = client_ca.size();
|
||||||
|
httplib::SSLServer svr(server_pem);
|
||||||
|
|
||||||
|
httplib::SSLClient::PemMemory client_pem{};
|
||||||
|
client_pem.cert_pem = client_cert.data();
|
||||||
|
client_pem.cert_pem_len = client_cert.size();
|
||||||
|
client_pem.key_pem = client_key.data();
|
||||||
|
client_pem.key_pem_len = client_key.size();
|
||||||
|
httplib::SSLClient cli("api.example.com", 443, client_pem);
|
||||||
|
```
|
||||||
|
|
||||||
|
`httplib::ws::WebSocketClient` has the same `PemMemory` constructor for `wss://` connections. See [README-websocket.md](README-websocket.md) for details.
|
||||||
|
|
||||||
### Peer Certificate Inspection
|
### Peer Certificate Inspection
|
||||||
|
|
||||||
On the server side, you can inspect the client's peer certificate from a request handler:
|
On the server side, you can inspect the client's peer certificate from a request handler:
|
||||||
@@ -195,7 +230,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
|
|||||||
| Platform | Behavior | Disable (compile time) |
|
| Platform | Behavior | Disable (compile time) |
|
||||||
| :------- | :------- | :--------------------- |
|
| :------- | :------- | :--------------------- |
|
||||||
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
||||||
| Windows | Verifies certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend. Revocation checking is best-effort: a revoked server certificate is rejected, while one whose revocation status cannot be determined is accepted. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||||
|
|
||||||
On Windows, verification can also be disabled at runtime:
|
On Windows, verification can also be disabled at runtime:
|
||||||
|
|
||||||
@@ -272,6 +307,39 @@ int main(void)
|
|||||||
|
|
||||||
`Post`, `Put`, `Patch`, `Delete` and `Options` methods are also supported.
|
`Post`, `Put`, `Patch`, `Delete` and `Options` methods are also supported.
|
||||||
|
|
||||||
|
### Custom HTTP methods
|
||||||
|
|
||||||
|
Methods outside the built-in set are rejected with `400 Bad Request` unless a handler is registered for them with `CustomRoute`. This covers the WebDAV methods of RFC 4918, `SUBSCRIBE` and friends from UPnP, and any other extension method.
|
||||||
|
|
||||||
|
```c++
|
||||||
|
svr.CustomRoute("PROPFIND", "/dav/:id", [](const Request& req, Response& res) {
|
||||||
|
// The request body is available as usual
|
||||||
|
auto id = req.path_params.at("id");
|
||||||
|
res.status = StatusCode::MultiStatus_207;
|
||||||
|
res.set_content(build_multistatus(req.body), "application/xml");
|
||||||
|
});
|
||||||
|
|
||||||
|
// A content reader overload is available too
|
||||||
|
svr.CustomRoute("REPORT", "/dav/.*",
|
||||||
|
[](const Request& req, Response& res,
|
||||||
|
const ContentReader& content_reader) {
|
||||||
|
content_reader([&](const char* data, size_t data_length) {
|
||||||
|
// ...
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Patterns work exactly as they do for `Get` and the other methods, so regular expressions and path parameters are both available.
|
||||||
|
|
||||||
|
Note the following:
|
||||||
|
|
||||||
|
* The method name must be a valid HTTP method token (RFC 9110) and must be registered before `listen()` is called.
|
||||||
|
* `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH` and `PRI` cannot be registered this way. Use the dedicated methods above instead.
|
||||||
|
* A rejected registration makes `is_valid()` return `false`, and `listen()` then fails rather than starting a server with a route that would never fire.
|
||||||
|
* Static file serving and WebSocket upgrades remain `GET`/`HEAD` only.
|
||||||
|
* `Allow` and the WebDAV `DAV:` header are not generated automatically. Register an `Options` handler if clients need them.
|
||||||
|
|
||||||
### Bind a socket to multiple interfaces and any available port
|
### Bind a socket to multiple interfaces and any available port
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -279,6 +347,25 @@ int port = svr.bind_to_any_port("0.0.0.0");
|
|||||||
svr.listen_after_bind();
|
svr.listen_after_bind();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Port sharing and exclusive binding
|
||||||
|
|
||||||
|
By default, the server socket enables address/port reuse: `SO_REUSEPORT` where it is available (Linux, macOS), and `SO_REUSEADDR` otherwise (Windows). A restarted server can bind again immediately, but binding to a port that another server is already listening on also succeeds, and connections are distributed between them.
|
||||||
|
|
||||||
|
If you want `listen()` to fail when the port is already in use, replace the default socket options with `set_socket_options`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Setting only `SO_REUSEADDR` is not enough on Windows. There, `SO_REUSEADDR` allows two sockets that both set it to bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
|
||||||
|
|
||||||
### Static File Server
|
### Static File Server
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -365,6 +452,9 @@ svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> `req.path` is percent-decoded and may contain control characters such as CR/LF. Escape request data before writing it to a log file (see [docker/main.cc](docker/main.cc) for an example).
|
||||||
|
|
||||||
#### Pre-compression Logging
|
#### Pre-compression Logging
|
||||||
|
|
||||||
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
||||||
@@ -379,6 +469,8 @@ svr.set_pre_compression_logger([](const httplib::Request& req, const httplib::Re
|
|||||||
|
|
||||||
The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called.
|
The pre-compression logger is only called when compression would be applied. For responses without compression, only the access logger is called.
|
||||||
|
|
||||||
|
For a static file response (see [Static file compression](#static-file-compression)), `res.body` is empty when the logger runs. The bytes are still on disk at that point, not in memory.
|
||||||
|
|
||||||
#### Error Logging
|
#### 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.
|
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.
|
||||||
@@ -475,14 +567,15 @@ svr.set_pre_request_handler([](const auto& req, auto& res) {
|
|||||||
|
|
||||||
```
|
```
|
||||||
Request received
|
Request received
|
||||||
|
│
|
||||||
|
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
|
||||||
|
│ └─ returns a status other than 100 → stop here
|
||||||
│
|
│
|
||||||
├─ pre_routing_handler route not matched yet, body not read
|
├─ pre_routing_handler route not matched yet, body not read
|
||||||
│ └─ returns Handled → stop here
|
│ └─ returns Handled → stop here
|
||||||
│
|
│
|
||||||
├─ file_request_handler (GET/HEAD, static file serving)
|
├─ file_request_handler (GET/HEAD, static file serving)
|
||||||
│
|
│
|
||||||
├─ expect_100_continue_handler (when the request has "Expect: 100-continue")
|
|
||||||
│
|
|
||||||
├─ route matching → req.matched_route is set
|
├─ route matching → req.matched_route is set
|
||||||
│
|
│
|
||||||
├─ pre_request_handler route matched, body NOT read yet
|
├─ pre_request_handler route matched, body NOT read yet
|
||||||
@@ -498,6 +591,10 @@ Request received
|
|||||||
|
|
||||||
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
|
Use `pre_routing_handler` to reject a request as early as possible, before the route is known. Use `pre_request_handler` for route-specific checks, since `req.matched_route` is available and the body has not been read yet.
|
||||||
|
|
||||||
|
For a request with `Expect: 100-continue`, the `100 Continue` response is not sent until the body is about to be read. A request rejected before that point (by `pre_routing_handler`, `pre_request_handler`, or because no route matched) gets its final response without `100 Continue`, so the client never sends the body.
|
||||||
|
|
||||||
|
A WebSocket upgrade request that matches a route registered with `svr.WebSocket()` takes a shorter path: `pre_routing_handler`, then route matching (`req.matched_route` is set), then `pre_request_handler`, then the WebSocket handler. If either hook returns `Handled`, its response is sent as a regular HTTP response and the connection is not upgraded. Once the connection is upgraded, `post_routing_handler` does not run.
|
||||||
|
|
||||||
### Response user data
|
### Response user data
|
||||||
|
|
||||||
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
`res.user_data` is a type-safe key-value store that lets pre-routing or pre-request handlers pass arbitrary data to route handlers.
|
||||||
@@ -660,6 +757,12 @@ svr.Post("/content_receiver",
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (default 1024) caps the number of
|
||||||
|
form-data parts only on the buffered path, where every part is accumulated into
|
||||||
|
`req.form`. The content receiver keeps nothing, so the cap does not apply here.
|
||||||
|
If your handler needs an upper bound on the number of parts, count them yourself
|
||||||
|
and return `false` from the callback to stop the parser.
|
||||||
|
|
||||||
### Send content with the content provider
|
### Send content with the content provider
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -751,7 +854,9 @@ svr.Get("/content", [&](const Request &req, Response &res) {
|
|||||||
|
|
||||||
### 'Expect: 100-continue' handler
|
### 'Expect: 100-continue' handler
|
||||||
|
|
||||||
By default, the server sends a `100 Continue` response for an `Expect: 100-continue` header.
|
By default, the server accepts an `Expect: 100-continue` header and sends a `100 Continue` response when it starts reading the request body. If the request is answered without reading the body, `100 Continue` is not sent and the connection is closed after the response.
|
||||||
|
|
||||||
|
The handler runs before `pre_routing_handler`. Returning `100` lets the request proceed; returning any other status sends that status as the final response and closes the connection.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Send a '417 Expectation Failed' response.
|
// Send a '417 Expectation Failed' response.
|
||||||
@@ -1290,6 +1395,8 @@ res->status; // 200
|
|||||||
cli.set_interface("eth0"); // Interface name, IP address or host name
|
cli.set_interface("eth0"); // Interface name, IP address or host name
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The same method is available on `httplib::ws::WebSocketClient`.
|
||||||
|
|
||||||
### Override the connection target for a hostname
|
### Override the connection target for a hostname
|
||||||
|
|
||||||
`set_hostname_addr_map` redirects where the socket connects, without changing
|
`set_hostname_addr_map` redirects where the socket connects, without changing
|
||||||
@@ -1357,6 +1464,29 @@ httplib::Server svr;
|
|||||||
svr.listen("127.0.0.1", 8080);
|
svr.listen("127.0.0.1", 8080);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Ordered Headers, Query Parameters, and Form Data
|
||||||
|
|
||||||
|
`Headers`, `Params`, `FormFields`, and `FormFiles` preserve the order entries were received (for a parsed request) or inserted (for one you build yourself). Earlier versions stored these in `std::multimap` or `std::unordered_multimap`, which either sorted entries by key or gave no ordering guarantee at all for repeated keys. RFC 9110 §5.3 and RFC 7578 §5.2 both require the original order to be preserved, so this is now guaranteed rather than incidental.
|
||||||
|
|
||||||
|
```c++
|
||||||
|
// A request with two Accept-Encoding lines...
|
||||||
|
// Accept-Encoding: gzip
|
||||||
|
// Accept-Encoding: br
|
||||||
|
// ...visits "gzip" before "br", not the other way around.
|
||||||
|
for (auto it = req.headers.equal_range("Accept-Encoding").first;
|
||||||
|
it != req.headers.end(); ++it) {
|
||||||
|
std::cout << it->second << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get_header_value(key, id) reaches a specific one directly.
|
||||||
|
auto second = req.get_header_value("Accept-Encoding", 1); // "br"
|
||||||
|
```
|
||||||
|
|
||||||
|
`Headers` matches field names case-insensitively, as before. `Params`, `FormFields`, and `FormFiles` are case-sensitive.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> Iterators on these containers follow `std::vector` rules: inserting a new entry invalidates existing iterators. Code that keeps an iterator across a call to `insert()`/`emplace()` needs to re-fetch it afterward.
|
||||||
|
|
||||||
## Payload Limit
|
## 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.
|
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.
|
||||||
@@ -1373,6 +1503,45 @@ The server can apply compression to the following MIME type contents:
|
|||||||
- application/protobuf
|
- application/protobuf
|
||||||
- application/xhtml+xml
|
- application/xhtml+xml
|
||||||
|
|
||||||
|
A response that already carries `Content-Encoding` is sent as it is. A handler serving content it encoded itself, an asset compressed at build time for instance, keeps its own coding and its own bytes:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
svr.Get("/app.js", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_header("Content-Encoding", "gzip");
|
||||||
|
res.set_content(gzipped_asset, "application/javascript");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This holds for every kind of response, including the file-backed ones below.
|
||||||
|
|
||||||
|
`Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
|
||||||
|
|
||||||
|
### Static file compression
|
||||||
|
|
||||||
|
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
svr.set_static_file_compression(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
Only files within a size range are compressed, and both ends of it can be moved:
|
||||||
|
|
||||||
|
```c++
|
||||||
|
svr.set_static_file_compression_min_length(512);
|
||||||
|
svr.set_static_file_compression_max_length(1024 * 1024);
|
||||||
|
```
|
||||||
|
|
||||||
|
The lower bound defaults to 1400 bytes. A response that already fits in a single 1500-byte MTU is not delivered any faster for being smaller, and a file of a few bytes comes back larger than it went in, since gzip's header and trailer outweigh what deflate saves. `0` compresses everything down to a single byte, and `CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH` sets the default at compile time. An empty file is never compressed regardless.
|
||||||
|
|
||||||
|
The upper bound defaults to 4MB, and exists for a different reason: the file is compressed per request, and the compressed bytes are held in memory until the response has been written, so the peak cost scales with the number of requests in flight. It is a bound on what one request can cost, not a statement about how well large files compress, which is why raising it is reasonable when the files are known and the traffic is not. `0` removes the limit, and `CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH` sets the default at compile time.
|
||||||
|
|
||||||
|
A compressed response keeps its `Content-Length`, so `HEAD` still reports the size a `GET` would return. Two details are worth knowing:
|
||||||
|
|
||||||
|
- Range requests are answered from the uncompressed representation, so `Content-Range` keeps naming the file's own bytes.
|
||||||
|
- The `ETag` carries the coding it belongs to (`W/"...-gzip"`), so a client that cached the compressed form revalidates against the right validator.
|
||||||
|
|
||||||
|
Content providers registered with `set_content_provider()` are not covered. Feeding one through a compressor would hold each write back until the compressor's window filled, which breaks providers that produce their body incrementally. Use `set_chunked_content_provider()` to compress a generated body.
|
||||||
|
|
||||||
### Zlib Support
|
### Zlib Support
|
||||||
|
|
||||||
'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. `libz` should be linked.
|
'gzip' compression is available with `CPPHTTPLIB_ZLIB_SUPPORT`. `libz` should be linked.
|
||||||
@@ -1421,7 +1590,6 @@ res->body; // Compressed data
|
|||||||
Unix Domain Socket Support
|
Unix Domain Socket Support
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
Unix Domain Socket support is available on Linux and macOS.
|
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
// Server
|
// Server
|
||||||
|
|||||||
+68
-9
@@ -3,7 +3,25 @@
|
|||||||
# A/B throughput comparison between two git refs.
|
# A/B throughput comparison between two git refs.
|
||||||
#
|
#
|
||||||
# Usage: ./ab.sh [--base REF] [--head REF] [--rounds N] [--duration S]
|
# Usage: ./ab.sh [--base REF] [--head REF] [--rounds N] [--duration S]
|
||||||
# [--connections N] [--threads N]
|
# [--connections N] [--threads N] [--path PATH] [--tls]
|
||||||
|
# [--large-mib N] [--timeout S]
|
||||||
|
#
|
||||||
|
# --path selects the workload. The harness serves:
|
||||||
|
# / small body via set_content(); the response line, the
|
||||||
|
# headers and the body already share a single write(), so
|
||||||
|
# this is the least sensitive case
|
||||||
|
# /large large body via set_content()
|
||||||
|
# /static/small.js 1 KiB file from a mount point, where the headers and the
|
||||||
|
# body are two separate writes
|
||||||
|
# /static/large.bin same, with the body large enough to dominate
|
||||||
|
#
|
||||||
|
# --large-mib sizes the two large workloads (default 1).
|
||||||
|
#
|
||||||
|
# --tls runs the same workload over HTTPS, which writes through
|
||||||
|
# SSLSocketStream instead of SocketStream.
|
||||||
|
#
|
||||||
|
# --timeout is bombardier's per-request timeout. Its 2s default aborts large
|
||||||
|
# TLS responses, and the run then fails on the non-2xx check.
|
||||||
#
|
#
|
||||||
# Absolute numbers from a single run are meaningless: on a quiet 8-core laptop
|
# Absolute numbers from a single run are meaningless: on a quiet 8-core laptop
|
||||||
# the same binary varies by +/-20% run to run, and shared CI runners are worse.
|
# the same binary varies by +/-20% run to run, and shared CI runners are worse.
|
||||||
@@ -21,6 +39,10 @@ DURATION="5s"
|
|||||||
CONNECTIONS=10
|
CONNECTIONS=10
|
||||||
THREADS=""
|
THREADS=""
|
||||||
PORT=8080
|
PORT=8080
|
||||||
|
REQ_PATH="/"
|
||||||
|
TLS=0
|
||||||
|
LARGE_MIB=1
|
||||||
|
TIMEOUT="30s"
|
||||||
|
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -30,6 +52,10 @@ while [ $# -gt 0 ]; do
|
|||||||
--duration) DURATION="$2"; shift 2 ;;
|
--duration) DURATION="$2"; shift 2 ;;
|
||||||
--connections) CONNECTIONS="$2"; shift 2 ;;
|
--connections) CONNECTIONS="$2"; shift 2 ;;
|
||||||
--threads) THREADS="$2"; shift 2 ;;
|
--threads) THREADS="$2"; shift 2 ;;
|
||||||
|
--path) REQ_PATH="$2"; shift 2 ;;
|
||||||
|
--large-mib) LARGE_MIB="$2"; shift 2 ;;
|
||||||
|
--timeout) TIMEOUT="$2"; shift 2 ;;
|
||||||
|
--tls) TLS=1; shift ;;
|
||||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -62,6 +88,7 @@ HEAD_SHA=$(git -C "$REPO_ROOT" rev-parse --short "$HEAD_REF")
|
|||||||
echo "==> base: $BASE_REF ($BASE_SHA)"
|
echo "==> base: $BASE_REF ($BASE_SHA)"
|
||||||
echo "==> head: $HEAD_REF ($HEAD_SHA)"
|
echo "==> head: $HEAD_REF ($HEAD_SHA)"
|
||||||
echo "==> rounds=$ROUNDS duration=$DURATION connections=$CONNECTIONS threads=$THREADS"
|
echo "==> rounds=$ROUNDS duration=$DURATION connections=$CONNECTIONS threads=$THREADS"
|
||||||
|
echo "==> path=$REQ_PATH tls=$TLS large=${LARGE_MIB}MiB"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
|
if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
|
||||||
@@ -69,18 +96,49 @@ if [ "$BASE_SHA" = "$HEAD_SHA" ]; then
|
|||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- Toolchain bits that depend on --tls ---
|
||||||
|
SCHEME="http"
|
||||||
|
INSECURE=""
|
||||||
|
TLS_CXXFLAGS=""
|
||||||
|
TLS_LDFLAGS=""
|
||||||
|
TLS_ARGS=""
|
||||||
|
if [ "$TLS" = "1" ]; then
|
||||||
|
SCHEME="https"
|
||||||
|
INSECURE="-k"
|
||||||
|
TLS_CXXFLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT"
|
||||||
|
TLS_LDFLAGS="-lssl -lcrypto"
|
||||||
|
if command -v pkg-config >/dev/null 2>&1 && pkg-config --exists openssl; then
|
||||||
|
TLS_CXXFLAGS="$TLS_CXXFLAGS $(pkg-config --cflags openssl)"
|
||||||
|
TLS_LDFLAGS="$(pkg-config --libs openssl)"
|
||||||
|
elif command -v brew >/dev/null 2>&1 && brew --prefix openssl >/dev/null 2>&1; then
|
||||||
|
OPENSSL_PREFIX=$(brew --prefix openssl)
|
||||||
|
TLS_CXXFLAGS="$TLS_CXXFLAGS -I$OPENSSL_PREFIX/include"
|
||||||
|
TLS_LDFLAGS="-L$OPENSSL_PREFIX/lib -lssl -lcrypto"
|
||||||
|
fi
|
||||||
|
if [ "$(uname -s)" = "Darwin" ]; then
|
||||||
|
TLS_LDFLAGS="$TLS_LDFLAGS -framework CoreFoundation -framework Security"
|
||||||
|
fi
|
||||||
|
TLS_ARGS="--cert $REPO_ROOT/test/cert.pem --key $REPO_ROOT/test/key.pem"
|
||||||
|
for f in "$REPO_ROOT/test/cert.pem" "$REPO_ROOT/test/key.pem"; do
|
||||||
|
[ -f "$f" ] || { echo "Error: $f not found" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# --- Build both refs ---
|
# --- Build both refs ---
|
||||||
|
# The harness source always comes from the invoking worktree, so both refs run
|
||||||
|
# an identical workload and a ref that predates a harness change stays
|
||||||
|
# measurable. Only httplib.h varies, through -I.
|
||||||
|
HARNESS="$REPO_ROOT/benchmark/cpp-httplib/main.cpp"
|
||||||
|
[ -f "$HARNESS" ] || { echo "Error: $HARNESS not found" >&2; exit 1; }
|
||||||
|
|
||||||
build() {
|
build() {
|
||||||
local name=$1 ref=$2
|
local name=$1 ref=$2
|
||||||
git -C "$REPO_ROOT" worktree add --detach --quiet "$WORKDIR/$name" "$ref"
|
git -C "$REPO_ROOT" worktree add --detach --quiet "$WORKDIR/$name" "$ref"
|
||||||
if [ ! -f "$WORKDIR/$name/benchmark/cpp-httplib/main.cpp" ]; then
|
|
||||||
echo "Error: benchmark/cpp-httplib/main.cpp missing in $ref" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
"$CXX" -o "$WORKDIR/$name/server-ab" -O2 -std=c++11 \
|
"$CXX" -o "$WORKDIR/$name/server-ab" -O2 -std=c++11 \
|
||||||
-I"$WORKDIR/$name" \
|
-I"$WORKDIR/$name" \
|
||||||
-DCPPHTTPLIB_THREAD_POOL_COUNT="$THREADS" \
|
-DCPPHTTPLIB_THREAD_POOL_COUNT="$THREADS" \
|
||||||
"$WORKDIR/$name/benchmark/cpp-httplib/main.cpp" -lpthread
|
$TLS_CXXFLAGS \
|
||||||
|
"$HARNESS" -lpthread $TLS_LDFLAGS
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "==> Building..."
|
echo "==> Building..."
|
||||||
@@ -92,7 +150,8 @@ measure() {
|
|||||||
local name=$1
|
local name=$1
|
||||||
local json rc
|
local json rc
|
||||||
|
|
||||||
"$WORKDIR/$name/server-ab" >/dev/null 2>&1 &
|
"$WORKDIR/$name/server-ab" --port "$PORT" --dir "$WORKDIR/$name-www" \
|
||||||
|
--large-mib "$LARGE_MIB" $TLS_ARGS >/dev/null 2>&1 &
|
||||||
local pid=$!
|
local pid=$!
|
||||||
|
|
||||||
# Wait for the listener (no dependency on nc)
|
# Wait for the listener (no dependency on nc)
|
||||||
@@ -103,8 +162,8 @@ measure() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
json=$(bombardier -c "$CONNECTIONS" -d "$DURATION" -o json -p r \
|
json=$(bombardier -c "$CONNECTIONS" -d "$DURATION" -t "$TIMEOUT" -o json -p r $INSECURE \
|
||||||
"http://127.0.0.1:$PORT/" 2>/dev/null)
|
"$SCHEME://127.0.0.1:$PORT$REQ_PATH" 2>/dev/null)
|
||||||
rc=$?
|
rc=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,139 @@
|
|||||||
#include "httplib.h"
|
#include "httplib.h"
|
||||||
|
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace httplib;
|
using namespace httplib;
|
||||||
|
|
||||||
int main() {
|
namespace {
|
||||||
Server svr;
|
|
||||||
|
|
||||||
|
// The workloads differ in which part of the write path they exercise:
|
||||||
|
//
|
||||||
|
// / small body set through set_content(); the response line,
|
||||||
|
// the headers and the body already share a single write()
|
||||||
|
// /large large body set through set_content(); the body is copied
|
||||||
|
// into the header buffer before that single write().
|
||||||
|
// --large-mib sets its size (and large.bin's).
|
||||||
|
// /static/small.js small file served from a mount point, where the headers
|
||||||
|
// and the body are two separate writes
|
||||||
|
// /static/large.bin same, with the body large enough to dominate
|
||||||
|
//
|
||||||
|
// Bodies are generated at startup so the repository carries no fixtures.
|
||||||
|
|
||||||
|
const size_t SMALL_SIZE = 1024;
|
||||||
|
const size_t LARGE_SIZE_DEFAULT_MIB = 1;
|
||||||
|
|
||||||
|
std::string filler(size_t n) {
|
||||||
|
std::string s;
|
||||||
|
s.reserve(n);
|
||||||
|
while (s.size() < n) {
|
||||||
|
s += "0123456789abcdef";
|
||||||
|
}
|
||||||
|
s.resize(n);
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_file(const std::string &path, const std::string &content) {
|
||||||
|
std::ofstream f(path.c_str(), std::ios::binary);
|
||||||
|
f.write(content.data(), static_cast<std::streamsize>(content.size()));
|
||||||
|
return f.good();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string default_dir() {
|
||||||
|
const char *tmp = std::getenv("TMPDIR");
|
||||||
|
std::string base = tmp && *tmp ? tmp : "/tmp";
|
||||||
|
if (!base.empty() && base[base.size() - 1] == '/') {
|
||||||
|
base.erase(base.size() - 1);
|
||||||
|
}
|
||||||
|
return base + "/cpp-httplib-bench";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool make_dir(const std::string &path) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
return _mkdir(path.c_str()) == 0 || errno == EEXIST;
|
||||||
|
#else
|
||||||
|
return ::mkdir(path.c_str(), 0755) == 0 || errno == EEXIST;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup(Server &svr, const std::string &large, const std::string &dir) {
|
||||||
svr.Get("/", [](const Request &, Response &res) {
|
svr.Get("/", [](const Request &, Response &res) {
|
||||||
res.set_content("Hello World!", "text/plain");
|
res.set_content("Hello World!", "text/plain");
|
||||||
});
|
});
|
||||||
|
|
||||||
svr.listen("0.0.0.0", 8080);
|
svr.Get("/large", [&large](const Request &, Response &res) {
|
||||||
|
res.set_content(large, "application/octet-stream");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.set_mount_point("/static", dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int port = 8080;
|
||||||
|
std::string dir = default_dir();
|
||||||
|
std::string cert;
|
||||||
|
std::string key;
|
||||||
|
size_t large_mib = LARGE_SIZE_DEFAULT_MIB;
|
||||||
|
|
||||||
|
for (int i = 1; i < argc; i++) {
|
||||||
|
auto last = i + 1 < argc;
|
||||||
|
if (!std::strcmp(argv[i], "--port") && last) {
|
||||||
|
port = std::atoi(argv[++i]);
|
||||||
|
} else if (!std::strcmp(argv[i], "--large-mib") && last) {
|
||||||
|
large_mib = static_cast<size_t>(std::atoi(argv[++i]));
|
||||||
|
} else if (!std::strcmp(argv[i], "--dir") && last) {
|
||||||
|
dir = argv[++i];
|
||||||
|
} else if (!std::strcmp(argv[i], "--cert") && last) {
|
||||||
|
cert = argv[++i];
|
||||||
|
} else if (!std::strcmp(argv[i], "--key") && last) {
|
||||||
|
key = argv[++i];
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"usage: %s [--port N] [--dir PATH] [--large-mib N]"
|
||||||
|
" [--cert PATH --key PATH]\n",
|
||||||
|
argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!make_dir(dir)) {
|
||||||
|
std::fprintf(stderr, "cannot create %s\n", dir.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto large = filler(large_mib * 1024 * 1024);
|
||||||
|
if (!write_file(dir + "/small.js", filler(SMALL_SIZE)) ||
|
||||||
|
!write_file(dir + "/large.bin", large)) {
|
||||||
|
std::fprintf(stderr, "cannot write fixtures under %s\n", dir.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cert.empty()) {
|
||||||
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||||
|
SSLServer svr(cert.c_str(), key.c_str());
|
||||||
|
if (!svr.is_valid()) {
|
||||||
|
std::fprintf(stderr, "cannot load %s / %s\n", cert.c_str(), key.c_str());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
setup(svr, large, dir);
|
||||||
|
svr.listen("0.0.0.0", port);
|
||||||
|
return 0;
|
||||||
|
#else
|
||||||
|
std::fprintf(stderr, "built without CPPHTTPLIB_OPENSSL_SUPPORT\n");
|
||||||
|
return 1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
Server svr;
|
||||||
|
setup(svr, large, dir);
|
||||||
|
svr.listen("0.0.0.0", port);
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-6
@@ -48,6 +48,23 @@ std::string get_error_time_format() {
|
|||||||
return ss.str();
|
return ss.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Escape a value for a log line the way NGINX does: '"', '\\', control
|
||||||
|
// bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
|
||||||
|
// (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
|
||||||
|
// writing them verbatim would let a client forge extra log lines.
|
||||||
|
std::string escape_log(const std::string &s) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(s.size());
|
||||||
|
for (unsigned char c : s) {
|
||||||
|
if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
|
||||||
|
out += std::format("\\x{:02X}", c);
|
||||||
|
} else {
|
||||||
|
out += static_cast<char>(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// NGINX Combined log format:
|
// NGINX Combined log format:
|
||||||
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||||
// "$http_referer" "$http_user_agent"
|
// "$http_referer" "$http_user_agent"
|
||||||
@@ -55,7 +72,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
std::string remote_user =
|
std::string remote_user =
|
||||||
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
||||||
auto time_local = get_time_format();
|
auto time_local = get_time_format();
|
||||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
// $request is the original request line, so log the raw target rather than
|
||||||
|
// the percent-decoded req.path.
|
||||||
|
auto request = std::format("{} {} {}", req.method, req.target, req.version);
|
||||||
auto status = res.status;
|
auto status = res.status;
|
||||||
auto body_bytes_sent = res.body.size();
|
auto body_bytes_sent = res.body.size();
|
||||||
auto http_referer = req.get_header_value("Referer");
|
auto http_referer = req.get_header_value("Referer");
|
||||||
@@ -64,9 +83,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
if (http_user_agent.empty()) http_user_agent = "-";
|
if (http_user_agent.empty()) http_user_agent = "-";
|
||||||
|
|
||||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||||
req.remote_addr, remote_user, time_local, request,
|
req.remote_addr, remote_user, time_local,
|
||||||
status, body_bytes_sent, http_referer,
|
escape_log(request), status, body_bytes_sent,
|
||||||
http_user_agent)
|
escape_log(http_referer), escape_log(http_user_agent))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +98,15 @@ void nginx_error_logger(const Error &err, const Request *req) {
|
|||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
auto request =
|
auto request =
|
||||||
std::format("{} {} {}", req->method, req->path, req->version);
|
std::format("{} {} {}", req->method, req->target, req->version);
|
||||||
auto host = req->get_header_value("Host");
|
auto host = req->get_header_value("Host");
|
||||||
if (host.empty()) host = "-";
|
if (host.empty()) host = "-";
|
||||||
|
|
||||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||||
"\"{}\", host: \"{}\"",
|
"\"{}\", host: \"{}\"",
|
||||||
time_local, level, to_string(err),
|
time_local, level, to_string(err),
|
||||||
req->remote_addr, request, host)
|
req->remote_addr, escape_log(request),
|
||||||
|
escape_log(host))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
} else {
|
} else {
|
||||||
// If no request context, just log the error
|
// If no request context, just log the error
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.52.0"
|
version = "0.60.1"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -48,3 +48,5 @@ cli.set_read_timeout(10s);
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use [C13. Set an overall timeout](../c13-max-timeout).
|
> **Warning:** The read timeout covers a single receive call — not the whole request. If data keeps trickling in during a large download, the request can take half an hour without ever hitting the timeout. To cap the total request time, use [C13. Set an overall timeout](../c13-max-timeout).
|
||||||
|
|
||||||
|
> For WebSocket client timeouts, see [W06. Set Timeouts](../w06-websocket-timeouts).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E01. Implement an SSE Server"
|
title: "E01. Implement an SSE Server"
|
||||||
order: 47
|
order: 48
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E02. Use Named Events in SSE"
|
title: "E02. Use Named Events in SSE"
|
||||||
order: 48
|
order: 49
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E03. Handle SSE Reconnection"
|
title: "E03. Handle SSE Reconnection"
|
||||||
order: 49
|
order: 50
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E04. Receive SSE on the Client"
|
title: "E04. Receive SSE on the Client"
|
||||||
order: 50
|
order: 51
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ A collection of recipes that answer "How do I...?" questions. Each recipe is sel
|
|||||||
- [S21. Configure the thread pool](s21-thread-pool)
|
- [S21. Configure the thread pool](s21-thread-pool)
|
||||||
- [S22. Talk over a Unix domain socket](s22-unix-socket)
|
- [S22. Talk over a Unix domain socket](s22-unix-socket)
|
||||||
|
|
||||||
|
### Protocol Extensions
|
||||||
|
- [S23. Handle custom HTTP methods](s23-custom-methods)
|
||||||
|
|
||||||
## TLS / Security
|
## TLS / Security
|
||||||
|
|
||||||
- [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends)
|
- [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](t01-tls-backends)
|
||||||
@@ -94,3 +97,5 @@ A collection of recipes that answer "How do I...?" questions. Each recipe is sel
|
|||||||
- [W02. Set a WebSocket heartbeat](w02-websocket-ping)
|
- [W02. Set a WebSocket heartbeat](w02-websocket-ping)
|
||||||
- [W03. Handle connection close](w03-websocket-close)
|
- [W03. Handle connection close](w03-websocket-close)
|
||||||
- [W04. Send and receive binary frames](w04-websocket-binary)
|
- [W04. Send and receive binary frames](w04-websocket-binary)
|
||||||
|
- [W05. Configure TLS for wss:// connections](w05-websocket-tls)
|
||||||
|
- [W06. Set timeouts](w06-websocket-timeouts)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ order: 20
|
|||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
With `httplib::Server`, you register a handler per HTTP method. Just pass a pattern and a lambda to `Get()`, `Post()`, `Put()`, or `Delete()`.
|
With `httplib::Server`, you register a handler per HTTP method. Just pass a pattern and a lambda to `Get()`, `Post()`, `Put()`, or `Delete()`. For methods outside the built-in set, such as WebDAV's `PROPFIND`, use `CustomRoute()`.
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
|
|
||||||
@@ -64,3 +64,5 @@ To add a response header, use `res.set_header("Name", "Value")`.
|
|||||||
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see [S18. Control startup order with `listen_after_bind`](../s18-listen-after-bind).
|
> **Note:** `listen()` is a blocking call. To run it on a different thread, wrap it in `std::thread`. If you need non-blocking startup, see [S18. Control startup order with `listen_after_bind`](../s18-listen-after-bind).
|
||||||
|
|
||||||
> To use path parameters like `/users/:id`, see [S03. Use path parameters](../s03-path-params).
|
> To use path parameters like `/users/:id`, see [S03. Use path parameters](../s03-path-params).
|
||||||
|
|
||||||
|
> For methods outside the built-in set, such as WebDAV's `PROPFIND`, see [S23. Handle custom HTTP methods](../s23-custom-methods).
|
||||||
|
|||||||
@@ -66,6 +66,38 @@ svr.Post("/upload",
|
|||||||
|
|
||||||
Only a small chunk sits in memory at any moment, so gigabyte-scale files are no problem.
|
Only a small chunk sits in memory at any moment, so gigabyte-scale files are no problem.
|
||||||
|
|
||||||
|
## Count the parts yourself
|
||||||
|
|
||||||
|
There is a cap on the number of parts, `CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT` (1024 by default), but it only applies to the buffered path, where every part is accumulated into `req.form`. The `ContentReader` keeps nothing on the library side, so the cap does not apply here.
|
||||||
|
|
||||||
|
If you want an upper bound, count the parts yourself and return `false` from the header callback. The parser stops right there.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.Post("/upload",
|
||||||
|
[](const httplib::Request &req, httplib::Response &res,
|
||||||
|
const httplib::ContentReader &content_reader) {
|
||||||
|
size_t count = 0;
|
||||||
|
|
||||||
|
auto ok = content_reader(
|
||||||
|
[&](const httplib::FormData &file) {
|
||||||
|
if (++count > 100) { return false; } // stop here
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[&](const char *data, size_t len) {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
res.status = httplib::StatusCode::BadRequest_400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
When `content_reader` returns `false`, set the response status yourself. The rest of the body is left unread and the connection is closed, so a client that is still sending sees the connection drop.
|
||||||
|
|
||||||
> **Warning:** When you use `HandlerWithContentReader`, `req.body` stays **empty**. Handle the body yourself inside the callbacks.
|
> **Warning:** When you use `HandlerWithContentReader`, `req.body` stays **empty**. Handle the body yourself inside the callbacks.
|
||||||
|
|
||||||
> For the client side of multipart uploads, see [C07. Upload a file as multipart form data](../c07-multipart-upload).
|
> For the client side of multipart uploads, see [C07. Upload a file as multipart form data](../c07-multipart-upload).
|
||||||
|
|||||||
@@ -48,6 +48,31 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Tiny responses barely benefit from compression and just waste CPU time. cpp-httplib skips compression for bodies that are too small to bother with.
|
## Static files need to be opted in
|
||||||
|
|
||||||
|
Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_static_file_compression(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
Only files within a size range are compressed, and both ends of it can be moved:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_static_file_compression_min_length(512);
|
||||||
|
svr.set_static_file_compression_max_length(1024 * 1024);
|
||||||
|
```
|
||||||
|
|
||||||
|
The lower bound defaults to 1400 bytes. A response that already fits in a single 1500-byte MTU is not delivered any faster for being smaller, and a file of a few bytes comes back larger than it went in, because gzip's header and trailer outweigh what deflate saves.
|
||||||
|
|
||||||
|
The upper bound defaults to 4MB and exists for a different reason: the file is compressed on every request, and the compressed bytes stay in memory until the response has been written, so the peak cost scales with the number of requests in flight. It bounds what a single request can cost, and says nothing about how well large files compress, so raising it is reasonable when the files are known and the traffic is not.
|
||||||
|
|
||||||
|
Either bound takes `0` to turn it off, and each has a compile-time default (`CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH`, `CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH`).
|
||||||
|
|
||||||
|
A compressed response keeps its `Content-Length`, so `HEAD` reports the same size a `GET` would. Two details to know: Range requests are answered from the uncompressed representation, and the `ETag` carries the coding it belongs to, as in `W/"...-gzip"`.
|
||||||
|
|
||||||
|
Content providers registered with `set_content_provider()` are not covered. Running one through a compressor holds each write back until the internal buffer fills, which stalls providers that build their body a piece at a time. To compress a generated body, use `set_chunked_content_provider()`.
|
||||||
|
|
||||||
|
> **Note:** The size range covers static files only. A body passed to `set_content()` is compressed whenever the client accepts it and the MIME type is compressible, however small it is, so a response of a few bytes ends up larger than it started. Decide in the handler if you want to avoid that.
|
||||||
|
|
||||||
> For the client-side counterpart, see [C15. Enable compression](../c15-compression).
|
> For the client-side counterpart, see [C15. Enable compression](../c15-compression).
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ svr.set_pre_request_handler(
|
|||||||
|
|
||||||
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
|
`matched_route` is the pattern **before** path parameters are expanded (e.g. `/admin/users/:id`). You compare against the route definition, not the actual request path, so IDs or names don't throw you off.
|
||||||
|
|
||||||
|
The pre-request handler also runs for routes registered with `svr.WebSocket()`. It is called before the `101 Switching Protocols` response, so returning `Handled` sends your HTTP response (such as a 403) and the connection is never upgraded.
|
||||||
|
|
||||||
## Return values
|
## Return values
|
||||||
|
|
||||||
Same as pre-routing — return `HandlerResponse`.
|
Same as pre-routing — return `HandlerResponse`.
|
||||||
|
|||||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
|||||||
|
|
||||||
## Check the return values
|
## Check the return values
|
||||||
|
|
||||||
`bind_to_port()` returns `false` on failure — typically when the port is already taken. Always check it.
|
`bind_to_port()` returns `false` on failure, for example when you don't have permission to bind to the port. Always check it.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
std::cerr << "port already in use" << std::endl;
|
std::cerr << "bind failed" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
`listen_after_bind()` blocks until the server stops and returns `true` on a clean shutdown.
|
||||||
|
|
||||||
|
## Detect a port that's already in use
|
||||||
|
|
||||||
|
With the default settings, you can actually bind to a port another server is already using. That's because cpp-httplib sets `SO_REUSEPORT` (Linux, macOS) or `SO_REUSEADDR` (Windows) on the server socket. A restarted server can bind again right away. The flip side is that a second server on the same port starts without an error, and connections get split between the two.
|
||||||
|
|
||||||
|
To make `bind_to_port()` fail on a port in use, replace the socket options with `set_socket_options()`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
|
std::cerr << "port already in use" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_socket_options()` replaces the defaults entirely. Setting `SO_REUSEADDR` on Linux and macOS keeps the "restarted server can bind again right away" behavior.
|
||||||
|
|
||||||
|
> **Note:** `SO_REUSEADDR` alone isn't enough on Windows. Two sockets that both set it can bind to the same port, so use `SO_EXCLUSIVEADDRUSE` instead.
|
||||||
|
|
||||||
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](../s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
> **Note:** To auto-pick a free port, see [S17. Bind to any available port](../s17-bind-any-port). Under the hood, that's just `bind_to_any_port()` + `listen_after_bind()`.
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "S23. Handle custom HTTP methods"
|
||||||
|
order: 42
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
The server rejects HTTP methods it does not know with `400 Bad Request`. To accept an extension method, such as the WebDAV methods of RFC 4918 (`PROPFIND`, `PROPPATCH`, `MKCOL` and friends) or UPnP's `SUBSCRIBE`, register a handler with `CustomRoute()`. Registering the handler is what makes the server accept the method.
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.CustomRoute("PROPFIND", "/dav/:id",
|
||||||
|
[](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
// The request body is available as usual
|
||||||
|
auto id = req.path_params.at("id");
|
||||||
|
res.status = httplib::StatusCode::MultiStatus_207;
|
||||||
|
res.set_content(build_multistatus(req.body), "application/xml");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Patterns work the same way as they do for `Get()`. Regular expressions and path parameters are both available.
|
||||||
|
|
||||||
|
## Advertise your methods with OPTIONS
|
||||||
|
|
||||||
|
A WebDAV client asks the server about its capabilities with `OPTIONS` before doing anything else. cpp-httplib generates neither the `DAV:` header nor `Allow`, so return them yourself. Forget this and clients will turn you away even though your `PROPFIND` works.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.Options("/dav/.*", [](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
res.set_header("DAV", "1");
|
||||||
|
res.set_header("Allow", "OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Read the body as a stream
|
||||||
|
|
||||||
|
There is a content reader overload, just like the one on `Post()`. Use it when you would rather not hold a large XML document in memory all at once.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.CustomRoute("REPORT", "/dav/.*",
|
||||||
|
[](const httplib::Request &req, httplib::Response &res,
|
||||||
|
const httplib::ContentReader &content_reader) {
|
||||||
|
content_reader([&](const char *data, size_t data_length) {
|
||||||
|
// Process it a chunk at a time
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
res.status = httplib::StatusCode::MultiStatus_207;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Things to keep in mind
|
||||||
|
|
||||||
|
- The method name has to be a valid HTTP method token (RFC 9110), and it must be registered before you call `listen()`
|
||||||
|
- `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`, `TRACE`, `PATCH` and `PRI` cannot be registered here. Use the dedicated methods for those
|
||||||
|
- A rejected registration makes `is_valid()` return `false` and `listen()` fail, so the server never starts holding a handler that would never run
|
||||||
|
- Static file serving and WebSocket upgrades stay `GET`/`HEAD` only
|
||||||
|
|
||||||
|
> **Note:** cpp-httplib takes you as far as routing the method. If you want to call it WebDAV, generating the `207 Multi-Status` XML, interpreting the `Depth` header and managing locks are all yours to implement. The protocol itself lives outside the library.
|
||||||
|
|
||||||
|
> For the basics of registering handlers, see [S01. Register GET / POST / PUT / DELETE handlers](../s01-handlers).
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL"
|
title: "T01. Choosing Between OpenSSL, mbedTLS, and wolfSSL"
|
||||||
order: 42
|
order: 43
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T02. Control SSL Certificate Verification"
|
title: "T02. Control SSL Certificate Verification"
|
||||||
order: 43
|
order: 44
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,3 +51,5 @@ On most Linux distributions, root certificates live in a single file like `/etc/
|
|||||||
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](../t01-tls-backends).
|
> The same APIs work on the mbedTLS and wolfSSL backends. For choosing between backends, see [T01. Choosing between OpenSSL, mbedTLS, and wolfSSL](../t01-tls-backends).
|
||||||
|
|
||||||
> For details on diagnosing failures, see [C18. Handle SSL errors](../c18-ssl-errors).
|
> For details on diagnosing failures, see [C18. Handle SSL errors](../c18-ssl-errors).
|
||||||
|
|
||||||
|
> For TLS configuration on a WebSocket client (`wss://`), see [W05. Configure TLS for wss:// Connections](../w05-websocket-tls).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T03. Start an SSL/TLS Server"
|
title: "T03. Start an SSL/TLS Server"
|
||||||
order: 44
|
order: 45
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T04. Configure mTLS"
|
title: "T04. Configure mTLS"
|
||||||
order: 45
|
order: 46
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,6 +57,22 @@ auto res = cli.Get("/");
|
|||||||
|
|
||||||
Note you're using `SSLClient` directly, not `Client`. If the private key has a password, pass it as the fifth argument.
|
Note you're using `SSLClient` directly, not `Client`. If the private key has a password, pass it as the fifth argument.
|
||||||
|
|
||||||
|
The client side has the same `PemMemory` struct too, letting you set the client certificate from PEM in memory.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::SSLClient::PemMemory pem{};
|
||||||
|
pem.cert_pem = client_cert.data();
|
||||||
|
pem.cert_pem_len = client_cert.size();
|
||||||
|
pem.key_pem = client_key.data();
|
||||||
|
pem.key_pem_len = client_key.size();
|
||||||
|
|
||||||
|
httplib::SSLClient cli("api.example.com", 443, pem);
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
```
|
||||||
|
|
||||||
|
> For mTLS with a WebSocket client (`wss://`), see [W05. Configure TLS for wss:// Connections](../w05-websocket-tls).
|
||||||
|
|
||||||
## Read client info from a handler
|
## Read client info from a handler
|
||||||
|
|
||||||
To see which client connected from inside a handler, use `req.peer_cert()`. Details in [T05. Access the peer certificate on the server](../t05-peer-cert).
|
To see which client connected from inside a handler, use `req.peer_cert()`. Details in [T05. Access the peer certificate on the server](../t05-peer-cert).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T05. Access the Peer Certificate on the Server Side"
|
title: "T05. Access the Peer Certificate on the Server Side"
|
||||||
order: 46
|
order: 47
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W01. Implement a WebSocket Echo Server and Client"
|
title: "W01. Implement a WebSocket Echo Server and Client"
|
||||||
order: 51
|
order: 52
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ The `read()` return value is a `ReadResult` enum:
|
|||||||
- `ReadResult::Text`: received a text message
|
- `ReadResult::Text`: received a text message
|
||||||
- `ReadResult::Binary`: received a binary message
|
- `ReadResult::Binary`: received a binary message
|
||||||
- `ReadResult::Fail`: error, or connection closed
|
- `ReadResult::Fail`: error, or connection closed
|
||||||
|
- `ReadResult::Timeout`: a read timeout you set with `set_read_timeout()` elapsed with nothing received; the connection is still open. The compile-time default timeout closes the connection and is reported as `Fail` instead — see [W06. Set Timeouts](../w06-websocket-timeouts)
|
||||||
|
|
||||||
## Client: talk to the echo server
|
## Client: talk to the echo server
|
||||||
|
|
||||||
@@ -85,4 +86,4 @@ svr.new_task_queue = [] {
|
|||||||
|
|
||||||
See [S21. Configure the thread pool](../s21-thread-pool).
|
See [S21. Configure the thread pool](../s21-thread-pool).
|
||||||
|
|
||||||
> **Note:** To run WebSocket over HTTPS, use `httplib::SSLServer` instead of `httplib::Server` — the same `WebSocket()` handler just works. On the client side, use a `wss://` URL.
|
> **Note:** To run WebSocket over HTTPS, use `httplib::SSLServer` instead of `httplib::Server` — the same `WebSocket()` handler just works. On the client side, use a `wss://` URL. For CA and client certificate configuration, see [W05. Configure TLS for wss:// Connections](../w05-websocket-tls).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W02. Set a WebSocket Heartbeat"
|
title: "W02. Set a WebSocket Heartbeat"
|
||||||
order: 52
|
order: 53
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -75,6 +75,6 @@ The counter is reset whenever `read()` consumes an incoming Pong frame, so this
|
|||||||
|
|
||||||
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
|
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
|
||||||
|
|
||||||
Even with `0`, a dead connection won't linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. Think of `max_missed_pongs` as the knob for detecting an unresponsive peer **faster** than that.
|
On the server side, even with `0`, a dead connection won't linger forever: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no backstop of its own — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side, it is also how you notice one **faster** than that 5-minute fallback.
|
||||||
|
|
||||||
> For handling a closed connection, see [W03. Handle connection close](../w03-websocket-close).
|
> For handling a closed connection, see [W03. Handle connection close](../w03-websocket-close).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W03. Handle Connection Close"
|
title: "W03. Handle Connection Close"
|
||||||
order: 53
|
order: 54
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W04. Send and Receive Binary Frames"
|
title: "W04. Send and Receive Binary Frames"
|
||||||
order: 54
|
order: 55
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,6 +42,9 @@ switch (result) {
|
|||||||
case httplib::ws::ReadResult::Fail:
|
case httplib::ws::ReadResult::Fail:
|
||||||
// error or closed
|
// error or closed
|
||||||
break;
|
break;
|
||||||
|
case httplib::ws::ReadResult::Timeout:
|
||||||
|
// read timeout elapsed; the connection is still open
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: "W05. Configure TLS for wss:// Connections"
|
||||||
|
order: 56
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
Client-side TLS configuration for `wss://` (WebSocket over TLS) connections uses almost the same API as `SSLClient`. `ws::WebSocketClient` handles both `ws://` and `wss://` through the same class, so there's no separate class to switch to the way `SSLClient` requires.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient ws1("ws://localhost:8080/ws"); // plaintext
|
||||||
|
httplib::ws::WebSocketClient ws2("wss://localhost:8443/ws"); // TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying the server certificate
|
||||||
|
|
||||||
|
Use `set_ca_cert_path()` to point at your own CA certificate. The signature matches `SSLClient`: the first argument is the CA certificate file, the second is an optional CA directory.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient ws("wss://internal.example.com/ws");
|
||||||
|
ws.set_ca_cert_path("/etc/ssl/certs/internal-ca.pem");
|
||||||
|
|
||||||
|
if (ws.connect()) {
|
||||||
|
ws.send("hello");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To disable certificate verification entirely, use `enable_server_certificate_verification(false)`. For details on that behavior, see [T02. Control SSL Certificate Verification](../t02-cert-verification).
|
||||||
|
|
||||||
|
## Presenting a client certificate (mTLS)
|
||||||
|
|
||||||
|
`ws::WebSocketClient` has a constructor overload that takes a `PemMemory` struct, letting `wss://` connections present a client certificate.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient::PemMemory pem{};
|
||||||
|
pem.cert_pem = client_cert.data();
|
||||||
|
pem.cert_pem_len = client_cert.size();
|
||||||
|
pem.key_pem = client_key.data();
|
||||||
|
pem.key_pem_len = client_key.size();
|
||||||
|
|
||||||
|
httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem);
|
||||||
|
|
||||||
|
if (ws.connect()) {
|
||||||
|
ws.send("hello");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Passing `PemMemory` to a `ws://` (non-TLS) URL is silently ignored. There's no constructor that reads the cert files directly, so unlike `SSLClient` you always load the PEM into memory yourself before passing it in.
|
||||||
|
|
||||||
|
For the full mTLS picture, including server-side setup and use cases, see [T04. Configure mTLS](../t04-mtls).
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
title: "W06. Set Timeouts"
|
||||||
|
order: 57
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
`ws::WebSocketClient` has the same three kinds of timeouts as `Client`, with the same meaning.
|
||||||
|
|
||||||
|
| Kind | API | Default |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Connection | `set_connection_timeout` | 300s |
|
||||||
|
| Read | `set_read_timeout` | none — waits forever (`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
|
||||||
|
| Write | `set_write_timeout` | 5s |
|
||||||
|
|
||||||
|
## Basic usage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
|
||||||
|
|
||||||
|
ws.set_connection_timeout(5, 0); // 5 seconds
|
||||||
|
ws.set_read_timeout(30, 0); // 30 seconds
|
||||||
|
ws.set_write_timeout(10, 0); // 10 seconds
|
||||||
|
|
||||||
|
if (ws.connect()) {
|
||||||
|
ws.send("hello");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the connection and write timeouts before calling `connect()`. The read timeout can be changed at any time — setting it on an open connection takes effect on the next `read()`.
|
||||||
|
|
||||||
|
## Use `std::chrono`
|
||||||
|
|
||||||
|
Just like `Client`, there's an overload that takes a `std::chrono` duration directly.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_connection_timeout(5s);
|
||||||
|
ws.set_read_timeout(30s);
|
||||||
|
ws.set_write_timeout(10s);
|
||||||
|
```
|
||||||
|
|
||||||
|
## What the read timeout means
|
||||||
|
|
||||||
|
`set_read_timeout()` applies to a single `read()` call. If no message arrives within that time, `read()` returns `ReadResult::Timeout`: **the connection is still open** and nothing was consumed, so you can send on it and read again. That is what separates it from `ReadResult::Fail`, which means the connection is gone.
|
||||||
|
|
||||||
|
This is what lets one thread own a connection in both directions:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_read_timeout(100ms);
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
flush_outgoing(ws); // nothing arrived — send whatever is queued
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without a read timeout, `read()` blocks until a message arrives, so the thread holding the connection never gets to its writes.
|
||||||
|
|
||||||
|
Two things to know about `Timeout`:
|
||||||
|
|
||||||
|
- It leaves `msg` untouched, and it is non-zero. So `while (ws.read(msg))` is not usable once a read timeout is set — the loop would keep running with the *previous* message still in `msg`.
|
||||||
|
- It is only reported on a message boundary. If the timeout elapses partway through a fragmented message, that message cannot be resumed and `read()` returns `Fail`.
|
||||||
|
|
||||||
|
For connections where long idle periods are normal — waiting on notifications, for example — either leave the read timeout unset, or treat `Timeout` as the no-op it is and keep looping.
|
||||||
|
|
||||||
|
## On the server side
|
||||||
|
|
||||||
|
A handler's `ws::WebSocket` has `set_read_timeout()` too, and the pattern above is how a handler relays between connections instead of parking in `read()`.
|
||||||
|
|
||||||
|
The server default is 300s (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`) rather than "forever": it is a backstop that reclaims a worker from a peer that has gone silent, since a WebSocket handler holds its worker for the life of the connection.
|
||||||
|
|
||||||
|
Because it is a backstop rather than something the handler asked for, it does not surface as `Timeout`. When it elapses, `read()` returns `Fail` and closes the connection, so a handler written as `while (ws.read(msg))` ends the way it always has. Only a timeout the handler set itself with `set_read_timeout()` comes back as `Timeout`.
|
||||||
|
|
||||||
|
> Unresponsive-peer detection via Ping/Pong is a separate mechanism. See [W02. Set a WebSocket Heartbeat](../w02-websocket-ping) for details.
|
||||||
|
|
||||||
|
## How this differs from `Client`
|
||||||
|
|
||||||
|
For `Client`'s timeout configuration, see [C12. Set Timeouts](../c12-timeouts). The behavior and API are nearly identical, but `WebSocketClient` has no equivalent to `set_max_timeout()` for capping the whole request — once connected, the connection stays open for as long as you keep calling `read()`.
|
||||||
@@ -107,6 +107,8 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
A check inside the handler runs after the handshake has completed. To refuse the connection with an HTTP status such as 401 before it is upgraded, use `set_pre_request_handler()` instead. It also runs for WebSocket routes. See [S11. Authenticate per route with a pre-request handler](../../cookbook/s11-pre-request).
|
||||||
|
|
||||||
## Using WSS
|
## Using WSS
|
||||||
|
|
||||||
WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`.
|
WebSocket over HTTPS (WSS) is also supported. On the server side, just register a WebSocket handler on `httplib::SSLServer`.
|
||||||
|
|||||||
@@ -48,3 +48,5 @@ cli.set_read_timeout(10s);
|
|||||||
```
|
```
|
||||||
|
|
||||||
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合は[C13. 全体タイムアウトを設定する](../c13-max-timeout)を使ってください。
|
> **Warning:** 読み取りタイムアウトは「1回の受信待ち」に対するタイムアウトです。大きなファイルのダウンロードで途中ずっとデータが流れている限り、リクエスト全体で30分かかっても発火しません。リクエスト全体の時間制限を設けたい場合は[C13. 全体タイムアウトを設定する](../c13-max-timeout)を使ってください。
|
||||||
|
|
||||||
|
> WebSocketクライアントのタイムアウト設定は[W06. タイムアウトを設定する](../w06-websocket-timeouts)を参照してください。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E01. SSEサーバーを実装する"
|
title: "E01. SSEサーバーを実装する"
|
||||||
order: 47
|
order: 48
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E02. SSEでイベント名を使い分ける"
|
title: "E02. SSEでイベント名を使い分ける"
|
||||||
order: 48
|
order: 49
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E03. SSEの再接続を処理する"
|
title: "E03. SSEの再接続を処理する"
|
||||||
order: 49
|
order: 50
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "E04. SSEをクライアントで受信する"
|
title: "E04. SSEをクライアントで受信する"
|
||||||
order: 50
|
order: 51
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ status: "draft"
|
|||||||
- [S21. マルチスレッド数を設定する](s21-thread-pool)
|
- [S21. マルチスレッド数を設定する](s21-thread-pool)
|
||||||
- [S22. Unix domain socketで通信する](s22-unix-socket)
|
- [S22. Unix domain socketで通信する](s22-unix-socket)
|
||||||
|
|
||||||
|
### プロトコル拡張
|
||||||
|
- [S23. カスタムHTTPメソッドを扱う](s23-custom-methods)
|
||||||
|
|
||||||
## TLS / セキュリティ
|
## TLS / セキュリティ
|
||||||
|
|
||||||
- [T01. OpenSSL・mbedTLS・wolfSSLの選択指針](t01-tls-backends)
|
- [T01. OpenSSL・mbedTLS・wolfSSLの選択指針](t01-tls-backends)
|
||||||
@@ -94,3 +97,5 @@ status: "draft"
|
|||||||
- [W02. ハートビートを設定する](w02-websocket-ping)
|
- [W02. ハートビートを設定する](w02-websocket-ping)
|
||||||
- [W03. 接続クローズをハンドリングする](w03-websocket-close)
|
- [W03. 接続クローズをハンドリングする](w03-websocket-close)
|
||||||
- [W04. バイナリフレームを送受信する](w04-websocket-binary)
|
- [W04. バイナリフレームを送受信する](w04-websocket-binary)
|
||||||
|
- [W05. wss接続でTLSを設定する](w05-websocket-tls)
|
||||||
|
- [W06. タイムアウトを設定する](w06-websocket-timeouts)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ order: 20
|
|||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
`httplib::Server`では、HTTPメソッドごとにハンドラを登録します。`Get()`、`Post()`、`Put()`、`Delete()`の各メソッドにパターンとラムダを渡すだけです。
|
`httplib::Server`では、HTTPメソッドごとにハンドラを登録します。`Get()`、`Post()`、`Put()`、`Delete()`の各メソッドにパターンとラムダを渡すだけです。WebDAVの`PROPFIND`のような組み込み以外のメソッドを扱いたいときは、`CustomRoute()`を使います。
|
||||||
|
|
||||||
## 基本の使い方
|
## 基本の使い方
|
||||||
|
|
||||||
@@ -64,3 +64,5 @@ svr.Get("/me", [](const httplib::Request &req, httplib::Response &res) {
|
|||||||
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要なら[S18. `listen_after_bind`で起動順序を制御する](../s18-listen-after-bind)を参照してください。
|
> **Note:** `listen()`はブロックする関数です。別スレッドで動かしたいときは`std::thread`で包むか、ノンブロッキング起動が必要なら[S18. `listen_after_bind`で起動順序を制御する](../s18-listen-after-bind)を参照してください。
|
||||||
|
|
||||||
> パスパラメーター(`/users/:id`)を使いたい場合は[S03. パスパラメーターを使う](../s03-path-params)を参照してください。
|
> パスパラメーター(`/users/:id`)を使いたい場合は[S03. パスパラメーターを使う](../s03-path-params)を参照してください。
|
||||||
|
|
||||||
|
> WebDAVの`PROPFIND`のような組み込み以外のメソッドは[S23. カスタムHTTPメソッドを扱う](../s23-custom-methods)を参照してください。
|
||||||
|
|||||||
@@ -66,6 +66,38 @@ svr.Post("/upload",
|
|||||||
|
|
||||||
メモリには常に小さなチャンクしか載らないので、ギガバイト級のファイルでも扱えます。
|
メモリには常に小さなチャンクしか載らないので、ギガバイト級のファイルでも扱えます。
|
||||||
|
|
||||||
|
## パート数は自分で数える
|
||||||
|
|
||||||
|
`CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT`(デフォルト1024)というパート数の上限がありますが、これが効くのは`req.form`にすべてのパートを溜め込むバッファリング側だけです。`ContentReader`はライブラリ側で何も溜め込まないので、この上限は適用されません。
|
||||||
|
|
||||||
|
パート数に上限をつけたいときは、自分で数えてヘッダーのコールバックから`false`を返してください。パースはその場で止まります。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.Post("/upload",
|
||||||
|
[](const httplib::Request &req, httplib::Response &res,
|
||||||
|
const httplib::ContentReader &content_reader) {
|
||||||
|
size_t count = 0;
|
||||||
|
|
||||||
|
auto ok = content_reader(
|
||||||
|
[&](const httplib::FormData &file) {
|
||||||
|
if (++count > 100) { return false; } // ここで打ち切る
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[&](const char *data, size_t len) {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
res.status = httplib::StatusCode::BadRequest_400;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
`content_reader`が`false`を返したら、レスポンスのステータスは自分でセットしてください。ボディの残りは読まずに接続を閉じるので、送信中のクライアントには接続が切れたように見えます。
|
||||||
|
|
||||||
> **Warning:** `HandlerWithContentReader`を使うと、`req.body`は**空のまま**です。ボディはコールバック内で自分で処理してください。
|
> **Warning:** `HandlerWithContentReader`を使うと、`req.body`は**空のまま**です。ボディはコールバック内で自分で処理してください。
|
||||||
|
|
||||||
> クライアント側でマルチパートを送る方法は[C07. ファイルをマルチパートフォームとしてアップロードする](../c07-multipart-upload)を参照してください。
|
> クライアント側でマルチパートを送る方法は[C07. ファイルをマルチパートフォームとしてアップロードする](../c07-multipart-upload)を参照してください。
|
||||||
|
|||||||
@@ -48,6 +48,31 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** 小さなレスポンスは圧縮しても効果が薄く、むしろCPU時間を無駄にすることがあります。cpp-httplibは小さすぎるボディは圧縮をスキップします。
|
## 静的ファイルは明示的に有効にする
|
||||||
|
|
||||||
|
`set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_static_file_compression(true);
|
||||||
|
```
|
||||||
|
|
||||||
|
圧縮の対象になるのは一定のサイズ範囲に収まるファイルだけで、上下どちらの境界も変更できます。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_static_file_compression_min_length(512);
|
||||||
|
svr.set_static_file_compression_max_length(1024 * 1024);
|
||||||
|
```
|
||||||
|
|
||||||
|
下限のデフォルトは1400バイトです。1500バイトのMTUに収まるレスポンスは、小さくしたところで到達が速くなるわけではありません。さらに数バイトのファイルは、gzipのヘッダとトレーラがdeflateの削減分を上回るため、かえって大きくなって返ります。
|
||||||
|
|
||||||
|
上限のデフォルトは4MBで、こちらは理由が違います。リクエストのたびに圧縮が走り、圧縮後のバイト列はレスポンスを書き終えるまでメモリに載るため、ピーク時のコストが同時処理中のリクエスト数に比例するからです。つまり1リクエストあたりのコストを抑えるための値であって、大きいファイルは圧縮しても無駄だという意味ではありません。配信するファイルが分かっていてトラフィックがそれほど多くないなら、引き上げて構いません。
|
||||||
|
|
||||||
|
どちらの境界も`0`で無効にできます。コンパイル時のデフォルトは`CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH`と`CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH`で決まります。
|
||||||
|
|
||||||
|
圧縮しても`Content-Length`は付いたままなので、`HEAD`は`GET`と同じサイズを返します。細かい挙動として、Rangeリクエストは非圧縮の表現から切り出して返し、`ETag`には`W/"...-gzip"`のように使われた圧縮方式が入ります。
|
||||||
|
|
||||||
|
なお`set_content_provider()`で登録したコンテンツプロバイダは対象外です。圧縮器を通すと、内部バッファが埋まるまで書き込みが送出されず、ボディを少しずつ生成するプロバイダが止まってしまうためです。生成したボディを圧縮したい場合は`set_chunked_content_provider()`を使ってください。
|
||||||
|
|
||||||
|
> **Note:** サイズ範囲が効くのは静的ファイルだけです。`set_content()`に渡したボディは、圧縮対象のMIMEタイプでクライアントが受け入れていれば、大きさによらず圧縮されます。数バイトのレスポンスはgzipのヘッダ分だけかえって大きくなるので、避けたい場合はハンドラ側で判断してください。
|
||||||
|
|
||||||
> クライアント側の挙動は[C15. 圧縮を有効にする](../c15-compression)を参照してください。
|
> クライアント側の挙動は[C15. 圧縮を有効にする](../c15-compression)を参照してください。
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ svr.set_pre_request_handler(
|
|||||||
|
|
||||||
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`)です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
|
`matched_route`はパスパラメーターを展開する**前**のパターン文字列(例: `/admin/users/:id`)です。特定の値ではなく、ルート定義のパターンで判定できるので、IDや名前に左右されません。
|
||||||
|
|
||||||
|
`svr.WebSocket()`で登録したルートでも、Pre-requestハンドラは呼ばれます。呼ばれるのは`101 Switching Protocols`を返す前なので、`Handled`を返すとそのHTTPレスポンス(403など)がそのまま返り、WebSocketへのアップグレードは行われません。
|
||||||
|
|
||||||
## 戻り値の意味
|
## 戻り値の意味
|
||||||
|
|
||||||
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
Pre-routingハンドラと同じく、`HandlerResponse`を返します。
|
||||||
|
|||||||
@@ -43,15 +43,40 @@ svr.listen_after_bind();
|
|||||||
|
|
||||||
## 戻り値のチェック
|
## 戻り値のチェック
|
||||||
|
|
||||||
`bind_to_port()`は失敗すると`false`を返します。ポートが既に使われている場合などです。必ずチェックしてください。
|
`bind_to_port()`は失敗すると`false`を返します。ポートにbindする権限が無い場合などです。必ずチェックしてください。
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
std::cerr << "port already in use" << std::endl;
|
std::cerr << "bind failed" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
`listen_after_bind()`はサーバーが停止するまでブロックし、正常終了なら`true`を返します。
|
||||||
|
|
||||||
|
## 使用中のポートを検出する
|
||||||
|
|
||||||
|
実は、デフォルトの設定では、ほかのサーバーが使っているポートにもbindできてしまいます。cpp-httplibがサーバーソケットに`SO_REUSEPORT`(Linux、macOS)か`SO_REUSEADDR`(Windows)を設定しているからです。再起動したサーバーはすぐにbindし直せます。その代わり、同じポートで2つ目のサーバーを起動してもエラーにならず、接続が両方に振り分けられます。
|
||||||
|
|
||||||
|
使用中のポートで`bind_to_port()`を失敗させたいときは、`set_socket_options()`でソケットオプションを差し替えてください。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_socket_options([](socket_t sock) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, 1);
|
||||||
|
#else
|
||||||
|
httplib::set_socket_opt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
|
||||||
|
#endif
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!svr.bind_to_port("0.0.0.0", 8080)) {
|
||||||
|
std::cerr << "port already in use" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`set_socket_options()`はデフォルトの設定を丸ごと置き換えます。Linux、macOSで`SO_REUSEADDR`を設定しているのは、再起動したサーバーがすぐにbindし直せるようにするためです。
|
||||||
|
|
||||||
|
> **Note:** Windowsでは`SO_REUSEADDR`だけでは足りません。お互いに`SO_REUSEADDR`を設定したソケット同士は、同じポートにbindできてしまいます。`SO_EXCLUSIVEADDRUSE`を使ってください。
|
||||||
|
|
||||||
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](../s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
> **Note:** 空いているポートを自動で選びたいときは[S17. ポートを動的に割り当てる](../s17-bind-any-port)を参照してください。こちらも内部では`bind_to_any_port()` + `listen_after_bind()`の組み合わせです。
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
title: "S23. カスタムHTTPメソッドを扱う"
|
||||||
|
order: 42
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
サーバーは知らないHTTPメソッドを`400 Bad Request`で弾きます。RFC 4918のWebDAVメソッド(`PROPFIND`、`PROPPATCH`、`MKCOL`など)やUPnPの`SUBSCRIBE`のような拡張メソッドを受け付けたいときは、`CustomRoute()`でハンドラを登録してください。登録したことがそのまま「このメソッドを受け付ける」という意味になります。
|
||||||
|
|
||||||
|
## 基本の使い方
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.CustomRoute("PROPFIND", "/dav/:id",
|
||||||
|
[](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
// リクエストボディも通常どおり読める
|
||||||
|
auto id = req.path_params.at("id");
|
||||||
|
res.status = httplib::StatusCode::MultiStatus_207;
|
||||||
|
res.set_content(build_multistatus(req.body), "application/xml");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
パターンの書き方は`Get()`などと同じです。正規表現もパスパラメーターもそのまま使えます。
|
||||||
|
|
||||||
|
## OPTIONSで対応メソッドを知らせる
|
||||||
|
|
||||||
|
WebDAVクライアントは接続すると、まず`OPTIONS`でサーバーの能力を問い合わせます。cpp-httplibは`DAV:`ヘッダーも`Allow`ヘッダーも自動生成しないので、自分で返してください。ここを忘れると、`PROPFIND`が正しく動いてもクライアントに拒否されます。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.Options("/dav/.*", [](const httplib::Request &req, httplib::Response &res) {
|
||||||
|
res.set_header("DAV", "1");
|
||||||
|
res.set_header("Allow", "OPTIONS, GET, HEAD, PROPFIND, PROPPATCH, MKCOL");
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## ボディをストリーミングで受け取る
|
||||||
|
|
||||||
|
`Post()`などと同じく、Content Reader版のオーバーロードがあります。大きなXMLを一度にメモリへ載せたくないときに使ってください。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.CustomRoute("REPORT", "/dav/.*",
|
||||||
|
[](const httplib::Request &req, httplib::Response &res,
|
||||||
|
const httplib::ContentReader &content_reader) {
|
||||||
|
content_reader([&](const char *data, size_t data_length) {
|
||||||
|
// 少しずつ処理する
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
res.status = httplib::StatusCode::MultiStatus_207;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 覚えておくこと
|
||||||
|
|
||||||
|
- メソッド名はHTTPのトークン(RFC 9110)である必要があります。`listen()`より前に登録してください
|
||||||
|
- `GET`、`HEAD`、`POST`、`PUT`、`DELETE`、`CONNECT`、`OPTIONS`、`TRACE`、`PATCH`、`PRI`は登録できません。これらには専用のメソッドを使ってください
|
||||||
|
- 登録が拒否されると`is_valid()`が`false`になり、`listen()`が失敗します。呼ばれないハンドラを抱えたままサーバーが起動することはありません
|
||||||
|
- 静的ファイルの配信とWebSocketのアップグレードは`GET`/`HEAD`のままです
|
||||||
|
|
||||||
|
> **Note:** cpp-httplibが用意するのはメソッドのルーティングまでです。WebDAVを名乗るなら、`207 Multi-Status`のXML生成、`Depth`ヘッダーの解釈、ロックの管理は自分で実装することになります。プロトコルの本体はライブラリの外側です。
|
||||||
|
|
||||||
|
> ハンドラ登録の基本は[S01. GET / POST / PUT / DELETEハンドラを登録する](../s01-handlers)を参照してください。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T01. OpenSSL・mbedTLS・wolfSSLの選択指針"
|
title: "T01. OpenSSL・mbedTLS・wolfSSLの選択指針"
|
||||||
order: 42
|
order: 43
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T02. SSL証明書の検証を制御する"
|
title: "T02. SSL証明書の検証を制御する"
|
||||||
order: 43
|
order: 44
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -51,3 +51,5 @@ cli.enable_server_hostname_verification(false);
|
|||||||
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方は[T01. OpenSSL・mbedTLS・wolfSSLの選択指針](../t01-tls-backends)を参照してください。
|
> mbedTLSやwolfSSLバックエンドでも同じAPIが使えます。バックエンドの選び方は[T01. OpenSSL・mbedTLS・wolfSSLの選択指針](../t01-tls-backends)を参照してください。
|
||||||
|
|
||||||
> 失敗したときの詳細を調べる方法は[C18. SSLエラーをハンドリングする](../c18-ssl-errors)を参照してください。
|
> 失敗したときの詳細を調べる方法は[C18. SSLエラーをハンドリングする](../c18-ssl-errors)を参照してください。
|
||||||
|
|
||||||
|
> WebSocketクライアント(`wss://`)のTLS設定は[W05. wss接続でTLSを設定する](../w05-websocket-tls)を参照してください。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T03. SSL/TLSサーバーを立ち上げる"
|
title: "T03. SSL/TLSサーバーを立ち上げる"
|
||||||
order: 44
|
order: 45
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T04. mTLSを設定する"
|
title: "T04. mTLSを設定する"
|
||||||
order: 45
|
order: 46
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -57,6 +57,22 @@ auto res = cli.Get("/");
|
|||||||
|
|
||||||
`Client`ではなく`SSLClient`を直接使う点に注意してください。秘密鍵にパスワードがある場合は第5引数で渡せます。
|
`Client`ではなく`SSLClient`を直接使う点に注意してください。秘密鍵にパスワードがある場合は第5引数で渡せます。
|
||||||
|
|
||||||
|
クライアント側にも同じ`PemMemory`構造体があり、メモリ上のPEMからクライアント証明書を設定できます。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::SSLClient::PemMemory pem{};
|
||||||
|
pem.cert_pem = client_cert.data();
|
||||||
|
pem.cert_pem_len = client_cert.size();
|
||||||
|
pem.key_pem = client_key.data();
|
||||||
|
pem.key_pem_len = client_key.size();
|
||||||
|
|
||||||
|
httplib::SSLClient cli("api.example.com", 443, pem);
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
```
|
||||||
|
|
||||||
|
> WebSocketクライアント(`wss://`)でmTLSを使う場合は[W05. wss接続でTLSを設定する](../w05-websocket-tls)を参照してください。
|
||||||
|
|
||||||
## ハンドラからクライアント情報を取得する
|
## ハンドラからクライアント情報を取得する
|
||||||
|
|
||||||
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくは[T05. サーバー側でピア証明書を参照する](../t05-peer-cert)を参照してください。
|
ハンドラの中で、どのクライアントが接続してきたかを確認したいときは`req.peer_cert()`を使います。詳しくは[T05. サーバー側でピア証明書を参照する](../t05-peer-cert)を参照してください。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "T05. サーバー側でピア証明書を参照する"
|
title: "T05. サーバー側でピア証明書を参照する"
|
||||||
order: 46
|
order: 47
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W01. WebSocketエコーサーバー/クライアントを実装する"
|
title: "W01. WebSocketエコーサーバー/クライアントを実装する"
|
||||||
order: 51
|
order: 52
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -31,11 +31,12 @@ int main() {
|
|||||||
|
|
||||||
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
||||||
|
|
||||||
`read()`の返り値は`ReadResult`列挙値で、次の3種類です。
|
`read()`の返り値は`ReadResult`列挙値で、次の4種類です。
|
||||||
|
|
||||||
- `ReadResult::Text`: テキストメッセージを受信
|
- `ReadResult::Text`: テキストメッセージを受信
|
||||||
- `ReadResult::Binary`: バイナリメッセージを受信
|
- `ReadResult::Binary`: バイナリメッセージを受信
|
||||||
- `ReadResult::Fail`: エラー、または接続が閉じた
|
- `ReadResult::Fail`: エラー、または接続が閉じた
|
||||||
|
- `ReadResult::Timeout`: `set_read_timeout()`で自分が設定した読み取りタイムアウトが、何も受信しないまま経過した。接続は開いたまま。コンパイル時のデフォルトのタイムアウトは接続を閉じ、`Fail`として返る([W06. タイムアウトを設定する](../w06-websocket-timeouts)を参照)
|
||||||
|
|
||||||
## クライアント: エコーを叩く
|
## クライアント: エコーを叩く
|
||||||
|
|
||||||
@@ -85,4 +86,4 @@ svr.new_task_queue = [] {
|
|||||||
|
|
||||||
詳細は[S21. マルチスレッド数を設定する](../s21-thread-pool)を参照してください。
|
詳細は[S21. マルチスレッド数を設定する](../s21-thread-pool)を参照してください。
|
||||||
|
|
||||||
> **Note:** HTTPSサーバーの上でWebSocketを動かしたいときは、`httplib::Server`の代わりに`httplib::SSLServer`を使えば、同じ`WebSocket()`ハンドラがそのまま動きます。クライアント側は`wss://`スキームを指定するだけです。
|
> **Note:** HTTPSサーバーの上でWebSocketを動かしたいときは、`httplib::Server`の代わりに`httplib::SSLServer`を使えば、同じ`WebSocket()`ハンドラがそのまま動きます。クライアント側は`wss://`スキームを指定するだけです。CA証明書やクライアント証明書の設定は[W05. wss接続でTLSを設定する](../w05-websocket-tls)を参照してください。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W02. ハートビートを設定する"
|
title: "W02. ハートビートを設定する"
|
||||||
order: 52
|
order: 53
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -75,6 +75,6 @@ cli.set_websocket_max_missed_pongs(2); // 2回連続でPongが返ってこなけ
|
|||||||
|
|
||||||
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
||||||
|
|
||||||
ただし`0`のままでも最終的に接続が残り続けることはありません。`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働き、フレームが一定時間来なければ`read()`が失敗します。つまり`max_missed_pongs`は「**もっと速く**無応答を検出したい」ときに使うオプションだと考えてください。
|
サーバ側は`0`のままでも接続が残り続けることはありません。ハンドラが`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働きます。一方クライアント側にはこの保険がなく、読み取りタイムアウトを設定しない限り無期限に待つので、無応答ピアを検出する手段は`max_missed_pongs`だけです。どちらの側でも「**もっと速く**検出したい」ときに使うオプションでもあります。
|
||||||
|
|
||||||
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](../w03-websocket-close)を参照してください。
|
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](../w03-websocket-close)を参照してください。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W03. 接続クローズをハンドリングする"
|
title: "W03. 接続クローズをハンドリングする"
|
||||||
order: 53
|
order: 54
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
title: "W04. バイナリフレームを送受信する"
|
title: "W04. バイナリフレームを送受信する"
|
||||||
order: 54
|
order: 55
|
||||||
status: "draft"
|
status: "draft"
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -42,6 +42,9 @@ switch (result) {
|
|||||||
case httplib::ws::ReadResult::Fail:
|
case httplib::ws::ReadResult::Fail:
|
||||||
// エラーまたは切断
|
// エラーまたは切断
|
||||||
break;
|
break;
|
||||||
|
case httplib::ws::ReadResult::Timeout:
|
||||||
|
// 読み取りタイムアウト。接続は開いたまま
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
title: "W05. wss接続でTLSを設定する"
|
||||||
|
order: 56
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
`wss://`(WebSocket over TLS)接続のクライアント側TLS設定は、`SSLClient`とほぼ同じAPIです。`ws::WebSocketClient`は`ws://`と`wss://`を同じクラスで扱うので、`SSLClient`のような別クラスへの切り替えは不要です。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient ws1("ws://localhost:8080/ws"); // 平文
|
||||||
|
httplib::ws::WebSocketClient ws2("wss://localhost:8443/ws"); // TLS
|
||||||
|
```
|
||||||
|
|
||||||
|
## サーバー証明書の検証
|
||||||
|
|
||||||
|
`set_ca_cert_path()`で独自のCA証明書を指定できます。シグネチャは`SSLClient`と同じで、第1引数がCA証明書ファイル、第2引数がCA証明書ディレクトリ(省略可)です。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient ws("wss://internal.example.com/ws");
|
||||||
|
ws.set_ca_cert_path("/etc/ssl/certs/internal-ca.pem");
|
||||||
|
|
||||||
|
if (ws.connect()) {
|
||||||
|
ws.send("hello");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
証明書検証そのものを無効にしたい場合は`enable_server_certificate_verification(false)`が使えます。挙動の詳細は[T02. SSL証明書の検証を制御する](../t02-cert-verification)を参照してください。
|
||||||
|
|
||||||
|
## クライアント証明書を使う(mTLS)
|
||||||
|
|
||||||
|
`ws::WebSocketClient`には`PemMemory`構造体を受け取るコンストラクタがあり、`wss://`接続でクライアント証明書を提示できます。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient::PemMemory pem{};
|
||||||
|
pem.cert_pem = client_cert.data();
|
||||||
|
pem.cert_pem_len = client_cert.size();
|
||||||
|
pem.key_pem = client_key.data();
|
||||||
|
pem.key_pem_len = client_key.size();
|
||||||
|
|
||||||
|
httplib::ws::WebSocketClient ws("wss://api.example.com/ws", pem);
|
||||||
|
|
||||||
|
if (ws.connect()) {
|
||||||
|
ws.send("hello");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`ws://`(非TLS)のURLに`PemMemory`を渡した場合は黙って無視されます。`SSLClient`と違い、ファイルパスから直接読み込むコンストラクタは用意されていないので、PEMをメモリ上に読み込んでから渡す必要があります。
|
||||||
|
|
||||||
|
mTLSの全体像(サーバー側の設定や用途の解説を含む)は[T04. mTLSを設定する](../t04-mtls)を参照してください。
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
title: "W06. タイムアウトを設定する"
|
||||||
|
order: 57
|
||||||
|
status: "draft"
|
||||||
|
---
|
||||||
|
|
||||||
|
`ws::WebSocketClient`には`Client`と同じ3種類のタイムアウトがあり、意味も同じです。
|
||||||
|
|
||||||
|
| 種類 | API | デフォルト |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 接続タイムアウト | `set_connection_timeout` | 300秒 |
|
||||||
|
| 読み取りタイムアウト | `set_read_timeout` | なし。無期限に待つ(`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
|
||||||
|
| 書き込みタイムアウト | `set_write_timeout` | 5秒 |
|
||||||
|
|
||||||
|
## 基本の使い方
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
httplib::ws::WebSocketClient ws("ws://localhost:8080/ws");
|
||||||
|
|
||||||
|
ws.set_connection_timeout(5, 0); // 5秒
|
||||||
|
ws.set_read_timeout(30, 0); // 30秒
|
||||||
|
ws.set_write_timeout(10, 0); // 10秒
|
||||||
|
|
||||||
|
if (ws.connect()) {
|
||||||
|
ws.send("hello");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接続タイムアウトと書き込みタイムアウトは`connect()`を呼ぶ前に設定してください。読み取りタイムアウトはいつでも変更でき、接続済みの状態で設定した場合は次の`read()`から効きます。
|
||||||
|
|
||||||
|
## `std::chrono`で指定する
|
||||||
|
|
||||||
|
`Client`と同じく、`std::chrono`の期間を直接渡すオーバーロードもあります。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_connection_timeout(5s);
|
||||||
|
ws.set_read_timeout(30s);
|
||||||
|
ws.set_write_timeout(10s);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 読み取りタイムアウトの意味
|
||||||
|
|
||||||
|
`set_read_timeout()`は「1回の`read()`呼び出し」に対するタイムアウトです。メッセージが届かないまま指定時間が経過すると`read()`は`ReadResult::Timeout`を返します。このとき**接続は開いたまま**で、1バイトも読み進めていないので、そのまま送信して読み直せます。接続が失われたことを意味する`ReadResult::Fail`とはここが違います。
|
||||||
|
|
||||||
|
1本の接続を1つのスレッドで双方向に扱えるのはこのためです。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_read_timeout(100ms);
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
flush_outgoing(ws); // 何も届いていない。溜まっている分を送る
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
読み取りタイムアウトを設定しないと`read()`はメッセージが届くまで戻らないので、接続を持っているスレッドは送信に手が回りません。
|
||||||
|
|
||||||
|
`Timeout`について2点あります。
|
||||||
|
|
||||||
|
- `msg`は書き換えられません。値も0以外なので、読み取りタイムアウトを設定した状態で`while (ws.read(msg))`と書くと、**前回のメッセージ**が`msg`に残ったままループが回り続けます。
|
||||||
|
- 報告されるのはメッセージの境界だけです。分割されたメッセージの途中でタイムアウトした場合、そのメッセージは再開できないので`read()`は`Fail`を返します。
|
||||||
|
|
||||||
|
通知の待受のように長時間メッセージが来ないことが正常な接続では、読み取りタイムアウトを設定しないままにするか、`Timeout`を「まだ何も来ていない」印として扱ってループを続けてください。
|
||||||
|
|
||||||
|
## サーバ側
|
||||||
|
|
||||||
|
ハンドラが受け取る`ws::WebSocket`にも`set_read_timeout()`があります。ハンドラが`read()`で止まったままにならないので、上と同じ書き方で複数の接続の間をメッセージが中継できます。
|
||||||
|
|
||||||
|
サーバ側のデフォルトは「無期限」ではなく300秒(`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`)です。WebSocketのハンドラは接続が続く限りワーカーを1つ占有するので、無言になったピアからワーカーを回収する保険として働きます。
|
||||||
|
|
||||||
|
この保険はハンドラが求めたタイムアウトではないので、`Timeout`としては返りません。経過すると`read()`は`Fail`を返して接続を閉じるため、`while (ws.read(msg))`と書いたハンドラは従来どおりそこで終わります。`Timeout`が返るのは、ハンドラ自身が`set_read_timeout()`で設定したタイムアウトだけです。
|
||||||
|
|
||||||
|
> Ping/Pongによる無応答ピア検出は別の仕組みです。詳しくは[W02. ハートビートを設定する](../w02-websocket-ping)を参照してください。
|
||||||
|
|
||||||
|
## `Client`との違い
|
||||||
|
|
||||||
|
`Client`のタイムアウト設定については[C12. タイムアウトを設定する](../c12-timeouts)を参照してください。挙動とAPIはほぼ同じですが、`WebSocketClient`には`set_max_timeout()`に相当するリクエスト全体のタイムアウトはありません。接続を確立したあとは、`read()`のループを回し続ける限り接続が維持されます。
|
||||||
@@ -107,6 +107,8 @@ svr.WebSocket("/ws", [](const httplib::Request &req, httplib::ws::WebSocket &ws)
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ハンドラー内のチェックは、ハンドシェイクが完了した後に行われます。アップグレードする前に401などのHTTPステータスで接続を拒否したい場合は、`set_pre_request_handler()`を使ってください。WebSocketのルートでも呼ばれます。詳しくは[S11. Pre-request handlerでルート単位の認証を行う](../../cookbook/s11-pre-request)を参照してください。
|
||||||
|
|
||||||
## WSSで使う
|
## WSSで使う
|
||||||
|
|
||||||
HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。
|
HTTPS上のWebSocket(WSS)にも対応しています。サーバー側は `httplib::SSLServer` にWebSocketハンドラーを登録するだけです。
|
||||||
|
|||||||
+124
-114
@@ -2,133 +2,143 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
using namespace httplib;
|
using namespace httplib;
|
||||||
|
|
||||||
// Example usage of parse_accept_header function
|
// Example usage of parse_accept_header function
|
||||||
std::cout << "=== Accept Header Parser Example ===" << std::endl;
|
std::cout << "=== Accept Header Parser Example ===" << std::endl;
|
||||||
|
|
||||||
// Example 1: Simple Accept header
|
// Example 1: Simple Accept header
|
||||||
std::string accept1 = "text/html,application/json,text/plain";
|
std::string accept1 = "text/html,application/json,text/plain";
|
||||||
std::vector<std::string> result1;
|
std::vector<std::string> result1;
|
||||||
if (detail::parse_accept_header(accept1, result1)) {
|
if (detail::parse_accept_header(accept1, result1)) {
|
||||||
std::cout << "\nExample 1: " << accept1 << std::endl;
|
std::cout << "\nExample 1: " << accept1 << std::endl;
|
||||||
std::cout << "Parsed order:" << std::endl;
|
std::cout << "Parsed order:" << std::endl;
|
||||||
for (size_t i = 0; i < result1.size(); ++i) {
|
for (size_t i = 0; i < result1.size(); ++i) {
|
||||||
std::cout << " " << (i + 1) << ". " << result1[i] << std::endl;
|
std::cout << " " << (i + 1) << ". " << result1[i] << std::endl;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
std::cout << "\nExample 1: Failed to parse Accept header" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Accept header with quality values
|
||||||
|
std::string accept2 =
|
||||||
|
"text/html;q=0.9,application/json;q=1.0,text/plain;q=0.8";
|
||||||
|
std::vector<std::string> result2;
|
||||||
|
if (detail::parse_accept_header(accept2, result2)) {
|
||||||
|
std::cout << "\nExample 2: " << accept2 << std::endl;
|
||||||
|
std::cout << "Parsed order (sorted by priority):" << std::endl;
|
||||||
|
for (size_t i = 0; i < result2.size(); ++i) {
|
||||||
|
std::cout << " " << (i + 1) << ". " << result2[i] << std::endl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cout << "\nExample 2: Failed to parse Accept header" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 3: Browser-like Accept header
|
||||||
|
std::string accept3 = "text/html,application/xhtml+xml,application/"
|
||||||
|
"xml;q=0.9,image/webp,*/*;q=0.8";
|
||||||
|
std::vector<std::string> result3;
|
||||||
|
if (detail::parse_accept_header(accept3, result3)) {
|
||||||
|
std::cout << "\nExample 3: " << accept3 << std::endl;
|
||||||
|
std::cout << "Parsed order:" << std::endl;
|
||||||
|
for (size_t i = 0; i < result3.size(); ++i) {
|
||||||
|
std::cout << " " << (i + 1) << ". " << result3[i] << std::endl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cout << "\nExample 3: Failed to parse Accept header" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 4: Invalid Accept header examples
|
||||||
|
std::cout << "\n=== Invalid Accept Header Examples ===" << std::endl;
|
||||||
|
|
||||||
|
std::vector<std::string> invalid_examples = {
|
||||||
|
"text/html;q=1.5,application/json", // q > 1.0
|
||||||
|
"text/html;q=-0.1,application/json", // q < 0.0
|
||||||
|
"text/html;q=invalid,application/json", // invalid q value
|
||||||
|
"invalidtype,application/json" // invalid media type
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &invalid_accept : invalid_examples) {
|
||||||
|
std::vector<std::string> temp_result;
|
||||||
|
std::cout << "\nTesting invalid: " << invalid_accept << std::endl;
|
||||||
|
if (detail::parse_accept_header(invalid_accept, temp_result)) {
|
||||||
|
std::cout << " Unexpectedly succeeded!" << std::endl;
|
||||||
} else {
|
} else {
|
||||||
std::cout << "\nExample 1: Failed to parse Accept header" << std::endl;
|
std::cout << " Correctly rejected as invalid" << std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 4: Server usage example
|
||||||
|
std::cout << "\n=== Server Usage Example ===" << std::endl;
|
||||||
|
Server svr;
|
||||||
|
|
||||||
|
svr.Get("/api/data", [](const Request &req, Response &res) {
|
||||||
|
// Get Accept header
|
||||||
|
auto accept_header = req.get_header_value("Accept");
|
||||||
|
if (accept_header.empty()) {
|
||||||
|
accept_header = "*/*"; // Default if no Accept header
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example 2: Accept header with quality values
|
// Parse accept header to get preferred content types
|
||||||
std::string accept2 = "text/html;q=0.9,application/json;q=1.0,text/plain;q=0.8";
|
std::vector<std::string> preferred_types;
|
||||||
std::vector<std::string> result2;
|
if (!detail::parse_accept_header(accept_header, preferred_types)) {
|
||||||
if (detail::parse_accept_header(accept2, result2)) {
|
// Invalid Accept header
|
||||||
std::cout << "\nExample 2: " << accept2 << std::endl;
|
res.status = 400; // Bad Request
|
||||||
std::cout << "Parsed order (sorted by priority):" << std::endl;
|
res.set_content("Invalid Accept header", "text/plain");
|
||||||
for (size_t i = 0; i < result2.size(); ++i) {
|
return;
|
||||||
std::cout << " " << (i + 1) << ". " << result2[i] << std::endl;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::cout << "\nExample 2: Failed to parse Accept header" << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example 3: Browser-like Accept header
|
std::cout << "Client Accept header: " << accept_header << std::endl;
|
||||||
std::string accept3 = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8";
|
std::cout << "Preferred types in order:" << std::endl;
|
||||||
std::vector<std::string> result3;
|
for (size_t i = 0; i < preferred_types.size(); ++i) {
|
||||||
if (detail::parse_accept_header(accept3, result3)) {
|
std::cout << " " << (i + 1) << ". " << preferred_types[i] << std::endl;
|
||||||
std::cout << "\nExample 3: " << accept3 << std::endl;
|
|
||||||
std::cout << "Parsed order:" << std::endl;
|
|
||||||
for (size_t i = 0; i < result3.size(); ++i) {
|
|
||||||
std::cout << " " << (i + 1) << ". " << result3[i] << std::endl;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
std::cout << "\nExample 3: Failed to parse Accept header" << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example 4: Invalid Accept header examples
|
// Choose response format based on client preference
|
||||||
std::cout << "\n=== Invalid Accept Header Examples ===" << std::endl;
|
std::string response_content;
|
||||||
|
std::string content_type;
|
||||||
|
|
||||||
std::vector<std::string> invalid_examples = {
|
for (const auto &type : preferred_types) {
|
||||||
"text/html;q=1.5,application/json", // q > 1.0
|
if (type == "application/json" || type == "application/*" ||
|
||||||
"text/html;q=-0.1,application/json", // q < 0.0
|
type == "*/*") {
|
||||||
"text/html;q=invalid,application/json", // invalid q value
|
response_content =
|
||||||
"invalidtype,application/json", // invalid media type
|
"{\"message\": \"Hello, World!\", \"data\": [1, 2, 3]}";
|
||||||
",application/json" // empty entry
|
content_type = "application/json";
|
||||||
};
|
break;
|
||||||
|
} else if (type == "text/html" || type == "text/*") {
|
||||||
for (const auto& invalid_accept : invalid_examples) {
|
response_content = "<html><body><h1>Hello, World!</h1><p>Data: 1, 2, "
|
||||||
std::vector<std::string> temp_result;
|
"3</p></body></html>";
|
||||||
std::cout << "\nTesting invalid: " << invalid_accept << std::endl;
|
content_type = "text/html";
|
||||||
if (detail::parse_accept_header(invalid_accept, temp_result)) {
|
break;
|
||||||
std::cout << " Unexpectedly succeeded!" << std::endl;
|
} else if (type == "text/plain") {
|
||||||
} else {
|
response_content = "Hello, World!\nData: 1, 2, 3";
|
||||||
std::cout << " Correctly rejected as invalid" << std::endl;
|
content_type = "text/plain";
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example 4: Server usage example
|
if (response_content.empty()) {
|
||||||
std::cout << "\n=== Server Usage Example ===" << std::endl;
|
// No supported content type found
|
||||||
Server svr;
|
res.status = 406; // Not Acceptable
|
||||||
|
res.set_content("No acceptable content type found", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
svr.Get("/api/data", [](const Request& req, Response& res) {
|
res.set_content(response_content, content_type);
|
||||||
// Get Accept header
|
std::cout << "Responding with: " << content_type << std::endl;
|
||||||
auto accept_header = req.get_header_value("Accept");
|
});
|
||||||
if (accept_header.empty()) {
|
|
||||||
accept_header = "*/*"; // Default if no Accept header
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse accept header to get preferred content types
|
std::cout << "Server configured. You can test it with:" << std::endl;
|
||||||
std::vector<std::string> preferred_types;
|
std::cout
|
||||||
if (!detail::parse_accept_header(accept_header, preferred_types)) {
|
<< " curl -H \"Accept: application/json\" http://localhost:8080/api/data"
|
||||||
// Invalid Accept header
|
<< std::endl;
|
||||||
res.status = 400; // Bad Request
|
std::cout << " curl -H \"Accept: text/html\" http://localhost:8080/api/data"
|
||||||
res.set_content("Invalid Accept header", "text/plain");
|
<< std::endl;
|
||||||
return;
|
std::cout << " curl -H \"Accept: text/plain\" http://localhost:8080/api/data"
|
||||||
}
|
<< std::endl;
|
||||||
|
std::cout << " curl -H \"Accept: text/html;q=0.9,application/json;q=1.0\" "
|
||||||
|
"http://localhost:8080/api/data"
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
std::cout << "Client Accept header: " << accept_header << std::endl;
|
return 0;
|
||||||
std::cout << "Preferred types in order:" << std::endl;
|
|
||||||
for (size_t i = 0; i < preferred_types.size(); ++i) {
|
|
||||||
std::cout << " " << (i + 1) << ". " << preferred_types[i] << std::endl;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose response format based on client preference
|
|
||||||
std::string response_content;
|
|
||||||
std::string content_type;
|
|
||||||
|
|
||||||
for (const auto& type : preferred_types) {
|
|
||||||
if (type == "application/json" || type == "application/*" || type == "*/*") {
|
|
||||||
response_content = "{\"message\": \"Hello, World!\", \"data\": [1, 2, 3]}";
|
|
||||||
content_type = "application/json";
|
|
||||||
break;
|
|
||||||
} else if (type == "text/html" || type == "text/*") {
|
|
||||||
response_content = "<html><body><h1>Hello, World!</h1><p>Data: 1, 2, 3</p></body></html>";
|
|
||||||
content_type = "text/html";
|
|
||||||
break;
|
|
||||||
} else if (type == "text/plain") {
|
|
||||||
response_content = "Hello, World!\nData: 1, 2, 3";
|
|
||||||
content_type = "text/plain";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response_content.empty()) {
|
|
||||||
// No supported content type found
|
|
||||||
res.status = 406; // Not Acceptable
|
|
||||||
res.set_content("No acceptable content type found", "text/plain");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.set_content(response_content, content_type);
|
|
||||||
std::cout << "Responding with: " << content_type << std::endl;
|
|
||||||
});
|
|
||||||
|
|
||||||
std::cout << "Server configured. You can test it with:" << std::endl;
|
|
||||||
std::cout << " curl -H \"Accept: application/json\" http://localhost:8080/api/data" << std::endl;
|
|
||||||
std::cout << " curl -H \"Accept: text/html\" http://localhost:8080/api/data" << std::endl;
|
|
||||||
std::cout << " curl -H \"Accept: text/plain\" http://localhost:8080/api/data" << std::endl;
|
|
||||||
std::cout << " curl -H \"Accept: text/html;q=0.9,application/json;q=1.0\" http://localhost:8080/api/data" << std::endl;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// sample.cc
|
// server.cc
|
||||||
//
|
//
|
||||||
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
// Copyright (c) 2026 Yuji Hirose. All rights reserved.
|
||||||
// MIT License
|
// MIT License
|
||||||
|
|||||||
+1
-1
@@ -107,7 +107,7 @@ if host_machine.system() == 'windows'
|
|||||||
elif host_machine.system() == 'darwin'
|
elif host_machine.system() == 'darwin'
|
||||||
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation'], required: async_ns_opt)
|
async_ns_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation'], required: async_ns_opt)
|
||||||
else
|
else
|
||||||
has_gai_a = cxx.has_function('getaddrinfo_a', args: '-D_GNU_SOURCE')
|
has_gai_a = async_ns_opt.allowed() and cxx.has_function('getaddrinfo_a', args: '-D_GNU_SOURCE')
|
||||||
if has_gai_a
|
if has_gai_a
|
||||||
async_ns_dep = declare_dependency()
|
async_ns_dep = declare_dependency()
|
||||||
else
|
else
|
||||||
|
|||||||
+7
-3
@@ -164,14 +164,18 @@ if [ "$DRY_RUN" -eq 1 ]; then
|
|||||||
echo "==> Dry run complete. No changes were made."
|
echo "==> Dry run complete. No changes were made."
|
||||||
else
|
else
|
||||||
echo "==> Updating httplib.h..."
|
echo "==> Updating httplib.h..."
|
||||||
sed -i '' "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
# `-i.bak` is the in-place form GNU and BSD sed both accept (`-i ''` is
|
||||||
sed -i '' "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
# BSD-only: GNU sed reads the '' as the script).
|
||||||
|
sed -i.bak "s/#define CPPHTTPLIB_VERSION \"[^\"]*\"/#define CPPHTTPLIB_VERSION \"$NEW_VERSION\"/" httplib.h
|
||||||
|
sed -i.bak "s/#define CPPHTTPLIB_VERSION_NUM \"0x[0-9a-fA-F]*\"/#define CPPHTTPLIB_VERSION_NUM \"$VERSION_HEX\"/" httplib.h
|
||||||
|
rm -f httplib.h.bak
|
||||||
echo " CPPHTTPLIB_VERSION = \"$NEW_VERSION\""
|
echo " CPPHTTPLIB_VERSION = \"$NEW_VERSION\""
|
||||||
echo " CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
|
echo " CPPHTTPLIB_VERSION_NUM = \"$VERSION_HEX\""
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "==> Updating docs-src/config.toml..."
|
echo "==> Updating docs-src/config.toml..."
|
||||||
sed -i '' "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
|
sed -i.bak "s/^version = \"[^\"]*\"/version = \"$NEW_VERSION\"/" docs-src/config.toml
|
||||||
|
rm -f docs-src/config.toml.bak
|
||||||
echo " version = \"$NEW_VERSION\""
|
echo " version = \"$NEW_VERSION\""
|
||||||
|
|
||||||
# --- Step 6: Commit, tag, and push ---
|
# --- Step 6: Commit, tag, and push ---
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ if(HTTPLIB_IS_USING_OPENSSL)
|
|||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
COMMAND_ERROR_IS_FATAL ANY
|
COMMAND_ERROR_IS_FATAL ANY
|
||||||
)
|
)
|
||||||
|
# cert_san_types.pem: the bytes of each SAN read as the other type:
|
||||||
|
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=san-types -addext subjectAltName=DNS:a.zz,IP:42.46.122.122 -out cert_san_types.pem
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
COMMAND_ERROR_IS_FATAL ANY
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(fuzzing)
|
add_subdirectory(fuzzing)
|
||||||
|
|||||||
+5
-1
@@ -1,5 +1,5 @@
|
|||||||
CXX = clang++
|
CXX = clang++
|
||||||
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
|
CXXFLAGS = -g -std=c++11 -I. -Wall -Wextra -Wtype-limits -Wconversion -Wshadow -DCPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO -fsanitize=address $(EXTRA_CXXFLAGS) # -fno-exceptions -DCPPHTTPLIB_NO_EXCEPTIONS
|
||||||
|
|
||||||
ifneq ($(OS), Windows_NT)
|
ifneq ($(OS), Windows_NT)
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
@@ -264,6 +264,10 @@ test_websocket_heartbeat : test_websocket_heartbeat.cc ../httplib.h Makefile
|
|||||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
|
$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_heartbeat.cc $(TEST_ARGS)
|
||||||
@file $@
|
@file $@
|
||||||
|
|
||||||
|
test_websocket_thread_safety : test_websocket_thread_safety.cc ../httplib.h Makefile cert.pem
|
||||||
|
$(CXX) -o $@ -I.. $(CXXFLAGS) test_websocket_thread_safety.cc $(TEST_ARGS)
|
||||||
|
@file $@
|
||||||
|
|
||||||
test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem
|
test_proxy : test_proxy.cc ../httplib.h Makefile cert.pem
|
||||||
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS)
|
$(CXX) -o $@ -I.. $(CXXFLAGS) test_proxy.cc $(TEST_ARGS)
|
||||||
|
|
||||||
|
|||||||
@@ -33,3 +33,7 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=127.0.0.1" -
|
|||||||
# different address. The SAN address must match; the CN address
|
# different address. The SAN address must match; the CN address
|
||||||
# must be ignored.
|
# must be ignored.
|
||||||
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem
|
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem
|
||||||
|
|
||||||
|
# cert_san_types.pem: the bytes of each SAN read as the other type:
|
||||||
|
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||||
|
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=san-types" -addext "subjectAltName=DNS:a.zz,IP:42.46.122.122" -out cert_san_types.pem
|
||||||
|
|||||||
+11
-1
@@ -137,6 +137,15 @@ cert_ipv6_pem = custom_target(
|
|||||||
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=::1', '-addext', 'subjectAltName=IP:2001:db8::1', '-out', '@OUTPUT@']
|
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=::1', '-addext', 'subjectAltName=IP:2001:db8::1', '-out', '@OUTPUT@']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# cert_san_types.pem: the bytes of each SAN read as the other type: DNS:a.zz is
|
||||||
|
# 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||||
|
cert_san_types_pem = custom_target(
|
||||||
|
'cert_san_types_pem',
|
||||||
|
input: key_pem,
|
||||||
|
output: 'cert_san_types.pem',
|
||||||
|
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=san-types', '-addext', 'subjectAltName=DNS:a.zz,IP:42.46.122.122', '-out', '@OUTPUT@']
|
||||||
|
)
|
||||||
|
|
||||||
# Copy test files to the build directory
|
# Copy test files to the build directory
|
||||||
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
||||||
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
||||||
@@ -178,7 +187,8 @@ test(
|
|||||||
client_encrypted_pbes1_key_pem,
|
client_encrypted_pbes1_key_pem,
|
||||||
client_encrypted_cert_pem,
|
client_encrypted_cert_pem,
|
||||||
cert_ip_cn_pem,
|
cert_ip_cn_pem,
|
||||||
cert_ipv6_pem
|
cert_ipv6_pem,
|
||||||
|
cert_san_types_pem
|
||||||
],
|
],
|
||||||
workdir: meson.current_build_dir(),
|
workdir: meson.current_build_dir(),
|
||||||
timeout: 300
|
timeout: 300
|
||||||
|
|||||||
@@ -18,3 +18,24 @@ services:
|
|||||||
context: ./
|
context: ./
|
||||||
args:
|
args:
|
||||||
auth: digest
|
auth: digest
|
||||||
|
|
||||||
|
# Self-hosted stand-in for the httpbin.org-style auth-testing endpoints
|
||||||
|
# (/basic-auth, /digest-auth) that BaseAuthTest/DigestAuthTest exercise
|
||||||
|
# through the proxies above, so those tests don't depend on an external
|
||||||
|
# site's uptime.
|
||||||
|
httpbin_backend:
|
||||||
|
image: mccutchen/go-httpbin:latest
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
# TLS termination in front of httpbin_backend (which only speaks plain
|
||||||
|
# HTTP) so the SSL variants of those tests can CONNECT-tunnel through the
|
||||||
|
# proxies to "httpbin" on port 443, same as the NoSSL variants do on 80.
|
||||||
|
httpbin:
|
||||||
|
image: nginx:alpine
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- httpbin_backend
|
||||||
|
volumes:
|
||||||
|
- ./httpbin_nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||||
|
- ../cert.pem:/etc/nginx/certs/cert.pem:ro
|
||||||
|
- ../key.pem:/etc/nginx/certs/key.pem:ro
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name httpbin;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://httpbin_backend:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name httpbin;
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/certs/cert.pem;
|
||||||
|
ssl_certificate_key /etc/nginx/certs/key.pem;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://httpbin_backend:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
}
|
||||||
|
}
|
||||||
+5370
-317
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -18,4 +18,4 @@ emailAddress = test@email.address
|
|||||||
challengePassword = 1234
|
challengePassword = 1234
|
||||||
|
|
||||||
[SAN]
|
[SAN]
|
||||||
subjectAltName=IP:127.0.0.1
|
subjectAltName=IP:127.0.0.1,DNS:localhost
|
||||||
|
|||||||
+36
-25
@@ -22,23 +22,25 @@ template <typename T> void ProxyTest(T &cli, bool basic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(ProxyTest, NoSSLBasic) {
|
TEST(ProxyTest, NoSSLBasic) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
ProxyTest(cli, true);
|
ProxyTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(ProxyTest, SSLBasic) {
|
TEST(ProxyTest, SSLBasic) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
ProxyTest(cli, true);
|
ProxyTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ProxyTest, NoSSLDigest) {
|
TEST(ProxyTest, NoSSLDigest) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
ProxyTest(cli, false);
|
ProxyTest(cli, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ProxyTest, SSLDigest) {
|
TEST(ProxyTest, SSLDigest) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
ProxyTest(cli, false);
|
ProxyTest(cli, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -63,23 +65,25 @@ void RedirectProxyText(T &cli, const char *path, bool basic) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
TEST(RedirectTest, HTTPBinNoSSLBasic) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
RedirectProxyText(cli, "/redirect/2", true);
|
RedirectProxyText(cli, "/redirect/2", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
TEST(RedirectTest, HTTPBinNoSSLDigest) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
RedirectProxyText(cli, "/redirect/2", false);
|
RedirectProxyText(cli, "/redirect/2", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, HTTPBinSSLBasic) {
|
TEST(RedirectTest, HTTPBinSSLBasic) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
RedirectProxyText(cli, "/redirect/2", true);
|
RedirectProxyText(cli, "/redirect/2", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, HTTPBinSSLDigest) {
|
TEST(RedirectTest, HTTPBinSSLDigest) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
RedirectProxyText(cli, "/redirect/2", false);
|
RedirectProxyText(cli, "/redirect/2", false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -173,7 +177,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|||||||
cli.Get("/basic-auth/hello/world",
|
cli.Get("/basic-auth/hello/world",
|
||||||
Headers{make_basic_authentication_header("hello", "world")});
|
Headers{make_basic_authentication_header("hello", "world")});
|
||||||
ASSERT_TRUE(res != nullptr);
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||||
|
"\"authorized\":true}\n"),
|
||||||
normalizeJson(res->body));
|
normalizeJson(res->body));
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
@@ -182,7 +187,8 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|||||||
cli.set_basic_auth("hello", "world");
|
cli.set_basic_auth("hello", "world");
|
||||||
auto res = cli.Get("/basic-auth/hello/world");
|
auto res = cli.Get("/basic-auth/hello/world");
|
||||||
ASSERT_TRUE(res != nullptr);
|
ASSERT_TRUE(res != nullptr);
|
||||||
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\"}\n"),
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||||
|
"\"authorized\":true}\n"),
|
||||||
normalizeJson(res->body));
|
normalizeJson(res->body));
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
@@ -203,13 +209,14 @@ template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(BaseAuthTest, NoSSL) {
|
TEST(BaseAuthTest, NoSSL) {
|
||||||
Client cli("httpcan.org");
|
Client cli("httpbin");
|
||||||
BaseAuthTestFromHTTPWatch(cli);
|
BaseAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(BaseAuthTest, SSL) {
|
TEST(BaseAuthTest, SSL) {
|
||||||
SSLClient cli("httpcan.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
BaseAuthTestFromHTTPWatch(cli);
|
BaseAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -228,21 +235,21 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
// go-httpbin (the "httpbin" test double) only implements MD5 and
|
||||||
|
// SHA-256 for digest auth, so SHA-256 is as far as this can exercise
|
||||||
|
// the client's digest-auth algorithm selection end-to-end.
|
||||||
std::vector<std::string> paths = {
|
std::vector<std::string> paths = {
|
||||||
"/digest-auth/auth/hello/world/MD5",
|
"/digest-auth/auth/hello/world/MD5",
|
||||||
"/digest-auth/auth/hello/world/SHA-256",
|
"/digest-auth/auth/hello/world/SHA-256",
|
||||||
"/digest-auth/auth/hello/world/SHA-512",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
cli.set_digest_auth("hello", "world");
|
cli.set_digest_auth("hello", "world");
|
||||||
for (auto path : paths) {
|
for (auto path : paths) {
|
||||||
auto res = cli.Get(path.c_str());
|
auto res = cli.Get(path.c_str());
|
||||||
ASSERT_TRUE(res != nullptr);
|
ASSERT_TRUE(res != nullptr);
|
||||||
std::string algo(path.substr(path.rfind('/') + 1));
|
EXPECT_EQ(normalizeJson("{\"authenticated\":true,\"user\":\"hello\","
|
||||||
EXPECT_EQ(
|
"\"authorized\":true}\n"),
|
||||||
normalizeJson("{\"algorithm\":\"" + algo +
|
normalizeJson(res->body));
|
||||||
"\",\"authenticated\":true,\"user\":\"hello\"}\n"),
|
|
||||||
normalizeJson(res->body));
|
|
||||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,12 +270,13 @@ template <typename T> void DigestAuthTestFromHTTPWatch(T &cli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(DigestAuthTest, SSL) {
|
TEST(DigestAuthTest, SSL) {
|
||||||
SSLClient cli("httpcan.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
DigestAuthTestFromHTTPWatch(cli);
|
DigestAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(DigestAuthTest, NoSSL) {
|
TEST(DigestAuthTest, NoSSL) {
|
||||||
Client cli("httpcan.org");
|
Client cli("httpbin");
|
||||||
DigestAuthTestFromHTTPWatch(cli);
|
DigestAuthTestFromHTTPWatch(cli);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -329,22 +337,24 @@ template <typename T> void KeepAliveTest(T &cli, bool basic) {
|
|||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(KeepAliveTest, NoSSLWithBasic) {
|
TEST(KeepAliveTest, NoSSLWithBasic) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
KeepAliveTest(cli, true);
|
KeepAliveTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeepAliveTest, SSLWithBasic) {
|
TEST(KeepAliveTest, SSLWithBasic) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
KeepAliveTest(cli, true);
|
KeepAliveTest(cli, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeepAliveTest, NoSSLWithDigest) {
|
TEST(KeepAliveTest, NoSSLWithDigest) {
|
||||||
Client cli("httpbingo.org");
|
Client cli("httpbin");
|
||||||
KeepAliveTest(cli, false);
|
KeepAliveTest(cli, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(KeepAliveTest, SSLWithDigest) {
|
TEST(KeepAliveTest, SSLWithDigest) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
KeepAliveTest(cli, false);
|
KeepAliveTest(cli, false);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -353,7 +363,8 @@ TEST(KeepAliveTest, SSLWithDigest) {
|
|||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(ProxyTest, SSLOpenStream) {
|
TEST(ProxyTest, SSLOpenStream) {
|
||||||
SSLClient cli("httpbingo.org");
|
SSLClient cli("httpbin");
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
cli.set_proxy("localhost", 3128);
|
cli.set_proxy("localhost", 3128);
|
||||||
cli.set_proxy_basic_auth("hello", "world");
|
cli.set_proxy_basic_auth("hello", "world");
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,14 @@
|
|||||||
// without waiting 30 seconds.
|
// without waiting 30 seconds.
|
||||||
|
|
||||||
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
||||||
#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 3
|
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 3
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 3
|
||||||
#include <httplib.h>
|
#include <httplib.h>
|
||||||
|
|
||||||
#include "gtest/gtest.h"
|
#include "gtest/gtest.h"
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
|
||||||
using namespace httplib;
|
using namespace httplib;
|
||||||
|
|
||||||
class WebSocketHeartbeatTest : public ::testing::Test {
|
class WebSocketHeartbeatTest : public ::testing::Test {
|
||||||
@@ -191,6 +194,76 @@ TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
|
|||||||
EXPECT_FALSE(client.is_open());
|
EXPECT_FALSE(client.is_open());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The compile-time client read timeout (3s here) was never asked for through
|
||||||
|
// set_read_timeout(), so when it elapses read() reports Fail and closes the
|
||||||
|
// connection rather than handing back a Timeout on a still-open one.
|
||||||
|
TEST_F(WebSocketPongTimeoutTest, CompileTimeClientReadTimeoutIsFail) {
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||||
|
client.set_websocket_ping_interval(0);
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
// Server pings are off and its handler never sends, so nothing arrives.
|
||||||
|
std::string msg;
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Fail);
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
}
|
||||||
|
|
||||||
|
// The compile-time server read timeout (3s here) is a backstop that reclaims
|
||||||
|
// the worker from a peer gone quiet, not a timeout the handler asked for. When
|
||||||
|
// it elapses read() must return Fail, so a handler written as
|
||||||
|
// `while (ws.read(msg))` ends instead of re-running its body with the previous
|
||||||
|
// message still in `msg`.
|
||||||
|
class WebSocketServerReadTimeoutTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
svr_.set_websocket_ping_interval(0);
|
||||||
|
svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &ws) {
|
||||||
|
std::string msg;
|
||||||
|
while (ws.read(msg)) {
|
||||||
|
iterations_++;
|
||||||
|
ws.send(msg);
|
||||||
|
}
|
||||||
|
handler_done_.set_value();
|
||||||
|
});
|
||||||
|
|
||||||
|
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_;
|
||||||
|
std::atomic<int> iterations_{0};
|
||||||
|
std::promise<void> handler_done_;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(WebSocketServerReadTimeoutTest, BackstopEndsHandlerLoop) {
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||||
|
client.set_websocket_ping_interval(0); // nothing reaches the server's read()
|
||||||
|
client.set_read_timeout(10, 0); // fail rather than hang
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
ASSERT_TRUE(client.send("hello"));
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_EQ(client.read(msg), ws::Text);
|
||||||
|
EXPECT_EQ("hello", msg);
|
||||||
|
|
||||||
|
// The client now stays silent. The server's backstop elapses and the
|
||||||
|
// handler returns, having run its loop body exactly once.
|
||||||
|
auto done = handler_done_.get_future();
|
||||||
|
ASSERT_EQ(done.wait_for(std::chrono::seconds(6)), std::future_status::ready);
|
||||||
|
EXPECT_EQ(1, iterations_.load());
|
||||||
|
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Fail);
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
}
|
||||||
|
|
||||||
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
|
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
|
||||||
// even with a small max_missed_pongs budget. This is the positive counterpart
|
// even with a small max_missed_pongs budget. This is the positive counterpart
|
||||||
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
// Standalone test for TLS-session thread safety on wss:// connections.
|
||||||
|
//
|
||||||
|
// A wss:// WebSocket enters one TLS session from multiple threads: the read
|
||||||
|
// path, the application's send()/close(), and the heartbeat ping thread. A
|
||||||
|
// TLS session must never be entered concurrently, so httplib routes wss://
|
||||||
|
// through WebSocketSSLStream, which serializes every TLS call. These tests
|
||||||
|
// drive that concurrency directly. Built with ASan in CI, so a regression
|
||||||
|
// surfaces as a heap-buffer-overflow, not just a flaky assertion.
|
||||||
|
|
||||||
|
// Fire the heartbeat every second so the ping-vs-read case actually crosses a
|
||||||
|
// ping while the reader is idle.
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
||||||
|
#include <httplib.h>
|
||||||
|
|
||||||
|
#include "gtest/gtest.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
|
||||||
|
using namespace httplib;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
const size_t kPayloadBytes = 2048;
|
||||||
|
const size_t kSendCount = 2000;
|
||||||
|
const size_t kBurstPerFrame = 100;
|
||||||
|
const int kCloseCycles = 20;
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class WebSocketTlsThreadSafetyTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
WebSocketTlsThreadSafetyTest() : svr_("cert.pem", "key.pem") {}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
if (thread_.joinable()) {
|
||||||
|
svr_.stop();
|
||||||
|
thread_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registers the handler and starts the TLS server. Called by each test
|
||||||
|
// after its own server configuration, since the heartbeat test needs the
|
||||||
|
// pings the others switch off.
|
||||||
|
bool start(Server::WebSocketHandler handler) {
|
||||||
|
if (!svr_.is_valid()) { return false; }
|
||||||
|
svr_.WebSocket("/ws", std::move(handler));
|
||||||
|
port_ = svr_.bind_to_any_port("localhost");
|
||||||
|
if (port_ <= 0) { return false; }
|
||||||
|
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
|
||||||
|
svr_.wait_until_ready();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url() const {
|
||||||
|
return "wss://localhost:" + std::to_string(port_) + "/ws";
|
||||||
|
}
|
||||||
|
|
||||||
|
SSLServer svr_;
|
||||||
|
int port_ = 0;
|
||||||
|
std::thread thread_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A sender thread hammers send() while another thread loops read(). Both
|
||||||
|
// enter the same TLS session, and every echoed frame must arrive intact.
|
||||||
|
TEST_F(WebSocketTlsThreadSafetyTest, SendWhileAnotherThreadReads) {
|
||||||
|
svr_.set_websocket_ping_interval(0);
|
||||||
|
ASSERT_TRUE(start([](const Request &, ws::WebSocket &sock) {
|
||||||
|
std::string msg;
|
||||||
|
while (sock.read(msg) != ws::ReadResult::Fail) {
|
||||||
|
if (!sock.send(msg.data(), msg.size())) { break; }
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
ws::WebSocketClient cli(url());
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
ASSERT_TRUE(cli.connect());
|
||||||
|
|
||||||
|
const std::string payload(kPayloadBytes, 'x');
|
||||||
|
|
||||||
|
// close() drains the peer's Close reply with its own frame reader, so once
|
||||||
|
// it starts, two threads parse frames from one stream and can split a
|
||||||
|
// payload between them. That is frame-level, not TLS-level, and happens on
|
||||||
|
// ws:// too, so only frames completed before close() are checked here.
|
||||||
|
std::atomic<bool> closing(false);
|
||||||
|
std::atomic<size_t> frames_read(0);
|
||||||
|
std::atomic<size_t> corrupt_frames(0);
|
||||||
|
std::thread reader([&]() {
|
||||||
|
std::string msg;
|
||||||
|
while (cli.read(msg) != ws::ReadResult::Fail) {
|
||||||
|
if (msg != payload && !closing.load()) { corrupt_frames++; }
|
||||||
|
frames_read++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
size_t sent = 0;
|
||||||
|
for (size_t i = 0; i < kSendCount; i++) {
|
||||||
|
if (!cli.send(payload.data(), payload.size())) { break; }
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
closing.store(true);
|
||||||
|
cli.close();
|
||||||
|
reader.join();
|
||||||
|
|
||||||
|
EXPECT_EQ(kSendCount, sent);
|
||||||
|
EXPECT_EQ(static_cast<size_t>(0), corrupt_frames.load());
|
||||||
|
EXPECT_GT(frames_read.load(), static_cast<size_t>(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// close() sends a Close frame and drains the peer's reply while a second
|
||||||
|
// thread is inside read(). Repeated to shake out the race.
|
||||||
|
TEST_F(WebSocketTlsThreadSafetyTest, CloseWhileAnotherThreadReads) {
|
||||||
|
svr_.set_websocket_ping_interval(0);
|
||||||
|
ASSERT_TRUE(start([](const Request &, ws::WebSocket &sock) {
|
||||||
|
const std::string burst(64, 'p');
|
||||||
|
std::string msg;
|
||||||
|
while (sock.read(msg) != ws::ReadResult::Fail) {
|
||||||
|
for (size_t i = 0; i < kBurstPerFrame; i++) {
|
||||||
|
if (!sock.send(burst.data(), burst.size())) { return; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (int cycle = 0; cycle < kCloseCycles; cycle++) {
|
||||||
|
ws::WebSocketClient cli(url());
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
ASSERT_TRUE(cli.connect()) << "cycle " << cycle;
|
||||||
|
|
||||||
|
std::thread reader([&]() {
|
||||||
|
std::string msg;
|
||||||
|
while (cli.read(msg) != ws::ReadResult::Fail) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const std::string trigger(64, 't');
|
||||||
|
ASSERT_TRUE(cli.send(trigger.data(), trigger.size()));
|
||||||
|
|
||||||
|
cli.close();
|
||||||
|
reader.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The heartbeat ping thread writes to the TLS session on its own timer while
|
||||||
|
// the application blocks in read() with no traffic. The ping's write must not
|
||||||
|
// collide with the reader. The 1-second interval above means several pings
|
||||||
|
// fire on both sides during this idle window.
|
||||||
|
TEST_F(WebSocketTlsThreadSafetyTest, HeartbeatPingWhileReaderIsIdle) {
|
||||||
|
ASSERT_TRUE(start([](const Request &, ws::WebSocket &sock) {
|
||||||
|
std::string msg;
|
||||||
|
while (sock.read(msg) != ws::ReadResult::Fail) {}
|
||||||
|
}));
|
||||||
|
|
||||||
|
ws::WebSocketClient cli(url());
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
ASSERT_TRUE(cli.connect());
|
||||||
|
|
||||||
|
// No data frames are sent, so the reader stays parked inside read() while
|
||||||
|
// both sides exchange pings and pongs on the heartbeat timer. read() only
|
||||||
|
// returns once close() below tears the connection down.
|
||||||
|
std::thread reader([&]() {
|
||||||
|
std::string msg;
|
||||||
|
while (cli.read(msg) != ws::ReadResult::Fail) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(4));
|
||||||
|
|
||||||
|
// The connection survived the heartbeat exchange without a TLS-session race.
|
||||||
|
EXPECT_TRUE(cli.is_open());
|
||||||
|
|
||||||
|
cli.close();
|
||||||
|
reader.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CPPHTTPLIB_SSL_ENABLED
|
||||||
Reference in New Issue
Block a user