#endif
for (size_t i = 0;; i++) {
+ // Fast path: whatever the stream has already buffered can be scanned for
+ // the terminator in one pass. Asking for a byte at a time costs a virtual
+ // call, a bounds check and a one-byte copy per character of the request.
+ size_t buffered_size = 0;
+ if (auto buffered = strm_.buffered_data(buffered_size)) {
+ auto take = buffered_size;
+ auto terminated = false;
+
+ for (size_t at = 0; at < buffered_size;) {
+ auto nl = static_cast<const char *>(
+ memchr(buffered + at, '\n', buffered_size - at));
+ if (!nl) { break; }
+ auto pos = static_cast<size_t>(nl - buffered);
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ take = pos + 1;
+ terminated = true;
+ break;
+#else
+ // A bare LF does not end the line; keep looking for CRLF. The CR may
+ // be the last byte of an earlier chunk, hence prev_byte.
+ if ((pos > 0 ? buffered[pos - 1] : prev_byte) == '\r') {
+ take = pos + 1;
+ terminated = true;
+ break;
+ }
+ at = pos + 1;
+#endif
+ }
+
+ if (size() + take > CPPHTTPLIB_MAX_LINE_LENGTH) { return false; }
+#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
+ prev_byte = buffered[take - 1];
+#endif
+ append(buffered, take);
+ strm_.consume_buffered(take);
+ i += take;
+ if (terminated) { return true; }
+ continue;
+ }
+
if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
// Treat exceptionally long lines as an error to
// prevent infinite loops/memory exhaustion
return true;
}
-void stream_line_reader::append(char c) {
- if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
- fixed_buffer_[fixed_buffer_used_size_++] = c;
+void stream_line_reader::append(char c) { append(&c, 1); }
+
+void stream_line_reader::append(const char *data, size_t size) {
+ // Once the line has outgrown the fixed buffer everything must keep going to
+ // the growable one, even if a later chunk would have fit. Without the
+ // emptiness check a short append after a long one would land in the fixed
+ // buffer, which ptr() and size() no longer look at, and be lost.
+ if (growable_buffer_.empty() &&
+ fixed_buffer_used_size_ + size < fixed_buffer_size_) {
+ memcpy(fixed_buffer_ + fixed_buffer_used_size_, data, size);
+ fixed_buffer_used_size_ += size;
fixed_buffer_[fixed_buffer_used_size_] = '\0';
} else {
+ // Unlike the per-character overload, this can be the very first append of
+ // the line, so the fixed buffer may hold nothing and carry no terminator
+ // yet. assign() takes an explicit length and does not need one.
if (growable_buffer_.empty()) {
- assert(fixed_buffer_[fixed_buffer_used_size_] == '\0');
growable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_);
}
- growable_buffer_ += c;
+ growable_buffer_.append(data, size);
}
}
is_open_empty_file = true;
return false;
}
+
+ if (addr_ == MAP_FAILED) {
+ // Clear the sentinel before `close()`, since `is_open()` only checks
+ // `addr_` against nullptr and `munmap()` must not be called with it.
+ addr_ = nullptr;
+ close();
+ return false;
+ }
#endif
return true;
socket_t socket() const override;
time_t duration() const override;
void set_read_timeout(time_t sec, time_t usec = 0) override;
+ const char *buffered_data(size_t &size) const override;
+ void consume_buffered(size_t size) override;
+
+ // The caller has just seen this socket become readable. Lets the next read
+ // skip its own readiness wait, which would otherwise ask the kernel a
+ // question that was answered a moment ago. Consumed by that read.
+ void set_readable_hint() { readable_hint_ = true; }
private:
+ bool ensure_readable();
+
socket_t sock_;
time_t read_timeout_sec_;
time_t read_timeout_usec_;
std::vector<char> read_buff_;
size_t read_buff_off_ = 0;
size_t read_buff_content_size_ = 0;
+ bool readable_hint_ = false;
static const size_t read_buff_size_ = 1024l * 4;
};
[&](bool close_connection, bool &connection_closed) {
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
write_timeout_sec, write_timeout_usec);
+ // process_server_socket_core() only gets here once keep_alive() has
+ // seen the socket go readable.
+ strm.set_readable_hint();
return callback(strm, close_connection, connection_closed);
});
}
}
#endif
+bool contains_case_ignore(const std::string &s, const char *token) {
+ auto token_end = token + std::strlen(token);
+ return std::search(s.begin(), s.end(), token, token_end, [](char a, char b) {
+ return case_ignore::to_lower(a) == case_ignore::to_lower(b);
+ }) != s.end();
+}
+
+// Content codings are case-insensitive (RFC 9110 8.4.1). Matching them
+// case-sensitively would make a response labeled e.g. "GZIP" look like an
+// unknown coding, and its payload would be handed back still compressed.
+bool is_zlib_encoding(const std::string &encoding) {
+ return case_ignore::equal(encoding, "gzip") ||
+ case_ignore::equal(encoding, "deflate");
+}
+
+bool is_brotli_encoding(const std::string &encoding) {
+ return contains_case_ignore(encoding, "br");
+}
+
+bool is_zstd_encoding(const std::string &encoding) {
+ return contains_case_ignore(encoding, "zstd");
+}
+
+// Returns true if the content coding is one cpp-httplib is able to decompress
+// when the corresponding support is compiled in.
+bool is_known_content_encoding(const std::string &encoding) {
+ return is_zlib_encoding(encoding) || is_brotli_encoding(encoding) ||
+ is_zstd_encoding(encoding);
+}
+
std::unique_ptr<decompressor>
create_decompressor(const std::string &encoding) {
std::unique_ptr<decompressor> decompressor;
- if (encoding == "gzip" || encoding == "deflate") {
+ if (is_zlib_encoding(encoding)) {
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
decompressor = detail::make_unique<gzip_decompressor>();
#endif
- } else if (encoding.find("br") != std::string::npos) {
+ } else if (is_brotli_encoding(encoding)) {
#ifdef CPPHTTPLIB_BROTLI_SUPPORT
decompressor = detail::make_unique<brotli_decompressor>();
#endif
- } else if (encoding == "zstd" || encoding.find("zstd") != std::string::npos) {
+ } else if (is_zstd_encoding(encoding)) {
#ifdef CPPHTTPLIB_ZSTD_SUPPORT
decompressor = detail::make_unique<zstd_decompressor>();
#endif
size_t get_header_value_count(const Headers &headers,
const std::string &key) {
- auto r = headers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return headers.count(key);
}
template <typename Map>
bool is_chunked_transfer_encoding(const Headers &headers) {
// RFC 9112 6.1: a message is framed with the chunked coding when "chunked"
// is the final transfer coding. A single field value may list several
- // codings ("gzip, chunked"), and the list may be split across multiple
- // Transfer-Encoding header lines (RFC 9110 5.3). Match the last coding token
- // case-insensitively rather than comparing the whole value against "chunked".
+ // codings ("gzip, chunked"), and RFC 9110 5.3 lets that list be split across
+ // several Transfer-Encoding lines, which combine into one comma-separated
+ // list in the order the lines were received. Headers preserves that order,
+ // so the final coding is the last token of the last line. Match it
+ // case-insensitively rather than comparing the whole value against
+ // "chunked".
//
// Security: reading a chunked message as unframed leaves its body in the
// socket, where a keep-alive connection parses it as a smuggled request.
- // Headers is an unordered_multimap whose iteration order for duplicate keys
- // is not portable, so when there is more than one Transfer-Encoding line we
- // cannot tell which coding is truly final. In that ambiguous case we fail
- // safe by treating the message as chunked (a mis-parse just closes the
- // connection, whereas the opposite error enables smuggling).
+ // Server::process_request() answers 400 and closes when the final coding is
+ // not chunked, so a request whose framing cannot be determined never
+ // reaches the "no body" path.
auto rng = headers.equal_range("Transfer-Encoding");
+ if (rng.first == rng.second) { return false; }
- size_t line_count = 0;
- bool chunked_present = false;
- bool last_line_ends_with_chunked = false;
+ // Cleared per line, so a trailing line carrying no coding at all leaves the
+ // combined list ending in nothing rather than inheriting the line before it.
+ std::string last_coding;
for (auto it = rng.first; it != rng.second; ++it) {
- line_count++;
const auto &value = it->second;
-
- std::string last_coding;
- bool line_has_chunked = false;
+ last_coding.clear();
split(value.data(), value.data() + value.size(), ',',
- [&](const char *b, const char *e) {
- last_coding.assign(b, e);
- if (case_ignore::equal(last_coding, "chunked")) {
- line_has_chunked = true;
- }
- });
-
- if (line_has_chunked) { chunked_present = true; }
- last_line_ends_with_chunked = case_ignore::equal(last_coding, "chunked");
+ [&](const char *b, const char *e) { last_coding.assign(b, e); });
}
- if (line_count == 0) { return false; }
- if (line_count == 1) { return last_line_ends_with_chunked; }
- return chunked_present;
+ return case_ignore::equal(last_coding, "chunked");
}
template <typename T, typename U>
std::unique_ptr<decompressor> decompressor;
if (!encoding.empty()) {
+ // A coding we know about but were not built with is an error. An
+ // unrecognized coding (including "identity") is left alone and the
+ // payload is passed through as-is, since some servers misuse the header,
+ // e.g. by sending a character set such as "Content-Encoding: UTF-8".
decompressor = detail::create_decompressor(encoding);
- if (!decompressor) {
- // Unsupported encoding or no support compiled in
+ if (!decompressor && detail::is_known_content_encoding(encoding)) {
status = StatusCode::UnsupportedMediaType_415;
return false;
}
return query;
}
+// Splits one "key=value" span of a query string at its first '='. A span with
+// no '=' at all lands entirely in key, leaving val empty, which is how a bare
+// "?flag" keeps its name.
+void divide_query_pair(const char *b, const char *e, std::string &key,
+ std::string &val) {
+ divide(b, static_cast<std::size_t>(e - b), '=',
+ [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,
+ std::size_t rhs_size) {
+ key.assign(lhs_data, lhs_size);
+ val.assign(rhs_data, rhs_size);
+ });
+}
+
void parse_query_text(const char *data, std::size_t size,
Params ¶ms) {
std::set<std::string> cache;
std::string key;
std::string val;
- divide(b, static_cast<std::size_t>(e - b), '=',
- [&](const char *lhs_data, std::size_t lhs_size, const char *rhs_data,
- std::size_t rhs_size) {
- key.assign(lhs_data, lhs_size);
- val.assign(rhs_data, rhs_size);
- });
+ divide_query_pair(b, e, key, val);
if (!key.empty()) {
params.emplace(decode_query_component(key), decode_query_component(val));
// Normalize a query string by decoding and re-encoding each key/value pair
// while preserving the original parameter order. This avoids double-encoding
-// and ensures consistent encoding without reordering (unlike Params which
-// uses std::multimap and sorts keys).
+// and ensures consistent encoding. It works on the raw string rather than
+// parsing into Params and re-serializing, because that round trip cannot
+// reproduce the input: params_to_query_str() always emits '=', so a bare
+// "flag" would come back as "flag=", and parse_query_text() drops exactly
+// duplicated pairs.
std::string normalize_query_string(const std::string &query) {
std::string result;
split(query.data(), query.data() + query.size(), '&',
[&](const char *b, const char *e) {
std::string key;
std::string val;
- divide(b, static_cast<std::size_t>(e - b), '=',
- [&](const char *lhs_data, std::size_t lhs_size,
- const char *rhs_data, std::size_t rhs_size) {
- key.assign(lhs_data, lhs_size);
- val.assign(rhs_data, rhs_size);
- });
+ divide_query_pair(b, e, key, val);
if (!key.empty()) {
auto dec_key = decode_query_component(key);
return result;
}
+// Build the request target that goes on the wire from a caller-supplied path.
+// Shared by the buffered send path and the streaming API so that both put the
+// same bytes in the request line for the same input.
+std::string encode_request_target(const std::string &target,
+ bool path_encode) {
+ // `substr(0, npos)` yields the whole string, which is what the no-query
+ // case needs.
+ auto query_pos = target.find('?');
+ auto path_part = target.substr(0, query_pos);
+ std::string query_part;
+ if (query_pos != std::string::npos) {
+ query_part = target.substr(query_pos + 1);
+ }
+
+ auto result = path_encode ? encode_path(path_part) : std::move(path_part);
+
+ if (!query_part.empty()) {
+ // When path encoding is disabled the caller has supplied an already-encoded
+ // target and expects the exact bytes to be sent on the wire, so skip
+ // normalization for the query too. Normalizing would decode-then-re-encode
+ // it and corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`,
+ // which a strict RFC 3986 server decodes back as `+`, not a space).
+ if (path_encode) {
+ auto normalized = normalize_query_string(query_part);
+ if (!normalized.empty()) {
+ result += '?';
+ result += normalized;
+ }
+ } else {
+ result += '?';
+ result += query_part;
+ }
+ }
+
+ return result;
+}
+
bool parse_multipart_boundary(const std::string &content_type,
std::string &boundary) {
std::map<std::string, std::string> params;
} // namespace fields
-bool perform_websocket_handshake(Stream &strm, const std::string &host,
- int port, bool is_ssl,
- const std::string &path,
- const Headers &headers,
+bool perform_websocket_handshake(Stream &strm, Request &req,
std::string &selected_subprotocol) {
- // Validate path and host
- if (!fields::is_field_value(path) || !fields::is_field_value(host)) {
- return false;
- }
-
- // Validate user-provided headers
- for (const auto &h : headers) {
- if (!fields::is_field_valid(h.first, h.second)) { return false; }
- }
-
// Generate random Sec-WebSocket-Key
thread_local std::mt19937 rng(std::random_device{}());
std::string key_bytes(16, '\0');
}
auto client_key = base64_encode(key_bytes);
- // Build upgrade request
- std::string req_str = "GET " + path + " HTTP/1.1\r\n";
- req_str += "Host: " + make_host_and_port_string(host, port, is_ssl) + "\r\n";
- req_str += "Upgrade: websocket\r\n";
- req_str += "Connection: Upgrade\r\n";
- req_str += "Sec-WebSocket-Key: " + client_key + "\r\n";
- req_str += "Sec-WebSocket-Version: 13\r\n";
- for (const auto &h : headers) {
- req_str += h.first + ": " + h.second + "\r\n";
+ req.headers.erase("Upgrade");
+ req.headers.erase("Connection");
+ req.headers.erase("Sec-WebSocket-Key");
+ req.headers.erase("Sec-WebSocket-Version");
+ req.headers.emplace("Upgrade", "websocket");
+ req.headers.emplace("Connection", "Upgrade");
+ req.headers.emplace("Sec-WebSocket-Key", client_key);
+ req.headers.emplace("Sec-WebSocket-Version", "13");
+
+ // Build the request in memory first, like ClientImpl::write_request does.
+ // Writing straight to the socket would leak a request line onto the wire
+ // before check_and_write_headers gets a chance to reject an invalid header,
+ // and would emit one small write per header.
+ BufferStream bstrm;
+
+ if (write_request_line(bstrm, req.method, req.path) < 0) { return false; }
+
+ auto error = Error::Success;
+ if (!check_and_write_headers(bstrm, req.headers, write_headers, error)) {
+ return false;
}
- req_str += "\r\n";
- if (strm.write(req_str.data(), req_str.size()) < 0) { return false; }
+ const auto &data = bstrm.get_buffer();
+ if (!write_data(strm, data.data(), data.size())) { return false; }
// Verify 101 response and Sec-WebSocket-Accept header
auto expected_accept = websocket_accept_key(client_key);
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
/*
time_t duration() const override;
void set_read_timeout(time_t sec, time_t usec = 0) override;
+ // See SocketStream::set_readable_hint().
+ void set_readable_hint() { readable_hint_ = true; }
+
private:
+ bool ensure_readable();
+
socket_t sock_;
tls::session_t session_;
time_t read_timeout_sec_;
time_t write_timeout_usec_;
time_t max_timeout_msec_;
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
+ bool readable_hint_ = false;
};
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
}
#endif
-bool is_ip_address(const std::string &host) {
- struct in_addr addr4;
- struct in6_addr addr6;
- return inet_pton(AF_INET, host.c_str(), &addr4) == 1 ||
- inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
-}
-
template <typename T>
bool process_server_socket_ssl(
const std::atomic<socket_t> &svr_sock, tls::session_t session,
[&](bool close_connection, bool &connection_closed) {
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
write_timeout_sec, write_timeout_usec);
+ // See the non-TLS path in process_server_socket().
+ strm.set_readable_hint();
return callback(strm, close_connection, connection_closed);
});
}
case Error::UnsupportedAddressFamily: return "Unsupported address family";
case Error::HTTPParsing: return "HTTP parsing failed";
case Error::InvalidRangeHeader: return "Invalid Range header";
+ case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
default: break;
}
}
size_t Request::get_trailer_value_count(const std::string &key) const {
- auto r = trailers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return trailers.count(key);
}
bool Request::has_param(const std::string &key) const {
}
size_t Request::get_param_value_count(const std::string &key) const {
- auto r = params.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return params.count(key);
}
bool Request::is_multipart_form_data() const {
}
size_t MultipartFormData::get_field_count(const std::string &key) const {
- auto r = fields.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return fields.count(key);
}
FormData MultipartFormData::get_file(const std::string &key,
}
size_t MultipartFormData::get_file_count(const std::string &key) const {
- auto r = files.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return files.count(key);
}
// Multipart FormData writer implementation
}
size_t Response::get_trailer_value_count(const std::string &key) const {
- auto r = trailers.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return trailers.count(key);
}
void Response::set_redirect(const std::string &url, int stat) {
size_t
Result::get_request_header_value_count(const std::string &key) const {
- auto r = request_headers_.equal_range(key);
- return static_cast<size_t>(std::distance(r.first, r.second));
+ return request_headers_.count(key);
}
// Stream implementation
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
}
+bool SocketStream::ensure_readable() {
+ if (readable_hint_) {
+ readable_hint_ = false;
+ return true;
+ }
+ return wait_readable();
+}
+
+const char *SocketStream::buffered_data(size_t &size) const {
+ size = read_buff_content_size_ - read_buff_off_;
+ return size ? read_buff_.data() + read_buff_off_ : nullptr;
+}
+
+void SocketStream::consume_buffered(size_t size) {
+ assert(size <= read_buff_content_size_ - read_buff_off_);
+ read_buff_off_ += size;
+}
+
bool SocketStream::is_peer_alive() const {
return detail::is_socket_alive(sock_);
}
}
}
- if (!wait_readable()) {
+ if (!ensure_readable()) {
error_ = Error::Timeout;
return -1;
}
!tls::is_peer_closed(session_, sock_);
}
+bool SSLSocketStream::ensure_readable() {
+ if (readable_hint_) {
+ readable_hint_ = false;
+ return true;
+ }
+ return wait_readable();
+}
+
bool SSLSocketStream::is_peer_alive() const {
return !tls::is_peer_closed(session_, sock_);
}
error_ = Error::ConnectionClosed;
}
return ret;
- } else if (wait_readable()) {
+ } else if (ensure_readable()) {
tls::TlsError err;
auto ret = tls::read(session_, ptr, size, err);
if (ret < 0) {
}
void Server::stop() noexcept {
- if (is_running_) {
- assert(svr_sock_ != INVALID_SOCKET);
- std::atomic<socket_t> sock(svr_sock_.exchange(INVALID_SOCKET));
+ // Release the listening socket whether or not the accept loop is running:
+ // bind_to_port() without listen_after_bind() still owns the descriptor. The
+ // exchange is what makes this safe to call concurrently with the accept loop.
+ socket_t sock = svr_sock_.exchange(INVALID_SOCKET);
+ if (sock != INVALID_SOCKET) {
detail::shutdown_socket(sock);
detail::close_socket(sock);
}
};
if (res.content_length_ > 0) {
- if (req.ranges.empty()) {
+ // Only a 206 response is served as a partial representation, matching the
+ // condition `apply_ranges()` used to decide the Content-Length and the
+ // multipart boundary. Since `detail::range_error()` validates `req.ranges`
+ // only for a 2xx status, slicing under any other status would write a body
+ // that disagrees with the header already sent, from an unchecked offset.
+ auto is_partial =
+ !req.ranges.empty() && res.status == StatusCode::PartialContent_206;
+
+ if (!is_partial) {
return detail::write_content(strm, res.content_provider_, 0,
res.content_length_, is_shutting_down);
} else if (req.ranges.size() == 1) {
}
bool Server::listen_internal() {
- if (is_decommissioned) { return false; }
+ // A stop() between bind and listen leaves nothing to accept on. Report
+ // failure instead of returning success without ever serving, and mark the
+ // server decommissioned the way any failed listen does so that a concurrent
+ // wait_until_ready() wakes up instead of spinning forever.
+ if (is_decommissioned || svr_sock_ == INVALID_SOCKET) {
+ is_decommissioned = true;
+ return false;
+ }
auto ret = true;
is_running_ = true;
return write_response(strm, close_connection, req, res);
}
- // RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
- // any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
- // tolerated for compatibility with existing clients.
- if (req.get_header_value_u64("Content-Length") > 0 &&
- req.has_header("Transfer-Encoding")) {
+ // RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
+ // otherwise let an intermediary and this parser disagree on where the body
+ // ends and enable request smuggling. Two cases: a non-zero Content-Length
+ // alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
+ // compatibility with existing clients), and a Transfer-Encoding whose final
+ // coding is not chunked, which leaves the body length undeterminable. The
+ // latter must not fall through to the "no body" path, or the body bytes are
+ // parsed as the next request on a persistent connection.
+ if (req.has_header("Transfer-Encoding") &&
+ (req.get_header_value_u64("Content-Length") > 0 ||
+ !detail::is_chunked_transfer_encoding(req.headers))) {
connection_closed = true;
res.status = StatusCode::BadRequest_400;
return write_response(strm, close_connection, req, res);
write_timeout_sec_, write_timeout_usec_, interface_, error);
}
- // Check is custom IP specified for host_
+ // Check is custom IP or hostname specified for host_
+ std::string connect_host;
std::string ip;
- auto it = addr_map_.find(host_);
- if (it != addr_map_.end()) { ip = it->second; }
+ detail::apply_addr_map(addr_map_, host_, connect_host, ip);
return detail::create_client_socket(
- host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
+ connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
write_timeout_usec_, interface_, error);
if (!r.has_header(header.first)) { r.headers.insert(header); }
}
+ // RFC 9110 5.3 recommends sending control data such as Host first, so
+ // prepend it rather than appending it after the caller's own fields.
if (!r.has_header("Host")) {
if (address_family_ == AF_UNIX) {
- r.headers.emplace("Host", "localhost");
+ r.headers.emplace_front("Host", "localhost");
} else {
- r.headers.emplace(
+ r.headers.emplace_front(
"Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
}
}
handle.response = detail::make_unique<Response>();
handle.error = Error::Success;
- auto query_path = params.empty() ? path : append_query_params(path, params);
+ // Encode the target exactly like the buffered send path does, so that the
+ // same `path` produces the same request line through either API.
+ auto raw_query_path =
+ params.empty() ? path : append_query_params(path, params);
+ auto query_path = detail::encode_request_target(raw_query_path, path_encode_);
+
handle.connection_ = detail::make_unique<ClientConnection>();
{
auto content_encoding = handle.response->get_header_value("Content-Encoding");
if (!content_encoding.empty()) {
+ // Same policy as prepare_content_receiver(): reject a coding we know about
+ // but were not built with, pass an unrecognized one through as-is.
handle.decompressor_ = detail::create_decompressor(content_encoding);
+ if (!handle.decompressor_) {
+ if (detail::is_known_content_encoding(content_encoding)) {
+ handle.error = Error::UnsupportedContentEncoding;
+ handle.response.reset();
+ return handle;
+ }
+ } else if (!handle.decompressor_->is_valid()) {
+ handle.error = Error::Compression;
+ handle.response.reset();
+ return handle;
+ }
}
return handle;
{
detail::BufferStream bstrm;
- // Extract path and query from req.path
- std::string path_part, query_part;
+ // Extract the query from req.path. The encoding itself is delegated to
+ // `encode_request_target`; the raw query is still needed here to decide
+ // between populating `req.params` from it and falling back to building a
+ // query out of caller-supplied `req.params`.
auto query_pos = req.path.find('?');
- if (query_pos != std::string::npos) {
- path_part = req.path.substr(0, query_pos);
- query_part = req.path.substr(query_pos + 1);
- } else {
- path_part = req.path;
- query_part = "";
- }
+ auto query_part = query_pos == std::string::npos
+ ? std::string()
+ : req.path.substr(query_pos + 1);
- // Encode path part. If the original `req.path` already contained a
- // query component, preserve its raw query string (including parameter
- // order) instead of reparsing and reassembling it which may reorder
- // parameters due to container ordering (e.g. `Params` uses
- // `std::multimap`). When there is no query in `req.path`, fall back to
- // building a query from `req.params` so existing callers that pass
- // `Params` continue to work.
auto path_with_query =
- path_encode_ ? detail::encode_path(path_part) : path_part;
+ detail::encode_request_target(req.path, path_encode_);
if (!query_part.empty()) {
- // Normalize the query string (decode then re-encode) while preserving
- // the original parameter order. When path encoding is disabled the
- // caller has supplied an already-encoded target and expects the exact
- // bytes to be sent on the wire, so skip normalization for the query
- // too. Normalizing here would decode-then-re-encode the query and
- // corrupt pre-encoded binary payloads (e.g. turning `%20` into `+`,
- // which a strict RFC 3986 server decodes back as `+`, not a space).
- if (path_encode_) {
- auto normalized = detail::normalize_query_string(query_part);
- if (!normalized.empty()) { path_with_query += '?' + normalized; }
- } else {
- path_with_query += '?' + query_part;
- }
-
- // Still populate req.params for handlers/users who read them.
+ // The query already came in through `req.path`; still populate
+ // `req.params` for handlers/users who read them.
detail::parse_query_text(query_part, req.params);
- } else {
- // No query in path; parse any query_part (empty) and append params
- // from `req.params` when present (preserves prior behavior for
- // callers who provide Params separately).
- detail::parse_query_text(query_part, req.params);
- if (!req.params.empty()) {
- path_with_query = append_query_params(path_with_query, req.params);
- }
+ } else if (!req.params.empty()) {
+ // No query in `req.path`; build one from `req.params` so existing
+ // callers that pass `Params` separately continue to work.
+ path_with_query = append_query_params(path_with_query, req.params);
}
// Write request line and headers
}
if (res.status != StatusCode::NotModified_304) {
- int dummy_status;
+ auto content_status = 0;
auto max_length = (!has_payload_max_length_ && req.content_receiver)
? (std::numeric_limits<size_t>::max)()
: payload_max_length_;
- if (!detail::read_content(strm, res, max_length, dummy_status,
+ if (!detail::read_content(strm, res, max_length, content_status,
std::move(progress), std::move(out),
decompress_)) {
- if (error != Error::Canceled) { error = Error::Read; }
+ if (error != Error::Canceled) {
+ // Tell the caller apart from a plain read failure when the body could
+ // not be decoded because of its Content-Encoding.
+ switch (content_status) {
+ case StatusCode::UnsupportedMediaType_415:
+ error = Error::UnsupportedContentEncoding;
+ break;
+ case StatusCode::InternalServerError_500:
+ error = Error::Compression;
+ break;
+ default: error = Error::Read; break;
+ }
+ }
output_error_log(error, &req);
return false;
}
return true;
}
+void WebSocketClient::prepare_default_headers(Request &req) {
+#ifdef CPPHTTPLIB_SSL_ENABLED
+ auto is_ssl = is_ssl_;
+#else
+ auto is_ssl = false;
+#endif
+
+ if (!req.has_header("Host")) {
+ if (address_family_ == AF_UNIX) {
+ req.headers.emplace("Host", "localhost");
+ } else {
+ req.headers.emplace(
+ "Host", detail::make_host_and_port_string(host_, port_, is_ssl));
+ }
+ }
+
+#ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT
+ if (!req.has_header("User-Agent")) {
+ auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION;
+ req.set_header("User-Agent", agent);
+ }
+#endif
+}
+
bool WebSocketClient::connect() {
if (!is_valid_) { return false; }
shutdown_and_close();
- // Check is custom IP specified for host_
+ // Check is custom IP or hostname specified for host_
+ std::string connect_host;
std::string ip;
- auto it = addr_map_.find(host_);
- if (it != addr_map_.end()) { ip = it->second; }
+ detail::apply_addr_map(addr_map_, host_, connect_host, ip);
Error error;
sock_ = detail::create_client_socket(
- host_, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
+ connect_host, ip, port_, address_family_, tcp_nodelay_, ipv6_v6only_,
socket_options_, connection_timeout_sec_, connection_timeout_usec_,
read_timeout_sec_, read_timeout_usec_, write_timeout_sec_,
write_timeout_usec_, interface_, error);
return false;
}
-#ifdef CPPHTTPLIB_SSL_ENABLED
- auto is_ssl = is_ssl_;
-#else
- auto is_ssl = false;
-#endif
+ Request req;
+ req.method = "GET";
+ req.path = path_;
+ req.headers = headers_;
+ prepare_default_headers(req);
std::string selected_subprotocol;
- if (!detail::perform_websocket_handshake(*strm, host_, port_, is_ssl, path_,
- headers_, selected_subprotocol)) {
+ if (!detail::perform_websocket_handshake(*strm, req, selected_subprotocol)) {
shutdown_and_close();
return false;
}
subprotocol_ = std::move(selected_subprotocol);
- Request req;
- req.method = "GET";
- req.path = path_;
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
websocket_ping_interval_sec_,
websocket_max_missed_pongs_));
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
-#define CPPHTTPLIB_VERSION "0.51.0"
-#define CPPHTTPLIB_VERSION_NUM "0x003300"
+#define CPPHTTPLIB_VERSION "0.52.0"
+#define CPPHTTPLIB_VERSION_NUM "0x003400"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
#endif
#ifndef CPPHTTPLIB_LISTEN_BACKLOG
-#define CPPHTTPLIB_LISTEN_BACKLOG 5
+#define CPPHTTPLIB_LISTEN_BACKLOG 128
#endif
#ifndef CPPHTTPLIB_MAX_LINE_LENGTH
#include <functional>
#include <iomanip>
#include <iostream>
+#include <iterator>
#include <list>
#include <map>
#include <memory>
#include <sys/stat.h>
#include <system_error>
#include <thread>
+#include <type_traits>
#include <unordered_map>
#include <unordered_set>
#include <utility>
+#include <vector>
// 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
NetworkAuthenticationRequired_511 = 511,
};
+namespace detail {
+
+// A multimap that keeps its entries in the order they were inserted.
+//
+// HTTP needs that order in two places. RFC 9110 5.3 makes the order of header
+// fields sharing a field name significant and forbids a proxy from reordering
+// them, and a query string's parameters are meaningful in the order the caller
+// wrote them. Neither standard container expresses it: std::unordered_multimap
+// gives no ordering guarantee at all for equivalent keys (libstdc++ yields
+// reverse insertion order, libc++ insertion order), and std::multimap sorts by
+// key, which would drop control data such as Host behind whatever else the
+// message carries and alphabetise a query string.
+//
+// Entries are therefore kept in a flat vector, in order. Lookup is a linear
+// scan, which beats hashing for the handful of entries a message carries
+// (headers are capped at CPPHTTPLIB_HEADER_MAX_COUNT).
+//
+// KeyEqual compares keys; it is what makes Headers case-insensitive and
+// Params, whose parameter names are case-sensitive, not.
+template <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 =
- std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
- detail::case_ignore::equal_to>;
+ detail::insertion_ordered_multimap<std::string,
+ detail::case_ignore::equal_to>;
-using Params = std::multimap<std::string, std::string>;
+// 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 DownloadProgress = std::function<bool(size_t current, size_t total)>;
std::string content;
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 {
FormFields fields; // Text fields from multipart
UnsupportedAddressFamily,
HTTPParsing,
InvalidRangeHeader,
+ UnsupportedContentEncoding,
// For internal use only
SSLPeerCouldBeClosed_,
(void)usec;
}
+ // Bytes already pulled off the socket and sitting in this stream's own
+ // buffer. Exposing them lets a line reader scan for a terminator in one
+ // pass instead of asking for a byte at a time. A stream that does no
+ // buffering of its own reports none, and readers fall back to read().
+ virtual const char *buffered_data(size_t &size) const {
+ size = 0;
+ return nullptr;
+ }
+
+ // Discards `size` bytes previously returned by buffered_data().
+ virtual void consume_buffered(size_t size) { (void)size; }
+
ssize_t write(const char *ptr);
ssize_t write(const std::string &s);
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
bool socket_should_be_closed_when_request_is_done_ = false;
- // Hostname-IP map
+ // Hostname to connection target map. The value is an IP literal or another
+ // hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
// Default headers
std::string make_host_and_port_string(const std::string &host, int port,
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);
void divide(
private:
void append(char c);
+ void append(const char *data, size_t size);
Stream &strm_;
char *fixed_buffer_;
private:
void shutdown_and_close();
bool create_stream(std::unique_ptr<Stream> &strm);
+ void prepare_default_headers(Request &req);
std::string host_;
int port_;
time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND;
std::string interface_;
- // Hostname-IP map
+ // Hostname to connection target map. The value is an IP literal or another
+ // hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
#ifdef CPPHTTPLIB_SSL_ENABLED