diff --git a/README.md b/README.md index c0df271e..63743322 100644 --- a/README.md +++ b/README.md @@ -1590,6 +1590,8 @@ This holds for every kind of response, including the file-backed ones below. `Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart. +Turn this compression off with `svr.set_compress(false)`. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). A server whose responses mix the two should turn compression off, or keep such secrets out of those responses. Static file compression, below, is set separately. + ### Static file compression Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with: diff --git a/docs-src/pages/en/cookbook/s08-compress-response.md b/docs-src/pages/en/cookbook/s08-compress-response.md index 335d0751..7fc4b29f 100644 --- a/docs-src/pages/en/cookbook/s08-compress-response.md +++ b/docs-src/pages/en/cookbook/s08-compress-response.md @@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) { }); ``` +## Turning it off + +```cpp +svr.set_compress(false); +``` + +Handler responses, buffered or streamed, are then sent as they are. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). Static file compression is set separately. + ## Static files need to be opted in Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with: diff --git a/docs-src/pages/ja/cookbook/s08-compress-response.md b/docs-src/pages/ja/cookbook/s08-compress-response.md index 09cf9084..fb842d85 100644 --- a/docs-src/pages/ja/cookbook/s08-compress-response.md +++ b/docs-src/pages/ja/cookbook/s08-compress-response.md @@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) { }); ``` +## 圧縮を止める + +```cpp +svr.set_compress(false); +``` + +ハンドラのレスポンスは、通常のボディもストリーミングも、そのまま送られます。HTTPSでは、攻撃者が選べる文字列とCSRFトークンなどの秘密が同じレスポンスに載ると、圧縮後の長さから秘密が漏れることがあります(BREACH攻撃)。静的ファイルの圧縮は別に設定します。 + ## 静的ファイルは明示的に有効にする `set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。 diff --git a/httplib.h b/httplib.h index bfc322c2..b11a6325 100644 --- a/httplib.h +++ b/httplib.h @@ -2357,6 +2357,8 @@ public: Server &set_payload_max_length(size_t length); + Server &set_compress(bool on); + 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); @@ -2431,6 +2433,7 @@ 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 compress_ = true; bool static_file_compression_ = false; size_t static_file_compression_min_length_ = CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH; @@ -13854,6 +13857,11 @@ inline Server &Server::set_payload_max_length(size_t length) { return *this; } +inline Server &Server::set_compress(bool on) { + compress_ = on; + return *this; +} + inline Server &Server::set_static_file_compression(bool on) { static_file_compression_ = on; return *this; @@ -14882,7 +14890,8 @@ inline void Server::apply_ranges(const Request &req, Response &res, "multipart/byteranges; boundary=" + boundary); } - auto type = detail::encoding_type(req, res); + auto type = + compress_ ? detail::encoding_type(req, res) : detail::EncodingType::None; if (res.body.empty()) { if (res.content_length_ > 0 && req.ranges.size() == 1 && diff --git a/test/test.cc b/test/test.cc index 78d273c9..f586fcb1 100644 --- a/test/test.cc +++ b/test/test.cc @@ -14908,6 +14908,45 @@ TEST(ContentEncodingTest, PreEncodedResponseIsNotCompressedAgain) { EXPECT_EQ("Hello World!", res->body); } +// set_compress(false) sends every handler response as it is, buffered or +// streamed +TEST(ContentEncodingTest, SetCompressFalseSendsIdentity) { + const std::string text(1000, 'a'); + + Server svr; + svr.set_compress(false); + svr.Get("/buffered", [&](const Request &, Response &res) { + res.set_content(text, "text/plain"); + }); + svr.Get("/streamed", [&](const Request &, Response &res) { + res.set_chunked_content_provider("text/plain", [&](size_t, DataSink &sink) { + sink.write(text.data(), text.size()); + sink.done(); + return true; + }); + }); + + auto port = svr.bind_to_any_port(HOST); + ASSERT_LT(0, port); + thread t = thread([&] { svr.listen_after_bind(); }); + auto se = detail::scope_exit([&] { + svr.stop(); + t.join(); + ASSERT_FALSE(svr.is_running()); + }); + svr.wait_until_ready(); + + Client cli(HOST, port); + cli.set_decompress(false); + for (auto path : {"/buffered", "/streamed"}) { + SCOPED_TRACE(path); + auto res = cli.Get(path, Headers{{"Accept-Encoding", "gzip"}}); + ASSERT_TRUE(res) << "Error: " << to_string(res.error()); + EXPECT_FALSE(res->has_header("Content-Encoding")); + EXPECT_EQ(text, res->body); + } +} + // A chunked provider settles its coding where the headers are written and // reuses it at write time. Both ends of that have to hold: a handler's own // Content-Encoding suppresses the coding, and a response without one is still