mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79d83feb18 | |||
| fe56a07da5 | |||
| 907257f51d | |||
| 0c2f535b74 | |||
| c7ba963a17 | |||
| 4465e81b9f | |||
| 44215e23e9 | |||
| 91219d4508 | |||
| c86c192f3e | |||
| 008e107d0f | |||
| d278f965cc | |||
| 4c4b62dd7e | |||
| b1792ef29c | |||
| 0f3d063f0a | |||
| 0d7d637466 | |||
| 1ff0c8588d | |||
| b1cc8095a8 |
@@ -120,6 +120,155 @@ jobs:
|
||||
- name: build and run ThreadPool test
|
||||
run: cd test && make test_thread_pool && ./test_thread_pool
|
||||
|
||||
# BoringSSL is Google's fork of OpenSSL. It has no API stability guarantee
|
||||
# and is not packaged by distros, so we build it from source. cpp-httplib
|
||||
# treats it as an OpenSSL backend variant via the OPENSSL_IS_BORINGSSL
|
||||
# macro (see httplib.h). This job is best-effort: continue-on-error keeps
|
||||
# upstream API drift from blocking PRs while still surfacing breakage.
|
||||
ubuntu-boringssl:
|
||||
runs-on: ubuntu-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
|
||||
continue-on-error: true
|
||||
name: ubuntu (boringssl, best-effort)
|
||||
env:
|
||||
# Tracking HEAD keeps us honest about upstream churn. If breakage
|
||||
# becomes routine, replace HEAD with a 40-char commit SHA; the
|
||||
# resolve step uses the SHA directly when it matches that shape.
|
||||
BORINGSSL_REF: HEAD
|
||||
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install common libraries
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libcurl4-openssl-dev zlib1g-dev libbrotli-dev libzstd-dev
|
||||
- name: resolve BoringSSL commit
|
||||
id: boringssl-rev
|
||||
# Accept either a ref name (resolved via git ls-remote) or a full
|
||||
# 40-char SHA used directly. ls-remote does not list arbitrary
|
||||
# commit SHAs, so pinning requires the second path.
|
||||
run: |
|
||||
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
sha="${BORINGSSL_REF}"
|
||||
echo "Using pinned BoringSSL SHA: ${sha}"
|
||||
else
|
||||
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
|
||||
if [ -z "$sha" ]; then
|
||||
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
|
||||
fi
|
||||
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
|
||||
- name: cache BoringSSL build
|
||||
id: boringssl-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.BORINGSSL_PREFIX }}
|
||||
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
|
||||
- name: build BoringSSL
|
||||
if: steps.boringssl-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -e
|
||||
git clone https://boringssl.googlesource.com/boringssl boringssl
|
||||
cd boringssl
|
||||
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
|
||||
cmake --build build -j"$(nproc)" --target install
|
||||
- name: build and run tests (BoringSSL)
|
||||
# Override OPENSSL_SUPPORT to point the existing OpenSSL Makefile path
|
||||
# at BoringSSL's prefix. BoringSSL defines OPENSSL_IS_BORINGSSL in
|
||||
# <openssl/base.h>, which httplib.h and test.cc use to switch on API
|
||||
# differences (e.g. SAN-only hostname verification, no CN fallback).
|
||||
#
|
||||
# BoringSSL's public headers (<openssl/stack.h>) use std::enable_if_t,
|
||||
# so consumers must compile with C++14 or later. cpp-httplib itself
|
||||
# supports C++11, but anyone pairing it with BoringSSL inherits this
|
||||
# constraint. EXTRA_CXXFLAGS appends after the Makefile's -std=c++11
|
||||
# and the later flag wins.
|
||||
run: |
|
||||
cd test
|
||||
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -lpthread"
|
||||
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
|
||||
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
|
||||
env:
|
||||
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
|
||||
|
||||
# macOS counterpart of the BoringSSL job. Same best-effort posture; the
|
||||
# extra framework links cover the macOS Keychain integration that
|
||||
# httplib.h auto-enables for any TLS backend on macOS.
|
||||
macos-boringssl:
|
||||
runs-on: macos-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
|
||||
continue-on-error: true
|
||||
name: macos (boringssl, best-effort)
|
||||
env:
|
||||
BORINGSSL_REF: HEAD
|
||||
BORINGSSL_PREFIX: ${{ github.workspace }}/boringssl-install
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: resolve BoringSSL commit
|
||||
id: boringssl-rev
|
||||
# Accept either a ref name (resolved via git ls-remote) or a full
|
||||
# 40-char SHA used directly. ls-remote does not list arbitrary
|
||||
# commit SHAs, so pinning requires the second path.
|
||||
run: |
|
||||
if [[ "${BORINGSSL_REF}" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
sha="${BORINGSSL_REF}"
|
||||
echo "Using pinned BoringSSL SHA: ${sha}"
|
||||
else
|
||||
sha=$(git ls-remote https://boringssl.googlesource.com/boringssl "${BORINGSSL_REF}" | awk '{print $1}')
|
||||
if [ -z "$sha" ]; then
|
||||
echo "Failed to resolve BoringSSL ref ${BORINGSSL_REF}" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Resolved ${BORINGSSL_REF} -> ${sha}"
|
||||
fi
|
||||
echo "sha=${sha}" >> "$GITHUB_OUTPUT"
|
||||
- name: cache BoringSSL build
|
||||
id: boringssl-cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.BORINGSSL_PREFIX }}
|
||||
key: boringssl-${{ runner.os }}-${{ steps.boringssl-rev.outputs.sha }}
|
||||
- name: build BoringSSL
|
||||
if: steps.boringssl-cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
set -e
|
||||
git clone https://boringssl.googlesource.com/boringssl boringssl
|
||||
cd boringssl
|
||||
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
|
||||
cmake -S . -B build \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DBUILD_SHARED_LIBS=OFF \
|
||||
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
|
||||
-DCMAKE_INSTALL_PREFIX="${BORINGSSL_PREFIX}"
|
||||
cmake --build build -j"$(sysctl -n hw.ncpu)" --target install
|
||||
- name: build and run tests (BoringSSL)
|
||||
run: |
|
||||
cd test
|
||||
# CoreFoundation/Security frameworks satisfy the Keychain integration
|
||||
# auto-enabled in httplib.h for macOS TLS builds.
|
||||
BORINGSSL_FLAGS="-DCPPHTTPLIB_OPENSSL_SUPPORT -I${BORINGSSL_PREFIX}/include -L${BORINGSSL_PREFIX}/lib -lssl -lcrypto -framework CoreFoundation -framework Security"
|
||||
make test_split OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
|
||||
make test_openssl_parallel OPENSSL_SUPPORT="${BORINGSSL_FLAGS}" EXTRA_CXXFLAGS="-std=c++17"
|
||||
env:
|
||||
LSAN_OPTIONS: suppressions=lsan_suppressions.txt
|
||||
|
||||
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
|
||||
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
|
||||
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
|
||||
@@ -250,6 +399,54 @@ jobs:
|
||||
- name: build and run ThreadPool test
|
||||
run: cd test && make test_thread_pool && ./test_thread_pool
|
||||
|
||||
ios-parse-check:
|
||||
runs-on: macos-latest
|
||||
if: >
|
||||
(github.event_name == 'push') ||
|
||||
(github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
|
||||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_macos == 'true')
|
||||
name: ios header parse check (not officially supported)
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: install OpenSSL headers
|
||||
run: brew install openssl@3
|
||||
- name: verify header parses on iOS target
|
||||
run: |
|
||||
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
|
||||
OPENSSL_INC=$(brew --prefix openssl@3)/include
|
||||
echo "Using iOS SDK: $IOS_SDK"
|
||||
echo '#include "httplib.h"' | clang++ \
|
||||
-isysroot "$IOS_SDK" \
|
||||
-target arm64-apple-ios16.0 \
|
||||
-std=c++11 \
|
||||
-DCPPHTTPLIB_OPENSSL_SUPPORT \
|
||||
-I"$OPENSSL_INC" \
|
||||
-I. -Wall -Wextra \
|
||||
-fsyntax-only -x c++ -
|
||||
- name: verify CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is rejected on iOS
|
||||
run: |
|
||||
IOS_SDK=$(xcrun --sdk iphoneos --show-sdk-path)
|
||||
OPENSSL_INC=$(brew --prefix openssl@3)/include
|
||||
out=$(echo '#include "httplib.h"' | clang++ \
|
||||
-isysroot "$IOS_SDK" \
|
||||
-target arm64-apple-ios16.0 \
|
||||
-std=c++11 \
|
||||
-DCPPHTTPLIB_OPENSSL_SUPPORT \
|
||||
-DCPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN \
|
||||
-I"$OPENSSL_INC" \
|
||||
-I. \
|
||||
-fsyntax-only -x c++ - 2>&1 || true)
|
||||
if echo "$out" | grep -q "only supported on macOS"; then
|
||||
echo "OK: #error fired as expected"
|
||||
else
|
||||
echo "FAIL: expected #error did not fire"
|
||||
echo "--- compiler output ---"
|
||||
echo "$out"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
windows:
|
||||
runs-on: windows-latest
|
||||
if: >
|
||||
|
||||
@@ -73,6 +73,9 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
|
||||
> [!NOTE]
|
||||
> **Mbed TLS / wolfSSL limitation:** `get_ca_certs()` and `get_ca_names()` only reflect CA certificates loaded via `load_ca_cert_store()`. Certificates loaded through `set_ca_cert_path()` or system certificates (`load_system_certs`) are not enumerable.
|
||||
|
||||
> [!NOTE]
|
||||
> **BoringSSL (best-effort):** BoringSSL builds under `CPPHTTPLIB_OPENSSL_SUPPORT` and is exercised by CI against current upstream. Because BoringSSL does not guarantee API stability, support is best-effort — breakage may occasionally land. Two known behavioral differences vs OpenSSL: (1) BoringSSL's public headers require C++14 or later, so consumers must compile accordingly; (2) hostname verification is SAN-only per RFC 6125 §6.4.4 (no CN fallback).
|
||||
|
||||
```c++
|
||||
// Use either OpenSSL, Mbed TLS, or wolfSSL
|
||||
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
|
||||
@@ -1178,6 +1181,17 @@ cli.set_proxy_bearer_token_auth("pass");
|
||||
> [!NOTE]
|
||||
> OpenSSL is required for Digest Authentication.
|
||||
|
||||
#### Bypass the proxy for specific hosts (`NO_PROXY`)
|
||||
|
||||
```cpp
|
||||
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
|
||||
```
|
||||
|
||||
Each pattern is `*`, a hostname suffix, an IP literal, or a CIDR block.
|
||||
Hostname matching is case-insensitive with a dot-boundary rule. See the
|
||||
[NO_PROXY cookbook](https://yhirose.github.io/cpp-httplib/en/cookbook/c16-proxy)
|
||||
for details and for reading the variable from the environment.
|
||||
|
||||
### Range
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -61,7 +61,7 @@ if(@HTTPLIB_IS_USING_ZSTD@)
|
||||
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
|
||||
set(httplib_fd_zstd_required_arg REQUIRED)
|
||||
endif()
|
||||
find_package(zstd QUIET)
|
||||
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||
if(NOT zstd_FOUND)
|
||||
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
|
||||
if(PKG_CONFIG_FOUND)
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.45.0"
|
||||
version = "0.46.1"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -49,4 +49,39 @@ cli.set_bearer_token_auth("api-token"); // for the end server
|
||||
|
||||
`Proxy-Authorization` is sent to the proxy, `Authorization` to the end server.
|
||||
|
||||
> **Note:** cpp-httplib does not read `HTTP_PROXY` or `HTTPS_PROXY` environment variables automatically. If you want to honor them, read them in your application and pass the values to `set_proxy()`.
|
||||
## Bypass the proxy for specific hosts
|
||||
|
||||
You often want internal endpoints to skip the proxy. Configure a bypass list with `set_no_proxy()`.
|
||||
|
||||
```cpp
|
||||
cli.set_proxy("proxy.internal", 8080);
|
||||
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
|
||||
```
|
||||
|
||||
Each entry is one of:
|
||||
|
||||
- `*` — bypass the proxy for all hosts
|
||||
- a hostname suffix (e.g. `example.com`) — matches `example.com` itself and any subdomain (`foo.example.com`). A leading dot is permitted but informational; both forms are equivalent.
|
||||
- a single IP literal (e.g. `192.168.1.1`, `::1`)
|
||||
- a CIDR block (e.g. `10.0.0.0/8`, `fe80::/10`)
|
||||
|
||||
Hostname matching is case-insensitive and uses a dot-boundary rule, so an entry of `example.com` does **not** match `evilexample.com`. IP comparisons are normalized through `inet_pton`, so `127.0.0.1` cannot be bypassed via alternate string forms (e.g. `127.000.000.001`). When an entry matches, the `Proxy-Authorization` header is suppressed as well.
|
||||
|
||||
Malformed entries are silently dropped. Port-specific entries such as `example.com:8080` are not supported (cpp-httplib's other host-keyed APIs are also keyed on hostname only).
|
||||
|
||||
## Read proxy settings from the environment
|
||||
|
||||
cpp-httplib doesn't touch `HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY` on its own — the config API is always explicit, the same way `set_ca_cert_path()` is. If you'd like that behavior, read the variables in your application and feed them to `set_proxy()` and `set_no_proxy()`.
|
||||
|
||||
```cpp
|
||||
if (const char *v = std::getenv("no_proxy")) {
|
||||
std::vector<std::string> patterns;
|
||||
std::stringstream ss(v);
|
||||
for (std::string item; std::getline(ss, item, ',');) {
|
||||
if (!item.empty()) { patterns.push_back(item); }
|
||||
}
|
||||
cli.set_no_proxy(patterns);
|
||||
}
|
||||
```
|
||||
|
||||
If you also read `HTTP_PROXY` yourself, honor the lowercase `http_proxy` only. The uppercase form is poisoned in CGI/FastCGI environments by the `Proxy:` request header ([CVE-2016-5385 / "httpoxy"](https://httpoxy.org/)). `HTTPS_PROXY` and `NO_PROXY` are safe in either case because their names don't begin with `HTTP_`.
|
||||
|
||||
@@ -49,4 +49,39 @@ cli.set_bearer_token_auth("api-token"); // エンドサーバー向け
|
||||
|
||||
プロキシには`Proxy-Authorization`、エンドサーバーには`Authorization`ヘッダーが送られます。
|
||||
|
||||
> **Note:** 環境変数の`HTTP_PROXY`や`HTTPS_PROXY`は自動的には読まれません。必要ならアプリケーション側で読み取って`set_proxy()`に渡してください。
|
||||
## 特定のホストだけプロキシをバイパスする
|
||||
|
||||
社内エンドポイントなどはプロキシを経由させたくないことがあります。`set_no_proxy()`で除外リストを指定できます。
|
||||
|
||||
```cpp
|
||||
cli.set_proxy("proxy.internal", 8080);
|
||||
cli.set_no_proxy({"internal.corp", "10.0.0.0/8", "*.dev.local"});
|
||||
```
|
||||
|
||||
エントリは次のいずれかです。
|
||||
|
||||
- `*` — すべてのホストでバイパス
|
||||
- ホスト名サフィックス(例: `example.com`)— `example.com`本体と任意のサブドメイン(`foo.example.com`)にマッチ。先頭にドットを付けても同じ意味です(`.example.com`)。
|
||||
- 単一のIPリテラル(例: `192.168.1.1`、`::1`)
|
||||
- CIDRブロック(例: `10.0.0.0/8`、`fe80::/10`)
|
||||
|
||||
ホスト名のマッチは大文字小文字を区別せず、ドット境界でしか一致しません。たとえば`example.com`というエントリは`evilexample.com`にはマッチしません。IPの比較は`inet_pton`で正規化されるので、`127.0.0.1`を`127.000.000.001`のような別表記でバイパスすることはできません。マッチした場合、`Proxy-Authorization`ヘッダーも自動的に外れます。
|
||||
|
||||
不正な書式のエントリは黙って捨てられます。`example.com:8080`のようなポート指定エントリはサポート外です(cpp-httplibの他のホストキーAPIもホスト名のみを扱う設計のため)。
|
||||
|
||||
## 環境変数からプロキシ設定を読み込む
|
||||
|
||||
cpp-httplib本体は`HTTP_PROXY` / `HTTPS_PROXY` / `NO_PROXY`を読みません。`set_ca_cert_path()`と同じで、設定APIは常に明示的にしています。環境変数を反映させたい場合は、アプリ側で読んで`set_proxy()`や`set_no_proxy()`に渡してください。
|
||||
|
||||
```cpp
|
||||
if (const char *v = std::getenv("no_proxy")) {
|
||||
std::vector<std::string> patterns;
|
||||
std::stringstream ss(v);
|
||||
for (std::string item; std::getline(ss, item, ',');) {
|
||||
if (!item.empty()) { patterns.push_back(item); }
|
||||
}
|
||||
cli.set_no_proxy(patterns);
|
||||
}
|
||||
```
|
||||
|
||||
`HTTP_PROXY`も自分で読むなら、小文字の`http_proxy`だけを採用してください。大文字の方はCGI/FastCGI環境で`Proxy:`リクエストヘッダーから汚染される可能性があります([CVE-2016-5385 / "httpoxy"](https://httpoxy.org/))。`HTTPS_PROXY`と`NO_PROXY`は名前が`HTTP_`で始まらないので、どちらの大文字小文字でも安全です。
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.45.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002d00"
|
||||
#define CPPHTTPLIB_VERSION "0.46.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002e01"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -339,16 +339,26 @@ using socket_t = int;
|
||||
#include <utility>
|
||||
|
||||
// On macOS with a TLS backend, enable Keychain root certificates by default
|
||||
// unless the user explicitly opts out.
|
||||
// unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since
|
||||
// the SecTrustSettings APIs used to enumerate anchor certificates are macOS
|
||||
// only; on those platforms the user must provide a CA bundle explicitly.
|
||||
#if defined(__APPLE__) && defined(__clang__) && \
|
||||
!defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \
|
||||
(defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \
|
||||
defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \
|
||||
defined(CPPHTTPLIB_WOLFSSL_SUPPORT))
|
||||
#if TARGET_OS_OSX
|
||||
#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#if defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && \
|
||||
defined(__APPLE__) && !TARGET_OS_OSX
|
||||
#error \
|
||||
"CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN is only supported on macOS. On iOS/tvOS/watchOS, supply a CA bundle via set_ca_cert_path()."
|
||||
#endif
|
||||
|
||||
// On Windows, enable Schannel certificate verification by default
|
||||
// unless the user explicitly opts out.
|
||||
@@ -382,7 +392,7 @@ using socket_t = int;
|
||||
#endif // _WIN32
|
||||
|
||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
#if TARGET_OS_MAC
|
||||
#if TARGET_OS_OSX
|
||||
#include <Security/Security.h>
|
||||
#endif
|
||||
#endif
|
||||
@@ -430,7 +440,7 @@ using socket_t = int;
|
||||
#endif
|
||||
#endif // _WIN32
|
||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
#if TARGET_OS_MAC
|
||||
#if TARGET_OS_OSX
|
||||
#include <Security/Security.h>
|
||||
#endif
|
||||
#endif
|
||||
@@ -473,7 +483,7 @@ using socket_t = int;
|
||||
#endif
|
||||
#endif // _WIN32
|
||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
#if TARGET_OS_MAC
|
||||
#if TARGET_OS_OSX
|
||||
#include <Security/Security.h>
|
||||
#endif
|
||||
#endif
|
||||
@@ -1597,7 +1607,7 @@ private:
|
||||
std::regex regex_;
|
||||
};
|
||||
|
||||
int close_socket(socket_t sock);
|
||||
int close_socket(socket_t sock) noexcept;
|
||||
|
||||
ssize_t write_headers(Stream &strm, const Headers &headers);
|
||||
|
||||
@@ -1633,6 +1643,8 @@ public:
|
||||
using Expect100ContinueHandler =
|
||||
std::function<int(const Request &, Response &)>;
|
||||
|
||||
using StartHandler = std::function<void()>;
|
||||
|
||||
using WebSocketHandler =
|
||||
std::function<void(const Request &, ws::WebSocket &)>;
|
||||
using SubProtocolSelector =
|
||||
@@ -1684,6 +1696,9 @@ public:
|
||||
Server &set_pre_request_handler(HandlerWithResponse handler);
|
||||
|
||||
Server &set_expect_100_continue_handler(Expect100ContinueHandler handler);
|
||||
|
||||
Server &set_start_handler(StartHandler handler);
|
||||
|
||||
Server &set_logger(Logger logger);
|
||||
Server &set_pre_compression_logger(Logger logger);
|
||||
Server &set_error_logger(ErrorLogger error_logger);
|
||||
@@ -1734,7 +1749,7 @@ public:
|
||||
|
||||
bool is_running() const;
|
||||
void wait_until_ready() const;
|
||||
void stop();
|
||||
void stop() noexcept;
|
||||
void decommission();
|
||||
|
||||
std::function<TaskQueue *(void)> new_task_queue;
|
||||
@@ -1873,6 +1888,7 @@ private:
|
||||
Handler post_routing_handler_;
|
||||
HandlerWithResponse pre_request_handler_;
|
||||
Expect100ContinueHandler expect_100_continue_handler_;
|
||||
StartHandler start_handler_;
|
||||
|
||||
mutable std::mutex logger_mutex_;
|
||||
Logger logger_;
|
||||
@@ -2014,6 +2030,31 @@ inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf,
|
||||
|
||||
class decompressor;
|
||||
|
||||
enum class NoProxyKind {
|
||||
Wildcard, // "*"
|
||||
HostnameSuffix, // "example.com" or ".example.com"
|
||||
IPv4Cidr, // "10.0.0.0/8" (or single IP, treated as /32)
|
||||
IPv6Cidr, // "fe80::/10" (or single IP, treated as /128)
|
||||
};
|
||||
|
||||
// Unified 16-byte buffer holding either a v4 (first 4 bytes) or v6 address.
|
||||
// Lets one CIDR matcher cover both families.
|
||||
using IPBytes = std::array<uint8_t, 16>;
|
||||
|
||||
struct NoProxyEntry {
|
||||
NoProxyKind kind = NoProxyKind::Wildcard;
|
||||
std::string hostname_pattern; // lowercased, leading/trailing dot stripped
|
||||
IPBytes net{};
|
||||
int prefix_bits = 0;
|
||||
};
|
||||
|
||||
struct NormalizedTarget {
|
||||
std::string hostname; // lowercase; brackets and trailing dot removed
|
||||
bool is_ipv4 = false;
|
||||
bool is_ipv6 = false;
|
||||
IPBytes ip{};
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
class ClientImpl {
|
||||
@@ -2230,6 +2271,7 @@ public:
|
||||
void set_proxy_basic_auth(const std::string &username,
|
||||
const std::string &password);
|
||||
void set_proxy_bearer_token_auth(const std::string &token);
|
||||
void set_no_proxy(const std::vector<std::string> &patterns);
|
||||
|
||||
void set_logger(Logger logger);
|
||||
void set_error_logger(ErrorLogger error_logger);
|
||||
@@ -2255,16 +2297,19 @@ protected:
|
||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||
Response &res, bool &success, Error &error);
|
||||
|
||||
bool is_proxy_enabled_for_host(const std::string &host) const;
|
||||
|
||||
// All of:
|
||||
// shutdown_ssl
|
||||
// shutdown_socket
|
||||
// close_socket
|
||||
// should ONLY be called when socket_mutex_ is locked.
|
||||
// Also, shutdown_ssl and close_socket should also NOT be called concurrently
|
||||
// with a DIFFERENT thread sending requests using that socket.
|
||||
// disconnect
|
||||
// should ONLY be called when socket_mutex_ is locked, and only when
|
||||
// no other thread is using the socket.
|
||||
virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully);
|
||||
void shutdown_socket(Socket &socket) const;
|
||||
void close_socket(Socket &socket);
|
||||
void disconnect(bool gracefully);
|
||||
|
||||
bool process_request(Stream &strm, Request &req, Response &res,
|
||||
bool close_connection, Error &error);
|
||||
@@ -2342,6 +2387,11 @@ protected:
|
||||
std::string proxy_basic_auth_password_;
|
||||
std::string proxy_bearer_token_auth_token_;
|
||||
|
||||
std::vector<detail::NoProxyEntry> no_proxy_entries_;
|
||||
|
||||
mutable detail::NormalizedTarget host_normalized_;
|
||||
mutable bool host_normalized_valid_ = false;
|
||||
|
||||
mutable std::mutex logger_mutex_;
|
||||
Logger logger_;
|
||||
ErrorLogger error_logger_;
|
||||
@@ -2602,6 +2652,7 @@ public:
|
||||
void set_proxy_basic_auth(const std::string &username,
|
||||
const std::string &password);
|
||||
void set_proxy_bearer_token_auth(const std::string &token);
|
||||
void set_no_proxy(const std::vector<std::string> &patterns);
|
||||
void set_logger(Logger logger);
|
||||
void set_error_logger(ErrorLogger error_logger);
|
||||
|
||||
@@ -3028,8 +3079,6 @@ bool parse_range_header(const std::string &s, Ranges &ranges);
|
||||
bool parse_accept_header(const std::string &s,
|
||||
std::vector<std::string> &content_types);
|
||||
|
||||
int close_socket(socket_t sock);
|
||||
|
||||
ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);
|
||||
|
||||
ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
|
||||
@@ -3799,6 +3848,7 @@ public:
|
||||
void set_socket_options(SocketOptions socket_options);
|
||||
void set_connection_timeout(time_t sec, time_t usec = 0);
|
||||
void set_interface(const std::string &intf);
|
||||
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
void set_ca_cert_path(const std::string &path);
|
||||
@@ -3833,6 +3883,9 @@ private:
|
||||
time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
|
||||
std::string interface_;
|
||||
|
||||
// Hostname-IP map
|
||||
std::map<std::string, std::string> addr_map_;
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
bool is_ssl_ = false;
|
||||
tls::ctx_t tls_ctx_ = nullptr;
|
||||
@@ -4586,7 +4639,7 @@ inline std::string sha1(const std::string &input) {
|
||||
// Pre-processing: adding padding bits
|
||||
std::string msg = input;
|
||||
uint64_t original_bit_len = static_cast<uint64_t>(msg.size()) * 8;
|
||||
msg.push_back(static_cast<char>(0x80));
|
||||
msg.push_back(static_cast<char>(0x80u));
|
||||
while (msg.size() % 64 != 56) {
|
||||
msg.push_back(0);
|
||||
}
|
||||
@@ -5422,7 +5475,7 @@ inline void mmap::close() {
|
||||
#endif
|
||||
size_ = 0;
|
||||
}
|
||||
inline int close_socket(socket_t sock) {
|
||||
inline int close_socket(socket_t sock) noexcept {
|
||||
#ifdef _WIN32
|
||||
return closesocket(sock);
|
||||
#else
|
||||
@@ -5649,7 +5702,7 @@ inline bool process_client_socket(
|
||||
return callback(strm);
|
||||
}
|
||||
|
||||
inline int shutdown_socket(socket_t sock) {
|
||||
inline int shutdown_socket(socket_t sock) noexcept {
|
||||
#ifdef _WIN32
|
||||
return shutdown(sock, SD_BOTH);
|
||||
#else
|
||||
@@ -5687,7 +5740,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows-specific implementation using GetAddrInfoEx with overlapped I/O
|
||||
OVERLAPPED overlapped = {0};
|
||||
OVERLAPPED overlapped = {};
|
||||
HANDLE event = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!event) { return EAI_FAIL; }
|
||||
|
||||
@@ -5696,7 +5749,7 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
PADDRINFOEXW result_addrinfo = nullptr;
|
||||
HANDLE cancel_handle = nullptr;
|
||||
|
||||
ADDRINFOEXW hints_ex = {0};
|
||||
ADDRINFOEXW hints_ex = {};
|
||||
if (hints) {
|
||||
hints_ex.ai_flags = hints->ai_flags;
|
||||
hints_ex.ai_family = hints->ai_family;
|
||||
@@ -8400,6 +8453,14 @@ inline void coalesce_ranges(Ranges &ranges, size_t content_length) {
|
||||
|
||||
inline bool range_error(Request &req, Response &res) {
|
||||
if (!req.ranges.empty() && 200 <= res.status && res.status < 300) {
|
||||
if (res.body.empty() && res.content_provider_ && res.content_length_ == 0) {
|
||||
req.ranges.clear();
|
||||
if (res.status == StatusCode::PartialContent_206) {
|
||||
res.status = StatusCode::OK_200;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t content_len = static_cast<ssize_t>(
|
||||
res.content_length_ ? res.content_length_ : res.body.size());
|
||||
|
||||
@@ -10505,6 +10566,176 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
|
||||
return prepare_host_string(host) + ":" + std::to_string(port);
|
||||
}
|
||||
|
||||
bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out);
|
||||
NormalizedTarget normalize_target(const std::string &host);
|
||||
bool ip_in_cidr(const IPBytes &ip, const IPBytes &net, int prefix_bits);
|
||||
bool host_matches_no_proxy(const NormalizedTarget &target,
|
||||
const std::vector<NoProxyEntry> &entries);
|
||||
|
||||
inline bool ip_in_cidr(const IPBytes &ip, const IPBytes &net, int prefix_bits) {
|
||||
if (prefix_bits < 0 || prefix_bits > 128) { return false; }
|
||||
if (prefix_bits == 0) { return true; }
|
||||
int full_bytes = prefix_bits / 8;
|
||||
int rem_bits = prefix_bits % 8;
|
||||
if (full_bytes > 0 && std::memcmp(ip.data(), net.data(),
|
||||
static_cast<size_t>(full_bytes)) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (rem_bits == 0) { return true; }
|
||||
auto i = static_cast<size_t>(full_bytes);
|
||||
auto mask = static_cast<uint8_t>(0xFFu << (8 - rem_bits));
|
||||
return (ip[i] & mask) == (net[i] & mask);
|
||||
}
|
||||
|
||||
inline bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) {
|
||||
if (token.empty()) { return false; }
|
||||
|
||||
if (token == "*") {
|
||||
out.kind = NoProxyKind::Wildcard;
|
||||
return true;
|
||||
}
|
||||
|
||||
auto slash = token.find('/');
|
||||
std::string addr_part =
|
||||
(slash == std::string::npos) ? token : token.substr(0, slash);
|
||||
std::string prefix_part =
|
||||
(slash == std::string::npos) ? std::string() : token.substr(slash + 1);
|
||||
|
||||
// A bare slash or trailing-slash CIDR like "10.0.0.0/" is malformed;
|
||||
// don't silently treat it as a /32 (or /128).
|
||||
if (slash != std::string::npos && prefix_part.empty()) { return false; }
|
||||
|
||||
// Accept the bracketed IPv6 form ("[::1]", "[fe80::]/10") as well as the
|
||||
// bare form. Brackets have no meaning for IPv4, so skip the IPv4 attempt
|
||||
// when brackets are present.
|
||||
bool bracketed = addr_part.size() >= 2 && addr_part.front() == '[' &&
|
||||
addr_part.back() == ']';
|
||||
if (bracketed) { addr_part = addr_part.substr(1, addr_part.size() - 2); }
|
||||
|
||||
if (!bracketed) {
|
||||
struct in_addr v4;
|
||||
if (inet_pton(AF_INET, addr_part.c_str(), &v4) == 1) {
|
||||
int prefix = 32;
|
||||
if (!prefix_part.empty()) {
|
||||
auto r = from_chars(prefix_part.data(),
|
||||
prefix_part.data() + prefix_part.size(), prefix);
|
||||
if (r.ec != std::errc{} ||
|
||||
r.ptr != prefix_part.data() + prefix_part.size()) {
|
||||
return false;
|
||||
}
|
||||
if (prefix < 0 || prefix > 32) { return false; }
|
||||
}
|
||||
out.kind = NoProxyKind::IPv4Cidr;
|
||||
std::memcpy(out.net.data(), &v4, sizeof(v4));
|
||||
out.prefix_bits = prefix;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
struct in6_addr v6;
|
||||
if (inet_pton(AF_INET6, addr_part.c_str(), &v6) == 1) {
|
||||
int prefix = 128;
|
||||
if (!prefix_part.empty()) {
|
||||
auto r = from_chars(prefix_part.data(),
|
||||
prefix_part.data() + prefix_part.size(), prefix);
|
||||
if (r.ec != std::errc{} ||
|
||||
r.ptr != prefix_part.data() + prefix_part.size()) {
|
||||
return false;
|
||||
}
|
||||
if (prefix < 0 || prefix > 128) { return false; }
|
||||
}
|
||||
out.kind = NoProxyKind::IPv6Cidr;
|
||||
std::memcpy(out.net.data(), &v6, sizeof(v6));
|
||||
out.prefix_bits = prefix;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bracketed entries can only be IPv6. If the IPv6 parse above failed,
|
||||
// the entry is malformed — don't fall through to the hostname branch.
|
||||
if (bracketed) { return false; }
|
||||
|
||||
// A '/' on a non-IP token means a CIDR prefix without an address. Reject.
|
||||
if (slash != std::string::npos) { return false; }
|
||||
// Port-specific entries (host:port) are not supported.
|
||||
if (token.find(':') != std::string::npos) { return false; }
|
||||
|
||||
std::string hostname = case_ignore::to_lower(token);
|
||||
while (!hostname.empty() && hostname.front() == '.') {
|
||||
hostname.erase(hostname.begin());
|
||||
}
|
||||
while (!hostname.empty() && hostname.back() == '.') {
|
||||
hostname.pop_back();
|
||||
}
|
||||
if (hostname.empty()) { return false; }
|
||||
|
||||
out.kind = NoProxyKind::HostnameSuffix;
|
||||
out.hostname_pattern = std::move(hostname);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline NormalizedTarget normalize_target(const std::string &host) {
|
||||
NormalizedTarget t;
|
||||
std::string h = host;
|
||||
|
||||
if (h.size() >= 2 && h.front() == '[' && h.back() == ']') {
|
||||
h = h.substr(1, h.size() - 2);
|
||||
}
|
||||
|
||||
// Strip a single trailing dot so "example.com." canonicalizes to
|
||||
// "example.com".
|
||||
if (!h.empty() && h.back() == '.') { h.pop_back(); }
|
||||
|
||||
t.hostname = case_ignore::to_lower(h);
|
||||
|
||||
if (!t.hostname.empty()) {
|
||||
struct in_addr v4;
|
||||
struct in6_addr v6;
|
||||
if (inet_pton(AF_INET, t.hostname.c_str(), &v4) == 1) {
|
||||
t.is_ipv4 = true;
|
||||
std::memcpy(t.ip.data(), &v4, sizeof(v4));
|
||||
} else if (inet_pton(AF_INET6, t.hostname.c_str(), &v6) == 1) {
|
||||
t.is_ipv6 = true;
|
||||
std::memcpy(t.ip.data(), &v6, sizeof(v6));
|
||||
}
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
inline bool host_matches_no_proxy(const NormalizedTarget &target,
|
||||
const std::vector<NoProxyEntry> &entries) {
|
||||
if (target.hostname.empty()) { return false; }
|
||||
for (const auto &e : entries) {
|
||||
switch (e.kind) {
|
||||
case NoProxyKind::Wildcard: return true;
|
||||
case NoProxyKind::IPv4Cidr:
|
||||
if (target.is_ipv4 && ip_in_cidr(target.ip, e.net, e.prefix_bits)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case NoProxyKind::IPv6Cidr:
|
||||
if (target.is_ipv6 && ip_in_cidr(target.ip, e.net, e.prefix_bits)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case NoProxyKind::HostnameSuffix:
|
||||
if (target.is_ipv4 || target.is_ipv6) { break; }
|
||||
if (target.hostname == e.hostname_pattern) { return true; }
|
||||
// Dot-boundary suffix match: prevents "evilexample.com" from matching
|
||||
// an entry of "example.com".
|
||||
if (target.hostname.size() > e.hostname_pattern.size() + 1) {
|
||||
auto offset = target.hostname.size() - e.hostname_pattern.size();
|
||||
if (target.hostname[offset - 1] == '.' &&
|
||||
target.hostname.compare(offset, e.hostname_pattern.size(),
|
||||
e.hostname_pattern) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
inline bool check_and_write_headers(Stream &strm, Headers &headers,
|
||||
T header_writer, Error &error) {
|
||||
@@ -10879,6 +11110,11 @@ Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Server &Server::set_start_handler(StartHandler handler) {
|
||||
start_handler_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline Server &Server::set_address_family(int family) {
|
||||
address_family_ = family;
|
||||
return *this;
|
||||
@@ -11004,7 +11240,7 @@ inline void Server::wait_until_ready() const {
|
||||
}
|
||||
}
|
||||
|
||||
inline void Server::stop() {
|
||||
inline void Server::stop() noexcept {
|
||||
if (is_running_) {
|
||||
assert(svr_sock_ != INVALID_SOCKET);
|
||||
std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
|
||||
@@ -11574,6 +11810,8 @@ inline bool Server::listen_internal() {
|
||||
is_running_ = true;
|
||||
auto se = detail::scope_exit([&]() { is_running_ = false; });
|
||||
|
||||
if (start_handler_) { start_handler_(); }
|
||||
|
||||
{
|
||||
std::unique_ptr<TaskQueue> task_queue(new_task_queue());
|
||||
|
||||
@@ -12310,6 +12548,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
|
||||
proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_;
|
||||
proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_;
|
||||
proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_;
|
||||
no_proxy_entries_ = rhs.no_proxy_entries_;
|
||||
logger_ = rhs.logger_;
|
||||
error_logger_ = rhs.error_logger_;
|
||||
|
||||
@@ -12325,8 +12564,25 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline bool
|
||||
ClientImpl::is_proxy_enabled_for_host(const std::string &host) const {
|
||||
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
|
||||
if (no_proxy_entries_.empty()) { return true; }
|
||||
// host_ is const so its normalized form is invariant; cache it. The
|
||||
// cross-host path (setup_redirect_client passing next_host) re-normalizes.
|
||||
if (host == host_) {
|
||||
if (!host_normalized_valid_) {
|
||||
host_normalized_ = detail::normalize_target(host_);
|
||||
host_normalized_valid_ = true;
|
||||
}
|
||||
return !detail::host_matches_no_proxy(host_normalized_, no_proxy_entries_);
|
||||
}
|
||||
auto target = detail::normalize_target(host);
|
||||
return !detail::host_matches_no_proxy(target, no_proxy_entries_);
|
||||
}
|
||||
|
||||
inline socket_t ClientImpl::create_client_socket(Error &error) const {
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
if (is_proxy_enabled_for_host(host_)) {
|
||||
return detail::create_client_socket(
|
||||
proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_,
|
||||
ipv6_v6only_, socket_options_, connection_timeout_sec_,
|
||||
@@ -12398,6 +12654,12 @@ inline void ClientImpl::close_socket(Socket &socket) {
|
||||
socket.sock = INVALID_SOCKET;
|
||||
}
|
||||
|
||||
inline void ClientImpl::disconnect(bool gracefully) {
|
||||
shutdown_ssl(socket_, gracefully);
|
||||
shutdown_socket(socket_);
|
||||
close_socket(socket_);
|
||||
}
|
||||
|
||||
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
|
||||
Response &res,
|
||||
bool skip_100_continue) const {
|
||||
@@ -12469,14 +12731,8 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
||||
#endif
|
||||
|
||||
if (!is_alive) {
|
||||
// Attempt to avoid sigpipe by shutting down non-gracefully if it
|
||||
// seems like the other side has already closed the connection Also,
|
||||
// there cannot be any requests in flight from other threads since we
|
||||
// locked request_mutex_, so safe to close everything immediately
|
||||
const bool shutdown_gracefully = false;
|
||||
shutdown_ssl(socket_, shutdown_gracefully);
|
||||
shutdown_socket(socket_);
|
||||
close_socket(socket_);
|
||||
// Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
|
||||
disconnect(/*gracefully=*/false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12526,9 +12782,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
||||
|
||||
if (socket_should_be_closed_when_request_is_done_ || close_connection ||
|
||||
!ret) {
|
||||
shutdown_ssl(socket_, true);
|
||||
shutdown_socket(socket_);
|
||||
close_socket(socket_);
|
||||
disconnect(/*gracefully=*/true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12641,11 +12895,7 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!is_alive) {
|
||||
shutdown_ssl(socket_, false);
|
||||
shutdown_socket(socket_);
|
||||
close_socket(socket_);
|
||||
}
|
||||
if (!is_alive) { disconnect(/*gracefully=*/false); }
|
||||
}
|
||||
|
||||
if (!is_alive) {
|
||||
@@ -12937,7 +13187,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
||||
|
||||
bool ret;
|
||||
|
||||
if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {
|
||||
if (!is_ssl() && is_proxy_enabled_for_host(host_)) {
|
||||
auto req2 = req;
|
||||
req2.path = "http://" +
|
||||
detail::make_host_and_port_string(host_, port_, false) +
|
||||
@@ -12961,9 +13211,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
||||
// to call it from a different thread since it's a thread-safety issue
|
||||
// to do these things to the socket if another thread is using the socket.
|
||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||
shutdown_ssl(socket_, true);
|
||||
shutdown_socket(socket_);
|
||||
close_socket(socket_);
|
||||
disconnect(/*gracefully=*/true);
|
||||
}
|
||||
|
||||
if (300 < res.status && res.status < 400 && follow_location_) {
|
||||
@@ -12976,6 +13224,14 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
||||
res.status == StatusCode::ProxyAuthenticationRequired_407) &&
|
||||
req.authorization_count_ < 5) {
|
||||
auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;
|
||||
|
||||
// Only retry when the 407 actually came from a proxy hop: plain HTTP
|
||||
// through an enabled proxy. HTTPS via CONNECT tunnels the 407 from the
|
||||
// origin (#2457); direct/bypassed origins have no proxy hop at all.
|
||||
if (is_proxy && !(!is_ssl() && is_proxy_enabled_for_host(host_))) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
const auto &username =
|
||||
is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
|
||||
const auto &password =
|
||||
@@ -13143,13 +13399,13 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
|
||||
// host. This function is only called for cross-host redirects; same-host
|
||||
// redirects are handled directly in ClientImpl::redirect().
|
||||
|
||||
// Setup proxy configuration (CRITICAL ORDER - proxy must be set
|
||||
// before proxy auth)
|
||||
// Copy the proxy configuration unconditionally; the per-target bypass is
|
||||
// re-evaluated at send time, so a later hop to a non-bypassed host can
|
||||
// still use the proxy.
|
||||
client.no_proxy_entries_ = no_proxy_entries_;
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
// First set proxy host and port
|
||||
client.set_proxy(proxy_host_, proxy_port_);
|
||||
|
||||
// Then set proxy authentication (order matters!)
|
||||
if (!proxy_basic_auth_username_.empty()) {
|
||||
client.set_proxy_basic_auth(proxy_basic_auth_username_,
|
||||
proxy_basic_auth_password_);
|
||||
@@ -13240,14 +13496,6 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
}
|
||||
}
|
||||
|
||||
if (!proxy_basic_auth_username_.empty() &&
|
||||
!proxy_basic_auth_password_.empty()) {
|
||||
if (!req.has_header("Proxy-Authorization")) {
|
||||
req.headers.insert(make_basic_authentication_header(
|
||||
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
|
||||
}
|
||||
}
|
||||
|
||||
if (!bearer_token_auth_token_.empty()) {
|
||||
if (!req.has_header("Authorization")) {
|
||||
req.headers.insert(make_bearer_token_authentication_header(
|
||||
@@ -13255,8 +13503,18 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
}
|
||||
}
|
||||
|
||||
if (!proxy_bearer_token_auth_token_.empty()) {
|
||||
if (!req.has_header("Proxy-Authorization")) {
|
||||
// Proxy-Authorization is only sent when the proxy is actually used for
|
||||
// this target — otherwise NO_PROXY-matched requests would leak proxy
|
||||
// credentials directly to the destination server.
|
||||
if (is_proxy_enabled_for_host(host_)) {
|
||||
if (!proxy_basic_auth_username_.empty() &&
|
||||
!proxy_basic_auth_password_.empty() &&
|
||||
!req.has_header("Proxy-Authorization")) {
|
||||
req.headers.insert(make_basic_authentication_header(
|
||||
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
|
||||
}
|
||||
if (!proxy_bearer_token_auth_token_.empty() &&
|
||||
!req.has_header("Proxy-Authorization")) {
|
||||
req.headers.insert(make_bearer_token_authentication_header(
|
||||
proxy_bearer_token_auth_token_, true));
|
||||
}
|
||||
@@ -13566,7 +13824,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (is_ssl() && !expect_100_continue) {
|
||||
auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1;
|
||||
auto is_proxy_enabled = is_proxy_enabled_for_host(host_);
|
||||
if (!is_proxy_enabled) {
|
||||
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
|
||||
error = Error::SSLPeerCouldBeClosed_;
|
||||
@@ -13577,13 +13835,28 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
}
|
||||
#endif
|
||||
|
||||
// Handle Expect: 100-continue with timeout
|
||||
if (expect_100_continue && CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) {
|
||||
time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000;
|
||||
time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000;
|
||||
auto ret = detail::select_read(strm.socket(), sec, usec);
|
||||
if (ret <= 0) {
|
||||
// Timeout or error: send body anyway (server didn't respond in time)
|
||||
// Handle Expect: 100-continue.
|
||||
//
|
||||
// Wait for an interim/early response by attempting to read the status line
|
||||
// under a short timeout, instead of trusting raw socket readability. Over
|
||||
// TLS, post-handshake records (e.g. session tickets) make the socket
|
||||
// readable without any HTTP response being available; relying on
|
||||
// `select_read` there caused the body to be withheld forever and the
|
||||
// request to fail with `Read` (#2458). If no status line arrives within the
|
||||
// timeout, send the body anyway (matching curl's behavior).
|
||||
auto status_line_read = false;
|
||||
if (expect_100_continue && write_request_success) {
|
||||
if (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND > 0) {
|
||||
time_t sec = CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND / 1000;
|
||||
time_t usec = (CPPHTTPLIB_EXPECT_100_TIMEOUT_MSECOND % 1000) * 1000;
|
||||
strm.set_read_timeout(sec, usec);
|
||||
status_line_read = read_response_line(strm, req, res, false);
|
||||
strm.set_read_timeout(read_timeout_sec_, read_timeout_usec_);
|
||||
}
|
||||
|
||||
if (!status_line_read) {
|
||||
// No interim response within the timeout: send the body and handle the
|
||||
// response as usual.
|
||||
if (!write_request_body(strm, req, error)) { return false; }
|
||||
expect_100_continue = false; // Switch to normal response handling
|
||||
}
|
||||
@@ -13591,7 +13864,8 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
|
||||
// Receive response and headers
|
||||
// When using Expect: 100-continue, don't auto-skip `100 Continue` response
|
||||
if (!read_response_line(strm, req, res, !expect_100_continue) ||
|
||||
if ((!status_line_read &&
|
||||
!read_response_line(strm, req, res, !expect_100_continue)) ||
|
||||
!detail::read_headers(strm, res.headers)) {
|
||||
if (write_request_success) { error = Error::Read; }
|
||||
output_error_log(error, &req);
|
||||
@@ -14573,10 +14847,7 @@ inline void ClientImpl::stop() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, still holding the mutex, we can shut everything down ourselves
|
||||
shutdown_ssl(socket_, true);
|
||||
shutdown_socket(socket_);
|
||||
close_socket(socket_);
|
||||
disconnect(/*gracefully=*/true);
|
||||
}
|
||||
|
||||
inline std::string ClientImpl::host() const { return host_; }
|
||||
@@ -14667,6 +14938,8 @@ inline void ClientImpl::set_interface(const std::string &intf) {
|
||||
inline void ClientImpl::set_proxy(const std::string &host, int port) {
|
||||
proxy_host_ = host;
|
||||
proxy_port_ = port;
|
||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||
disconnect(/*gracefully=*/true);
|
||||
}
|
||||
|
||||
inline void ClientImpl::set_proxy_basic_auth(const std::string &username,
|
||||
@@ -14679,6 +14952,22 @@ inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {
|
||||
proxy_bearer_token_auth_token_ = token;
|
||||
}
|
||||
|
||||
inline void ClientImpl::set_no_proxy(const std::vector<std::string> &patterns) {
|
||||
std::vector<detail::NoProxyEntry> parsed;
|
||||
parsed.reserve(patterns.size());
|
||||
for (const auto &p : patterns) {
|
||||
auto trimmed = detail::trim_copy(p);
|
||||
if (trimmed.empty()) { continue; }
|
||||
detail::NoProxyEntry entry;
|
||||
if (detail::parse_no_proxy_entry(trimmed, entry)) {
|
||||
parsed.push_back(std::move(entry));
|
||||
}
|
||||
}
|
||||
no_proxy_entries_ = std::move(parsed);
|
||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||
disconnect(/*gracefully=*/true);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
inline void ClientImpl::set_digest_auth(const std::string &username,
|
||||
const std::string &password) {
|
||||
@@ -15380,6 +15669,9 @@ inline void Client::set_proxy_basic_auth(const std::string &username,
|
||||
inline void Client::set_proxy_bearer_token_auth(const std::string &token) {
|
||||
cli_->set_proxy_bearer_token_auth(token);
|
||||
}
|
||||
inline void Client::set_no_proxy(const std::vector<std::string> &patterns) {
|
||||
cli_->set_no_proxy(patterns);
|
||||
}
|
||||
|
||||
inline void Client::set_logger(Logger logger) {
|
||||
cli_->set_logger(std::move(logger));
|
||||
@@ -15609,7 +15901,7 @@ inline bool SSLClient::setup_proxy_connection(
|
||||
Socket &socket,
|
||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||
Response &res, bool &success, Error &error) {
|
||||
if (proxy_host_.empty() || proxy_port_ == -1) { return true; }
|
||||
if (!is_proxy_enabled_for_host(host_)) { return true; }
|
||||
|
||||
if (!connect_with_proxy(socket, start_time, res, success, error)) {
|
||||
return false;
|
||||
@@ -15722,7 +16014,7 @@ inline bool SSLClient::connect_with_proxy(
|
||||
inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) {
|
||||
if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; }
|
||||
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) { return true; }
|
||||
if (is_proxy_enabled_for_host(host_)) { return true; }
|
||||
|
||||
if (!initialize_ssl(socket, error)) {
|
||||
shutdown_socket(socket);
|
||||
@@ -16145,9 +16437,18 @@ inline bool enumerate_windows_system_certs(Callback cb) {
|
||||
template <typename Callback>
|
||||
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
||||
bool loaded = false;
|
||||
CFArrayRef certs = nullptr;
|
||||
OSStatus status = SecTrustCopyAnchorCertificates(&certs);
|
||||
if (status == errSecSuccess && certs) {
|
||||
const SecTrustSettingsDomain domains[] = {
|
||||
kSecTrustSettingsDomainSystem,
|
||||
kSecTrustSettingsDomainAdmin,
|
||||
kSecTrustSettingsDomainUser,
|
||||
};
|
||||
for (auto domain : domains) {
|
||||
CFArrayRef certs = nullptr;
|
||||
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
|
||||
if (status != errSecSuccess || !certs) {
|
||||
if (certs) CFRelease(certs);
|
||||
continue;
|
||||
}
|
||||
CFIndex count = CFArrayGetCount(certs);
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
SecCertificateRef cert =
|
||||
@@ -16510,28 +16811,36 @@ inline bool load_system_certs(ctx_t ctx) {
|
||||
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
||||
if (!store) return false;
|
||||
|
||||
CFArrayRef certs = nullptr;
|
||||
if (SecTrustCopyAnchorCertificates(&certs) != errSecSuccess || !certs) {
|
||||
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||
}
|
||||
|
||||
bool loaded_any = false;
|
||||
auto count = CFArrayGetCount(certs);
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
auto cert = reinterpret_cast<SecCertificateRef>(
|
||||
const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
|
||||
CFDataRef der = SecCertificateCopyData(cert);
|
||||
if (der) {
|
||||
const unsigned char *data = CFDataGetBytePtr(der);
|
||||
auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
|
||||
if (x509) {
|
||||
if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
|
||||
X509_free(x509);
|
||||
}
|
||||
CFRelease(der);
|
||||
const SecTrustSettingsDomain domains[] = {
|
||||
kSecTrustSettingsDomainSystem,
|
||||
kSecTrustSettingsDomainAdmin,
|
||||
kSecTrustSettingsDomainUser,
|
||||
};
|
||||
for (auto domain : domains) {
|
||||
CFArrayRef certs = nullptr;
|
||||
if (SecTrustSettingsCopyCertificates(domain, &certs) != errSecSuccess ||
|
||||
!certs) {
|
||||
if (certs) CFRelease(certs);
|
||||
continue;
|
||||
}
|
||||
auto count = CFArrayGetCount(certs);
|
||||
for (CFIndex i = 0; i < count; i++) {
|
||||
auto cert = reinterpret_cast<SecCertificateRef>(
|
||||
const_cast<void *>(CFArrayGetValueAtIndex(certs, i)));
|
||||
CFDataRef der = SecCertificateCopyData(cert);
|
||||
if (der) {
|
||||
const unsigned char *data = CFDataGetBytePtr(der);
|
||||
auto x509 = d2i_X509(nullptr, &data, CFDataGetLength(der));
|
||||
if (x509) {
|
||||
if (X509_STORE_add_cert(store, x509) == 1) { loaded_any = true; }
|
||||
X509_free(x509);
|
||||
}
|
||||
CFRelease(der);
|
||||
}
|
||||
}
|
||||
CFRelease(certs);
|
||||
}
|
||||
CFRelease(certs);
|
||||
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||
#else
|
||||
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||
@@ -19957,6 +20266,7 @@ inline WebSocketClient::WebSocketClient(
|
||||
if (!uc.port.empty() && !detail::parse_port(uc.port, port_)) { return; }
|
||||
|
||||
path_ = std::move(uc.path);
|
||||
if (!uc.query.empty()) { path_ += uc.query; }
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
is_ssl_ = is_ssl;
|
||||
@@ -20021,9 +20331,14 @@ inline bool WebSocketClient::connect() {
|
||||
if (!is_valid_) { return false; }
|
||||
shutdown_and_close();
|
||||
|
||||
// Check is custom IP specified for host_
|
||||
std::string ip;
|
||||
auto it = addr_map_.find(host_);
|
||||
if (it != addr_map_.end()) { ip = it->second; }
|
||||
|
||||
Error error;
|
||||
sock_ = detail::create_client_socket(
|
||||
host_, std::string(), port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||
host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
|
||||
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||
write_timeout_usec_, interface_, error);
|
||||
@@ -20118,6 +20433,11 @@ inline void WebSocketClient::set_interface(const std::string &intf) {
|
||||
interface_ = intf;
|
||||
}
|
||||
|
||||
inline void WebSocketClient::set_hostname_addr_map(
|
||||
std::map<std::string, std::string> addr_map) {
|
||||
addr_map_ = std::move(addr_map);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
|
||||
inline void WebSocketClient::set_ca_cert_path(const std::string &path) {
|
||||
|
||||
+18
-3
@@ -62,7 +62,7 @@ HEAD_SHORT=$(git rev-parse --short HEAD)
|
||||
echo " Latest commit: $HEAD_SHORT"
|
||||
|
||||
# Fetch all workflow runs for the HEAD commit
|
||||
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,conclusion,headSha)
|
||||
RUNS=$(gh run list --commit "$HEAD_SHA" --json name,status,conclusion,headSha)
|
||||
|
||||
NUM_RUNS=$(echo "$RUNS" | jq 'length')
|
||||
|
||||
@@ -75,8 +75,17 @@ fi
|
||||
echo " Found $NUM_RUNS workflow run(s):"
|
||||
|
||||
FAILED=0
|
||||
RUNNING=0
|
||||
ABIDIFF_PASSED=0
|
||||
while IFS=$'\t' read -r name conclusion; do
|
||||
while IFS=$'\t' read -r name status conclusion; do
|
||||
# A run that hasn't completed yet has an empty conclusion; don't treat it
|
||||
# as a failure — the release should wait until CI finishes.
|
||||
if [ "$status" != "completed" ]; then
|
||||
echo " [ .. ] $name (still running)"
|
||||
RUNNING=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$name" == *abidiff* ]] || [[ "$name" == *abi* && "$name" != *stability* ]]; then
|
||||
if [ "$conclusion" = "success" ]; then
|
||||
echo " [ OK ] $name"
|
||||
@@ -94,7 +103,13 @@ while IFS=$'\t' read -r name conclusion; do
|
||||
echo " [FAIL] $name ($conclusion)"
|
||||
FAILED=1
|
||||
fi
|
||||
done < <(echo "$RUNS" | jq -r '.[] | [.name, .conclusion] | @tsv')
|
||||
done < <(echo "$RUNS" | jq -r '.[] | [.name, .status, .conclusion] | @tsv')
|
||||
|
||||
if [ "$RUNNING" -eq 1 ]; then
|
||||
echo ""
|
||||
echo "Error: Some CI checks are still running. Wait for them to complete before releasing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$FAILED" -eq 1 ]; then
|
||||
echo ""
|
||||
|
||||
+1048
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user