Skip to content

Commit 9ce908a

Browse files
committed
Resolve #2162
1 parent aabd063 commit 9ce908a

File tree

3 files changed

+329
-2
lines changed

3 files changed

+329
-2
lines changed

example/Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
1818
BROTLI_DIR = $(PREFIX)/opt/brotli
1919
BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
2020

21-
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client
21+
all: server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header
2222

2323
server : server.cc ../httplib.h Makefile
2424
$(CXX) -o server $(CXXFLAGS) server.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
@@ -56,9 +56,12 @@ one_time_request : one_time_request.cc ../httplib.h Makefile
5656
server_and_client : server_and_client.cc ../httplib.h Makefile
5757
$(CXX) -o server_and_client $(CXXFLAGS) server_and_client.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
5858

59+
accept_header : accept_header.cc ../httplib.h Makefile
60+
$(CXX) -o accept_header $(CXXFLAGS) accept_header.cc $(OPENSSL_SUPPORT) $(ZLIB_SUPPORT) $(BROTLI_SUPPORT)
61+
5962
pem:
6063
openssl genrsa 2048 > key.pem
6164
openssl req -new -key key.pem | openssl x509 -days 3650 -req -signkey key.pem > cert.pem
6265

6366
clean:
64-
rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client *.pem
67+
rm server client hello simplecli simplesvr upload redirect ssesvr ssecli benchmark one_time_request server_and_client accept_header *.pem

