mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-03 13:38:36 +00:00
Compare commits
14
Commits
v0.57.1
...
086a648364
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
086a648364 | ||
|
|
639391ad7f | ||
|
|
0715c2739e | ||
|
|
3330d0eb06 | ||
|
|
c1c2b1f4b4 | ||
|
|
e11dbec7b3 | ||
|
|
174bce5ccf | ||
|
|
57c4f7f385 | ||
|
|
2fb2dbbe1e | ||
|
|
8b6ab24159 | ||
|
|
5a202d3d5f | ||
|
|
4f3f9ef19b | ||
|
|
6d59d1e2df | ||
|
|
9386b25dd7 |
No files matched your search
+21
-3
@@ -15,7 +15,7 @@
|
|||||||
* HTTPLIB_REQUIRE_BROTLI (default off)
|
* HTTPLIB_REQUIRE_BROTLI (default off)
|
||||||
* HTTPLIB_REQUIRE_ZSTD (default off)
|
* HTTPLIB_REQUIRE_ZSTD (default off)
|
||||||
* HTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES (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_COMPILE (default off)
|
||||||
* HTTPLIB_INSTALL (default on)
|
* HTTPLIB_INSTALL (default on)
|
||||||
* HTTPLIB_SHARED (default off) builds as a shared library (if HTTPLIB_COMPILE is 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)
|
set(HTTPLIB_IS_USING_MACOSX_AUTOMATIC_ROOT_CERTIFICATES FALSE)
|
||||||
endif()
|
endif()
|
||||||
set(HTTPLIB_IS_USING_NON_BLOCKING_GETADDRINFO ${HTTPLIB_USE_NON_BLOCKING_GETADDRINFO})
|
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
|
# Threads needed for <thread> on some systems, and for <pthread.h> on Linux
|
||||||
set(THREADS_PREFER_PTHREAD_FLAG TRUE)
|
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
|
# 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>"
|
"$<$<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
|
# 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.
|
# 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::common>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_BROTLI}>:Brotli::encoder>
|
$<$<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_WOLFSSL}>:CPPHTTPLIB_WOLFSSL_SUPPORT>
|
||||||
$<$<BOOL:${HTTPLIB_IS_USING_MBEDTLS}>:CPPHTTPLIB_MBEDTLS_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>
|
$<$<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
|
# CMake configuration files installation directory
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@ sse.on_error([](httplib::Error err) { });
|
|||||||
#### Configuration
|
#### Configuration
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
// Set reconnect interval (default: 3000ms)
|
// Set reconnect interval (default: 3000ms, minimum: 100ms)
|
||||||
sse.set_reconnect_interval(5000);
|
sse.set_reconnect_interval(5000);
|
||||||
|
|
||||||
// Set max reconnect attempts (default: 0 = unlimited)
|
// Set max reconnect attempts (default: 0 = unlimited)
|
||||||
|
|||||||
@@ -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
|
#### Pre-compression Logging
|
||||||
|
|
||||||
You can also set a pre-compression logger to capture request/response data before compression is applied:
|
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();
|
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:
|
// NGINX Combined log format:
|
||||||
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
// $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent
|
||||||
// "$http_referer" "$http_user_agent"
|
// "$http_referer" "$http_user_agent"
|
||||||
@@ -55,7 +72,9 @@ void nginx_access_logger(const Request &req, const Response &res) {
|
|||||||
std::string remote_user =
|
std::string remote_user =
|
||||||
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
"-"; // cpp-httplib doesn't have built-in auth user tracking
|
||||||
auto time_local = get_time_format();
|
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 status = res.status;
|
||||||
auto body_bytes_sent = res.body.size();
|
auto body_bytes_sent = res.body.size();
|
||||||
auto http_referer = req.get_header_value("Referer");
|
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 = "-";
|
if (http_user_agent.empty()) http_user_agent = "-";
|
||||||
|
|
||||||
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
std::cout << std::format("{} - {} [{}] \"{}\" {} {} \"{}\" \"{}\"",
|
||||||
req.remote_addr, remote_user, time_local, request,
|
req.remote_addr, remote_user, time_local,
|
||||||
status, body_bytes_sent, http_referer,
|
escape_log(request), status, body_bytes_sent,
|
||||||
http_user_agent)
|
escape_log(http_referer), escape_log(http_user_agent))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,14 +98,15 @@ void nginx_error_logger(const Error &err, const Request *req) {
|
|||||||
|
|
||||||
if (req) {
|
if (req) {
|
||||||
auto request =
|
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");
|
auto host = req->get_header_value("Host");
|
||||||
if (host.empty()) host = "-";
|
if (host.empty()) host = "-";
|
||||||
|
|
||||||
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
std::cerr << std::format("{} [{}] {}, client: {}, request: "
|
||||||
"\"{}\", host: \"{}\"",
|
"\"{}\", host: \"{}\"",
|
||||||
time_local, level, to_string(err),
|
time_local, level, to_string(err),
|
||||||
req->remote_addr, request, host)
|
req->remote_addr, escape_log(request),
|
||||||
|
escape_log(host))
|
||||||
<< std::endl;
|
<< std::endl;
|
||||||
} else {
|
} else {
|
||||||
// If no request context, just log the error
|
// If no request context, just log the error
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.57.1"
|
version = "0.58.0"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.57.1"
|
#define CPPHTTPLIB_VERSION "0.58.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x003901"
|
#define CPPHTTPLIB_VERSION_NUM "0x003a00"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -939,6 +939,48 @@ inline bool parse_url(const std::string &url, UrlComponents &uc) {
|
|||||||
return true;
|
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
|
} // namespace detail
|
||||||
|
|
||||||
enum class SSLVerifierResponse {
|
enum class SSLVerifierResponse {
|
||||||
@@ -1842,6 +1884,7 @@ struct Response {
|
|||||||
ContentProvider content_provider_;
|
ContentProvider content_provider_;
|
||||||
ContentProviderResourceReleaser content_provider_resource_releaser_;
|
ContentProviderResourceReleaser content_provider_resource_releaser_;
|
||||||
bool is_chunked_content_provider_ = false;
|
bool is_chunked_content_provider_ = false;
|
||||||
|
bool is_file_content_provider_ = false;
|
||||||
bool content_provider_success_ = false;
|
bool content_provider_success_ = false;
|
||||||
std::string file_content_path_;
|
std::string file_content_path_;
|
||||||
std::string file_content_content_type_;
|
std::string file_content_content_type_;
|
||||||
@@ -2986,7 +3029,7 @@ private:
|
|||||||
bool read_response_line(Stream &strm, const Request &req, Response &res,
|
bool read_response_line(Stream &strm, const Request &req, Response &res,
|
||||||
bool skip_100_continue = true) const;
|
bool skip_100_continue = true) const;
|
||||||
bool write_request(Stream &strm, Request &req, bool close_connection,
|
bool write_request(Stream &strm, Request &req, bool close_connection,
|
||||||
Error &error, bool skip_body = false);
|
Error &error, bool skip_body, bool &rejected_locally);
|
||||||
bool write_request_body(Stream &strm, Request &req, Error &error);
|
bool write_request_body(Stream &strm, Request &req, Error &error);
|
||||||
void prepare_default_headers(Request &r, bool for_stream,
|
void prepare_default_headers(Request &r, bool for_stream,
|
||||||
const std::string &ct);
|
const std::string &ct);
|
||||||
@@ -3914,6 +3957,7 @@ bool is_field_vchar(char c);
|
|||||||
bool is_field_content(const std::string &s);
|
bool is_field_content(const std::string &s);
|
||||||
bool is_field_value(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_field_valid(const std::string &name, const std::string &value);
|
||||||
|
bool is_request_target(const std::string &s);
|
||||||
|
|
||||||
} // namespace fields
|
} // namespace fields
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
@@ -4870,7 +4914,8 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
|||||||
msg.id = value;
|
msg.id = value;
|
||||||
} else if (field == "retry") {
|
} else if (field == "retry") {
|
||||||
// Parse retry interval in milliseconds
|
// 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;
|
int v = 0;
|
||||||
auto res =
|
auto res =
|
||||||
detail::from_chars(value.data(), value.data() + value.size(), v);
|
detail::from_chars(value.data(), value.data() + value.size(), v);
|
||||||
@@ -5006,11 +5051,15 @@ inline bool SSEClient::should_reconnect(int count) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void SSEClient::wait_for_reconnect() {
|
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;
|
auto waited = 0;
|
||||||
while (running_.load() && waited < reconnect_interval_ms_) {
|
while (running_.load() && waited < interval_ms) {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(step_ms));
|
||||||
waited += 100;
|
waited += step_ms;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5778,15 +5827,15 @@ inline std::string encode_path(const std::string &s) {
|
|||||||
switch (s[i]) {
|
switch (s[i]) {
|
||||||
case ' ': result += "%20"; break;
|
case ' ': result += "%20"; break;
|
||||||
case '+': result += "%2B"; break;
|
case '+': result += "%2B"; break;
|
||||||
case '\r': result += "%0D"; break;
|
|
||||||
case '\n': result += "%0A"; break;
|
|
||||||
case '\'': result += "%27"; break;
|
case '\'': result += "%27"; break;
|
||||||
case ',': result += "%2C"; break;
|
case ',': result += "%2C"; break;
|
||||||
// case ':': result += "%3A"; break; // ok? probably...
|
// case ':': result += "%3A"; break; // ok? probably...
|
||||||
case ';': result += "%3B"; break;
|
case ';': result += "%3B"; break;
|
||||||
default:
|
default:
|
||||||
auto c = static_cast<uint8_t>(s[i]);
|
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 += '%';
|
result += '%';
|
||||||
char hex[4];
|
char hex[4];
|
||||||
auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
|
auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c);
|
||||||
@@ -7580,7 +7629,8 @@ inline bool parse_quality(const char *b, const char *e, std::string &token,
|
|||||||
|
|
||||||
double v = 0.0;
|
double v = 0.0;
|
||||||
auto res = from_chars(pb + r.first, pb + r.second, v);
|
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;
|
invalid = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -8517,14 +8567,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,
|
inline ssize_t write_request_line(Stream &strm, const std::string &method,
|
||||||
const std::string &path) {
|
const std::string &path) {
|
||||||
// Neither the method nor the request target may carry CR/LF (or other
|
// 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
|
// 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
|
// line and injects headers or a whole request.
|
||||||
// (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.
|
|
||||||
if (!fields::is_token(method)) { return -1; }
|
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;
|
std::string s = method;
|
||||||
s += ' ';
|
s += ' ';
|
||||||
@@ -8746,9 +8793,9 @@ inline bool compress_content_provider(const ContentProvider &content_provider,
|
|||||||
return cmp.compress(nullptr, 0, true, append);
|
return cmp.compress(nullptr, 0, true, append);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serves `m` as the response body. `set_content_provider()` clears the coding,
|
// 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
|
// and the file flag, so recording them has to come after; keeping all of it
|
||||||
// file-serving path cannot get that order wrong.
|
// here means a third file-serving path cannot get that order wrong.
|
||||||
inline void set_file_content_provider(Response &res,
|
inline void set_file_content_provider(Response &res,
|
||||||
const std::shared_ptr<mmap> &m,
|
const std::shared_ptr<mmap> &m,
|
||||||
const std::string &content_type,
|
const std::string &content_type,
|
||||||
@@ -8760,6 +8807,7 @@ inline void set_file_content_provider(Response &res,
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
res.is_file_content_provider_ = true;
|
||||||
res.content_coding_ = encoding;
|
res.content_coding_ = encoding;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10177,6 +10225,12 @@ inline bool is_field_valid(const std::string &name, const std::string &value) {
|
|||||||
return is_field_name(name) && is_field_value(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
|
} // namespace fields
|
||||||
|
|
||||||
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||||
@@ -10644,10 +10698,9 @@ inline bool match_hostname(const std::string &pattern,
|
|||||||
// Verify certificate using Windows CertGetCertificateChain API.
|
// Verify certificate using Windows CertGetCertificateChain API.
|
||||||
// This provides real-time certificate validation with Windows Update
|
// This provides real-time certificate validation with Windows Update
|
||||||
// integration, independent of the TLS backend (OpenSSL or MbedTLS).
|
// integration, independent of the TLS backend (OpenSSL or MbedTLS).
|
||||||
inline bool
|
inline bool verify_cert_with_windows_schannel(
|
||||||
verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
const std::vector<unsigned char> &der_cert, const std::string &hostname,
|
||||||
const std::string &hostname,
|
bool verify_hostname, uint64_t &out_error, tls::const_session_t session) {
|
||||||
bool verify_hostname, uint64_t &out_error) {
|
|
||||||
if (der_cert.empty()) { return false; }
|
if (der_cert.empty()) { return false; }
|
||||||
|
|
||||||
out_error = 0;
|
out_error = 0;
|
||||||
@@ -10665,6 +10718,24 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
|||||||
auto cert_guard =
|
auto cert_guard =
|
||||||
scope_exit([&] { CertFreeCertificateContext(cert_context); });
|
scope_exit([&] { CertFreeCertificateContext(cert_context); });
|
||||||
|
|
||||||
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||||
|
// Add the intermediates the server sent. Without them CryptoAPI follows the
|
||||||
|
// leaf's AIA URL, which may lead to an issuer under an untrusted root.
|
||||||
|
auto store = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, 0, 0, nullptr);
|
||||||
|
auto store_guard = scope_exit([&] { CertCloseStore(store, 0); });
|
||||||
|
auto sk = SSL_get_peer_cert_chain(static_cast<const SSL *>(session));
|
||||||
|
for (int i = 1; sk && i < sk_X509_num(sk); i++) {
|
||||||
|
std::vector<unsigned char> der;
|
||||||
|
tls::get_cert_der(sk_X509_value(sk, i), der);
|
||||||
|
CertAddEncodedCertificateToStore(store, X509_ASN_ENCODING, der.data(),
|
||||||
|
static_cast<DWORD>(der.size()),
|
||||||
|
CERT_STORE_ADD_USE_EXISTING, nullptr);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
(void)session;
|
||||||
|
auto store = cert_context->hCertStore;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Setup chain parameters
|
// Setup chain parameters
|
||||||
CERT_CHAIN_PARA chain_para = {};
|
CERT_CHAIN_PARA chain_para = {};
|
||||||
chain_para.cbSize = sizeof(chain_para);
|
chain_para.cbSize = sizeof(chain_para);
|
||||||
@@ -10672,7 +10743,7 @@ verify_cert_with_windows_schannel(const std::vector<unsigned char> &der_cert,
|
|||||||
// Build certificate chain with revocation checking
|
// Build certificate chain with revocation checking
|
||||||
PCCERT_CHAIN_CONTEXT chain_context = nullptr;
|
PCCERT_CHAIN_CONTEXT chain_context = nullptr;
|
||||||
auto chain_result = CertGetCertificateChain(
|
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_CACHE_END_CERT | CERT_CHAIN_REVOCATION_CHECK_END_CERT |
|
||||||
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
|
CERT_CHAIN_REVOCATION_ACCUMULATIVE_TIMEOUT,
|
||||||
nullptr, &chain_context);
|
nullptr, &chain_context);
|
||||||
@@ -10895,8 +10966,8 @@ inline bool setup_client_tls_session(
|
|||||||
if (get_cert_der(server_cert, der)) {
|
if (get_cert_der(server_cert, der)) {
|
||||||
uint64_t wincrypt_error = 0;
|
uint64_t wincrypt_error = 0;
|
||||||
if (!verify_cert_with_windows_schannel(
|
if (!verify_cert_with_windows_schannel(
|
||||||
der, host, options.server_hostname_verification,
|
der, host, options.server_hostname_verification, wincrypt_error,
|
||||||
wincrypt_error)) {
|
session)) {
|
||||||
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
return fail(Error::SSLServerVerification, 0, wincrypt_error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11660,6 +11731,7 @@ inline void Response::set_content_provider(
|
|||||||
if (in_length > 0) { content_provider_ = std::move(provider); }
|
if (in_length > 0) { content_provider_ = std::move(provider); }
|
||||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||||
is_chunked_content_provider_ = false;
|
is_chunked_content_provider_ = false;
|
||||||
|
is_file_content_provider_ = false;
|
||||||
content_coding_ = detail::EncodingType::None;
|
content_coding_ = detail::EncodingType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11671,6 +11743,7 @@ inline void Response::set_content_provider(
|
|||||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||||
is_chunked_content_provider_ = false;
|
is_chunked_content_provider_ = false;
|
||||||
|
is_file_content_provider_ = false;
|
||||||
content_coding_ = detail::EncodingType::None;
|
content_coding_ = detail::EncodingType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -11682,6 +11755,7 @@ inline void Response::set_chunked_content_provider(
|
|||||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||||
is_chunked_content_provider_ = true;
|
is_chunked_content_provider_ = true;
|
||||||
|
is_file_content_provider_ = false;
|
||||||
content_coding_ = detail::EncodingType::None;
|
content_coding_ = detail::EncodingType::None;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13248,6 +13322,8 @@ inline bool Server::parse_request_line(const char *s, Request &req) const {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!detail::fields::is_request_target(req.target)) { return false; }
|
||||||
|
|
||||||
{
|
{
|
||||||
// Skip URL fragment
|
// Skip URL fragment
|
||||||
for (size_t i = 0; i < req.target.size(); i++) {
|
for (size_t i = 0; i < req.target.size(); i++) {
|
||||||
@@ -13335,9 +13411,27 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
if (!detail::write_response_line(bstrm, res.status)) { return false; }
|
||||||
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
|
if (header_writer_(bstrm, res.headers) <= 0) { return false; }
|
||||||
|
|
||||||
// Combine small body with headers to reduce write syscalls
|
// Combine a small body with the headers so the whole response leaves in a
|
||||||
if (req.method != "HEAD" && !res.body.empty() && !res.content_provider_) {
|
// single write. A large body is written on its own instead: a copy of it
|
||||||
bstrm.write(res.body.data(), res.body.size());
|
// 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
|
// Log before writing to avoid race condition with client-side code that
|
||||||
@@ -13348,17 +13442,20 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
auto &data = bstrm.get_buffer();
|
auto &data = bstrm.get_buffer();
|
||||||
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
|
if (!detail::write_data(strm, data.data(), data.size())) { return false; }
|
||||||
|
|
||||||
// Streaming body
|
if (body_is_separate) {
|
||||||
auto ret = true;
|
return detail::write_data(strm, res.body.data(), res.body.size());
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
inline bool
|
||||||
@@ -14300,8 +14397,21 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
res.version = "HTTP/1.1";
|
res.version = "HTTP/1.1";
|
||||||
res.headers = default_headers_;
|
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)) {
|
if (!parse_request_line(line_reader.ptr(), req)) {
|
||||||
|
connection_closed = true;
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
output_error_log(Error::InvalidRequestLine, &req);
|
output_error_log(Error::InvalidRequestLine, &req);
|
||||||
return write_response(strm, close_connection, req, res);
|
return write_response(strm, close_connection, req, res);
|
||||||
@@ -14309,20 +14419,28 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
|
|
||||||
// Request headers
|
// Request headers
|
||||||
if (!detail::read_headers(strm, req.headers)) {
|
if (!detail::read_headers(strm, req.headers)) {
|
||||||
|
connection_closed = true;
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
output_error_log(Error::InvalidHeaders, &req);
|
output_error_log(Error::InvalidHeaders, &req);
|
||||||
return write_response(strm, close_connection, req, res);
|
return write_response(strm, close_connection, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
// RFC 9112 §6.3: Reject requests whose framing is invalid or ambiguous,
|
||||||
// otherwise let an intermediary and this parser disagree on where the body
|
// which would otherwise let an intermediary and this parser disagree on
|
||||||
// ends and enable request smuggling. Two cases: a non-zero Content-Length
|
// where the body ends and enable request smuggling. Three cases: a
|
||||||
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
// Content-Length that is not a valid decimal length (e.g. "42, 42", "+42"
|
||||||
// compatibility with existing clients), and a Transfer-Encoding whose final
|
// or empty), which would otherwise be read as "no body"; a non-zero
|
||||||
// coding is not chunked, which leaves the body length undeterminable. The
|
// Content-Length alongside any Transfer-Encoding (Content-Length: 0 is
|
||||||
// latter must not fall through to the "no body" path, or the body bytes are
|
// tolerated for compatibility with existing clients); and a
|
||||||
// parsed as the next request on a persistent connection.
|
// Transfer-Encoding whose final coding is not chunked, which leaves the body
|
||||||
if (detail::has_conflicting_content_length(req.headers) ||
|
// 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") &&
|
(req.has_header("Transfer-Encoding") &&
|
||||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||||
connection_closed = true;
|
connection_closed = true;
|
||||||
@@ -14595,17 +14713,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
// keep-alive. Without framing there is no body to drain — reading would
|
// keep-alive. Without framing there is no body to drain — reading would
|
||||||
// consume the next request (issue #2450). If the response has committed the
|
// consume the next request (issue #2450). If the response has committed the
|
||||||
// connection to close, there is no next request to protect.
|
// connection to close, there is no next request to protect.
|
||||||
if (!req.body_consumed_ && detail::has_framed_body(req)) {
|
if (!req.body_consumed_ && detail::has_framed_body(req) &&
|
||||||
if (detail::has_header_token(res.headers, "Connection", "close")) {
|
!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;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15119,16 +15233,30 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
|||||||
prepare_default_headers(req, true, content_type);
|
prepare_default_headers(req, true, content_type);
|
||||||
|
|
||||||
auto &strm = *handle.stream_;
|
auto &strm = *handle.stream_;
|
||||||
if (detail::write_request_line(strm, req.method, req.path) < 0) {
|
|
||||||
handle.error = Error::Write;
|
|
||||||
handle.response.reset();
|
|
||||||
return handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!detail::check_and_write_headers(strm, req.headers, header_writer_,
|
// Build the request line and headers in memory first, like write_request()
|
||||||
handle.error)) {
|
// does, so that a rejected header leaves nothing on the wire.
|
||||||
handle.response.reset();
|
{
|
||||||
return handle;
|
detail::BufferStream bstrm;
|
||||||
|
|
||||||
|
if (detail::write_request_line(bstrm, req.method, req.path) < 0) {
|
||||||
|
handle.error = Error::Write;
|
||||||
|
handle.response.reset();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||||
|
handle.error)) {
|
||||||
|
handle.response.reset();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto &data = bstrm.get_buffer();
|
||||||
|
if (!detail::write_data(strm, data.data(), data.size())) {
|
||||||
|
handle.error = Error::Write;
|
||||||
|
handle.response.reset();
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.empty()) {
|
if (!body.empty()) {
|
||||||
@@ -15492,7 +15620,10 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
|||||||
if (location.empty()) { return false; }
|
if (location.empty()) { return false; }
|
||||||
|
|
||||||
detail::UrlComponents uc;
|
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
|
// Only follow http/https redirects
|
||||||
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
if (!uc.scheme.empty() && uc.scheme != "http" && uc.scheme != "https") {
|
||||||
@@ -15681,7 +15812,9 @@ inline bool ClientImpl::write_content_with_provider(Stream &strm,
|
|||||||
|
|
||||||
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||||
bool close_connection, Error &error,
|
bool close_connection, Error &error,
|
||||||
bool skip_body) {
|
bool skip_body, bool &rejected_locally) {
|
||||||
|
rejected_locally = false;
|
||||||
|
|
||||||
// Prepare additional headers
|
// Prepare additional headers
|
||||||
if (close_connection) {
|
if (close_connection) {
|
||||||
if (!req.has_header("Connection")) {
|
if (!req.has_header("Connection")) {
|
||||||
@@ -15775,11 +15908,13 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
// set_path_encode(false)) must fail the request cleanly instead of
|
// set_path_encode(false)) must fail the request cleanly instead of
|
||||||
// emitting a request-line-less, header-injecting request.
|
// emitting a request-line-less, header-injecting request.
|
||||||
error = Error::Write;
|
error = Error::Write;
|
||||||
|
rejected_locally = true;
|
||||||
output_error_log(error, &req);
|
output_error_log(error, &req);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
if (!detail::check_and_write_headers(bstrm, req.headers, header_writer_,
|
||||||
error)) {
|
error)) {
|
||||||
|
rejected_locally = true;
|
||||||
output_error_log(error, &req);
|
output_error_log(error, &req);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -16048,8 +16183,16 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
|||||||
detail::has_header_token(req.headers, "Expect", "100-continue");
|
detail::has_header_token(req.headers, "Expect", "100-continue");
|
||||||
|
|
||||||
// Send request (skip body if using Expect: 100-continue)
|
// Send request (skip body if using Expect: 100-continue)
|
||||||
|
auto rejected_locally = false;
|
||||||
auto write_request_success =
|
auto write_request_success =
|
||||||
write_request(strm, req, close_connection, error, expect_100_continue);
|
write_request(strm, req, close_connection, error, expect_100_continue,
|
||||||
|
rejected_locally);
|
||||||
|
|
||||||
|
// A failed write normally still reads the response below, since the server
|
||||||
|
// may have answered early (e.g. 413/414) and closed while the body was being
|
||||||
|
// sent. A request rejected before any byte reached the socket gets no such
|
||||||
|
// response, and waiting for one would block until the read timeout.
|
||||||
|
if (rejected_locally) { return false; }
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
if (is_ssl() && !expect_100_continue) {
|
if (is_ssl() && !expect_100_continue) {
|
||||||
|
|||||||
+436
-44
@@ -1059,6 +1059,10 @@ TEST(ParseAcceptHeaderTest, InvalidCases) {
|
|||||||
EXPECT_FALSE(detail::parse_accept_header(
|
EXPECT_FALSE(detail::parse_accept_header(
|
||||||
"text/html;q=invalid,application/json", result));
|
"text/html;q=invalid,application/json", result));
|
||||||
|
|
||||||
|
// A valid numeric prefix does not make the entire quality value valid.
|
||||||
|
EXPECT_FALSE(detail::parse_accept_header(
|
||||||
|
"text/html;q=0.5junk,application/json", result));
|
||||||
|
|
||||||
// Empty quality value
|
// Empty quality value
|
||||||
EXPECT_FALSE(
|
EXPECT_FALSE(
|
||||||
detail::parse_accept_header("text/html;q=,application/json", result));
|
detail::parse_accept_header("text/html;q=,application/json", result));
|
||||||
@@ -2112,6 +2116,16 @@ TEST(ParseAcceptEncoding5, AcceptEncodingQZeroVariants) {
|
|||||||
EXPECT_TRUE(ret == detail::EncodingType::None);
|
EXPECT_TRUE(ret == detail::EncodingType::None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ParseAcceptEncodingTest, RejectsTrailingQualityCharacters) {
|
||||||
|
Request req;
|
||||||
|
req.set_header("Accept-Encoding", "gzip;q=0.5junk");
|
||||||
|
|
||||||
|
Response res;
|
||||||
|
res.set_header("Content-Type", "text/plain");
|
||||||
|
|
||||||
|
EXPECT_EQ(detail::EncodingType::None, detail::encoding_type(req, res));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ParseAcceptEncoding6, AcceptEncodingXGzipQZero) {
|
TEST(ParseAcceptEncoding6, AcceptEncodingXGzipQZero) {
|
||||||
// x-gzip;q=0 should not cause "gzip" to be incorrectly detected
|
// x-gzip;q=0 should not cause "gzip" to be incorrectly detected
|
||||||
Request req;
|
Request req;
|
||||||
@@ -4046,6 +4060,37 @@ TEST(PathUrlEncodeTest, StreamingCRLFInTargetIsEncoded) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(PathUrlEncodeTest, ControlCharsInPathAreEncoded) {
|
||||||
|
// Every control character is percent-encoded, not just CR/LF, so the target
|
||||||
|
// passes the request-target check in write_request_line.
|
||||||
|
Server svr;
|
||||||
|
|
||||||
|
std::string target;
|
||||||
|
svr.set_pre_routing_handler([&](const Request &req, Response &res) {
|
||||||
|
target = req.target;
|
||||||
|
res.status = StatusCode::OK_200;
|
||||||
|
return Server::HandlerResponse::Handled;
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto thread = std::thread([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
|
||||||
|
auto res = cli.Get("/a\tb\x1b\x7f");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ("/a%09b%1B%7F", target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(PathUrlEncodeTest, StreamingCRLFRejectedWhenPathEncodeDisabled) {
|
TEST(PathUrlEncodeTest, StreamingCRLFRejectedWhenPathEncodeDisabled) {
|
||||||
// Nothing may reach the wire: a raw CR/LF target would split the request
|
// Nothing may reach the wire: a raw CR/LF target would split the request
|
||||||
// line and inject headers.
|
// line and inject headers.
|
||||||
@@ -10078,7 +10123,7 @@ ssize_t write_request_line(Stream &strm, const std::string &method,
|
|||||||
} // namespace detail
|
} // namespace detail
|
||||||
} // namespace httplib
|
} // namespace httplib
|
||||||
|
|
||||||
TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
|
TEST(RequestLineInjectionTest, RejectsInvalidCharsInTarget) {
|
||||||
// A well-formed target is written verbatim.
|
// A well-formed target is written verbatim.
|
||||||
{
|
{
|
||||||
detail::BufferStream strm;
|
detail::BufferStream strm;
|
||||||
@@ -10087,15 +10132,18 @@ TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
|
|||||||
EXPECT_EQ("GET /path?a=b HTTP/1.1\r\n", strm.get_buffer());
|
EXPECT_EQ("GET /path?a=b HTTP/1.1\r\n", strm.get_buffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
// A target carrying CR/LF must be rejected before anything reaches the wire,
|
// A target carrying CR/LF, SP or other control octets must be rejected
|
||||||
// otherwise it splits the request line and injects a header or a whole
|
// before anything reaches the wire, otherwise it splits the request line and
|
||||||
// request. This is what a decoded redirect Location ("%0D%0A") turns into
|
// injects a header or a whole request. This is what a decoded redirect
|
||||||
// when path encoding is disabled.
|
// Location ("%0D%0A") turns into when path encoding is disabled.
|
||||||
const std::string evil_targets[] = {
|
const std::string evil_targets[] = {
|
||||||
"/a\r\nInjected: pwned",
|
"/a\r\nInjected: pwned",
|
||||||
"/a\rInjected",
|
"/a\rInjected",
|
||||||
"/a\nInjected",
|
"/a\nInjected",
|
||||||
"/a\r\n\r\nGET /evil HTTP/1.1\r\nHost: victim\r\n\r\n",
|
"/a\r\n\r\nGET /evil HTTP/1.1\r\nHost: victim\r\n\r\n",
|
||||||
|
"/a b",
|
||||||
|
"/a\tb",
|
||||||
|
"/a\x7f",
|
||||||
};
|
};
|
||||||
for (const auto &evil : evil_targets) {
|
for (const auto &evil : evil_targets) {
|
||||||
detail::BufferStream strm;
|
detail::BufferStream strm;
|
||||||
@@ -10106,11 +10154,11 @@ TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(RequestLineInjectionTest, ClientRejectsCRLFTargetEndToEnd) {
|
TEST(RequestLineInjectionTest, ClientRejectsCRLFTargetEndToEnd) {
|
||||||
// End-to-end counterpart to RejectsCRLFInTarget. With path encoding disabled
|
// End-to-end counterpart to RejectsInvalidCharsInTarget. With path encoding
|
||||||
// the client transmits the target verbatim, so a CR/LF-bearing target -- what
|
// disabled the client transmits the target verbatim, so a CR/LF-bearing
|
||||||
// a redirect Location "%0D%0A" decodes to -- reaches write_request. The
|
// target -- what a redirect Location "%0D%0A" decodes to -- reaches
|
||||||
// client must fail cleanly with Error::Write instead of putting a
|
// write_request. The client must fail cleanly with Error::Write instead of
|
||||||
// request-line-less, header-injecting request on the wire.
|
// putting a request-line-less, header-injecting request on the wire.
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
svr.Get("/a", [](const Request &, Response &res) {
|
svr.Get("/a", [](const Request &, Response &res) {
|
||||||
@@ -10196,9 +10244,6 @@ TEST(RequestLineInjectionTest, ClientRejectsNonTokenMethodEndToEnd) {
|
|||||||
|
|
||||||
{
|
{
|
||||||
Client cli(HOST, port);
|
Client cli(HOST, port);
|
||||||
// Nothing is written, so shorten the read timeout the connection would
|
|
||||||
// otherwise sit in.
|
|
||||||
cli.set_read_timeout(1, 0);
|
|
||||||
|
|
||||||
const std::string evil_methods[] = {
|
const std::string evil_methods[] = {
|
||||||
"GET /smuggled HTTP/1.1\r\nHost: x\r\n\r\nGET",
|
"GET /smuggled HTTP/1.1\r\nHost: x\r\n\r\nGET",
|
||||||
@@ -10379,6 +10424,22 @@ TEST(ServerRequestParsingTest, InvalidSpaceInURL) {
|
|||||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(ServerRequestParsingTest, InvalidControlCharInURL) {
|
||||||
|
for (auto target : {"/h\ri", "/h\x7fi"}) {
|
||||||
|
std::string out;
|
||||||
|
test_raw_request(std::string("GET ") + target + " HTTP/1.1\r\n\r\n", &out);
|
||||||
|
EXPECT_EQ("HTTP/1.1 400 Bad Request", out.substr(0, 24)) << target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ServerRequestParsingTest, NonAsciiInURLAccepted) {
|
||||||
|
std::string out;
|
||||||
|
test_raw_request("GET /hi?q=\xE3\x81\x82 HTTP/1.1\r\n"
|
||||||
|
"Connection: close\r\n\r\n",
|
||||||
|
&out);
|
||||||
|
EXPECT_EQ("HTTP/1.1 200 OK", out.substr(0, 15));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ServerRequestParsingTest, RemoteAddrSetOnBadRequest) {
|
TEST(ServerRequestParsingTest, RemoteAddrSetOnBadRequest) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
@@ -13897,6 +13958,15 @@ TEST(SSLClientTest, WindowsCertificateVerification_Disabled) {
|
|||||||
auto res = cli.Get("/");
|
auto res = cli.Get("/");
|
||||||
if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); }
|
if (res) { EXPECT_NE(StatusCode::InternalServerError_500, res->status); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The server sends an intermediate cross-signed by a root Windows trusts,
|
||||||
|
// while the leaf's AIA URL leads to one under a root Windows does not trust.
|
||||||
|
TEST(SSLClientTest, WindowsCertificateVerification_ServerIntermediates_Online) {
|
||||||
|
SSLClient cli("accounts.spotify.com", 443);
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error())
|
||||||
|
<< " ssl_backend_error=" << res.ssl_backend_error();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
TEST(SSLClientTest, ServerCertificateVerification1_Online) {
|
TEST(SSLClientTest, ServerCertificateVerification1_Online) {
|
||||||
@@ -17284,6 +17354,93 @@ TEST(RedirectTest, RedirectWithPlusInPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RedirectTest, ResolveRelativeLocation) {
|
||||||
|
// Examples from RFC 3986 section 5.4, base "http://a/b/c/d;p?q".
|
||||||
|
const std::vector<std::pair<std::string, std::string>> cases = {
|
||||||
|
{"g", "/b/c/g"},
|
||||||
|
{"./g", "/b/c/g"},
|
||||||
|
{"g/", "/b/c/g/"},
|
||||||
|
{"?y", "/b/c/d;p?y"},
|
||||||
|
{"g?y", "/b/c/g?y"},
|
||||||
|
{"#s", "/b/c/d;p?q#s"},
|
||||||
|
{"g#s", "/b/c/g#s"},
|
||||||
|
{"g?y#s", "/b/c/g?y#s"},
|
||||||
|
{";x", "/b/c/;x"},
|
||||||
|
{"g;x", "/b/c/g;x"},
|
||||||
|
{".", "/b/c/"},
|
||||||
|
{"./", "/b/c/"},
|
||||||
|
{"..", "/b/"},
|
||||||
|
{"../", "/b/"},
|
||||||
|
{"../g", "/b/g"},
|
||||||
|
{"../..", "/"},
|
||||||
|
{"../../", "/"},
|
||||||
|
{"../../g", "/g"},
|
||||||
|
{"../../../g", "/g"},
|
||||||
|
{"g.", "/b/c/g."},
|
||||||
|
{".g", "/b/c/.g"},
|
||||||
|
{"g..", "/b/c/g.."},
|
||||||
|
{"..g", "/b/c/..g"},
|
||||||
|
{"./../g", "/b/g"},
|
||||||
|
{"./g/.", "/b/c/g/"},
|
||||||
|
{"g/./h", "/b/c/g/h"},
|
||||||
|
{"g/../h", "/b/c/h"},
|
||||||
|
{"g;x=1/./y", "/b/c/g;x=1/y"},
|
||||||
|
{"g;x=1/../y", "/b/c/y"},
|
||||||
|
{"g?y/./x", "/b/c/g?y/./x"},
|
||||||
|
{"g#s/../x", "/b/c/g#s/../x"},
|
||||||
|
// Not relative-path references, so left for parse_url.
|
||||||
|
{"/g", "/g"},
|
||||||
|
{"//g", "//g"},
|
||||||
|
{"http://g/x", "http://g/x"},
|
||||||
|
{"g:h", "g:h"},
|
||||||
|
};
|
||||||
|
for (const auto &c : cases) {
|
||||||
|
EXPECT_EQ(c.second,
|
||||||
|
detail::resolve_relative_location(c.first, "/b/c/d;p?q"))
|
||||||
|
<< c.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
|
||||||
|
Server svr;
|
||||||
|
|
||||||
|
svr.Get("/dir/page", [](const Request &req, Response &res) {
|
||||||
|
res.set_redirect(req.get_param_value("to"));
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/dir/next", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(req.target, "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Get("/other", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(req.target, "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto thread = std::thread([&]() { svr.listen(HOST, PORT); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
const std::vector<std::pair<std::string, std::string>> cases = {
|
||||||
|
{"next", "/dir/next"},
|
||||||
|
{"./next?x=1", "/dir/next?x=1"},
|
||||||
|
{"../other", "/other"},
|
||||||
|
};
|
||||||
|
for (const auto &c : cases) {
|
||||||
|
Client cli(HOST, PORT);
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
|
||||||
|
auto res = cli.Get("/dir/page?to=" + encode_query_component(c.first));
|
||||||
|
ASSERT_TRUE(res) << c.first << ": " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status) << c.first;
|
||||||
|
EXPECT_EQ(c.second, res->body) << c.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(RedirectTest, Issue2185_Online) {
|
TEST(RedirectTest, Issue2185_Online) {
|
||||||
SSLClient client("github.com");
|
SSLClient client("github.com");
|
||||||
@@ -17415,6 +17572,96 @@ TEST(VulnerabilityTest, CRLFInjectionInHeaders) {
|
|||||||
server_thread.join();
|
server_thread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A request rejected before any byte reaches the socket must fail right away
|
||||||
|
// instead of waiting for a response the server will never send.
|
||||||
|
TEST(ClientRejectedRequestTest, DoesNotWaitForResponse) {
|
||||||
|
// The kernel completes the TCP handshake from the listen backlog, so the
|
||||||
|
// client connects, but nothing ever reads, responds or closes.
|
||||||
|
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 + 1));
|
||||||
|
::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, 8));
|
||||||
|
|
||||||
|
auto cli = Client("127.0.0.1", PORT + 1);
|
||||||
|
cli.set_read_timeout(10, 0);
|
||||||
|
|
||||||
|
auto elapsed_ms = [](std::chrono::steady_clock::time_point start) {
|
||||||
|
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now() - start)
|
||||||
|
.count();
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
Request req;
|
||||||
|
req.method = "GE T";
|
||||||
|
req.path = "/";
|
||||||
|
auto start = std::chrono::steady_clock::now();
|
||||||
|
auto res = cli.send(req);
|
||||||
|
EXPECT_FALSE(res);
|
||||||
|
EXPECT_EQ(Error::Write, res.error());
|
||||||
|
EXPECT_LT(elapsed_ms(start), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto start = std::chrono::steady_clock::now();
|
||||||
|
auto res = cli.Get("/", Headers{{"A", "B\r\nEvil: 1"}});
|
||||||
|
EXPECT_FALSE(res);
|
||||||
|
EXPECT_EQ(Error::InvalidHeaders, res.error());
|
||||||
|
EXPECT_LT(elapsed_ms(start), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_FALSE(cli.is_socket_open());
|
||||||
|
|
||||||
|
detail::close_socket(srv);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ClientRejectedRequestTest, OpenStreamSendsNothingOnInvalidHeader) {
|
||||||
|
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 + 1));
|
||||||
|
::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, 1));
|
||||||
|
|
||||||
|
std::string received;
|
||||||
|
auto server_thread = std::thread([&] {
|
||||||
|
auto sock = ::accept(srv, nullptr, nullptr);
|
||||||
|
if (sock == INVALID_SOCKET) { return; }
|
||||||
|
detail::set_socket_opt_time(sock, SOL_SOCKET, SO_RCVTIMEO, 2, 0);
|
||||||
|
|
||||||
|
char buf[2048];
|
||||||
|
ssize_t n;
|
||||||
|
while ((n = ::recv(sock, buf, sizeof(buf), 0)) > 0) {
|
||||||
|
received.append(buf, static_cast<size_t>(n));
|
||||||
|
}
|
||||||
|
detail::close_socket(sock);
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
auto cli = Client("127.0.0.1", PORT + 1);
|
||||||
|
|
||||||
|
// "Z" sorts after the default headers, so writing straight to the socket
|
||||||
|
// would have sent the request line and those headers before the rejection.
|
||||||
|
auto handle =
|
||||||
|
cli.open_stream("GET", "/", Params{}, Headers{{"Z", "B\r\nEvil: 1"}});
|
||||||
|
EXPECT_FALSE(handle.is_valid());
|
||||||
|
EXPECT_EQ(Error::InvalidHeaders, handle.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
server_thread.join();
|
||||||
|
detail::close_socket(srv);
|
||||||
|
|
||||||
|
EXPECT_TRUE(received.empty()) << received;
|
||||||
|
}
|
||||||
|
|
||||||
TEST(PathParamsTest, StaticMatch) {
|
TEST(PathParamsTest, StaticMatch) {
|
||||||
const auto pattern = "/users/all";
|
const auto pattern = "/users/all";
|
||||||
detail::PathParamsMatcher matcher(pattern);
|
detail::PathParamsMatcher matcher(pattern);
|
||||||
@@ -22409,6 +22656,67 @@ TEST_F(SSEIntegrationTest, AutoReconnectAfterDisconnect) {
|
|||||||
EXPECT_GE(message_count.load(), 2);
|
EXPECT_GE(message_count.load(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test: A retry field that is not all ASCII digits is ignored
|
||||||
|
TEST_F(SSEIntegrationTest, NonDigitRetryFieldIgnored) {
|
||||||
|
std::atomic<int> connection_count{0};
|
||||||
|
|
||||||
|
server_->Get("/bad-retry",
|
||||||
|
[&connection_count](const Request &, Response &res) {
|
||||||
|
connection_count.fetch_add(1);
|
||||||
|
res.set_chunked_content_provider(
|
||||||
|
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||||
|
if (offset == 0) {
|
||||||
|
std::string event = "retry: -1\ndata: hello\n\n";
|
||||||
|
sink.write(event.data(), event.size());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Client client("localhost", get_port());
|
||||||
|
sse::SSEClient sse(client, "/bad-retry");
|
||||||
|
|
||||||
|
sse.set_reconnect_interval(10000);
|
||||||
|
sse.start_async();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||||
|
sse.stop();
|
||||||
|
|
||||||
|
EXPECT_EQ(connection_count.load(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test: A retry field of all ASCII digits sets the reconnection time
|
||||||
|
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
||||||
|
std::atomic<int> connection_count{0};
|
||||||
|
|
||||||
|
server_->Get("/zero-retry",
|
||||||
|
[&connection_count](const Request &, Response &res) {
|
||||||
|
connection_count.fetch_add(1);
|
||||||
|
res.set_chunked_content_provider(
|
||||||
|
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||||
|
if (offset == 0) {
|
||||||
|
std::string event = "retry: 0\ndata: hello\n\n";
|
||||||
|
sink.write(event.data(), event.size());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Client client("localhost", get_port());
|
||||||
|
sse::SSEClient sse(client, "/zero-retry");
|
||||||
|
|
||||||
|
sse.set_reconnect_interval(10000);
|
||||||
|
sse.start_async();
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||||
|
sse.stop();
|
||||||
|
|
||||||
|
// The server-supplied interval is applied, but never below 100ms, so
|
||||||
|
// "retry: 0" does not cause a busy reconnect loop
|
||||||
|
EXPECT_GE(connection_count.load(), 2);
|
||||||
|
EXPECT_LE(connection_count.load(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
// Test: Last-Event-ID sent on reconnect
|
// Test: Last-Event-ID sent on reconnect
|
||||||
TEST_F(SSEIntegrationTest, LastEventIdSentOnReconnect) {
|
TEST_F(SSEIntegrationTest, LastEventIdSentOnReconnect) {
|
||||||
std::atomic<int> connection_count{0};
|
std::atomic<int> connection_count{0};
|
||||||
@@ -24990,14 +25298,15 @@ TEST(SymlinkTest, SymlinkEscapeFromBaseDirectory) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
TEST(RequestSmugglingTest, UnconsumedGETBodyOnFileHandler) {
|
// Sends `outer_head` (with every "{len}" replaced by the length of an embedded
|
||||||
// A GET request with Content-Length to a static file handler must have its
|
// "GET /smuggled" request), reads the first response, and only then sends the
|
||||||
// body drained before the keep-alive connection is reused. Otherwise the
|
// embedded request as the body. Returns how many times /smuggled ran.
|
||||||
// unread body bytes are interpreted as the next HTTP request.
|
//
|
||||||
//
|
// The body is sent AFTER receiving the first response (as in the original
|
||||||
// The body is sent AFTER receiving the first response (as in the original
|
// PoC) so that the stream_line_reader cannot buffer it together with the
|
||||||
// PoC) so that the stream_line_reader cannot buffer it together with the
|
// headers of the first request.
|
||||||
// headers of the first request.
|
static int count_smuggled_requests(const std::string &outer_head,
|
||||||
|
std::string &first_response) {
|
||||||
Server svr;
|
Server svr;
|
||||||
svr.set_mount_point("/", "./www");
|
svr.set_mount_point("/", "./www");
|
||||||
|
|
||||||
@@ -25006,6 +25315,9 @@ TEST(RequestSmugglingTest, UnconsumedGETBodyOnFileHandler) {
|
|||||||
smuggled_count++;
|
smuggled_count++;
|
||||||
res.set_content("oops", "text/plain");
|
res.set_content("oops", "text/plain");
|
||||||
});
|
});
|
||||||
|
svr.Post("/post", [&](const Request &req, Response &res) {
|
||||||
|
res.set_content(req.body, "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
auto port = svr.bind_to_any_port("localhost");
|
auto port = svr.bind_to_any_port("localhost");
|
||||||
thread t = thread([&] { svr.listen_after_bind(); });
|
thread t = thread([&] { svr.listen_after_bind(); });
|
||||||
@@ -25021,28 +25333,28 @@ TEST(RequestSmugglingTest, UnconsumedGETBodyOnFileHandler) {
|
|||||||
/*connection_timeout_sec=*/2, 0,
|
/*connection_timeout_sec=*/2, 0,
|
||||||
/*read_timeout_sec=*/2, 0,
|
/*read_timeout_sec=*/2, 0,
|
||||||
/*write_timeout_sec=*/2, 0, std::string(), error);
|
/*write_timeout_sec=*/2, 0, std::string(), error);
|
||||||
ASSERT_NE(INVALID_SOCKET, sock);
|
EXPECT_NE(INVALID_SOCKET, sock);
|
||||||
|
if (sock == INVALID_SOCKET) { return -1; }
|
||||||
auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); });
|
auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); });
|
||||||
|
|
||||||
// The "smuggled" request will be sent as the body of the outer GET
|
// The "smuggled" request will be sent as the body of the outer request
|
||||||
std::string smuggled = "GET /smuggled HTTP/1.1\r\n"
|
std::string smuggled = "GET /smuggled HTTP/1.1\r\n"
|
||||||
"Host: localhost\r\n"
|
"Host: localhost\r\n"
|
||||||
"Connection: close\r\n"
|
"Connection: close\r\n"
|
||||||
"\r\n";
|
"\r\n";
|
||||||
|
|
||||||
|
auto head = outer_head;
|
||||||
|
auto len = std::to_string(smuggled.size());
|
||||||
|
for (auto pos = head.find("{len}"); pos != std::string::npos;
|
||||||
|
pos = head.find("{len}", pos + len.size())) {
|
||||||
|
head.replace(pos, 5, len);
|
||||||
|
}
|
||||||
|
|
||||||
// Step 1: Send only the outer request headers (no body yet)
|
// Step 1: Send only the outer request headers (no body yet)
|
||||||
std::string outer_headers = "GET /file HTTP/1.1\r\n"
|
auto sent = send(sock, head.data(), head.size(), 0);
|
||||||
"Host: localhost\r\n"
|
EXPECT_EQ(static_cast<ssize_t>(head.size()), sent);
|
||||||
"Content-Length: " +
|
|
||||||
std::to_string(smuggled.size()) +
|
|
||||||
"\r\n"
|
|
||||||
"\r\n";
|
|
||||||
|
|
||||||
auto sent = send(sock, outer_headers.data(), outer_headers.size(), 0);
|
// Step 2: Read the first response
|
||||||
ASSERT_EQ(static_cast<ssize_t>(outer_headers.size()), sent);
|
|
||||||
|
|
||||||
// Step 2: Read the first response (server serves file without reading body)
|
|
||||||
std::string first_response;
|
|
||||||
char buf[4096];
|
char buf[4096];
|
||||||
for (;;) {
|
for (;;) {
|
||||||
auto n = recv(sock, buf, sizeof(buf), 0);
|
auto n = recv(sock, buf, sizeof(buf), 0);
|
||||||
@@ -25064,23 +25376,103 @@ TEST(RequestSmugglingTest, UnconsumedGETBodyOnFileHandler) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ASSERT_TRUE(first_response.find("HTTP/1.1 200") != std::string::npos);
|
|
||||||
|
|
||||||
// Step 3: Now send the body, which looks like a new HTTP request.
|
// Step 3: Now send the body, which looks like a new HTTP request. On a
|
||||||
// On a vulnerable server the keep-alive loop reads this as a second request.
|
// vulnerable server the keep-alive loop reads this as a second request. The
|
||||||
sent = send(sock, smuggled.data(), smuggled.size(), 0);
|
// server may already have closed the connection, so the result is ignored.
|
||||||
ASSERT_EQ(static_cast<ssize_t>(smuggled.size()), sent);
|
send(sock, smuggled.data(), smuggled.size(), 0);
|
||||||
|
|
||||||
// Step 4: Try to read a second response (should NOT exist after fix)
|
// Half-close so that a server which drained the body sees EOF and closes,
|
||||||
std::string second_response;
|
// instead of the read below waiting for the read timeout.
|
||||||
|
#ifdef _WIN32
|
||||||
|
::shutdown(sock, SD_SEND);
|
||||||
|
#else
|
||||||
|
::shutdown(sock, SHUT_WR);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Step 4: Read until the server closes the connection
|
||||||
for (;;) {
|
for (;;) {
|
||||||
auto n = recv(sock, buf, sizeof(buf), 0);
|
auto n = recv(sock, buf, sizeof(buf), 0);
|
||||||
if (n <= 0) break;
|
if (n <= 0) break;
|
||||||
second_response.append(buf, static_cast<size_t>(n));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The smuggled request must NOT have been processed
|
return smuggled_count.load();
|
||||||
EXPECT_EQ(0, smuggled_count.load());
|
}
|
||||||
|
|
||||||
|
TEST(RequestSmugglingTest, UnconsumedGETBodyOnFileHandler) {
|
||||||
|
// A GET request with Content-Length to a static file handler must have its
|
||||||
|
// body drained before the keep-alive connection is reused. Otherwise the
|
||||||
|
// unread body bytes are interpreted as the next HTTP request.
|
||||||
|
std::string first_response;
|
||||||
|
EXPECT_EQ(0, count_smuggled_requests("GET /file HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Content-Length: {len}\r\n"
|
||||||
|
"\r\n",
|
||||||
|
first_response));
|
||||||
|
EXPECT_EQ(0u, first_response.find("HTTP/1.1 200"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RequestSmugglingTest, InvalidContentLengthRejected) {
|
||||||
|
// RFC 9112 §6.3: a Content-Length that is not a valid decimal length must
|
||||||
|
// be answered with 400 and the connection closed. Treating it as "no body"
|
||||||
|
// leaves the body to be parsed as the next request.
|
||||||
|
const char *values[] = {"{len}, {len}", "+{len}", "0x2e", "", " {len}x"};
|
||||||
|
const char *targets[] = {
|
||||||
|
"GET /file", // handler that never reads the body
|
||||||
|
"GET /not-found", // no handler matches (404)
|
||||||
|
"POST /post", // handler that reads the body
|
||||||
|
};
|
||||||
|
for (auto target : targets) {
|
||||||
|
for (auto value : values) {
|
||||||
|
std::string first_response;
|
||||||
|
EXPECT_EQ(0, count_smuggled_requests(std::string(target) +
|
||||||
|
" HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Content-Length: " +
|
||||||
|
value + "\r\n\r\n",
|
||||||
|
first_response))
|
||||||
|
<< target << " with Content-Length: " << value;
|
||||||
|
EXPECT_EQ(0u, first_response.find("HTTP/1.1 400"))
|
||||||
|
<< target << " with Content-Length: " << value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RequestSmugglingTest, RejectedRequestLineClosesConnection) {
|
||||||
|
// A 400 for an unparseable request line leaves the rest of the message
|
||||||
|
// unread, so the connection must be closed rather than reused.
|
||||||
|
std::string first_response;
|
||||||
|
EXPECT_EQ(0, count_smuggled_requests("FOO /file HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Content-Length: {len}\r\n"
|
||||||
|
"\r\n",
|
||||||
|
first_response));
|
||||||
|
EXPECT_EQ(0u, first_response.find("HTTP/1.1 400"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RequestSmugglingTest, RejectedHeadersCloseConnection) {
|
||||||
|
// Same as above for a header block that fails to parse.
|
||||||
|
std::string first_response;
|
||||||
|
EXPECT_EQ(0, count_smuggled_requests("GET /file HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"Bad Header\r\n"
|
||||||
|
"Content-Length: {len}\r\n"
|
||||||
|
"\r\n",
|
||||||
|
first_response));
|
||||||
|
EXPECT_EQ(0u, first_response.find("HTTP/1.1 400"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RequestSmugglingTest, ErrorResponseClosesConnection) {
|
||||||
|
// RFC 9112 §9.6: an error response carries "Connection: close", so the
|
||||||
|
// server must not read another request on that connection, even when the
|
||||||
|
// request had no body to drain.
|
||||||
|
std::string first_response;
|
||||||
|
EXPECT_EQ(0, count_smuggled_requests("GET /not-found HTTP/1.1\r\n"
|
||||||
|
"Host: localhost\r\n"
|
||||||
|
"\r\n",
|
||||||
|
first_response));
|
||||||
|
EXPECT_EQ(0u, first_response.find("HTTP/1.1 404"));
|
||||||
|
EXPECT_NE(std::string::npos, first_response.find("Connection: close"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(RequestSmugglingTest, ContentLengthAndTransferEncodingRejected) {
|
TEST(RequestSmugglingTest, ContentLengthAndTransferEncodingRejected) {
|
||||||
|
|||||||
Reference in new issue
Block a user