mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-05 06:43:43 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf3693cb5c | ||
|
|
4fd9ae8f42 | ||
|
|
3d40dfc727 | ||
|
|
dd71728110 | ||
|
|
7255a7e979 | ||
|
|
8a3abfb597 | ||
|
|
10aadd57f7 | ||
|
|
43863e1f67 | ||
|
|
0db1df7cf2 | ||
|
|
639391ad7f | ||
|
|
0715c2739e | ||
|
|
3330d0eb06 | ||
|
|
c1c2b1f4b4 | ||
|
|
e11dbec7b3 | ||
|
|
174bce5ccf | ||
|
|
57c4f7f385 | ||
|
|
2fb2dbbe1e | ||
|
|
8b6ab24159 | ||
|
|
5a202d3d5f |
+21
-3
@@ -15,7 +15,7 @@
|
||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||
* HTTPLIB_REQUIRE_ZSTD (default off)
|
||||
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (default off)
|
||||
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on)
|
||||
* HTTPLIB_USE_NON_BLOCKING_GETADDRINFO (default on when supported)
|
||||
* HTTPLIB_COMPILE (default off)
|
||||
* HTTPLIB_INSTALL (default on)
|
||||
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is ON)
|
||||
@@ -181,6 +181,24 @@ if(HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES)
|
||||
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
||||
endif()
|
||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
||||
if(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO AND WIN32)
|
||||
include(CheckCXXSymbolExists)
|
||||
|
||||
set(_httplib_cmake_required_definitions ${CMAKE_REQUIRED_DEFINITIONS})
|
||||
set(_httplib_cmake_required_libraries ${CMAKE_REQUIRED_LIBRARIES})
|
||||
list(APPEND CMAKE_REQUIRED_DEFINITIONS -D_WIN32_WINNT=0x0A00)
|
||||
list(APPEND CMAKE_REQUIRED_LIBRARIES ws2_32)
|
||||
check_cxx_symbol_exists(GetAddrInfoExCancel "winsock2.h;ws2tcpip.h" HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||
set(CMAKE_REQUIRED_DEFINITIONS ${_httplib_cmake_required_definitions})
|
||||
set(CMAKE_REQUIRED_LIBRARIES ${_httplib_cmake_required_libraries})
|
||||
unset(_httplib_cmake_required_definitions)
|
||||
unset(_httplib_cmake_required_libraries)
|
||||
|
||||
if(NOT HTTPLIB_HAVE_GETADDRINFOEXCANCEL)
|
||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO FALSE)
|
||||
message(WARNING "GetAddrInfoExCancel is unavailable; disabling non-blocking getaddrinfo.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
||||
@@ -367,7 +385,7 @@ target_link_libraries(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
# Needed for API from MacOS Security framework
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_OPENSSL}>,$<BOOL:${HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:-framework CFNetwork -framework CoreFoundation -framework Security>"
|
||||
# Needed for non-blocking getaddrinfo on MacOS
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||
"$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>>:-framework CFNetwork -framework CoreFoundation>"
|
||||
# Can't put multiple targets in a single generator expression or it bugs out.
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::common>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
||||
@@ -390,7 +408,7 @@ target_compile_definitions(${PROJECT_NAME} ${_INTERFACE_OR_PUBLIC}
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_SUPPORT>
|
||||
$<$<AND:$<PLATFORM_ID:Darwin>,$<BOOL:${HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES}>>:CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES>
|
||||
$<$<BOOL:${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
||||
$<$<BOOL:${HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO}>:CPPHTTPLIB_USE_NON_BLOCKING_GETADDRINFO>
|
||||
)
|
||||
|
||||
# CMake configuration files installation directory
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ sse.on_error([](httplib::Error err) { });
|
||||
#### Configuration
|
||||
|
||||
```cpp
|
||||
// Set reconnect interval (default: 3000ms)
|
||||
// Set reconnect interval (default: 3000ms, minimum: 100ms)
|
||||
sse.set_reconnect_interval(5000);
|
||||
|
||||
// Set max reconnect attempts (default: 0 = unlimited)
|
||||
|
||||
@@ -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 certs via CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) with revocation checking | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend, with revocation checking. 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, verification can also be disabled at runtime:
|
||||
|
||||
@@ -452,6 +452,9 @@ svr.set_logger([](const httplib::Request& req, const httplib::Response& res) {
|
||||
});
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> `req.path` is percent-decoded and may contain control characters such as CR/LF. Escape request data before writing it to a log file (see [docker/main.cc](docker/main.cc) for an example).
|
||||
|
||||
#### Pre-compression Logging
|
||||
|
||||
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
||||
|
||||
+26
-6
@@ -48,6 +48,23 @@ std::string get_error_time_format() {
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
// Escape a value for a log line the way NGINX does: '"', '\\', control
|
||||
// bytes and non-ASCII bytes become \xHH. Request fields are attacker-controlled
|
||||
// (e.g. a raw CR in the request target or a decoded %0D%0A in req.path), so
|
||||
// writing them verbatim would let a client forge extra log lines.
|
||||
std::string escape_log(const std::string &s) {
|
||||
std::string out;
|
||||
out.reserve(s.size());
|
||||
for (unsigned char c : s) {
|
||||
if (c == '"' || c == '\\' || c < 0x20 || c >= 0x7f) {
|
||||
out += std::format("\\x{:02X}", c);
|
||||
} else {
|
||||
out += static_cast<char>(c);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// NGINX Combined log format:
|
||||
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||
// "$http_referer" "$http_user_agent"
|
||||
@@ -55,7 +72,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
||||
std::string remote_user =
|
||||
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
||||
auto time_local = get_time_format();
|
||||
auto request = std::format("{} {} {}", req.method, req.path, req.version);
|
||||
// $request is the original request line, so log the raw target rather than
|
||||
// the percent-decoded req.path.
|
||||
auto request = std::format("{} {} {}", req.method, req.target, req.version);
|
||||
auto status = res.status;
|
||||
auto body_bytes_sent = res.body.size();
|
||||
auto http_referer = req.get_header_value("Referer");
|
||||
@@ -64,9 +83,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
||||
if (http_user_agent.empty()) http_user_agent = "-";
|
||||
|
||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||
req.remote_addr, remote_user, time_local, request,
|
||||
status, body_bytes_sent, http_referer,
|
||||
http_user_agent)
|
||||
req.remote_addr, remote_user, time_local,
|
||||
escape_log(request), status, body_bytes_sent,
|
||||
escape_log(http_referer), escape_log(http_user_agent))
|
||||
<< std::endl;
|
||||
}
|
||||
|
||||
@@ -79,14 +98,15 @@ void nginx_error_logger(const Error &err, const Request *req) {
|
||||
|
||||
if (req) {
|
||||
auto request =
|
||||
std::format("{} {} {}", req->method, req->path, req->version);
|
||||
std::format("{} {} {}", req->method, req->target, req->version);
|
||||
auto host = req->get_header_value("Host");
|
||||
if (host.empty()) host = "-";
|
||||
|
||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||
"\"{}\", host: \"{}\"",
|
||||
time_local, level, to_string(err),
|
||||
req->remote_addr, request, host)
|
||||
req->remote_addr, escape_log(request),
|
||||
escape_log(host))
|
||||
<< std::endl;
|
||||
} else {
|
||||
// If no request context, just log the error
|
||||
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.58.0"
|
||||
version = "0.59.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.58.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003a00"
|
||||
#define CPPHTTPLIB_VERSION "0.59.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x003b00"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -831,14 +831,21 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
|
||||
return {p, std::errc{}};
|
||||
}
|
||||
|
||||
inline bool parse_port(const char *s, size_t len, int &port) {
|
||||
inline bool parse_int_in_range(const char *s, size_t len, int lo, int hi,
|
||||
int &out) {
|
||||
int val = 0;
|
||||
auto r = from_chars(s, s + len, val);
|
||||
if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
|
||||
port = val;
|
||||
if (r.ec != std::errc{} || r.ptr != s + len || val < lo || val > hi) {
|
||||
return false;
|
||||
}
|
||||
out = 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);
|
||||
}
|
||||
@@ -939,6 +946,48 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolves a relative-path or query-only Location value against the path of
|
||||
// the request being redirected (RFC 3986 section 5.2). Absolute URIs and
|
||||
// references starting with '/' are returned unchanged.
|
||||
inline std::string resolve_relative_location(const std::string &location,
|
||||
const std::string &base) {
|
||||
if (location.empty() || location[0] == '/') { return location; }
|
||||
|
||||
// A ':' in the first segment means the value has a scheme.
|
||||
if (location.find(':') < location.find_first_of("/?#")) { return location; }
|
||||
|
||||
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; }
|
||||
|
||||
auto base_path = base.substr(0, base.find_first_of("?#"));
|
||||
if (location[0] == '?') { return base_path + location; }
|
||||
|
||||
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
|
||||
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
|
||||
|
||||
// Remove "." and ".." segments from the merged path.
|
||||
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
|
||||
std::string path;
|
||||
size_t i = 0;
|
||||
while (i < path_end) {
|
||||
auto next = (std::min)(merged.find('/', i + 1), path_end);
|
||||
auto segment = merged.substr(i + 1, next - i - 1);
|
||||
auto is_last = next == path_end;
|
||||
if (segment == "." || segment == "..") {
|
||||
if (segment == "..") {
|
||||
path.erase((std::min)(path.rfind('/'), path.size()));
|
||||
}
|
||||
if (is_last) { path += '/'; }
|
||||
} else {
|
||||
path += '/';
|
||||
path += segment;
|
||||
}
|
||||
i = next;
|
||||
}
|
||||
if (path.empty()) { path = "/"; }
|
||||
|
||||
return path + merged.substr(path_end);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
enum class SSLVerifierResponse {
|
||||
@@ -1842,6 +1891,7 @@ struct Response {
|
||||
ContentProvider content_provider_;
|
||||
ContentProviderResourceReleaser content_provider_resource_releaser_;
|
||||
bool is_chunked_content_provider_ = false;
|
||||
bool is_file_content_provider_ = false;
|
||||
bool content_provider_success_ = false;
|
||||
std::string file_content_path_;
|
||||
std::string file_content_content_type_;
|
||||
@@ -3399,6 +3449,9 @@ 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;
|
||||
@@ -3914,6 +3967,7 @@ bool is_field_vchar(char c);
|
||||
bool is_field_content(const std::string &s);
|
||||
bool is_field_value(const std::string &s);
|
||||
bool is_field_valid(const std::string &name, const std::string &value);
|
||||
bool is_request_target(const std::string &s);
|
||||
|
||||
} // namespace fields
|
||||
} // namespace detail
|
||||
@@ -4322,7 +4376,8 @@ public:
|
||||
void stop();
|
||||
|
||||
private:
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms,
|
||||
bool &has_data);
|
||||
void run_event_loop();
|
||||
void dispatch_event(const SSEMessage &msg);
|
||||
bool should_reconnect(int count) const;
|
||||
@@ -4832,30 +4887,23 @@ inline void SSEClient::stop() {
|
||||
}
|
||||
|
||||
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
int &retry_ms) {
|
||||
int &retry_ms, bool &has_data) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty() || line == "\r") { return true; }
|
||||
if (line.empty()) { return true; }
|
||||
|
||||
// Lines starting with ':' are comments (ignored)
|
||||
if (!line.empty() && line[0] == ':') { return false; }
|
||||
if (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 + 1 < line.size()) {
|
||||
if (colon_pos != std::string::npos && 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
|
||||
@@ -4863,14 +4911,16 @@ 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 (!msg.data.empty()) { msg.data += "\n"; }
|
||||
if (has_data) { 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;
|
||||
} else if (field == "retry") {
|
||||
// Parse retry interval in milliseconds
|
||||
{
|
||||
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||
if (detail::is_numeric(value)) {
|
||||
int v = 0;
|
||||
auto res =
|
||||
detail::from_chars(value.data(), value.data() + value.size(), v);
|
||||
@@ -4937,6 +4987,7 @@ inline void SSEClient::run_event_loop() {
|
||||
// Event receiving loop
|
||||
std::string buffer;
|
||||
SSEMessage current_msg;
|
||||
bool has_data = false;
|
||||
|
||||
while (running_.load() && result.next()) {
|
||||
buffer.append(result.data(), result.size());
|
||||
@@ -4950,18 +5001,25 @@ 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_);
|
||||
parse_sse_line(line, current_msg, reconnect_interval_ms_, has_data);
|
||||
|
||||
if (event_complete && !current_msg.data.empty()) {
|
||||
// Update last_event_id for reconnection
|
||||
if (event_complete) {
|
||||
// Update last_event_id for reconnection, even for an event that
|
||||
// has no data
|
||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
||||
|
||||
// Dispatch event to appropriate handler
|
||||
dispatch_event(current_msg);
|
||||
// An event without a data field is not dispatched
|
||||
if (has_data) { dispatch_event(current_msg); }
|
||||
|
||||
// Reset the message for the next event either way
|
||||
current_msg.clear();
|
||||
has_data = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5006,11 +5064,15 @@ inline bool SSEClient::should_reconnect(int count) const {
|
||||
}
|
||||
|
||||
inline void SSEClient::wait_for_reconnect() {
|
||||
// Use small increments to check running_ flag frequently
|
||||
// Use small increments to check running_ flag frequently.
|
||||
// Always wait at least one increment, so that a zero interval (e.g.
|
||||
// "retry: 0" from the server) cannot cause a busy reconnect loop.
|
||||
const auto step_ms = 100;
|
||||
auto interval_ms = (std::max)(reconnect_interval_ms_, step_ms);
|
||||
auto waited = 0;
|
||||
while (running_.load() && waited < reconnect_interval_ms_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
waited += 100;
|
||||
while (running_.load() && waited < interval_ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(step_ms));
|
||||
waited += step_ms;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5071,6 +5133,9 @@ bool is_peer_closed(session_t session, socket_t sock);
|
||||
|
||||
// Certificate verification
|
||||
cert_t get_peer_cert(const_session_t session);
|
||||
// The certificates the peer sent, leaf first. Free each with free_cert(), and
|
||||
// do not use them after free_session(), as with get_peer_cert().
|
||||
size_t get_peer_certs(const_session_t session, std::vector<cert_t> &certs);
|
||||
void free_cert(cert_t cert);
|
||||
bool verify_hostname(cert_t cert, const char *hostname);
|
||||
uint64_t hostname_mismatch_code();
|
||||
@@ -5778,15 +5843,15 @@ inline std::string encode_path(const std::string &s) {
|
||||
switch (s[i]) {
|
||||
case ' ': result += "%20"; break;
|
||||
case '+': result += "%2B"; break;
|
||||
case '\r': result += "%0D"; break;
|
||||
case '\n': result += "%0A"; break;
|
||||
case '\'': result += "%27"; break;
|
||||
case ',': result += "%2C"; break;
|
||||
// case ':': result += "%3A"; break; // ok? probably...
|
||||
case ';': result += "%3B"; break;
|
||||
default:
|
||||
auto c = static_cast<uint8_t>(s[i]);
|
||||
if (c >= 0x80) {
|
||||
// Control characters (incl. CR/LF) and non-ASCII bytes are not allowed
|
||||
// in a request-target as-is.
|
||||
if (c < 0x20 || c == 0x7f || c >= 0x80) {
|
||||
result += '%';
|
||||
char hex[4];
|
||||
auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
|
||||
@@ -6559,18 +6624,30 @@ inline bool keep_alive(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
// `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) {
|
||||
assert(keep_alive_max_count > 0);
|
||||
auto ret = false;
|
||||
auto count = keep_alive_max_count;
|
||||
while (count > 0 && keep_alive(svr_sock, sock, keep_alive_timeout_sec)) {
|
||||
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;
|
||||
}
|
||||
auto close_connection = count == 1;
|
||||
auto connection_closed = false;
|
||||
ret = callback(close_connection, connection_closed);
|
||||
ret = callback(socket_readable, close_connection, connection_closed);
|
||||
if (!ret || connection_closed) { break; }
|
||||
count--;
|
||||
}
|
||||
@@ -6584,14 +6661,16 @@ 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,
|
||||
[&](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 strm.is_readable(); },
|
||||
[&](bool socket_readable, bool close_connection,
|
||||
bool &connection_closed) {
|
||||
if (socket_readable) { strm.set_readable_hint(); }
|
||||
return callback(strm, close_connection, connection_closed);
|
||||
});
|
||||
}
|
||||
@@ -7580,7 +7659,8 @@ inline bool parse_quality(const char *b, const char *e, std::string &token,
|
||||
|
||||
double v = 0.0;
|
||||
auto res = from_chars(pb + r.first, pb + r.second, v);
|
||||
if (res.ec != std::errc{} || v < 0.0 || v > 1.0) {
|
||||
if (res.ec != std::errc{} || res.ptr != pb + r.second ||
|
||||
v < 0.0 || v > 1.0) {
|
||||
invalid = true;
|
||||
return true;
|
||||
}
|
||||
@@ -8517,14 +8597,11 @@ bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status,
|
||||
|
||||
inline ssize_t write_request_line(Stream &strm, const std::string &method,
|
||||
const std::string &path) {
|
||||
// Neither the method nor the request target may carry CR/LF (or other
|
||||
// control octets); otherwise a value smuggled into either splits the request
|
||||
// line and injects headers or a whole request. The method must be a token
|
||||
// (RFC 9110 Section 9.1), which also rejects an empty method and embedded
|
||||
// spaces. The target gets the same field-value check that already guards
|
||||
// header values in check_and_write_headers.
|
||||
// Neither the method nor the request target may carry CR/LF, SP or other
|
||||
// control octets; otherwise a value smuggled into either splits the request
|
||||
// line and injects headers or a whole request.
|
||||
if (!fields::is_token(method)) { return -1; }
|
||||
if (!fields::is_field_value(path)) { return -1; }
|
||||
if (!fields::is_request_target(path)) { return -1; }
|
||||
|
||||
std::string s = method;
|
||||
s += ' ';
|
||||
@@ -8746,9 +8823,9 @@ inline bool compress_content_provider(const ContentProvider &content_provider,
|
||||
return cmp.compress(nullptr, 0, true, append);
|
||||
}
|
||||
|
||||
// Serves `m` as the response body. `set_content_provider()` clears the coding,
|
||||
// so recording it has to come after; keeping both here means a third
|
||||
// file-serving path cannot get that order wrong.
|
||||
// Serves `m` as the response body. `set_content_provider()` clears the coding
|
||||
// and the file flag, so recording them has to come after; keeping all of it
|
||||
// here means a third file-serving path cannot get that order wrong.
|
||||
inline void set_file_content_provider(Response &res,
|
||||
const std::shared_ptr<mmap> &m,
|
||||
const std::string &content_type,
|
||||
@@ -8760,6 +8837,7 @@ inline void set_file_content_provider(Response &res,
|
||||
return true;
|
||||
});
|
||||
|
||||
res.is_file_content_provider_ = true;
|
||||
res.content_coding_ = encoding;
|
||||
}
|
||||
|
||||
@@ -10051,6 +10129,19 @@ 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) {
|
||||
@@ -10177,6 +10268,12 @@ inline bool is_field_valid(const std::string &name, const std::string &value) {
|
||||
return is_field_name(name) && is_field_value(value);
|
||||
}
|
||||
|
||||
// RFC 9112 §2.2/§3.2: the request-target has no SP, HTAB or other control
|
||||
// characters (incl. bare CR). obs-text (raw UTF-8) is allowed.
|
||||
inline bool is_request_target(const std::string &s) {
|
||||
return std::all_of(s.begin(), s.end(), is_field_vchar);
|
||||
}
|
||||
|
||||
} // namespace fields
|
||||
|
||||
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||
@@ -10501,13 +10598,16 @@ 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,
|
||||
[&](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 strm.is_readable(); },
|
||||
[&](bool socket_readable, bool close_connection,
|
||||
bool &connection_closed) {
|
||||
if (socket_readable) { strm.set_readable_hint(); }
|
||||
return callback(strm, close_connection, connection_closed);
|
||||
});
|
||||
}
|
||||
@@ -10548,7 +10648,13 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
|
||||
}
|
||||
|
||||
std::string algo = "MD5";
|
||||
if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); }
|
||||
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; }
|
||||
}
|
||||
|
||||
std::string response;
|
||||
{
|
||||
@@ -10571,14 +10677,23 @@ inline std::pair<std::string, std::string> make_digest_authentication_header(
|
||||
|
||||
auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("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 + "\"");
|
||||
// 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 key = is_proxy ? "Proxy-Authorization" : "Authorization";
|
||||
return std::make_pair(key, field);
|
||||
@@ -10643,11 +10758,10 @@ 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 (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) {
|
||||
// integration, independent of the TLS backend.
|
||||
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) {
|
||||
if (der_cert.empty()) { return false; }
|
||||
|
||||
out_error = 0;
|
||||
@@ -10665,14 +10779,41 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
||||
auto cert_guard =
|
||||
scope_exit([&] { CertFreeCertificateContext(cert_context); });
|
||||
|
||||
// Give CryptoAPI the certificates the server sent. Without them it follows
|
||||
// the leaf's AIA URL, which may lead to an issuer under an untrusted root.
|
||||
std::vector<tls::cert_t> peer_certs;
|
||||
tls::get_peer_certs(session, peer_certs);
|
||||
auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr);
|
||||
auto store_guard = scope_exit([&] {
|
||||
for (auto cert : peer_certs) {
|
||||
tls::free_cert(cert);
|
||||
}
|
||||
if (store) { CertCloseStore(store, 0); }
|
||||
});
|
||||
for (auto cert : peer_certs) {
|
||||
std::vector<unsigned char> der;
|
||||
if (store && tls::get_cert_der(cert, der)) {
|
||||
CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(),
|
||||
static_cast<DWORD>(der.size()),
|
||||
CERT_STORE_ADD_USE_EXISTING, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup chain parameters
|
||||
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(
|
||||
nullptr, cert_context, nullptr, cert_context->hCertStore, &chain_para,
|
||||
nullptr, cert_context, nullptr, store, &chain_para,
|
||||
CERT_CHAIN_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
|
||||
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
|
||||
nullptr, &chain_context);
|
||||
@@ -10780,6 +10921,9 @@ 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
|
||||
};
|
||||
|
||||
@@ -10814,12 +10958,24 @@ 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. 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);
|
||||
// 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);
|
||||
#endif
|
||||
|
||||
{
|
||||
@@ -10864,10 +11020,12 @@ inline bool setup_client_tls_session(
|
||||
|
||||
if (verification_status == SSLVerifierResponse::NoDecisionMade &&
|
||||
server_certificate_verification) {
|
||||
auto verify_result = get_verify_result(session);
|
||||
if (verify_result != 0) {
|
||||
return fail(Error::SSLServerVerification, 0,
|
||||
static_cast<uint64_t>(verify_result));
|
||||
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 server_cert = get_peer_cert(session);
|
||||
@@ -10887,18 +11045,17 @@ inline bool setup_client_tls_session(
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||
// Additional Windows Schannel verification.
|
||||
// This provides real-time certificate validation with Windows Update
|
||||
// integration, working with both OpenSSL and MbedTLS backends.
|
||||
// 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.
|
||||
if (options.windows_cert_verification) {
|
||||
std::vector<unsigned char> der;
|
||||
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)) {
|
||||
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -11660,6 +11817,7 @@ inline void Response::set_content_provider(
|
||||
if (in_length > 0) { content_provider_ = std::move(provider); }
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = false;
|
||||
is_file_content_provider_ = false;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
@@ -11671,6 +11829,7 @@ inline void Response::set_content_provider(
|
||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = false;
|
||||
is_file_content_provider_ = false;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
@@ -11682,6 +11841,7 @@ inline void Response::set_chunked_content_provider(
|
||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = true;
|
||||
is_file_content_provider_ = false;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
@@ -12363,14 +12523,10 @@ 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()) {
|
||||
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; }
|
||||
if (!prefix_part.empty() &&
|
||||
!parse_int_in_range(prefix_part.data(), prefix_part.size(), 0, 32,
|
||||
prefix)) {
|
||||
return false;
|
||||
}
|
||||
out.kind = NoProxyKind::IPv4Cidr;
|
||||
std::memcpy(out.net.data(), &v4, sizeof(v4));
|
||||
@@ -12382,14 +12538,10 @@ 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()) {
|
||||
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; }
|
||||
if (!prefix_part.empty() &&
|
||||
!parse_int_in_range(prefix_part.data(), prefix_part.size(), 0, 128,
|
||||
prefix)) {
|
||||
return false;
|
||||
}
|
||||
out.kind = NoProxyKind::IPv6Cidr;
|
||||
std::memcpy(out.net.data(), &v6, sizeof(v6));
|
||||
@@ -13248,6 +13400,8 @@ inline bool Server::parse_request_line(const char *s, Request &req) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!detail::fields::is_request_target(req.target)) { return false; }
|
||||
|
||||
{
|
||||
// Skip URL fragment
|
||||
for (size_t i = 0; i < req.target.size(); i++) {
|
||||
@@ -13335,9 +13489,27 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
||||
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
|
||||
|
||||
// Combine small body with headers to reduce write syscalls
|
||||
if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) {
|
||||
bstrm.write(res.body.data(), res.body.size());
|
||||
// Combine a small body with the headers so the whole response leaves in a
|
||||
// single write. A large body is written on its own instead: a copy of it
|
||||
// costs more than the extra write saves.
|
||||
auto send_body = req.method != "HEAD";
|
||||
auto body_is_separate = false;
|
||||
auto provider_done = false;
|
||||
if (send_body && !res.body.empty() && !res.content_provider_) {
|
||||
if (res.body.size() < CPPHTTPLIB_SEND_BUFSIZ) {
|
||||
bstrm.write(res.body.data(), res.body.size());
|
||||
} else {
|
||||
body_is_separate = true;
|
||||
}
|
||||
} else if (send_body && res.content_provider_ &&
|
||||
res.is_file_content_provider_ &&
|
||||
res.content_length_ < CPPHTTPLIB_SEND_BUFSIZ) {
|
||||
// A small file is read into the same buffer. Other providers may produce
|
||||
// their data over time, so they are never held back.
|
||||
if (!write_content_with_provider(bstrm, req, res, boundary, content_type)) {
|
||||
return false;
|
||||
}
|
||||
provider_done = true;
|
||||
}
|
||||
|
||||
// Log before writing to avoid race condition with client-side code that
|
||||
@@ -13348,17 +13520,20 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
||||
auto &data = bstrm.get_buffer();
|
||||
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
|
||||
|
||||
// Streaming body
|
||||
auto ret = true;
|
||||
if (req.method != "HEAD" && res.content_provider_) {
|
||||
if (write_content_with_provider(strm, req, res, boundary, content_type)) {
|
||||
res.content_provider_success_ = true;
|
||||
} else {
|
||||
ret = false;
|
||||
}
|
||||
if (body_is_separate) {
|
||||
return detail::write_data(strm, res.body.data(), res.body.size());
|
||||
}
|
||||
|
||||
return ret;
|
||||
// Streaming body
|
||||
if (send_body && res.content_provider_) {
|
||||
if (!provider_done &&
|
||||
!write_content_with_provider(strm, req, res, boundary, content_type)) {
|
||||
return false;
|
||||
}
|
||||
res.content_provider_success_ = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool
|
||||
@@ -14289,6 +14464,13 @@ 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;
|
||||
@@ -14300,8 +14482,21 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
res.version = "HTTP/1.1";
|
||||
res.headers = default_headers_;
|
||||
|
||||
// Request line and headers
|
||||
// RFC 9112 §9.6: a server that sends the "close" connection option must
|
||||
// close the connection after that response, whichever path wrote it (an
|
||||
// error status, a handler, or a rejected request). Reading on would also
|
||||
// parse whatever the client sent next on a connection it considers done.
|
||||
auto honor_connection_close = detail::scope_exit([&] {
|
||||
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
||||
connection_closed = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Request line and headers. A rejected message leaves the rest of it (and
|
||||
// any body) unread, so the connection cannot be reused: the leftover bytes
|
||||
// would be parsed as the next request.
|
||||
if (!parse_request_line(line_reader.ptr(), req)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
output_error_log(Error::InvalidRequestLine, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
@@ -14309,20 +14504,28 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
|
||||
// Request headers
|
||||
if (!detail::read_headers(strm, req.headers)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
output_error_log(Error::InvalidHeaders, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
||||
// otherwise let an intermediary and this parser disagree on where the body
|
||||
// ends and enable request smuggling. Two cases: a non-zero Content-Length
|
||||
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
||||
// compatibility with existing clients), and a Transfer-Encoding whose final
|
||||
// coding is not chunked, which leaves the body length undeterminable. The
|
||||
// latter must not fall through to the "no body" path, or the body bytes are
|
||||
// parsed as the next request on a persistent connection.
|
||||
if (detail::has_conflicting_content_length(req.headers) ||
|
||||
// RFC 9112 §6.3: Reject requests whose framing is invalid or ambiguous,
|
||||
// which would otherwise let an intermediary and this parser disagree on
|
||||
// where the body ends and enable request smuggling. Three cases: a
|
||||
// Content-Length that is not a valid decimal length (e.g. "42, 42", "+42"
|
||||
// or empty), which would otherwise be read as "no body"; a non-zero
|
||||
// Content-Length alongside any Transfer-Encoding (Content-Length: 0 is
|
||||
// tolerated for compatibility with existing clients); and a
|
||||
// Transfer-Encoding whose final coding is not chunked, which leaves the body
|
||||
// length undeterminable. None of them may fall through to the "no body"
|
||||
// path, or the body bytes are parsed as the next request on a persistent
|
||||
// connection.
|
||||
auto is_invalid_content_length = false;
|
||||
detail::get_header_value_u64(req.headers, "Content-Length", 0, 0,
|
||||
is_invalid_content_length);
|
||||
if (is_invalid_content_length ||
|
||||
detail::has_conflicting_content_length(req.headers) ||
|
||||
(req.has_header("Transfer-Encoding") &&
|
||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||
connection_closed = true;
|
||||
@@ -14595,17 +14798,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
// keep-alive. Without framing there is no body to drain — reading would
|
||||
// consume the next request (issue #2450). If the response has committed the
|
||||
// connection to close, there is no next request to protect.
|
||||
if (!req.body_consumed_ && detail::has_framed_body(req)) {
|
||||
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
||||
if (!req.body_consumed_ && detail::has_framed_body(req) &&
|
||||
!detail::has_header_token(res.headers, "Connection", "close")) {
|
||||
int dummy_status;
|
||||
if (!detail::read_content(
|
||||
strm, req, payload_max_length_, dummy_status, nullptr,
|
||||
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
||||
connection_closed = true;
|
||||
} else {
|
||||
int dummy_status;
|
||||
if (!detail::read_content(
|
||||
strm, req, payload_max_length_, dummy_status, nullptr,
|
||||
[](const char *, size_t, size_t, size_t) { return true; },
|
||||
false)) {
|
||||
connection_closed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15506,7 +15705,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
if (location.empty()) { return false; }
|
||||
|
||||
detail::UrlComponents uc;
|
||||
if (!detail::parse_url(location, uc)) { return false; }
|
||||
if (!detail::parse_url(detail::resolve_relative_location(location, req.path),
|
||||
uc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only follow http/https redirects
|
||||
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
||||
@@ -18393,6 +18595,9 @@ 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);
|
||||
}
|
||||
|
||||
@@ -18454,6 +18659,9 @@ 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;
|
||||
@@ -19423,6 +19631,24 @@ inline cert_t get_peer_cert(const_session_t session) {
|
||||
static_cast<SSL *>(const_cast<void *>(session))));
|
||||
}
|
||||
|
||||
inline size_t get_peer_certs(const_session_t session,
|
||||
std::vector<cert_t> &certs) {
|
||||
certs.clear();
|
||||
if (!session) { return 0; }
|
||||
auto ssl = static_cast<const SSL *>(session);
|
||||
// On the server side, the chain leaves out the peer's own certificate
|
||||
if (SSL_is_server(ssl)) {
|
||||
if (auto leaf = get_peer_cert(session)) { certs.push_back(leaf); }
|
||||
}
|
||||
auto sk = SSL_get_peer_cert_chain(ssl);
|
||||
for (int i = 0; sk && i < sk_X509_num(sk); i++) {
|
||||
auto x509 = sk_X509_value(sk, i);
|
||||
X509_up_ref(x509);
|
||||
certs.push_back(static_cast<cert_t>(x509));
|
||||
}
|
||||
return certs.size();
|
||||
}
|
||||
|
||||
inline void free_cert(cert_t cert) {
|
||||
if (cert) { X509_free(static_cast<X509 *>(cert)); }
|
||||
}
|
||||
@@ -20767,6 +20993,18 @@ inline cert_t get_peer_cert(const_session_t session) {
|
||||
return const_cast<mbedtls_x509_crt *>(cert);
|
||||
}
|
||||
|
||||
inline size_t get_peer_certs(const_session_t session,
|
||||
std::vector<cert_t> &certs) {
|
||||
certs.clear();
|
||||
// Mbed TLS parses the whole received chain into a list headed by the peer
|
||||
// certificate, owned by the session like get_peer_cert()'s result
|
||||
for (auto crt = static_cast<mbedtls_x509_crt *>(get_peer_cert(session));
|
||||
crt && crt->raw.len > 0; crt = crt->next) {
|
||||
certs.push_back(static_cast<cert_t>(crt));
|
||||
}
|
||||
return certs.size();
|
||||
}
|
||||
|
||||
inline void free_cert(cert_t cert) {
|
||||
// Mbed TLS: peer certificate is owned by the SSL context.
|
||||
// No-op here, but callers should still call this for cross-backend
|
||||
@@ -21924,6 +22162,24 @@ inline cert_t get_peer_cert(const_session_t session) {
|
||||
return static_cast<cert_t>(cert);
|
||||
}
|
||||
|
||||
inline size_t get_peer_certs(const_session_t session,
|
||||
std::vector<cert_t> &certs) {
|
||||
certs.clear();
|
||||
if (!session) { return 0; }
|
||||
// wolfSSL keeps the received chain only when built with SESSION_CERTS
|
||||
#ifdef SESSION_CERTS
|
||||
auto wsession =
|
||||
static_cast<impl::WolfSSLSession *>(const_cast<void *>(session));
|
||||
auto chain = wolfSSL_get_peer_chain(wsession->ssl);
|
||||
auto count = chain ? wolfSSL_get_chain_count(chain) : 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
auto x509 = wolfSSL_get_chain_X509(chain, i);
|
||||
if (x509) { certs.push_back(static_cast<cert_t>(x509)); }
|
||||
}
|
||||
#endif
|
||||
return certs.size();
|
||||
}
|
||||
|
||||
inline void free_cert(cert_t cert) {
|
||||
if (cert) { wolfSSL_X509_free(static_cast<WOLFSSL_X509 *>(cert)); }
|
||||
}
|
||||
|
||||
+805
-266
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user