mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-19 11:19:36 +00:00
Combine repeated field lines before parsing list-valued headers
RFC 9110 Section 5.2 and 5.3 define the combined value of repeated field lines as their values joined by commas in the order they were received. Several call sites read only the first occurrence and then split that on commas, so whatever the later field lines carried was silently dropped: an acceptable media type or content coding, an ETag, a WebSocket subprotocol, a declared trailer name, or an address a proxy appended as its own line rather than by extending the one it received. Add detail::get_combined_header_value() and use it for Accept, Accept-Encoding, If-None-Match, Sec-WebSocket-Protocol, Trailer and X-Forwarded-For. Empty field lines are skipped so the combined value never starts with a bare comma, which parse_accept_header() rejects outright. Also drop the now-dead manual trimming in parse_trailers() and replace the istringstream-based subprotocol tokenizer with detail::split(); split() already trims each token and skips empty ones.
This commit is contained in:
1 parent
7ae9ffad3e
commit
161f787fee
2 files changed
+303
-31
No files matched your search
@@ -3550,6 +3550,9 @@ socket_t create_client_socket(const std::string &host, const std::string &ip,
|
||||
const char *get_header_value(const Headers &headers, const std::string &key,
|
||||
const char *def, size_t id);
|
||||
|
||||
std::string get_combined_header_value(const Headers &headers,
|
||||
const std::string &key);
|
||||
|
||||
std::string params_to_query_str(const Params ¶ms);
|
||||
|
||||
void parse_query_text(const char *data, std::size_t size, Params ¶ms);
|
||||
@@ -5714,22 +5717,14 @@ inline bool parse_trailers(stream_line_reader &line_reader, Headers &dest,
|
||||
"trailer"};
|
||||
|
||||
case_ignore::unordered_set<std::string> declared_trailers;
|
||||
auto trailer_header = get_header_value(src_headers, "Trailer", "", 0);
|
||||
if (trailer_header && std::strlen(trailer_header)) {
|
||||
auto len = std::strlen(trailer_header);
|
||||
split(trailer_header, trailer_header + len, ',',
|
||||
[&](const char *b, const char *e) {
|
||||
const char *kbeg = b;
|
||||
const char *kend = e;
|
||||
while (kbeg < kend && (*kbeg == ' ' || *kbeg == '\t')) {
|
||||
++kbeg;
|
||||
}
|
||||
while (kend > kbeg && (kend[-1] == ' ' || kend[-1] == '\t')) {
|
||||
--kend;
|
||||
}
|
||||
std::string key(kbeg, static_cast<size_t>(kend - kbeg));
|
||||
if (!key.empty() &&
|
||||
prohibited_trailers.find(key) == prohibited_trailers.end()) {
|
||||
auto trailer_header = get_combined_header_value(src_headers, "Trailer");
|
||||
if (!trailer_header.empty()) {
|
||||
// split() trims each token and skips empty ones, so the name arrives ready
|
||||
// to look up.
|
||||
split(trailer_header.data(), trailer_header.data() + trailer_header.size(),
|
||||
',', [&](const char *b, const char *e) {
|
||||
std::string key(b, e);
|
||||
if (prohibited_trailers.find(key) == prohibited_trailers.end()) {
|
||||
declared_trailers.insert(key);
|
||||
}
|
||||
});
|
||||
@@ -7326,7 +7321,7 @@ inline EncodingType encoding_type(const Request &req, const Response &res) {
|
||||
return EncodingType::None;
|
||||
}
|
||||
|
||||
const auto &s = req.get_header_value("Accept-Encoding");
|
||||
auto s = get_combined_header_value(req.headers, "Accept-Encoding");
|
||||
if (s.empty()) { return EncodingType::None; }
|
||||
|
||||
// Single-pass: iterate tokens and track the best supported encoding.
|
||||
@@ -7774,6 +7769,27 @@ inline size_t get_header_value_count(const Headers &headers,
|
||||
return headers.count(key);
|
||||
}
|
||||
|
||||
// RFC 9110 Section 5.2 and 5.3: a field that is defined as a comma-separated
|
||||
// list may be sent as several field lines, and the combined field value is
|
||||
// those values joined by commas in the order they were received. Callers that
|
||||
// parse such a list must work on the combined value; reading only the first
|
||||
// occurrence silently drops whatever the later field lines carry.
|
||||
inline std::string get_combined_header_value(const Headers &headers,
|
||||
const std::string &key) {
|
||||
std::string combined;
|
||||
auto rng = headers.equal_range(key);
|
||||
for (auto it = rng.first; it != rng.second; ++it) {
|
||||
// RFC 9110 Section 5.6.1.2: a recipient has to parse and ignore empty list
|
||||
// elements, so an empty field line must not contribute a bare comma to the
|
||||
// combined value. parse_accept_header() rejects a leading comma outright,
|
||||
// which would turn a legal request into 400 Bad Request.
|
||||
if (it->second.empty()) { continue; }
|
||||
if (!combined.empty()) { combined += ", "; }
|
||||
combined += it->second;
|
||||
}
|
||||
return combined;
|
||||
}
|
||||
|
||||
template <typename Map>
|
||||
inline typename Map::mapped_type
|
||||
get_multimap_value(const Map &m, const std::string &key, size_t id) {
|
||||
@@ -12905,7 +12921,8 @@ inline bool Server::check_if_not_modified(const Request &req, Response &res,
|
||||
// 2. If-Modified-Since is checked only when If-None-Match is absent
|
||||
if (req.has_header("If-None-Match")) {
|
||||
if (!etag.empty()) {
|
||||
auto val = req.get_header_value("If-None-Match");
|
||||
auto val =
|
||||
detail::get_combined_header_value(req.headers, "If-None-Match");
|
||||
|
||||
// NOTE: We use exact string matching here. This works correctly
|
||||
// because our server always generates weak ETags (W/"..."), and
|
||||
@@ -13467,7 +13484,13 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
[&](const std::string &proxy) { return proxy == remote_addr; });
|
||||
|
||||
if (is_trusted_peer && req.has_header("X-Forwarded-For")) {
|
||||
auto x_forwarded_for = req.get_header_value("X-Forwarded-For");
|
||||
// Some proxies append the address they observed as a separate
|
||||
// X-Forwarded-For field line instead of extending the one the client sent
|
||||
// (e.g. HAProxy's "option forwardfor"), so the whole combined value has to
|
||||
// be scanned. Reading only the first occurrence would hand back the
|
||||
// client-supplied, and therefore forgeable, value.
|
||||
auto x_forwarded_for =
|
||||
detail::get_combined_header_value(req.headers, "X-Forwarded-For");
|
||||
auto derived = get_client_ip(x_forwarded_for, trusted_proxies_);
|
||||
req.remote_addr = derived.empty() ? remote_addr : derived;
|
||||
} else {
|
||||
@@ -13479,7 +13502,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
req.local_port = local_port;
|
||||
|
||||
if (req.has_header("Accept")) {
|
||||
const auto &accept_header = req.get_header_value("Accept");
|
||||
auto accept_header =
|
||||
detail::get_combined_header_value(req.headers, "Accept");
|
||||
if (!detail::parse_accept_header(accept_header, req.accept_content_types)) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
@@ -13543,19 +13567,15 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
// Negotiate subprotocol
|
||||
std::string selected_subprotocol;
|
||||
if (entry.sub_protocol_selector) {
|
||||
auto protocol_header = req.get_header_value("Sec-WebSocket-Protocol");
|
||||
auto protocol_header = detail::get_combined_header_value(
|
||||
req.headers, "Sec-WebSocket-Protocol");
|
||||
if (!protocol_header.empty()) {
|
||||
std::vector<std::string> protocols;
|
||||
std::istringstream iss(protocol_header);
|
||||
std::string token;
|
||||
while (std::getline(iss, token, ',')) {
|
||||
// Trim whitespace
|
||||
auto start = token.find_first_not_of(' ');
|
||||
auto end = token.find_last_not_of(' ');
|
||||
if (start != std::string::npos) {
|
||||
protocols.push_back(token.substr(start, end - start + 1));
|
||||
}
|
||||
}
|
||||
detail::split(protocol_header.data(),
|
||||
protocol_header.data() + protocol_header.size(), ',',
|
||||
[&](const char *b, const char *e) {
|
||||
protocols.emplace_back(b, e);
|
||||
});
|
||||
selected_subprotocol = entry.sub_protocol_selector(protocols);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user