mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-08-05 17:40:46 -05:00
vendor : update cpp-httplib to 0.52.0 (#26485)
This commit is contained in:
committed by
GitHub
parent
fe2adf0e72
commit
94bc47f280
585
vendor/cpp-httplib/httplib.cpp
vendored
585
vendor/cpp-httplib/httplib.cpp
vendored
@@ -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<const char *>(
|
||||
memchr(buffered + at, '\n', buffered_size - at));
|
||||
if (!nl) { break; }
|
||||
auto pos = static_cast<size_t>(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 @@ public:
|
||||
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 @@ private:
|
||||
std::vector<char> 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<socket_t> &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<decompressor>
|
||||
create_decompressor(const std::string &encoding) {
|
||||
std::unique_ptr<decompressor> decompressor;
|
||||
|
||||
if (encoding == "gzip" || encoding == "deflate") {
|
||||
if (is_zlib_encoding(encoding)) {
|
||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||
decompressor = detail::make_unique<gzip_decompressor>();
|
||||
#endif
|
||||
} else if (encoding.find("br") != std::string::npos) {
|
||||
} else if (is_brotli_encoding(encoding)) {
|
||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||
decompressor = detail::make_unique<brotli_decompressor>();
|
||||
#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<zstd_decompressor>();
|
||||
#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<size_t>(std::distance(r.first, r.second));
|
||||
return headers.count(key);
|
||||
}
|
||||
|
||||
template <typename Map>
|
||||
@@ -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 <typename T, typename U>
|
||||
@@ -3420,9 +3509,12 @@ bool prepare_content_receiver(T &x, int &status,
|
||||
std::unique_ptr<decompressor> 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<std::size_t>(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<std::string> 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<std::size_t>(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<std::size_t>(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<std::string, std::string> 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_str += "\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");
|
||||
|
||||
if (strm.write(req_str.data(), req_str.size()) < 0) { return false; }
|
||||
// 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;
|
||||
}
|
||||
|
||||
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<std::string, std::string> &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 @@ public:
|
||||
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 @@ private:
|
||||
time_t write_timeout_usec_;
|
||||
time_t max_timeout_msec_;
|
||||
const std::chrono::time_point<std::chrono::steady_clock> 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 <typename T>
|
||||
bool process_server_socket_ssl(
|
||||
const std::atomic<socket_t> &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<size_t>(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<size_t>(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<size_t>(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<size_t>(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<size_t>(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<size_t>(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<socket_t> 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<Response>();
|
||||
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<ClientConnection>();
|
||||
|
||||
{
|
||||
@@ -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<size_t>::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<Stream> &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<WebSocket>(new WebSocket(std::move(strm), req, false,
|
||||
websocket_ping_interval_sec_,
|
||||
websocket_max_missed_pongs_));
|
||||
|
||||
Reference in New Issue
Block a user