Compare commits

..
14 Commits
Author SHA1 Message Date
yhirose 24e58ce5b2 Release v0.64.0 2026-10-29 17:08:34 -04:00
Kim, Hyuk a0ab09fc0a Add set_basic_auth and set_bearer_token_auth to WebSocketClient (#2652)
ClientImpl and SSLClient let callers set origin credentials with
set_basic_auth()/set_bearer_token_auth(), but WebSocketClient could only
take a fixed Authorization header through its constructor. Add both
setters: each connect() adds the Authorization header to the upgrade
request from the current credentials (Basic over Bearer), unless the
constructor headers already carry one, and never to the proxy's CONNECT
request.
2026-10-28 16:15:17 -04:00
Kim, Hyuk 290c4c0bbe Add set_tunnel_hostname_addr_map() and deprecate set_hostname_addr_map() (#2647)
Through a CONNECT proxy it is the proxy that connects to the origin, and the CONNECT target was always the original hostname, so there was no way to send the tunnel to another address. set_tunnel_hostname_addr_map() sets that target on SSLClient, Client and WebSocketClient. The CONNECT request's Host follows it, including the Digest retry, while the Host header, the SNI and the certificate check inside the tunnel keep the original hostname. The map is also copied to a redirect client.

set_hostname_addr_map() is deprecated in favor of set_direct_hostname_addr_map(), which behaves the same and names the connection it applies to. The two maps are independent and correspond to curl's --resolve and --connect-to; neither applies to the proxy's own address.
2026-10-27 21:09:25 -04:00
metsw24-max 25ba40f6fe Reject an EOF without close_notify in the Mbed TLS and wolfSSL read (#2649)
Mbed TLS and wolfSSL read a TLS EOF without close_notify as a clean end, so a response delimited by the connection close could be cut short and still come back complete. Both reads now end cleanly only on a close_notify, as on OpenSSL.
2026-10-27 18:31:55 -04:00
metsw24-max 578e4815b7 Treat ':' as a path separator in sanitize_filename on Windows (#2651)
On Windows a ':' ends a drive prefix or starts an NTFS stream name, so a filename such as "C:.." or "a.txt:stream" survived sanitize_filename and could replace the upload directory or write to an alternate data stream. Cut at ':' on Windows as well.
2026-10-27 18:16:21 -04:00
Wleeaf 252dbaa505 Remove dot segments from absolute redirect locations (#2648)
resolve_relative_location removed "." and ".." segments only from a relative-path Location. A Location that starts with "/" or an absolute URI was requested as is, so a redirect to /dir/../target went out as /dir/../target and a server that does not normalize the path answered 404.

RFC 3986 section 5.2.2 applies remove_dot_segments to the target path of every reference that has a path, and Go and urllib3 do the same. The loop now lives in detail::remove_dot_segments, and ClientImpl::redirect applies it to an absolute path as well. Relative locations resolve as before.
2026-10-25 22:56:32 -04:00
metsw24-max 8710fbd25d Refuse a ':' in a path a mount point serves on Windows (#2643)
On NTFS a ':' in a path names a stream of the file, so /dir/test.html::$DATA opened the default stream of dir/test.html and passed a set_pre_routing_handler() guard on /dir/test.html, and /setup.exe:Zone.Identifier served the stream Windows attaches to downloaded files. is_valid_path() now refuses a ':' on Windows.

Elsewhere a ':' is an ordinary file name character, so it is still served: a file such as 2024-01-01T10:00:00.log would otherwise answer 404. The test pins both sides.
2026-10-25 22:39:44 -04:00
metsw24-max 2f8b9f6f57 Guard last_event_id() against the async event loop in SSEClient (#2644)
last_event_id() returned a reference to a string that run_event_loop() assigns on its own thread under start_async(), so a caller reading it from another thread raced the writes. The member is now guarded by a mutex and the getter returns a copy.

The return type changes from const std::string & to std::string. Code that binds the result to a const reference or copies it still compiles.
2026-10-25 22:21:55 -04:00
metsw24-maxandyhirose 391de32115 finish short writes in write_websocket_frame and open_stream_ (#2642)
write_websocket_frame and the request-body write in open_stream_ called
Stream::write once and only failed on a negative return, so a short
write (one record per mbedtls_ssl_write call, or SO_SNDTIMEO expiring
after part of the data went out) dropped the rest of the frame or body.
Route both through write_data, as the other body writers do.

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-10-25 22:19:53 -04:00
metsw24-max ba32b468a4 Honour Keychain trust settings when loading macOS system CAs (#2641)
The macOS system CA loader took every certificate that has Trust Settings, in the system, admin or user domain, as a trusted root without reading those settings. A root set to "Never Trust" in Keychain Access, or trusted only for S/MIME or code signing, still verified TLS servers.

The loader now evaluates the settings for the SSL policy. A per-user setting overrides the admin domain, which overrides the system domain, and only TrustRoot and TrustAsRoot are loaded. The OpenSSL path shares the helper that the Mbed TLS and wolfSSL paths already use.
2026-10-25 21:41:36 -04:00
Luochenchen 76f84a5bfd Honor set_decompress(false) in response streams (#2640)
open_stream_() created a decoder whatever set_decompress() said, so a response stream decoded the body when decompression was disabled, and failed with UnsupportedContentEncoding in a build without that decoder. The buffered path already checks the option; the streaming path now does too. The new test covers open_stream() and stream::Get().

Fixes #2639.
2026-10-25 20:05:56 -04:00
yhirose d23d1b6029 Match a host name against subjectAltName only
OpenSSL 4.1 no longer falls back to the subject Common Name when it
checks a host name (RFC 9525), so SSLClientServerTest.TlsVerifyHostname
began failing in the Windows CI job that installs the newest OpenSSL 4.x.

Make every backend agree instead of following whichever OpenSSL is
installed. OpenSSL passes X509_CHECK_FLAG_NEVER_CHECK_SUBJECT to both the
handshake check and verify_hostname(), and Mbed TLS and wolfSSL drop their
own CN fallback. A certificate that names its host only in the CN is now
rejected on all backends, as it already was on BoringSSL.
2026-10-25 18:58:43 -04:00
Wleeaf 7d349cbeed Follow only 301, 302, 303, 307 and 308 redirects (#2646)
is_redirect treated any 3xx with a Location as a redirect, so with set_follow_location(true) a 304 or 305 that carried a Location was followed. Only 301, 302, 303, 307 and 308 are redirects a client follows (RFC 9110 section 15.4), as in Go, requests and urllib3. A Location is still required. The 304 check in process_request that is_redirect now covers is dropped.
2026-10-25 18:55:20 -04:00
WleeafandWleeaf 5ceef83c54 Open a new connection for a redirect after sending Connection: close (#2645)
Without keep-alive, the client sends "Connection: close", but before
following a redirect it disconnected only when the response repeated
the option. RFC 9112 (section 9.6) lets a server close the connection
without repeating it, and a client that sent "close" must not send
another request on that connection. The redirected request was written
to the closing connection and failed with "Failed to read connection"
whenever the server closed it first. The client now disconnects before
a redirect or an auth retry whenever the request asked for the close.

Co-authored-by: Wleeaf <179125857+wleeaf@users.noreply.github.com>
2026-10-25 17:19:34 -04:00
11 changed files with 1427 additions and 184 deletions

No files matched your search

+1 -1
View File
@@ -100,7 +100,7 @@ sse.stop();
```cpp
bool connected = sse.is_connected();
const std::string &id = sse.last_event_id();
std::string id = sse.last_event_id();
```
## Examples
+22 -1
View File
@@ -200,6 +200,10 @@ void close(CloseStatus status = CloseStatus::Normal,
const std::string &reason = "");
bool is_open() const;
// Authentication - the same setters Client has (Basic wins over Bearer).
void set_basic_auth(const std::string &username, const std::string &password);
void set_bearer_token_auth(const std::string &token);
// Proxy - the same setters Client has. connect() opens a CONNECT tunnel
// through the proxy for ws:// as well as wss:// (RFC 6455 4.1); the proxy
// credentials are sent only on the CONNECT request, never through the tunnel.
@@ -233,7 +237,8 @@ void set_address_family(int family);
void set_ipv6_v6only(bool on);
void set_socket_options(SocketOptions socket_options);
void set_interface(const std::string &intf);
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
// SSL configuration (wss:// only, SSL builds only)
void set_ca_cert_path(const std::string &ca_cert_file_path,
@@ -403,6 +408,22 @@ svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &r
The server accepts a handshake from any `Origin`. A browser sends its cookies with a handshake another site starts, so if cookies authenticate the connection, also check `req.get_header_value("Origin")` against your own origins in one of these handlers (cross-site WebSocket hijacking).
### Authentication
```cpp
httplib::ws::WebSocketClient ws("wss://example.com/ws");
ws.set_bearer_token_auth("token123"); // Authorization: Bearer token123
// or
ws.set_basic_auth("user", "pass"); // Authorization: Basic dXNlcjpwYXNz
if (ws.connect()) { /* ... */ }
```
The credentials are read on every `connect()`, so a new token takes effect on
the next reconnect. An `Authorization` header passed to the constructor wins
over them. Through a proxy they are sent only inside the tunnel, never on
`CONNECT`.
### Custom Headers and Timeouts
```cpp
+39 -12
View File
@@ -76,7 +76,10 @@ cpp-httplib supports multiple TLS backends through an abstraction layer:
> A wolfSSL server that verifies client certificates does not resume TLS sessions: wolfSSL shares its session cache between contexts, so a resumed session could carry a certificate checked against another server's client CA.
> [!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).
> **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. One known difference vs OpenSSL: BoringSSL's public headers require C++14 or later, so consumers must compile accordingly.
> [!NOTE]
> **Hostname verification is SAN-only on every backend** (RFC 9525). A host is matched against the certificate's subjectAltName entries, never its Common Name, so a certificate that names its host only in the CN is rejected. Reissue it with a subjectAltName.
```c++
// Use either OpenSSL, Mbed TLS, or wolfSSL
@@ -240,7 +243,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
| Platform | Behavior | Disable (compile time) |
| :------- | :------- | :--------------------- |
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| macOS | Loads system certs from Keychain, honouring their Trust Settings: a certificate set to "Never Trust", or trusted only for another policy such as S/MIME, is not loaded (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend. Revocation checking is best-effort: a revoked server certificate is rejected, while one whose revocation status cannot be determined is accepted. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
On Windows, the CryptoAPI check can also be turned off at runtime. The TLS backend then verifies the chain instead, as on other platforms:
@@ -406,7 +409,7 @@ ret = svr.remove_mount_point("/");
ret = svr.remove_mount_point("/public");
```
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, and no segment ending in `.` or a space, so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, no segment ending in `.` or a space and, on Windows, no `:` (it names an NTFS stream), so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
```cpp
// User defined file extension and MIME type mappings
@@ -739,7 +742,7 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
auto safe = httplib::sanitize_filename(file.filename);
```
This function keeps only the part after the last `/` or `\`, removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`.
This function keeps only the part after the last `/` or `\` (or, on Windows, `:`, which starts a drive prefix such as `C:` or an NTFS stream name), removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`.
> [!NOTE]
> `name` and `filename` are read as quoted-strings (RFC 9110 §5.6.4), where `\"` and `\\` stand for `"` and `\`. Browsers send a backslash as it is, so from a browser, a value with `\\` in it arrives with a single `\`, and one that ends in `\` is not read correctly.
@@ -1474,17 +1477,17 @@ The same method is available on `httplib::ws::WebSocketClient`.
### Override the connection target for a hostname
`set_hostname_addr_map` redirects where the socket connects, without changing
the identity of the request. The hostname the client was constructed with keeps
supplying the `Host` header, the SNI, and the name that the server certificate
is verified against, so this is a connection-level override only, not a way to
talk to a different origin.
`set_direct_hostname_addr_map` redirects where the socket connects, without
changing the identity of the request. The hostname the client was constructed
with keeps supplying the `Host` header, the SNI, and the name that the server
certificate is verified against, so this is a connection-level override only,
not a way to talk to a different origin.
```cpp
httplib::Client cli("https://example.com");
// Connect to this IP address instead of resolving "example.com"
cli.set_hostname_addr_map({{"example.com", "192.168.1.10"}});
cli.set_direct_hostname_addr_map({{"example.com", "192.168.1.10"}});
```
A mapped value may be an IP literal or another hostname. An IP literal is used
@@ -1492,13 +1495,37 @@ as-is; anything else is resolved as a name, so a host that is only reachable
under a different name works too:
```cpp
cli.set_hostname_addr_map({{"example.com", "internal.example.lan"}});
cli.set_direct_hostname_addr_map({{"example.com", "internal.example.lan"}});
```
An empty value is ignored, leaving the original hostname as the connection
target.
The same method is available on `httplib::ws::WebSocketClient`.
The direct map applies only when the client connects to the origin itself.
Through a proxy, use `set_tunnel_hostname_addr_map`: its value becomes the
`CONNECT` target (and that request's `Host`) that the proxy is asked to connect
to (HTTPS and WebSocket). The `Host` header, the SNI and the certificate check
inside the tunnel keep the original hostname:
```cpp
httplib::Client cli("https://sni.example");
cli.set_tunnel_hostname_addr_map({{"sni.example", "server.example"}});
cli.set_proxy("proxy.example", 3128);
// CONNECT server.example:443, SNI sni.example
```
The two maps are independent; to use the same target on both paths, set both.
They correspond to curl's `--resolve` and `--connect-to`.
Neither map applies to the proxy's own address ("direct" means not through a
proxy, as in a PAC file's `DIRECT`). To connect to the proxy at a fixed
address, pass an IP literal to `set_proxy()`.
`set_hostname_addr_map` is deprecated and works the same as
`set_direct_hostname_addr_map`.
These methods are also available on `httplib::ws::WebSocketClient`.
### Automatic Path Encoding
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.63.0"
version = "0.64.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // The port is unused, but must not be 0
// Client
httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX);
cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
auto res = cli.Get("/");
```
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // ポート番号は使われない(0
// クライアント
httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX);
cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
auto res = cli.Get("/");
```
+311 -129
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.63.0"
#define CPPHTTPLIB_VERSION_NUM "0x003f00"
#define CPPHTTPLIB_VERSION "0.64.0"
#define CPPHTTPLIB_VERSION_NUM "0x004000"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -995,6 +995,30 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
return true;
}
// Removes "." and ".." segments from a path that starts with '/' (RFC 3986
// section 5.2.4).
inline std::string remove_dot_segments(const std::string &path) {
std::string out;
size_t i = 0;
while (i < path.size()) {
auto next = (std::min)(path.find('/', i + 1), path.size());
auto segment = path.substr(i + 1, next - i - 1);
auto is_last = next == path.size();
if (segment == "." || segment == "..") {
if (segment == "..") {
out.erase((std::min)(out.rfind('/'), out.size()));
}
if (is_last) { out += '/'; }
} else {
out += '/';
out += segment;
}
i = next;
}
if (out.empty()) { out = "/"; }
return out;
}
// Resolves a relative-path or query-only Location value against the path of
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
// references starting with '/' are returned unchanged.
@@ -1015,26 +1039,8 @@ inline std::string resolve_relative_location(const std::string &location,
// Remove "." and ".." segments from the merged path.
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
std::string path;
size_t i = 0;
while (i < path_end) {
auto next = (std::min)(merged.find('/', i + 1), path_end);
auto segment = merged.substr(i + 1, next - i - 1);
auto is_last = next == path_end;
if (segment == "." || segment == "..") {
if (segment == "..") {
path.erase((std::min)(path.rfind('/'), path.size()));
}
if (is_last) { path += '/'; }
} else {
path += '/';
path += segment;
}
i = next;
}
if (path.empty()) { path = "/"; }
return path + merged.substr(path_end);
return remove_dot_segments(merged.substr(0, path_end)) +
merged.substr(path_end);
}
} // namespace detail
@@ -2921,7 +2927,12 @@ public:
size_t is_socket_open() const;
socket_t socket() const;
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers);
@@ -3053,6 +3064,9 @@ protected:
// hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> tunnel_addr_map_;
// Default headers
Headers default_headers_;
@@ -3342,7 +3356,12 @@ public:
size_t is_socket_open() const;
socket_t socket() const;
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers);
@@ -3797,6 +3816,8 @@ template <typename T>
bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer,
Error &error);
bool write_data(Stream &strm, const char *d, size_t l);
std::string trim_copy(const std::string &s);
void divide(
@@ -4496,7 +4517,7 @@ public:
// State accessors
bool is_connected() const;
const std::string &last_event_id() const;
std::string last_event_id() const;
// Blocking start - runs event loop with auto-reconnect
void start();
@@ -4534,7 +4555,9 @@ private:
// State
std::atomic<bool> running_{false};
std::atomic<bool> connected_{false};
// Written by the event loop, which start_async() runs on its own thread
std::string last_event_id_;
mutable std::mutex last_event_id_mutex_;
// Async support
std::thread async_thread_;
@@ -4738,7 +4761,15 @@ public:
set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
void set_interface(const std::string &intf);
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_basic_auth(const std::string &username, const std::string &password);
void set_bearer_token_auth(const std::string &token);
void set_proxy(const std::string &host, int port);
void set_proxy_basic_auth(const std::string &username,
@@ -4810,6 +4841,13 @@ private:
// hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> tunnel_addr_map_;
std::string basic_auth_username_;
std::string basic_auth_password_;
std::string bearer_token_auth_token_;
std::string proxy_host_;
int proxy_port_ = -1;
std::string proxy_basic_auth_username_;
@@ -5032,7 +5070,8 @@ inline SSEClient &SSEClient::set_headers(const Headers &headers) {
inline bool SSEClient::is_connected() const { return connected_.load(); }
inline const std::string &SSEClient::last_event_id() const {
inline std::string SSEClient::last_event_id() const {
std::lock_guard<std::mutex> lock(last_event_id_mutex_);
return last_event_id_;
}
@@ -5207,7 +5246,10 @@ inline void SSEClient::run_event_loop() {
if (event_complete) {
// Update last_event_id for reconnection, even for an event that
// has no data. An empty id clears it.
if (has_id) { last_event_id_ = current_msg.id; }
if (has_id) {
std::lock_guard<std::mutex> lock(last_event_id_mutex_);
last_event_id_ = current_msg.id;
}
// An event without a data field is not dispatched
if (has_data) { dispatch_event(current_msg); }
@@ -5344,6 +5386,8 @@ cert_t get_peer_cert(const_session_t session);
// do not use them after free_session(), as with get_peer_cert().
size_t get_peer_certs(const_session_t session, std::vector<cert_t> &certs);
void free_cert(cert_t cert);
// Matches the host against the subjectAltName entries only, never the Common
// Name (RFC 9525).
bool verify_hostname(cert_t cert, const char *hostname);
uint64_t hostname_mismatch_code();
long get_verify_result(const_session_t session);
@@ -5813,32 +5857,40 @@ inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
if (len < 126) {
header[1] = static_cast<uint8_t>(len);
if (mask) { header[1] |= 0x80; }
if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
if (!write_data(strm, reinterpret_cast<char *>(header), 2)) {
return false;
}
} else if (len <= 0xFFFF) {
header[1] = 126;
if (mask) { header[1] |= 0x80; }
if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
if (!write_data(strm, reinterpret_cast<char *>(header), 2)) {
return false;
}
uint8_t ext[2];
ext[0] = static_cast<uint8_t>((len >> 8) & 0xFF);
ext[1] = static_cast<uint8_t>(len & 0xFF);
if (strm.write(reinterpret_cast<char *>(ext), 2) < 0) { return false; }
if (!write_data(strm, reinterpret_cast<char *>(ext), 2)) { return false; }
} else {
header[1] = 127;
if (mask) { header[1] |= 0x80; }
if (strm.write(reinterpret_cast<char *>(header), 2) < 0) { return false; }
if (!write_data(strm, reinterpret_cast<char *>(header), 2)) {
return false;
}
uint8_t ext[8];
for (int i = 7; i >= 0; i--) {
ext[7 - i] =
static_cast<uint8_t>((static_cast<uint64_t>(len) >> (i * 8)) & 0xFF);
}
if (strm.write(reinterpret_cast<char *>(ext), 8) < 0) { return false; }
if (!write_data(strm, reinterpret_cast<char *>(ext), 8)) { return false; }
}
if (mask) {
// Generate random mask key
uint8_t mask_key[4];
fill_random_bytes(mask_key, sizeof(mask_key));
if (strm.write(reinterpret_cast<char *>(mask_key), 4) < 0) { return false; }
if (!write_data(strm, reinterpret_cast<char *>(mask_key), 4)) {
return false;
}
// Write masked payload in chunks
const size_t chunk_size = 4096;
@@ -5849,11 +5901,11 @@ inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
buf[i] =
data[offset + i] ^ static_cast<char>(mask_key[(offset + i) % 4]);
}
if (strm.write(buf.data(), n) < 0) { return false; }
if (!write_data(strm, buf.data(), n)) { return false; }
}
} else {
if (len > 0) {
if (strm.write(data, len) < 0) { return false; }
if (!write_data(strm, data, len)) { return false; }
}
}
@@ -5989,9 +6041,10 @@ inline bool is_valid_close_payload(const std::string &payload) {
namespace detail {
// A path a mount point serves: absolute, with no empty segment except a
// trailing '/' and no segment ending in '.' or a space (which covers "." and
// ".." and names Windows trims into another file's name), so the file served
// is the one that req.path names to the handlers that ran before it.
// trailing '/', no segment ending in '.' or a space (which covers "." and
// ".." and names Windows trims into another file's name) and, on Windows, no
// ':' (which names an NTFS stream, so "a.txt::$DATA" is a.txt), so the file
// served is the one that req.path names to the handlers that ran before it.
inline bool is_valid_path(const std::string &path) {
if (path.empty() || path[0] != '/') { return false; }
@@ -5999,6 +6052,9 @@ inline bool is_valid_path(const std::string &path) {
for (auto i = beg; i <= path.size(); i++) {
if (i < path.size() && path[i] != '/') {
if (path[i] == '\0' || path[i] == '\\') { return false; }
#ifdef _WIN32
if (path[i] == ':') { return false; }
#endif
continue;
}
auto len = i - beg;
@@ -11947,8 +12003,13 @@ inline std::string decode_query_component(const std::string &component,
}
inline std::string sanitize_filename(const std::string &filename) {
// Extract basename: find the last path separator (/ or \)
// Extract basename: find the last path separator (/ or \) and, on Windows,
// ':', which ends a drive prefix ("C:..") or names an NTFS stream ("a:b")
#ifdef _WIN32
auto pos = filename.find_last_of("/\\:");
#else
auto pos = filename.find_last_of("/\\");
#endif
auto result =
(pos != std::string::npos) ? filename.substr(pos + 1) : filename;
@@ -13013,6 +13074,17 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
return prepare_host_string(host) + ":" + std::to_string(port);
}
// Request target of a CONNECT for `host`, with `addr_map` applied.
inline std::string
make_connect_request_target(const std::map<std::string, std::string> &addr_map,
const std::string &host, int port) {
std::string connect_host;
std::string ip;
apply_addr_map(addr_map, host, connect_host, ip);
return make_host_and_port_string_always_port(ip.empty() ? connect_host : ip,
port);
}
// Value for the Host header a client sends when the caller supplied none.
// Only the value: callers decide where in their header list it goes.
inline std::string make_default_host_header_value(const std::string &host,
@@ -15967,7 +16039,7 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
}
if (!req.body.empty()) {
if (strm.write(req.body.data(), req.body.size()) < 0) {
if (!detail::write_data(strm, req.body.data(), req.body.size())) {
handle.error = Error::Write;
handle.response.reset();
return handle;
@@ -16018,7 +16090,7 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
auto content_encoding = detail::get_combined_header_value(
handle.response->headers, "Content-Encoding");
if (!content_encoding.empty()) {
if (decompress_ && !content_encoding.empty()) {
// Same policy as prepare_content_receiver(): reject a coding we know about
// but were not built with, pass an unrecognized one through as-is.
handle.decompressor_ = detail::create_decompressor(content_encoding);
@@ -16213,10 +16285,18 @@ inline bool ChunkedDecoder::parse_trailers_into(Headers &dest,
return parse_trailers(lr, dest, src_headers);
}
// A 3xx without a Location, such as a 304, is a final response
// Only 301, 302, 303, 307 and 308 redirect. Any other 3xx, such as a 304, or
// a redirect without a Location, is a final response.
inline bool is_redirect(const Response &res) {
return 300 < res.status && res.status < 400 &&
!res.get_header_value("Location").empty();
switch (res.status) {
case StatusCode::MovedPermanently_301:
case StatusCode::Found_302:
case StatusCode::SeeOther_303:
case StatusCode::TemporaryRedirect_307:
case StatusCode::PermanentRedirect_308:
return !res.get_header_value("Location").empty();
default: return false;
}
}
} // namespace detail
@@ -16278,9 +16358,10 @@ 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_);
// Also when the request asked for it (e.g. a body that was never sent),
// so that a redirect or an auth retry below does not reuse the socket.
if (socket_should_be_closed_when_request_is_done_ ||
// Also when the request asked for it (e.g. a body that was never sent,
// or "Connection: close"), so that a redirect or an auth retry below does
// not reuse the socket.
if (socket_should_be_closed_when_request_is_done_ || close_connection ||
detail::has_header_token(res.headers, "Connection", "close") ||
(res.version == "HTTP/1.0" && res.reason != "Connection established")) {
disconnect(/*gracefully=*/true);
@@ -16374,6 +16455,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
if (next_scheme.empty()) { next_scheme = scheme; }
if (next_host.empty()) { next_host = host_; }
if (next_path.empty()) { next_path = "/"; }
// An absolute or absolute-path location can have dot segments too.
if (next_path[0] == '/') {
next_path = detail::remove_dot_segments(next_path);
}
auto path = std::move(next_path) + next_query;
@@ -16528,6 +16613,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
// Copy network and socket settings
client.addr_map_ = addr_map_;
client.tunnel_addr_map_ = tunnel_addr_map_;
client.set_address_family(address_family_);
client.set_tcp_nodelay(tcp_nodelay_);
client.set_ipv6_v6only(ipv6_v6only_);
@@ -17025,8 +17111,7 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
}
}
auto redirect = follow_location_ && detail::is_redirect(res) &&
res.status != StatusCode::NotModified_304;
auto redirect = follow_location_ && detail::is_redirect(res);
if (req.response_handler && !redirect) {
if (!req.response_handler(res)) {
@@ -18041,9 +18126,19 @@ inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
inline void
ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
set_direct_hostname_addr_map(std::move(addr_map));
}
inline void ClientImpl::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
inline void ClientImpl::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
tunnel_addr_map_ = std::move(addr_map);
}
inline void ClientImpl::set_default_headers(Headers headers) {
default_headers_ = std::move(headers);
}
@@ -18752,7 +18847,17 @@ inline socket_t Client::socket() const { return cli_->socket(); }
inline void
Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
cli_->set_hostname_addr_map(std::move(addr_map));
cli_->set_direct_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
cli_->set_direct_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
cli_->set_tunnel_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_default_headers(Headers headers) {
@@ -19120,6 +19225,10 @@ inline bool SSLClient::connect_with_proxy(
std::chrono::time_point<std::chrono::steady_clock> start_time,
Response &res, bool &success, Error &error) {
success = true;
auto connect_target =
detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
auto retargeted = connect_target !=
detail::make_host_and_port_string_always_port(host_, port_);
Response proxy_res;
if (!detail::process_client_socket(
socket.sock, read_timeout_sec_, read_timeout_usec_,
@@ -19127,8 +19236,8 @@ inline bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) {
Request req2;
req2.method = "CONNECT";
req2.path =
detail::make_host_and_port_string_always_port(host_, port_);
req2.path = connect_target;
if (retargeted) { req2.set_header("Host", connect_target); }
if (max_timeout_msec_ > 0) {
req2.start_time_ = std::chrono::steady_clock::now();
}
@@ -19167,8 +19276,8 @@ inline bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) {
Request req3;
req3.method = "CONNECT";
req3.path = detail::make_host_and_port_string_always_port(
host_, port_);
req3.path = connect_target;
if (retargeted) { req3.set_header("Host", connect_target); }
req3.headers.insert(detail::make_digest_authentication_header(
req3, make_request_target(req3), auth, 1,
detail::random_string(10), proxy_digest_auth_username_,
@@ -19582,15 +19691,75 @@ inline bool enumerate_windows_system_certs(Callback cb) {
#endif
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
// Enumerate macOS Keychain certificates and call callback with DER data
// The trust a certificate's Trust Settings give it for SSL server
// verification (SecTrustSettings.h). An empty array trusts it as a root.
// Otherwise the first usage-constraints dictionary that applies decides: one
// naming a policy other than SSL, a host name or an application does not
// apply, and a result left out means TrustRoot. Keychain Access writes "Never
// Trust" as kSecTrustSettingsResultDeny.
inline SecTrustSettingsResult
macos_ssl_trust_settings_result(CFArrayRef trust_settings) {
auto count = CFArrayGetCount(trust_settings);
if (count == 0) { return kSecTrustSettingsResultTrustRoot; }
for (CFIndex i = 0; i < count; i++) {
auto value = CFArrayGetValueAtIndex(trust_settings, i);
if (!value || CFGetTypeID(value) != CFDictionaryGetTypeID()) { continue; }
auto constraint = static_cast<CFDictionaryRef>(value);
auto policy_value =
CFDictionaryGetValue(constraint, kSecTrustSettingsPolicy);
if (policy_value) {
auto policy = static_cast<SecPolicyRef>(const_cast<void *>(policy_value));
auto is_ssl = false;
if (auto properties = SecPolicyCopyProperties(policy)) {
auto oid = CFDictionaryGetValue(properties, kSecPolicyOid);
is_ssl = oid && CFEqual(oid, kSecPolicyAppleSSL);
CFRelease(properties);
}
if (!is_ssl) { continue; }
}
if (CFDictionaryContainsKey(constraint, kSecTrustSettingsPolicyString) ||
CFDictionaryContainsKey(constraint, kSecTrustSettingsApplication)) {
continue;
}
auto result = kSecTrustSettingsResultTrustRoot;
auto number = static_cast<CFNumberRef>(
CFDictionaryGetValue(constraint, kSecTrustSettingsResult));
if (number) {
SInt32 number_value = 0;
if (!CFNumberGetValue(number, kCFNumberSInt32Type, &number_value)) {
continue;
}
result = static_cast<SecTrustSettingsResult>(number_value);
}
if (result == kSecTrustSettingsResultTrustRoot ||
result == kSecTrustSettingsResultTrustAsRoot ||
result == kSecTrustSettingsResultDeny) {
return result;
}
}
return kSecTrustSettingsResultUnspecified;
}
// Enumerate the macOS Keychain certificates trusted for SSL and call callback
// with DER data. A certificate with Trust Settings is not trusted just by
// having them: the settings say whether it is, and a per-user setting
// overrides the admin domain's, which overrides the system domain's (whose
// roots are all trusted). So a system root the user set to "Never Trust" is
// left out, as is one trusted only for S/MIME or code signing.
template <typename Callback>
inline bool enumerate_macos_keychain_certs(Callback cb) {
bool loaded = false;
const SecTrustSettingsDomain domains[] = {
kSecTrustSettingsDomainSystem,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainUser,
kSecTrustSettingsDomainAdmin,
kSecTrustSettingsDomainSystem,
};
std::set<std::string> decided;
std::vector<std::string> trusted;
for (auto domain : domains) {
CFArrayRef certs = nullptr;
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
@@ -19603,16 +19772,43 @@ inline bool enumerate_macos_keychain_certs(Callback cb) {
SecCertificateRef cert =
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
CFDataRef data = SecCertificateCopyData(cert);
if (data) {
if (cb(CFDataGetBytePtr(data),
static_cast<size_t>(CFDataGetLength(data)))) {
loaded = true;
if (!data) { continue; }
std::string der(reinterpret_cast<const char *>(CFDataGetBytePtr(data)),
static_cast<size_t>(CFDataGetLength(data)));
CFRelease(data);
// Decided by a domain that overrides this one
if (decided.find(der) != decided.end()) { continue; }
auto result = kSecTrustSettingsResultTrustRoot;
if (domain != kSecTrustSettingsDomainSystem) {
result = kSecTrustSettingsResultUnspecified;
CFArrayRef trust_settings = nullptr;
if (SecTrustSettingsCopyTrustSettings(cert, domain, &trust_settings) ==
errSecSuccess &&
trust_settings) {
result = macos_ssl_trust_settings_result(trust_settings);
}
CFRelease(data);
if (trust_settings) CFRelease(trust_settings);
}
// Neither trusted nor distrusted here; a domain below may decide
if (result == kSecTrustSettingsResultUnspecified) { continue; }
decided.insert(der);
if (result == kSecTrustSettingsResultTrustRoot ||
result == kSecTrustSettingsResultTrustAsRoot) {
trusted.push_back(std::move(der));
}
}
CFRelease(certs);
}
bool loaded = false;
for (const auto &der : trusted) {
if (cb(reinterpret_cast<const unsigned char *>(der.data()), der.size())) {
loaded = true;
}
}
return loaded;
}
#endif
@@ -20046,36 +20242,14 @@ inline bool load_system_certs(ctx_t ctx) {
auto store = SSL_CTX_get_cert_store(ssl_ctx);
if (!store) return 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);
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);
}
auto loaded_any = impl::enumerate_macos_keychain_certs(
[&](const unsigned char *data, size_t len) {
auto x509 = d2i_X509(nullptr, &data, static_cast<long>(len));
if (!x509) { return false; }
auto added = X509_STORE_add_cert(store, x509) == 1;
X509_free(x509);
return added;
});
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
#else
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
@@ -20189,10 +20363,12 @@ inline bool set_sni(session_t session, const char *hostname,
auto ssl = static_cast<SSL *>(session);
// The handshake's chain verification then checks the name as well
if (verify_hostname &&
X509_VERIFY_PARAM_set1_host(SSL_get0_param(ssl), hostname,
strlen(hostname)) != 1) {
return false;
if (verify_hostname) {
auto param = SSL_get0_param(ssl);
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NEVER_CHECK_SUBJECT);
if (X509_VERIFY_PARAM_set1_host(param, hostname, strlen(hostname)) != 1) {
return false;
}
}
#if defined(OPENSSL_IS_BORINGSSL)
@@ -20441,7 +20617,8 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
if (detail::is_ip_address(hostname)) {
return X509_check_ip_asc(x509, hostname, 0) == 1;
}
return X509_check_host(x509, hostname, strlen(hostname), 0, nullptr) == 1;
return X509_check_host(x509, hostname, strlen(hostname),
X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, nullptr) == 1;
}
inline uint64_t hostname_mismatch_code() {
@@ -20932,6 +21109,11 @@ inline ErrorCode map_mbedtls_error(int ret, int &out_errno,
out_errno = errno;
return ErrorCode::SyscallError;
}
if (ret == MBEDTLS_ERR_SSL_CONN_EOF) {
// EOF without close_notify
out_errno = 0;
return ErrorCode::SyscallError;
}
if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
// Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
// the handshake's chain verification (see set_sni()); a mismatch there
@@ -21019,7 +21201,7 @@ inline int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) {
return MBEDTLS_ERR_NET_RECV_FAILED;
}
#endif
if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; }
// EOF is MBEDTLS_ERR_SSL_CONN_EOF; only a close_notify is a clean end
return static_cast<int>(ret);
}
@@ -21675,10 +21857,8 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
return static_cast<ssize_t>(ret);
}
if (ret == 0) {
err.code = ErrorCode::PeerClosed;
return 0;
}
// 0 means EOF without close_notify, which is an error as on OpenSSL
if (ret == 0) { ret = MBEDTLS_ERR_SSL_CONN_EOF; }
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
err.backend_code = static_cast<uint64_t>(-ret);
@@ -21827,7 +22007,6 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
// in buf.tag and the raw value in buf.p / buf.len.
auto has_dns_san = false;
for (auto san = &mcert->subject_alt_names; san; san = san->next) {
if (!san->buf.p || san->buf.len == 0) { continue; }
const unsigned char *p = san->buf.p;
@@ -21842,24 +22021,11 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
return true;
}
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
has_dns_san = true;
std::string san_name(reinterpret_cast<const char *>(p), len);
if (detail::match_hostname(san_name, host_str)) { return true; }
}
}
// Fallback: the Common Names (CN) in the subject, as X509_check_host does it:
// never for an IP host, which only an iPAddress SAN names, and never next to
// a DNS SAN (RFC 6125 6.4.4).
if (is_ip || has_dns_san) { return false; }
for (auto name = &mcert->subject; name; name = name->next) {
if (MBEDTLS_OID_CMP(MBEDTLS_OID_AT_CN, &name->oid) == 0) {
std::string cn(reinterpret_cast<const char *>(name->val.p),
name->val.len);
if (detail::match_hostname(cn, host_str)) { return true; }
}
}
return false;
}
@@ -22299,6 +22465,8 @@ inline ErrorCode map_wolfssl_error(WOLFSSL *ssl, int ssl_error,
case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite;
case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;
// EOF without close_notify
case SOCKET_PEER_CLOSED_E: out_errno = 0; return ErrorCode::SyscallError;
default:
if (ssl) {
// wolfSSL stores the low-level error code as a negative value.
@@ -22869,13 +23037,10 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
return static_cast<ssize_t>(ret);
}
if (ret == 0) {
err.code = ErrorCode::PeerClosed;
return 0;
}
// 0 is also returned for EOF without close_notify; get_error tells them apart
int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
if (err.code == ErrorCode::PeerClosed) { return 0; }
err.backend_code = static_cast<uint64_t>(ssl_error);
impl::wolfssl_last_error() = err.backend_code;
return -1;
@@ -23007,7 +23172,6 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
auto is_ip = ip_len > 0;
// Check Subject Alternative Names
auto has_dns_san = false;
auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));
@@ -23020,7 +23184,6 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
if (!is_ip && names->type == WOLFSSL_GEN_DNS) {
// DNS name
has_dns_san = true;
unsigned char *dns_name = nullptr;
int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName);
if (dns_name && dns_len > 0) {
@@ -23047,12 +23210,7 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
wolfSSL_sk_free(san_names);
}
// Fallback: the Common Name (CN) in the subject, as X509_check_host does it:
// never for an IP host, which only an iPAddress SAN names, and never next to
// a DNS SAN (RFC 6125 6.4.4).
if (is_ip || has_dns_san) { return false; }
auto cn = get_cert_subject_cn(cert);
return !cn.empty() && detail::match_hostname(cn, host_str);
return false;
}
inline uint64_t hostname_mismatch_code() {
@@ -23839,6 +23997,9 @@ inline void WebSocketClient::prepare_default_headers(Request &req) {
}
detail::add_default_user_agent_header(req);
detail::add_authorization_header(req, basic_auth_username_,
basic_auth_password_,
bearer_token_auth_token_, false);
}
inline Result WebSocketClient::connect() {
@@ -23964,9 +24125,19 @@ inline void WebSocketClient::set_interface(const std::string &intf) {
inline void WebSocketClient::set_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
set_direct_hostname_addr_map(std::move(addr_map));
}
inline void WebSocketClient::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
inline void WebSocketClient::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
tunnel_addr_map_ = std::move(addr_map);
}
inline bool WebSocketClient::is_proxy_enabled() const {
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
if (no_proxy_entries_.empty()) { return true; }
@@ -24016,7 +24187,8 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
Error &error) {
Request req;
req.method = "CONNECT";
req.path = detail::make_host_and_port_string_always_port(host_, port_);
req.path =
detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
req.headers.emplace("Host", req.path);
detail::add_default_user_agent_header(req);
detail::add_authorization_header(req, proxy_basic_auth_username_,
@@ -24050,6 +24222,16 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
return true;
}
inline void WebSocketClient::set_basic_auth(const std::string &username,
const std::string &password) {
basic_auth_username_ = username;
basic_auth_password_ = password;
}
inline void WebSocketClient::set_bearer_token_auth(const std::string &token) {
bearer_token_auth_token_ = token;
}
inline void WebSocketClient::set_proxy(const std::string &host, int port) {
proxy_host_ = host;
proxy_port_ = port;
+8
View File
@@ -153,6 +153,14 @@ if(HTTPLIB_IS_USING_OPENSSL)
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# cert_dns_cn.pem: CN is the DNS name the tests connect to, with NO
# subjectAltName. A host is named by a SAN only (RFC 9525),
# so verifying it against this cert must fail.
execute_process(
COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=localhost -out cert_dns_cn.pem
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not
# leftmost. "*.leftmost.example.test" matches a single
# label; the wildcard in "www.*.example.test" must not be
+5
View File
@@ -38,6 +38,11 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=san-types" -addext "subjectAltName=DNS:a.zz,IP:42.46.122.122" -out cert_san_types.pem
# cert_dns_cn.pem: CN is the DNS name the tests connect to, with NO
# subjectAltName. A host is named by a SAN only (RFC 9525), so
# verifying it against this cert must fail.
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=localhost" -out cert_dns_cn.pem
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
# "*.leftmost.example.test" matches a single label; the
# wildcard in "www.*.example.test" must not be honoured.
+11
View File
@@ -146,6 +146,16 @@ cert_san_types_pem = custom_target(
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=san-types', '-addext', 'subjectAltName=DNS:a.zz,IP:42.46.122.122', '-out', '@OUTPUT@']
)
# cert_dns_cn.pem: CN is the DNS name the tests connect to, with NO
# subjectAltName. A host is named by a SAN only (RFC 9525), so verifying it
# against this cert must fail.
cert_dns_cn_pem = custom_target(
'cert_dns_cn_pem',
input: key_pem,
output: 'cert_dns_cn.pem',
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=localhost', '-out', '@OUTPUT@']
)
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
# "*.leftmost.example.test" matches a single label; the wildcard in
# "www.*.example.test" must not be honoured.
@@ -197,6 +207,7 @@ test(
client_encrypted_pbes1_key_pem,
client_encrypted_cert_pem,
cert_ip_cn_pem,
cert_dns_cn_pem,
cert_ipv6_pem,
cert_san_types_pem,
cert_wildcard_san_pem
+1027 -38
View File
File diff suppressed because it is too large. Load diff