mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-30 13:00:42 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c419c904da | ||
|
|
878ba41099 | ||
|
|
941745c33d | ||
|
|
11daa99c86 | ||
|
|
cbaf751da1 | ||
|
|
33d84a12b6 | ||
|
|
5107381963 | ||
|
|
0acbee16dc | ||
|
|
1f12bbaabb | ||
|
|
5189edc52e |
No files matched your search
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.60.1"
|
version = "0.61.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.60.1"
|
#define CPPHTTPLIB_VERSION "0.61.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x003c01"
|
#define CPPHTTPLIB_VERSION_NUM "0x003d00"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -1141,8 +1141,7 @@ private:
|
|||||||
pointer operator->() const { return data_ + idx_; }
|
pointer operator->() const { return data_ + idx_; }
|
||||||
|
|
||||||
iterator_t &operator++() {
|
iterator_t &operator++() {
|
||||||
// Saturating, so that advancing past the last entry of a key (which
|
// Saturating, so that advancing past the last entry of a key stays at
|
||||||
// get_multimap_value() does when asked for an out-of-range id) stays at
|
|
||||||
// end() instead of running off the container.
|
// end() instead of running off the container.
|
||||||
if (idx_ >= size_) { return *this; }
|
if (idx_ >= size_) { return *this; }
|
||||||
++idx_;
|
++idx_;
|
||||||
@@ -3477,13 +3476,20 @@ inline bool is_numeric(const std::string &str) {
|
|||||||
return !str.empty() && std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
|
return !str.empty() && std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// std::ranges::next(first, n, last) for C++11
|
||||||
|
template <typename It> inline It bounded_next(It first, size_t n, It last) {
|
||||||
|
for (; n > 0 && first != last; --n) {
|
||||||
|
++first;
|
||||||
|
}
|
||||||
|
return first;
|
||||||
|
}
|
||||||
|
|
||||||
inline size_t get_header_value_u64(const Headers &headers,
|
inline size_t get_header_value_u64(const Headers &headers,
|
||||||
const std::string &key, size_t def,
|
const std::string &key, size_t def,
|
||||||
size_t id, bool &is_invalid_value) {
|
size_t id, bool &is_invalid_value) {
|
||||||
is_invalid_value = false;
|
is_invalid_value = false;
|
||||||
auto rng = headers.equal_range(key);
|
auto rng = headers.equal_range(key);
|
||||||
auto it = rng.first;
|
auto it = bounded_next(rng.first, id, rng.second);
|
||||||
std::advance(it, static_cast<ssize_t>(id));
|
|
||||||
if (it != rng.second) {
|
if (it != rng.second) {
|
||||||
if (is_numeric(it->second)) {
|
if (is_numeric(it->second)) {
|
||||||
// Parse at size_t width so an out-of-range Content-Length is reported
|
// Parse at size_t width so an out-of-range Content-Length is reported
|
||||||
@@ -3968,6 +3974,7 @@ 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);
|
bool is_request_target(const std::string &s);
|
||||||
|
bool is_host(const std::string &s);
|
||||||
|
|
||||||
} // namespace fields
|
} // namespace fields
|
||||||
} // namespace detail
|
} // namespace detail
|
||||||
@@ -4918,9 +4925,12 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
|||||||
msg.data += value;
|
msg.data += value;
|
||||||
has_data = true;
|
has_data = true;
|
||||||
} else if (field == "id") {
|
} else if (field == "id") {
|
||||||
// Empty id is valid (clears the last event ID)
|
// An id that cannot be sent back as Last-Event-ID is ignored. Empty id is
|
||||||
msg.id = value;
|
// valid (clears the last event ID)
|
||||||
has_id = true;
|
if (detail::fields::is_field_value(value)) {
|
||||||
|
msg.id = value;
|
||||||
|
has_id = true;
|
||||||
|
}
|
||||||
} 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.
|
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||||
@@ -4994,22 +5004,29 @@ inline void SSEClient::run_event_loop() {
|
|||||||
auto has_data = false;
|
auto has_data = false;
|
||||||
auto has_id = false;
|
auto has_id = false;
|
||||||
auto first_line = true;
|
auto first_line = true;
|
||||||
|
auto after_cr = false;
|
||||||
|
|
||||||
while (running_.load() && result.next()) {
|
while (running_.load() && result.next()) {
|
||||||
|
// What is left in the buffer has no line ending
|
||||||
|
auto search_pos = buffer.size();
|
||||||
buffer.append(result.data(), result.size());
|
buffer.append(result.data(), result.size());
|
||||||
|
|
||||||
// Process complete lines in the buffer
|
// Process complete lines in the buffer. A line ends with \r\n, \n or \r
|
||||||
|
// per the SSE spec
|
||||||
size_t line_start = 0;
|
size_t line_start = 0;
|
||||||
size_t newline_pos;
|
size_t eol_pos;
|
||||||
|
|
||||||
while ((newline_pos = buffer.find('\n', line_start)) !=
|
while ((eol_pos = buffer.find_first_of("\r\n", search_pos)) !=
|
||||||
std::string::npos) {
|
std::string::npos) {
|
||||||
auto line = buffer.substr(line_start, newline_pos - line_start);
|
auto line = buffer.substr(line_start, eol_pos - line_start);
|
||||||
line_start = newline_pos + 1;
|
line_start = eol_pos + 1;
|
||||||
|
search_pos = line_start;
|
||||||
|
|
||||||
// Strip the \r of a CRLF line ending so that every field, including
|
// The \n of a \r\n may arrive in a later chunk than its \r
|
||||||
// one without a colon, sees the same line
|
auto is_lf_of_crlf =
|
||||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
after_cr && line.empty() && buffer[eol_pos] == '\n';
|
||||||
|
after_cr = buffer[eol_pos] == '\r';
|
||||||
|
if (is_lf_of_crlf) { continue; }
|
||||||
|
|
||||||
// A UTF-8 BOM at the start of the stream is ignored per the SSE spec
|
// A UTF-8 BOM at the start of the stream is ignored per the SSE spec
|
||||||
if (first_line) {
|
if (first_line) {
|
||||||
@@ -8135,8 +8152,7 @@ inline const char *get_header_value(const Headers &headers,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto rng = headers.equal_range(key);
|
auto rng = headers.equal_range(key);
|
||||||
auto it = rng.first;
|
auto it = bounded_next(rng.first, id, rng.second);
|
||||||
std::advance(it, static_cast<ssize_t>(id));
|
|
||||||
if (it != rng.second) { return it->second.c_str(); }
|
if (it != rng.second) { return it->second.c_str(); }
|
||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
@@ -8189,8 +8205,7 @@ template <typename Map>
|
|||||||
inline typename Map::mapped_type
|
inline typename Map::mapped_type
|
||||||
get_multimap_value(const Map &m, const std::string &key, size_t id) {
|
get_multimap_value(const Map &m, const std::string &key, size_t id) {
|
||||||
auto rng = m.equal_range(key);
|
auto rng = m.equal_range(key);
|
||||||
auto it = rng.first;
|
auto it = bounded_next(rng.first, id, rng.second);
|
||||||
std::advance(it, static_cast<ssize_t>(id));
|
|
||||||
if (it != rng.second) { return it->second; }
|
if (it != rng.second) { return it->second; }
|
||||||
return typename Map::mapped_type();
|
return typename Map::mapped_type();
|
||||||
}
|
}
|
||||||
@@ -9178,7 +9193,7 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
|
|||||||
return std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
|
return std::all_of(str.cbegin(), str.cend(), is_ascii_digit);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (s.size() > 7 && s.compare(0, 6, "bytes=") == 0) {
|
if (s.size() > 7 && case_ignore::equal(s.substr(0, 6), "bytes=")) {
|
||||||
const auto pos = static_cast<size_t>(6);
|
const auto pos = static_cast<size_t>(6);
|
||||||
const auto len = static_cast<size_t>(s.size() - 6);
|
const auto len = static_cast<size_t>(s.size() - 6);
|
||||||
auto all_valid_ranges = true;
|
auto all_valid_ranges = true;
|
||||||
@@ -9236,8 +9251,30 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
|
|||||||
} catch (...) { return false; }
|
} catch (...) { return false; }
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Removes each quoted-string, honoring a quoted-pair (RFC 9110 Section 5.6.4)
|
||||||
|
inline std::string remove_quoted_strings(const std::string &s) {
|
||||||
|
std::string out;
|
||||||
|
auto in_quotes = false;
|
||||||
|
for (size_t i = 0; i < s.size(); i++) {
|
||||||
|
if (s[i] == '"') {
|
||||||
|
in_quotes = !in_quotes;
|
||||||
|
} else if (!in_quotes) {
|
||||||
|
out += s[i];
|
||||||
|
} else if (s[i] == '\\') {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
inline bool parse_accept_header(const std::string &s,
|
inline bool parse_accept_header(const std::string &s,
|
||||||
std::vector<std::string> &content_types) {
|
std::vector<std::string> &content_types) {
|
||||||
|
// Parameter values are not used here, so a quoted-string is dropped rather
|
||||||
|
// than parsed: a ',' or ';' inside one must not be read as a delimiter.
|
||||||
|
if (s.find('"') != std::string::npos) {
|
||||||
|
return parse_accept_header(remove_quoted_strings(s), content_types);
|
||||||
|
}
|
||||||
|
|
||||||
content_types.clear();
|
content_types.clear();
|
||||||
|
|
||||||
// Empty string is considered valid (no preference)
|
// Empty string is considered valid (no preference)
|
||||||
@@ -10312,6 +10349,17 @@ inline bool is_request_target(const std::string &s) {
|
|||||||
return std::all_of(s.begin(), s.end(), is_field_vchar);
|
return std::all_of(s.begin(), s.end(), is_field_vchar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 9110 §7.2: the bytes of uri-host [ ":" port ], i.e. unreserved,
|
||||||
|
// sub-delims, "%", ":" and the IP-literal brackets. An empty value is valid.
|
||||||
|
inline bool is_host(const std::string &s) {
|
||||||
|
return std::all_of(s.begin(), s.end(), [](char c) {
|
||||||
|
return is_ascii_alnum(c) || c == '-' || c == '.' || c == '_' || c == '~' ||
|
||||||
|
c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' ||
|
||||||
|
c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||
|
||||||
|
c == '=' || c == '%' || c == ':' || c == '[' || c == ']';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace fields
|
} // namespace fields
|
||||||
|
|
||||||
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||||
@@ -11684,8 +11732,7 @@ inline bool Request::is_multipart_form_data() const {
|
|||||||
inline std::string MultipartFormData::get_field(const std::string &key,
|
inline std::string MultipartFormData::get_field(const std::string &key,
|
||||||
size_t id) const {
|
size_t id) const {
|
||||||
auto rng = fields.equal_range(key);
|
auto rng = fields.equal_range(key);
|
||||||
auto it = rng.first;
|
auto it = detail::bounded_next(rng.first, id, rng.second);
|
||||||
std::advance(it, static_cast<ssize_t>(id));
|
|
||||||
if (it != rng.second) { return it->second.content; }
|
if (it != rng.second) { return it->second.content; }
|
||||||
return std::string();
|
return std::string();
|
||||||
}
|
}
|
||||||
@@ -14570,6 +14617,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
return write_response(strm, close_connection, req, res);
|
return write_response(strm, close_connection, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 9112 §3.2: Reject more than one Host field line or an invalid Host
|
||||||
|
// value, so that every recipient of the request sees the same authority.
|
||||||
|
if (req.get_header_value_count("Host") > 1 ||
|
||||||
|
!detail::fields::is_host(req.get_header_value("Host"))) {
|
||||||
|
connection_closed = true;
|
||||||
|
res.status = StatusCode::BadRequest_400;
|
||||||
|
output_error_log(Error::InvalidHeaders, &req);
|
||||||
|
return write_response(strm, close_connection, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the request URI doesn't exceed the limit
|
// Check if the request URI doesn't exceed the limit
|
||||||
if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
|
if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
|
||||||
connection_closed = true;
|
connection_closed = true;
|
||||||
@@ -15887,7 +15944,8 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
|
|||||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||||
client.set_proxy(proxy_host_, proxy_port_);
|
client.set_proxy(proxy_host_, proxy_port_);
|
||||||
|
|
||||||
if (!proxy_basic_auth_username_.empty()) {
|
if (!proxy_basic_auth_username_.empty() ||
|
||||||
|
!proxy_basic_auth_password_.empty()) {
|
||||||
client.set_proxy_basic_auth(proxy_basic_auth_username_,
|
client.set_proxy_basic_auth(proxy_basic_auth_username_,
|
||||||
proxy_basic_auth_password_);
|
proxy_basic_auth_password_);
|
||||||
}
|
}
|
||||||
@@ -15990,8 +16048,8 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
||||||
// would leak proxy credentials to the destination server.
|
// would leak proxy credentials to the destination server.
|
||||||
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
||||||
if (!proxy_basic_auth_username_.empty() &&
|
if ((!proxy_basic_auth_username_.empty() ||
|
||||||
!proxy_basic_auth_password_.empty() &&
|
!proxy_basic_auth_password_.empty()) &&
|
||||||
!req.has_header("Proxy-Authorization")) {
|
!req.has_header("Proxy-Authorization")) {
|
||||||
req.headers.insert(make_basic_authentication_header(
|
req.headers.insert(make_basic_authentication_header(
|
||||||
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
|
proxy_basic_auth_username_, proxy_basic_auth_password_, true));
|
||||||
|
|||||||
+229
-16
@@ -1119,6 +1119,39 @@ TEST(ParseAcceptHeaderTest, EmptyListElementsAreIgnored) {
|
|||||||
EXPECT_FALSE(detail::parse_accept_header(",invalidtype,", result));
|
EXPECT_FALSE(detail::parse_accept_header(",invalidtype,", result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 9110 Section 5.6.6: a parameter value may be a quoted-string, and a ','
|
||||||
|
// or ';' inside one does not end the list element or the parameter. Section
|
||||||
|
// 5.6.4: nor does a '"' that a backslash escapes end the quoted-string.
|
||||||
|
TEST(ParseAcceptHeaderTest, DelimitersInQuotedParameterValues) {
|
||||||
|
struct {
|
||||||
|
const char *value;
|
||||||
|
std::vector<std::string> expected;
|
||||||
|
} cases[] = {
|
||||||
|
{"text/html;profile=\"a,b\",application/json",
|
||||||
|
{"text/html", "application/json"}},
|
||||||
|
// A "q=" inside a quoted value is not the weight
|
||||||
|
{"text/html;profile=\"a;q=0.1\",application/json;q=0.5",
|
||||||
|
{"text/html", "application/json"}},
|
||||||
|
// The weight after a quoted value still applies
|
||||||
|
{"text/html;profile=\"a,b;q=0.1\";q=0.8,application/json;q=0.9",
|
||||||
|
{"application/json", "text/html"}},
|
||||||
|
{"text/html;profile=\"a\\\",b\";q=0.8,application/json;q=0.9",
|
||||||
|
{"application/json", "text/html"}},
|
||||||
|
{"text/html;profile=\"a\\\"b\",application/json",
|
||||||
|
{"text/html", "application/json"}},
|
||||||
|
// An escaped backslash does not escape the '"' after it
|
||||||
|
{"text/html;profile=\"a\\\\\",application/json",
|
||||||
|
{"text/html", "application/json"}},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &c : cases) {
|
||||||
|
std::vector<std::string> result;
|
||||||
|
EXPECT_TRUE(detail::parse_accept_header(c.value, result))
|
||||||
|
<< "value: " << c.value;
|
||||||
|
EXPECT_EQ(c.expected, result) << "value: " << c.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
|
TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
@@ -1890,6 +1923,28 @@ TEST(HeadersOrderTest, IdBeyondTheLastDuplicateYieldsDefault) {
|
|||||||
EXPECT_EQ("def", req.get_header_value("X-Missing", "def", 3));
|
EXPECT_EQ("def", req.get_header_value("X-Missing", "def", 3));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(HeadersOrderTest, HugeIdYieldsDefault) {
|
||||||
|
Request req;
|
||||||
|
req.set_header("X-First", "1");
|
||||||
|
req.set_header("X-Test", "2");
|
||||||
|
req.params.emplace("first", "1");
|
||||||
|
req.params.emplace("test", "2");
|
||||||
|
req.form.fields.emplace("first", FormField{"first", "1", {}});
|
||||||
|
req.form.fields.emplace("test", FormField{"test", "2", {}});
|
||||||
|
req.form.files.emplace("first", FormData{"first", "1", "", "", {}});
|
||||||
|
req.form.files.emplace("test", FormData{"test", "2", "", "", {}});
|
||||||
|
|
||||||
|
// The second one is negative when read as a signed value
|
||||||
|
const auto max_id = (std::numeric_limits<size_t>::max)();
|
||||||
|
for (auto id : {max_id / 2, max_id}) {
|
||||||
|
EXPECT_EQ("def", req.get_header_value("X-Test", "def", id));
|
||||||
|
EXPECT_EQ(7U, req.get_header_value_u64("X-Test", 7, id));
|
||||||
|
EXPECT_EQ("", req.get_param_value("test", id));
|
||||||
|
EXPECT_EQ("", req.form.get_field("test", id));
|
||||||
|
EXPECT_EQ("", req.form.get_file("test", id).content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(HeadersOrderTest, TraversalFollowsInsertionOrder) {
|
TEST(HeadersOrderTest, TraversalFollowsInsertionOrder) {
|
||||||
Headers headers;
|
Headers headers;
|
||||||
headers.emplace("Host", "example.com");
|
headers.emplace("Host", "example.com");
|
||||||
@@ -2033,6 +2088,13 @@ TEST(ParseHeaderValueTest, Range) {
|
|||||||
EXPECT_EQ(0, ranges[0].first);
|
EXPECT_EQ(0, ranges[0].first);
|
||||||
EXPECT_EQ(-1, ranges[0].second);
|
EXPECT_EQ(-1, ranges[0].second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 9110 14.1: range unit names are case-insensitive.
|
||||||
|
for (const auto &s : {"Bytes=1-10", "BYTES=1-10", "bYtEs=1-10"}) {
|
||||||
|
Ranges ranges;
|
||||||
|
EXPECT_TRUE(detail::parse_range_header(s, ranges)) << s;
|
||||||
|
EXPECT_EQ((Ranges{{1, 10}}), ranges) << s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ParseAcceptEncoding1, AcceptEncoding) {
|
TEST(ParseAcceptEncoding1, AcceptEncoding) {
|
||||||
@@ -19686,6 +19748,58 @@ TEST(RequestSmugglingTest, DuplicateContentLengthSameValues) {
|
|||||||
ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n")));
|
ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(HostHeaderValidationTest, AmbiguousOrInvalidHostRejected) {
|
||||||
|
Server svr;
|
||||||
|
svr.Get("/test", [](const Request &, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
thread t = thread([&] { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
auto status_line = [&](const std::string &host_lines) {
|
||||||
|
std::string res;
|
||||||
|
send_request(
|
||||||
|
5, "GET /test HTTP/1.1\r\n" + host_lines + "Connection: close\r\n\r\n",
|
||||||
|
&res, port);
|
||||||
|
return res.substr(0, res.find("\r\n"));
|
||||||
|
};
|
||||||
|
|
||||||
|
const char *accepted[] = {
|
||||||
|
"Host: localhost\r\n",
|
||||||
|
"Host: a.example:8080\r\n",
|
||||||
|
"Host: my_service\r\n",
|
||||||
|
"Host: [::1]:8080\r\n",
|
||||||
|
"Host: [fe80::1%25eth0]\r\n",
|
||||||
|
"Host: \r\n", // RFC 9112 §3.2: the target URI has no authority
|
||||||
|
};
|
||||||
|
for (auto host_lines : accepted) {
|
||||||
|
EXPECT_EQ("HTTP/1.1 200 OK", status_line(host_lines)) << host_lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *rejected[] = {
|
||||||
|
"Host: a.example\r\nHost: b.example\r\n",
|
||||||
|
"Host: a.example\r\nHost: a.example\r\n",
|
||||||
|
"Host: a@b.example\r\n",
|
||||||
|
"Host: a.example/b\r\n",
|
||||||
|
"Host: a.example\\b\r\n",
|
||||||
|
"Host: a.example?b\r\n",
|
||||||
|
"Host: a.example#b\r\n",
|
||||||
|
"Host: a.example b.example\r\n",
|
||||||
|
};
|
||||||
|
for (auto host_lines : rejected) {
|
||||||
|
EXPECT_EQ("HTTP/1.1 400 Bad Request", status_line(host_lines))
|
||||||
|
<< host_lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
|
TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
@@ -22793,6 +22907,37 @@ TEST_F(SSEParsingTest, FieldNameOnlyWithCarriageReturn) {
|
|||||||
EXPECT_EQ(msgs[0].data, "");
|
EXPECT_EQ(msgs[0].data, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SSEParsingTest, CarriageReturnLineEnding) {
|
||||||
|
auto msgs = parse("event: update\rdata: hello\r\r");
|
||||||
|
ASSERT_EQ(msgs.size(), 1u);
|
||||||
|
EXPECT_EQ(msgs[0].event, "update");
|
||||||
|
EXPECT_EQ(msgs[0].data, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(SSEParsingTest, LineEndingSplitAcrossChunks) {
|
||||||
|
// Chunk boundaries fall inside every \r\n and right before the first one
|
||||||
|
std::vector<std::string> chunks = {"event: update", "\r", "\ndata: hello\r",
|
||||||
|
"\n\r", "\nevent: end\ndata: end\n\n"};
|
||||||
|
server_->Get("/split", [chunks](const Request &, Response &res) {
|
||||||
|
auto i = std::make_shared<size_t>(0);
|
||||||
|
res.set_chunked_content_provider(
|
||||||
|
"text/event-stream", [chunks, i](size_t /*offset*/, DataSink &sink) {
|
||||||
|
if (*i == chunks.size()) {
|
||||||
|
sink.done();
|
||||||
|
} else {
|
||||||
|
sink.write(chunks[*i].data(), chunks[*i].size());
|
||||||
|
(*i)++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
auto msgs = collect("/split");
|
||||||
|
ASSERT_EQ(msgs.size(), 1u);
|
||||||
|
EXPECT_EQ(msgs[0].event, "update");
|
||||||
|
EXPECT_EQ(msgs[0].data, "hello");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, LeadingByteOrderMarkIgnored) {
|
TEST_F(SSEParsingTest, LeadingByteOrderMarkIgnored) {
|
||||||
// Only a BOM at the start of the stream is ignored
|
// Only a BOM at the start of the stream is ignored
|
||||||
auto msgs = parse("\xEF\xBB\xBF"
|
auto msgs = parse("\xEF\xBB\xBF"
|
||||||
@@ -22869,6 +23014,28 @@ TEST_F(SSEParsingTest, EmptyEventIdClearsLastEventId) {
|
|||||||
EXPECT_EQ(msgs[2].data, "not sent");
|
EXPECT_EQ(msgs[2].data, "not sent");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SSEParsingTest, UnsendableEventIdKeepsLastEventId) {
|
||||||
|
// The first connection sets an id and then sends ids that cannot be carried
|
||||||
|
// in a header; the second echoes back the Last-Event-ID it was reconnected
|
||||||
|
// with
|
||||||
|
std::atomic<int> connection_count{0};
|
||||||
|
server_->Get("/id-unsendable", [&](const Request &req, Response &res) {
|
||||||
|
if (connection_count++ == 0) {
|
||||||
|
res.set_content(std::string("id: 42\n\nid: a") + '\0' +
|
||||||
|
"b\n\nid: \x01\n\nid: 7\n\nid: 7 \n\n",
|
||||||
|
"text/event-stream");
|
||||||
|
} else {
|
||||||
|
res.set_content("data: " + req.get_header_value("Last-Event-ID") +
|
||||||
|
"\n\nevent: end\ndata: end\n\n",
|
||||||
|
"text/event-stream");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto msgs = collect("/id-unsendable");
|
||||||
|
ASSERT_EQ(msgs.size(), 1u);
|
||||||
|
EXPECT_EQ(msgs[0].data, "42");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
||||||
auto msgs = parse("event: notification\nid: evt-42\n"
|
auto msgs = parse("event: notification\nid: evt-42\n"
|
||||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
||||||
@@ -23320,20 +23487,25 @@ TEST_F(SSEIntegrationTest, NonDigitRetryFieldIgnored) {
|
|||||||
|
|
||||||
// Test: A retry field of all ASCII digits sets the reconnection time
|
// Test: A retry field of all ASCII digits sets the reconnection time
|
||||||
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
||||||
std::atomic<int> connection_count{0};
|
std::mutex mutex;
|
||||||
|
std::condition_variable cv;
|
||||||
|
std::vector<std::chrono::steady_clock::time_point> connected_at;
|
||||||
|
|
||||||
server_->Get("/zero-retry",
|
server_->Get("/zero-retry", [&](const Request &, Response &res) {
|
||||||
[&connection_count](const Request &, Response &res) {
|
{
|
||||||
connection_count.fetch_add(1);
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
res.set_chunked_content_provider(
|
connected_at.push_back(std::chrono::steady_clock::now());
|
||||||
"text/event-stream", [](size_t offset, DataSink &sink) {
|
cv.notify_all();
|
||||||
if (offset == 0) {
|
}
|
||||||
std::string event = "retry: 0\ndata: hello\n\n";
|
res.set_chunked_content_provider(
|
||||||
sink.write(event.data(), event.size());
|
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||||
}
|
if (offset == 0) {
|
||||||
return false;
|
std::string event = "retry: 0\ndata: hello\n\n";
|
||||||
});
|
sink.write(event.data(), event.size());
|
||||||
});
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
Client client("localhost", get_port());
|
Client client("localhost", get_port());
|
||||||
sse::SSEClient sse(client, "/zero-retry");
|
sse::SSEClient sse(client, "/zero-retry");
|
||||||
@@ -23341,13 +23513,21 @@ TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
|||||||
sse.set_reconnect_interval(10000);
|
sse.set_reconnect_interval(10000);
|
||||||
sse.start_async();
|
sse.start_async();
|
||||||
|
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(mutex);
|
||||||
|
cv.wait_for(lock, std::chrono::seconds(5),
|
||||||
|
[&] { return connected_at.size() >= 2; });
|
||||||
|
}
|
||||||
sse.stop();
|
sse.stop();
|
||||||
|
|
||||||
// The server-supplied interval is applied, but never below 100ms, so
|
// The server-supplied interval is applied, but never below 100ms, so
|
||||||
// "retry: 0" does not cause a busy reconnect loop
|
// "retry: 0" does not cause a busy reconnect loop
|
||||||
EXPECT_GE(connection_count.load(), 2);
|
std::lock_guard<std::mutex> lock(mutex);
|
||||||
EXPECT_LE(connection_count.load(), 10);
|
ASSERT_GE(connected_at.size(), 2u);
|
||||||
|
auto interval_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
connected_at[1] - connected_at[0])
|
||||||
|
.count();
|
||||||
|
EXPECT_GE(interval_ms, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test: Last-Event-ID sent on reconnect
|
// Test: Last-Event-ID sent on reconnect
|
||||||
@@ -26795,6 +26975,39 @@ TEST(NoProxyTest, ProxyAuthorizationSentWhenNotBypassed) {
|
|||||||
EXPECT_TRUE(s.last_had_proxy_authz());
|
EXPECT_TRUE(s.last_had_proxy_authz());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(NoProxyTest, ProxyAuthorizationSentWithOneEmptyCredential) {
|
||||||
|
std::atomic<int> hits{0};
|
||||||
|
std::atomic<int> hits_with_authz{0};
|
||||||
|
|
||||||
|
no_proxy_test::ScopedServer proxy_mock;
|
||||||
|
proxy_mock.svr().Get(".*", [&](const Request &req, Response &res) {
|
||||||
|
hits++;
|
||||||
|
if (req.has_header("Proxy-Authorization")) { hits_with_authz++; }
|
||||||
|
if (req.target.find("/redir") != std::string::npos) {
|
||||||
|
res.set_redirect("http://other.example/landed");
|
||||||
|
} else {
|
||||||
|
res.set_content("via-proxy", "text/plain");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
proxy_mock.listen();
|
||||||
|
|
||||||
|
for (const auto &c : {std::make_pair("u", ""), std::make_pair("", "p")}) {
|
||||||
|
hits = 0;
|
||||||
|
hits_with_authz = 0;
|
||||||
|
|
||||||
|
Client cli("public.example", 80);
|
||||||
|
cli.set_proxy("127.0.0.1", proxy_mock.port());
|
||||||
|
cli.set_proxy_basic_auth(c.first, c.second);
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
|
||||||
|
// The redirect to another host goes through the proxy as well
|
||||||
|
auto res = cli.Get("/redir");
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(2, hits.load());
|
||||||
|
EXPECT_EQ(2, hits_with_authz.load());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(NoProxyTest, EmptyNoProxyKeepsProxyOn) {
|
TEST(NoProxyTest, EmptyNoProxyKeepsProxyOn) {
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
auto cli = make_client("anything.test", s);
|
auto cli = make_client("anything.test", s);
|
||||||
|
|||||||
Reference in new issue
Block a user