Compare commits

..

21 Commits

Author SHA1 Message Date
yhirose 79d83feb18 Fix WebSocketClient dropping query string from URL during handshake (#2468)
The constructor stored only uc.path in path_, discarding uc.query, so the
WebSocket upgrade handshake sent the Request-URI without the query string.
Append the query to path_ so query parameters (e.g. auth tokens) are sent.
2026-06-07 17:08:11 -04:00
yhirose fe56a07da5 Wait for in-progress CI runs before releasing
The release check treated runs with an empty conclusion as failures.
Now it inspects each run's status and aborts with an error if any CI
check is still running, so releases wait until CI completes.
2026-06-06 13:38:36 -04:00
Kim, Hyuk 907257f51d add set_hostname_addr_map to WebSocketClient (#2463)
* add set_hostname_addr_map to WebSocketClient

* add WebSocketTest unit test cases
* SpecifyServerIPAddress_AnotherHostname
* SpecifyServerIPAddress_RealHostname

* Change wrong_ip from 0.0.0.0 to 192.0.2.1

Use 192.0.2.1 (RFC 5737 documentation address) to ensure it acts
as a non-routable address and does not alias to loopback.

* Fix style check

* set short timeout in WebSocketTest.SpecifyServerIPAddress_RealHostname

cannot reach wrong_ip
2026-06-05 16:34:20 -04:00
yhirose 0c2f535b74 Fix #2467 2026-06-04 21:20:37 -04:00
Florian Fischer c7ba963a17 Ignore ranges for unknown-length streams (#2465) 2026-06-04 20:15:21 -04:00
yhirose 4465e81b9f Fix #2464 2026-06-03 22:24:52 -04:00
yhirose 44215e23e9 Release v0.46.1 2026-06-01 12:24:27 -04:00
yhirose 91219d4508 Fix #2458: send body when no 100 Continue arrives over TLS (#2460)
The auto-added `Expect: 100-continue` (for bodies >= 1024 bytes) decided
whether to withhold the request body based on raw socket readability via
select_read(). Over TLS, post-handshake records such as TLS 1.3 session
tickets make the socket readable without any HTTP response being
available, so the client withheld the body and then blocked reading a
response that never came, failing with `Failed to read connection`.

Decide based on whether a status line can actually be read within the
100-continue timeout instead: temporarily shorten the read timeout, try
to read the status line, and if none arrives, send the body and proceed
as usual (matching curl). This keeps the `100 Continue` and early
final-response paths working while no longer being fooled by TLS records.

Add a regression test using a raw OpenSSL server that never sends
`100 Continue`.
2026-05-29 06:19:40 -04:00
NsPro04 c86c192f3e Fix: (#2459)
"httplib.h(5733,29): warning : missing field 'InternalHigh' initializer [-Wmissing-field-initializers]"
"httplib.h(5742,28): warning : missing field 'ai_family' initializer [-Wmissing-field-initializers]"
2026-05-28 18:19:33 -04:00
yhirose 008e107d0f Release v0.46.0 2026-05-25 00:30:27 -04:00
yhirose d278f965cc Fix #2457 2026-05-25 00:21:57 -04:00
yhirose 4c4b62dd7e Feature 2446 no proxy env (#2448)
* Route proxy-enabled checks through is_proxy_enabled_for_host helper

In preparation for NO_PROXY support (#2446), centralize the proxy-enabled
decision in a single helper so the upcoming bypass logic can be added in
one place rather than to six divergent call sites. The helper's body for
now is identical to the existing condition; the host parameter is unused
until set_no_proxy() lands.

Refactored sites:
  ClientImpl::create_client_socket
  ClientImpl::handle_request           (HTTP request rewrite)
  ClientImpl::setup_redirect_client
  ClientImpl::process_request          (SSL is_proxy_enabled flag)
  SSLClient::setup_proxy_connection
  SSLClient::ensure_socket_connection

The two prepare_default_headers Proxy-Authorization injection blocks
(currently gated only on proxy auth credentials being set) are
intentionally not wrapped here. Doing so would change behavior in the
rare misconfiguration case where credentials are set without set_proxy,
so the gating is deferred to the NO_PROXY commit where it becomes
meaningful.

No behavior change. All 608 unit tests and the 22 squid-backed proxy
tests pass.

* Add detail::parse_proxy_url with control-char and scheme validation

Building block for the upcoming set_proxy_from_env (#2446). Parses
"http(s)://[user[:pass]@]host[:port][/...]" into a detail::ProxyUrl
struct.

Rejects:
  - empty input
  - any control character (< 0x20 or 0x7F), including CR/LF/NUL — these
    would otherwise let a malicious env value inject extra header lines
    into a CONNECT request or Proxy-Authorization header
  - schemes other than http and https
  - ports outside [1, 65535]
  - malformed IPv6 host literals (validated via inet_pton(AF_INET6))
  - non-numeric or trailing-garbage port strings

Notes:
  - userinfo is split on the LAST '@' so passwords containing '@' are
    preserved in the password field
  - if no port is present, defaults to 80 (http) / 443 (https)
  - integer parse goes through detail::from_chars to stay compatible
    with -fno-exceptions builds

The helper has no callers yet; it lands consumer-side when
set_proxy_from_env arrives. All 608 unit tests pass.

* Add NO_PROXY parsing and matching helpers in detail namespace

Building blocks for the upcoming Client::set_no_proxy (#2446):

  - NoProxyEntry / NoProxyKind: parsed list entry (wildcard, hostname
    suffix, IPv4 CIDR, IPv6 CIDR)
  - NormalizedTarget: pre-normalized form of the connection's target
    host (lowercase, brackets stripped, trailing dot stripped, with
    inet_pton already attempted)
  - parse_no_proxy_entry / parse_no_proxy_list: token / list parsing.
    Port-specific entries are rejected by design — cpp-httplib's other
    host-keyed APIs (e.g. set_hostname_addr_map) are hostname-only, so
    supporting host:port for NO_PROXY alone would be inconsistent.
  - ipv4_in_cidr / ipv6_in_cidr: CIDR membership. IPv4 special-cases
    prefix=0 to avoid the (1u << 32) shift UB. IPv6 uses byte-wise
    memcmp plus a masked partial-byte compare.
  - normalize_target: prepares the target host for matching. Routes
    every IP literal through inet_pton so "127.0.0.1" vs
    "127.000.000.001" vs decimal-form integers cannot be used to bypass
    a NO_PROXY entry via alternate string forms.
  - host_matches_no_proxy: matches a normalized target against an
    entry list. Hostname suffix matching uses a dot-boundary rule so
    "evilexample.com" does NOT match the entry "example.com". IPv4 and
    IPv6 entries match only their own address family — IPv4-mapped IPv6
    ("::ffff:1.2.3.4") is not cross-matched against IPv4 entries.

These helpers have no callers yet; they land consumer-side in the
upcoming set_no_proxy / set_proxy_from_env commits. All 608 unit tests
pass.

* Add Client::set_no_proxy and wire NO_PROXY into proxy decision

Implements the user-facing half of #2446 (set_proxy_from_env follows in
the next commit). When a NO_PROXY pattern matches the target host, the
client now bypasses the configured proxy and the corresponding
Proxy-Authorization header is suppressed.

Public API:
  - Client::set_no_proxy(const std::vector<std::string> &patterns)
    Patterns: "*", hostname suffix (e.g. "example.com" or
    ".example.com"), IPv4/IPv6 CIDR (e.g. "10.0.0.0/8", "fe80::/10"),
    or single IP literals. Replaces any previous list. Malformed
    entries are silently dropped.

Internals:
  - is_proxy_enabled_for_host now consults no_proxy_entries_, normalizing
    the target through inet_pton so leading-zero or alternate-form IPs
    cannot be used to bypass an entry.
  - prepare_default_headers gates both Proxy-Authorization injection
    blocks (basic and bearer) on is_proxy_enabled_for_host(host_).
    Previously, Proxy-Authorization was sent whenever proxy auth
    credentials were configured, even when the request was going direct
    to the target. With NO_PROXY now in play, that path would leak
    proxy credentials to the destination server — analog of the
    redirect-leak class of bugs (cf. CVE-2023-32681 in Python requests,
    GHSA-6hrp-7fq9-3qv2 in cpp-httplib).
  - setup_redirect_client now takes the redirect target host as a
    parameter and re-evaluates is_proxy_enabled_for_host against it.
    no_proxy_entries_ is always copied to the redirect client so the
    bypass policy follows across redirects. This is the cross-origin
    leak surface that GHSA-c3h8-fqq4-xm4g lives in; centralizing the
    decision through is_proxy_enabled_for_host removes the chance of
    branch divergence.
  - copy_settings copies no_proxy_entries_.

The slight behavior change for the rare misconfiguration "set
proxy_basic_auth without set_proxy" — Proxy-Authorization is no longer
sent in that case — is deliberate. The header has no addressee when
the proxy is unset.

All 608 unit tests and 22 squid-backed proxy integration tests pass.

* Add Client::set_proxy_from_env with httpoxy mitigation

Final user-facing piece for #2446. Reads proxy-related environment
variables and configures the client.

  - HTTPS clients (SSLClient) read https_proxy / HTTPS_PROXY
  - HTTP clients read http_proxy (lowercase only — see below)
  - Both also read no_proxy / NO_PROXY
  - Returns true if at least one variable was found and applied

The lowercase-only http_proxy rule mitigates httpoxy / CVE-2016-5385.
In CGI / FastCGI environments the uppercase HTTP_PROXY collides with
the HTTP_* namespace used to expose request headers, so a remote
attacker controlling the "Proxy:" header can inject a proxy URL.
cpp-httplib follows curl, Go, and Python requests in honoring only
the lowercase form. https_proxy/HTTPS_PROXY and no_proxy/NO_PROXY do
not have this problem because their names don't begin with HTTP_.

Scheme dispatch uses virtual is_ssl(): an SSLClient picks
https_proxy and a plain ClientImpl picks http_proxy. There is
intentionally no cross-scheme fallback — the two variables describe
different traffic.

set_proxy_from_env() reads getenv() synchronously and is documented
as "call once at startup" — concurrent setenv from other threads is
undefined.

All 608 unit tests pass.

* Add NO_PROXY behavior tests

27 black-box tests exercising the public Client API only (no detail::
calls, BORDER-friendly; no EXPECT_NO_THROW, -fno-exceptions-friendly).

In-process proxy mock + target server. Each test asserts which side
of the routing decision each request landed on, and what headers (in
particular Proxy-Authorization) the receiving side saw.

Coverage:

  Suffix matching (dot-boundary rule)
    - exact-host match
    - subdomain match
    - "evilexample.com" does NOT match "example.com"  ← regression
      guard for the classic NO_PROXY suffix-match pitfall
    - "example.com.evil.com" does NOT match
    - leading-dot pattern still matches the bare domain (Go/curl
      convention)
    - case-insensitive
    - trailing-dot host normalization

  Wildcard
    - "*" bypasses everything

  IP normalization
    - exact IPv4 match
    - "::1" matches "0:0:0:0:0:0:0:1" via inet_pton
    - IPv4-mapped IPv6 ("::ffff:127.0.0.1") is NOT cross-matched
      against an IPv4 entry

  CIDR
    - basic v4 in-cidr / not-in-cidr
    - "0.0.0.0/0" (prefix=0; verifies no shift UB)
    - bare IP treated as /32
    - malformed prefix (/33) silently dropped → no NO_PROXY effect

  Proxy-Authorization handling
    - suppressed when NO_PROXY matches the target
    - sent when NO_PROXY does not match

  Backward compat
    - default behavior unchanged when set_no_proxy is never called

  Parsing edge cases
    - port-specific entries ("host:port") rejected
    - empty / whitespace tokens dropped

  Cross-origin redirect (analog of GHSA-6hrp-7fq9-3qv2)
    - redirect target in NO_PROXY → redirect leg goes direct, no
      Proxy-Authorization carried over

  set_proxy_from_env (Unix only — uses setenv/unsetenv)
    - lowercase http_proxy applied
    - uppercase HTTP_PROXY ignored (httpoxy / CVE-2016-5385)
    - NO_PROXY-only env returns true and applies the bypass list
    - CRLF in env value rejected (cf. CVE-2026-21428)
    - empty env value treated as unset

635 tests (608 prior + 27 new) pass under both the regular and the
split builds.

* Document set_no_proxy and set_proxy_from_env in README

Adds two subsections under "Proxy server support":

  - "Bypass the proxy for specific hosts (NO_PROXY)" — set_no_proxy,
    pattern syntax, dot-boundary rule, IP normalization, limitations
    (no port-specific entries, no v4-mapped v6 cross-match, replace
    semantics).

  - "Read proxy settings from the environment" — set_proxy_from_env,
    which variables are read, the lowercase-only http_proxy rule with
    an inline httpoxy / CVE-2016-5385 explanation, threading
    expectations.

Documentation only. Closes the doc gap from #2446.

* Document NO_PROXY and set_proxy_from_env in cookbook c16-proxy

Replaces the now-incorrect Note at the bottom of c16-proxy ("cpp-httplib
does not read HTTP_PROXY...") with the actual API.

JA is the master per the project's translation workflow; the EN
translation lands in the same PR. Both pages remain `status: "draft"`
for normal review.

Adds two sections:

  - Bypass the proxy for specific hosts (set_no_proxy):
    pattern syntax, dot-boundary rule, case-insensitivity, IP
    normalization via inet_pton, port-specific-entries unsupported,
    malformed entries dropped.

  - Read proxy settings from the environment (set_proxy_from_env):
    which variables are read, lowercase-only http_proxy with an
    inline httpoxy / CVE-2016-5385 explanation, threading caveat.

* Simplify NO_PROXY implementation per review

Apply seven post-implementation cleanups:

  - Move ProxyUrl, ProxyEnvSettings and most helper forward declarations
    below the BORDER. Only NoProxyKind/NoProxyEntry/NormalizedTarget stay
    above (they are used as ClientImpl members or by inline cache state).
    This shrinks the public header surface area considerably.

  - Drop ProxyUrl::scheme: the field was write-only after parsing. Track
    is_https as a local during parse_proxy_url and use it for the
    default-port branch directly.

  - Hoist the duplicate is_proxy_enabled_for_host(host_) gate in
    write_request: the previous form had two adjacent gates bracketing
    an unrelated end-server bearer-token block. Reordering puts the two
    proxy-auth blocks together under a single gate.

  - Drop the redundant trim_copy + empty-check inside parse_no_proxy_list:
    detail::split already trims each token and skips empties, so the inner
    work was dead code.

  - Cache normalize_target(host_) on the client. host_ is const, so the
    normalized form is invariant for the client's lifetime. The gate is
    called up to 7 times per request when NO_PROXY is configured;
    caching avoids repeating two heap allocations + two inet_pton calls
    per request. Cross-host calls (only setup_redirect_client passing
    next_host) still compute fresh.

  - Trim narrative comments in setup_redirect_client and
    set_proxy_from_env: replace WHAT-narration with single-line WHY
    statements.

  - Drop test comments that paraphrased their own test name.

All 635 unit tests pass under both the regular and split builds.

* Inline proxy URL parsing and env reading; drop intermediate structs

The previous design had two intermediate structs that existed only to
ferry parsed values between helper functions and the consuming method:

  - detail::ProxyUrl: filled by parse_proxy_url, drained back into
    proxy_host_ / proxy_port_ / proxy_basic_auth_* by set_proxy_from_env.
  - detail::ProxyEnvSettings: bundle of two ProxyUrl + a NoProxyEntry
    vector returned by read_proxy_env, drained by set_proxy_from_env.

Both bundles had exactly one producer and exactly one consumer. Drop
them and let the parsing flow directly into ClientImpl state:

  - New private member ClientImpl::apply_proxy_url(url) parses a proxy
    URL and, on success, assigns the result to proxy_host_, proxy_port_,
    and proxy_basic_auth_*. Same validation as before (CRLF rejection,
    scheme allowlist, port range, IPv6 bracket validation), same commit-
    on-success ordering — the local variables are kept until every check
    has passed so a malformed URL leaves no partial state.

  - set_proxy_from_env now reads getenv() directly, dispatches between
    https_proxy / http_proxy via virtual is_ssl(), and applies via
    apply_proxy_url. NO_PROXY is parsed in place via parse_no_proxy_list.

Net effect:

  - Two structs and two free helper functions removed (~150 lines of
    declaration + body deleted).
  - set_proxy_from_env body grows ~20 lines (still well under 50).
  - Per-request hot path is unchanged (NoProxyEntry / NormalizedTarget
    cache stays). Setup path is marginally faster (no intermediate
    string copies through ProxyUrl / ProxyEnvSettings).

635 unit tests pass under both the regular and split builds.

* Trim doc comments to match the rest of httplib.h

The new code carried inline doc comments (15-line set_no_proxy block,
18-line set_proxy_from_env block, plus narrating comments inside parser
bodies, plus section dividers in the test file) that were heavy
compared to the rest of the codebase — neighboring setters like
set_proxy / set_proxy_basic_auth carry no doc at all, the test file
does not use sub-section dividers, and the README / cookbook already
document the behavior in detail.

Removed:
  - Public-API doc blocks on set_no_proxy and set_proxy_from_env.
  - Narrating comments inside parse_no_proxy_entry, normalize_target,
    apply_proxy_url, host_matches_no_proxy that were just describing
    the obvious code structure.
  - Multi-line BORDER-rationale meta comments.
  - In-test sub-section dividers ("// ---- Hostname suffix matching",
    etc.) and per-class doc comments on the test fixtures.
  - Test-side comments that paraphrased their own test name.
  - Redundant ordering comments inside setup_redirect_client.

Kept:
  - Security WHY comments (CRLF rejection, dot-boundary suffix matching,
    httpoxy / CVE-2016-5385, GHSA-6hrp-7fq9-3qv2 analog, CVE-2026-21428).
  - Regression-target WHY comments (UB shift on prefix=0).
  - Non-obvious external knowledge (detail::split already trims).

635 unit tests still pass under both the regular and split builds.

* Add NO_PROXY tests covering edge cases found during PR review

Three regression guards added during review of an alternate NO_PROXY
implementation (PR #2449). All three pass on the current implementation
and surface bugs in the alternate one:

  - BareIPv6LiteralMatchesIPv6Cidr: a host given as a bare IPv6 literal
    (no surrounding brackets) must still be recognized as IPv6 for CIDR
    matching. An implementation that only detects IPv6 when the host
    string starts with '[' would split the host at the first ':' and
    misclassify it as a hostname.

  - TrailingDotOnEntryIsNormalized: trailing dots must be canonicalized
    on BOTH sides — host and entry. An implementation that strips the
    host-side trailing dot only would fail to match host "example.com"
    against entry "example.com." because the substring lengths differ.

  - ValidEntryWithSurroundingWhitespaceStillMatches: an entry with
    leading/trailing whitespace must still match. An implementation
    that feeds raw tokens directly to inet_pton would reject valid
    CIDRs ("  10.0.0.0/8  ") because of the spaces.

635 unit tests pass.

* Unify IPv4/IPv6 CIDR matching into a single byte-buffer helper

Adopts the unified 16-byte address representation suggested by the
alternate NO_PROXY implementation in PR #2449. Both v4 and v6 entries
now share one storage type and one matcher; the v4/v6 distinction is
only the address-family flag and the max prefix length.

  - detail::NoProxyEntry: replaces in_addr v4_net + in6_addr v6_net
    with a single IPBytes net (std::array<uint8_t, 16>). v4 occupies
    the first 4 bytes, v6 fills all 16.
  - detail::NormalizedTarget: replaces in_addr v4 + in6_addr v6 with
    a single IPBytes ip.
  - Replaces detail::ipv4_in_cidr and detail::ipv6_in_cidr with one
    detail::ip_in_cidr that takes the address, the network, the prefix
    length and the family's max bits (32 for v4, 128 for v6). The mask
    is constructed by the byte-fill approach from the previous v6
    helper, which is straightforward to read and avoids the shift UB
    that the v4 helper had to special-case.
  - The NoProxyKind enum keeps IPv4Cidr / IPv6Cidr as separate values
    so the match dispatch stays explicit and IPv4 entries cannot
    accidentally cross-match an IPv6 target (the same address-family
    isolation the previous code had).

Net change: -28 lines + -1 helper function. All 30 NoProxyTest cases
plus 643 unit tests pass under both the regular and split builds.

* Drop set_proxy_from_env per #2446 discussion

Per @unterwegi's feedback in #2446, environment variable handling
conflicts with cpp-httplib's long-standing policy of explicit
configuration (e.g. set_ca_cert_path requires explicit paths instead
of reading SSL_CERT_FILE / SSL_CERT_DIR). The NO_PROXY matching logic
is the genuinely tricky part worth keeping in the library; getenv
parsing is trivial and is left to the caller.

- Remove Client::set_proxy_from_env, ClientImpl::set_proxy_from_env,
  and ClientImpl::apply_proxy_url
- Remove ScopedEnv test helper and env-driven NoProxyTest cases
- Replace the "Read proxy settings from the environment" docs with a
  short snippet showing how to parse no_proxy and feed set_no_proxy()
- Keep set_no_proxy() and all NO_PROXY pattern matching intact

* docs: blend NO_PROXY env-var note into c16-proxy cookbook style

Match the granularity of the surrounding sections: imperative heading,
inline paragraph instead of a heavyweight callout, and a simpler getenv
snippet without the C++17 if-init.

* Skip digest 407 retry when target is bypassed by NO_PROXY

Before this fix, a NO_PROXY-bypassed origin that returns
407 Proxy-Authentication-Required with a Digest challenge would
trigger the same retry path the proxy uses, computing a
Proxy-Authorization header from proxy_digest_auth_* and sending the
user's proxy credentials directly to that (potentially hostile)
origin.

A 407 from a direct origin is semantically meaningless — RFC 9110
defines it strictly as a proxy response. Skip the retry when the
current target is not actually going through the proxy and let the
407 propagate to the caller unchanged.

Regression test BypassedTargetReturning407DoesNotLeakProxyDigest
Credentials reproduces the leak without this gate.

* Make set_no_proxy safe across redirects and keep-alive

Two correctness bugs that the dynamic NO_PROXY API exposed:

1. Multi-hop redirect through a bypassed host lost the proxy.
   setup_redirect_client only copied proxy_host_/port and the proxy auth
   credentials when is_proxy_enabled_for_host(next_host) was true. After
   a chain like A (proxied) -> B (NO_PROXY-matched, direct) -> C, the
   redirect client built for B had no proxy configured, so the further
   B -> C hop went direct even when C should have been proxied. Copy the
   proxy configuration unconditionally and let is_proxy_enabled_for_host
   gate at send time. The next_host parameter is no longer needed and
   removed from the signature.

2. Keep-alive socket reuse with a stale bypass decision. set_proxy() /
   set_no_proxy() left the existing keep-alive socket open, so the next
   request reused a socket pointed at the previous endpoint (proxy vs
   origin) while write_request emitted the new request-line form
   (absolute vs relative URL). Add invalidate_keep_alive_socket() and
   call it from both setters; the helper handles the in-flight case by
   deferring the close.

Regression tests MultiHopRedirectThroughBypassedHostKeepsProxy and
KeepAliveSocketInvalidatedOnSetNoProxy reproduce each bug without the
respective fix.

* Tighten NO_PROXY entry parsing

Three small parser fixes surfaced during code review:

- Accept bracketed IPv6 entries like "[::1]" and "[fe80::]/10". Users
  coming from URL syntax naturally write the bracketed form; previously
  it was silently rejected because inet_pton does not accept brackets
  and the subsequent ':' check tripped.
- Reject malformed trailing-slash CIDRs like "127.0.0.1/" instead of
  silently treating them as /32 (or /128). A typoed entry quietly
  turning into a single-host bypass changes semantics with no
  diagnostic.
- Delete detail::parse_no_proxy_list — leftover from the removed
  set_proxy_from_env path, no longer called from anywhere.

New regression tests: BracketedIPv6EntryAccepted,
BracketedIPv6CidrEntryAccepted, TrailingSlashCidrIsRejected.

* Refactor: introduce disconnect() and remove invalidate_keep_alive_socket

Replace the repeated `shutdown_ssl + shutdown_socket + close_socket`
pattern with a single `disconnect(bool gracefully)` helper. Used by
`stop()`, the send_() peer-closed and epilogue branches, and the close
in process_request after a non-keep-alive response.

Drop `invalidate_keep_alive_socket()` — its body collapses to a
`lock + disconnect()` pair which is now inlined in `set_proxy()` and
`set_no_proxy()` directly.

Also simplify `setup_redirect_client`: drop the now-unused next_host
parameter and the verbose comment block; the per-target proxy decision
is re-evaluated at send time anyway.

Net -47 lines in httplib.h.

* Fix MultiHopRedirect test on Windows; trim NoProxyTest comments

The bypass leg redirected to "http://localhost:<port>/...", but on
Windows `localhost` resolves to ::1 first while the mock server is
bound to 127.0.0.1, causing the redirect leg to time out. Use the
literal 127.0.0.1 in the Location and switch the NO_PROXY entry to
match, so the test exercises the same multi-hop path on every
platform.

Also trim the heavier inline comments and EXPECT messages I added on
recent NoProxyTest cases so they match the surrounding test style.

* Consolidate NoProxyTest server boilerplate; drop hardcoded sentinel ports

Add a small ScopedServer helper to no_proxy_test that wraps the
bind/listen/thread/cleanup dance (~13 lines per server before). Use it
to rewrite the four big tests (Redirect, BypassedTarget407, MultiHop,
KeepAlive), shaving ~100 lines.

Also drop the hardcoded port-1 / port-80 sentinels that violated the
"new standalone tests MUST use bind_to_any_port" convention and risked
collisions across gtest shards: re-use existing dynamic ports
(target.port() / bypass_server.port()) instead.

Verified pass under 4-shard parallel run.

* Trim README NO_PROXY section to match surrounding granularity

The block had ballooned to 62 lines while neighboring subsections
(Authentication, Proxy server support, Range, Redirect) are 13-18 each.
Collapse to a single code example + one-line behavior summary; point at
the cookbook for the entry-form details, env-var parsing snippet, and
httpoxy note that used to live inline.
2026-05-24 23:50:48 -04:00
yhirose b1792ef29c Release v0.45.1 2026-05-24 20:58:48 -04:00
yhirose 0f3d063f0a ci: add best-effort BoringSSL job (#2456)
Adds Ubuntu and macOS CI jobs that build BoringSSL from source and exercise cpp-httplib's existing OpenSSL backend path (continue-on-error: best-effort). Makes SSLClientServerTest.TlsVerifyHostname backend-aware (BoringSSL is SAN-only per RFC 6125 §6.4.4). README notes BoringSSL as a best-effort variant with the C++14 and SAN-only caveats.
2026-05-24 02:48:46 -04:00
sakurai-ryuhei 0d7d637466 Fix zstd detection in installed httplibConfig.cmake (#2453) 2026-05-23 11:59:58 -04:00
yhirose 1ff0c8588d Fix iOS build break and modernize macOS Keychain cert loading (#2455)
* Replace deprecated SecTrustCopyAnchorCertificates on macOS

SecTrustCopyAnchorCertificates was deprecated in macOS 13. Switch to
SecTrustSettingsCopyCertificates, iterating over the System, Admin, and
User trust domains to retain equivalent coverage of anchor certificates.

* Restrict Keychain cert loading to macOS

TARGET_OS_MAC is true on all Apple platforms including iOS, tvOS, and
watchOS, which caused the keychain enumeration path to be compiled on
iOS where SecTrustSettingsCopyCertificates is unavailable.

Narrow the auto-enable and the Security.h include guards to
TARGET_OS_OSX, and emit an explicit #error when the user defines
CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on a non-macOS Apple platform,
directing them to use set_ca_cert_path() with a bundled CA file.

Addresses the iOS build break reported in #2454.

* Add iOS header parse check to CI

Run a cross-compile syntax check against the iOS SDK to catch
accidental use of macOS-only APIs or guards (e.g. TARGET_OS_MAC vs
TARGET_OS_OSX) that would silently break iOS builds. Also verify that
defining CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN on iOS fires the
expected #error.

iOS is not officially supported as a runtime target; this job only
guarantees the header stays parse-clean on iOS toolchains.
2026-05-23 08:39:45 -04:00
NsPro04 b1cc8095a8 Specifying "Server::stop()" as noexcept (#2451)
* The current implementation of "Server::stop()" doesn't throw an exception, so why not specify this explicitly?

* Adding the missing "noexcept" to the declaration
2026-05-16 09:50:08 -04:00
yhirose 28f8264d13 Release v0.45.0 2026-05-15 09:22:11 +09:00
yhirose 91271c062d Fix keep-alive corruption on requests without framed body (#2450) 2026-05-15 06:57:51 +09:00
yhirose d755c43d58 Extract has_framed_body and is_connection_persistent helpers 2026-05-15 06:56:16 +09:00
yhirose 5c9285776e Fix crash on empty X-Forwarded-For with trusted proxies configured 2026-05-14 23:19:36 +09:00
9 changed files with 1903 additions and 131 deletions
+197
View File
@@ -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: >
+14
View File
@@ -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
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.44.0"
version = "0.46.1"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+36 -1
View File
@@ -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_`.
+36 -1
View File
@@ -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_`で始まらないので、どちらの大文字小文字でも安全です。
+445 -124
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.44.0"
#define CPPHTTPLIB_VERSION_NUM "0x002c00"
#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());
@@ -8578,17 +8639,24 @@ write_multipart_ranges_data(Stream &strm, const Request &req, Response &res,
});
}
inline bool has_framed_body(const Request &req) {
return is_chunked_transfer_encoding(req.headers) ||
req.get_header_value_u64("Content-Length") > 0;
}
inline bool is_connection_persistent(const Request &req) {
auto conn = req.get_header_value("Connection");
if (conn == "close") { return false; }
if (req.version == "HTTP/1.0" && conn != "Keep-Alive") { return false; }
return true;
}
inline bool expect_content(const Request &req) {
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
req.method == "DELETE") {
return true;
}
if (req.has_header("Content-Length") &&
req.get_header_value_u64("Content-Length") > 0) {
return true;
}
if (is_chunked_transfer_encoding(req.headers)) { return true; }
return false;
return has_framed_body(req);
}
#ifdef _WIN32
@@ -10498,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) {
@@ -10872,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;
@@ -10997,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));
@@ -11304,29 +11547,18 @@ inline bool Server::read_content_core(
size_t /*len*/) { return receiver(buf, n); };
}
// RFC 7230 Section 3.3.3: If this is a request message and none of the above
// are true (no Transfer-Encoding and no Content-Length), then the message
// body length is zero (no message body is present).
//
// For non-SSL builds, detect clients that send a body without a
// Content-Length header (raw HTTP over TCP). Check both the stream's
// internal read buffer (data already read from the socket during header
// parsing) and the socket itself for pending data. If data is found and
// exceeds the configured payload limit, reject with 413.
// For SSL builds we cannot reliably peek the decrypted application bytes,
// so keep the original behaviour.
// RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body.
// For non-SSL builds we still scan non-persistent connections for stray
// body bytes so the payload limit is enforced (413). On keep-alive,
// pending bytes may be the next request (issue #2450), so skip.
#if !defined(CPPHTTPLIB_SSL_ENABLED)
if (!req.has_header("Content-Length") &&
!detail::is_chunked_transfer_encoding(req.headers)) {
// Only check if payload_max_length is set to a finite value
if (payload_max_length_ > 0 &&
if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 &&
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
// Check if there is data already buffered in the stream (read during
// header parsing) or pending on the socket. Use a non-blocking socket
// check to avoid deadlock when the client sends no body.
bool has_data = strm.is_readable();
auto has_data = strm.is_readable();
if (!has_data) {
socket_t s = strm.socket();
auto s = strm.socket();
if (s != INVALID_SOCKET) {
has_data = detail::select_read(s, 0, 0) > 0;
}
@@ -11578,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());
@@ -11888,6 +12122,11 @@ get_client_ip(const std::string &x_forwarded_for,
ip_list.emplace_back(std::string(b + r.first, b + r.second));
});
// A malformed X-Forwarded-For (empty, comma-only, whitespace-only) yields
// no segments. Signal "no client IP derived" with an empty string so the
// caller can fall back to the connection-level remote address.
if (ip_list.empty()) { return std::string(); }
for (size_t i = 0; i < ip_list.size(); ++i) {
auto ip = ip_list[i];
@@ -11978,7 +12217,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
if (!trusted_proxies_.empty() && req.has_header("X-Forwarded-For")) {
auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
req.remote_addr = get_client_ip(x_forwarded_for, trusted_proxies_);
auto derived = get_client_ip(x_forwarded_for, trusted_proxies_);
req.remote_addr = derived.empty() ? remote_addr : derived;
} else {
req.remote_addr = remote_addr;
}
@@ -12180,15 +12420,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
ret = write_response(strm, close_connection, req, res);
}
// Drain any unconsumed request body to prevent request smuggling on
// keep-alive connections.
if (!req.body_consumed_ && detail::expect_content(req)) {
int drain_status = 200; // required by read_content signature
// Drain any unconsumed framed body to prevent request smuggling on
// keep-alive. Without framing there is no body to drain — reading would
// consume the next request (issue #2450).
if (!req.body_consumed_ && detail::has_framed_body(req)) {
int dummy_status;
if (!detail::read_content(
strm, req, payload_max_length_, drain_status, nullptr,
strm, req, payload_max_length_, dummy_status, nullptr,
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
// Body exceeds payload limit or read error — close the connection
// to prevent leftover bytes from being misinterpreted.
connection_closed = true;
}
}
@@ -12309,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_;
@@ -12324,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_,
@@ -12397,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 {
@@ -12468,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);
}
}
@@ -12525,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);
}
});
@@ -12640,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) {
@@ -12936,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) +
@@ -12960,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_) {
@@ -12975,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 =
@@ -13142,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_);
@@ -13239,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(
@@ -13254,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));
}
@@ -13565,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_;
@@ -13576,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
}
@@ -13590,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);
@@ -14572,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_; }
@@ -14666,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,
@@ -14678,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) {
@@ -15379,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));
@@ -15608,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;
@@ -15721,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);
@@ -16144,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 =
@@ -16509,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;
@@ -19956,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;
@@ -20020,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);
@@ -20117,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
View File
@@ -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 ""
+1155
View File
File diff suppressed because it is too large Load Diff