mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-26 12:24:46 +00:00
Compare commits
9
Commits
a210b43e11
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
252dbaa505 | ||
|
|
8710fbd25d | ||
|
|
2f8b9f6f57 | ||
|
|
391de32115 | ||
|
|
ba32b468a4 | ||
|
|
76f84a5bfd | ||
|
|
d23d1b6029 | ||
|
|
7d349cbeed | ||
|
|
5ceef83c54 |
No files matched your search
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -3797,6 +3803,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 +4504,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 +4542,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_;
|
||||
@@ -5032,7 +5042,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 +5218,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 +5358,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 +5829,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 +5873,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 +6013,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 +6024,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;
|
||||
@@ -15967,7 +15995,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 +16046,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 +16241,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 +16314,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 +16411,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;
|
||||
|
||||
@@ -17025,8 +17066,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)) {
|
||||
@@ -19582,15 +19622,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 +19703,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 +20173,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 +20294,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 +20548,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() {
|
||||
@@ -21827,7 +21935,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 +21949,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;
|
||||
}
|
||||
|
||||
@@ -23007,7 +23101,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 +23113,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 +23139,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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+487
-18
@@ -41,6 +41,7 @@ inline std::string u8_to_string(const char8_t *s) {
|
||||
#define SERVER_CERT_FILE "./cert.pem"
|
||||
#define SERVER_CERT2_FILE "./cert2.pem"
|
||||
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
|
||||
#define SERVER_CERT_DNS_CN_FILE "./cert_dns_cn.pem"
|
||||
#define SERVER_CERT_IPV6_FILE "./cert_ipv6.pem"
|
||||
#define SERVER_CERT_SAN_TYPES_FILE "./cert_san_types.pem"
|
||||
#define SERVER_CERT_WILDCARD_SAN_FILE "./cert_wildcard_san.pem"
|
||||
@@ -71,6 +72,12 @@ namespace tls {
|
||||
ca_store_t create_ca_store(const char *pem, size_t len);
|
||||
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
||||
std::vector<std::string> get_ca_names(ctx_t ctx);
|
||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
namespace impl {
|
||||
SecTrustSettingsResult
|
||||
macos_ssl_trust_settings_result(CFArrayRef trust_settings);
|
||||
} // namespace impl
|
||||
#endif
|
||||
} // namespace tls
|
||||
} // namespace httplib
|
||||
#endif
|
||||
@@ -700,6 +707,18 @@ TEST(IsValidPathTest, RejectsSegmentsEndingInDotOrSpace) {
|
||||
}
|
||||
}
|
||||
|
||||
// ':' names an NTFS stream on Windows and is an ordinary character in a file
|
||||
// name anywhere else.
|
||||
TEST(IsValidPathTest, ColonIsRejectedOnlyOnWindows) {
|
||||
for (auto path : {"/a.txt::$DATA", "/a.txt:Zone.Identifier", "/dir:x/a"}) {
|
||||
#ifdef _WIN32
|
||||
EXPECT_FALSE(detail::is_valid_path(path)) << path;
|
||||
#else
|
||||
EXPECT_TRUE(detail::is_valid_path(path)) << path;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CanCompressContentTypeTest, ComparesWholeTypes) {
|
||||
EXPECT_TRUE(detail::can_compress_content_type("application/json"));
|
||||
EXPECT_TRUE(detail::can_compress_content_type("text/plain; charset=utf-8"));
|
||||
@@ -4255,6 +4274,48 @@ TEST(RedirectToDifferentPort, TrailingCharactersInPort) {
|
||||
EXPECT_FALSE(res);
|
||||
}
|
||||
|
||||
TEST(RedirectTest, OtherStatusesWithLocationAreNotFollowed) {
|
||||
Server svr;
|
||||
std::atomic<bool> followed{false};
|
||||
|
||||
svr.Get("/304", [](const Request & /*req*/, Response &res) {
|
||||
res.status = StatusCode::NotModified_304;
|
||||
res.set_header("Location", "/target");
|
||||
});
|
||||
|
||||
svr.Get("/305", [](const Request & /*req*/, Response &res) {
|
||||
res.status = StatusCode::UseProxy_305;
|
||||
res.set_header("Location", "/target");
|
||||
});
|
||||
|
||||
svr.Get("/target", [&](const Request & /*req*/, Response &res) {
|
||||
followed = true;
|
||||
res.set_content("target", "text/plain");
|
||||
});
|
||||
|
||||
auto th = std::thread([&]() { svr.listen("localhost", PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
th.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", PORT);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/304");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::NotModified_304, res->status);
|
||||
|
||||
res = cli.Get("/305");
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::UseProxy_305, res->status);
|
||||
|
||||
EXPECT_FALSE(followed);
|
||||
}
|
||||
|
||||
TEST(RedirectFromPageWithContent, Redirect) {
|
||||
Server svr;
|
||||
|
||||
@@ -4306,6 +4367,75 @@ TEST(RedirectFromPageWithContent, Redirect) {
|
||||
}
|
||||
}
|
||||
|
||||
// A server can close the connection after a request with "Connection: close"
|
||||
// without repeating the option in its response (RFC 9112 Section 9.6), so the
|
||||
// redirected request must go out on a new connection.
|
||||
TEST(RedirectTest, NewConnectionAfterConnectionClose) {
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(static_cast<uint16_t>(PORT + 2));
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
ASSERT_EQ(0, ::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)));
|
||||
ASSERT_EQ(0, ::listen(srv, 2));
|
||||
|
||||
auto send_text = [](socket_t sock, const std::string &text) {
|
||||
::send(sock,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(text.c_str()),
|
||||
static_cast<int>(text.size()),
|
||||
#else
|
||||
text.c_str(), text.size(),
|
||||
#endif
|
||||
0);
|
||||
};
|
||||
|
||||
auto reused = false;
|
||||
auto server_thread = std::thread([&] {
|
||||
char buf[4096];
|
||||
auto first = ::accept(srv, nullptr, nullptr);
|
||||
if (first == INVALID_SOCKET) { return; }
|
||||
detail::set_socket_opt_time(first, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
||||
::recv(first, buf, sizeof(buf), 0);
|
||||
send_text(
|
||||
first,
|
||||
"HTTP/1.1 302 Found\r\nLocation: /2\r\nContent-Length: 0\r\n\r\n");
|
||||
|
||||
auto next = first;
|
||||
if (::recv(first, buf, sizeof(buf), 0) > 0) {
|
||||
reused = true;
|
||||
} else {
|
||||
detail::close_socket(first);
|
||||
next = ::accept(srv, nullptr, nullptr);
|
||||
if (next == INVALID_SOCKET) { return; }
|
||||
detail::set_socket_opt_time(next, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
||||
::recv(next, buf, sizeof(buf), 0);
|
||||
}
|
||||
send_text(next, "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n"
|
||||
"Connection: close\r\n\r\nok");
|
||||
detail::close_socket(next);
|
||||
});
|
||||
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
cli.set_follow_location(true);
|
||||
cli.set_read_timeout(5, 0);
|
||||
auto res = cli.Get("/1");
|
||||
|
||||
server_thread.join();
|
||||
detail::close_socket(srv);
|
||||
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
EXPECT_EQ("ok", res->body);
|
||||
EXPECT_FALSE(reused);
|
||||
}
|
||||
|
||||
TEST(RedirectFromPageWithContentIP6, Redirect) {
|
||||
Server svr;
|
||||
|
||||
@@ -12642,7 +12772,8 @@ TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) {
|
||||
for (auto path :
|
||||
{"/./dir/test.html", "/dir/./test.html", "//dir/test.html",
|
||||
"/dir//test.html", "/dir/../dir/test.html", "/%2e/dir/test.html",
|
||||
"/dir/test.html.", "/dir/test.html%20"}) {
|
||||
"/dir/test.html.", "/dir/test.html%20", "/dir/test.html::$DATA",
|
||||
"/dir/test.html%3A%3A$DATA"}) {
|
||||
SCOPED_TRACE(path);
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||
@@ -16578,8 +16709,7 @@ TEST(SSLClientServerTest, CustomizeServerSSLCtxGeneric) {
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
// Test verify_hostname for both OpenSSL and MbedTLS backends
|
||||
// Verifies that wildcard matching and exact matching work consistently
|
||||
// Test verify_hostname on every backend: a host is named by a SAN only
|
||||
TEST(SSLClientServerTest, TlsVerifyHostname) {
|
||||
using namespace httplib::tls;
|
||||
|
||||
@@ -16613,7 +16743,7 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
|
||||
|
||||
if (!ctx.cert) return false;
|
||||
|
||||
// Test 1: "Common Name" should match (our test server cert CN)
|
||||
// Test 1: "Common Name" is the CN of our test server cert, which has no SAN
|
||||
verify_result_cn = ctx.check_hostname("Common Name");
|
||||
|
||||
// Test 2: wrong hostname should not match
|
||||
@@ -16629,27 +16759,50 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
|
||||
ASSERT_TRUE(verify_callback_called)
|
||||
<< "Verify callback should have been called";
|
||||
|
||||
// CN="Common Name" should match our test certificate
|
||||
//
|
||||
// BoringSSL intentionally drops CN-based hostname matching per RFC 6125
|
||||
// §6.4.4 — only SubjectAltName is consulted. Other backends (OpenSSL,
|
||||
// MbedTLS, wolfSSL) still honor the CN fallback, so flip the expectation
|
||||
// for BoringSSL builds. OPENSSL_IS_BORINGSSL is defined by BoringSSL's
|
||||
// <openssl/base.h>, which is included transitively when
|
||||
// CPPHTTPLIB_OPENSSL_SUPPORT is set against a BoringSSL install.
|
||||
#if defined(OPENSSL_IS_BORINGSSL)
|
||||
EXPECT_FALSE(verify_result_cn)
|
||||
<< "BoringSSL should reject CN-based hostname matching (SAN-only)";
|
||||
#else
|
||||
EXPECT_TRUE(verify_result_cn)
|
||||
<< "verify_hostname should match 'Common Name' (certificate CN)";
|
||||
#endif
|
||||
<< "verify_hostname must not match the certificate CN (no SAN)";
|
||||
|
||||
// Wrong hostname should not match
|
||||
EXPECT_FALSE(verify_result_wrong)
|
||||
<< "verify_hostname should not match 'wronghost.example.com'";
|
||||
}
|
||||
|
||||
// A DNS name in the CN must not authenticate a host (RFC 9525), whichever
|
||||
// backend verifies it.
|
||||
TEST(SSLClientServerTest, TlsVerifyHostnameDnsNotMatchedByCommonName) {
|
||||
// Certificate CN is "localhost" and it carries no SAN.
|
||||
SSLServer svr(SERVER_CERT_DNS_CN_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.Get("/test", [](const Request &, Response &res) {
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port(HOST);
|
||||
thread t([&]() { svr.listen_after_bind(); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
SSLClient cli("localhost", port);
|
||||
cli.set_ca_cert_path(SERVER_CERT_DNS_CN_FILE);
|
||||
cli.enable_server_certificate_verification(true);
|
||||
cli.set_connection_timeout(5);
|
||||
|
||||
auto res = cli.Get("/test");
|
||||
ASSERT_FALSE(res);
|
||||
EXPECT_EQ(Error::SSLServerHostnameVerification, res.error());
|
||||
|
||||
// The same certificate is accepted once host name verification is off, so the
|
||||
// failure above is the host name and not the chain.
|
||||
cli.enable_server_hostname_verification(false);
|
||||
res = cli.Get("/test");
|
||||
ASSERT_TRUE(res) << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
// An IP-literal host must only be authenticated via an iPAddress SAN, never via
|
||||
// the certificate's Common Name (RFC 9110). This mirrors the OpenSSL backend's
|
||||
// X509_check_ip behavior and must hold for every backend.
|
||||
@@ -18169,6 +18322,98 @@ TEST(SSLClientTest, EnableSystemCaCustomCaVerifiesLocalServer) {
|
||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||
// A usage-constraints dictionary of a Keychain Trust Settings array, with
|
||||
// the keys SecTrustSettingsCopyTrustSettings() returns.
|
||||
static CFDictionaryRef
|
||||
make_usage_constraint(SecPolicyRef policy, const char *policy_string,
|
||||
const SecTrustSettingsResult *result) {
|
||||
auto constraint =
|
||||
CFDictionaryCreateMutable(nullptr, 0, &kCFTypeDictionaryKeyCallBacks,
|
||||
&kCFTypeDictionaryValueCallBacks);
|
||||
if (policy) {
|
||||
CFDictionarySetValue(constraint, kSecTrustSettingsPolicy, policy);
|
||||
}
|
||||
if (policy_string) {
|
||||
auto str = CFStringCreateWithCString(nullptr, policy_string,
|
||||
kCFStringEncodingUTF8);
|
||||
CFDictionarySetValue(constraint, kSecTrustSettingsPolicyString, str);
|
||||
CFRelease(str);
|
||||
}
|
||||
if (result) {
|
||||
auto number_value = static_cast<SInt32>(*result);
|
||||
auto number = CFNumberCreate(nullptr, kCFNumberSInt32Type, &number_value);
|
||||
CFDictionarySetValue(constraint, kSecTrustSettingsResult, number);
|
||||
CFRelease(number);
|
||||
}
|
||||
return constraint;
|
||||
}
|
||||
|
||||
static SecTrustSettingsResult
|
||||
ssl_trust_settings_result(std::vector<CFDictionaryRef> constraints) {
|
||||
auto trust_settings =
|
||||
CFArrayCreateMutable(nullptr, 0, &kCFTypeArrayCallBacks);
|
||||
for (auto constraint : constraints) {
|
||||
CFArrayAppendValue(trust_settings, constraint);
|
||||
CFRelease(constraint);
|
||||
}
|
||||
auto result = tls::impl::macos_ssl_trust_settings_result(trust_settings);
|
||||
CFRelease(trust_settings);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The system CA loader takes a Keychain certificate as a trusted root only
|
||||
// when its Trust Settings say so for SSL, not merely because it has some.
|
||||
TEST(SSLClientTest, MacOSKeychainTrustSettingsForSSL) {
|
||||
const auto trust_root = kSecTrustSettingsResultTrustRoot;
|
||||
const auto trust_as_root = kSecTrustSettingsResultTrustAsRoot;
|
||||
const auto deny = kSecTrustSettingsResultDeny;
|
||||
const auto unspecified = kSecTrustSettingsResultUnspecified;
|
||||
auto ssl_policy = SecPolicyCreateSSL(true, nullptr);
|
||||
auto x509_policy = SecPolicyCreateBasicX509();
|
||||
|
||||
// "Always Trust", which stores no constraint at all
|
||||
EXPECT_EQ(kSecTrustSettingsResultTrustRoot, ssl_trust_settings_result({}));
|
||||
EXPECT_EQ(kSecTrustSettingsResultTrustRoot,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(nullptr, nullptr, nullptr)}));
|
||||
EXPECT_EQ(kSecTrustSettingsResultTrustRoot,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(ssl_policy, nullptr, &trust_root)}));
|
||||
EXPECT_EQ(kSecTrustSettingsResultTrustAsRoot,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(ssl_policy, nullptr, &trust_as_root)}));
|
||||
|
||||
// "Never Trust", for everything or for SSL
|
||||
EXPECT_EQ(kSecTrustSettingsResultDeny,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(nullptr, nullptr, &deny)}));
|
||||
EXPECT_EQ(kSecTrustSettingsResultDeny,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(ssl_policy, nullptr, &deny)}));
|
||||
|
||||
// Trusted for another policy, for one host, or not at all
|
||||
EXPECT_EQ(kSecTrustSettingsResultUnspecified,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(x509_policy, nullptr, &trust_root)}));
|
||||
EXPECT_EQ(kSecTrustSettingsResultUnspecified,
|
||||
ssl_trust_settings_result({make_usage_constraint(
|
||||
ssl_policy, "www.example.com", &trust_root)}));
|
||||
EXPECT_EQ(kSecTrustSettingsResultUnspecified,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(nullptr, nullptr, &unspecified)}));
|
||||
|
||||
// The first constraint that applies to SSL decides
|
||||
EXPECT_EQ(kSecTrustSettingsResultDeny,
|
||||
ssl_trust_settings_result(
|
||||
{make_usage_constraint(x509_policy, nullptr, &trust_root),
|
||||
make_usage_constraint(nullptr, nullptr, &deny)}));
|
||||
|
||||
CFRelease(x509_policy);
|
||||
CFRelease(ssl_policy);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Regression test: for IP hosts the Mbed TLS and wolfSSL backends used to
|
||||
// skip chain verification entirely (only the certificate identity was
|
||||
// checked), so a server presenting an untrusted certificate with a matching
|
||||
@@ -20348,6 +20593,51 @@ TEST(RedirectTest, ResolveRelativeLocation) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RedirectTest, RemoveDotSegments) {
|
||||
// RFC 3986 section 5.2.4
|
||||
EXPECT_EQ("/a/g", detail::remove_dot_segments("/a/b/c/./../../g"));
|
||||
EXPECT_EQ("/mid/6", detail::remove_dot_segments("/mid/content=5/../6"));
|
||||
EXPECT_EQ("/g", detail::remove_dot_segments("/./g"));
|
||||
EXPECT_EQ("/g", detail::remove_dot_segments("/../g"));
|
||||
EXPECT_EQ("/", detail::remove_dot_segments("/.."));
|
||||
EXPECT_EQ("/a/", detail::remove_dot_segments("/a/b/.."));
|
||||
EXPECT_EQ("/a/b", detail::remove_dot_segments("/a/b"));
|
||||
}
|
||||
|
||||
TEST(RedirectTest, AbsoluteLocationIsNormalized) {
|
||||
Server svr;
|
||||
|
||||
svr.Get("/absolute-path", [](const Request & /*req*/, Response &res) {
|
||||
res.set_redirect("/dir/../target?x=1");
|
||||
});
|
||||
svr.Get("/absolute-uri", [](const Request & /*req*/, Response &res) {
|
||||
res.set_redirect("http://localhost:" + std::to_string(PORT) +
|
||||
"/dir/./../target");
|
||||
});
|
||||
svr.Get("/target", [](const Request &req, Response &res) {
|
||||
res.set_content(req.path, "text/plain");
|
||||
});
|
||||
|
||||
auto th = std::thread([&]() { svr.listen("localhost", PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
th.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", PORT);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
for (auto path : {"/absolute-path", "/absolute-uri"}) {
|
||||
auto res = cli.Get(path);
|
||||
ASSERT_TRUE(res) << path << ": " << to_string(res.error());
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status) << path;
|
||||
EXPECT_EQ("/target", res->body) << path;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
|
||||
Server svr;
|
||||
|
||||
@@ -23627,6 +23917,55 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
|
||||
server_thread.join();
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// As above for the body of a streamed request: a send() cut short by the write
|
||||
// timeout must not leave the rest of the body unsent.
|
||||
TEST(OpenStreamPartialWriteTest, BodyIsWrittenWhole) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
Server svr;
|
||||
svr.set_socket_options([](socket_t sock) {
|
||||
int size = 8192;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVBUF,
|
||||
reinterpret_cast<const char *>(&size), sizeof(size));
|
||||
});
|
||||
svr.Post("/size", [](const Request &, Response &res,
|
||||
const ContentReader &content_reader) {
|
||||
// Outlast the client's write timeout before reading the body.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
size_t received = 0;
|
||||
content_reader([&](const char *, size_t len) {
|
||||
received += len;
|
||||
return true;
|
||||
});
|
||||
res.set_content(std::to_string(received), "text/plain");
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port(HOST);
|
||||
std::thread t([&]() { svr.listen_after_bind(); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli(HOST, port);
|
||||
cli.set_write_timeout(0, 500000);
|
||||
cli.set_socket_options([](socket_t sock) {
|
||||
int size = 8192;
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDBUF,
|
||||
reinterpret_cast<const char *>(&size), sizeof(size));
|
||||
});
|
||||
|
||||
const std::string body(4 * 1024 * 1024, 'A');
|
||||
auto handle = cli.open_stream("POST", "/size", {}, {}, body,
|
||||
"application/octet-stream");
|
||||
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
|
||||
EXPECT_EQ(StatusCode::OK_200, handle.response->status);
|
||||
EXPECT_EQ(std::to_string(body.size()), read_all(handle));
|
||||
}
|
||||
#endif
|
||||
|
||||
// Serves `response` to the single request `fn` makes with a fresh client.
|
||||
template <typename Fn>
|
||||
static void with_single_response(const std::string &response, Fn fn) {
|
||||
@@ -23645,6 +23984,37 @@ static void with_single_response(const std::string &response, Fn fn) {
|
||||
fn(cli);
|
||||
}
|
||||
|
||||
// set_decompress(false) hands out the encoded bytes through the streaming APIs
|
||||
// too, also in a build without the decoder for the advertised coding.
|
||||
TEST(StreamDecompressionTest, DisabledPreservesEncodedBody) {
|
||||
const std::string gzipped(GZIPPED_HELLO_WORLD, sizeof(GZIPPED_HELLO_WORLD));
|
||||
const auto response = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Encoding: gzip\r\n"
|
||||
"Content-Length: " +
|
||||
std::to_string(gzipped.size()) +
|
||||
"\r\n"
|
||||
"Connection: close\r\n\r\n" +
|
||||
gzipped;
|
||||
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
cli.set_decompress(false);
|
||||
auto handle = cli.open_stream("GET", "/");
|
||||
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
|
||||
EXPECT_EQ("gzip", handle.response->get_header_value("Content-Encoding"));
|
||||
EXPECT_EQ(gzipped, read_all(handle));
|
||||
EXPECT_FALSE(handle.has_read_error());
|
||||
});
|
||||
|
||||
with_single_response(response, [&](Client &cli) {
|
||||
cli.set_decompress(false);
|
||||
auto res = stream::Get(cli, "/", 7);
|
||||
ASSERT_TRUE(res) << to_string(res.error());
|
||||
EXPECT_EQ("gzip", res.get_header_value("Content-Encoding"));
|
||||
EXPECT_EQ(gzipped, res.read_all());
|
||||
EXPECT_FALSE(res.has_read_error());
|
||||
});
|
||||
}
|
||||
|
||||
TEST(StreamBodyEndTest, BodyEndingAtConnectionClose) {
|
||||
// Neither Content-Length nor chunked: the body ends at the close
|
||||
const char *close_delimited = "HTTP/1.1 200 OK\r\n"
|
||||
@@ -26147,6 +26517,47 @@ TEST_F(SSEIntegrationTest, LastEventIdHeader) {
|
||||
EXPECT_EQ(sse.last_event_id(), "evt-123");
|
||||
}
|
||||
|
||||
// Test: last_event_id() read from another thread while start_async() runs
|
||||
TEST_F(SSEIntegrationTest, LastEventIdWhileRunning) {
|
||||
// Ids of every length up to 200, so the stored id keeps changing size
|
||||
server_->Get("/many-ids", [](const Request &, Response &res) {
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||
if (offset == 0) {
|
||||
std::string events;
|
||||
for (size_t i = 0; i < 2000; i++) {
|
||||
events += "id: " + std::string(1 + i % 200, 'a') + "\n";
|
||||
events += "data: x\n\n";
|
||||
}
|
||||
events += "id: end\ndata: x\n\n";
|
||||
sink.write(events.data(), events.size());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
Client client("localhost", get_port());
|
||||
sse::SSEClient sse(client, "/many-ids");
|
||||
// Wait out the rest of the test rather than replay the stream
|
||||
sse.set_reconnect_interval(10000);
|
||||
sse.set_max_reconnect_attempts(1);
|
||||
sse.start_async();
|
||||
|
||||
auto well_formed = true;
|
||||
std::string id;
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (id != "end" && std::chrono::steady_clock::now() < deadline) {
|
||||
id = sse.last_event_id();
|
||||
if (id != "end" && id.find_first_not_of('a') != std::string::npos) {
|
||||
well_formed = false;
|
||||
}
|
||||
}
|
||||
sse.stop();
|
||||
|
||||
EXPECT_TRUE(well_formed);
|
||||
EXPECT_EQ(sse.last_event_id(), "end");
|
||||
}
|
||||
|
||||
// Test: Manual stop
|
||||
TEST_F(SSEIntegrationTest, ManualStop) {
|
||||
// Endpoint that sends one event and stays open briefly. The server outlives
|
||||
@@ -27932,6 +28343,64 @@ TEST_F(WebSocketIntegrationTest, ChronoTimeoutSetters) {
|
||||
EXPECT_LT(elapsed, 30000);
|
||||
}
|
||||
|
||||
#ifndef _WIN32
|
||||
// A send() that times out after part of the data went out returns the count
|
||||
// written so far. Every byte of the frame must still be sent, or the peer reads
|
||||
// the next frame's header and payload as the rest of this one.
|
||||
TEST(WebSocketPartialWriteTest, FrameIsWrittenWhole) {
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
|
||||
const std::string large(4 * 1024 * 1024, 'A');
|
||||
std::atomic<bool> large_sent{false};
|
||||
std::atomic<bool> small_sent{false};
|
||||
|
||||
Server svr;
|
||||
svr.set_write_timeout(0, 500000);
|
||||
svr.set_socket_options([](socket_t sock) {
|
||||
int size = 8192;
|
||||
setsockopt(sock, SOL_SOCKET, SO_SNDBUF,
|
||||
reinterpret_cast<const char *>(&size), sizeof(size));
|
||||
});
|
||||
svr.WebSocket("/ws", [&](const Request &, ws::WebSocket &ws) {
|
||||
large_sent = ws.send(large.data(), large.size());
|
||||
small_sent = ws.send("after");
|
||||
std::string msg;
|
||||
while (ws.read(msg)) {}
|
||||
});
|
||||
|
||||
auto port = svr.bind_to_any_port(HOST);
|
||||
std::thread t([&]() { svr.listen_after_bind(); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
ws::WebSocketClient client("ws://" + std::string(HOST) + ":" +
|
||||
std::to_string(port) + "/ws");
|
||||
client.set_read_timeout(5);
|
||||
client.set_socket_options([](socket_t sock) {
|
||||
int size = 8192;
|
||||
setsockopt(sock, SOL_SOCKET, SO_RCVBUF,
|
||||
reinterpret_cast<const char *>(&size), sizeof(size));
|
||||
});
|
||||
ASSERT_TRUE(client.connect());
|
||||
|
||||
// Outlast the server's write timeout, then drain.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(750));
|
||||
|
||||
std::string msg;
|
||||
EXPECT_EQ(ws::ReadResult::Binary, client.read(msg));
|
||||
EXPECT_TRUE(msg == large);
|
||||
EXPECT_EQ(ws::ReadResult::Text, client.read(msg));
|
||||
EXPECT_EQ("after", msg);
|
||||
EXPECT_TRUE(large_sent);
|
||||
EXPECT_TRUE(small_sent);
|
||||
|
||||
client.close();
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST(WebSocketPreRoutingTest, RejectWithoutAuth) {
|
||||
Server svr;
|
||||
|
||||
|
||||
Reference in new issue
Block a user