mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -939,6 +939,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 +1884,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_;
|
||||
@@ -3914,6 +3957,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
|
||||
@@ -4870,7 +4914,8 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
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);
|
||||
@@ -5006,11 +5051,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5778,15 +5827,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);
|
||||
@@ -7580,7 +7629,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 +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,
|
||||
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 +8793,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 +8807,7 @@ inline void set_file_content_provider(Response &res,
|
||||
return true;
|
||||
});
|
||||
|
||||
res.is_file_content_provider_ = true;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -11660,6 +11714,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 +11726,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 +11738,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;
|
||||
}
|
||||
|
||||
@@ -13248,6 +13305,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 +13394,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_) {
|
||||
// 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 +13425,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
|
||||
@@ -15506,7 +15586,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") {
|
||||
|
||||
+222
-10
@@ -1059,6 +1059,10 @@ TEST(ParseAcceptHeaderTest, InvalidCases) {
|
||||
EXPECT_FALSE(detail::parse_accept_header(
|
||||
"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
|
||||
EXPECT_FALSE(
|
||||
detail::parse_accept_header("text/html;q=,application/json", result));
|
||||
@@ -2112,6 +2116,16 @@ TEST(ParseAcceptEncoding5, AcceptEncodingQZeroVariants) {
|
||||
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) {
|
||||
// x-gzip;q=0 should not cause "gzip" to be incorrectly detected
|
||||
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) {
|
||||
// Nothing may reach the wire: a raw CR/LF target would split the request
|
||||
// line and inject headers.
|
||||
@@ -10078,7 +10123,7 @@ ssize_t write_request_line(Stream &strm, const std::string &method,
|
||||
} // namespace detail
|
||||
} // namespace httplib
|
||||
|
||||
TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
|
||||
TEST(RequestLineInjectionTest, RejectsInvalidCharsInTarget) {
|
||||
// A well-formed target is written verbatim.
|
||||
{
|
||||
detail::BufferStream strm;
|
||||
@@ -10087,15 +10132,18 @@ TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
|
||||
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,
|
||||
// otherwise it splits the request line and injects a header or a whole
|
||||
// request. This is what a decoded redirect Location ("%0D%0A") turns into
|
||||
// when path encoding is disabled.
|
||||
// A target carrying CR/LF, SP or other control octets must be rejected
|
||||
// before anything reaches the wire, otherwise it splits the request line and
|
||||
// injects a header or a whole request. This is what a decoded redirect
|
||||
// Location ("%0D%0A") turns into when path encoding is disabled.
|
||||
const std::string evil_targets[] = {
|
||||
"/a\r\nInjected: pwned",
|
||||
"/a\rInjected",
|
||||
"/a\nInjected",
|
||||
"/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) {
|
||||
detail::BufferStream strm;
|
||||
@@ -10106,11 +10154,11 @@ TEST(RequestLineInjectionTest, RejectsCRLFInTarget) {
|
||||
}
|
||||
|
||||
TEST(RequestLineInjectionTest, ClientRejectsCRLFTargetEndToEnd) {
|
||||
// End-to-end counterpart to RejectsCRLFInTarget. With path encoding disabled
|
||||
// the client transmits the target verbatim, so a CR/LF-bearing target -- what
|
||||
// a redirect Location "%0D%0A" decodes to -- reaches write_request. The
|
||||
// client must fail cleanly with Error::Write instead of putting a
|
||||
// request-line-less, header-injecting request on the wire.
|
||||
// End-to-end counterpart to RejectsInvalidCharsInTarget. With path encoding
|
||||
// disabled the client transmits the target verbatim, so a CR/LF-bearing
|
||||
// target -- what a redirect Location "%0D%0A" decodes to -- reaches
|
||||
// write_request. The client must fail cleanly with Error::Write instead of
|
||||
// putting a request-line-less, header-injecting request on the wire.
|
||||
Server svr;
|
||||
|
||||
svr.Get("/a", [](const Request &, Response &res) {
|
||||
@@ -10376,6 +10424,22 @@ TEST(ServerRequestParsingTest, InvalidSpaceInURL) {
|
||||
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) {
|
||||
Server svr;
|
||||
|
||||
@@ -17281,6 +17345,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
|
||||
TEST(RedirectTest, Issue2185_Online) {
|
||||
SSLClient client("github.com");
|
||||
@@ -22496,6 +22647,67 @@ TEST_F(SSEIntegrationTest, AutoReconnectAfterDisconnect) {
|
||||
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_F(SSEIntegrationTest, LastEventIdSentOnReconnect) {
|
||||
std::atomic<int> connection_count{0};
|
||||
|
||||
Reference in New Issue
Block a user