mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-27 12:31:28 +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
|
```cpp
|
||||||
bool connected = sse.is_connected();
|
bool connected = sse.is_connected();
|
||||||
const std::string &id = sse.last_event_id();
|
std::string id = sse.last_event_id();
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## 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.
|
> 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]
|
> [!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++
|
```c++
|
||||||
// Use either OpenSSL, Mbed TLS, or wolfSSL
|
// 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) |
|
| 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` |
|
| 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:
|
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");
|
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
|
```cpp
|
||||||
// User defined file extension and MIME type mappings
|
// User defined file extension and MIME type mappings
|
||||||
|
|||||||
@@ -995,6 +995,30 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
|
|||||||
return true;
|
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
|
// 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
|
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
|
||||||
// references starting with '/' are returned unchanged.
|
// 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.
|
// Remove "." and ".." segments from the merged path.
|
||||||
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
|
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
|
||||||
std::string path;
|
return remove_dot_segments(merged.substr(0, path_end)) +
|
||||||
size_t i = 0;
|
merged.substr(path_end);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
@@ -3797,6 +3803,8 @@ template <typename T>
|
|||||||
bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer,
|
bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer,
|
||||||
Error &error);
|
Error &error);
|
||||||
|
|
||||||
|
bool write_data(Stream &strm, const char *d, size_t l);
|
||||||
|
|
||||||
std::string trim_copy(const std::string &s);
|
std::string trim_copy(const std::string &s);
|
||||||
|
|
||||||
void divide(
|
void divide(
|
||||||
@@ -4496,7 +4504,7 @@ public:
|
|||||||
|
|
||||||
// State accessors
|
// State accessors
|
||||||
bool is_connected() const;
|
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
|
// Blocking start - runs event loop with auto-reconnect
|
||||||
void start();
|
void start();
|
||||||
@@ -4534,7 +4542,9 @@ private:
|
|||||||
// State
|
// State
|
||||||
std::atomic<bool> running_{false};
|
std::atomic<bool> running_{false};
|
||||||
std::atomic<bool> connected_{false};
|
std::atomic<bool> connected_{false};
|
||||||
|
// Written by the event loop, which start_async() runs on its own thread
|
||||||
std::string last_event_id_;
|
std::string last_event_id_;
|
||||||
|
mutable std::mutex last_event_id_mutex_;
|
||||||
|
|
||||||
// Async support
|
// Async support
|
||||||
std::thread async_thread_;
|
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 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_;
|
return last_event_id_;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5207,7 +5218,10 @@ inline void SSEClient::run_event_loop() {
|
|||||||
if (event_complete) {
|
if (event_complete) {
|
||||||
// Update last_event_id for reconnection, even for an event that
|
// Update last_event_id for reconnection, even for an event that
|
||||||
// has no data. An empty id clears it.
|
// 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
|
// An event without a data field is not dispatched
|
||||||
if (has_data) { dispatch_event(current_msg); }
|
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().
|
// 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);
|
size_t get_peer_certs(const_session_t session, std::vector<cert_t> &certs);
|
||||||
void free_cert(cert_t cert);
|
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);
|
bool verify_hostname(cert_t cert, const char *hostname);
|
||||||
uint64_t hostname_mismatch_code();
|
uint64_t hostname_mismatch_code();
|
||||||
long get_verify_result(const_session_t session);
|
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) {
|
if (len < 126) {
|
||||||
header[1] = static_cast<uint8_t>(len);
|
header[1] = static_cast<uint8_t>(len);
|
||||||
if (mask) { header[1] |= 0x80; }
|
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) {
|
} else if (len <= 0xFFFF) {
|
||||||
header[1] = 126;
|
header[1] = 126;
|
||||||
if (mask) { header[1] |= 0x80; }
|
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];
|
uint8_t ext[2];
|
||||||
ext[0] = static_cast<uint8_t>((len >> 8) & 0xFF);
|
ext[0] = static_cast<uint8_t>((len >> 8) & 0xFF);
|
||||||
ext[1] = static_cast<uint8_t>(len & 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 {
|
} else {
|
||||||
header[1] = 127;
|
header[1] = 127;
|
||||||
if (mask) { header[1] |= 0x80; }
|
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];
|
uint8_t ext[8];
|
||||||
for (int i = 7; i >= 0; i--) {
|
for (int i = 7; i >= 0; i--) {
|
||||||
ext[7 - i] =
|
ext[7 - i] =
|
||||||
static_cast<uint8_t>((static_cast<uint64_t>(len) >> (i * 8)) & 0xFF);
|
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) {
|
if (mask) {
|
||||||
// Generate random mask key
|
// Generate random mask key
|
||||||
uint8_t mask_key[4];
|
uint8_t mask_key[4];
|
||||||
fill_random_bytes(mask_key, sizeof(mask_key));
|
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
|
// Write masked payload in chunks
|
||||||
const size_t chunk_size = 4096;
|
const size_t chunk_size = 4096;
|
||||||
@@ -5849,11 +5873,11 @@ inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
|
|||||||
buf[i] =
|
buf[i] =
|
||||||
data[offset + i] ^ static_cast<char>(mask_key[(offset + i) % 4]);
|
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 {
|
} else {
|
||||||
if (len > 0) {
|
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 {
|
namespace detail {
|
||||||
|
|
||||||
// A path a mount point serves: absolute, with no empty segment except a
|
// 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
|
// trailing '/', no segment ending in '.' or a space (which covers "." and
|
||||||
// ".." and names Windows trims into another file's name), so the file served
|
// ".." and names Windows trims into another file's name) and, on Windows, no
|
||||||
// is the one that req.path names to the handlers that ran before it.
|
// ':' (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) {
|
inline bool is_valid_path(const std::string &path) {
|
||||||
if (path.empty() || path[0] != '/') { return false; }
|
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++) {
|
for (auto i = beg; i <= path.size(); i++) {
|
||||||
if (i < path.size() && path[i] != '/') {
|
if (i < path.size() && path[i] != '/') {
|
||||||
if (path[i] == '\0' || path[i] == '\\') { return false; }
|
if (path[i] == '\0' || path[i] == '\\') { return false; }
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (path[i] == ':') { return false; }
|
||||||
|
#endif
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto len = i - beg;
|
auto len = i - beg;
|
||||||
@@ -15967,7 +15995,7 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!req.body.empty()) {
|
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.error = Error::Write;
|
||||||
handle.response.reset();
|
handle.response.reset();
|
||||||
return handle;
|
return handle;
|
||||||
@@ -16018,7 +16046,7 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
|
|||||||
|
|
||||||
auto content_encoding = detail::get_combined_header_value(
|
auto content_encoding = detail::get_combined_header_value(
|
||||||
handle.response->headers, "Content-Encoding");
|
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
|
// Same policy as prepare_content_receiver(): reject a coding we know about
|
||||||
// but were not built with, pass an unrecognized one through as-is.
|
// but were not built with, pass an unrecognized one through as-is.
|
||||||
handle.decompressor_ = detail::create_decompressor(content_encoding);
|
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);
|
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) {
|
inline bool is_redirect(const Response &res) {
|
||||||
return 300 < res.status && res.status < 400 &&
|
switch (res.status) {
|
||||||
!res.get_header_value("Location").empty();
|
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
|
} // 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 call it from a different thread since it's a thread-safety issue
|
||||||
// to do these things to the socket if another thread is using the socket.
|
// to do these things to the socket if another thread is using the socket.
|
||||||
std::lock_guard<std::mutex> guard(socket_mutex_);
|
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||||
// Also when the request asked for it (e.g. a body that was never sent),
|
// 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.
|
// or "Connection: close"), so that a redirect or an auth retry below does
|
||||||
if (socket_should_be_closed_when_request_is_done_ ||
|
// not reuse the socket.
|
||||||
|
if (socket_should_be_closed_when_request_is_done_ || close_connection ||
|
||||||
detail::has_header_token(res.headers, "Connection", "close") ||
|
detail::has_header_token(res.headers, "Connection", "close") ||
|
||||||
(res.version == "HTTP/1.0" && res.reason != "Connection established")) {
|
(res.version == "HTTP/1.0" && res.reason != "Connection established")) {
|
||||||
disconnect(/*gracefully=*/true);
|
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_scheme.empty()) { next_scheme = scheme; }
|
||||||
if (next_host.empty()) { next_host = host_; }
|
if (next_host.empty()) { next_host = host_; }
|
||||||
if (next_path.empty()) { next_path = "/"; }
|
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;
|
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) &&
|
auto redirect = follow_location_ && detail::is_redirect(res);
|
||||||
res.status != StatusCode::NotModified_304;
|
|
||||||
|
|
||||||
if (req.response_handler && !redirect) {
|
if (req.response_handler && !redirect) {
|
||||||
if (!req.response_handler(res)) {
|
if (!req.response_handler(res)) {
|
||||||
@@ -19582,15 +19622,75 @@ inline bool enumerate_windows_system_certs(Callback cb) {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
#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>
|
template <typename Callback>
|
||||||
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
inline bool enumerate_macos_keychain_certs(Callback cb) {
|
||||||
bool loaded = false;
|
|
||||||
const SecTrustSettingsDomain domains[] = {
|
const SecTrustSettingsDomain domains[] = {
|
||||||
kSecTrustSettingsDomainSystem,
|
|
||||||
kSecTrustSettingsDomainAdmin,
|
|
||||||
kSecTrustSettingsDomainUser,
|
kSecTrustSettingsDomainUser,
|
||||||
|
kSecTrustSettingsDomainAdmin,
|
||||||
|
kSecTrustSettingsDomainSystem,
|
||||||
};
|
};
|
||||||
|
std::set<std::string> decided;
|
||||||
|
std::vector<std::string> trusted;
|
||||||
for (auto domain : domains) {
|
for (auto domain : domains) {
|
||||||
CFArrayRef certs = nullptr;
|
CFArrayRef certs = nullptr;
|
||||||
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
|
OSStatus status = SecTrustSettingsCopyCertificates(domain, &certs);
|
||||||
@@ -19603,16 +19703,43 @@ inline bool enumerate_macos_keychain_certs(Callback cb) {
|
|||||||
SecCertificateRef cert =
|
SecCertificateRef cert =
|
||||||
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
|
(SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
|
||||||
CFDataRef data = SecCertificateCopyData(cert);
|
CFDataRef data = SecCertificateCopyData(cert);
|
||||||
if (data) {
|
if (!data) { continue; }
|
||||||
if (cb(CFDataGetBytePtr(data),
|
std::string der(reinterpret_cast<const char *>(CFDataGetBytePtr(data)),
|
||||||
static_cast<size_t>(CFDataGetLength(data)))) {
|
static_cast<size_t>(CFDataGetLength(data)));
|
||||||
loaded = true;
|
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);
|
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;
|
return loaded;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -20046,36 +20173,14 @@ inline bool load_system_certs(ctx_t ctx) {
|
|||||||
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
auto store = SSL_CTX_get_cert_store(ssl_ctx);
|
||||||
if (!store) return false;
|
if (!store) return false;
|
||||||
|
|
||||||
bool loaded_any = false;
|
auto loaded_any = impl::enumerate_macos_keychain_certs(
|
||||||
const SecTrustSettingsDomain domains[] = {
|
[&](const unsigned char *data, size_t len) {
|
||||||
kSecTrustSettingsDomainSystem,
|
auto x509 = d2i_X509(nullptr, &data, static_cast<long>(len));
|
||||||
kSecTrustSettingsDomainAdmin,
|
if (!x509) { return false; }
|
||||||
kSecTrustSettingsDomainUser,
|
auto added = X509_STORE_add_cert(store, x509) == 1;
|
||||||
};
|
X509_free(x509);
|
||||||
for (auto domain : domains) {
|
return added;
|
||||||
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);
|
|
||||||
}
|
|
||||||
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
return loaded_any || SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||||
#else
|
#else
|
||||||
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
return SSL_CTX_set_default_verify_paths(ssl_ctx) == 1;
|
||||||
@@ -20189,10 +20294,12 @@ inline bool set_sni(session_t session, const char *hostname,
|
|||||||
auto ssl = static_cast<SSL *>(session);
|
auto ssl = static_cast<SSL *>(session);
|
||||||
|
|
||||||
// The handshake's chain verification then checks the name as well
|
// The handshake's chain verification then checks the name as well
|
||||||
if (verify_hostname &&
|
if (verify_hostname) {
|
||||||
X509_VERIFY_PARAM_set1_host(SSL_get0_param(ssl), hostname,
|
auto param = SSL_get0_param(ssl);
|
||||||
strlen(hostname)) != 1) {
|
X509_VERIFY_PARAM_set_hostflags(param, X509_CHECK_FLAG_NEVER_CHECK_SUBJECT);
|
||||||
return false;
|
if (X509_VERIFY_PARAM_set1_host(param, hostname, strlen(hostname)) != 1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if defined(OPENSSL_IS_BORINGSSL)
|
#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)) {
|
if (detail::is_ip_address(hostname)) {
|
||||||
return X509_check_ip_asc(x509, hostname, 0) == 1;
|
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() {
|
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
|
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
|
||||||
// in buf.tag and the raw value in buf.p / buf.len.
|
// 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) {
|
for (auto san = &mcert->subject_alt_names; san; san = san->next) {
|
||||||
if (!san->buf.p || san->buf.len == 0) { continue; }
|
if (!san->buf.p || san->buf.len == 0) { continue; }
|
||||||
const unsigned char *p = san->buf.p;
|
const unsigned char *p = san->buf.p;
|
||||||
@@ -21842,24 +21949,11 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
|
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
|
||||||
has_dns_san = true;
|
|
||||||
std::string san_name(reinterpret_cast<const char *>(p), len);
|
std::string san_name(reinterpret_cast<const char *>(p), len);
|
||||||
if (detail::match_hostname(san_name, host_str)) { return true; }
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23007,7 +23101,6 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
|||||||
auto is_ip = ip_len > 0;
|
auto is_ip = ip_len > 0;
|
||||||
|
|
||||||
// Check Subject Alternative Names
|
// Check Subject Alternative Names
|
||||||
auto has_dns_san = false;
|
|
||||||
auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
|
auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
|
||||||
wolfSSL_X509_get_ext_d2i(x509, NID_subject_alt_name, nullptr, nullptr));
|
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) {
|
if (!is_ip && names->type == WOLFSSL_GEN_DNS) {
|
||||||
// DNS name
|
// DNS name
|
||||||
has_dns_san = true;
|
|
||||||
unsigned char *dns_name = nullptr;
|
unsigned char *dns_name = nullptr;
|
||||||
int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName);
|
int dns_len = wolfSSL_ASN1_STRING_to_UTF8(&dns_name, names->d.dNSName);
|
||||||
if (dns_name && dns_len > 0) {
|
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);
|
wolfSSL_sk_free(san_names);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: the Common Name (CN) in the subject, as X509_check_host does it:
|
return false;
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
inline uint64_t hostname_mismatch_code() {
|
inline uint64_t hostname_mismatch_code() {
|
||||||
|
|||||||
@@ -153,6 +153,14 @@ if(HTTPLIB_IS_USING_OPENSSL)
|
|||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
COMMAND_ERROR_IS_FATAL ANY
|
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
|
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not
|
||||||
# leftmost. "*.leftmost.example.test" matches a single
|
# leftmost. "*.leftmost.example.test" matches a single
|
||||||
# label; the wildcard in "www.*.example.test" must not be
|
# 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".
|
# 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
|
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.
|
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
|
||||||
# "*.leftmost.example.test" matches a single label; the
|
# "*.leftmost.example.test" matches a single label; the
|
||||||
# wildcard in "www.*.example.test" must not be honoured.
|
# 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@']
|
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.
|
# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
|
||||||
# "*.leftmost.example.test" matches a single label; the wildcard in
|
# "*.leftmost.example.test" matches a single label; the wildcard in
|
||||||
# "www.*.example.test" must not be honoured.
|
# "www.*.example.test" must not be honoured.
|
||||||
@@ -197,6 +207,7 @@ test(
|
|||||||
client_encrypted_pbes1_key_pem,
|
client_encrypted_pbes1_key_pem,
|
||||||
client_encrypted_cert_pem,
|
client_encrypted_cert_pem,
|
||||||
cert_ip_cn_pem,
|
cert_ip_cn_pem,
|
||||||
|
cert_dns_cn_pem,
|
||||||
cert_ipv6_pem,
|
cert_ipv6_pem,
|
||||||
cert_san_types_pem,
|
cert_san_types_pem,
|
||||||
cert_wildcard_san_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_CERT_FILE "./cert.pem"
|
||||||
#define SERVER_CERT2_FILE "./cert2.pem"
|
#define SERVER_CERT2_FILE "./cert2.pem"
|
||||||
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.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_IPV6_FILE "./cert_ipv6.pem"
|
||||||
#define SERVER_CERT_SAN_TYPES_FILE "./cert_san_types.pem"
|
#define SERVER_CERT_SAN_TYPES_FILE "./cert_san_types.pem"
|
||||||
#define SERVER_CERT_WILDCARD_SAN_FILE "./cert_wildcard_san.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);
|
ca_store_t create_ca_store(const char *pem, size_t len);
|
||||||
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
||||||
std::vector<std::string> get_ca_names(ctx_t ctx);
|
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 tls
|
||||||
} // namespace httplib
|
} // namespace httplib
|
||||||
#endif
|
#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) {
|
TEST(CanCompressContentTypeTest, ComparesWholeTypes) {
|
||||||
EXPECT_TRUE(detail::can_compress_content_type("application/json"));
|
EXPECT_TRUE(detail::can_compress_content_type("application/json"));
|
||||||
EXPECT_TRUE(detail::can_compress_content_type("text/plain; charset=utf-8"));
|
EXPECT_TRUE(detail::can_compress_content_type("text/plain; charset=utf-8"));
|
||||||
@@ -4255,6 +4274,48 @@ TEST(RedirectToDifferentPort, TrailingCharactersInPort) {
|
|||||||
EXPECT_FALSE(res);
|
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) {
|
TEST(RedirectFromPageWithContent, Redirect) {
|
||||||
Server svr;
|
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) {
|
TEST(RedirectFromPageWithContentIP6, Redirect) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
@@ -12642,7 +12772,8 @@ TEST(MountTest, AliasedPathDoesNotPassPreRoutingGuard) {
|
|||||||
for (auto path :
|
for (auto path :
|
||||||
{"/./dir/test.html", "/dir/./test.html", "//dir/test.html",
|
{"/./dir/test.html", "/dir/./test.html", "//dir/test.html",
|
||||||
"/dir//test.html", "/dir/../dir/test.html", "/%2e/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);
|
SCOPED_TRACE(path);
|
||||||
auto res = cli.Get(path);
|
auto res = cli.Get(path);
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
@@ -16578,8 +16709,7 @@ TEST(SSLClientServerTest, CustomizeServerSSLCtxGeneric) {
|
|||||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
ASSERT_EQ(StatusCode::OK_200, res->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test verify_hostname for both OpenSSL and MbedTLS backends
|
// Test verify_hostname on every backend: a host is named by a SAN only
|
||||||
// Verifies that wildcard matching and exact matching work consistently
|
|
||||||
TEST(SSLClientServerTest, TlsVerifyHostname) {
|
TEST(SSLClientServerTest, TlsVerifyHostname) {
|
||||||
using namespace httplib::tls;
|
using namespace httplib::tls;
|
||||||
|
|
||||||
@@ -16613,7 +16743,7 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
|
|||||||
|
|
||||||
if (!ctx.cert) return false;
|
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");
|
verify_result_cn = ctx.check_hostname("Common Name");
|
||||||
|
|
||||||
// Test 2: wrong hostname should not match
|
// Test 2: wrong hostname should not match
|
||||||
@@ -16629,27 +16759,50 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
|
|||||||
ASSERT_TRUE(verify_callback_called)
|
ASSERT_TRUE(verify_callback_called)
|
||||||
<< "Verify callback should have been 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)
|
EXPECT_FALSE(verify_result_cn)
|
||||||
<< "BoringSSL should reject CN-based hostname matching (SAN-only)";
|
<< "verify_hostname must not match the certificate CN (no SAN)";
|
||||||
#else
|
|
||||||
EXPECT_TRUE(verify_result_cn)
|
|
||||||
<< "verify_hostname should match 'Common Name' (certificate CN)";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Wrong hostname should not match
|
// Wrong hostname should not match
|
||||||
EXPECT_FALSE(verify_result_wrong)
|
EXPECT_FALSE(verify_result_wrong)
|
||||||
<< "verify_hostname should not match 'wronghost.example.com'";
|
<< "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
|
// 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
|
// the certificate's Common Name (RFC 9110). This mirrors the OpenSSL backend's
|
||||||
// X509_check_ip behavior and must hold for every backend.
|
// X509_check_ip behavior and must hold for every backend.
|
||||||
@@ -18169,6 +18322,98 @@ TEST(SSLClientTest, EnableSystemCaCustomCaVerifiesLocalServer) {
|
|||||||
ASSERT_EQ(StatusCode::OK_200, res->status);
|
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
|
// Regression test: for IP hosts the Mbed TLS and wolfSSL backends used to
|
||||||
// skip chain verification entirely (only the certificate identity was
|
// skip chain verification entirely (only the certificate identity was
|
||||||
// checked), so a server presenting an untrusted certificate with a matching
|
// 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) {
|
TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
@@ -23627,6 +23917,55 @@ TEST(OpenStreamMalformedContentLength, OutOfRange) {
|
|||||||
server_thread.join();
|
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.
|
// Serves `response` to the single request `fn` makes with a fresh client.
|
||||||
template <typename Fn>
|
template <typename Fn>
|
||||||
static void with_single_response(const std::string &response, Fn 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);
|
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) {
|
TEST(StreamBodyEndTest, BodyEndingAtConnectionClose) {
|
||||||
// Neither Content-Length nor chunked: the body ends at the close
|
// Neither Content-Length nor chunked: the body ends at the close
|
||||||
const char *close_delimited = "HTTP/1.1 200 OK\r\n"
|
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");
|
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: Manual stop
|
||||||
TEST_F(SSEIntegrationTest, ManualStop) {
|
TEST_F(SSEIntegrationTest, ManualStop) {
|
||||||
// Endpoint that sends one event and stays open briefly. The server outlives
|
// Endpoint that sends one event and stays open briefly. The server outlives
|
||||||
@@ -27932,6 +28343,64 @@ TEST_F(WebSocketIntegrationTest, ChronoTimeoutSetters) {
|
|||||||
EXPECT_LT(elapsed, 30000);
|
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) {
|
TEST(WebSocketPreRoutingTest, RejectWithoutAuth) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user