|
|
@@ -134,6 +134,16 @@
|
|
|
#define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192
|
|
|
#endif
|
|
|
|
|
|
+#ifndef CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH
|
|
|
+// 1400 rather than a round number: a body that already fits in one 1500-byte
|
|
|
+// MTU gains nothing from being made smaller.
|
|
|
+#define CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH 1400
|
|
|
+#endif
|
|
|
+
|
|
|
+#ifndef CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH
|
|
|
+#define CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH (4 * 1024 * 1024) // 4MB
|
|
|
+#endif
|
|
|
+
|
|
|
#ifndef CPPHTTPLIB_RANGE_MAX_COUNT
|
|
|
#define CPPHTTPLIB_RANGE_MAX_COUNT 1024
|
|
|
#endif
|
|
|
@@ -1733,6 +1743,14 @@ struct Request {
|
|
|
#endif
|
|
|
};
|
|
|
|
|
|
+namespace detail {
|
|
|
+
|
|
|
+// Declared up here, away from the rest of the compression helpers, because
|
|
|
+// `Response` stores one.
|
|
|
+enum class EncodingType { None = 0, Gzip, Brotli, Zstd };
|
|
|
+
|
|
|
+} // namespace detail
|
|
|
+
|
|
|
struct Response {
|
|
|
std::string version;
|
|
|
int status = -1;
|
|
|
@@ -1798,6 +1816,11 @@ struct Response {
|
|
|
bool content_provider_success_ = false;
|
|
|
std::string file_content_path_;
|
|
|
std::string file_content_content_type_;
|
|
|
+
|
|
|
+ // Content coding chosen for a file-backed content provider, decided once
|
|
|
+ // where the file is opened so that the ETag and the body cannot disagree.
|
|
|
+ // `EncodingType::None` for every other kind of response.
|
|
|
+ detail::EncodingType file_content_encoding_ = detail::EncodingType::None;
|
|
|
};
|
|
|
|
|
|
enum class Error {
|
|
|
@@ -2200,6 +2223,10 @@ public:
|
|
|
|
|
|
Server &set_payload_max_length(size_t length);
|
|
|
|
|
|
+ Server &set_static_file_compression(bool on);
|
|
|
+ Server &set_static_file_compression_min_length(size_t length);
|
|
|
+ Server &set_static_file_compression_max_length(size_t length);
|
|
|
+
|
|
|
Server &set_websocket_ping_interval(time_t sec);
|
|
|
template <class Rep, class Period>
|
|
|
Server &set_websocket_ping_interval(
|
|
|
@@ -2270,6 +2297,11 @@ protected:
|
|
|
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
|
|
|
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
|
|
|
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
|
|
+ bool static_file_compression_ = false;
|
|
|
+ size_t static_file_compression_min_length_ =
|
|
|
+ CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
|
|
|
+ size_t static_file_compression_max_length_ =
|
|
|
+ CPPHTTPLIB_STATIC_FILE_COMPRESSION_MAX_LENGTH;
|
|
|
time_t websocket_ping_interval_sec_ =
|
|
|
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
|
|
int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS;
|
|
|
@@ -2326,6 +2358,10 @@ private:
|
|
|
const HandlersForContentReader &handlers) const;
|
|
|
|
|
|
bool parse_request_line(const char *s, Request &req) const;
|
|
|
+ detail::EncodingType static_file_encoding(const Request &req,
|
|
|
+ const std::string &content_type,
|
|
|
+ size_t length) const;
|
|
|
+ bool apply_static_file_compression(const Request &req, Response &res) const;
|
|
|
void apply_ranges(const Request &req, Response &res,
|
|
|
std::string &content_type, std::string &boundary) const;
|
|
|
bool write_response(Stream &strm, bool close_connection, Request &req,
|
|
|
@@ -3625,7 +3661,7 @@ ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);
|
|
|
|
|
|
ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags);
|
|
|
|
|
|
-enum class EncodingType { None = 0, Gzip, Brotli, Zstd };
|
|
|
+EncodingType encoding_type(const Request &req, const std::string &content_type);
|
|
|
|
|
|
EncodingType encoding_type(const Request &req, const Response &res);
|
|
|
|
|
|
@@ -5075,7 +5111,8 @@ inline std::string from_i_to_hex(size_t n) {
|
|
|
return ret;
|
|
|
}
|
|
|
|
|
|
-inline std::string compute_etag(const FileStat &fs) {
|
|
|
+inline std::string compute_etag(const FileStat &fs,
|
|
|
+ const std::string &suffix = std::string()) {
|
|
|
if (!fs.is_file()) { return std::string(); }
|
|
|
|
|
|
// If mtime cannot be determined (negative value indicates an error
|
|
|
@@ -5089,7 +5126,7 @@ inline std::string compute_etag(const FileStat &fs) {
|
|
|
auto size = fs.size();
|
|
|
|
|
|
return std::string("W/\"") + from_i_to_hex(mtime) + "-" +
|
|
|
- from_i_to_hex(size) + "\"";
|
|
|
+ from_i_to_hex(size) + suffix + "\"";
|
|
|
}
|
|
|
|
|
|
// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994
|
|
|
@@ -7449,10 +7486,9 @@ inline bool parse_quality(const char *b, const char *e, std::string &token,
|
|
|
return !invalid;
|
|
|
}
|
|
|
|
|
|
-inline EncodingType encoding_type(const Request &req, const Response &res) {
|
|
|
- if (!can_compress_content_type(res.get_header_value("Content-Type"))) {
|
|
|
- return EncodingType::None;
|
|
|
- }
|
|
|
+inline EncodingType encoding_type(const Request &req,
|
|
|
+ const std::string &content_type) {
|
|
|
+ if (!can_compress_content_type(content_type)) { return EncodingType::None; }
|
|
|
|
|
|
auto s = get_combined_header_value(req.headers, "Accept-Encoding");
|
|
|
if (s.empty()) { return EncodingType::None; }
|
|
|
@@ -7506,6 +7542,10 @@ inline EncodingType encoding_type(const Request &req, const Response &res) {
|
|
|
return best;
|
|
|
}
|
|
|
|
|
|
+inline EncodingType encoding_type(const Request &req, const Response &res) {
|
|
|
+ return encoding_type(req, res.get_header_value("Content-Type"));
|
|
|
+}
|
|
|
+
|
|
|
inline std::unique_ptr<compressor> make_compressor(EncodingType type) {
|
|
|
#ifdef CPPHTTPLIB_ZLIB_SUPPORT
|
|
|
if (type == EncodingType::Gzip) {
|
|
|
@@ -8531,6 +8571,67 @@ write_content_without_length(Stream &strm,
|
|
|
// down
|
|
|
}
|
|
|
|
|
|
+// Runs a known-length content provider to completion and compresses what it
|
|
|
+// writes into `out`. Nothing is buffered in identity form: a provider backed
|
|
|
+// by an mmap hands the compressor a pointer straight into the mapping.
|
|
|
+inline bool compress_content_provider(const ContentProvider &content_provider,
|
|
|
+ size_t length, compressor &cmp,
|
|
|
+ std::string &out) {
|
|
|
+ size_t offset = 0;
|
|
|
+ auto ok = true;
|
|
|
+ auto finished = false;
|
|
|
+ DataSink data_sink;
|
|
|
+
|
|
|
+ auto append = [&](const char *data, size_t data_len) {
|
|
|
+ out.append(data, data_len);
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+
|
|
|
+ data_sink.write = [&](const char *d, size_t l) -> bool {
|
|
|
+ if (!ok) { return false; }
|
|
|
+ offset += l;
|
|
|
+ if (l > 0 && !cmp.compress(d, l, false, append)) { ok = false; }
|
|
|
+ return ok;
|
|
|
+ };
|
|
|
+
|
|
|
+ // The body is framed by `length`, so a provider that reports itself done
|
|
|
+ // early has truncated it; the short-body check below turns that into a
|
|
|
+ // failure rather than calling the provider again forever.
|
|
|
+ data_sink.done = [&]() { finished = true; };
|
|
|
+
|
|
|
+ while (offset < length && !finished) {
|
|
|
+ auto prev_offset = offset;
|
|
|
+ if (!content_provider(offset, length - offset, data_sink) || !ok) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ // No Stream to block on here, so a provider that keeps returning true
|
|
|
+ // without writing would spin. Treat a pass that made no progress as a
|
|
|
+ // failure.
|
|
|
+ if (offset == prev_offset) { return false; }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (offset != length) { return false; }
|
|
|
+
|
|
|
+ return cmp.compress(nullptr, 0, true, append);
|
|
|
+}
|
|
|
+
|
|
|
+// Serves `m` as the response body. `set_content_provider()` clears the coding,
|
|
|
+// so recording it has to come after; keeping both here means a third
|
|
|
+// file-serving path cannot get that order wrong.
|
|
|
+inline void set_file_content_provider(Response &res,
|
|
|
+ const std::shared_ptr<mmap> &m,
|
|
|
+ const std::string &content_type,
|
|
|
+ EncodingType encoding) {
|
|
|
+ res.set_content_provider(
|
|
|
+ m->size(), content_type,
|
|
|
+ [m](size_t offset, size_t length, DataSink &sink) -> bool {
|
|
|
+ sink.write(m->data() + offset, length);
|
|
|
+ return true;
|
|
|
+ });
|
|
|
+
|
|
|
+ res.file_content_encoding_ = encoding;
|
|
|
+}
|
|
|
+
|
|
|
template <typename T, typename U>
|
|
|
inline bool
|
|
|
write_content_chunked(Stream &strm, const ContentProvider &content_provider,
|
|
|
@@ -11392,6 +11493,7 @@ inline void Response::set_content(const char *s, size_t n,
|
|
|
auto rng = headers.equal_range("Content-Type");
|
|
|
headers.erase(rng.first, rng.second);
|
|
|
set_header("Content-Type", content_type);
|
|
|
+ file_content_encoding_ = detail::EncodingType::None;
|
|
|
}
|
|
|
|
|
|
inline void Response::set_content(const std::string &s,
|
|
|
@@ -11406,6 +11508,7 @@ inline void Response::set_content(std::string &&s,
|
|
|
auto rng = headers.equal_range("Content-Type");
|
|
|
headers.erase(rng.first, rng.second);
|
|
|
set_header("Content-Type", content_type);
|
|
|
+ file_content_encoding_ = detail::EncodingType::None;
|
|
|
}
|
|
|
|
|
|
inline void Response::set_content_provider(
|
|
|
@@ -11416,6 +11519,7 @@ inline void Response::set_content_provider(
|
|
|
if (in_length > 0) { content_provider_ = std::move(provider); }
|
|
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
|
|
is_chunked_content_provider_ = false;
|
|
|
+ file_content_encoding_ = detail::EncodingType::None;
|
|
|
}
|
|
|
|
|
|
inline void Response::set_content_provider(
|
|
|
@@ -11426,6 +11530,7 @@ inline void Response::set_content_provider(
|
|
|
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
|
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
|
|
is_chunked_content_provider_ = false;
|
|
|
+ file_content_encoding_ = detail::EncodingType::None;
|
|
|
}
|
|
|
|
|
|
inline void Response::set_chunked_content_provider(
|
|
|
@@ -11436,6 +11541,7 @@ inline void Response::set_chunked_content_provider(
|
|
|
content_provider_ = detail::ContentProviderAdapter(std::move(provider));
|
|
|
content_provider_resource_releaser_ = std::move(resource_releaser);
|
|
|
is_chunked_content_provider_ = true;
|
|
|
+ file_content_encoding_ = detail::EncodingType::None;
|
|
|
}
|
|
|
|
|
|
inline void Response::set_file_content(const std::string &path,
|
|
|
@@ -12883,6 +12989,21 @@ inline Server &Server::set_payload_max_length(size_t length) {
|
|
|
return *this;
|
|
|
}
|
|
|
|
|
|
+inline Server &Server::set_static_file_compression(bool on) {
|
|
|
+ static_file_compression_ = on;
|
|
|
+ return *this;
|
|
|
+}
|
|
|
+
|
|
|
+inline Server &Server::set_static_file_compression_min_length(size_t length) {
|
|
|
+ static_file_compression_min_length_ = length;
|
|
|
+ return *this;
|
|
|
+}
|
|
|
+
|
|
|
+inline Server &Server::set_static_file_compression_max_length(size_t length) {
|
|
|
+ static_file_compression_max_length_ = length;
|
|
|
+ return *this;
|
|
|
+}
|
|
|
+
|
|
|
inline Server &Server::set_websocket_max_missed_pongs(int count) {
|
|
|
websocket_max_missed_pongs_ = count;
|
|
|
return *this;
|
|
|
@@ -13339,7 +13460,29 @@ inline bool Server::handle_file_request(Request &req, Response &res) {
|
|
|
res.set_header(kv.first, kv.second);
|
|
|
}
|
|
|
|
|
|
- auto etag = detail::compute_etag(stat);
|
|
|
+ auto content_type_of = [&]() {
|
|
|
+ return detail::find_content_type(
|
|
|
+ path, file_extension_and_mimetype_map_, default_file_mimetype_);
|
|
|
+ };
|
|
|
+
|
|
|
+ // Only the ETag needs the content type this early, and only to name
|
|
|
+ // the coding. Deciding it here would otherwise put a regex in front
|
|
|
+ // of the 304 below, which serving a file never used to pay for.
|
|
|
+ std::string content_type;
|
|
|
+ auto encoding = detail::EncodingType::None;
|
|
|
+ if (static_file_compression_) {
|
|
|
+ content_type = content_type_of();
|
|
|
+ encoding = static_file_encoding(req, content_type, stat.size());
|
|
|
+ }
|
|
|
+
|
|
|
+ // The ETag names the representation actually sent, so a client that
|
|
|
+ // cached the compressed form revalidates against the compressed ETag
|
|
|
+ // and still gets a 304, while one that took identity keeps the plain
|
|
|
+ // ETag.
|
|
|
+ auto etag = detail::compute_etag(
|
|
|
+ stat, encoding == detail::EncodingType::None
|
|
|
+ ? std::string()
|
|
|
+ : std::string("-") + detail::encoding_name(encoding));
|
|
|
if (!etag.empty()) { res.set_header("ETag", etag); }
|
|
|
|
|
|
auto mtime = stat.mtime();
|
|
|
@@ -13359,14 +13502,9 @@ inline bool Server::handle_file_request(Request &req, Response &res) {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- res.set_content_provider(
|
|
|
- mm->size(),
|
|
|
- detail::find_content_type(path, file_extension_and_mimetype_map_,
|
|
|
- default_file_mimetype_),
|
|
|
- [mm](size_t offset, size_t length, DataSink &sink) -> bool {
|
|
|
- sink.write(mm->data() + offset, length);
|
|
|
- return true;
|
|
|
- });
|
|
|
+ if (!static_file_compression_) { content_type = content_type_of(); }
|
|
|
+
|
|
|
+ detail::set_file_content_provider(res, mm, content_type, encoding);
|
|
|
|
|
|
if (req.method != "HEAD" && file_request_handler_) {
|
|
|
file_request_handler_(req, res);
|
|
|
@@ -13745,9 +13883,88 @@ inline bool Server::dispatch_request(Request &req, Response &res,
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
+// Decides the content coding for a response served straight from a file. Both
|
|
|
+// the ETag, which has to name the representation actually sent, and
|
|
|
+// `apply_static_file_compression()` go through this, so the two cannot drift
|
|
|
+// apart.
|
|
|
+inline detail::EncodingType Server::static_file_encoding(
|
|
|
+ const Request &req, const std::string &content_type, size_t length) const {
|
|
|
+ if (!static_file_compression_) { return detail::EncodingType::None; }
|
|
|
+
|
|
|
+ // Nothing to compress, and an empty file already answers with
|
|
|
+ // `Content-Length: 0`. Checked on its own so that a zero floor still cannot
|
|
|
+ // turn an empty body into a 20-byte gzip stream.
|
|
|
+ if (length == 0) { return detail::EncodingType::None; }
|
|
|
+
|
|
|
+ // A file that already fits in a single packet gains nothing from being made
|
|
|
+ // smaller, since it still travels in that one segment, and a file of a few
|
|
|
+ // bytes comes out larger than it went in.
|
|
|
+ if (length < static_file_compression_min_length_) {
|
|
|
+ return detail::EncodingType::None;
|
|
|
+ }
|
|
|
+
|
|
|
+ // RFC 9110 applies Range to the representation after content coding, so a
|
|
|
+ // compressed 206 would mean compressing the whole file and then slicing it.
|
|
|
+ // Serve ranges from the identity representation instead.
|
|
|
+ if (!req.ranges.empty()) { return detail::EncodingType::None; }
|
|
|
+
|
|
|
+ if (static_file_compression_max_length_ > 0 &&
|
|
|
+ length > static_file_compression_max_length_) {
|
|
|
+ return detail::EncodingType::None;
|
|
|
+ }
|
|
|
+
|
|
|
+ return detail::encoding_type(req, content_type);
|
|
|
+}
|
|
|
+
|
|
|
+// Compresses a file-backed content provider into `res.body` and takes over the
|
|
|
+// framing headers. Returns false when the response is left untouched.
|
|
|
+inline bool Server::apply_static_file_compression(const Request &req,
|
|
|
+ Response &res) const {
|
|
|
+ auto type = res.file_content_encoding_;
|
|
|
+ if (type == detail::EncodingType::None || !res.content_provider_) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ auto compressor = detail::make_compressor(type);
|
|
|
+ if (!compressor) { return false; }
|
|
|
+
|
|
|
+ output_pre_compression_log(req, res);
|
|
|
+
|
|
|
+ std::string compressed;
|
|
|
+ if (!detail::compress_content_provider(res.content_provider_,
|
|
|
+ res.content_length_, *compressor,
|
|
|
+ compressed)) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ res.body.swap(compressed);
|
|
|
+
|
|
|
+ // The provider was consumed in full, so a resource releaser registered with
|
|
|
+ // it should hear about a success when the response goes away.
|
|
|
+ res.content_provider_success_ = true;
|
|
|
+ res.content_provider_ = nullptr;
|
|
|
+ res.content_length_ = 0;
|
|
|
+ res.file_content_encoding_ = detail::EncodingType::None;
|
|
|
+
|
|
|
+ res.set_header("Content-Encoding", detail::encoding_name(type));
|
|
|
+ res.set_header("Vary", "Accept-Encoding");
|
|
|
+ res.set_header("Content-Length", std::to_string(res.body.size()));
|
|
|
+
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
inline void Server::apply_ranges(const Request &req, Response &res,
|
|
|
std::string &content_type,
|
|
|
std::string &boundary) const {
|
|
|
+ // A known-length content provider leaves `res.body` empty, so the compressor
|
|
|
+ // at the end of this function never runs for one (issue #2545). A file-backed
|
|
|
+ // provider is fully readable right here, so compress it and answer with an
|
|
|
+ // ordinary body: `Content-Length` and HEAD keep working, and the response
|
|
|
+ // takes the same path as `set_content()` from here on. Range requests never
|
|
|
+ // get a content coding, so `Content-Range` still names identity bytes and
|
|
|
+ // none of the framing below applies.
|
|
|
+ if (apply_static_file_compression(req, res)) { return; }
|
|
|
+
|
|
|
if (req.ranges.size() > 1 && res.status == StatusCode::PartialContent_206) {
|
|
|
auto it = res.headers.find("Content-Type");
|
|
|
if (it != res.headers.end()) {
|
|
|
@@ -14180,12 +14397,9 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
path, file_extension_and_mimetype_map_, default_file_mimetype_);
|
|
|
}
|
|
|
|
|
|
- res.set_content_provider(
|
|
|
- mm->size(), content_type,
|
|
|
- [mm](size_t offset, size_t length, DataSink &sink) -> bool {
|
|
|
- sink.write(mm->data() + offset, length);
|
|
|
- return true;
|
|
|
- });
|
|
|
+ detail::set_file_content_provider(
|
|
|
+ res, mm, content_type,
|
|
|
+ static_file_encoding(req, content_type, mm->size()));
|
|
|
}
|
|
|
}
|
|
|
|