mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-21 11:39:48 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008e107d0f | ||
|
|
d278f965cc | ||
|
|
4c4b62dd7e | ||
|
|
b1792ef29c | ||
|
|
0f3d063f0a | ||
|
|
0d7d637466 | ||
|
|
1ff0c8588d | ||
|
|
b1cc8095a8 |
No files matched your search
@@ -120,6 +120,155 @@ jobs:
|
|||||||
- 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
|
||||||
|
|
||||||
|
# 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.
|
# Reproducer for https://github.com/yhirose/cpp-httplib/issues/2431.
|
||||||
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
|
# On Linux/glibc, getaddrinfo_with_timeout() schedules an asynchronous
|
||||||
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
|
# DNS lookup with getaddrinfo_a(GAI_NOWAIT) using a stack-local gaicb.
|
||||||
@@ -250,6 +399,54 @@ jobs:
|
|||||||
- 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
|
||||||
|
|
||||||
|
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:
|
windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
if: >
|
if: >
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
|
|||||||
> [!NOTE]
|
> [!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.
|
> **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++
|
```c++
|
||||||
// Use either OpenSSL, Mbed TLS, or wolfSSL
|
// Use either OpenSSL, Mbed TLS, or wolfSSL
|
||||||
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
|
#define CPPHTTPLIB_OPENSSL_SUPPORT // or CPPHTTPLIB_MBEDTLS_SUPPORT or CPPHTTPLIB_WOLFSSL_SUPPORT
|
||||||
@@ -1178,6 +1181,17 @@ cli.set_proxy_bearer_token_auth("pass");
|
|||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> OpenSSL is required for Digest Authentication.
|
> 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
|
### Range
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ if(@HTTPLIB_IS_USING_ZSTD@)
|
|||||||
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
|
if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED)
|
||||||
set(httplib_fd_zstd_required_arg REQUIRED)
|
set(httplib_fd_zstd_required_arg REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
find_package(zstd QUIET)
|
find_package(zstd 1.5.6 CONFIG QUIET)
|
||||||
if(NOT zstd_FOUND)
|
if(NOT zstd_FOUND)
|
||||||
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
|
find_package(PkgConfig ${httplib_fd_zstd_quiet_arg} ${httplib_fd_zstd_required_arg})
|
||||||
if(PKG_CONFIG_FOUND)
|
if(PKG_CONFIG_FOUND)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.45.0"
|
version = "0.46.0"
|
||||||
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."
|
||||||
|
|||||||
@@ -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.
|
`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`ヘッダーが送られます。
|
プロキシには`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
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.45.0"
|
#define CPPHTTPLIB_VERSION "0.46.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x002d00"
|
#define CPPHTTPLIB_VERSION_NUM "0x002e00"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -339,16 +339,26 @@ using socket_t = int;
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
// On macOS with a TLS backend, enable Keychain root certificates by default
|
// 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__) && \
|
#if defined(__APPLE__) && defined(__clang__) && \
|
||||||
!defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \
|
!defined(CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES) && \
|
||||||
(defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \
|
(defined(CPPHTTPLIB_OPENSSL_SUPPORT) || \
|
||||||
defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \
|
defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || \
|
||||||
defined(CPPHTTPLIB_WOLFSSL_SUPPORT))
|
defined(CPPHTTPLIB_WOLFSSL_SUPPORT))
|
||||||
|
#if TARGET_OS_OSX
|
||||||
#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#ifndef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||||
#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#define CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||||
#endif
|
#endif
|
||||||
#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
|
// On Windows, enable Schannel certificate verification by default
|
||||||
// unless the user explicitly opts out.
|
// unless the user explicitly opts out.
|
||||||
@@ -382,7 +392,7 @@ using socket_t = int;
|
|||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||||
#if TARGET_OS_MAC
|
#if TARGET_OS_OSX
|
||||||
#include <Security/Security.h>
|
#include <Security/Security.h>
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
@@ -430,7 +440,7 @@ using socket_t = int;
|
|||||||
#endif
|
#endif
|
||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||||
#if TARGET_OS_MAC
|
#if TARGET_OS_OSX
|
||||||
#include <Security/Security.h>
|
#include <Security/Security.h>
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
@@ -473,7 +483,7 @@ using socket_t = int;
|
|||||||
#endif
|
#endif
|
||||||
#endif // _WIN32
|
#endif // _WIN32
|
||||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||||
#if TARGET_OS_MAC
|
#if TARGET_OS_OSX
|
||||||
#include <Security/Security.h>
|
#include <Security/Security.h>
|
||||||
#endif
|
#endif
|
||||||
#endif
|
#endif
|
||||||
@@ -1597,7 +1607,7 @@ private:
|
|||||||
std::regex regex_;
|
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);
|
ssize_t write_headers(Stream &strm, const Headers &headers);
|
||||||
|
|
||||||
@@ -1734,7 +1744,7 @@ public:
|
|||||||
|
|
||||||
bool is_running() const;
|
bool is_running() const;
|
||||||
void wait_until_ready() const;
|
void wait_until_ready() const;
|
||||||
void stop();
|
void stop() noexcept;
|
||||||
void decommission();
|
void decommission();
|
||||||
|
|
||||||
std::function<TaskQueue *(void)> new_task_queue;
|
std::function<TaskQueue *(void)> new_task_queue;
|
||||||
@@ -2014,6 +2024,31 @@ inline ssize_t read_body_content(Stream *stream, BodyReader &br, char *buf,
|
|||||||
|
|
||||||
class decompressor;
|
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
|
} // namespace detail
|
||||||
|
|
||||||
class ClientImpl {
|
class ClientImpl {
|
||||||
@@ -2230,6 +2265,7 @@ public:
|
|||||||
void set_proxy_basic_auth(const std::string &username,
|
void set_proxy_basic_auth(const std::string &username,
|
||||||
const std::string &password);
|
const std::string &password);
|
||||||
void set_proxy_bearer_token_auth(const std::string &token);
|
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_logger(Logger logger);
|
||||||
void set_error_logger(ErrorLogger error_logger);
|
void set_error_logger(ErrorLogger error_logger);
|
||||||
@@ -2255,16 +2291,19 @@ protected:
|
|||||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||||
Response &res, bool &success, Error &error);
|
Response &res, bool &success, Error &error);
|
||||||
|
|
||||||
|
bool is_proxy_enabled_for_host(const std::string &host) const;
|
||||||
|
|
||||||
// All of:
|
// All of:
|
||||||
// shutdown_ssl
|
// shutdown_ssl
|
||||||
// shutdown_socket
|
// shutdown_socket
|
||||||
// close_socket
|
// close_socket
|
||||||
// should ONLY be called when socket_mutex_ is locked.
|
// disconnect
|
||||||
// Also, shutdown_ssl and close_socket should also NOT be called concurrently
|
// should ONLY be called when socket_mutex_ is locked, and only when
|
||||||
// with a DIFFERENT thread sending requests using that socket.
|
// no other thread is using the socket.
|
||||||
virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully);
|
virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully);
|
||||||
void shutdown_socket(Socket &socket) const;
|
void shutdown_socket(Socket &socket) const;
|
||||||
void close_socket(Socket &socket);
|
void close_socket(Socket &socket);
|
||||||
|
void disconnect(bool gracefully);
|
||||||
|
|
||||||
bool process_request(Stream &strm, Request &req, Response &res,
|
bool process_request(Stream &strm, Request &req, Response &res,
|
||||||
bool close_connection, Error &error);
|
bool close_connection, Error &error);
|
||||||
@@ -2342,6 +2381,11 @@ protected:
|
|||||||
std::string proxy_basic_auth_password_;
|
std::string proxy_basic_auth_password_;
|
||||||
std::string proxy_bearer_token_auth_token_;
|
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_;
|
mutable std::mutex logger_mutex_;
|
||||||
Logger logger_;
|
Logger logger_;
|
||||||
ErrorLogger error_logger_;
|
ErrorLogger error_logger_;
|
||||||
@@ -2602,6 +2646,7 @@ public:
|
|||||||
void set_proxy_basic_auth(const std::string &username,
|
void set_proxy_basic_auth(const std::string &username,
|
||||||
const std::string &password);
|
const std::string &password);
|
||||||
void set_proxy_bearer_token_auth(const std::string &token);
|
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_logger(Logger logger);
|
||||||
void set_error_logger(ErrorLogger error_logger);
|
void set_error_logger(ErrorLogger error_logger);
|
||||||
|
|
||||||
@@ -3028,8 +3073,6 @@ bool parse_range_header(const std::string &s, Ranges &ranges);
|
|||||||
bool parse_accept_header(const std::string &s,
|
bool parse_accept_header(const std::string &s,
|
||||||
std::vector<std::string> &content_types);
|
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 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);
|
ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
|
||||||
@@ -5422,7 +5465,7 @@ inline void mmap::close() {
|
|||||||
#endif
|
#endif
|
||||||
size_ = 0;
|
size_ = 0;
|
||||||
}
|
}
|
||||||
inline int close_socket(socket_t sock) {
|
inline int close_socket(socket_t sock) noexcept {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return closesocket(sock);
|
return closesocket(sock);
|
||||||
#else
|
#else
|
||||||
@@ -5649,7 +5692,7 @@ inline bool process_client_socket(
|
|||||||
return callback(strm);
|
return callback(strm);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline int shutdown_socket(socket_t sock) {
|
inline int shutdown_socket(socket_t sock) noexcept {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
return shutdown(sock, SD_BOTH);
|
return shutdown(sock, SD_BOTH);
|
||||||
#else
|
#else
|
||||||
@@ -10505,6 +10548,176 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
|
|||||||
return prepare_host_string(host) + ":" + std::to_string(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>
|
template <typename T>
|
||||||
inline bool check_and_write_headers(Stream &strm, Headers &headers,
|
inline bool check_and_write_headers(Stream &strm, Headers &headers,
|
||||||
T header_writer, Error &error) {
|
T header_writer, Error &error) {
|
||||||
@@ -11004,7 +11217,7 @@ inline void Server::wait_until_ready() const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void Server::stop() {
|
inline void Server::stop() noexcept {
|
||||||
if (is_running_) {
|
if (is_running_) {
|
||||||
assert(svr_sock_ != INVALID_SOCKET);
|
assert(svr_sock_ != INVALID_SOCKET);
|
||||||
std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
|
std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
|
||||||
@@ -12310,6 +12523,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
|
|||||||
proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_;
|
proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_;
|
||||||
proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_;
|
proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_;
|
||||||
proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_;
|
proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_;
|
||||||
|
no_proxy_entries_ = rhs.no_proxy_entries_;
|
||||||
logger_ = rhs.logger_;
|
logger_ = rhs.logger_;
|
||||||
error_logger_ = rhs.error_logger_;
|
error_logger_ = rhs.error_logger_;
|
||||||
|
|
||||||
@@ -12325,8 +12539,25 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
|
|||||||
#endif
|
#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 {
|
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(
|
return detail::create_client_socket(
|
||||||
proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_,
|
proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_,
|
||||||
ipv6_v6only_, socket_options_, connection_timeout_sec_,
|
ipv6_v6only_, socket_options_, connection_timeout_sec_,
|
||||||
@@ -12398,6 +12629,12 @@ inline void ClientImpl::close_socket(Socket &socket) {
|
|||||||
socket.sock = INVALID_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,
|
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
|
||||||
Response &res,
|
Response &res,
|
||||||
bool skip_100_continue) const {
|
bool skip_100_continue) const {
|
||||||
@@ -12469,14 +12706,8 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!is_alive) {
|
if (!is_alive) {
|
||||||
// Attempt to avoid sigpipe by shutting down non-gracefully if it
|
// Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
|
||||||
// seems like the other side has already closed the connection Also,
|
disconnect(/*gracefully=*/false);
|
||||||
// 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_);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12526,9 +12757,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
|||||||
|
|
||||||
if (socket_should_be_closed_when_request_is_done_ || close_connection ||
|
if (socket_should_be_closed_when_request_is_done_ || close_connection ||
|
||||||
!ret) {
|
!ret) {
|
||||||
shutdown_ssl(socket_, true);
|
disconnect(/*gracefully=*/true);
|
||||||
shutdown_socket(socket_);
|
|
||||||
close_socket(socket_);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -12641,11 +12870,7 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
if (!is_alive) {
|
if (!is_alive) { disconnect(/*gracefully=*/false); }
|
||||||
shutdown_ssl(socket_, false);
|
|
||||||
shutdown_socket(socket_);
|
|
||||||
close_socket(socket_);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_alive) {
|
if (!is_alive) {
|
||||||
@@ -12937,7 +13162,7 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
|||||||
|
|
||||||
bool ret;
|
bool ret;
|
||||||
|
|
||||||
if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) {
|
if (!is_ssl() && is_proxy_enabled_for_host(host_)) {
|
||||||
auto req2 = req;
|
auto req2 = req;
|
||||||
req2.path = "http://" +
|
req2.path = "http://" +
|
||||||
detail::make_host_and_port_string(host_, port_, false) +
|
detail::make_host_and_port_string(host_, port_, false) +
|
||||||
@@ -12961,9 +13186,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 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.
|
// to do these things to the socket if another thread is using the socket.
|
||||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||||
shutdown_ssl(socket_, true);
|
disconnect(/*gracefully=*/true);
|
||||||
shutdown_socket(socket_);
|
|
||||||
close_socket(socket_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (300 < res.status && res.status < 400 && follow_location_) {
|
if (300 < res.status && res.status < 400 && follow_location_) {
|
||||||
@@ -12976,6 +13199,14 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
|||||||
res.status == StatusCode::ProxyAuthenticationRequired_407) &&
|
res.status == StatusCode::ProxyAuthenticationRequired_407) &&
|
||||||
req.authorization_count_ < 5) {
|
req.authorization_count_ < 5) {
|
||||||
auto is_proxy = res.status == StatusCode::ProxyAuthenticationRequired_407;
|
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 =
|
const auto &username =
|
||||||
is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
|
is_proxy ? proxy_digest_auth_username_ : digest_auth_username_;
|
||||||
const auto &password =
|
const auto &password =
|
||||||
@@ -13143,13 +13374,13 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
|
|||||||
// host. This function is only called for cross-host redirects; same-host
|
// host. This function is only called for cross-host redirects; same-host
|
||||||
// redirects are handled directly in ClientImpl::redirect().
|
// redirects are handled directly in ClientImpl::redirect().
|
||||||
|
|
||||||
// Setup proxy configuration (CRITICAL ORDER - proxy must be set
|
// Copy the proxy configuration unconditionally; the per-target bypass is
|
||||||
// before proxy auth)
|
// 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) {
|
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||||
// First set proxy host and port
|
|
||||||
client.set_proxy(proxy_host_, proxy_port_);
|
client.set_proxy(proxy_host_, proxy_port_);
|
||||||
|
|
||||||
// Then set proxy authentication (order matters!)
|
|
||||||
if (!proxy_basic_auth_username_.empty()) {
|
if (!proxy_basic_auth_username_.empty()) {
|
||||||
client.set_proxy_basic_auth(proxy_basic_auth_username_,
|
client.set_proxy_basic_auth(proxy_basic_auth_username_,
|
||||||
proxy_basic_auth_password_);
|
proxy_basic_auth_password_);
|
||||||
@@ -13240,14 +13471,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 (!bearer_token_auth_token_.empty()) {
|
||||||
if (!req.has_header("Authorization")) {
|
if (!req.has_header("Authorization")) {
|
||||||
req.headers.insert(make_bearer_token_authentication_header(
|
req.headers.insert(make_bearer_token_authentication_header(
|
||||||
@@ -13255,8 +13478,18 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!proxy_bearer_token_auth_token_.empty()) {
|
// Proxy-Authorization is only sent when the proxy is actually used for
|
||||||
if (!req.has_header("Proxy-Authorization")) {
|
// 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(
|
req.headers.insert(make_bearer_token_authentication_header(
|
||||||
proxy_bearer_token_auth_token_, true));
|
proxy_bearer_token_auth_token_, true));
|
||||||
}
|
}
|
||||||
@@ -13566,7 +13799,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
|||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
if (is_ssl() && !expect_100_continue) {
|
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 (!is_proxy_enabled) {
|
||||||
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
|
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
|
||||||
error = Error::SSLPeerCouldBeClosed_;
|
error = Error::SSLPeerCouldBeClosed_;
|
||||||
@@ -14573,10 +14806,7 @@ inline void ClientImpl::stop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, still holding the mutex, we can shut everything down ourselves
|
disconnect(/*gracefully=*/true);
|
||||||
shutdown_ssl(socket_, true);
|
|
||||||
shutdown_socket(socket_);
|
|
||||||
close_socket(socket_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline std::string ClientImpl::host() const { return host_; }
|
inline std::string ClientImpl::host() const { return host_; }
|
||||||
@@ -14667,6 +14897,8 @@ inline void ClientImpl::set_interface(const std::string &intf) {
|
|||||||
inline void ClientImpl::set_proxy(const std::string &host, int port) {
|
inline void ClientImpl::set_proxy(const std::string &host, int port) {
|
||||||
proxy_host_ = host;
|
proxy_host_ = host;
|
||||||
proxy_port_ = port;
|
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,
|
inline void ClientImpl::set_proxy_basic_auth(const std::string &username,
|
||||||
@@ -14679,6 +14911,22 @@ inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) {
|
|||||||
proxy_bearer_token_auth_token_ = 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
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
inline void ClientImpl::set_digest_auth(const std::string &username,
|
inline void ClientImpl::set_digest_auth(const std::string &username,
|
||||||
const std::string &password) {
|
const std::string &password) {
|
||||||
@@ -15380,6 +15628,9 @@ inline void Client::set_proxy_basic_auth(const std::string &username,
|
|||||||
inline void Client::set_proxy_bearer_token_auth(const std::string &token) {
|
inline void Client::set_proxy_bearer_token_auth(const std::string &token) {
|
||||||
cli_->set_proxy_bearer_token_auth(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) {
|
inline void Client::set_logger(Logger logger) {
|
||||||
cli_->set_logger(std::move(logger));
|
cli_->set_logger(std::move(logger));
|
||||||
@@ -15609,7 +15860,7 @@ inline bool SSLClient::setup_proxy_connection(
|
|||||||
Socket &socket,
|
Socket &socket,
|
||||||
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||||
Response &res, bool &success, Error &error) {
|
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)) {
|
if (!connect_with_proxy(socket, start_time, res, success, error)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -15722,7 +15973,7 @@ inline bool SSLClient::connect_with_proxy(
|
|||||||
inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) {
|
inline bool SSLClient::ensure_socket_connection(Socket &socket, Error &error) {
|
||||||
if (!ClientImpl::ensure_socket_connection(socket, error)) { return false; }
|
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)) {
|
if (!initialize_ssl(socket, error)) {
|
||||||
shutdown_socket(socket);
|
shutdown_socket(socket);
|
||||||
@@ -16145,9 +16396,18 @@ inline bool enumerate_windows_system_certs(Callback cb) {
|
|||||||
template <typename Callback>
|
template <typename Callback>
|
||||||
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
||||||
bool loaded = false;
|
bool loaded = false;
|
||||||
|
const SecTrustSettingsDomain domains[] = {
|
||||||
|
kSecTrustSettingsDomainSystem,
|
||||||
|
kSecTrustSettingsDomainAdmin,
|
||||||
|
kSecTrustSettingsDomainUser,
|
||||||
|
};
|
||||||
|
for (auto domain : domains) {
|
||||||
CFArrayRef certs = nullptr;
|
CFArrayRef certs = nullptr;
|
||||||
OSStatus status = SecTrustCopyAnchorCertificates(&certs);
|
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
|
||||||
if (status == errSecSuccess && certs) {
|
if (status != errSecSuccess || !certs) {
|
||||||
|
if (certs) CFRelease(certs);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
CFIndex count = CFArrayGetCount(certs);
|
CFIndex count = CFArrayGetCount(certs);
|
||||||
for (CFIndex i = 0; i < count; i++) {
|
for (CFIndex i = 0; i < count; i++) {
|
||||||
SecCertificateRef cert =
|
SecCertificateRef cert =
|
||||||
@@ -16510,12 +16770,19 @@ inline bool load_system_certs(ctx_t ctx) {
|
|||||||
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
||||||
if (!store) return false;
|
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;
|
bool loaded_any = false;
|
||||||
|
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);
|
auto count = CFArrayGetCount(certs);
|
||||||
for (CFIndex i = 0; i < count; i++) {
|
for (CFIndex i = 0; i < count; i++) {
|
||||||
auto cert = reinterpret_cast<SecCertificateRef>(
|
auto cert = reinterpret_cast<SecCertificateRef>(
|
||||||
@@ -16532,6 +16799,7 @@ inline bool load_system_certs(ctx_t ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CFRelease(certs);
|
CFRelease(certs);
|
||||||
|
}
|
||||||
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||||
#else
|
#else
|
||||||
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||||
|
|||||||
+786
@@ -10673,8 +10673,20 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
|
|||||||
<< "Verify callback should have been called";
|
<< "Verify callback should have been called";
|
||||||
|
|
||||||
// CN="Common Name" should match our test certificate
|
// CN="Common Name" should match our test certificate
|
||||||
|
//
|
||||||
|
// BoringSSL intentionally drops CN-based hostname matching per RFC 6125
|
||||||
|
// §6.4.4 — only SubjectAltName is consulted. Other backends (OpenSSL,
|
||||||
|
// MbedTLS, wolfSSL) still honor the CN fallback, so flip the expectation
|
||||||
|
// for BoringSSL builds. OPENSSL_IS_BORINGSSL is defined by BoringSSL's
|
||||||
|
// <openssl/base.h>, which is included transitively when
|
||||||
|
// CPPHTTPLIB_OPENSSL_SUPPORT is set against a BoringSSL install.
|
||||||
|
#if defined(OPENSSL_IS_BORINGSSL)
|
||||||
|
EXPECT_FALSE(verify_result_cn)
|
||||||
|
<< "BoringSSL should reject CN-based hostname matching (SAN-only)";
|
||||||
|
#else
|
||||||
EXPECT_TRUE(verify_result_cn)
|
EXPECT_TRUE(verify_result_cn)
|
||||||
<< "verify_hostname should match 'Common Name' (certificate CN)";
|
<< "verify_hostname should match 'Common Name' (certificate CN)";
|
||||||
|
#endif
|
||||||
|
|
||||||
// Wrong hostname should not match
|
// Wrong hostname should not match
|
||||||
EXPECT_FALSE(verify_result_wrong)
|
EXPECT_FALSE(verify_result_wrong)
|
||||||
@@ -18264,3 +18276,777 @@ TEST(KeepAliveTest, DeleteWithoutContentLengthDoesNotEatNextRequest) {
|
|||||||
|
|
||||||
EXPECT_EQ(2, delete_count.load());
|
EXPECT_EQ(2, delete_count.load());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namespace no_proxy_test {
|
||||||
|
|
||||||
|
// Server bound to 127.0.0.1:<dynamic>, listen thread spawned by listen(),
|
||||||
|
// auto-stopped + joined on scope exit. Register handlers via svr() BEFORE
|
||||||
|
// calling listen().
|
||||||
|
class ScopedServer {
|
||||||
|
public:
|
||||||
|
ScopedServer() { port_ = svr_.bind_to_any_port("127.0.0.1"); }
|
||||||
|
~ScopedServer() {
|
||||||
|
svr_.stop();
|
||||||
|
if (th_.joinable()) { th_.join(); }
|
||||||
|
}
|
||||||
|
Server &svr() { return svr_; }
|
||||||
|
int port() const { return port_; }
|
||||||
|
void listen() {
|
||||||
|
th_ = std::thread([this] { svr_.listen_after_bind(); });
|
||||||
|
svr_.wait_until_ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Server svr_;
|
||||||
|
std::thread th_;
|
||||||
|
int port_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ProxyAndTargetServers {
|
||||||
|
public:
|
||||||
|
ProxyAndTargetServers() {
|
||||||
|
proxy_mock_.Get(".*", [this](const Request &req, Response &res) {
|
||||||
|
proxy_hits_++;
|
||||||
|
last_had_proxy_authz_ = req.has_header("Proxy-Authorization");
|
||||||
|
res.set_content("via-proxy", "text/plain");
|
||||||
|
});
|
||||||
|
target_.Get(".*", [this](const Request &req, Response &res) {
|
||||||
|
target_hits_++;
|
||||||
|
last_had_proxy_authz_ = req.has_header("Proxy-Authorization");
|
||||||
|
res.set_content("direct", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy_port_ = proxy_mock_.bind_to_any_port("127.0.0.1");
|
||||||
|
target_port_ = target_.bind_to_any_port("127.0.0.1");
|
||||||
|
proxy_thread_ = std::thread([this] { proxy_mock_.listen_after_bind(); });
|
||||||
|
target_thread_ = std::thread([this] { target_.listen_after_bind(); });
|
||||||
|
proxy_mock_.wait_until_ready();
|
||||||
|
target_.wait_until_ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
~ProxyAndTargetServers() {
|
||||||
|
proxy_mock_.stop();
|
||||||
|
target_.stop();
|
||||||
|
if (proxy_thread_.joinable()) { proxy_thread_.join(); }
|
||||||
|
if (target_thread_.joinable()) { target_thread_.join(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Server &proxy_mock() { return proxy_mock_; }
|
||||||
|
Server &target() { return target_; }
|
||||||
|
int proxy_port() const { return proxy_port_; }
|
||||||
|
int target_port() const { return target_port_; }
|
||||||
|
int proxy_hits() const { return proxy_hits_.load(); }
|
||||||
|
int target_hits() const { return target_hits_.load(); }
|
||||||
|
bool last_had_proxy_authz() const { return last_had_proxy_authz_.load(); }
|
||||||
|
|
||||||
|
void reset_counters() {
|
||||||
|
proxy_hits_ = 0;
|
||||||
|
target_hits_ = 0;
|
||||||
|
last_had_proxy_authz_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Server proxy_mock_;
|
||||||
|
Server target_;
|
||||||
|
std::thread proxy_thread_;
|
||||||
|
std::thread target_thread_;
|
||||||
|
int proxy_port_ = 0;
|
||||||
|
int target_port_ = 0;
|
||||||
|
std::atomic<int> proxy_hits_{0};
|
||||||
|
std::atomic<int> target_hits_{0};
|
||||||
|
std::atomic<bool> last_had_proxy_authz_{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
inline std::unique_ptr<Client> make_client(const std::string &host,
|
||||||
|
ProxyAndTargetServers &s) {
|
||||||
|
auto cli = detail::make_unique<Client>(host, s.target_port());
|
||||||
|
cli->set_hostname_addr_map({{host, "127.0.0.1"}});
|
||||||
|
cli->set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
return cli;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace no_proxy_test
|
||||||
|
|
||||||
|
using no_proxy_test::make_client;
|
||||||
|
using no_proxy_test::ProxyAndTargetServers;
|
||||||
|
|
||||||
|
TEST(NoProxyTest, ExactHostnameBypasses) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("example.com", s);
|
||||||
|
cli->set_no_proxy({"example.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, SubdomainBypasses) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("foo.example.com", s);
|
||||||
|
cli->set_no_proxy({"example.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, EvilExampleDoesNotMatchExample) {
|
||||||
|
// Regression guard: "evilexample.com" must not be considered a subdomain
|
||||||
|
// of "example.com". Without the dot-boundary rule, a naive endsWith
|
||||||
|
// check would let traffic bypass the proxy and leak credentials direct
|
||||||
|
// to the attacker host.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("evilexample.com", s);
|
||||||
|
cli->set_no_proxy({"example.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, ExampleDotEvilDoesNotMatchExample) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("example.com.evil.com", s);
|
||||||
|
cli->set_no_proxy({"example.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, LeadingDotPatternMatchesBareDomain) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("example.com", s);
|
||||||
|
cli->set_no_proxy({".example.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, CaseInsensitiveHostname) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("Example.COM", s);
|
||||||
|
cli->set_no_proxy({"EXAMPLE.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, TrailingDotIsNormalized) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("example.com.", s);
|
||||||
|
cli->set_no_proxy({"example.com"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, TrailingDotOnEntryIsNormalized) {
|
||||||
|
// Trailing dots must be normalized on BOTH sides — host and entry.
|
||||||
|
// An implementation that only strips the host-side trailing dot would
|
||||||
|
// fail to match host "example.com" against entry "example.com." because
|
||||||
|
// a literal substring search would compare 11 chars against 12.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("example.com", s);
|
||||||
|
cli->set_no_proxy({"example.com."});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, WildcardBypassesEverything) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("anything.invalid.test", s);
|
||||||
|
cli->set_no_proxy({"*"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv4LiteralExactMatch) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("0:0:0:0:0:0:0:1", s.target_port());
|
||||||
|
cli.set_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"::1"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
|
||||||
|
// Regression guard: a bare IPv6 host literal (no surrounding brackets)
|
||||||
|
// must still be recognized as IPv6 for CIDR matching. Implementations
|
||||||
|
// that detect IPv6 only when the host begins with '[' would parse the
|
||||||
|
// host as a hostname and miss the match.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("fe80::1", s.target_port());
|
||||||
|
cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"fe80::/10"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("::1", s.target_port());
|
||||||
|
cli.set_hostname_addr_map({{"::1", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"[::1]"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("fe80::1", s.target_port());
|
||||||
|
cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"[fe80::]/10"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) {
|
||||||
|
// Policy: keep address families separate. "::ffff:1.2.3.4" must NOT
|
||||||
|
// satisfy a NO_PROXY entry of "1.2.3.4". This avoids subtle bypass
|
||||||
|
// tricks via address-family conversion.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("::ffff:127.0.0.1", s.target_port());
|
||||||
|
cli.set_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv4CidrMatch) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"127.0.0.0/8"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv4CidrNonMatch) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"10.0.0.0/8"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv4CidrPrefixZeroMatchesAll) {
|
||||||
|
// Prefix 0 must not trigger the (1u << 32) shift UB. Result: every
|
||||||
|
// IPv4 target matches.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"0.0.0.0/0"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, IPv4CidrSingleHostNoSlash) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, MalformedCidrPrefixIsDropped) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"127.0.0.0/33"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, TrailingSlashCidrIsRejected) {
|
||||||
|
// Empty prefix after the slash must be rejected, not silently treated as /32.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({"127.0.0.1/"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, ProxyAuthorizationSuppressedWhenBypassed) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("internal.corp", s);
|
||||||
|
cli->set_proxy_basic_auth("u", "p");
|
||||||
|
cli->set_no_proxy({"internal.corp"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_FALSE(s.last_had_proxy_authz())
|
||||||
|
<< "Proxy-Authorization must not be sent direct to the target";
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, ProxyAuthorizationSentWhenNotBypassed) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("public.example", s);
|
||||||
|
cli->set_proxy_basic_auth("u", "p");
|
||||||
|
cli->set_no_proxy({"internal.corp"}); // does not match
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_TRUE(s.last_had_proxy_authz());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, EmptyNoProxyKeepsProxyOn) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("anything.test", s);
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, PortSpecificEntryRejected) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("example.com", s);
|
||||||
|
cli->set_no_proxy({"example.com:8080"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, EmptyAndWhitespaceEntriesDropped) {
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
auto cli = make_client("anything.test", s);
|
||||||
|
cli->set_no_proxy({"", " ", "\t"});
|
||||||
|
|
||||||
|
auto res = cli->Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(s.proxy_hits(), 1);
|
||||||
|
EXPECT_EQ(0, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, ValidEntryWithSurroundingWhitespaceStillMatches) {
|
||||||
|
// An entry with leading/trailing whitespace must still match — env-style
|
||||||
|
// values are commonly pasted with stray spaces and an implementation
|
||||||
|
// that feeds the raw token directly to inet_pton would fail to match
|
||||||
|
// valid CIDRs because of the spaces.
|
||||||
|
ProxyAndTargetServers s;
|
||||||
|
Client cli("127.0.0.1", s.target_port());
|
||||||
|
cli.set_proxy("127.0.0.1", s.proxy_port());
|
||||||
|
cli.set_no_proxy({" 127.0.0.0/8 "});
|
||||||
|
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(0, s.proxy_hits());
|
||||||
|
EXPECT_EQ(1, s.target_hits());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) {
|
||||||
|
// Analog of GHSA-6hrp-7fq9-3qv2: redirect to a NO_PROXY-matched host must
|
||||||
|
// go direct and must NOT carry Proxy-Authorization.
|
||||||
|
std::atomic<int> proxy_hits{0};
|
||||||
|
std::atomic<int> target_hits{0};
|
||||||
|
std::atomic<bool> target_saw_authz{false};
|
||||||
|
|
||||||
|
no_proxy_test::ScopedServer proxy_mock, target;
|
||||||
|
|
||||||
|
proxy_mock.svr().Get(".*", [&](const Request &, Response &res) {
|
||||||
|
proxy_hits++;
|
||||||
|
res.status = 302;
|
||||||
|
res.set_header("Location", "http://127.0.0.1:" +
|
||||||
|
std::to_string(target.port()) + "/landed");
|
||||||
|
});
|
||||||
|
target.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||||
|
target_hits++;
|
||||||
|
if (req.has_header("Proxy-Authorization")) { target_saw_authz = true; }
|
||||||
|
res.set_content("direct", "text/plain");
|
||||||
|
});
|
||||||
|
proxy_mock.listen();
|
||||||
|
target.listen();
|
||||||
|
|
||||||
|
Client cli("public.example", target.port());
|
||||||
|
cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", proxy_mock.port());
|
||||||
|
cli.set_proxy_basic_auth("u", "p");
|
||||||
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
|
||||||
|
auto res = cli.Get("/redir");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_GE(proxy_hits.load(), 1);
|
||||||
|
EXPECT_GE(target_hits.load(), 1);
|
||||||
|
EXPECT_FALSE(target_saw_authz.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
TEST(NoProxyTest, BypassedTargetReturning407DoesNotLeakProxyDigestCredentials) {
|
||||||
|
// Direct origin replying 407 must not trigger the digest retry; otherwise
|
||||||
|
// proxy creds would be sent to the (possibly hostile) origin.
|
||||||
|
std::atomic<int> target_hits{0};
|
||||||
|
std::atomic<bool> target_saw_proxy_authz{false};
|
||||||
|
|
||||||
|
no_proxy_test::ScopedServer target;
|
||||||
|
target.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||||
|
target_hits++;
|
||||||
|
if (req.has_header("Proxy-Authorization")) {
|
||||||
|
target_saw_proxy_authz = true;
|
||||||
|
}
|
||||||
|
res.status = StatusCode::ProxyAuthenticationRequired_407;
|
||||||
|
res.set_header("Proxy-Authenticate", "Digest realm=\"evil\", qop=\"auth\", "
|
||||||
|
"nonce=\"abc\", algorithm=MD5");
|
||||||
|
});
|
||||||
|
target.listen();
|
||||||
|
|
||||||
|
// The proxy address is set to the target's port: the bypass MUST kick in;
|
||||||
|
// if it doesn't, the test still routes "via proxy" to the same server and
|
||||||
|
// the Proxy-Authorization assertion below catches the leak.
|
||||||
|
Client cli("evil.example", target.port());
|
||||||
|
cli.set_hostname_addr_map({{"evil.example", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", target.port());
|
||||||
|
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
|
||||||
|
cli.set_no_proxy({"evil.example"});
|
||||||
|
|
||||||
|
auto res = cli.Get("/x");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(StatusCode::ProxyAuthenticationRequired_407, res->status);
|
||||||
|
EXPECT_EQ(1, target_hits.load());
|
||||||
|
EXPECT_FALSE(target_saw_proxy_authz.load());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) {
|
||||||
|
// A (via proxy) → B (NO_PROXY-matched, direct) → C must re-engage the proxy.
|
||||||
|
std::atomic<int> proxy_hits{0};
|
||||||
|
std::atomic<int> bypass_hits{0};
|
||||||
|
std::atomic<bool> proxy_saw_c_url{false};
|
||||||
|
|
||||||
|
no_proxy_test::ScopedServer proxy_mock, bypass_server;
|
||||||
|
|
||||||
|
proxy_mock.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||||
|
proxy_hits++;
|
||||||
|
if (req.path.find("/start") != std::string::npos) {
|
||||||
|
res.status = 302;
|
||||||
|
res.set_header("Location", "http://127.0.0.1:" +
|
||||||
|
std::to_string(bypass_server.port()) +
|
||||||
|
"/middle");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (req.path.find("/end") != std::string::npos) {
|
||||||
|
proxy_saw_c_url = true;
|
||||||
|
res.set_content("c-via-proxy", "text/plain");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.set_content("unexpected", "text/plain");
|
||||||
|
});
|
||||||
|
// public.example's "advertised" port is arbitrary (the request never lands
|
||||||
|
// there — it goes through the proxy), but use a dynamic value to stay
|
||||||
|
// friendly with sharded parallel runs.
|
||||||
|
int public_port = bypass_server.port();
|
||||||
|
bypass_server.svr().Get(".*", [&](const Request &, Response &res) {
|
||||||
|
bypass_hits++;
|
||||||
|
res.status = 302;
|
||||||
|
res.set_header("Location", "http://public.example:" +
|
||||||
|
std::to_string(public_port) + "/end");
|
||||||
|
});
|
||||||
|
proxy_mock.listen();
|
||||||
|
bypass_server.listen();
|
||||||
|
|
||||||
|
Client cli("public.example", public_port);
|
||||||
|
cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", proxy_mock.port());
|
||||||
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
|
||||||
|
auto res = cli.Get("/start");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("c-via-proxy", res->body);
|
||||||
|
EXPECT_GE(proxy_hits.load(), 2);
|
||||||
|
EXPECT_GE(bypass_hits.load(), 1);
|
||||||
|
EXPECT_TRUE(proxy_saw_c_url.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
|
||||||
|
// Mid-session set_no_proxy must drop any keep-alive socket so the next
|
||||||
|
// request reconnects to the correct endpoint.
|
||||||
|
std::atomic<int> proxy_hits{0};
|
||||||
|
std::atomic<int> target_hits{0};
|
||||||
|
|
||||||
|
no_proxy_test::ScopedServer proxy_mock, target;
|
||||||
|
|
||||||
|
proxy_mock.svr().Get(".*", [&](const Request &, Response &res) {
|
||||||
|
proxy_hits++;
|
||||||
|
res.set_content("via-proxy", "text/plain");
|
||||||
|
});
|
||||||
|
target.svr().Get(".*", [&](const Request &, Response &res) {
|
||||||
|
target_hits++;
|
||||||
|
res.set_content("direct", "text/plain");
|
||||||
|
});
|
||||||
|
proxy_mock.listen();
|
||||||
|
target.listen();
|
||||||
|
|
||||||
|
Client cli("public.example", target.port());
|
||||||
|
cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
|
||||||
|
cli.set_proxy("127.0.0.1", proxy_mock.port());
|
||||||
|
cli.set_keep_alive(true);
|
||||||
|
|
||||||
|
auto res1 = cli.Get("/a");
|
||||||
|
ASSERT_TRUE(res1);
|
||||||
|
EXPECT_EQ("via-proxy", res1->body);
|
||||||
|
EXPECT_EQ(1, proxy_hits.load());
|
||||||
|
EXPECT_EQ(0, target_hits.load());
|
||||||
|
|
||||||
|
cli.set_no_proxy({"public.example"});
|
||||||
|
|
||||||
|
auto res2 = cli.Get("/b");
|
||||||
|
ASSERT_TRUE(res2);
|
||||||
|
EXPECT_EQ("direct", res2->body);
|
||||||
|
EXPECT_EQ(1, proxy_hits.load());
|
||||||
|
EXPECT_EQ(1, target_hits.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(CPPHTTPLIB_SSL_ENABLED) && !defined(_WIN32)
|
||||||
|
namespace proxy_tunnel_test {
|
||||||
|
|
||||||
|
// SSLServer counterpart to no_proxy_test::ScopedServer.
|
||||||
|
class ScopedSSLServer {
|
||||||
|
public:
|
||||||
|
ScopedSSLServer() : svr_(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE) {
|
||||||
|
port_ = svr_.bind_to_any_port("127.0.0.1");
|
||||||
|
}
|
||||||
|
~ScopedSSLServer() {
|
||||||
|
svr_.stop();
|
||||||
|
if (th_.joinable()) { th_.join(); }
|
||||||
|
}
|
||||||
|
SSLServer &svr() { return svr_; }
|
||||||
|
int port() const { return port_; }
|
||||||
|
void listen() {
|
||||||
|
th_ = std::thread([this] { svr_.listen_after_bind(); });
|
||||||
|
svr_.wait_until_ready();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
SSLServer svr_;
|
||||||
|
std::thread th_;
|
||||||
|
int port_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Accepts CONNECT, replies 200, byte-forwards to forward_port.
|
||||||
|
class ScopedConnectProxy {
|
||||||
|
public:
|
||||||
|
explicit ScopedConnectProxy(int forward_port) : forward_port_(forward_port) {
|
||||||
|
listen_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (listen_fd_ < 0) { return; }
|
||||||
|
int yes = 1;
|
||||||
|
::setsockopt(listen_fd_, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||||
|
sockaddr_in a{};
|
||||||
|
a.sin_family = AF_INET;
|
||||||
|
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
a.sin_port = 0;
|
||||||
|
if (::bind(listen_fd_, reinterpret_cast<sockaddr *>(&a), sizeof(a)) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socklen_t alen = sizeof(a);
|
||||||
|
if (::getsockname(listen_fd_, reinterpret_cast<sockaddr *>(&a), &alen) !=
|
||||||
|
0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
port_ = ntohs(a.sin_port);
|
||||||
|
if (::listen(listen_fd_, 1) != 0) { return; }
|
||||||
|
th_ = std::thread([this] { run(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
~ScopedConnectProxy() {
|
||||||
|
stop_ = true;
|
||||||
|
if (listen_fd_ >= 0) {
|
||||||
|
::shutdown(listen_fd_, SHUT_RDWR);
|
||||||
|
::close(listen_fd_);
|
||||||
|
}
|
||||||
|
if (th_.joinable()) { th_.join(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
int port() const { return port_; }
|
||||||
|
int connect_hits() const { return connect_hits_.load(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run() {
|
||||||
|
while (!stop_.load()) {
|
||||||
|
fd_set rfds;
|
||||||
|
FD_ZERO(&rfds);
|
||||||
|
FD_SET(listen_fd_, &rfds);
|
||||||
|
timeval tv{0, 100 * 1000};
|
||||||
|
int sel = ::select(listen_fd_ + 1, &rfds, nullptr, nullptr, &tv);
|
||||||
|
if (sel <= 0) { continue; }
|
||||||
|
|
||||||
|
int client_fd = ::accept(listen_fd_, nullptr, nullptr);
|
||||||
|
if (client_fd < 0) { continue; }
|
||||||
|
|
||||||
|
std::string req;
|
||||||
|
char buf[2048];
|
||||||
|
while (req.find("\r\n\r\n") == std::string::npos) {
|
||||||
|
ssize_t n = ::recv(client_fd, buf, sizeof(buf), 0);
|
||||||
|
if (n <= 0) { break; }
|
||||||
|
req.append(buf, static_cast<size_t>(n));
|
||||||
|
}
|
||||||
|
if (req.compare(0, 7, "CONNECT") != 0) {
|
||||||
|
::close(client_fd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
connect_hits_++;
|
||||||
|
|
||||||
|
const char *ok = "HTTP/1.1 200 Connection established\r\n\r\n";
|
||||||
|
::send(client_fd, ok, std::strlen(ok), 0);
|
||||||
|
|
||||||
|
int origin_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
sockaddr_in o{};
|
||||||
|
o.sin_family = AF_INET;
|
||||||
|
o.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||||
|
o.sin_port = htons(static_cast<unsigned short>(forward_port_));
|
||||||
|
if (::connect(origin_fd, reinterpret_cast<sockaddr *>(&o), sizeof(o)) !=
|
||||||
|
0) {
|
||||||
|
::close(client_fd);
|
||||||
|
::close(origin_fd);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto forward = [](int from, int to) {
|
||||||
|
char b[8192];
|
||||||
|
for (;;) {
|
||||||
|
ssize_t n = ::recv(from, b, sizeof(b), 0);
|
||||||
|
if (n <= 0) { break; }
|
||||||
|
ssize_t off = 0;
|
||||||
|
while (off < n) {
|
||||||
|
ssize_t s = ::send(to, b + off, static_cast<size_t>(n - off), 0);
|
||||||
|
if (s <= 0) {
|
||||||
|
off = n;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
off += s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
::shutdown(to, SHUT_WR);
|
||||||
|
};
|
||||||
|
|
||||||
|
std::thread t1([&] { forward(client_fd, origin_fd); });
|
||||||
|
std::thread t2([&] { forward(origin_fd, client_fd); });
|
||||||
|
t1.join();
|
||||||
|
t2.join();
|
||||||
|
::close(client_fd);
|
||||||
|
::close(origin_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int forward_port_ = -1;
|
||||||
|
int listen_fd_ = -1;
|
||||||
|
int port_ = 0;
|
||||||
|
std::thread th_;
|
||||||
|
std::atomic<bool> stop_{false};
|
||||||
|
std::atomic<int> connect_hits_{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace proxy_tunnel_test
|
||||||
|
|
||||||
|
TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) {
|
||||||
|
// Origin inside a CONNECT tunnel replying 407 must not trigger the digest
|
||||||
|
// retry; otherwise proxy creds would be sent to the origin.
|
||||||
|
std::atomic<int> origin_hits{0};
|
||||||
|
std::atomic<bool> origin_saw_proxy_authz{false};
|
||||||
|
|
||||||
|
proxy_tunnel_test::ScopedSSLServer origin;
|
||||||
|
origin.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||||
|
origin_hits++;
|
||||||
|
if (req.has_header("Proxy-Authorization")) {
|
||||||
|
origin_saw_proxy_authz = true;
|
||||||
|
}
|
||||||
|
res.status = StatusCode::ProxyAuthenticationRequired_407;
|
||||||
|
res.set_header("Proxy-Authenticate",
|
||||||
|
"Digest realm=\"evil\", qop=\"auth\", nonce=\"abc\", "
|
||||||
|
"algorithm=MD5");
|
||||||
|
});
|
||||||
|
origin.listen();
|
||||||
|
|
||||||
|
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
|
||||||
|
ASSERT_NE(0, proxy.port());
|
||||||
|
|
||||||
|
SSLClient cli(HOST, origin.port());
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
cli.set_proxy(HOST, proxy.port());
|
||||||
|
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
|
||||||
|
|
||||||
|
auto res = cli.Get("/x");
|
||||||
|
ASSERT_TRUE(res);
|
||||||
|
EXPECT_EQ(StatusCode::ProxyAuthenticationRequired_407, res->status);
|
||||||
|
EXPECT_EQ(1, origin_hits.load());
|
||||||
|
EXPECT_FALSE(origin_saw_proxy_authz.load());
|
||||||
|
EXPECT_EQ(1, proxy.connect_hits());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
Reference in new issue
Block a user