httplib.h

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,7 @@ struct Request {
670670
std::function<bool()> is_connection_closed = []() { return true; };
671671

672672
// for client
673+
std::vector<std::string> accept_content_types;
673674
ResponseHandler response_handler;
674675
ContentReceiverWithProgress content_receiver;
675676
Progress progress;
@@ -2491,6 +2492,9 @@ bool parse_multipart_boundary(const std::string &content_type,
24912492

24922493
bool parse_range_header(const std::string &s, Ranges &ranges);
24932494

2495+
bool parse_accept_header(const std::string &s,
2496+
std::vector<std::string> &content_types);
2497+
24942498
int close_socket(socket_t sock);
24952499

24962500
ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags);
@@ -5026,6 +5030,123 @@ inline bool parse_range_header(const std::string &s, Ranges &ranges) try {
50265030
} catch (...) { return false; }
50275031
#endif
50285032

5033+
inline bool parse_accept_header(const std::string &s,
5034+
std::vector<std::string> &content_types) {
5035+
content_types.clear();
5036+
5037+
// Empty string is considered valid (no preference)
5038+
if (s.empty()) { return true; }
5039+
5040+
// Check for invalid patterns: leading/trailing commas or consecutive commas
5041+
if (s.front() == ',' || s.back() == ',' ||
5042+
s.find(",,") != std::string::npos) {
5043+
return false;
5044+
}
5045+
5046+
struct AcceptEntry {
5047+
std::string media_type;
5048+
double quality;
5049+
int order; // Original order in header
5050+
};
5051+
5052+
std::vector<AcceptEntry> entries;
5053+
int order = 0;
5054+
bool has_invalid_entry = false;
5055+
5056+
// Split by comma and parse each entry
5057+
split(s.data(), s.data() + s.size(), ',', [&](const char *b, const char *e) {
5058+
std::string entry(b, e);
5059+
entry = trim_copy(entry);
5060+
5061+
if (entry.empty()) {
5062+
has_invalid_entry = true;
5063+
return;
5064+
}
5065+
5066+
AcceptEntry accept_entry;
5067+
accept_entry.quality = 1.0; // Default quality
5068+
accept_entry.order = order++;
5069+
5070+
// Find q= parameter
5071+
auto q_pos = entry.find(";q=");
5072+
if (q_pos == std::string::npos) { q_pos = entry.find("; q="); }
5073+
5074+
if (q_pos != std::string::npos) {
5075+
// Extract media type (before q parameter)
5076+
accept_entry.media_type = trim_copy(entry.substr(0, q_pos));
5077+
5078+
// Extract quality value
5079+
auto q_start = entry.find('=', q_pos) + 1;
5080+
auto q_end = entry.find(';', q_start);
5081+
if (q_end == std::string::npos) { q_end = entry.length(); }
5082+
5083+
std::string quality_str =
5084+
trim_copy(entry.substr(q_start, q_end - q_start));
5085+
if (quality_str.empty()) {
5086+
has_invalid_entry = true;
5087+
return;
5088+
}
5089+
5090+
try {
5091+
accept_entry.quality = std::stod(quality_str);
5092+
// Check if quality is in valid range [0.0, 1.0]
5093+
if (accept_entry.quality < 0.0 || accept_entry.quality > 1.0) {
5094+
has_invalid_entry = true;
5095+
return;
5096+
}
5097+
} catch (...) {
5098+
has_invalid_entry = true;
5099+
return;
5100+
}
5101+
} else {
5102+
// No quality parameter, use entire entry as media type
5103+
accept_entry.media_type = entry;
5104+
}
5105+
5106+
// Remove additional parameters from media type
5107+
auto param_pos = accept_entry.media_type.find(';');
5108+
if (param_pos != std::string::npos) {
5109+
accept_entry.media_type =
5110+
trim_copy(accept_entry.media_type.substr(0, param_pos));
5111+
}
5112+
5113+
// Basic validation of media type format
5114+
if (accept_entry.media_type.empty()) {
5115+
has_invalid_entry = true;
5116+
return;
5117+
}
5118+
5119+
// Check for basic media type format (should contain '/' or be '*')
5120+
if (accept_entry.media_type != "*" &&
5121+
accept_entry.media_type.find('/') == std::string::npos) {
5122+
has_invalid_entry = true;
5123+
return;
5124+
}
5125+
5126+
entries.push_back(accept_entry);
5127+
});
5128+
5129+
// Return false if any invalid entry was found
5130+
if (has_invalid_entry) { return false; }
5131+
5132+
// Sort by quality (descending), then by original order (ascending)
5133+
std::sort(entries.begin(), entries.end(),
5134+
[](const AcceptEntry &a, const AcceptEntry &b) {
5135+
if (a.quality != b.quality) {
5136+
return a.quality > b.quality; // Higher quality first
5137+
}
5138+
return a.order < b.order; // Earlier order first for same quality
5139+
});
5140+
5141+
// Extract sorted media types
5142+
content_types.reserve(entries.size());
5143+
for (const auto &entry : entries) {
5144+
content_types.push_back(entry.media_type);
5145+
}
5146+
5147+
return true;
5148+
}
5149+
50295150
class MultipartFormDataParser {
50305151
public:
50315152
MultipartFormDataParser() = default;
@@ -7446,6 +7567,14 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
74467567
req.set_header("LOCAL_ADDR", req.local_addr);
74477568
req.set_header("LOCAL_PORT", std::to_string(req.local_port));
74487569

7570+
if (req.has_header("Accept")) {
7571+
const auto &accept_header = req.get_header_value("Accept");
7572+
if (!detail::parse_accept_header(accept_header, req.accept_content_types)) {
7573+
res.status = StatusCode::BadRequest_400;
7574+
return write_response(strm, close_connection, req, res);
7575+
}
7576+
}
7577+
74497578
if (req.has_header("Range")) {
74507579
const auto &range_header_value = req.get_header_value("Range");
74517580
if (!detail::parse_range_header(range_header_value, req.ranges)) {

test/test.cc

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,201 @@ TEST(TrimTests, TrimStringTests) {
308308
EXPECT_TRUE(detail::trim_copy("").empty());
309309
}
310310

311+
TEST(ParseAcceptHeaderTest, BasicAcceptParsing) {
312+
// Simple case without quality values
313+
std::vector<std::string> result1;
314+
EXPECT_TRUE(detail::parse_accept_header(
315+
"text/html,application/json,text/plain", result1));
316+
EXPECT_EQ(result1.size(), 3);
317+
EXPECT_EQ(result1[0], "text/html");
318+
EXPECT_EQ(result1[1], "application/json");
319+
EXPECT_EQ(result1[2], "text/plain");
320+
321+
// With quality values
322+
std::vector<std::string> result2;
323+
EXPECT_TRUE(detail::parse_accept_header(
324+
"text/html;q=0.9,application/json;q=1.0,text/plain;q=0.8", result2));
325+
EXPECT_EQ(result2.size(), 3);
326+
EXPECT_EQ(result2[0], "application/json"); // highest q value
327+
EXPECT_EQ(result2[1], "text/html");
328+
EXPECT_EQ(result2[2], "text/plain"); // lowest q value
329+
}
330+
331+
TEST(ParseAcceptHeaderTest, MixedQualityValues) {
332+
// Mixed with and without quality values
333+
std::vector<std::string> result;
334+
EXPECT_TRUE(detail::parse_accept_header(
335+
"text/html,application/json;q=0.5,text/plain;q=0.8", result));
336+
EXPECT_EQ(result.size(), 3);
337+
EXPECT_EQ(result[0], "text/html"); // no q value means 1.0
338+
EXPECT_EQ(result[1], "text/plain"); // q=0.8
339+
EXPECT_EQ(result[2], "application/json"); // q=0.5
340+
}
341+
342+
TEST(ParseAcceptHeaderTest, EdgeCases) {
343+
// Empty header
344+
std::vector<std::string> empty_result;
345+
EXPECT_TRUE(detail::parse_accept_header("", empty_result));
346+
EXPECT_TRUE(empty_result.empty());
347+
348+
// Single type
349+
std::vector<std::string> single_result;
350+
EXPECT_TRUE(detail::parse_accept_header("application/json", single_result));
351+
EXPECT_EQ(single_result.size(), 1);
352+
EXPECT_EQ(single_result[0], "application/json");
353+
354+
// Wildcard types
355+
std::vector<std::string> wildcard_result;
356+
EXPECT_TRUE(detail::parse_accept_header(
357+
"text/*;q=0.5,*/*;q=0.1,application/json", wildcard_result));
358+
EXPECT_EQ(wildcard_result.size(), 3);
359+
EXPECT_EQ(wildcard_result[0], "application/json");
360+
EXPECT_EQ(wildcard_result[1], "text/*");
361+
EXPECT_EQ(wildcard_result[2], "*/*");
362+
}
363+
364+
TEST(ParseAcceptHeaderTest, RealWorldExamples) {
365+
// Common browser Accept header
366+
std::vector<std::string> browser_result;
367+
EXPECT_TRUE(
368+
detail::parse_accept_header("text/html,application/xhtml+xml,application/"
369+
"xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
370+
browser_result));
371+
EXPECT_EQ(browser_result.size(), 6);
372+
EXPECT_EQ(browser_result[0], "text/html"); // q=1.0 (default)
373+
EXPECT_EQ(browser_result[1], "application/xhtml+xml"); // q=1.0 (default)
374+
EXPECT_EQ(browser_result[2], "image/webp"); // q=1.0 (default)
375+
EXPECT_EQ(browser_result[3], "image/apng"); // q=1.0 (default)
376+
EXPECT_EQ(browser_result[4], "application/xml"); // q=0.9
377+
EXPECT_EQ(browser_result[5], "*/*"); // q=0.8
378+
379+
// API client header
380+
std::vector<std::string> api_result;
381+
EXPECT_TRUE(detail::parse_accept_header(
382+
"application/json;q=0.9,application/xml;q=0.8,text/plain;q=0.1",
383+
api_result));
384+
EXPECT_EQ(api_result.size(), 3);
385+
EXPECT_EQ(api_result[0], "application/json");
386+
EXPECT_EQ(api_result[1], "application/xml");
387+
EXPECT_EQ(api_result[2], "text/plain");
388+
}
389+
390+
TEST(ParseAcceptHeaderTest, SpecialCases) {
391+
// Quality value with 3 decimal places
392+
std::vector<std::string> decimal_result;
393+
EXPECT_TRUE(detail::parse_accept_header(
394+
"text/html;q=0.123,application/json;q=0.456", decimal_result));
395+
EXPECT_EQ(decimal_result.size(), 2);
396+
EXPECT_EQ(decimal_result[0], "application/json"); // Higher q value
397+
EXPECT_EQ(decimal_result[1], "text/html");
398+
399+
// Zero quality (should still be included but with lowest priority)
400+
std::vector<std::string> zero_q_result;
401+
EXPECT_TRUE(detail::parse_accept_header("text/html;q=0,application/json;q=1",
402+
zero_q_result));
403+
EXPECT_EQ(zero_q_result.size(), 2);
404+
EXPECT_EQ(zero_q_result[0], "application/json"); // q=1
405+
EXPECT_EQ(zero_q_result[1], "text/html"); // q=0
406+
407+
// No spaces around commas
408+
std::vector<std::string> no_space_result;
409+
EXPECT_TRUE(detail::parse_accept_header(
410+
"text/html;q=0.9,application/json;q=0.8,text/plain;q=0.7",
411+
no_space_result));
412+
EXPECT_EQ(no_space_result.size(), 3);
413+
EXPECT_EQ(no_space_result[0], "text/html");
414+
EXPECT_EQ(no_space_result[1], "application/json");
415+
EXPECT_EQ(no_space_result[2], "text/plain");
416+
}
417+
418+
TEST(ParseAcceptHeaderTest, InvalidCases) {
419+
std::vector<std::string> result;
420+
421+
// Invalid quality value (> 1.0)
422+
EXPECT_FALSE(
423+
detail::parse_accept_header("text/html;q=1.5,application/json", result));
424+
425+
// Invalid quality value (< 0.0)
426+
EXPECT_FALSE(
427+
detail::parse_accept_header("text/html;q=-0.1,application/json", result));
428+
429+
// Invalid quality value (not a number)
430+
EXPECT_FALSE(detail::parse_accept_header(
431+
"text/html;q=invalid,application/json", result));
432+
433+
// Empty quality value
434+
EXPECT_FALSE(
435+
detail::parse_accept_header("text/html;q=,application/json", result));
436+
437+
// Invalid media type format (no slash and not wildcard)
438+
EXPECT_FALSE(
439+
detail::parse_accept_header("invalidtype,application/json", result));
440+
441+
// Empty media type
442+
result.clear();
443+
EXPECT_FALSE(detail::parse_accept_header(",application/json", result));
444+
445+
// Only commas
446+
result.clear();
447+
EXPECT_FALSE(detail::parse_accept_header(",,,", result));
448+
449+
// Valid cases should still work
450+
EXPECT_TRUE(detail::parse_accept_header("*/*", result));
451+
EXPECT_EQ(result.size(), 1);
452+
EXPECT_EQ(result[0], "*/*");
453+
454+
EXPECT_TRUE(detail::parse_accept_header("*", result));
455+
EXPECT_EQ(result.size(), 1);
456+
EXPECT_EQ(result[0], "*");
457+
458+
EXPECT_TRUE(detail::parse_accept_header("text/*", result));
459+
EXPECT_EQ(result.size(), 1);
460+
EXPECT_EQ(result[0], "text/*");
461+
}
462+
463+
TEST(ParseAcceptHeaderTest, ContentTypesPopulatedAndInvalidHeaderHandling) {
464+
Server svr;
465+
466+
svr.Get("/accept_ok", [&](const Request &req, Response &res) {
467+
EXPECT_EQ(req.accept_content_types.size(), 3);
468+
EXPECT_EQ(req.accept_content_types[0], "application/json");
469+
EXPECT_EQ(req.accept_content_types[1], "text/html");
470+
EXPECT_EQ(req.accept_content_types[2], "*/*");
471+
res.set_content("ok", "text/plain");
472+
});
473+
474+
svr.Get("/accept_bad_request", [&](const Request &req, Response &res) {
475+
EXPECT_TRUE(false);
476+
res.set_content("bad request", "text/plain");
477+
});
478+
479+
auto listen_thread = std::thread([&svr]() { svr.listen("localhost", PORT); });
480+
auto se = detail::scope_exit([&] {
481+
svr.stop();
482+
listen_thread.join();
483+
ASSERT_FALSE(svr.is_running());
484+
});
485+
486+
svr.wait_until_ready();
487+
488+
Client cli("localhost", PORT);
489+
490+
{
491+
auto res =
492+
cli.Get("/accept_ok",
493+
{{"Accept", "application/json, text/html;q=0.8, */*;q=0.1"}});
494+
ASSERT_TRUE(res);
495+
EXPECT_EQ(StatusCode::OK_200, res->status);
496+
}
497+
498+
{
499+
auto res = cli.Get("/accept_bad_request",
500+
{{"Accept", "text/html;q=abc,application/json"}});
501+
ASSERT_TRUE(res);
502+
EXPECT_EQ(StatusCode::BadRequest_400, res->status);
503+
}
504+
}
505+
311506
TEST(DivideTest, DivideStringTests) {
312507
auto divide = [](const std::string &str, char d) {
313508
std::string lhs;

0 commit comments

Comments
 (0)