From c698fb03c19a8a645e2cadf061fc4655e0dc8587 Mon Sep 17 00:00:00 2001 From: "Alessandro de Oliveira Faria (A.K.A. CABELO)" Date: Sun, 2 Aug 2026 22:47:18 -0300 Subject: [PATCH] vendor : update cpp-httplib to 0.52.0 --- scripts/sync_vendor.py | 2 +- vendor/cpp-httplib/httplib.cpp | 583 +++++++++++++++++++++++---------- vendor/cpp-httplib/httplib.h | 331 ++++++++++++++++++- 3 files changed, 732 insertions(+), 184 deletions(-) diff --git a/scripts/sync_vendor.py b/scripts/sync_vendor.py index 679e557a3215..9faa6a307c13 100755 --- a/scripts/sync_vendor.py +++ b/scripts/sync_vendor.py @@ -5,7 +5,7 @@ import sys import subprocess -HTTPLIB_VERSION = "refs/tags/v0.51.0" +HTTPLIB_VERSION = "refs/tags/v0.52.0" vendor = { "https://github.com/nlohmann/json/releases/latest/download/json.hpp": "vendor/nlohmann/json.hpp", diff --git a/vendor/cpp-httplib/httplib.cpp b/vendor/cpp-httplib/httplib.cpp index 15517963874a..30e2de896220 100644 --- a/vendor/cpp-httplib/httplib.cpp +++ b/vendor/cpp-httplib/httplib.cpp @@ -1412,6 +1412,46 @@ bool stream_line_reader::getline() { #endif for (size_t i = 0;; i++) { + // Fast path: whatever the stream has already buffered can be scanned for + // the terminator in one pass. Asking for a byte at a time costs a virtual + // call, a bounds check and a one-byte copy per character of the request. + size_t buffered_size = 0; + if (auto buffered = strm_.buffered_data(buffered_size)) { + auto take = buffered_size; + auto terminated = false; + + for (size_t at = 0; at < buffered_size;) { + auto nl = static_cast( + memchr(buffered + at, '\n', buffered_size - at)); + if (!nl) { break; } + auto pos = static_cast(nl - buffered); +#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + take = pos + 1; + terminated = true; + break; +#else + // A bare LF does not end the line; keep looking for CRLF. The CR may + // be the last byte of an earlier chunk, hence prev_byte. + if ((pos > 0 ? buffered[pos - 1] : prev_byte) == '\r') { + take = pos + 1; + terminated = true; + break; + } + at = pos + 1; +#endif + } + + if (size() + take > CPPHTTPLIB_MAX_LINE_LENGTH) { return false; } +#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR + prev_byte = buffered[take - 1]; +#endif + append(buffered, take); + strm_.consume_buffered(take); + i += take; + if (terminated) { return true; } + continue; + } + if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) { // Treat exceptionally long lines as an error to // prevent infinite loops/memory exhaustion @@ -1443,16 +1483,26 @@ bool stream_line_reader::getline() { return true; } -void stream_line_reader::append(char c) { - if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { - fixed_buffer_[fixed_buffer_used_size_++] = c; +void stream_line_reader::append(char c) { append(&c, 1); } + +void stream_line_reader::append(const char *data, size_t size) { + // Once the line has outgrown the fixed buffer everything must keep going to + // the growable one, even if a later chunk would have fit. Without the + // emptiness check a short append after a long one would land in the fixed + // buffer, which ptr() and size() no longer look at, and be lost. + if (growable_buffer_.empty() && + fixed_buffer_used_size_ + size < fixed_buffer_size_) { + memcpy(fixed_buffer_ + fixed_buffer_used_size_, data, size); + fixed_buffer_used_size_ += size; fixed_buffer_[fixed_buffer_used_size_] = '\0'; } else { + // Unlike the per-character overload, this can be the very first append of + // the line, so the fixed buffer may hold nothing and carry no terminator + // yet. assign() takes an explicit length and does not need one. if (growable_buffer_.empty()) { - assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); } - growable_buffer_ += c; + growable_buffer_.append(data, size); } } @@ -1525,6 +1575,14 @@ bool mmap::open(const char *path) { is_open_empty_file = true; return false; } + + if (addr_ == MAP_FAILED) { + // Clear the sentinel before `close()`, since `is_open()` only checks + // `addr_` against nullptr and `munmap()` must not be called with it. + addr_ = nullptr; + close(); + return false; + } #endif return true; @@ -1702,8 +1760,17 @@ class SocketStream final : public Stream { socket_t socket() const override; time_t duration() const override; void set_read_timeout(time_t sec, time_t usec = 0) override; + const char *buffered_data(size_t &size) const override; + void consume_buffered(size_t size) override; + + // The caller has just seen this socket become readable. Lets the next read + // skip its own readiness wait, which would otherwise ask the kernel a + // question that was answered a moment ago. Consumed by that read. + void set_readable_hint() { readable_hint_ = true; } private: + bool ensure_readable(); + socket_t sock_; time_t read_timeout_sec_; time_t read_timeout_usec_; @@ -1715,6 +1782,7 @@ class SocketStream final : public Stream { std::vector read_buff_; size_t read_buff_off_ = 0; size_t read_buff_content_size_ = 0; + bool readable_hint_ = false; static const size_t read_buff_size_ = 1024l * 4; }; @@ -1782,6 +1850,9 @@ process_server_socket(const std::atomic &svr_sock, socket_t sock, [&](bool close_connection, bool &connection_closed) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); + // process_server_socket_core() only gets here once keep_alive() has + // seen the socket go readable. + strm.set_readable_hint(); return callback(strm, close_connection, connection_closed); }); } @@ -3071,19 +3142,49 @@ bool zstd_decompressor::decompress(const char *data, size_t data_length, } #endif +bool contains_case_ignore(const std::string &s, const char *token) { + auto token_end = token + std::strlen(token); + return std::search(s.begin(), s.end(), token, token_end, [](char a, char b) { + return case_ignore::to_lower(a) == case_ignore::to_lower(b); + }) != s.end(); +} + +// Content codings are case-insensitive (RFC 9110 8.4.1). Matching them +// case-sensitively would make a response labeled e.g. "GZIP" look like an +// unknown coding, and its payload would be handed back still compressed. +bool is_zlib_encoding(const std::string &encoding) { + return case_ignore::equal(encoding, "gzip") || + case_ignore::equal(encoding, "deflate"); +} + +bool is_brotli_encoding(const std::string &encoding) { + return contains_case_ignore(encoding, "br"); +} + +bool is_zstd_encoding(const std::string &encoding) { + return contains_case_ignore(encoding, "zstd"); +} + +// Returns true if the content coding is one cpp-httplib is able to decompress +// when the corresponding support is compiled in. +bool is_known_content_encoding(const std::string &encoding) { + return is_zlib_encoding(encoding) || is_brotli_encoding(encoding) || + is_zstd_encoding(encoding); +} + std::unique_ptr create_decompressor(const std::string &encoding) { std::unique_ptr decompressor; - if (encoding == "gzip" || encoding == "deflate") { + if (is_zlib_encoding(encoding)) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT decompressor = detail::make_unique(); #endif - } else if (encoding.find("br") != std::string::npos) { + } else if (is_brotli_encoding(encoding)) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT decompressor = detail::make_unique(); #endif - } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) { + } else if (is_zstd_encoding(encoding)) { #ifdef CPPHTTPLIB_ZSTD_SUPPORT decompressor = detail::make_unique(); #endif @@ -3145,8 +3246,7 @@ const char *get_header_value(const Headers &headers, size_t get_header_value_count(const Headers &headers, const std::string &key) { - auto r = headers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return headers.count(key); } template @@ -3370,44 +3470,33 @@ ReadContentResult read_content_chunked(Stream &strm, T &x, bool is_chunked_transfer_encoding(const Headers &headers) { // RFC 9112 6.1: a message is framed with the chunked coding when "chunked" // is the final transfer coding. A single field value may list several - // codings ("gzip, chunked"), and the list may be split across multiple - // Transfer-Encoding header lines (RFC 9110 5.3). Match the last coding token - // case-insensitively rather than comparing the whole value against "chunked". + // codings ("gzip, chunked"), and RFC 9110 5.3 lets that list be split across + // several Transfer-Encoding lines, which combine into one comma-separated + // list in the order the lines were received. Headers preserves that order, + // so the final coding is the last token of the last line. Match it + // case-insensitively rather than comparing the whole value against + // "chunked". // // Security: reading a chunked message as unframed leaves its body in the // socket, where a keep-alive connection parses it as a smuggled request. - // Headers is an unordered_multimap whose iteration order for duplicate keys - // is not portable, so when there is more than one Transfer-Encoding line we - // cannot tell which coding is truly final. In that ambiguous case we fail - // safe by treating the message as chunked (a mis-parse just closes the - // connection, whereas the opposite error enables smuggling). + // Server::process_request() answers 400 and closes when the final coding is + // not chunked, so a request whose framing cannot be determined never + // reaches the "no body" path. auto rng = headers.equal_range("Transfer-Encoding"); + if (rng.first == rng.second) { return false; } - size_t line_count = 0; - bool chunked_present = false; - bool last_line_ends_with_chunked = false; + // Cleared per line, so a trailing line carrying no coding at all leaves the + // combined list ending in nothing rather than inheriting the line before it. + std::string last_coding; for (auto it = rng.first; it != rng.second; ++it) { - line_count++; const auto &value = it->second; - - std::string last_coding; - bool line_has_chunked = false; + last_coding.clear(); split(value.data(), value.data() + value.size(), ',', - [&](const char *b, const char *e) { - last_coding.assign(b, e); - if (case_ignore::equal(last_coding, "chunked")) { - line_has_chunked = true; - } - }); - - if (line_has_chunked) { chunked_present = true; } - last_line_ends_with_chunked = case_ignore::equal(last_coding, "chunked"); + [&](const char *b, const char *e) { last_coding.assign(b, e); }); } - if (line_count == 0) { return false; } - if (line_count == 1) { return last_line_ends_with_chunked; } - return chunked_present; + return case_ignore::equal(last_coding, "chunked"); } template @@ -3420,9 +3509,12 @@ bool prepare_content_receiver(T &x, int &status, std::unique_ptr decompressor; if (!encoding.empty()) { + // A coding we know about but were not built with is an error. An + // unrecognized coding (including "identity") is left alone and the + // payload is passed through as-is, since some servers misuse the header, + // e.g. by sending a character set such as "Content-Encoding: UTF-8". decompressor = detail::create_decompressor(encoding); - if (!decompressor) { - // Unsupported encoding or no support compiled in + if (!decompressor && detail::is_known_content_encoding(encoding)) { status = StatusCode::UnsupportedMediaType_415; return false; } @@ -3845,6 +3937,19 @@ std::string params_to_query_str(const Params ¶ms) { return query; } +// Splits one "key=value" span of a query string at its first '='. A span with +// no '=' at all lands entirely in key, leaving val empty, which is how a bare +// "?flag" keeps its name. +void divide_query_pair(const char *b, const char *e, std::string &key, + std::string &val) { + divide(b, static_cast(e - b), '=', + [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, + std::size_t rhs_size) { + key.assign(lhs_data, lhs_size); + val.assign(rhs_data, rhs_size); + }); +} + void parse_query_text(const char *data, std::size_t size, Params ¶ms) { std::set cache; @@ -3855,12 +3960,7 @@ void parse_query_text(const char *data, std::size_t size, std::string key; std::string val; - divide(b, static_cast(e - b), '=', - [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data, - std::size_t rhs_size) { - key.assign(lhs_data, lhs_size); - val.assign(rhs_data, rhs_size); - }); + divide_query_pair(b, e, key, val); if (!key.empty()) { params.emplace(decode_query_component(key), decode_query_component(val)); @@ -3874,20 +3974,18 @@ void parse_query_text(const std::string &s, Params ¶ms) { // Normalize a query string by decoding and re-encoding each key/value pair // while preserving the original parameter order. This avoids double-encoding -// and ensures consistent encoding without reordering (unlike Params which -// uses std::multimap and sorts keys). +// and ensures consistent encoding. It works on the raw string rather than +// parsing into Params and re-serializing, because that round trip cannot +// reproduce the input: params_to_query_str() always emits '=', so a bare +// "flag" would come back as "flag=", and parse_query_text() drops exactly +// duplicated pairs. std::string normalize_query_string(const std::string &query) { std::string result; split(query.data(), query.data() + query.size(), '&', [&](const char *b, const char *e) { std::string key; std::string val; - divide(b, static_cast(e - b), '=', - [&](const char *lhs_data, std::size_t lhs_size, - const char *rhs_data, std::size_t rhs_size) { - key.assign(lhs_data, lhs_size); - val.assign(rhs_data, rhs_size); - }); + divide_query_pair(b, e, key, val); if (!key.empty()) { auto dec_key = decode_query_component(key); @@ -3904,6 +4002,43 @@ std::string normalize_query_string(const std::string &query) { return result; } +// Build the request target that goes on the wire from a caller-supplied path. +// Shared by the buffered send path and the streaming API so that both put the +// same bytes in the request line for the same input. +std::string encode_request_target(const std::string &target, + bool path_encode) { + // `substr(0, npos)` yields the whole string, which is what the no-query + // case needs. + auto query_pos = target.find('?'); + auto path_part = target.substr(0, query_pos); + std::string query_part; + if (query_pos != std::string::npos) { + query_part = target.substr(query_pos + 1); + } + + auto result = path_encode ? encode_path(path_part) : std::move(path_part); + + if (!query_part.empty()) { + // When path encoding is disabled the caller has supplied an already-encoded + // target and expects the exact bytes to be sent on the wire, so skip + // normalization for the query too. Normalizing would decode-then-re-encode + // it and corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`, + // which a strict RFC 3986 server decodes back as `+`, not a space). + if (path_encode) { + auto normalized = normalize_query_string(query_part); + if (!normalized.empty()) { + result += '?'; + result += normalized; + } + } else { + result += '?'; + result += query_part; + } + } + + return result; +} + bool parse_multipart_boundary(const std::string &content_type, std::string &boundary) { std::map params; @@ -4969,21 +5104,8 @@ bool is_field_valid(const std::string &name, const std::string &value) { } // namespace fields -bool perform_websocket_handshake(Stream &strm, const std::string &host, - int port, bool is_ssl, - const std::string &path, - const Headers &headers, +bool perform_websocket_handshake(Stream &strm, Request &req, std::string &selected_subprotocol) { - // Validate path and host - if (!fields::is_field_value(path) || !fields::is_field_value(host)) { - return false; - } - - // Validate user-provided headers - for (const auto &h : headers) { - if (!fields::is_field_valid(h.first, h.second)) { return false; } - } - // Generate random Sec-WebSocket-Key thread_local std::mt19937 rng(std::random_device{}()); std::string key_bytes(16, '\0'); @@ -4993,19 +5115,30 @@ bool perform_websocket_handshake(Stream &strm, const std::string &host, } auto client_key = base64_encode(key_bytes); - // Build upgrade request - std::string req_str = "GET " + path + " HTTP/1.1\r\n"; - req_str += "Host: " + make_host_and_port_string(host, port, is_ssl) + "\r\n"; - req_str += "Upgrade: websocket\r\n"; - req_str += "Connection: Upgrade\r\n"; - req_str += "Sec-WebSocket-Key: " + client_key + "\r\n"; - req_str += "Sec-WebSocket-Version: 13\r\n"; - for (const auto &h : headers) { - req_str += h.first + ": " + h.second + "\r\n"; + req.headers.erase("Upgrade"); + req.headers.erase("Connection"); + req.headers.erase("Sec-WebSocket-Key"); + req.headers.erase("Sec-WebSocket-Version"); + req.headers.emplace("Upgrade", "websocket"); + req.headers.emplace("Connection", "Upgrade"); + req.headers.emplace("Sec-WebSocket-Key", client_key); + req.headers.emplace("Sec-WebSocket-Version", "13"); + + // Build the request in memory first, like ClientImpl::write_request does. + // Writing straight to the socket would leak a request line onto the wire + // before check_and_write_headers gets a chance to reject an invalid header, + // and would emit one small write per header. + BufferStream bstrm; + + if (write_request_line(bstrm, req.method, req.path) < 0) { return false; } + + auto error = Error::Success; + if (!check_and_write_headers(bstrm, req.headers, write_headers, error)) { + return false; } - req_str += "\r\n"; - if (strm.write(req_str.data(), req_str.size()) < 0) { return false; } + const auto &data = bstrm.get_buffer(); + if (!write_data(strm, data.data(), data.size())) { return false; } // Verify 101 response and Sec-WebSocket-Accept header auto expected_accept = websocket_accept_key(client_key); @@ -5013,6 +5146,39 @@ bool perform_websocket_handshake(Stream &strm, const std::string &host, selected_subprotocol); } +bool is_ip_address(const std::string &host) { + struct in_addr addr4; + struct in6_addr addr6; + return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || + inet_pton(AF_INET6, host.c_str(), &addr6) == 1; +} + +// Resolve where a client should connect for `host`, honoring a user-supplied +// hostname-to-address map. `host` itself is never rewritten, so it keeps +// supplying the Host header and SNI; only the connection target changes. +// +// A mapped IP literal goes to `ip`, which keeps create_socket's AI_NUMERICHOST +// path. Anything else goes to `connect_host`, which create_socket resolves as +// a name, or uses as the socket path when the address family is AF_UNIX. An +// absent or empty mapping leaves `host` as the connection target; without the +// empty check the value would reach getaddrinfo as a null node and silently +// resolve to loopback. +void apply_addr_map(const std::map &addr_map, + const std::string &host, std::string &connect_host, + std::string &ip) { + connect_host = host; + ip.clear(); + + auto it = addr_map.find(host); + if (it == addr_map.end() || it->second.empty()) { return; } + + if (is_ip_address(it->second)) { + ip = it->second; + } else { + connect_host = it->second; + } +} + } // namespace detail /* @@ -5044,7 +5210,12 @@ class SSLSocketStream final : public Stream { time_t duration() const override; void set_read_timeout(time_t sec, time_t usec = 0) override; + // See SocketStream::set_readable_hint(). + void set_readable_hint() { readable_hint_ = true; } + private: + bool ensure_readable(); + socket_t sock_; tls::session_t session_; time_t read_timeout_sec_; @@ -5053,6 +5224,7 @@ class SSLSocketStream final : public Stream { time_t write_timeout_usec_; time_t max_timeout_msec_; const std::chrono::time_point start_time_; + bool readable_hint_ = false; }; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT @@ -5196,13 +5368,6 @@ std::string SHA_512(const std::string &s) { } #endif -bool is_ip_address(const std::string &host) { - struct in_addr addr4; - struct in6_addr addr6; - return inet_pton(AF_INET, host.c_str(), &addr4) == 1 || - inet_pton(AF_INET6, host.c_str(), &addr6) == 1; -} - template bool process_server_socket_ssl( const std::atomic &svr_sock, tls::session_t session, @@ -5214,6 +5379,8 @@ bool process_server_socket_ssl( [&](bool close_connection, bool &connection_closed) { SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); + // See the non-TLS path in process_server_socket(). + strm.set_readable_hint(); return callback(strm, close_connection, connection_closed); }); } @@ -5665,6 +5832,7 @@ std::string to_string(const Error error) { case Error::UnsupportedAddressFamily: return "Unsupported address family"; case Error::HTTPParsing: return "HTTP parsing failed"; case Error::InvalidRangeHeader: return "Invalid Range header"; + case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding"; default: break; } @@ -6046,8 +6214,7 @@ std::string Request::get_trailer_value(const std::string &key, } size_t Request::get_trailer_value_count(const std::string &key) const { - auto r = trailers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return trailers.count(key); } bool Request::has_param(const std::string &key) const { @@ -6071,8 +6238,7 @@ Request::get_param_values(const std::string &key) const { } size_t Request::get_param_value_count(const std::string &key) const { - auto r = params.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return params.count(key); } bool Request::is_multipart_form_data() const { @@ -6105,8 +6271,7 @@ bool MultipartFormData::has_field(const std::string &key) const { } size_t MultipartFormData::get_field_count(const std::string &key) const { - auto r = fields.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return fields.count(key); } FormData MultipartFormData::get_file(const std::string &key, @@ -6129,8 +6294,7 @@ bool MultipartFormData::has_file(const std::string &key) const { } size_t MultipartFormData::get_file_count(const std::string &key) const { - auto r = files.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return files.count(key); } // Multipart FormData writer implementation @@ -6209,8 +6373,7 @@ std::string Response::get_trailer_value(const std::string &key, } size_t Response::get_trailer_value_count(const std::string &key) const { - auto r = trailers.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return trailers.count(key); } void Response::set_redirect(const std::string &url, int stat) { @@ -6306,8 +6469,7 @@ std::string Result::get_request_header_value(const std::string &key, size_t Result::get_request_header_value_count(const std::string &key) const { - auto r = request_headers_.equal_range(key); - return static_cast(std::distance(r.first, r.second)); + return request_headers_.count(key); } // Stream implementation @@ -6595,6 +6757,24 @@ bool SocketStream::wait_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0; } +bool SocketStream::ensure_readable() { + if (readable_hint_) { + readable_hint_ = false; + return true; + } + return wait_readable(); +} + +const char *SocketStream::buffered_data(size_t &size) const { + size = read_buff_content_size_ - read_buff_off_; + return size ? read_buff_.data() + read_buff_off_ : nullptr; +} + +void SocketStream::consume_buffered(size_t size) { + assert(size <= read_buff_content_size_ - read_buff_off_); + read_buff_off_ += size; +} + bool SocketStream::is_peer_alive() const { return detail::is_socket_alive(sock_); } @@ -6621,7 +6801,7 @@ ssize_t SocketStream::read(char *ptr, size_t size) { } } - if (!wait_readable()) { + if (!ensure_readable()) { error_ = Error::Timeout; return -1; } @@ -7099,6 +7279,14 @@ bool SSLSocketStream::wait_writable() const { !tls::is_peer_closed(session_, sock_); } +bool SSLSocketStream::ensure_readable() { + if (readable_hint_) { + readable_hint_ = false; + return true; + } + return wait_readable(); +} + bool SSLSocketStream::is_peer_alive() const { return !tls::is_peer_closed(session_, sock_); } @@ -7111,7 +7299,7 @@ ssize_t SSLSocketStream::read(char *ptr, size_t size) { error_ = Error::ConnectionClosed; } return ret; - } else if (wait_readable()) { + } else if (ensure_readable()) { tls::TlsError err; auto ret = tls::read(session_, ptr, size, err); if (ret < 0) { @@ -7533,9 +7721,11 @@ void Server::wait_until_ready() const { } void Server::stop() noexcept { - if (is_running_) { - assert(svr_sock_ != INVALID_SOCKET); - std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); + // Release the listening socket whether or not the accept loop is running: + // bind_to_port() without listen_after_bind() still owns the descriptor. The + // exchange is what makes this safe to call concurrently with the accept loop. + socket_t sock = svr_sock_.exchange(INVALID_SOCKET); + if (sock != INVALID_SOCKET) { detail::shutdown_socket(sock); detail::close_socket(sock); } @@ -7697,7 +7887,15 @@ Server::write_content_with_provider(Stream &strm, const Request &req, }; if (res.content_length_ > 0) { - if (req.ranges.empty()) { + // Only a 206 response is served as a partial representation, matching the + // condition `apply_ranges()` used to decide the Content-Length and the + // multipart boundary. Since `detail::range_error()` validates `req.ranges` + // only for a 2xx status, slicing under any other status would write a body + // that disagrees with the header already sent, from an unchecked offset. + auto is_partial = + !req.ranges.empty() && res.status == StatusCode::PartialContent_206; + + if (!is_partial) { return detail::write_content(strm, res.content_provider_, 0, res.content_length_, is_shutting_down); } else if (req.ranges.size() == 1) { @@ -8096,7 +8294,14 @@ int Server::bind_internal(const std::string &host, int port, } bool Server::listen_internal() { - if (is_decommissioned) { return false; } + // A stop() between bind and listen leaves nothing to accept on. Report + // failure instead of returning success without ever serving, and mark the + // server decommissioned the way any failed listen does so that a concurrent + // wait_until_ready() wakes up instead of spinning forever. + if (is_decommissioned || svr_sock_ == INVALID_SOCKET) { + is_decommissioned = true; + return false; + } auto ret = true; is_running_ = true; @@ -8492,11 +8697,17 @@ Server::process_request(Stream &strm, const std::string &remote_addr, return write_response(strm, close_connection, req, res); } - // RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and - // any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is - // tolerated for compatibility with existing clients. - if (req.get_header_value_u64("Content-Length") > 0 && - req.has_header("Transfer-Encoding")) { + // RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would + // otherwise let an intermediary and this parser disagree on where the body + // ends and enable request smuggling. Two cases: a non-zero Content-Length + // alongside any Transfer-Encoding (Content-Length: 0 is tolerated for + // compatibility with existing clients), and a Transfer-Encoding whose final + // coding is not chunked, which leaves the body length undeterminable. The + // latter must not fall through to the "no body" path, or the body bytes are + // parsed as the next request on a persistent connection. + if (req.has_header("Transfer-Encoding") && + (req.get_header_value_u64("Content-Length") > 0 || + !detail::is_chunked_transfer_encoding(req.headers))) { connection_closed = true; res.status = StatusCode::BadRequest_400; return write_response(strm, close_connection, req, res); @@ -8908,13 +9119,13 @@ socket_t ClientImpl::create_client_socket(Error &error) const { write_timeout_sec_, write_timeout_usec_, interface_, error); } - // Check is custom IP specified for host_ + // Check is custom IP or hostname specified for host_ + std::string connect_host; std::string ip; - auto it = addr_map_.find(host_); - if (it != addr_map_.end()) { ip = it->second; } + detail::apply_addr_map(addr_map_, host_, connect_host, ip); return detail::create_client_socket( - host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, + connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, socket_options_, connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, error); @@ -9142,11 +9353,13 @@ void ClientImpl::prepare_default_headers(Request &r, bool for_stream, if (!r.has_header(header.first)) { r.headers.insert(header); } } + // RFC 9110 5.3 recommends sending control data such as Host first, so + // prepend it rather than appending it after the caller's own fields. if (!r.has_header("Host")) { if (address_family_ == AF_UNIX) { - r.headers.emplace("Host", "localhost"); + r.headers.emplace_front("Host", "localhost"); } else { - r.headers.emplace( + r.headers.emplace_front( "Host", detail::make_host_and_port_string(host_, port_, is_ssl())); } } @@ -9197,7 +9410,12 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, handle.response = detail::make_unique(); handle.error = Error::Success; - auto query_path = params.empty() ? path : append_query_params(path, params); + // Encode the target exactly like the buffered send path does, so that the + // same `path` produces the same request line through either API. + auto raw_query_path = + params.empty() ? path : append_query_params(path, params); + auto query_path = detail::encode_request_target(raw_query_path, path_encode_); + handle.connection_ = detail::make_unique(); { @@ -9311,7 +9529,20 @@ ClientImpl::open_stream(const std::string &method, const std::string &path, auto content_encoding = handle.response->get_header_value("Content-Encoding"); if (!content_encoding.empty()) { + // Same policy as prepare_content_receiver(): reject a coding we know about + // but were not built with, pass an unrecognized one through as-is. handle.decompressor_ = detail::create_decompressor(content_encoding); + if (!handle.decompressor_) { + if (detail::is_known_content_encoding(content_encoding)) { + handle.error = Error::UnsupportedContentEncoding; + handle.response.reset(); + return handle; + } + } else if (!handle.decompressor_->is_valid()) { + handle.error = Error::Compression; + handle.response.reset(); + return handle; + } } return handle; @@ -9842,52 +10073,26 @@ bool ClientImpl::write_request(Stream &strm, Request &req, { detail::BufferStream bstrm; - // Extract path and query from req.path - std::string path_part, query_part; + // Extract the query from req.path. The encoding itself is delegated to + // `encode_request_target`; the raw query is still needed here to decide + // between populating `req.params` from it and falling back to building a + // query out of caller-supplied `req.params`. auto query_pos = req.path.find('?'); - if (query_pos != std::string::npos) { - path_part = req.path.substr(0, query_pos); - query_part = req.path.substr(query_pos + 1); - } else { - path_part = req.path; - query_part = ""; - } + auto query_part = query_pos == std::string::npos + ? std::string() + : req.path.substr(query_pos + 1); - // Encode path part. If the original `req.path` already contained a - // query component, preserve its raw query string (including parameter - // order) instead of reparsing and reassembling it which may reorder - // parameters due to container ordering (e.g. `Params` uses - // `std::multimap`). When there is no query in `req.path`, fall back to - // building a query from `req.params` so existing callers that pass - // `Params` continue to work. auto path_with_query = - path_encode_ ? detail::encode_path(path_part) : path_part; + detail::encode_request_target(req.path, path_encode_); if (!query_part.empty()) { - // Normalize the query string (decode then re-encode) while preserving - // the original parameter order. When path encoding is disabled the - // caller has supplied an already-encoded target and expects the exact - // bytes to be sent on the wire, so skip normalization for the query - // too. Normalizing here would decode-then-re-encode the query and - // corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`, - // which a strict RFC 3986 server decodes back as `+`, not a space). - if (path_encode_) { - auto normalized = detail::normalize_query_string(query_part); - if (!normalized.empty()) { path_with_query += '?' + normalized; } - } else { - path_with_query += '?' + query_part; - } - - // Still populate req.params for handlers/users who read them. + // The query already came in through `req.path`; still populate + // `req.params` for handlers/users who read them. detail::parse_query_text(query_part, req.params); - } else { - // No query in path; parse any query_part (empty) and append params - // from `req.params` when present (preserves prior behavior for - // callers who provide Params separately). - detail::parse_query_text(query_part, req.params); - if (!req.params.empty()) { - path_with_query = append_query_params(path_with_query, req.params); - } + } else if (!req.params.empty()) { + // No query in `req.path`; build one from `req.params` so existing + // callers that pass `Params` separately continue to work. + path_with_query = append_query_params(path_with_query, req.params); } // Write request line and headers @@ -10298,14 +10503,26 @@ bool ClientImpl::process_request(Stream &strm, Request &req, } if (res.status != StatusCode::NotModified_304) { - int dummy_status; + auto content_status = 0; auto max_length = (!has_payload_max_length_ && req.content_receiver) ? (std::numeric_limits::max)() : payload_max_length_; - if (!detail::read_content(strm, res, max_length, dummy_status, + if (!detail::read_content(strm, res, max_length, content_status, std::move(progress), std::move(out), decompress_)) { - if (error != Error::Canceled) { error = Error::Read; } + if (error != Error::Canceled) { + // Tell the caller apart from a plain read failure when the body could + // not be decoded because of its Content-Encoding. + switch (content_status) { + case StatusCode::UnsupportedMediaType_415: + error = Error::UnsupportedContentEncoding; + break; + case StatusCode::InternalServerError_500: + error = Error::Compression; + break; + default: error = Error::Read; break; + } + } output_error_log(error, &req); return false; } @@ -16769,18 +16986,42 @@ bool WebSocketClient::create_stream(std::unique_ptr &strm) { return true; } +void WebSocketClient::prepare_default_headers(Request &req) { +#ifdef CPPHTTPLIB_SSL_ENABLED + auto is_ssl = is_ssl_; +#else + auto is_ssl = false; +#endif + + if (!req.has_header("Host")) { + if (address_family_ == AF_UNIX) { + req.headers.emplace("Host", "localhost"); + } else { + req.headers.emplace( + "Host", detail::make_host_and_port_string(host_, port_, is_ssl)); + } + } + +#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT + if (!req.has_header("User-Agent")) { + auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; + req.set_header("User-Agent", agent); + } +#endif +} + bool WebSocketClient::connect() { if (!is_valid_) { return false; } shutdown_and_close(); - // Check is custom IP specified for host_ + // Check is custom IP or hostname specified for host_ + std::string connect_host; std::string ip; - auto it = addr_map_.find(host_); - if (it != addr_map_.end()) { ip = it->second; } + detail::apply_addr_map(addr_map_, host_, connect_host, ip); Error error; sock_ = detail::create_client_socket( - host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, + connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_, socket_options_, connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, error); @@ -16793,23 +17034,19 @@ bool WebSocketClient::connect() { return false; } -#ifdef CPPHTTPLIB_SSL_ENABLED - auto is_ssl = is_ssl_; -#else - auto is_ssl = false; -#endif + Request req; + req.method = "GET"; + req.path = path_; + req.headers = headers_; + prepare_default_headers(req); std::string selected_subprotocol; - if (!detail::perform_websocket_handshake(*strm, host_, port_, is_ssl, path_, - headers_, selected_subprotocol)) { + if (!detail::perform_websocket_handshake(*strm, req, selected_subprotocol)) { shutdown_and_close(); return false; } subprotocol_ = std::move(selected_subprotocol); - Request req; - req.method = "GET"; - req.path = path_; ws_ = std::unique_ptr(new WebSocket(std::move(strm), req, false, websocket_ping_interval_sec_, websocket_max_missed_pongs_)); diff --git a/vendor/cpp-httplib/httplib.h b/vendor/cpp-httplib/httplib.h index 6df357d706c9..94605defa31f 100644 --- a/vendor/cpp-httplib/httplib.h +++ b/vendor/cpp-httplib/httplib.h @@ -8,8 +8,8 @@ #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H -#define CPPHTTPLIB_VERSION "0.51.0" -#define CPPHTTPLIB_VERSION_NUM "0x003300" +#define CPPHTTPLIB_VERSION "0.52.0" +#define CPPHTTPLIB_VERSION_NUM "0x003400" #ifdef _WIN32 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 @@ -182,7 +182,7 @@ #endif #ifndef CPPHTTPLIB_LISTEN_BACKLOG -#define CPPHTTPLIB_LISTEN_BACKLOG 5 +#define CPPHTTPLIB_LISTEN_BACKLOG 128 #endif #ifndef CPPHTTPLIB_MAX_LINE_LENGTH @@ -321,6 +321,7 @@ using socket_t = int; #include #include #include +#include #include #include #include @@ -333,9 +334,11 @@ using socket_t = int; #include #include #include +#include #include #include #include +#include // On macOS with a TLS backend, enable Keychain root certificates by default // unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since @@ -968,11 +971,291 @@ enum StatusCode { NetworkAuthenticationRequired_511 = 511, }; +namespace detail { + +// A multimap that keeps its entries in the order they were inserted. +// +// HTTP needs that order in two places. RFC 9110 5.3 makes the order of header +// fields sharing a field name significant and forbids a proxy from reordering +// them, and a query string's parameters are meaningful in the order the caller +// wrote them. Neither standard container expresses it: std::unordered_multimap +// gives no ordering guarantee at all for equivalent keys (libstdc++ yields +// reverse insertion order, libc++ insertion order), and std::multimap sorts by +// key, which would drop control data such as Host behind whatever else the +// message carries and alphabetise a query string. +// +// Entries are therefore kept in a flat vector, in order. Lookup is a linear +// scan, which beats hashing for the handful of entries a message carries +// (headers are capped at CPPHTTPLIB_HEADER_MAX_COUNT). +// +// KeyEqual compares keys; it is what makes Headers case-insensitive and +// Params, whose parameter names are case-sensitive, not. +template class insertion_ordered_multimap { +public: + using key_type = std::string; + using mapped_type = Mapped; + using value_type = std::pair; + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using reference = value_type &; + using const_reference = const value_type &; + +private: + static size_type npos() { return static_cast(-1); } + + static bool keys_equal(const std::string &a, const std::string &b) { + return KeyEqual()(a, b); + } + + // Iterating yields every entry in insertion order, but equal_range() and + // find() have to walk only the entries sharing one key, which are not + // adjacent. Both are the same iterator type: key_idx_ selects between the + // two traversals, and since equality compares only the position, an iterator + // restricted to one key still compares equal to end(). + template class iterator_t { + public: + using iterator_category = std::bidirectional_iterator_tag; + using value_type = insertion_ordered_multimap::value_type; + using difference_type = insertion_ordered_multimap::difference_type; + using pointer = V *; + using reference = V &; + + iterator_t() : data_(nullptr), idx_(0), size_(0), key_idx_(npos()) {} + + template ::value, + int>::type = 0> + iterator_t(const iterator_t &rhs) + : data_(rhs.data_), idx_(rhs.idx_), size_(rhs.size_), + key_idx_(rhs.key_idx_) {} + + reference operator*() const { return data_[idx_]; } + pointer operator->() const { return data_ + idx_; } + + iterator_t &operator++() { + // Saturating, so that advancing past the last entry of a key (which + // get_multimap_value() does when asked for an out-of-range id) stays at + // end() instead of running off the container. + if (idx_ >= size_) { return *this; } + ++idx_; + if (key_idx_ != npos()) { + while (idx_ < size_ && !matches(idx_)) { + ++idx_; + } + } + return *this; + } + + iterator_t operator++(int) { + auto tmp = *this; + ++*this; + return tmp; + } + + iterator_t &operator--() { + if (idx_ == 0) { return *this; } + --idx_; + if (key_idx_ != npos()) { + while (idx_ > 0 && !matches(idx_)) { + --idx_; + } + } + return *this; + } + + iterator_t operator--(int) { + auto tmp = *this; + --*this; + return tmp; + } + + template bool operator==(const iterator_t &rhs) const { + return idx_ == rhs.idx_; + } + + template bool operator!=(const iterator_t &rhs) const { + return idx_ != rhs.idx_; + } + + private: + friend class insertion_ordered_multimap; + template friend class iterator_t; + + iterator_t(V *data, size_type idx, size_type size, size_type key_idx) + : data_(data), idx_(idx), size_(size), key_idx_(key_idx) {} + + bool matches(size_type i) const { + return keys_equal(data_[i].first, data_[key_idx_].first); + } + + V *data_; + size_type idx_; + size_type size_; + size_type key_idx_; + }; + +public: + using iterator = iterator_t; + using const_iterator = iterator_t; + + insertion_ordered_multimap() = default; + insertion_ordered_multimap(std::initializer_list il) + : entries_(il) {} + template + insertion_ordered_multimap(InputIt first, InputIt last) + : entries_(first, last) {} + + iterator begin() { return make_iter(0, npos()); } + iterator end() { return make_iter(entries_.size(), npos()); } + const_iterator begin() const { return make_citer(0, npos()); } + const_iterator end() const { return make_citer(entries_.size(), npos()); } + const_iterator cbegin() const { return begin(); } + const_iterator cend() const { return end(); } + + bool empty() const { return entries_.empty(); } + size_type size() const { return entries_.size(); } + void clear() { entries_.clear(); } + void swap(insertion_ordered_multimap &rhs) { entries_.swap(rhs.entries_); } + + iterator insert(const value_type &val) { + entries_.push_back(val); + return make_iter(entries_.size() - 1, npos()); + } + + iterator insert(value_type &&val) { + entries_.push_back(std::move(val)); + return make_iter(entries_.size() - 1, npos()); + } + + template iterator emplace(Args &&...args) { + entries_.emplace_back(std::forward(args)...); + return make_iter(entries_.size() - 1, npos()); + } + + // For entries that have to lead the message, such as the Host header field + // (RFC 9110 5.3 recommends sending control data first). + template iterator emplace_front(Args &&...args) { + entries_.emplace(entries_.begin(), std::forward(args)...); + return make_iter(0, npos()); + } + + iterator find(const std::string &key) { + auto i = index_of(key); + return i == npos() ? end() : make_iter(i, i); + } + + const_iterator find(const std::string &key) const { + auto i = index_of(key); + return i == npos() ? end() : make_citer(i, i); + } + + size_type count(const std::string &key) const { + size_type n = 0; + for (const auto &entry : entries_) { + if (keys_equal(entry.first, key)) { n++; } + } + return n; + } + + std::pair equal_range(const std::string &key) { + auto i = index_of(key); + return i == npos() ? std::make_pair(end(), end()) + : std::make_pair(make_iter(i, i), end()); + } + + std::pair + equal_range(const std::string &key) const { + auto i = index_of(key); + return i == npos() ? std::make_pair(end(), end()) + : std::make_pair(make_citer(i, i), end()); + } + + size_type erase(const std::string &key) { + auto before = entries_.size(); + entries_.erase(std::remove_if(entries_.begin(), entries_.end(), + [&](const value_type &entry) { + return keys_equal(entry.first, key); + }), + entries_.end()); + return before - entries_.size(); + } + + iterator erase(const_iterator pos) { + entries_.erase(entries_.begin() + static_cast(pos.idx_)); + return make_iter(pos.idx_, npos()); + } + + // Erases what iterating [first, last) would actually visit, so erasing an + // equal_range() removes only the entries with that key, not everything + // positioned between them. + iterator erase(const_iterator first, const_iterator last) { + auto from = first.idx_; + auto to = last.idx_; + if (from >= to) { return make_iter(from, npos()); } + + auto begin_it = entries_.begin(); + auto from_it = begin_it + static_cast(from); + auto to_it = begin_it + static_cast(to); + + if (first.key_idx_ == npos()) { + entries_.erase(from_it, to_it); + } else { + auto key = entries_[first.key_idx_].first; + auto keep = from_it; + for (auto it = from_it; it != to_it; ++it) { + if (!keys_equal(it->first, key)) { + if (keep != it) { *keep = std::move(*it); } + ++keep; + } + } + if (keep != to_it) { + keep = std::move(to_it, entries_.end(), keep); + } else { + keep = entries_.end(); + } + entries_.erase(keep, entries_.end()); + } + return make_iter(from, npos()); + } + + friend bool operator==(const insertion_ordered_multimap &lhs, + const insertion_ordered_multimap &rhs) { + return lhs.entries_ == rhs.entries_; + } + + friend bool operator!=(const insertion_ordered_multimap &lhs, + const insertion_ordered_multimap &rhs) { + return !(lhs == rhs); + } + +private: + size_type index_of(const std::string &key) const { + for (size_type i = 0; i < entries_.size(); i++) { + if (keys_equal(entries_[i].first, key)) { return i; } + } + return npos(); + } + + iterator make_iter(size_type idx, size_type key_idx) { + return iterator(entries_.data(), idx, entries_.size(), key_idx); + } + + const_iterator make_citer(size_type idx, size_type key_idx) const { + return const_iterator(entries_.data(), idx, entries_.size(), key_idx); + } + + std::vector entries_; +}; + +} // namespace detail + using Headers = - std::unordered_multimap; + detail::insertion_ordered_multimap; -using Params = std::multimap; +// Query parameter names are case-sensitive, unlike header field names. +using Params = + detail::insertion_ordered_multimap>; using Match = std::smatch; using DownloadProgress = std::function; @@ -1079,9 +1362,16 @@ struct FormField { std::string content; Headers headers; }; -using FormFields = std::multimap; +// RFC 7578 5.2: a form processor "SHOULD send back results in order" and +// "Intermediaries MUST NOT reorder the results", so a handler walking these +// should see the parts as they were sent. A std::multimap sorts by field name +// and loses that. Field names are case-sensitive, hence std::equal_to rather +// than the case-insensitive predicate Headers uses. +using FormFields = + detail::insertion_ordered_multimap>; -using FormFiles = std::multimap; +using FormFiles = + detail::insertion_ordered_multimap>; struct MultipartFormData { FormFields fields; // Text fields from multipart @@ -1514,6 +1804,7 @@ enum class Error { UnsupportedAddressFamily, HTTPParsing, InvalidRangeHeader, + UnsupportedContentEncoding, // For internal use only SSLPeerCouldBeClosed_, @@ -1545,6 +1836,18 @@ class Stream { (void)usec; } + // Bytes already pulled off the socket and sitting in this stream's own + // buffer. Exposing them lets a line reader scan for a terminator in one + // pass instead of asking for a byte at a time. A stream that does no + // buffering of its own reports none, and readers fall back to read(). + virtual const char *buffered_data(size_t &size) const { + size = 0; + return nullptr; + } + + // Discards `size` bytes previously returned by buffered_data(). + virtual void consume_buffered(size_t size) { (void)size; } + ssize_t write(const char *ptr); ssize_t write(const std::string &s); @@ -2452,7 +2755,8 @@ class ClientImpl { std::thread::id socket_requests_are_from_thread_ = std::thread::id(); bool socket_should_be_closed_when_request_is_done_ = false; - // Hostname-IP map + // Hostname to connection target map. The value is an IP literal or another + // hostname; only the connection target changes, never the identity. std::map addr_map_; // Default headers @@ -3154,6 +3458,10 @@ struct FileStat { std::string make_host_and_port_string(const std::string &host, int port, bool is_ssl); +template +bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer, + Error &error); + std::string trim_copy(const std::string &s); void divide( @@ -3364,6 +3672,7 @@ class stream_line_reader { private: void append(char c); + void append(const char *data, size_t size); Stream &strm_; char *fixed_buffer_; @@ -3992,6 +4301,7 @@ class WebSocketClient { private: void shutdown_and_close(); bool create_stream(std::unique_ptr &strm); + void prepare_default_headers(Request &req); std::string host_; int port_; @@ -4016,7 +4326,8 @@ class WebSocketClient { time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; std::string interface_; - // Hostname-IP map + // Hostname to connection target map. The value is an IP literal or another + // hostname; only the connection target changes, never the identity. std::map addr_map_; #ifdef CPPHTTPLIB_SSL_ENABLED