You've already forked cpp-httplib
Enhance ETag handling and validation in httplib.h and add comprehensive tests in test.cc
This commit is contained in:
33
httplib.h
33
httplib.h
@@ -2980,24 +2980,37 @@ inline std::string from_i_to_hex(size_t n) {
|
||||
inline std::string compute_etag(const FileStat &fs) {
|
||||
if (!fs.is_file()) { return std::string(); }
|
||||
|
||||
// If mtime cannot be determined (negative value indicates an error
|
||||
// or sentinel), do not generate an ETag. Returning a neutral / fixed
|
||||
// value like 0 could collide with a real file that legitimately has
|
||||
// mtime == 0 (epoch) and lead to misleading validators.
|
||||
auto mtime_raw = fs.mtime();
|
||||
auto mtime = mtime_raw < 0 ? 0 : static_cast<size_t>(mtime_raw);
|
||||
if (mtime_raw < 0) { return std::string(); }
|
||||
|
||||
auto mtime = static_cast<size_t>(mtime_raw);
|
||||
auto size = fs.size();
|
||||
|
||||
return std::string("W/\"") + from_i_to_hex(mtime) + "-" +
|
||||
from_i_to_hex(size) + "\"";
|
||||
}
|
||||
|
||||
// Format time_t as HTTP-date (RFC 7231): "Sun, 06 Nov 1994 08:49:37 GMT"
|
||||
// Format time_t as HTTP-date (RFC 9110 Section 5.6.7): "Sun, 06 Nov 1994
|
||||
// 08:49:37 GMT" This implementation is defensive: it validates `mtime`, checks
|
||||
// return values from `gmtime_r`/`gmtime_s`, and ensures `strftime` succeeds.
|
||||
inline std::string file_mtime_to_http_date(time_t mtime) {
|
||||
if (mtime < 0) { return std::string(); }
|
||||
|
||||
struct tm tm_buf;
|
||||
#ifdef _WIN32
|
||||
gmtime_s(&tm_buf, &mtime);
|
||||
if (gmtime_s(&tm_buf, &mtime) != 0) { return std::string(); }
|
||||
#else
|
||||
gmtime_r(&mtime, &tm_buf);
|
||||
if (gmtime_r(&mtime, &tm_buf) == nullptr) { return std::string(); }
|
||||
#endif
|
||||
char buf[64];
|
||||
strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf);
|
||||
if (strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm_buf) == 0) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
@@ -3043,7 +3056,8 @@ inline bool is_weak_etag(const std::string &s) {
|
||||
}
|
||||
|
||||
inline bool is_strong_etag(const std::string &s) {
|
||||
// Check if the string is a strong ETag (starts and ends with '"', at least 2 chars)
|
||||
// Check if the string is a strong ETag (starts and ends with '"', at least 2
|
||||
// chars)
|
||||
return s.size() >= 2 && s[0] == '"' && s.back() == '"';
|
||||
}
|
||||
|
||||
@@ -3167,7 +3181,8 @@ inline bool FileStat::is_dir() const {
|
||||
}
|
||||
|
||||
inline time_t FileStat::mtime() const {
|
||||
return ret_ >= 0 ? static_cast<time_t>(st_.st_mtime) : static_cast<time_t>(-1);
|
||||
return ret_ >= 0 ? static_cast<time_t>(st_.st_mtime)
|
||||
: static_cast<time_t>(-1);
|
||||
}
|
||||
|
||||
inline size_t FileStat::size() const {
|
||||
@@ -8460,7 +8475,9 @@ inline bool Server::check_if_not_modified(const Request &req, Response &res,
|
||||
[&](const char *b, const char *e) {
|
||||
auto len = static_cast<size_t>(e - b);
|
||||
if (len == 1 && *b == '*') return true;
|
||||
if (len == etag.size() && std::equal(b, e, etag.begin())) return true;
|
||||
if (len == etag.size() &&
|
||||
std::equal(b, e, etag.begin()))
|
||||
return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
133
test/test.cc
133
test/test.cc
@@ -12716,7 +12716,7 @@ TEST(ETagTest, StaticFileETagAndIfNoneMatch) {
|
||||
EXPECT_FALSE(etag.empty());
|
||||
|
||||
// Verify ETag format: W/"hex-hex"
|
||||
ASSERT_GE(etag.length(), 5u); // Minimum: W/""
|
||||
ASSERT_GE(etag.length(), 5u); // Minimum: W/""
|
||||
EXPECT_EQ('W', etag[0]);
|
||||
EXPECT_EQ('/', etag[1]);
|
||||
EXPECT_EQ('"', etag[2]);
|
||||
@@ -12967,3 +12967,134 @@ TEST(ETagTest, IfRangeWithDate) {
|
||||
t.join();
|
||||
std::remove(fname);
|
||||
}
|
||||
TEST(ETagTest, MalformedIfNoneMatchAndWhitespace) {
|
||||
using namespace httplib;
|
||||
|
||||
const char *fname = "etag_malformed.txt";
|
||||
const char *content = "malformed-etag";
|
||||
{
|
||||
std::ofstream ofs(fname);
|
||||
ofs << content;
|
||||
ASSERT_TRUE(ofs.good());
|
||||
}
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8092); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8092);
|
||||
|
||||
// baseline: should get 200 and an ETag
|
||||
auto res1 = cli.Get("/static/etag_malformed.txt");
|
||||
ASSERT_TRUE(res1);
|
||||
ASSERT_EQ(200, res1->status);
|
||||
ASSERT_TRUE(res1->has_header("ETag"));
|
||||
|
||||
// Malformed ETag value (missing quotes) should be treated as non-matching
|
||||
Headers h_bad = {{"If-None-Match", "W/noquotes"}};
|
||||
auto res_bad = cli.Get("/static/etag_malformed.txt", h_bad);
|
||||
ASSERT_TRUE(res_bad);
|
||||
EXPECT_EQ(200, res_bad->status);
|
||||
|
||||
// Whitespace-only header value should be considered invalid / non-matching
|
||||
Headers h_space = {{"If-None-Match", " "}};
|
||||
auto res_space = cli.Get("/static/etag_malformed.txt", h_space);
|
||||
ASSERT_TRUE(res_space);
|
||||
EXPECT_EQ(200, res_space->status);
|
||||
|
||||
svr.stop();
|
||||
t.join();
|
||||
std::remove(fname);
|
||||
}
|
||||
|
||||
TEST(ETagTest, InvalidIfModifiedSinceAndIfRangeDate) {
|
||||
using namespace httplib;
|
||||
|
||||
const char *fname = "ims_invalid_format.txt";
|
||||
const char *content = "ims-bad-format";
|
||||
{
|
||||
std::ofstream ofs(fname);
|
||||
ofs << content;
|
||||
ASSERT_TRUE(ofs.good());
|
||||
}
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8093); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8093);
|
||||
|
||||
auto res1 = cli.Get("/static/ims_invalid_format.txt");
|
||||
ASSERT_TRUE(res1);
|
||||
ASSERT_EQ(200, res1->status);
|
||||
ASSERT_TRUE(res1->has_header("Last-Modified"));
|
||||
|
||||
// If-Modified-Since with invalid format should not result in 304
|
||||
Headers h_bad_date = {{"If-Modified-Since", "not-a-valid-date"}};
|
||||
auto res_bad = cli.Get("/static/ims_invalid_format.txt", h_bad_date);
|
||||
ASSERT_TRUE(res_bad);
|
||||
EXPECT_EQ(200, res_bad->status);
|
||||
|
||||
// If-Range with invalid date format should be treated as mismatch -> full
|
||||
// content (200)
|
||||
Headers h_ifrange_bad = {{"Range", "bytes=0-3"},
|
||||
{"If-Range", "invalid-date"}};
|
||||
auto res_ifrange = cli.Get("/static/ims_invalid_format.txt", h_ifrange_bad);
|
||||
ASSERT_TRUE(res_ifrange);
|
||||
EXPECT_EQ(200, res_ifrange->status);
|
||||
|
||||
svr.stop();
|
||||
t.join();
|
||||
std::remove(fname);
|
||||
}
|
||||
|
||||
TEST(ETagTest, IfRangeWithMalformedETag) {
|
||||
using namespace httplib;
|
||||
|
||||
const char *fname = "ifrange_malformed.txt";
|
||||
const std::string content = "0123456789";
|
||||
{
|
||||
std::ofstream ofs(fname);
|
||||
ofs << content;
|
||||
ASSERT_TRUE(ofs.good());
|
||||
}
|
||||
|
||||
Server svr;
|
||||
svr.set_mount_point("/static", ".");
|
||||
auto t = std::thread([&]() { svr.listen("localhost", 8094); });
|
||||
svr.wait_until_ready();
|
||||
|
||||
Client cli("localhost", 8094);
|
||||
|
||||
// First request: get ETag
|
||||
auto res1 = cli.Get("/static/ifrange_malformed.txt");
|
||||
ASSERT_TRUE(res1);
|
||||
ASSERT_EQ(200, res1->status);
|
||||
ASSERT_TRUE(res1->has_header("ETag"));
|
||||
|
||||
// If-Range with malformed ETag (no quotes) should be treated as mismatch ->
|
||||
// full content (200)
|
||||
Headers h_malformed = {{"Range", "bytes=0-4"}, {"If-Range", "W/noquotes"}};
|
||||
auto res2 = cli.Get("/static/ifrange_malformed.txt", h_malformed);
|
||||
ASSERT_TRUE(res2);
|
||||
EXPECT_EQ(200, res2->status);
|
||||
EXPECT_EQ(content, res2->body);
|
||||
|
||||
svr.stop();
|
||||
t.join();
|
||||
std::remove(fname);
|
||||
}
|
||||
|
||||
TEST(ETagTest, DateParsingAndMtimeNegative) {
|
||||
using namespace httplib;
|
||||
|
||||
// parse_http_date should return -1 for invalid format
|
||||
time_t parsed = detail::parse_http_date("this is not a date");
|
||||
EXPECT_EQ(static_cast<time_t>(-1), parsed);
|
||||
|
||||
// file_mtime_to_http_date returns empty string for negative mtime
|
||||
std::string s = detail::file_mtime_to_http_date(static_cast<time_t>(-1));
|
||||
EXPECT_TRUE(s.empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user