|
|
@@ -321,6 +321,7 @@ using socket_t = int;
|
|
|
#include <functional>
|
|
|
#include <iomanip>
|
|
|
#include <iostream>
|
|
|
+#include <iterator>
|
|
|
#include <list>
|
|
|
#include <map>
|
|
|
#include <memory>
|
|
|
@@ -333,9 +334,11 @@ using socket_t = int;
|
|
|
#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
|
|
|
@@ -968,9 +971,263 @@ enum StatusCode {
|
|
|
NetworkAuthenticationRequired_511 = 511,
|
|
|
};
|
|
|
|
|
|
-using Headers =
|
|
|
- std::unordered_multimap<std::string, std::string, detail::case_ignore::hash,
|
|
|
- detail::case_ignore::equal_to>;
|
|
|
+// RFC 9110 5.3: the order in which header fields with the same field name are
|
|
|
+// received is significant, and a proxy must not reorder them. Neither
|
|
|
+// std::unordered_multimap (no ordering guarantee at all for equivalent keys:
|
|
|
+// libstdc++ yields reverse insertion order, libc++ insertion order) nor
|
|
|
+// std::multimap (sorts by field name, so control data such as Host no longer
|
|
|
+// leads the message) can express that, so header fields are kept in a flat
|
|
|
+// vector in the order they were received or set. Lookup is a linear scan,
|
|
|
+// which beats hashing for the at most CPPHTTPLIB_HEADER_MAX_COUNT fields a
|
|
|
+// message carries.
|
|
|
+class Headers {
|
|
|
+public:
|
|
|
+ using key_type = std::string;
|
|
|
+ using mapped_type = std::string;
|
|
|
+ using value_type = std::pair<std::string, std::string>;
|
|
|
+ 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); }
|
|
|
+
|
|
|
+ // Iterating a Headers yields every field in insertion order, but
|
|
|
+ // equal_range() and find() have to walk only the fields sharing one name,
|
|
|
+ // 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 name still compares equal to end().
|
|
|
+ template <typename V> class iterator_t {
|
|
|
+ public:
|
|
|
+ using iterator_category = std::bidirectional_iterator_tag;
|
|
|
+ using value_type = Headers::value_type;
|
|
|
+ using difference_type = Headers::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 field of a name (which
|
|
|
+ // get_header_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 Headers;
|
|
|
+ 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 detail::case_ignore::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>;
|
|
|
+
|
|
|
+ Headers() = default;
|
|
|
+ Headers(std::initializer_list<value_type> il) : entries_(il) {}
|
|
|
+ template <typename InputIt>
|
|
|
+ Headers(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(Headers &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());
|
|
|
+ }
|
|
|
+
|
|
|
+ // RFC 9110 5.3 recommends sending control data such as Host 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 (detail::case_ignore::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 detail::case_ignore::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 fields with that name, 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 (!detail::case_ignore::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 Headers &lhs, const Headers &rhs) {
|
|
|
+ return lhs.entries_ == rhs.entries_;
|
|
|
+ }
|
|
|
+
|
|
|
+ friend bool operator!=(const Headers &lhs, const Headers &rhs) {
|
|
|
+ return !(lhs == rhs);
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ size_type index_of(const std::string &key) const {
|
|
|
+ for (size_type i = 0; i < entries_.size(); i++) {
|
|
|
+ if (detail::case_ignore::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_;
|
|
|
+};
|
|
|
|
|
|
using Params = std::multimap<std::string, std::string>;
|
|
|
using Match = std::smatch;
|
|
|
@@ -1514,6 +1771,7 @@ enum class Error {
|
|
|
UnsupportedAddressFamily,
|
|
|
HTTPParsing,
|
|
|
InvalidRangeHeader,
|
|
|
+ UnsupportedContentEncoding,
|
|
|
|
|
|
// For internal use only
|
|
|
SSLPeerCouldBeClosed_,
|
|
|
@@ -1545,6 +1803,18 @@ public:
|
|
|
(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);
|
|
|
|
|
|
@@ -3369,6 +3639,7 @@ public:
|
|
|
|
|
|
private:
|
|
|
void append(char c);
|
|
|
+ void append(const char *data, size_t size);
|
|
|
|
|
|
Stream &strm_;
|
|
|
char *fixed_buffer_;
|
|
|
@@ -5462,6 +5733,46 @@ inline bool stream_line_reader::getline() {
|
|
|
#endif
|
|
|
|
|
|
for (size_t i = 0;; i++) {
|
|
|
+ // Fast path: whatever the stream has already buffered can be scanned for
|
|
|
+ // the terminator in one pass. Asking for a byte at a time costs a virtual
|
|
|
+ // call, a bounds check and a one-byte copy per character of the request.
|
|
|
+ size_t buffered_size = 0;
|
|
|
+ if (auto buffered = strm_.buffered_data(buffered_size)) {
|
|
|
+ auto take = buffered_size;
|
|
|
+ auto terminated = false;
|
|
|
+
|
|
|
+ for (size_t at = 0; at < buffered_size;) {
|
|
|
+ auto nl = static_cast<const char *>(
|
|
|
+ memchr(buffered + at, '\n', buffered_size - at));
|
|
|
+ if (!nl) { break; }
|
|
|
+ auto pos = static_cast<size_t>(nl - buffered);
|
|
|
+#ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
|
|
+ take = pos + 1;
|
|
|
+ terminated = true;
|
|
|
+ break;
|
|
|
+#else
|
|
|
+ // A bare LF does not end the line; keep looking for CRLF. The CR may
|
|
|
+ // be the last byte of an earlier chunk, hence prev_byte.
|
|
|
+ if ((pos > 0 ? buffered[pos - 1] : prev_byte) == '\r') {
|
|
|
+ take = pos + 1;
|
|
|
+ terminated = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ at = pos + 1;
|
|
|
+#endif
|
|
|
+ }
|
|
|
+
|
|
|
+ if (size() + take > CPPHTTPLIB_MAX_LINE_LENGTH) { return false; }
|
|
|
+#ifndef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR
|
|
|
+ prev_byte = buffered[take - 1];
|
|
|
+#endif
|
|
|
+ append(buffered, take);
|
|
|
+ strm_.consume_buffered(take);
|
|
|
+ i += take;
|
|
|
+ if (terminated) { return true; }
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
if (size() >= CPPHTTPLIB_MAX_LINE_LENGTH) {
|
|
|
// Treat exceptionally long lines as an error to
|
|
|
// prevent infinite loops/memory exhaustion
|
|
|
@@ -5493,16 +5804,26 @@ inline bool stream_line_reader::getline() {
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
-inline void stream_line_reader::append(char c) {
|
|
|
- if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) {
|
|
|
- fixed_buffer_[fixed_buffer_used_size_++] = c;
|
|
|
+inline void stream_line_reader::append(char c) { append(&c, 1); }
|
|
|
+
|
|
|
+inline 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);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -5575,6 +5896,14 @@ inline bool mmap::open(const char *path) {
|
|
|
is_open_empty_file = true;
|
|
|
return false;
|
|
|
}
|
|
|
+
|
|
|
+ if (addr_ == MAP_FAILED) {
|
|
|
+ // Clear the sentinel before `close()`, since `is_open()` only checks
|
|
|
+ // `addr_` against nullptr and `munmap()` must not be called with it.
|
|
|
+ addr_ = nullptr;
|
|
|
+ close();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
#endif
|
|
|
|
|
|
return true;
|
|
|
@@ -5752,8 +6081,17 @@ public:
|
|
|
socket_t socket() const override;
|
|
|
time_t duration() const override;
|
|
|
void set_read_timeout(time_t sec, time_t usec = 0) override;
|
|
|
+ const char *buffered_data(size_t &size) const override;
|
|
|
+ void consume_buffered(size_t size) override;
|
|
|
+
|
|
|
+ // The caller has just seen this socket become readable. Lets the next read
|
|
|
+ // skip its own readiness wait, which would otherwise ask the kernel a
|
|
|
+ // question that was answered a moment ago. Consumed by that read.
|
|
|
+ void set_readable_hint() { readable_hint_ = true; }
|
|
|
|
|
|
private:
|
|
|
+ bool ensure_readable();
|
|
|
+
|
|
|
socket_t sock_;
|
|
|
time_t read_timeout_sec_;
|
|
|
time_t read_timeout_usec_;
|
|
|
@@ -5765,6 +6103,7 @@ private:
|
|
|
std::vector<char> read_buff_;
|
|
|
size_t read_buff_off_ = 0;
|
|
|
size_t read_buff_content_size_ = 0;
|
|
|
+ bool readable_hint_ = false;
|
|
|
|
|
|
static const size_t read_buff_size_ = 1024l * 4;
|
|
|
};
|
|
|
@@ -5832,6 +6171,9 @@ process_server_socket(const std::atomic<socket_t> &svr_sock, socket_t sock,
|
|
|
[&](bool close_connection, bool &connection_closed) {
|
|
|
SocketStream strm(sock, read_timeout_sec, read_timeout_usec,
|
|
|
write_timeout_sec, write_timeout_usec);
|
|
|
+ // process_server_socket_core() only gets here once keep_alive() has
|
|
|
+ // seen the socket go readable.
|
|
|
+ strm.set_readable_hint();
|
|
|
return callback(strm, close_connection, connection_closed);
|
|
|
});
|
|
|
}
|
|
|
@@ -7121,19 +7463,49 @@ inline bool zstd_decompressor::decompress(const char *data, size_t data_length,
|
|
|
}
|
|
|
#endif
|
|
|
|
|
|
+inline 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.
|
|
|
+inline bool is_zlib_encoding(const std::string &encoding) {
|
|
|
+ return case_ignore::equal(encoding, "gzip") ||
|
|
|
+ case_ignore::equal(encoding, "deflate");
|
|
|
+}
|
|
|
+
|
|
|
+inline bool is_brotli_encoding(const std::string &encoding) {
|
|
|
+ return contains_case_ignore(encoding, "br");
|
|
|
+}
|
|
|
+
|
|
|
+inline 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.
|
|
|
+inline bool is_known_content_encoding(const std::string &encoding) {
|
|
|
+ return is_zlib_encoding(encoding) || is_brotli_encoding(encoding) ||
|
|
|
+ is_zstd_encoding(encoding);
|
|
|
+}
|
|
|
+
|
|
|
inline 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
|
|
|
@@ -7420,44 +7792,33 @@ inline ReadContentResult read_content_chunked(Stream &strm, T &x,
|
|
|
inline 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>
|
|
|
@@ -7470,9 +7831,12 @@ bool prepare_content_receiver(T &x, int &status,
|
|
|
std::unique_ptr<decompressor> decompressor;
|
|
|
|
|
|
if (!encoding.empty()) {
|
|
|
+ // A coding we know about but were not built with is an error. An
|
|
|
+ // unrecognized coding (including "identity") is left alone and the
|
|
|
+ // payload is passed through as-is, since some servers misuse the header,
|
|
|
+ // e.g. by sending a character set such as "Content-Encoding: UTF-8".
|
|
|
decompressor = detail::create_decompressor(encoding);
|
|
|
- if (!decompressor) {
|
|
|
- // Unsupported encoding or no support compiled in
|
|
|
+ if (!decompressor && detail::is_known_content_encoding(encoding)) {
|
|
|
status = StatusCode::UnsupportedMediaType_415;
|
|
|
return false;
|
|
|
}
|
|
|
@@ -9162,7 +9526,12 @@ public:
|
|
|
time_t duration() const override;
|
|
|
void set_read_timeout(time_t sec, time_t usec = 0) override;
|
|
|
|
|
|
+ // See SocketStream::set_readable_hint().
|
|
|
+ void set_readable_hint() { readable_hint_ = true; }
|
|
|
+
|
|
|
private:
|
|
|
+ bool ensure_readable();
|
|
|
+
|
|
|
socket_t sock_;
|
|
|
tls::session_t session_;
|
|
|
time_t read_timeout_sec_;
|
|
|
@@ -9171,6 +9540,7 @@ private:
|
|
|
time_t write_timeout_usec_;
|
|
|
time_t max_timeout_msec_;
|
|
|
const std::chrono::time_point<std::chrono::steady_clock> start_time_;
|
|
|
+ bool readable_hint_ = false;
|
|
|
};
|
|
|
|
|
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
|
|
@@ -9325,6 +9695,8 @@ inline bool process_server_socket_ssl(
|
|
|
[&](bool close_connection, bool &connection_closed) {
|
|
|
SSLSocketStream strm(sock, session, read_timeout_sec, read_timeout_usec,
|
|
|
write_timeout_sec, write_timeout_usec);
|
|
|
+ // See the non-TLS path in process_server_socket().
|
|
|
+ strm.set_readable_hint();
|
|
|
return callback(strm, close_connection, connection_closed);
|
|
|
});
|
|
|
}
|
|
|
@@ -9776,6 +10148,7 @@ inline std::string to_string(const Error error) {
|
|
|
case Error::UnsupportedAddressFamily: return "Unsupported address family";
|
|
|
case Error::HTTPParsing: return "HTTP parsing failed";
|
|
|
case Error::InvalidRangeHeader: return "Invalid Range header";
|
|
|
+ case Error::UnsupportedContentEncoding: return "Unsupported Content-Encoding";
|
|
|
default: break;
|
|
|
}
|
|
|
|
|
|
@@ -10706,6 +11079,24 @@ inline bool SocketStream::wait_writable() const {
|
|
|
return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0;
|
|
|
}
|
|
|
|
|
|
+inline bool SocketStream::ensure_readable() {
|
|
|
+ if (readable_hint_) {
|
|
|
+ readable_hint_ = false;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return wait_readable();
|
|
|
+}
|
|
|
+
|
|
|
+inline 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;
|
|
|
+}
|
|
|
+
|
|
|
+inline void SocketStream::consume_buffered(size_t size) {
|
|
|
+ assert(size <= read_buff_content_size_ - read_buff_off_);
|
|
|
+ read_buff_off_ += size;
|
|
|
+}
|
|
|
+
|
|
|
inline bool SocketStream::is_peer_alive() const {
|
|
|
return detail::is_socket_alive(sock_);
|
|
|
}
|
|
|
@@ -10732,7 +11123,7 @@ inline ssize_t SocketStream::read(char *ptr, size_t size) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (!wait_readable()) {
|
|
|
+ if (!ensure_readable()) {
|
|
|
error_ = Error::Timeout;
|
|
|
return -1;
|
|
|
}
|
|
|
@@ -11210,6 +11601,14 @@ inline bool SSLSocketStream::wait_writable() const {
|
|
|
!tls::is_peer_closed(session_, sock_);
|
|
|
}
|
|
|
|
|
|
+inline bool SSLSocketStream::ensure_readable() {
|
|
|
+ if (readable_hint_) {
|
|
|
+ readable_hint_ = false;
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return wait_readable();
|
|
|
+}
|
|
|
+
|
|
|
inline bool SSLSocketStream::is_peer_alive() const {
|
|
|
return !tls::is_peer_closed(session_, sock_);
|
|
|
}
|
|
|
@@ -11222,7 +11621,7 @@ inline ssize_t SSLSocketStream::read(char *ptr, size_t size) {
|
|
|
error_ = Error::ConnectionClosed;
|
|
|
}
|
|
|
return ret;
|
|
|
- } else if (wait_readable()) {
|
|
|
+ } else if (ensure_readable()) {
|
|
|
tls::TlsError err;
|
|
|
auto ret = tls::read(session_, ptr, size, err);
|
|
|
if (ret < 0) {
|
|
|
@@ -11644,9 +12043,11 @@ inline void Server::wait_until_ready() const {
|
|
|
}
|
|
|
|
|
|
inline 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);
|
|
|
}
|
|
|
@@ -11808,7 +12209,15 @@ Server::write_content_with_provider(Stream &strm, const Request &req,
|
|
|
};
|
|
|
|
|
|
if (res.content_length_ > 0) {
|
|
|
- if (req.ranges.empty()) {
|
|
|
+ // Only a 206 response is served as a partial representation, matching the
|
|
|
+ // condition `apply_ranges()` used to decide the Content-Length and the
|
|
|
+ // multipart boundary. Since `detail::range_error()` validates `req.ranges`
|
|
|
+ // only for a 2xx status, slicing under any other status would write a body
|
|
|
+ // that disagrees with the header already sent, from an unchecked offset.
|
|
|
+ auto is_partial =
|
|
|
+ !req.ranges.empty() && res.status == StatusCode::PartialContent_206;
|
|
|
+
|
|
|
+ if (!is_partial) {
|
|
|
return detail::write_content(strm, res.content_provider_, 0,
|
|
|
res.content_length_, is_shutting_down);
|
|
|
} else if (req.ranges.size() == 1) {
|
|
|
@@ -12207,7 +12616,14 @@ inline int Server::bind_internal(const std::string &host, int port,
|
|
|
}
|
|
|
|
|
|
inline 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;
|
|
|
@@ -12603,11 +13019,17 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
return write_response(strm, close_connection, req, res);
|
|
|
}
|
|
|
|
|
|
- // RFC 9112 §6.3: Reject requests with both a non-zero Content-Length and
|
|
|
- // any Transfer-Encoding to prevent request smuggling. Content-Length: 0 is
|
|
|
- // tolerated for compatibility with existing clients.
|
|
|
- if (req.get_header_value_u64("Content-Length") > 0 &&
|
|
|
- req.has_header("Transfer-Encoding")) {
|
|
|
+ // RFC 9112 §6.3: Reject requests whose framing is ambiguous, which would
|
|
|
+ // otherwise let an intermediary and this parser disagree on where the body
|
|
|
+ // ends and enable request smuggling. Two cases: a non-zero Content-Length
|
|
|
+ // alongside any Transfer-Encoding (Content-Length: 0 is tolerated for
|
|
|
+ // compatibility with existing clients), and a Transfer-Encoding whose final
|
|
|
+ // coding is not chunked, which leaves the body length undeterminable. The
|
|
|
+ // latter must not fall through to the "no body" path, or the body bytes are
|
|
|
+ // parsed as the next request on a persistent connection.
|
|
|
+ if (req.has_header("Transfer-Encoding") &&
|
|
|
+ (req.get_header_value_u64("Content-Length") > 0 ||
|
|
|
+ !detail::is_chunked_transfer_encoding(req.headers))) {
|
|
|
connection_closed = true;
|
|
|
res.status = StatusCode::BadRequest_400;
|
|
|
return write_response(strm, close_connection, req, res);
|
|
|
@@ -13253,11 +13675,13 @@ inline void ClientImpl::prepare_default_headers(Request &r, bool for_stream,
|
|
|
if (!r.has_header(header.first)) { r.headers.insert(header); }
|
|
|
}
|
|
|
|
|
|
+ // RFC 9110 5.3 recommends sending control data such as Host first, so
|
|
|
+ // prepend it rather than appending it after the caller's own fields.
|
|
|
if (!r.has_header("Host")) {
|
|
|
if (address_family_ == AF_UNIX) {
|
|
|
- r.headers.emplace("Host", "localhost");
|
|
|
+ r.headers.emplace_front("Host", "localhost");
|
|
|
} else {
|
|
|
- r.headers.emplace(
|
|
|
+ r.headers.emplace_front(
|
|
|
"Host", detail::make_host_and_port_string(host_, port_, is_ssl()));
|
|
|
}
|
|
|
}
|
|
|
@@ -13427,7 +13851,20 @@ ClientImpl::open_stream(const std::string &method, const std::string &path,
|
|
|
|
|
|
auto content_encoding = handle.response->get_header_value("Content-Encoding");
|
|
|
if (!content_encoding.empty()) {
|
|
|
+ // Same policy as prepare_content_receiver(): reject a coding we know about
|
|
|
+ // but were not built with, pass an unrecognized one through as-is.
|
|
|
handle.decompressor_ = detail::create_decompressor(content_encoding);
|
|
|
+ if (!handle.decompressor_) {
|
|
|
+ if (detail::is_known_content_encoding(content_encoding)) {
|
|
|
+ handle.error = Error::UnsupportedContentEncoding;
|
|
|
+ handle.response.reset();
|
|
|
+ return handle;
|
|
|
+ }
|
|
|
+ } else if (!handle.decompressor_->is_valid()) {
|
|
|
+ handle.error = Error::Compression;
|
|
|
+ handle.response.reset();
|
|
|
+ return handle;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
return handle;
|
|
|
@@ -14388,14 +14825,26 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
|
|
}
|
|
|
|
|
|
if (res.status != StatusCode::NotModified_304) {
|
|
|
- int dummy_status;
|
|
|
+ auto content_status = 0;
|
|
|
auto max_length = (!has_payload_max_length_ && req.content_receiver)
|
|
|
? (std::numeric_limits<size_t>::max)()
|
|
|
: payload_max_length_;
|
|
|
- if (!detail::read_content(strm, res, max_length, dummy_status,
|
|
|
+ if (!detail::read_content(strm, res, max_length, content_status,
|
|
|
std::move(progress), std::move(out),
|
|
|
decompress_)) {
|
|
|
- if (error != Error::Canceled) { error = Error::Read; }
|
|
|
+ if (error != Error::Canceled) {
|
|
|
+ // Tell the caller apart from a plain read failure when the body could
|
|
|
+ // not be decoded because of its Content-Encoding.
|
|
|
+ switch (content_status) {
|
|
|
+ case StatusCode::UnsupportedMediaType_415:
|
|
|
+ error = Error::UnsupportedContentEncoding;
|
|
|
+ break;
|
|
|
+ case StatusCode::InternalServerError_500:
|
|
|
+ error = Error::Compression;
|
|
|
+ break;
|
|
|
+ default: error = Error::Read; break;
|
|
|
+ }
|
|
|
+ }
|
|
|
output_error_log(error, &req);
|
|
|
return false;
|
|
|
}
|