mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-03 13:38:36 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39489ee376 | ||
|
|
75cc18b966 |
No files matched your search
+1
-1
@@ -119,7 +119,7 @@ using SubProtocolSelector =
|
||||
std::function<std::string(const std::vector<std::string> &protocols)>;
|
||||
```
|
||||
|
||||
The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols. A returned value that the client did not propose is ignored.
|
||||
The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols.
|
||||
|
||||
### WebSocket (Server-side)
|
||||
|
||||
|
||||
@@ -230,7 +230,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` |
|
||||
| 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 certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||
|
||||
On Windows, verification can also be disabled at runtime:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.60.1"
|
||||
version = "0.58.0"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.60.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003c01"
|
||||
#define CPPHTTPLIB_VERSION "0.58.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003a00"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -831,21 +831,14 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
|
||||
return {p, std::errc{}};
|
||||
}
|
||||
|
||||
inline bool parse_int_in_range(const char *s, size_t len, int lo, int hi,
|
||||
int &out) {
|
||||
inline bool parse_port(const char *s, size_t len, int &port) {
|
||||
int val = 0;
|
||||
auto r = from_chars(s, s + len, val);
|
||||
if (r.ec != std::errc{} || r.ptr != s + len || val < lo || val > hi) {
|
||||
return false;
|
||||
}
|
||||
out = val;
|
||||
if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
|
||||
port = val;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool parse_port(const char *s, size_t len, int &port) {
|
||||
return parse_int_in_range(s, len, 1, 65535, port);
|
||||
}
|
||||
|
||||
inline bool parse_port(const std::string &s, int &port) {
|
||||
return parse_port(s.data(), s.size(), port);
|
||||
}
|
||||
@@ -3449,9 +3442,6 @@ private:
|
||||
|
||||
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
bool enable_windows_cert_verification_ = true;
|
||||
// Like ca_cert_store_set_, tracks what ctx_ cannot report back: whether
|
||||
// set_server_certificate_verifier() installed a verifier.
|
||||
bool server_certificate_verifier_set_ = false;
|
||||
#endif
|
||||
|
||||
friend class ClientImpl;
|
||||
@@ -4376,8 +4366,7 @@ public:
|
||||
void stop();
|
||||
|
||||
private:
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms,
|
||||
bool &has_data, bool &has_id);
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);
|
||||
void run_event_loop();
|
||||
void dispatch_event(const SSEMessage &msg);
|
||||
bool should_reconnect(int count) const;
|
||||
@@ -4412,8 +4401,6 @@ private:
|
||||
|
||||
namespace ws {
|
||||
|
||||
class WebSocketClient;
|
||||
|
||||
enum class Opcode : uint8_t {
|
||||
Continuation = 0x0,
|
||||
Text = 0x1,
|
||||
@@ -4515,7 +4502,7 @@ public:
|
||||
|
||||
private:
|
||||
friend class httplib::Server;
|
||||
friend class httplib::ws::WebSocketClient;
|
||||
friend class WebSocketClient;
|
||||
|
||||
WebSocket(
|
||||
Stream &strm, const Request &req, bool is_server,
|
||||
@@ -4889,24 +4876,30 @@ inline void SSEClient::stop() {
|
||||
}
|
||||
|
||||
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
int &retry_ms, bool &has_data,
|
||||
bool &has_id) {
|
||||
int &retry_ms) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty()) { return true; }
|
||||
if (line.empty() || line == "\r") { return true; }
|
||||
|
||||
// Lines starting with ':' are comments (ignored)
|
||||
if (line[0] == ':') { return false; }
|
||||
if (!line.empty() && line[0] == ':') { return false; }
|
||||
|
||||
// Find the colon separator
|
||||
auto colon_pos = line.find(':');
|
||||
if (colon_pos == std::string::npos) {
|
||||
// Line with no colon is treated as field name with empty value
|
||||
return false;
|
||||
}
|
||||
|
||||
auto field = line.substr(0, colon_pos);
|
||||
std::string value;
|
||||
|
||||
// Value starts after colon, skip optional single space
|
||||
if (colon_pos != std::string::npos && colon_pos + 1 < line.size()) {
|
||||
if (colon_pos + 1 < line.size()) {
|
||||
auto value_start = colon_pos + 1;
|
||||
if (line[value_start] == ' ') { value_start++; }
|
||||
value = line.substr(value_start);
|
||||
// Remove trailing \r if present
|
||||
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
|
||||
}
|
||||
|
||||
// Handle known fields
|
||||
@@ -4914,13 +4907,11 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
msg.event = value;
|
||||
} else if (field == "data") {
|
||||
// Multiple data lines are concatenated with newlines
|
||||
if (has_data) { msg.data += "\n"; }
|
||||
if (!msg.data.empty()) { msg.data += "\n"; }
|
||||
msg.data += value;
|
||||
has_data = true;
|
||||
} else if (field == "id") {
|
||||
// Empty id is valid (clears the last event ID)
|
||||
msg.id = value;
|
||||
has_id = true;
|
||||
} else if (field == "retry") {
|
||||
// Parse retry interval in milliseconds
|
||||
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||
@@ -4991,8 +4982,6 @@ inline void SSEClient::run_event_loop() {
|
||||
// Event receiving loop
|
||||
std::string buffer;
|
||||
SSEMessage current_msg;
|
||||
auto has_data = false;
|
||||
auto has_id = false;
|
||||
|
||||
while (running_.load() && result.next()) {
|
||||
buffer.append(result.data(), result.size());
|
||||
@@ -5006,26 +4995,18 @@ inline void SSEClient::run_event_loop() {
|
||||
auto line = buffer.substr(line_start, newline_pos - line_start);
|
||||
line_start = newline_pos + 1;
|
||||
|
||||
// Strip the \r of a CRLF line ending so that every field, including
|
||||
// one without a colon, sees the same line
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
|
||||
// Parse the line and check if event is complete
|
||||
auto event_complete = parse_sse_line(
|
||||
line, current_msg, reconnect_interval_ms_, has_data, has_id);
|
||||
auto event_complete =
|
||||
parse_sse_line(line, current_msg, reconnect_interval_ms_);
|
||||
|
||||
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 (event_complete && !current_msg.data.empty()) {
|
||||
// Update last_event_id for reconnection
|
||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
||||
|
||||
// An event without a data field is not dispatched
|
||||
if (has_data) { dispatch_event(current_msg); }
|
||||
// Dispatch event to appropriate handler
|
||||
dispatch_event(current_msg);
|
||||
|
||||
// Reset the message for the next event either way
|
||||
current_msg.clear();
|
||||
has_data = false;
|
||||
has_id = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6630,30 +6611,18 @@ inline bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
return false;
|
||||
}
|
||||
|
||||
// `has_buffered_request` reports whether the connection's stream already holds
|
||||
// bytes of the next request. A client may pipeline its requests (RFC 9112
|
||||
// 9.3.2), so reading one request can pull the start of the next one into the
|
||||
// stream's buffer; that request must be served without waiting for the socket
|
||||
// to become readable again, since its bytes are no longer on the socket.
|
||||
// `callback` is told whether keep_alive() has just seen the socket go readable.
|
||||
template <typename P, typename T>
|
||||
inline bool process_server_socket_core(const std::atomic<socket_t> &svr_sock,
|
||||
socket_t sock,
|
||||
size_t keep_alive_max_count,
|
||||
time_t keep_alive_timeout_sec,
|
||||
P has_buffered_request, T callback) {
|
||||
template <typename T>
|
||||
inline bool
|
||||
process_server_socket_core(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
size_t keep_alive_max_count,
|
||||
time_t keep_alive_timeout_sec, T callback) {
|
||||
assert(keep_alive_max_count > 0);
|
||||
auto ret = false;
|
||||
auto count = keep_alive_max_count;
|
||||
while (count > 0) {
|
||||
auto socket_readable = false;
|
||||
if (!has_buffered_request()) {
|
||||
if (!keep_alive(svr_sock, sock, keep_alive_timeout_sec)) { break; }
|
||||
socket_readable = true;
|
||||
}
|
||||
while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) {
|
||||
auto close_connection = count == 1;
|
||||
auto connection_closed = false;
|
||||
ret = callback(socket_readable, close_connection, connection_closed);
|
||||
ret = callback(close_connection, connection_closed);
|
||||
if (!ret || connection_closed) { break; }
|
||||
count--;
|
||||
}
|
||||
@@ -6667,16 +6636,14 @@ process_server_socket(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
time_t keep_alive_timeout_sec, time_t read_timeout_sec,
|
||||
time_t read_timeout_usec, time_t write_timeout_sec,
|
||||
time_t write_timeout_usec, T callback) {
|
||||
// One stream per connection: its read buffer can already hold the start of
|
||||
// the next, pipelined request.
|
||||
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
|
||||
write_timeout_sec, write_timeout_usec);
|
||||
return process_server_socket_core(
|
||||
svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
|
||||
[&]() { return strm.is_readable(); },
|
||||
[&](bool socket_readable, bool close_connection,
|
||||
bool &connection_closed) {
|
||||
if (socket_readable) { strm.set_readable_hint(); }
|
||||
[&](bool close_connection, bool &connection_closed) {
|
||||
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
|
||||
write_timeout_sec, write_timeout_usec);
|
||||
// process_server_socket_core() only gets here once keep_alive() has
|
||||
// seen the socket go readable.
|
||||
strm.set_readable_hint();
|
||||
return callback(strm, close_connection, connection_closed);
|
||||
});
|
||||
}
|
||||
@@ -8274,11 +8241,9 @@ struct WebSocketUpgradeResponse {
|
||||
std::string selected_subprotocol;
|
||||
};
|
||||
|
||||
inline bool
|
||||
read_websocket_upgrade_response(Stream &strm,
|
||||
const std::string &expected_accept,
|
||||
const std::string &offered_subprotocols,
|
||||
WebSocketUpgradeResponse &upgrade) {
|
||||
inline bool read_websocket_upgrade_response(Stream &strm,
|
||||
const std::string &expected_accept,
|
||||
WebSocketUpgradeResponse &upgrade) {
|
||||
// Read status line
|
||||
const auto bufsiz = 2048;
|
||||
char buf[bufsiz];
|
||||
@@ -8335,22 +8300,6 @@ read_websocket_upgrade_response(Stream &strm,
|
||||
upgrade.selected_subprotocol = proto_it->second;
|
||||
}
|
||||
|
||||
// Verify the subprotocol is one the client offered (RFC 6455 4.1)
|
||||
if (!upgrade.selected_subprotocol.empty()) {
|
||||
auto was_offered = false;
|
||||
split(offered_subprotocols.data(),
|
||||
offered_subprotocols.data() + offered_subprotocols.size(), ',',
|
||||
[&](const char *b, const char *e) {
|
||||
if (std::string(b, e) == upgrade.selected_subprotocol) {
|
||||
was_offered = true;
|
||||
}
|
||||
});
|
||||
if (!was_offered) {
|
||||
upgrade.error = Error::WebSocketHandshake;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -8996,9 +8945,6 @@ inline bool redirect(T &cli, Request &req, Response &res,
|
||||
new_req.method = "GET";
|
||||
new_req.body.clear();
|
||||
new_req.headers.clear();
|
||||
new_req.content_length_ = 0;
|
||||
new_req.content_provider_ = nullptr;
|
||||
new_req.is_chunked_content_provider_ = false;
|
||||
}
|
||||
|
||||
Response new_res;
|
||||
@@ -9912,10 +9858,8 @@ inline bool range_error(Request &req, Response &res) {
|
||||
last_pos = content_len;
|
||||
}
|
||||
|
||||
// RFC 9110 14.1.2: a suffix-length longer than the representation
|
||||
// selects the entire representation.
|
||||
if (first_pos == -1) {
|
||||
first_pos = (std::max)(static_cast<ssize_t>(0), content_len - last_pos);
|
||||
first_pos = content_len - last_pos;
|
||||
last_pos = content_len - 1;
|
||||
}
|
||||
|
||||
@@ -10158,19 +10102,6 @@ inline std::string unescape_quoted_pairs(const std::string &s) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// Inverse of unescape_quoted_pairs: prepares a value to sit inside a
|
||||
// quoted-string. RFC 9110 §5.6.4 requires a literal '\' or '"' to be sent as a
|
||||
// quoted-pair, so the recipient recovers the original value.
|
||||
inline std::string escape_quoted_pairs(const std::string &s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (auto c : s) {
|
||||
if (c == '\\' || c == '"') { out += '\\'; }
|
||||
out += c;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline bool parse_www_authenticate(const Response &res,
|
||||
std::map<std::string, std::string> &auth,
|
||||
bool is_proxy) {
|
||||
@@ -10350,10 +10281,7 @@ inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||
|
||||
// Verify 101 response and Sec-WebSocket-Accept header
|
||||
auto expected_accept = websocket_accept_key(client_key);
|
||||
auto offered_subprotocols =
|
||||
get_combined_header_value(req.headers, "Sec-WebSocket-Protocol");
|
||||
return read_websocket_upgrade_response(strm, expected_accept,
|
||||
offered_subprotocols, upgrade);
|
||||
return read_websocket_upgrade_response(strm, expected_accept, upgrade);
|
||||
}
|
||||
|
||||
inline bool is_ip_address(const std::string &host) {
|
||||
@@ -10630,16 +10558,13 @@ inline bool process_server_socket_ssl(
|
||||
socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec,
|
||||
time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec,
|
||||
time_t write_timeout_usec, T callback) {
|
||||
// See process_server_socket(). The TLS library keeps already decrypted bytes
|
||||
// of a pipelined request, which keep_alive() cannot see on the socket.
|
||||
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
|
||||
write_timeout_sec, write_timeout_usec);
|
||||
return process_server_socket_core(
|
||||
svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec,
|
||||
[&]() { return strm.is_readable(); },
|
||||
[&](bool socket_readable, bool close_connection,
|
||||
bool &connection_closed) {
|
||||
if (socket_readable) { strm.set_readable_hint(); }
|
||||
[&](bool close_connection, bool &connection_closed) {
|
||||
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
|
||||
write_timeout_sec, write_timeout_usec);
|
||||
// See the non-TLS path in process_server_socket().
|
||||
strm.set_readable_hint();
|
||||
return callback(strm, close_connection, connection_closed);
|
||||
});
|
||||
}
|
||||
@@ -10680,13 +10605,7 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
|
||||
}
|
||||
|
||||
std::string algo = "MD5";
|
||||
if (auth.find("algorithm") != auth.end()) {
|
||||
// algorithm is an unquoted token (RFC 7616 §3.4). A server value that is
|
||||
// not a token would otherwise be emitted verbatim and could carry commas
|
||||
// or quotes that inject further auth-params into the header below.
|
||||
const auto &a = auth.at("algorithm");
|
||||
if (fields::is_token(a)) { algo = a; }
|
||||
}
|
||||
if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); }
|
||||
|
||||
std::string response;
|
||||
{
|
||||
@@ -10709,23 +10628,14 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
|
||||
|
||||
auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : "";
|
||||
|
||||
// Every value placed inside a quoted-string is escaped so a '"' in it cannot
|
||||
// close the string early. realm, nonce and opaque come straight from the
|
||||
// server's challenge (parse_www_authenticate() already de-escaped them), so
|
||||
// without this a crafted challenge injects extra auth-params into the header.
|
||||
auto field =
|
||||
"Digest username=\"" + detail::escape_quoted_pairs(username) +
|
||||
"\", realm=\"" + detail::escape_quoted_pairs(auth.at("realm")) +
|
||||
"\", nonce=\"" + detail::escape_quoted_pairs(auth.at("nonce")) +
|
||||
"\", uri=\"" + detail::escape_quoted_pairs(req.path) +
|
||||
"\", algorithm=" + algo +
|
||||
(qop.empty() ? ", response=\""
|
||||
: ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce +
|
||||
"\", response=\"") +
|
||||
response + "\"" +
|
||||
(opaque.empty()
|
||||
? ""
|
||||
: ", opaque=\"" + detail::escape_quoted_pairs(opaque) + "\"");
|
||||
auto field = "Digest username=\"" + username + "\", realm=\"" +
|
||||
auth.at("realm") + "\", nonce=\"" + auth.at("nonce") +
|
||||
"\", uri=\"" + req.path + "\", algorithm=" + algo +
|
||||
(qop.empty() ? ", response=\""
|
||||
: ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" +
|
||||
cnonce + "\", response=\"") +
|
||||
response + "\"" +
|
||||
(opaque.empty() ? "" : ", opaque=\"" + opaque + "\"");
|
||||
|
||||
auto key = is_proxy ? "Proxy-Authorization" : "Authorization";
|
||||
return std::make_pair(key, field);
|
||||
@@ -10790,7 +10700,7 @@ inline bool match_hostname(const std::string &pattern,
|
||||
#ifdef _WIN32
|
||||
// Verify certificate using Windows CertGetCertificateChain API.
|
||||
// This provides real-time certificate validation with Windows Update
|
||||
// integration, independent of the TLS backend.
|
||||
// integration, independent of the TLS backend (OpenSSL or MbedTLS).
|
||||
inline bool verify_cert_with_windows_schannel(
|
||||
const std::vector<unsigned char> &der_cert, const std::string &hostname,
|
||||
bool verify_hostname, uint64_t &out_error, tls::const_session_t session) {
|
||||
@@ -10835,13 +10745,6 @@ inline bool verify_cert_with_windows_schannel(
|
||||
CERT_CHAIN_PARA chain_para = {};
|
||||
chain_para.cbSize = sizeof(chain_para);
|
||||
|
||||
// Require the server authentication usage along the chain, which also
|
||||
// rejects roots that Windows trusts only for other purposes.
|
||||
LPSTR server_auth = const_cast<LPSTR>(szOID_PKIX_KP_SERVER_AUTH);
|
||||
chain_para.RequestedUsage.dwType = USAGE_MATCH_TYPE_AND;
|
||||
chain_para.RequestedUsage.Usage.cUsageIdentifier = 1;
|
||||
chain_para.RequestedUsage.Usage.rgpszUsageIdentifier = &server_auth;
|
||||
|
||||
// Build certificate chain with revocation checking
|
||||
PCCERT_CHAIN_CONTEXT chain_context = nullptr;
|
||||
auto chain_result = CertGetCertificateChain(
|
||||
@@ -10858,6 +10761,12 @@ inline bool verify_cert_with_windows_schannel(
|
||||
auto chain_guard =
|
||||
scope_exit([&] { CertFreeCertificateChain(chain_context); });
|
||||
|
||||
// Check if chain has errors
|
||||
if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {
|
||||
out_error = chain_context->TrustStatus.dwErrorStatus;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify SSL policy
|
||||
SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};
|
||||
extra_policy_para.cbSize = sizeof(extra_policy_para);
|
||||
@@ -10947,9 +10856,6 @@ struct ClientTlsSessionOptions {
|
||||
// The caller decides whether Schannel has anything to say about this
|
||||
// connection; see SSLClient::initialize_ssl().
|
||||
bool windows_cert_verification = false;
|
||||
// A server certificate verifier works on the backend's chain verification,
|
||||
// so the backend keeps deciding and Schannel only adds its own check.
|
||||
bool server_certificate_verifier_set = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -10984,24 +10890,12 @@ inline bool setup_client_tls_session(
|
||||
return fail(Error::SSLConnection, 0, 0);
|
||||
}
|
||||
|
||||
// With Windows verification on and no server certificate verifier set,
|
||||
// Schannel is the only chain verifier. The backend's trust store is a
|
||||
// snapshot of the Windows stores that lacks the roots Windows fetches on
|
||||
// demand, so the backend's verdict is not used.
|
||||
auto windows_verifies_chain = false;
|
||||
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
windows_verifies_chain = options.windows_cert_verification &&
|
||||
!options.server_certificate_verifier_set;
|
||||
#endif
|
||||
|
||||
#if defined(CPPHTTPLIB_MBEDTLS_SUPPORT) || defined(CPPHTTPLIB_WOLFSSL_SUPPORT)
|
||||
// Mbed TLS and wolfSSL need the verification mode set explicitly; OpenSSL
|
||||
// uses SSL_VERIFY_NONE and does all verification post-handshake. Unless
|
||||
// Schannel verifies the chain instead, chain verification happens during
|
||||
// the handshake even for IP hosts; the certificate identity is verified
|
||||
// post-handshake via verify_hostname().
|
||||
set_verify_client(ctx,
|
||||
server_certificate_verification && !windows_verifies_chain);
|
||||
// uses SSL_VERIFY_NONE and does all verification post-handshake. Chain
|
||||
// verification happens during the handshake even for IP hosts; the
|
||||
// certificate identity is verified post-handshake via verify_hostname().
|
||||
set_verify_client(ctx, server_certificate_verification);
|
||||
#endif
|
||||
|
||||
{
|
||||
@@ -11046,12 +10940,10 @@ inline bool setup_client_tls_session(
|
||||
|
||||
if (verification_status == SSLVerifierResponse::NoDecisionMade &&
|
||||
server_certificate_verification) {
|
||||
if (!windows_verifies_chain) {
|
||||
auto verify_result = get_verify_result(session);
|
||||
if (verify_result != 0) {
|
||||
return fail(Error::SSLServerVerification, 0,
|
||||
static_cast<uint64_t>(verify_result));
|
||||
}
|
||||
auto verify_result = get_verify_result(session);
|
||||
if (verify_result != 0) {
|
||||
return fail(Error::SSLServerVerification, 0,
|
||||
static_cast<uint64_t>(verify_result));
|
||||
}
|
||||
|
||||
auto server_cert = get_peer_cert(session);
|
||||
@@ -11071,17 +10963,18 @@ inline bool setup_client_tls_session(
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
// Windows Schannel verification, which lets Windows fetch missing roots
|
||||
// and intermediates on demand. It must not be skipped: unless a server
|
||||
// certificate verifier is set, it is the only chain check.
|
||||
// Additional Windows Schannel verification.
|
||||
// This provides real-time certificate validation with Windows Update
|
||||
// integration, working with both OpenSSL and MbedTLS backends.
|
||||
if (options.windows_cert_verification) {
|
||||
std::vector<unsigned char> der;
|
||||
uint64_t wincrypt_error = 0;
|
||||
if (!get_cert_der(server_cert, der) ||
|
||||
!verify_cert_with_windows_schannel(
|
||||
der, host, options.server_hostname_verification, wincrypt_error,
|
||||
session)) {
|
||||
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
||||
if (get_cert_der(server_cert, der)) {
|
||||
uint64_t wincrypt_error = 0;
|
||||
if (!verify_cert_with_windows_schannel(
|
||||
der, host, options.server_hostname_verification, wincrypt_error,
|
||||
session)) {
|
||||
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -12549,10 +12442,14 @@ inline bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) {
|
||||
struct in_addr v4;
|
||||
if (inet_pton(AF_INET, addr_part.c_str(), &v4) == 1) {
|
||||
int prefix = 32;
|
||||
if (!prefix_part.empty() &&
|
||||
!parse_int_in_range(prefix_part.data(), prefix_part.size(), 0, 32,
|
||||
prefix)) {
|
||||
return false;
|
||||
if (!prefix_part.empty()) {
|
||||
auto r = from_chars(prefix_part.data(),
|
||||
prefix_part.data() + prefix_part.size(), prefix);
|
||||
if (r.ec != std::errc{} ||
|
||||
r.ptr != prefix_part.data() + prefix_part.size()) {
|
||||
return false;
|
||||
}
|
||||
if (prefix < 0 || prefix > 32) { return false; }
|
||||
}
|
||||
out.kind = NoProxyKind::IPv4Cidr;
|
||||
std::memcpy(out.net.data(), &v4, sizeof(v4));
|
||||
@@ -12564,10 +12461,14 @@ inline bool parse_no_proxy_entry(const std::string &token, NoProxyEntry &out) {
|
||||
struct in6_addr v6;
|
||||
if (inet_pton(AF_INET6, addr_part.c_str(), &v6) == 1) {
|
||||
int prefix = 128;
|
||||
if (!prefix_part.empty() &&
|
||||
!parse_int_in_range(prefix_part.data(), prefix_part.size(), 0, 128,
|
||||
prefix)) {
|
||||
return false;
|
||||
if (!prefix_part.empty()) {
|
||||
auto r = from_chars(prefix_part.data(),
|
||||
prefix_part.data() + prefix_part.size(), prefix);
|
||||
if (r.ec != std::errc{} ||
|
||||
r.ptr != prefix_part.data() + prefix_part.size()) {
|
||||
return false;
|
||||
}
|
||||
if (prefix < 0 || prefix > 128) { return false; }
|
||||
}
|
||||
out.kind = NoProxyKind::IPv6Cidr;
|
||||
std::memcpy(out.net.data(), &v6, sizeof(v6));
|
||||
@@ -14490,13 +14391,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
// Connection has been closed on client
|
||||
if (!line_reader.getline()) { return false; }
|
||||
|
||||
// RFC 9112 2.2: ignore an empty line received before the request-line. Some
|
||||
// clients send an extra CRLF after a request body, which would otherwise be
|
||||
// parsed as the next request on a persistent connection.
|
||||
if (strcmp(line_reader.ptr(), "\r\n") == 0 && !line_reader.getline()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Request req;
|
||||
req.start_time_ = std::chrono::steady_clock::now();
|
||||
req.remote_addr = remote_addr;
|
||||
@@ -14690,12 +14584,6 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
protocols.emplace_back(b, e);
|
||||
});
|
||||
selected_subprotocol = entry.sub_protocol_selector(protocols);
|
||||
|
||||
// Ignore a selection the client did not offer (RFC 6455 4.2.2)
|
||||
if (std::find(protocols.begin(), protocols.end(),
|
||||
selected_subprotocol) == protocols.end()) {
|
||||
selected_subprotocol.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15766,7 +15654,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
if (next_host.empty()) { next_host = host_; }
|
||||
if (next_path.empty()) { next_path = "/"; }
|
||||
|
||||
auto path = std::move(next_path) + next_query;
|
||||
auto path = decode_path_component(next_path) + next_query;
|
||||
|
||||
// Same host redirect - use current client
|
||||
if (next_scheme == scheme && next_host == host_ && next_port == port_) {
|
||||
@@ -16021,9 +15909,9 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
// Write request line and headers
|
||||
if (detail::write_request_line(bstrm, req.method, path_with_query) < 0) {
|
||||
// A rejected method (not a token, e.g. carrying CR/LF) or target (e.g.
|
||||
// CR/LF in a caller-supplied path under set_path_encode(false)) must
|
||||
// fail the request cleanly instead of emitting a request-line-less,
|
||||
// header-injecting request.
|
||||
// CR/LF smuggled in via a decoded redirect Location under
|
||||
// set_path_encode(false)) must fail the request cleanly instead of
|
||||
// emitting a request-line-less, header-injecting request.
|
||||
error = Error::Write;
|
||||
rejected_locally = true;
|
||||
output_error_log(error, &req);
|
||||
@@ -18627,9 +18515,6 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
|
||||
inline void
|
||||
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
|
||||
if (!ctx_) { return; }
|
||||
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
server_certificate_verifier_set_ = static_cast<bool>(verifier);
|
||||
#endif
|
||||
tls::set_verify_callback(ctx_, verifier);
|
||||
}
|
||||
|
||||
@@ -18691,9 +18576,6 @@ inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) {
|
||||
enable_windows_cert_verification_ &&
|
||||
system_ca_mode_ != SystemCAMode::Disabled && ca_cert_file_path_.empty() &&
|
||||
ca_cert_dir_path_.empty() && ca_cert_pem_.empty() && !ca_cert_store_set_;
|
||||
// Only a verifier set through set_server_certificate_verifier() is seen
|
||||
// here, not one installed with tls::set_verify_callback() directly.
|
||||
options.server_certificate_verifier_set = server_certificate_verifier_set_;
|
||||
#endif
|
||||
|
||||
tls::session_t session = nullptr;
|
||||
@@ -21054,24 +20936,29 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
||||
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
|
||||
auto is_ip = ip_len > 0;
|
||||
|
||||
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
|
||||
// in buf.tag and the raw value in buf.p / buf.len.
|
||||
// Check Subject Alternative Names (SAN)
|
||||
// In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags
|
||||
// - DNS names: raw string bytes
|
||||
// - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6)
|
||||
const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
|
||||
while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
|
||||
const unsigned char *p = san->buf.p;
|
||||
size_t len = san->buf.len;
|
||||
auto san_type = san->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK;
|
||||
|
||||
if (is_ip) {
|
||||
// For an IP host, only a matching iPAddress SAN of the same family
|
||||
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
|
||||
if (san_type == MBEDTLS_X509_SAN_IP_ADDRESS && len == ip_len &&
|
||||
memcmp(p, ip_bytes, ip_len) == 0) {
|
||||
return true;
|
||||
if (len == ip_len && memcmp(p, ip_bytes, ip_len) == 0) { return true; }
|
||||
} else {
|
||||
// Check if this SAN is a DNS name (printable ASCII string)
|
||||
bool is_dns = len > 0;
|
||||
for (size_t i = 0; i < len && is_dns; i++) {
|
||||
if (p[i] < 32 || p[i] > 126) { is_dns = false; }
|
||||
}
|
||||
if (is_dns) {
|
||||
std::string san_name(reinterpret_cast<const char *>(p), len);
|
||||
if (detail::match_hostname(san_name, host_str)) { return true; }
|
||||
}
|
||||
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
|
||||
std::string san_name(reinterpret_cast<const char *>(p), len);
|
||||
if (detail::match_hostname(san_name, host_str)) { return true; }
|
||||
}
|
||||
san = san->next;
|
||||
}
|
||||
@@ -21150,45 +21037,65 @@ inline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
|
||||
const mbedtls_x509_sequence *cur = &x509->subject_alt_names;
|
||||
while (cur != nullptr) {
|
||||
if (cur->buf.len > 0) {
|
||||
// Mbed TLS stores SAN as ASN.1 sequences
|
||||
// The tag byte indicates the type
|
||||
const unsigned char *p = cur->buf.p;
|
||||
size_t value_len = cur->buf.len;
|
||||
size_t len = cur->buf.len;
|
||||
|
||||
SanEntry entry;
|
||||
switch (cur->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK) {
|
||||
case MBEDTLS_X509_SAN_DNS_NAME:
|
||||
entry.type = SanType::DNS;
|
||||
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||
break;
|
||||
case MBEDTLS_X509_SAN_IP_ADDRESS:
|
||||
entry.type = SanType::IP;
|
||||
if (value_len == 4) {
|
||||
// IPv4
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
|
||||
entry.value = buf;
|
||||
} else if (value_len == 16) {
|
||||
// IPv6
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
|
||||
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9],
|
||||
p[10], p[11], p[12], p[13], p[14], p[15]);
|
||||
entry.value = buf;
|
||||
// First byte is the tag
|
||||
unsigned char tag = *p;
|
||||
p++;
|
||||
len--;
|
||||
|
||||
// Parse length (simple single-byte length assumed)
|
||||
if (len > 0 && *p < 0x80) {
|
||||
size_t value_len = *p;
|
||||
p++;
|
||||
len--;
|
||||
|
||||
if (value_len <= len) {
|
||||
SanEntry entry;
|
||||
// ASN.1 context tags for GeneralName
|
||||
switch (tag & 0x1F) {
|
||||
case 2: // dNSName
|
||||
entry.type = SanType::DNS;
|
||||
entry.value =
|
||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
||||
break;
|
||||
case 7: // iPAddress
|
||||
entry.type = SanType::IP;
|
||||
if (value_len == 4) {
|
||||
// IPv4
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
|
||||
entry.value = buf;
|
||||
} else if (value_len == 16) {
|
||||
// IPv6
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf),
|
||||
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
|
||||
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8],
|
||||
p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
|
||||
entry.value = buf;
|
||||
}
|
||||
break;
|
||||
case 1: // rfc822Name (email)
|
||||
entry.type = SanType::EMAIL;
|
||||
entry.value =
|
||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
||||
break;
|
||||
case 6: // uniformResourceIdentifier
|
||||
entry.type = SanType::URI;
|
||||
entry.value =
|
||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
||||
break;
|
||||
default: entry.type = SanType::OTHER; break;
|
||||
}
|
||||
|
||||
if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
|
||||
}
|
||||
break;
|
||||
case MBEDTLS_X509_SAN_RFC822_NAME:
|
||||
entry.type = SanType::EMAIL;
|
||||
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||
break;
|
||||
case MBEDTLS_X509_SAN_UNIFORM_RESOURCE_IDENTIFIER:
|
||||
entry.type = SanType::URI;
|
||||
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||
break;
|
||||
default: entry.type = SanType::OTHER; break;
|
||||
}
|
||||
|
||||
if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
|
||||
}
|
||||
cur = cur->next;
|
||||
}
|
||||
@@ -22830,9 +22737,9 @@ inline void WebSocket::start_heartbeat() {
|
||||
if (ping_interval_sec_ == 0) { return; }
|
||||
ping_thread_ = std::thread([this]() {
|
||||
std::unique_lock<std::mutex> lock(ping_mutex_);
|
||||
// The predicate keeps a spurious wakeup from sending a ping early
|
||||
while (!ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_),
|
||||
[this]() { return closed_.load(); })) {
|
||||
while (!closed_) {
|
||||
ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
|
||||
if (closed_) { break; }
|
||||
// If the peer has failed to respond to the previous pings, give up.
|
||||
// RFC 6455 does not define a pong-timeout mechanism; this is an
|
||||
// opt-in liveness check controlled by max_missed_pongs_.
|
||||
|
||||
@@ -146,13 +146,6 @@ if(HTTPLIB_IS_USING_OPENSSL)
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
# cert_san_types.pem: the bytes of each SAN read as the other type:
|
||||
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||
execute_process(
|
||||
COMMAND ${OPENSSL_COMMAND} 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
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
endif()
|
||||
|
||||
add_subdirectory(fuzzing)
|
||||
@@ -33,7 +33,3 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=127.0.0.1" -
|
||||
# different address. The SAN address must match; the CN address
|
||||
# must be ignored.
|
||||
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem
|
||||
|
||||
# cert_san_types.pem: the bytes of each SAN read as the other type:
|
||||
# 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
|
||||
+1
-11
@@ -137,15 +137,6 @@ cert_ipv6_pem = custom_target(
|
||||
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=::1', '-addext', 'subjectAltName=IP:2001:db8::1', '-out', '@OUTPUT@']
|
||||
)
|
||||
|
||||
# cert_san_types.pem: the bytes of each SAN read as the other type: DNS:a.zz is
|
||||
# 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||
cert_san_types_pem = custom_target(
|
||||
'cert_san_types_pem',
|
||||
input: key_pem,
|
||||
output: 'cert_san_types.pem',
|
||||
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@']
|
||||
)
|
||||
|
||||
# Copy test files to the build directory
|
||||
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
||||
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
||||
@@ -187,8 +178,7 @@ test(
|
||||
client_encrypted_pbes1_key_pem,
|
||||
client_encrypted_cert_pem,
|
||||
cert_ip_cn_pem,
|
||||
cert_ipv6_pem,
|
||||
cert_san_types_pem
|
||||
cert_ipv6_pem
|
||||
],
|
||||
workdir: meson.current_build_dir(),
|
||||
timeout: 300
|
||||
|
||||
+231
-819
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user