diff --git a/httplib.h b/httplib.h index e62f53a4..a755ed7e 100644 --- a/httplib.h +++ b/httplib.h @@ -7642,13 +7642,6 @@ 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. @@ -7658,11 +7651,11 @@ inline bool is_zlib_encoding(const std::string &encoding) { } inline bool is_brotli_encoding(const std::string &encoding) { - return contains_case_ignore(encoding, "br"); + return case_ignore::equal(encoding, "br"); } inline bool is_zstd_encoding(const std::string &encoding) { - return contains_case_ignore(encoding, "zstd"); + return case_ignore::equal(encoding, "zstd"); } // Returns true if the content coding is one cpp-httplib is able to decompress diff --git a/test/test.cc b/test/test.cc index 83af17ef..c505cb3d 100644 --- a/test/test.cc +++ b/test/test.cc @@ -11049,6 +11049,52 @@ static const char GZIPPED_HELLO_WORLD[] = { // A content coding cpp-httplib recognizes but was not built with must be // reported as such. Handing the still-compressed payload back to the caller // would silently corrupt it. +// A content coding is a whole token, not something the field value merely +// contains. Matching "br" and "zstd" as substrings made unrelated values such +// as "fibre" look like Brotli, and turned a multi-coding value like +// "gzip, br" into a Brotli-labeled body, so a decompressor was run over data +// it was never meant to see. +TEST(ContentEncodingTest, SubstringOfACodingIsNotTheCoding) { + const std::string body = "\xff\xd8\xff\xe0 not really a jpeg"; + + Server svr; + + svr.Get("/fibre", [&](const Request & /*req*/, Response &res) { + res.set_content(body, "image/jpeg"); + res.set_header("Content-Encoding", "fibre"); + }); + + svr.Get("/multi", [&](const Request & /*req*/, Response &res) { + res.set_content(body, "image/jpeg"); + res.set_header("Content-Encoding", "gzip, br"); + }); + + svr.Get("/zstdish", [&](const Request & /*req*/, Response &res) { + res.set_content(body, "image/jpeg"); + res.set_header("Content-Encoding", "x-zstd-ish"); + }); + + auto port = svr.bind_to_any_port(HOST); + 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); + + for (const char *path : {"/fibre", "/multi", "/zstdish"}) { + auto res = cli.Get(path); + ASSERT_TRUE(res) << path << " -> " << to_string(res.error()); + EXPECT_EQ(StatusCode::OK_200, res->status) << path; + // Not a coding we recognize, so the payload comes back untouched + EXPECT_EQ(body, res->body) << path; + } +} + TEST(ContentEncodingTest, KnownEncodingWithoutSupportIsReported) { const std::string gzipped(GZIPPED_HELLO_WORLD, sizeof(GZIPPED_HELLO_WORLD));