mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-14 18:02:52 +02:00
vendor : update cpp-httplib to 0.56.0 (#28787)
This commit is contained in:
parent
2a3005c23f
commit
718f7b4175
Vendored
+217
-79
@@ -912,17 +912,42 @@ bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
|
||||
namespace ws {
|
||||
namespace impl {
|
||||
|
||||
bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
std::string &payload, bool &fin,
|
||||
bool expect_masked, size_t max_len) {
|
||||
// Read first 2 bytes
|
||||
// Read exactly `size` bytes. Stream::read may return less than asked for -- it
|
||||
// hands back whatever its buffer already holds -- so every multi-byte field has
|
||||
// to loop. Reading a 2-byte header with a single read() fails whenever the
|
||||
// header straddles the read buffer's boundary.
|
||||
//
|
||||
// Timeout is reported only when nothing at all was consumed. Once a byte has
|
||||
// been taken the stream sits mid-field and cannot be resumed, so a timeout
|
||||
// there is a failure like any other. (When read() fails it always records why,
|
||||
// so the error belongs to this call and not to an earlier one.)
|
||||
FrameRead read_exact(Stream &strm, void *buf, size_t size) {
|
||||
auto p = static_cast<char *>(buf);
|
||||
size_t total = 0;
|
||||
while (total < size) {
|
||||
auto n = strm.read(p + total, size - total);
|
||||
if (n <= 0) {
|
||||
auto timed_out = total == 0 && strm.get_error() == Error::Timeout;
|
||||
return timed_out ? FrameRead::Timeout : FrameRead::Fail;
|
||||
}
|
||||
total += static_cast<size_t>(n);
|
||||
}
|
||||
return FrameRead::Ok;
|
||||
}
|
||||
|
||||
FrameRead read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
std::string &payload, bool &fin,
|
||||
bool expect_masked, size_t max_len) {
|
||||
// Read first 2 bytes. This is the only read that may report a timeout: it
|
||||
// sits on a frame boundary, where nothing has been consumed yet.
|
||||
uint8_t header[2];
|
||||
if (strm.read(reinterpret_cast<char *>(header), 2) != 2) { return false; }
|
||||
FrameRead first = read_exact(strm, header, 2);
|
||||
if (first != FrameRead::Ok) { return first; }
|
||||
|
||||
fin = (header[0] & 0x80) != 0;
|
||||
|
||||
// RSV1, RSV2, RSV3 must be 0 when no extension is negotiated
|
||||
if (header[0] & 0x70) { return false; }
|
||||
if (header[0] & 0x70) { return FrameRead::Fail; }
|
||||
|
||||
opcode = static_cast<Opcode>(header[0] & 0x0F);
|
||||
bool masked = (header[1] & 0x80) != 0;
|
||||
@@ -932,46 +957,44 @@ bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
// MUST have a payload length of 125 bytes or less
|
||||
bool is_control = (static_cast<uint8_t>(opcode) & 0x08) != 0;
|
||||
if (is_control) {
|
||||
if (!fin) { return false; }
|
||||
if (payload_len > 125) { return false; }
|
||||
if (!fin) { return FrameRead::Fail; }
|
||||
if (payload_len > 125) { return FrameRead::Fail; }
|
||||
}
|
||||
|
||||
if (masked != expect_masked) { return false; }
|
||||
if (masked != expect_masked) { return FrameRead::Fail; }
|
||||
|
||||
// Extended payload length
|
||||
if (payload_len == 126) {
|
||||
uint8_t ext[2];
|
||||
if (strm.read(reinterpret_cast<char *>(ext), 2) != 2) { return false; }
|
||||
if (read_exact(strm, ext, 2) != FrameRead::Ok) { return FrameRead::Fail; }
|
||||
payload_len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1];
|
||||
} else if (payload_len == 127) {
|
||||
uint8_t ext[8];
|
||||
if (strm.read(reinterpret_cast<char *>(ext), 8) != 8) { return false; }
|
||||
if (read_exact(strm, ext, 8) != FrameRead::Ok) { return FrameRead::Fail; }
|
||||
// RFC 6455 Section 5.2: the most significant bit MUST be 0
|
||||
if (ext[0] & 0x80) { return false; }
|
||||
if (ext[0] & 0x80) { return FrameRead::Fail; }
|
||||
payload_len = 0;
|
||||
for (int i = 0; i < 8; i++) {
|
||||
payload_len = (payload_len << 8) | ext[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (payload_len > max_len) { return false; }
|
||||
if (payload_len > max_len) { return FrameRead::Fail; }
|
||||
|
||||
// Read mask key if present
|
||||
uint8_t mask_key[4] = {0};
|
||||
if (masked) {
|
||||
if (strm.read(reinterpret_cast<char *>(mask_key), 4) != 4) { return false; }
|
||||
if (read_exact(strm, mask_key, 4) != FrameRead::Ok) {
|
||||
return FrameRead::Fail;
|
||||
}
|
||||
}
|
||||
|
||||
// Read payload
|
||||
payload.resize(static_cast<size_t>(payload_len));
|
||||
if (payload_len > 0) {
|
||||
size_t total_read = 0;
|
||||
while (total_read < payload_len) {
|
||||
auto n = strm.read(&payload[total_read],
|
||||
static_cast<size_t>(payload_len - total_read));
|
||||
if (n <= 0) { return false; }
|
||||
total_read += static_cast<size_t>(n);
|
||||
}
|
||||
if (payload_len > 0 &&
|
||||
read_exact(strm, &payload[0], static_cast<size_t>(payload_len)) !=
|
||||
FrameRead::Ok) {
|
||||
return FrameRead::Fail;
|
||||
}
|
||||
|
||||
// Unmask if needed
|
||||
@@ -981,7 +1004,7 @@ bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return FrameRead::Ok;
|
||||
}
|
||||
|
||||
} // namespace impl
|
||||
@@ -1728,7 +1751,9 @@ ssize_t select_impl(socket_t sock, short events, time_t sec,
|
||||
pfd.events = events;
|
||||
pfd.revents = 0;
|
||||
|
||||
auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
|
||||
// A negative timeout waits forever, poll's own convention. 0 keeps meaning
|
||||
// "return immediately", which callers here rely on to probe a socket.
|
||||
auto timeout = sec < 0 ? -1 : static_cast<int>(sec * 1000 + usec / 1000);
|
||||
|
||||
return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });
|
||||
}
|
||||
@@ -1810,8 +1835,11 @@ private:
|
||||
bool ensure_readable();
|
||||
|
||||
socket_t sock_;
|
||||
time_t read_timeout_sec_;
|
||||
time_t read_timeout_usec_;
|
||||
// Atomic because ws::WebSocket::set_read_timeout() reaches this from another
|
||||
// thread while a read is in flight -- that is the point of it, for a caller
|
||||
// holding one connection and wanting control back to send on it.
|
||||
std::atomic<time_t> read_timeout_sec_;
|
||||
std::atomic<time_t> read_timeout_usec_;
|
||||
time_t write_timeout_sec_;
|
||||
time_t write_timeout_usec_;
|
||||
time_t max_timeout_msec_;
|
||||
@@ -2204,12 +2232,10 @@ int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
// actually finish before letting the stack frame go. The trade-off is that
|
||||
// a wedged DNS server can hold this thread for the system resolver timeout
|
||||
// (~30s by default) past the caller's connection timeout.
|
||||
struct gaicb request {};
|
||||
struct gaicb request{};
|
||||
struct gaicb *requests[1] = {&request};
|
||||
struct sigevent sevp {};
|
||||
struct timespec timeout {
|
||||
timeout_sec, 0
|
||||
};
|
||||
struct sigevent sevp{};
|
||||
struct timespec timeout{timeout_sec, 0};
|
||||
|
||||
request.ar_name = node;
|
||||
request.ar_service = service;
|
||||
@@ -2948,8 +2974,21 @@ EncodingType encoding_type(const Request &req,
|
||||
return best;
|
||||
}
|
||||
|
||||
// `content_type` is taken separately because a file-backed response has not
|
||||
// been given one yet when its coding has to be decided.
|
||||
EncodingType encoding_type(const Request &req, const Response &res,
|
||||
const std::string &content_type) {
|
||||
// The response already names a content coding of its own: a handler serving
|
||||
// a body it encoded itself (pre-compressed static assets, say), or a mount
|
||||
// point whose headers name the coding its files are stored in. Applying one
|
||||
// on top of that would double-encode the body and append a second
|
||||
// `Content-Encoding` field line.
|
||||
if (res.has_header("Content-Encoding")) { return EncodingType::None; }
|
||||
return encoding_type(req, content_type);
|
||||
}
|
||||
|
||||
EncodingType encoding_type(const Request &req, const Response &res) {
|
||||
return encoding_type(req, res.get_header_value("Content-Type"));
|
||||
return encoding_type(req, res, res.get_header_value("Content-Type"));
|
||||
}
|
||||
|
||||
std::unique_ptr<compressor> make_compressor(EncodingType type) {
|
||||
@@ -3677,6 +3716,17 @@ bool is_chunked_transfer_encoding(const Headers &headers) {
|
||||
return case_ignore::equal(last_coding, "chunked");
|
||||
}
|
||||
|
||||
bool has_conflicting_content_length(const Headers &headers) {
|
||||
// RFC 9112 §6.3: a message carrying both Transfer-Encoding and a non-zero
|
||||
// Content-Length is framed ambiguously. The body readers here delimit it by
|
||||
// the transfer coding and drop Content-Length, while an intermediary may do
|
||||
// the reverse, so the two disagree on where the body ends and a reused
|
||||
// connection is desynchronised (request/response smuggling). Content-Length:
|
||||
// 0 is tolerated for compatibility with existing peers.
|
||||
return has_header(headers, "Transfer-Encoding") &&
|
||||
get_header_value_u64(headers, "Content-Length", 0, 0) > 0;
|
||||
}
|
||||
|
||||
template <typename T, typename U>
|
||||
bool prepare_content_receiver(T &x, int &status,
|
||||
ContentReceiverWithProgress receiver,
|
||||
@@ -4035,7 +4085,7 @@ void set_file_content_provider(Response &res,
|
||||
return true;
|
||||
});
|
||||
|
||||
res.file_content_encoding_ = encoding;
|
||||
res.content_coding_ = encoding;
|
||||
}
|
||||
|
||||
template <typename T, typename U>
|
||||
@@ -4361,13 +4411,20 @@ bool parse_range_header(const std::string &s, Ranges &ranges) try {
|
||||
|
||||
ssize_t first = -1;
|
||||
if (!lhs.empty()) {
|
||||
ssize_t v;
|
||||
auto res = detail::from_chars(lhs.data(), lhs.data() + lhs.size(), v);
|
||||
if (res.ec == std::errc{}) { first = v; }
|
||||
// Reject an overflowing first-byte-pos; treating it as absent (-1)
|
||||
// would turn the range into a suffix range.
|
||||
auto res =
|
||||
detail::from_chars(lhs.data(), lhs.data() + lhs.size(), first);
|
||||
if (res.ec != std::errc{}) {
|
||||
all_valid_ranges = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t last = -1;
|
||||
if (!rhs.empty()) {
|
||||
// An overflowing last-byte-pos is past any content length, so keeping
|
||||
// -1 ("remainder", RFC 9110 14.1.2) is correct here.
|
||||
ssize_t v;
|
||||
auto res = detail::from_chars(rhs.data(), rhs.data() + rhs.size(), v);
|
||||
if (res.ec == std::errc{}) { last = v; }
|
||||
@@ -6902,7 +6959,7 @@ void Response::set_content(const char *s, size_t n,
|
||||
auto rng = headers.equal_range("Content-Type");
|
||||
headers.erase(rng.first, rng.second);
|
||||
set_header("Content-Type", content_type);
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_content(const std::string &s,
|
||||
@@ -6917,7 +6974,7 @@ void Response::set_content(std::string &&s,
|
||||
auto rng = headers.equal_range("Content-Type");
|
||||
headers.erase(rng.first, rng.second);
|
||||
set_header("Content-Type", content_type);
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_content_provider(
|
||||
@@ -6928,7 +6985,7 @@ void Response::set_content_provider(
|
||||
if (in_length > 0) { content_provider_ = std::move(provider); }
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = false;
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_content_provider(
|
||||
@@ -6939,7 +6996,7 @@ void Response::set_content_provider(
|
||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = false;
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_chunked_content_provider(
|
||||
@@ -6950,7 +7007,7 @@ void Response::set_chunked_content_provider(
|
||||
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
||||
content_provider_resource_releaser_ = std::move(resource_releaser);
|
||||
is_chunked_content_provider_ = true;
|
||||
file_content_encoding_ = detail::EncodingType::None;
|
||||
content_coding_ = detail::EncodingType::None;
|
||||
}
|
||||
|
||||
void Response::set_file_content(const std::string &path,
|
||||
@@ -7991,12 +8048,19 @@ ssize_t WebSocketSSLStream::read(char *ptr, size_t size) {
|
||||
needs_readable || (err.code == tls::ErrorCode::SyscallError &&
|
||||
WSAGetLastError() == WSAETIMEDOUT);
|
||||
#endif
|
||||
if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { return -1; }
|
||||
if (!needs_readable && err.code != tls::ErrorCode::WantWrite) {
|
||||
error_ = Error::Read;
|
||||
return -1;
|
||||
}
|
||||
if (!(needs_readable ? wait_readable() : wait_writable())) {
|
||||
error_ = Error::Timeout;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// Out of retries. Recording a reason matters: a caller that reads get_error()
|
||||
// to tell a timeout from a close would otherwise see whatever the previous
|
||||
// failure left behind (error_ is never cleared on success).
|
||||
error_ = Error::Read;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -8653,9 +8717,10 @@ Server::write_content_with_provider(Stream &strm, const Request &req,
|
||||
}
|
||||
} else {
|
||||
if (res.is_chunked_content_provider_) {
|
||||
auto type = detail::encoding_type(req, res);
|
||||
|
||||
auto compressor = detail::make_compressor(type);
|
||||
// Use the coding `apply_ranges()` chose when it wrote the headers;
|
||||
// re-negotiating here would disagree with them, e.g. once a handler's
|
||||
// own Content-Encoding header suppresses the negotiation.
|
||||
auto compressor = detail::make_compressor(res.content_coding_);
|
||||
if (!compressor) {
|
||||
compressor = detail::make_unique<detail::nocompressor>();
|
||||
}
|
||||
@@ -8881,7 +8946,8 @@ bool Server::handle_file_request(Request &req, Response &res) {
|
||||
auto encoding = detail::EncodingType::None;
|
||||
if (static_file_compression_) {
|
||||
content_type = content_type_of();
|
||||
encoding = static_file_encoding(req, content_type, stat.size());
|
||||
encoding =
|
||||
static_file_encoding(req, res, content_type, stat.size());
|
||||
}
|
||||
|
||||
// The ETag names the representation actually sent, so a client that
|
||||
@@ -9296,8 +9362,10 @@ bool Server::dispatch_request(Request &req, Response &res,
|
||||
// the ETag, which has to name the representation actually sent, and
|
||||
// `apply_static_file_compression()` go through this, so the two cannot drift
|
||||
// apart.
|
||||
detail::EncodingType Server::static_file_encoding(
|
||||
const Request &req, const std::string &content_type, size_t length) const {
|
||||
detail::EncodingType
|
||||
Server::static_file_encoding(const Request &req, const Response &res,
|
||||
const std::string &content_type,
|
||||
size_t length) const {
|
||||
if (!static_file_compression_) { return detail::EncodingType::None; }
|
||||
|
||||
// Nothing to compress, and an empty file already answers with
|
||||
@@ -9322,14 +9390,14 @@ detail::EncodingType Server::static_file_encoding(
|
||||
return detail::EncodingType::None;
|
||||
}
|
||||
|
||||
return detail::encoding_type(req, content_type);
|
||||
return detail::encoding_type(req, res, content_type);
|
||||
}
|
||||
|
||||
// Compresses a file-backed content provider into `res.body` and takes over the
|
||||
// framing headers. Returns false when the response is left untouched.
|
||||
bool Server::apply_static_file_compression(const Request &req,
|
||||
Response &res) const {
|
||||
auto type = res.file_content_encoding_;
|
||||
auto type = res.content_coding_;
|
||||
if (type == detail::EncodingType::None || !res.content_provider_) {
|
||||
return false;
|
||||
}
|
||||
@@ -9353,7 +9421,7 @@ bool Server::apply_static_file_compression(const Request &req,
|
||||
res.content_provider_success_ = true;
|
||||
res.content_provider_ = nullptr;
|
||||
res.content_length_ = 0;
|
||||
res.file_content_encoding_ = detail::EncodingType::None;
|
||||
res.content_coding_ = detail::EncodingType::None;
|
||||
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
@@ -9412,6 +9480,7 @@ void Server::apply_ranges(const Request &req, Response &res,
|
||||
if (res.content_provider_) {
|
||||
if (res.is_chunked_content_provider_) {
|
||||
res.set_header("Transfer-Encoding", "chunked");
|
||||
res.content_coding_ = type;
|
||||
if (type != detail::EncodingType::None) {
|
||||
res.set_header("Content-Encoding", detail::encoding_name(type));
|
||||
res.set_header("Vary", "Accept-Encoding");
|
||||
@@ -9568,8 +9637,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
// 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 ||
|
||||
if (detail::has_conflicting_content_length(req.headers) ||
|
||||
(req.has_header("Transfer-Encoding") &&
|
||||
!detail::is_chunked_transfer_encoding(req.headers))) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
@@ -9734,7 +9803,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
auto ws_strm =
|
||||
std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
||||
strm.socket(), const_cast<tls::session_t>(req.ssl),
|
||||
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0,
|
||||
CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
|
||||
write_timeout_sec_, write_timeout_usec_));
|
||||
ws::WebSocket ws(std::move(ws_strm), req, true,
|
||||
websocket_ping_interval_sec_,
|
||||
@@ -9744,7 +9813,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
}
|
||||
#endif
|
||||
// Use WebSocket-specific read timeout instead of HTTP timeout
|
||||
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
|
||||
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND,
|
||||
0);
|
||||
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
|
||||
websocket_max_missed_pongs_);
|
||||
entry.handler(req, ws);
|
||||
@@ -9808,7 +9878,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
|
||||
detail::set_file_content_provider(
|
||||
res, mm, content_type,
|
||||
static_file_encoding(req, content_type, mm->size()));
|
||||
static_file_encoding(req, res, content_type, mm->size()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10228,8 +10298,12 @@ Result ClientImpl::send_(Request &&req) {
|
||||
void ClientImpl::prepare_default_headers(Request &r, bool for_stream,
|
||||
const std::string &ct) {
|
||||
(void)for_stream;
|
||||
for (const auto &header : default_headers_) {
|
||||
if (!r.has_header(header.first)) { r.headers.insert(header); }
|
||||
// Default headers are meant for the origin and may carry its credentials, so
|
||||
// keep them off the CONNECT request the proxy reads.
|
||||
if (r.method != "CONNECT") {
|
||||
for (const auto &header : default_headers_) {
|
||||
if (!r.has_header(header.first)) { r.headers.insert(header); }
|
||||
}
|
||||
}
|
||||
|
||||
// RFC 9110 5.3 recommends sending control data such as Host first, so
|
||||
@@ -10379,6 +10453,17 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
||||
return handle;
|
||||
}
|
||||
|
||||
// Same framing check as ClientImpl::process_request(). A HEAD or bodyless
|
||||
// (204/304) response legitimately carries framing headers with no body.
|
||||
if (method != "HEAD" &&
|
||||
handle.response->status != StatusCode::NoContent_204 &&
|
||||
handle.response->status != StatusCode::NotModified_304 &&
|
||||
detail::has_conflicting_content_length(handle.response->headers)) {
|
||||
handle.error = Error::Read;
|
||||
handle.response.reset();
|
||||
return handle;
|
||||
}
|
||||
|
||||
handle.body_reader_.stream = handle.stream_;
|
||||
handle.body_reader_.payload_max_length = payload_max_length_;
|
||||
|
||||
@@ -10910,24 +10995,24 @@ bool ClientImpl::write_request(Stream &strm, Request &req,
|
||||
}
|
||||
}
|
||||
|
||||
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
|
||||
if (!req.has_header("Authorization")) {
|
||||
// A CONNECT request is read by the proxy; everything sent through the tunnel
|
||||
// it opens is read by the origin. Each credential goes only to its own hop.
|
||||
auto is_connect = req.method == "CONNECT";
|
||||
|
||||
if (!is_connect && !req.has_header("Authorization")) {
|
||||
if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) {
|
||||
req.headers.insert(make_basic_authentication_header(
|
||||
basic_auth_username_, basic_auth_password_, false));
|
||||
}
|
||||
}
|
||||
|
||||
if (!bearer_token_auth_token_.empty()) {
|
||||
if (!req.has_header("Authorization")) {
|
||||
} else if (!bearer_token_auth_token_.empty()) {
|
||||
req.headers.insert(make_bearer_token_authentication_header(
|
||||
bearer_token_auth_token_, false));
|
||||
}
|
||||
}
|
||||
|
||||
// Proxy-Authorization is only sent when the proxy is actually used for
|
||||
// this target — otherwise NO_PROXY-matched requests would leak proxy
|
||||
// credentials directly to the destination server.
|
||||
if (is_proxy_enabled_for_host(host_)) {
|
||||
// Proxy-Authorization is only sent when the proxy reads this message —
|
||||
// otherwise NO_PROXY-matched requests, and requests inside a TLS tunnel,
|
||||
// would leak proxy credentials to the destination server.
|
||||
if (is_proxy_enabled_for_host(host_) && (!is_ssl() || is_connect)) {
|
||||
if (!proxy_basic_auth_username_.empty() &&
|
||||
!proxy_basic_auth_password_.empty() &&
|
||||
!req.has_header("Proxy-Authorization")) {
|
||||
@@ -11323,6 +11408,17 @@ bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
// Body
|
||||
if ((res.status != StatusCode::NoContent_204) && req.method != "HEAD" &&
|
||||
req.method != "CONNECT") {
|
||||
// Reject ambiguous framing (RFC 9112 §6.3). Unlike a request, a response
|
||||
// whose final transfer coding is not chunked is not ambiguous: its body
|
||||
// runs until the server closes the connection, so it is not rejected.
|
||||
// HEAD/204 are excluded above and a 304 carries no body.
|
||||
if (res.status != StatusCode::NotModified_304 &&
|
||||
detail::has_conflicting_content_length(res.headers)) {
|
||||
error = Error::Read;
|
||||
output_error_log(error, &req);
|
||||
return false;
|
||||
}
|
||||
|
||||
auto redirect = 300 < res.status && res.status < 400 &&
|
||||
res.status != StatusCode::NotModified_304 &&
|
||||
follow_location_;
|
||||
@@ -17562,8 +17658,16 @@ ReadResult WebSocket::read(std::string &msg) {
|
||||
std::string payload;
|
||||
bool fin;
|
||||
|
||||
if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
|
||||
impl::FrameRead r =
|
||||
impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH);
|
||||
// A timeout landed on a frame boundary: the connection is untouched and
|
||||
// still usable, so hand control back without closing it. That is only
|
||||
// useful to a caller who asked for the timeout; the compile-time default
|
||||
// is a backstop against a peer gone quiet, and elapsing it closes the
|
||||
// connection so a plain `while (ws.read(msg))` loop ends.
|
||||
if (r == impl::FrameRead::Timeout && read_timeout_set_) { return Timeout; }
|
||||
if (r != impl::FrameRead::Ok) {
|
||||
closed_ = true;
|
||||
return Fail;
|
||||
}
|
||||
@@ -17600,9 +17704,14 @@ ReadResult WebSocket::read(std::string &msg) {
|
||||
Opcode cont_opcode;
|
||||
std::string cont_payload;
|
||||
bool cont_fin;
|
||||
if (!impl::read_websocket_frame(
|
||||
// A timeout is not reportable here: half of a fragmented message is
|
||||
// already in `msg` and read() has no way to resume it, so it is a
|
||||
// failure like any other. Timeouts are only ever seen on a message
|
||||
// boundary.
|
||||
if (impl::read_websocket_frame(
|
||||
strm_, cont_opcode, cont_payload, cont_fin, is_server_,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) !=
|
||||
impl::FrameRead::Ok) {
|
||||
closed_ = true;
|
||||
return Fail;
|
||||
}
|
||||
@@ -17696,7 +17805,8 @@ void WebSocket::close(CloseStatus status, const std::string &reason) {
|
||||
Opcode op;
|
||||
std::string resp;
|
||||
bool fin;
|
||||
while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) {
|
||||
while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125) ==
|
||||
impl::FrameRead::Ok) {
|
||||
if (op == Opcode::Close) { break; }
|
||||
}
|
||||
}
|
||||
@@ -17741,6 +17851,15 @@ const Request &WebSocket::request() const { return req_; }
|
||||
|
||||
bool WebSocket::is_open() const { return !closed_; }
|
||||
|
||||
void WebSocket::set_read_timeout(time_t sec, time_t usec) {
|
||||
// 0 waits forever here, as it does for SO_RCVTIMEO. The stream waits with
|
||||
// poll(), where 0 would instead mean "return immediately", so hand it the
|
||||
// negative poll uses for an unbounded wait.
|
||||
if (sec == 0 && usec == 0) { sec = -1; }
|
||||
strm_.set_read_timeout(sec, usec);
|
||||
read_timeout_set_ = true;
|
||||
}
|
||||
|
||||
// WebSocketClient implementation
|
||||
WebSocketClient::WebSocketClient(
|
||||
const std::string &scheme_host_port_path, const Headers &headers)
|
||||
@@ -17843,6 +17962,16 @@ void WebSocketClient::shutdown_and_close() {
|
||||
bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||
Error &error, int &ssl_error,
|
||||
uint64_t &ssl_backend_error) {
|
||||
// A read timeout of 0 means "wait forever", the way SO_RCVTIMEO reads it.
|
||||
// The streams wait with poll(), where 0 instead means "return immediately",
|
||||
// so they are given the negative poll uses for an unbounded wait.
|
||||
auto unbounded = read_timeout_sec_ == 0 && read_timeout_usec_ == 0;
|
||||
time_t strm_read_sec = unbounded ? -1 : read_timeout_sec_;
|
||||
time_t strm_read_usec = unbounded ? 0 : read_timeout_usec_;
|
||||
// The handshake belongs to establishing the connection, so an unset read
|
||||
// timeout leaves it bounded by the connection timeout instead of forever.
|
||||
time_t hs_sec = unbounded ? connection_timeout_sec_ : read_timeout_sec_;
|
||||
time_t hs_usec = unbounded ? connection_timeout_usec_ : read_timeout_usec_;
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
if (is_ssl_) {
|
||||
// A plain flag rather than SSLClient::load_certs()'s call_once: connect()
|
||||
@@ -17862,8 +17991,8 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||
detail::ClientTlsSessionError tls_error;
|
||||
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
|
||||
server_certificate_verification_,
|
||||
read_timeout_sec_, read_timeout_usec_,
|
||||
&tls_error, options)) {
|
||||
hs_sec, hs_usec, &tls_error,
|
||||
options)) {
|
||||
error = tls_error.error;
|
||||
ssl_error = tls_error.ssl_error;
|
||||
ssl_backend_error = tls_error.backend_error;
|
||||
@@ -17871,17 +18000,19 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||
}
|
||||
|
||||
strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
||||
sock_, tls_session_, read_timeout_sec_, read_timeout_usec_,
|
||||
write_timeout_sec_, write_timeout_usec_));
|
||||
sock_, tls_session_, strm_read_sec, strm_read_usec, write_timeout_sec_,
|
||||
write_timeout_usec_));
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
(void)error;
|
||||
(void)ssl_error;
|
||||
(void)ssl_backend_error;
|
||||
(void)hs_sec;
|
||||
(void)hs_usec;
|
||||
#endif
|
||||
strm = std::unique_ptr<Stream>(
|
||||
new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_,
|
||||
new detail::SocketStream(sock_, strm_read_sec, strm_read_usec,
|
||||
write_timeout_sec_, write_timeout_usec_));
|
||||
return true;
|
||||
}
|
||||
@@ -17951,6 +18082,9 @@ Result WebSocketClient::connect() {
|
||||
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
|
||||
websocket_ping_interval_sec_,
|
||||
websocket_max_missed_pongs_));
|
||||
// The stream was created with the timeout already; tell the WebSocket
|
||||
// whether it came from the caller, so read() knows to report it as Timeout.
|
||||
ws_->read_timeout_set_ = read_timeout_set_;
|
||||
return Result{Error::Success, upgrade.status, std::move(upgrade.headers)};
|
||||
}
|
||||
|
||||
@@ -17983,6 +18117,10 @@ const std::string &WebSocketClient::subprotocol() const {
|
||||
void WebSocketClient::set_read_timeout(time_t sec, time_t usec) {
|
||||
read_timeout_sec_ = sec;
|
||||
read_timeout_usec_ = usec;
|
||||
read_timeout_set_ = true;
|
||||
// The members above only seed the next connect(); read() consults the
|
||||
// stream, so an already-open connection has to be told directly.
|
||||
if (ws_) { ws_->set_read_timeout(sec, usec); }
|
||||
}
|
||||
|
||||
void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
|
||||
|
||||
Reference in New Issue
Block a user