vendor : update cpp-httplib to 0.52.0 (#26485)
This commit is contained in:
parent
fe2adf0e72
commit
94bc47f280
@@ -5,7 +5,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
HTTPLIB_VERSION = "refs/tags/v0.51.0"
|
HTTPLIB_VERSION = "refs/tags/v0.52.0"
|
||||||
|
|
||||||
vendor = {
|
vendor = {
|
||||||
"https://github.com/nlohmann/json/releases/latest/download/json.hpp": "vendor/nlohmann/json.hpp",
|
"https://github.com/nlohmann/json/releases/latest/download/json.hpp": "vendor/nlohmann/json.hpp",
|
||||||
|
|||||||
Vendored
+411
-174
@@ -1412,6 +1412,46 @@ bool stream_line_reader::getline() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
for (size_t i = 0;; i++) {
|
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) {
|
if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
|
||||||
// Treat exceptionally long lines as an error to
|
// Treat exceptionally long lines as an error to
|
||||||
// prevent infinite loops/memory exhaustion
|
// prevent infinite loops/memory exhaustion
|
||||||
@@ -1443,16 +1483,26 @@ bool stream_line_reader::getline() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void stream_line_reader::append(char c) {
|
void stream_line_reader::append(char c) { append(&c, 1); }
|
||||||
if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
|
|
||||||
fixed_buffer_[fixed_buffer_used_size_++] = c;
|
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';
|
fixed_buffer_[fixed_buffer_used_size_] = '\0';
|
||||||
} else {
|
} 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()) {
|
if (growable_buffer_.empty()) {
|
||||||
assert(fixed_buffer_[fixed_buffer_used_size_] == '\0');
|
|
||||||
growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);
|
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;
|
is_open_empty_file = true;
|
||||||
return false;
|
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
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -1702,8 +1760,17 @@ public:
|
|||||||
socket_t socket() const override;
|
socket_t socket() const override;
|
||||||
time_t duration() const override;
|
time_t duration() const override;
|
||||||
void set_read_timeout(time_t sec, time_t usec = 0) 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:
|
private:
|
||||||
|
bool ensure_readable();
|
||||||
|
|
||||||
socket_t sock_;
|
socket_t sock_;
|
||||||
time_t read_timeout_sec_;
|
time_t read_timeout_sec_;
|
||||||
time_t read_timeout_usec_;
|
time_t read_timeout_usec_;
|
||||||
@@ -1715,6 +1782,7 @@ private:
|
|||||||
std::vector<char> read_buff_;
|
std::vector<char> read_buff_;
|
||||||
size_t read_buff_off_ = 0;
|
size_t read_buff_off_ = 0;
|
||||||
size_t read_buff_content_size_ = 0;
|
size_t read_buff_content_size_ = 0;
|
||||||
|
bool readable_hint_ = false;
|
||||||
|
|
||||||
static const size_t read_buff_size_ = 1024l * 4;
|
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) {
|
[&](bool close_connection, bool &connection_closed) {
|
||||||
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
|
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
|
||||||
write_timeout_sec, write_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);
|
return callback(strm, close_connection, connection_closed);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -3071,19 +3142,49 @@ bool zstd_decompressor::decompress(const char *data, size_t data_length,
|
|||||||
}
|
}
|
||||||
#endif
|
#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>
|
std::unique_ptr<decompressor>
|
||||||
create_decompressor(const std::string &encoding) {
|
create_decompressor(const std::string &encoding) {
|
||||||
std::unique_ptr<decompressor> decompressor;
|
std::unique_ptr<decompressor> decompressor;
|
||||||
|
|
||||||
if (encoding == "gzip" || encoding == "deflate") {
|
if (is_zlib_encoding(encoding)) {
|
||||||
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
||||||
decompressor = detail::make_unique<gzip_decompressor>();
|
decompressor = detail::make_unique<gzip_decompressor>();
|
||||||
#endif
|
#endif
|
||||||
} else if (encoding.find("br") != std::string::npos) {
|
} else if (is_brotli_encoding(encoding)) {
|
||||||
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
|
||||||
decompressor = detail::make_unique<brotli_decompressor>();
|
decompressor = detail::make_unique<brotli_decompressor>();
|
||||||
#endif
|
#endif
|
||||||
} else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) {
|
} else if (is_zstd_encoding(encoding)) {
|
||||||
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
|
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
|
||||||
decompressor = detail::make_unique<zstd_decompressor>();
|
decompressor = detail::make_unique<zstd_decompressor>();
|
||||||
#endif
|
#endif
|
||||||
@@ -3145,8 +3246,7 @@ const char *get_header_value(const Headers &headers,
|
|||||||
|
|
||||||
size_t get_header_value_count(const Headers &headers,
|
size_t get_header_value_count(const Headers &headers,
|
||||||
const std::string &key) {
|
const std::string &key) {
|
||||||
auto r = headers.equal_range(key);
|
return headers.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Map>
|
template <typename Map>
|
||||||
@@ -3370,44 +3470,33 @@ ReadContentResult read_content_chunked(Stream &strm, T &x,
|
|||||||
bool is_chunked_transfer_encoding(const Headers &headers) {
|
bool is_chunked_transfer_encoding(const Headers &headers) {
|
||||||
// RFC 9112 6.1: a message is framed with the chunked coding when "chunked"
|
// 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
|
// is the final transfer coding. A single field value may list several
|
||||||
// codings ("gzip, chunked"), and the list may be split across multiple
|
// codings ("gzip, chunked"), and RFC 9110 5.3 lets that list be split across
|
||||||
// Transfer-Encoding header lines (RFC 9110 5.3). Match the last coding token
|
// several Transfer-Encoding lines, which combine into one comma-separated
|
||||||
// case-insensitively rather than comparing the whole value against "chunked".
|
// 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
|
// Security: reading a chunked message as unframed leaves its body in the
|
||||||
// socket, where a keep-alive connection parses it as a smuggled request.
|
// socket, where a keep-alive connection parses it as a smuggled request.
|
||||||
// Headers is an unordered_multimap whose iteration order for duplicate keys
|
// Server::process_request() answers 400 and closes when the final coding is
|
||||||
// is not portable, so when there is more than one Transfer-Encoding line we
|
// not chunked, so a request whose framing cannot be determined never
|
||||||
// cannot tell which coding is truly final. In that ambiguous case we fail
|
// reaches the "no body" path.
|
||||||
// safe by treating the message as chunked (a mis-parse just closes the
|
|
||||||
// connection, whereas the opposite error enables smuggling).
|
|
||||||
auto rng = headers.equal_range("Transfer-Encoding");
|
auto rng = headers.equal_range("Transfer-Encoding");
|
||||||
|
if (rng.first == rng.second) { return false; }
|
||||||
|
|
||||||
size_t line_count = 0;
|
// Cleared per line, so a trailing line carrying no coding at all leaves the
|
||||||
bool chunked_present = false;
|
// combined list ending in nothing rather than inheriting the line before it.
|
||||||
bool last_line_ends_with_chunked = false;
|
std::string last_coding;
|
||||||
|
|
||||||
for (auto it = rng.first; it != rng.second; ++it) {
|
for (auto it = rng.first; it != rng.second; ++it) {
|
||||||
line_count++;
|
|
||||||
const auto &value = it->second;
|
const auto &value = it->second;
|
||||||
|
last_coding.clear();
|
||||||
std::string last_coding;
|
|
||||||
bool line_has_chunked = false;
|
|
||||||
split(value.data(), value.data() + value.size(), ',',
|
split(value.data(), value.data() + value.size(), ',',
|
||||||
[&](const char *b, const char *e) {
|
[&](const char *b, const char *e) { last_coding.assign(b, 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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (line_count == 0) { return false; }
|
return case_ignore::equal(last_coding, "chunked");
|
||||||
if (line_count == 1) { return last_line_ends_with_chunked; }
|
|
||||||
return chunked_present;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, typename U>
|
template <typename T, typename U>
|
||||||
@@ -3420,9 +3509,12 @@ bool prepare_content_receiver(T &x, int &status,
|
|||||||
std::unique_ptr<decompressor> decompressor;
|
std::unique_ptr<decompressor> decompressor;
|
||||||
|
|
||||||
if (!encoding.empty()) {
|
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);
|
decompressor = detail::create_decompressor(encoding);
|
||||||
if (!decompressor) {
|
if (!decompressor && detail::is_known_content_encoding(encoding)) {
|
||||||
// Unsupported encoding or no support compiled in
|
|
||||||
status = StatusCode::UnsupportedMediaType_415;
|
status = StatusCode::UnsupportedMediaType_415;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -3845,6 +3937,19 @@ std::string params_to_query_str(const Params ¶ms) {
|
|||||||
return query;
|
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,
|
void parse_query_text(const char *data, std::size_t size,
|
||||||
Params ¶ms) {
|
Params ¶ms) {
|
||||||
std::set<std::string> cache;
|
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 key;
|
||||||
std::string val;
|
std::string val;
|
||||||
divide(b, static_cast<std::size_t>(e - b), '=',
|
divide_query_pair(b, e, key, val);
|
||||||
[&](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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!key.empty()) {
|
if (!key.empty()) {
|
||||||
params.emplace(decode_query_component(key), decode_query_component(val));
|
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
|
// Normalize a query string by decoding and re-encoding each key/value pair
|
||||||
// while preserving the original parameter order. This avoids double-encoding
|
// while preserving the original parameter order. This avoids double-encoding
|
||||||
// and ensures consistent encoding without reordering (unlike Params which
|
// and ensures consistent encoding. It works on the raw string rather than
|
||||||
// uses std::multimap and sorts keys).
|
// 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 normalize_query_string(const std::string &query) {
|
||||||
std::string result;
|
std::string result;
|
||||||
split(query.data(), query.data() + query.size(), '&',
|
split(query.data(), query.data() + query.size(), '&',
|
||||||
[&](const char *b, const char *e) {
|
[&](const char *b, const char *e) {
|
||||||
std::string key;
|
std::string key;
|
||||||
std::string val;
|
std::string val;
|
||||||
divide(b, static_cast<std::size_t>(e - b), '=',
|
divide_query_pair(b, e, key, val);
|
||||||
[&](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);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!key.empty()) {
|
if (!key.empty()) {
|
||||||
auto dec_key = decode_query_component(key);
|
auto dec_key = decode_query_component(key);
|
||||||
@@ -3904,6 +4002,43 @@ std::string normalize_query_string(const std::string &query) {
|
|||||||
return result;
|
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,
|
bool parse_multipart_boundary(const std::string &content_type,
|
||||||
std::string &boundary) {
|
std::string &boundary) {
|
||||||
std::map<std::string, std::string> params;
|
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
|
} // namespace fields
|
||||||
|
|
||||||
bool perform_websocket_handshake(Stream &strm, const std::string &host,
|
bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||||
int port, bool is_ssl,
|
|
||||||
const std::string &path,
|
|
||||||
const Headers &headers,
|
|
||||||
std::string &selected_subprotocol) {
|
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
|
// Generate random Sec-WebSocket-Key
|
||||||
thread_local std::mt19937 rng(std::random_device{}());
|
thread_local std::mt19937 rng(std::random_device{}());
|
||||||
std::string key_bytes(16, '\0');
|
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);
|
auto client_key = base64_encode(key_bytes);
|
||||||
|
|
||||||
// Build upgrade request
|
req.headers.erase("Upgrade");
|
||||||
std::string req_str = "GET " + path + " HTTP/1.1\r\n";
|
req.headers.erase("Connection");
|
||||||
req_str += "Host: " + make_host_and_port_string(host, port, is_ssl) + "\r\n";
|
req.headers.erase("Sec-WebSocket-Key");
|
||||||
req_str += "Upgrade: websocket\r\n";
|
req.headers.erase("Sec-WebSocket-Version");
|
||||||
req_str += "Connection: Upgrade\r\n";
|
req.headers.emplace("Upgrade", "websocket");
|
||||||
req_str += "Sec-WebSocket-Key: " + client_key + "\r\n";
|
req.headers.emplace("Connection", "Upgrade");
|
||||||
req_str += "Sec-WebSocket-Version: 13\r\n";
|
req.headers.emplace("Sec-WebSocket-Key", client_key);
|
||||||
for (const auto &h : headers) {
|
req.headers.emplace("Sec-WebSocket-Version", "13");
|
||||||
req_str += h.first + ": " + h.second + "\r\n";
|
|
||||||
}
|
|
||||||
req_str += "\r\n";
|
|
||||||
|
|
||||||
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
|
// Verify 101 response and Sec-WebSocket-Accept header
|
||||||
auto expected_accept = websocket_accept_key(client_key);
|
auto expected_accept = websocket_accept_key(client_key);
|
||||||
@@ -5013,6 +5146,39 @@ bool perform_websocket_handshake(Stream &strm, const std::string &host,
|
|||||||
selected_subprotocol);
|
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
|
} // namespace detail
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -5044,7 +5210,12 @@ public:
|
|||||||
time_t duration() const override;
|
time_t duration() const override;
|
||||||
void set_read_timeout(time_t sec, time_t usec = 0) 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:
|
private:
|
||||||
|
bool ensure_readable();
|
||||||
|
|
||||||
socket_t sock_;
|
socket_t sock_;
|
||||||
tls::session_t session_;
|
tls::session_t session_;
|
||||||
time_t read_timeout_sec_;
|
time_t read_timeout_sec_;
|
||||||
@@ -5053,6 +5224,7 @@ private:
|
|||||||
time_t write_timeout_usec_;
|
time_t write_timeout_usec_;
|
||||||
time_t max_timeout_msec_;
|
time_t max_timeout_msec_;
|
||||||
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
|
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
|
||||||
|
bool readable_hint_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||||
@@ -5196,13 +5368,6 @@ std::string SHA_512(const std::string &s) {
|
|||||||
}
|
}
|
||||||
#endif
|
#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>
|
template <typename T>
|
||||||
bool process_server_socket_ssl(
|
bool process_server_socket_ssl(
|
||||||
const std::atomic<socket_t> &svr_sock, tls::session_t session,
|
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) {
|
[&](bool close_connection, bool &connection_closed) {
|
||||||
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
|
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
|
||||||
write_timeout_sec, write_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);
|
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::UnsupportedAddressFamily: return "Unsupported address family";
|
||||||
case Error::HTTPParsing: return "HTTP parsing failed";
|
case Error::HTTPParsing: return "HTTP parsing failed";
|
||||||
case Error::InvalidRangeHeader: return "Invalid Range header";
|
case Error::InvalidRangeHeader: return "Invalid Range header";
|
||||||
|
case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
|
||||||
default: break;
|
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 {
|
size_t Request::get_trailer_value_count(const std::string &key) const {
|
||||||
auto r = trailers.equal_range(key);
|
return trailers.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Request::has_param(const std::string &key) const {
|
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 {
|
size_t Request::get_param_value_count(const std::string &key) const {
|
||||||
auto r = params.equal_range(key);
|
return params.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Request::is_multipart_form_data() const {
|
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 {
|
size_t MultipartFormData::get_field_count(const std::string &key) const {
|
||||||
auto r = fields.equal_range(key);
|
return fields.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FormData MultipartFormData::get_file(const std::string &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 {
|
size_t MultipartFormData::get_file_count(const std::string &key) const {
|
||||||
auto r = files.equal_range(key);
|
return files.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multipart FormData writer implementation
|
// 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 {
|
size_t Response::get_trailer_value_count(const std::string &key) const {
|
||||||
auto r = trailers.equal_range(key);
|
return trailers.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Response::set_redirect(const std::string &url, int stat) {
|
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
|
size_t
|
||||||
Result::get_request_header_value_count(const std::string &key) const {
|
Result::get_request_header_value_count(const std::string &key) const {
|
||||||
auto r = request_headers_.equal_range(key);
|
return request_headers_.count(key);
|
||||||
return static_cast<size_t>(std::distance(r.first, r.second));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stream implementation
|
// Stream implementation
|
||||||
@@ -6595,6 +6757,24 @@ bool SocketStream::wait_writable() const {
|
|||||||
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
|
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 {
|
bool SocketStream::is_peer_alive() const {
|
||||||
return detail::is_socket_alive(sock_);
|
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;
|
error_ = Error::Timeout;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -7099,6 +7279,14 @@ bool SSLSocketStream::wait_writable() const {
|
|||||||
!tls::is_peer_closed(session_, sock_);
|
!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 {
|
bool SSLSocketStream::is_peer_alive() const {
|
||||||
return !tls::is_peer_closed(session_, sock_);
|
return !tls::is_peer_closed(session_, sock_);
|
||||||
}
|
}
|
||||||
@@ -7111,7 +7299,7 @@ ssize_t SSLSocketStream::read(char *ptr, size_t size) {
|
|||||||
error_ = Error::ConnectionClosed;
|
error_ = Error::ConnectionClosed;
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
} else if (wait_readable()) {
|
} else if (ensure_readable()) {
|
||||||
tls::TlsError err;
|
tls::TlsError err;
|
||||||
auto ret = tls::read(session_, ptr, size, err);
|
auto ret = tls::read(session_, ptr, size, err);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
@@ -7533,9 +7721,11 @@ void Server::wait_until_ready() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Server::stop() noexcept {
|
void Server::stop() noexcept {
|
||||||
if (is_running_) {
|
// Release the listening socket whether or not the accept loop is running:
|
||||||
assert(svr_sock_ != INVALID_SOCKET);
|
// bind_to_port() without listen_after_bind() still owns the descriptor. The
|
||||||
std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
|
// 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::shutdown_socket(sock);
|
||||||
detail::close_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 (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,
|
return detail::write_content(strm, res.content_provider_, 0,
|
||||||
res.content_length_, is_shutting_down);
|
res.content_length_, is_shutting_down);
|
||||||
} else if (req.ranges.size() == 1) {
|
} else if (req.ranges.size() == 1) {
|
||||||
@@ -8096,7 +8294,14 @@ int Server::bind_internal(const std::string &host, int port,
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Server::listen_internal() {
|
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;
|
auto ret = true;
|
||||||
is_running_ = 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);
|
return write_response(strm, close_connection, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
|
// RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
||||||
// any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
|
// otherwise let an intermediary and this parser disagree on where the body
|
||||||
// tolerated for compatibility with existing clients.
|
// ends and enable request smuggling. Two cases: a non-zero Content-Length
|
||||||
if (req.get_header_value_u64("Content-Length") > 0 &&
|
// alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
||||||
req.has_header("Transfer-Encoding")) {
|
// 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;
|
connection_closed = true;
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
return write_response(strm, close_connection, req, res);
|
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);
|
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;
|
std::string ip;
|
||||||
auto it = addr_map_.find(host_);
|
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||||
if (it != addr_map_.end()) { ip = it->second; }
|
|
||||||
|
|
||||||
return detail::create_client_socket(
|
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_,
|
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
||||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||||
write_timeout_usec_, interface_, error);
|
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); }
|
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 (!r.has_header("Host")) {
|
||||||
if (address_family_ == AF_UNIX) {
|
if (address_family_ == AF_UNIX) {
|
||||||
r.headers.emplace("Host", "localhost");
|
r.headers.emplace_front("Host", "localhost");
|
||||||
} else {
|
} else {
|
||||||
r.headers.emplace(
|
r.headers.emplace_front(
|
||||||
"Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
|
"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.response = detail::make_unique<Response>();
|
||||||
handle.error = Error::Success;
|
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>();
|
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");
|
auto content_encoding = handle.response->get_header_value("Content-Encoding");
|
||||||
if (!content_encoding.empty()) {
|
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);
|
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;
|
return handle;
|
||||||
@@ -9842,52 +10073,26 @@ bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
{
|
{
|
||||||
detail::BufferStream bstrm;
|
detail::BufferStream bstrm;
|
||||||
|
|
||||||
// Extract path and query from req.path
|
// Extract the query from req.path. The encoding itself is delegated to
|
||||||
std::string path_part, query_part;
|
// `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('?');
|
auto query_pos = req.path.find('?');
|
||||||
if (query_pos != std::string::npos) {
|
auto query_part = query_pos == std::string::npos
|
||||||
path_part = req.path.substr(0, query_pos);
|
? std::string()
|
||||||
query_part = req.path.substr(query_pos + 1);
|
: req.path.substr(query_pos + 1);
|
||||||
} else {
|
|
||||||
path_part = req.path;
|
|
||||||
query_part = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 =
|
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()) {
|
if (!query_part.empty()) {
|
||||||
// Normalize the query string (decode then re-encode) while preserving
|
// The query already came in through `req.path`; still populate
|
||||||
// the original parameter order. When path encoding is disabled the
|
// `req.params` for handlers/users who read them.
|
||||||
// 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.
|
|
||||||
detail::parse_query_text(query_part, req.params);
|
detail::parse_query_text(query_part, req.params);
|
||||||
} else {
|
} else if (!req.params.empty()) {
|
||||||
// No query in path; parse any query_part (empty) and append params
|
// No query in `req.path`; build one from `req.params` so existing
|
||||||
// from `req.params` when present (preserves prior behavior for
|
// callers that pass `Params` separately continue to work.
|
||||||
// callers who provide Params separately).
|
path_with_query = append_query_params(path_with_query, req.params);
|
||||||
detail::parse_query_text(query_part, req.params);
|
|
||||||
if (!req.params.empty()) {
|
|
||||||
path_with_query = append_query_params(path_with_query, req.params);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write request line and headers
|
// Write request line and headers
|
||||||
@@ -10298,14 +10503,26 @@ bool ClientImpl::process_request(Stream &strm, Request &req,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res.status != StatusCode::NotModified_304) {
|
if (res.status != StatusCode::NotModified_304) {
|
||||||
int dummy_status;
|
auto content_status = 0;
|
||||||
auto max_length = (!has_payload_max_length_ && req.content_receiver)
|
auto max_length = (!has_payload_max_length_ && req.content_receiver)
|
||||||
? (std::numeric_limits<size_t>::max)()
|
? (std::numeric_limits<size_t>::max)()
|
||||||
: payload_max_length_;
|
: 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),
|
std::move(progress), std::move(out),
|
||||||
decompress_)) {
|
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);
|
output_error_log(error, &req);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -16769,18 +16986,42 @@ bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm) {
|
|||||||
return true;
|
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() {
|
bool WebSocketClient::connect() {
|
||||||
if (!is_valid_) { return false; }
|
if (!is_valid_) { return false; }
|
||||||
shutdown_and_close();
|
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;
|
std::string ip;
|
||||||
auto it = addr_map_.find(host_);
|
detail::apply_addr_map(addr_map_, host_, connect_host, ip);
|
||||||
if (it != addr_map_.end()) { ip = it->second; }
|
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
sock_ = detail::create_client_socket(
|
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_,
|
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
|
||||||
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
|
||||||
write_timeout_usec_, interface_, error);
|
write_timeout_usec_, interface_, error);
|
||||||
@@ -16793,23 +17034,19 @@ bool WebSocketClient::connect() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
Request req;
|
||||||
auto is_ssl = is_ssl_;
|
req.method = "GET";
|
||||||
#else
|
req.path = path_;
|
||||||
auto is_ssl = false;
|
req.headers = headers_;
|
||||||
#endif
|
prepare_default_headers(req);
|
||||||
|
|
||||||
std::string selected_subprotocol;
|
std::string selected_subprotocol;
|
||||||
if (!detail::perform_websocket_handshake(*strm, host_, port_, is_ssl, path_,
|
if (!detail::perform_websocket_handshake(*strm, req, selected_subprotocol)) {
|
||||||
headers_, selected_subprotocol)) {
|
|
||||||
shutdown_and_close();
|
shutdown_and_close();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
subprotocol_ = std::move(selected_subprotocol);
|
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,
|
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
|
||||||
websocket_ping_interval_sec_,
|
websocket_ping_interval_sec_,
|
||||||
websocket_max_missed_pongs_));
|
websocket_max_missed_pongs_));
|
||||||
|
|||||||
Vendored
+322
-11
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.51.0"
|
#define CPPHTTPLIB_VERSION "0.52.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x003300"
|
#define CPPHTTPLIB_VERSION_NUM "0x003400"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -182,7 +182,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CPPHTTPLIB_LISTEN_BACKLOG
|
#ifndef CPPHTTPLIB_LISTEN_BACKLOG
|
||||||
#define CPPHTTPLIB_LISTEN_BACKLOG 5
|
#define CPPHTTPLIB_LISTEN_BACKLOG 128
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CPPHTTPLIB_MAX_LINE_LENGTH
|
#ifndef CPPHTTPLIB_MAX_LINE_LENGTH
|
||||||
@@ -321,6 +321,7 @@ using socket_t = int;
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <iterator>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -333,9 +334,11 @@ using socket_t = int;
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <type_traits>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
// On macOS with a TLS backend, enable Keychain root certificates by default
|
// 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
|
// unless the user explicitly opts out. Not enabled on iOS/tvOS/watchOS since
|
||||||
@@ -968,11 +971,291 @@ enum StatusCode {
|
|||||||
NetworkAuthenticationRequired_511 = 511,
|
NetworkAuthenticationRequired_511 = 511,
|
||||||
};
|
};
|
||||||
|
|
||||||
using Headers =
|
namespace detail {
|
||||||
std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
|
|
||||||
detail::case_ignore::equal_to>;
|
|
||||||
|
|
||||||
using Params = std::multimap<std::string, std::string>;
|
// 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 <typename Mapped, typename KeyEqual> class insertion_ordered_multimap {
|
||||||
|
public:
|
||||||
|
using key_type = std::string;
|
||||||
|
using mapped_type = Mapped;
|
||||||
|
using value_type = std::pair<std::string, Mapped>;
|
||||||
|
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<size_type>(-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 <typename V> 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 <typename U,
|
||||||
|
typename std::enable_if<std::is_convertible<U *, V *>::value,
|
||||||
|
int>::type = 0>
|
||||||
|
iterator_t(const iterator_t<U> &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 <typename U> bool operator==(const iterator_t<U> &rhs) const {
|
||||||
|
return idx_ == rhs.idx_;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename U> bool operator!=(const iterator_t<U> &rhs) const {
|
||||||
|
return idx_ != rhs.idx_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class insertion_ordered_multimap;
|
||||||
|
template <typename> 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<value_type>;
|
||||||
|
using const_iterator = iterator_t<const value_type>;
|
||||||
|
|
||||||
|
insertion_ordered_multimap() = default;
|
||||||
|
insertion_ordered_multimap(std::initializer_list<value_type> il)
|
||||||
|
: entries_(il) {}
|
||||||
|
template <typename InputIt>
|
||||||
|
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 <typename... Args> iterator emplace(Args &&...args) {
|
||||||
|
entries_.emplace_back(std::forward<Args>(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 <typename... Args> iterator emplace_front(Args &&...args) {
|
||||||
|
entries_.emplace(entries_.begin(), std::forward<Args>(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<iterator, iterator> 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<const_iterator, const_iterator>
|
||||||
|
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<difference_type>(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<difference_type>(from);
|
||||||
|
auto to_it = begin_it + static_cast<difference_type>(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<value_type> entries_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace detail
|
||||||
|
|
||||||
|
using Headers =
|
||||||
|
detail::insertion_ordered_multimap<std::string,
|
||||||
|
detail::case_ignore::equal_to>;
|
||||||
|
|
||||||
|
// Query parameter names are case-sensitive, unlike header field names.
|
||||||
|
using Params =
|
||||||
|
detail::insertion_ordered_multimap<std::string, std::equal_to<std::string>>;
|
||||||
using Match = std::smatch;
|
using Match = std::smatch;
|
||||||
|
|
||||||
using DownloadProgress = std::function<bool(size_t current, size_t total)>;
|
using DownloadProgress = std::function<bool(size_t current, size_t total)>;
|
||||||
@@ -1079,9 +1362,16 @@ struct FormField {
|
|||||||
std::string content;
|
std::string content;
|
||||||
Headers headers;
|
Headers headers;
|
||||||
};
|
};
|
||||||
using FormFields = std::multimap<std::string, FormField>;
|
// 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<FormField, std::equal_to<std::string>>;
|
||||||
|
|
||||||
using FormFiles = std::multimap<std::string, FormData>;
|
using FormFiles =
|
||||||
|
detail::insertion_ordered_multimap<FormData, std::equal_to<std::string>>;
|
||||||
|
|
||||||
struct MultipartFormData {
|
struct MultipartFormData {
|
||||||
FormFields fields; // Text fields from multipart
|
FormFields fields; // Text fields from multipart
|
||||||
@@ -1514,6 +1804,7 @@ enum class Error {
|
|||||||
UnsupportedAddressFamily,
|
UnsupportedAddressFamily,
|
||||||
HTTPParsing,
|
HTTPParsing,
|
||||||
InvalidRangeHeader,
|
InvalidRangeHeader,
|
||||||
|
UnsupportedContentEncoding,
|
||||||
|
|
||||||
// For internal use only
|
// For internal use only
|
||||||
SSLPeerCouldBeClosed_,
|
SSLPeerCouldBeClosed_,
|
||||||
@@ -1545,6 +1836,18 @@ public:
|
|||||||
(void)usec;
|
(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 char *ptr);
|
||||||
ssize_t write(const std::string &s);
|
ssize_t write(const std::string &s);
|
||||||
|
|
||||||
@@ -2452,7 +2755,8 @@ protected:
|
|||||||
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
|
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
|
||||||
bool socket_should_be_closed_when_request_is_done_ = false;
|
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<std::string, std::string> addr_map_;
|
std::map<std::string, std::string> addr_map_;
|
||||||
|
|
||||||
// Default headers
|
// Default headers
|
||||||
@@ -3154,6 +3458,10 @@ private:
|
|||||||
std::string make_host_and_port_string(const std::string &host, int port,
|
std::string make_host_and_port_string(const std::string &host, int port,
|
||||||
bool is_ssl);
|
bool is_ssl);
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
bool check_and_write_headers(Stream &strm, Headers &headers, T header_writer,
|
||||||
|
Error &error);
|
||||||
|
|
||||||
std::string trim_copy(const std::string &s);
|
std::string trim_copy(const std::string &s);
|
||||||
|
|
||||||
void divide(
|
void divide(
|
||||||
@@ -3364,6 +3672,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void append(char c);
|
void append(char c);
|
||||||
|
void append(const char *data, size_t size);
|
||||||
|
|
||||||
Stream &strm_;
|
Stream &strm_;
|
||||||
char *fixed_buffer_;
|
char *fixed_buffer_;
|
||||||
@@ -3992,6 +4301,7 @@ public:
|
|||||||
private:
|
private:
|
||||||
void shutdown_and_close();
|
void shutdown_and_close();
|
||||||
bool create_stream(std::unique_ptr<Stream> &strm);
|
bool create_stream(std::unique_ptr<Stream> &strm);
|
||||||
|
void prepare_default_headers(Request &req);
|
||||||
|
|
||||||
std::string host_;
|
std::string host_;
|
||||||
int port_;
|
int port_;
|
||||||
@@ -4016,7 +4326,8 @@ private:
|
|||||||
time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
|
time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
|
||||||
std::string interface_;
|
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<std::string, std::string> addr_map_;
|
std::map<std::string, std::string> addr_map_;
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
|
|||||||
Reference in New Issue
Block a user