mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-29 12:50:51 +00:00
Compare commits
4
Commits
fee592c2c2
...
5107381963
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5107381963 | ||
|
|
0acbee16dc | ||
|
|
1f12bbaabb | ||
|
|
5189edc52e |
No files matched your search
@@ -3968,6 +3968,7 @@ bool is_field_content(const std::string &s);
|
||||
bool is_field_value(const std::string &s);
|
||||
bool is_field_valid(const std::string &name, const std::string &value);
|
||||
bool is_request_target(const std::string &s);
|
||||
bool is_host(const std::string &s);
|
||||
|
||||
} // namespace fields
|
||||
} // namespace detail
|
||||
@@ -4918,9 +4919,12 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
msg.data += value;
|
||||
has_data = true;
|
||||
} else if (field == "id") {
|
||||
// Empty id is valid (clears the last event ID)
|
||||
msg.id = value;
|
||||
has_id = true;
|
||||
// An id that cannot be sent back as Last-Event-ID is ignored. Empty id is
|
||||
// valid (clears the last event ID)
|
||||
if (detail::fields::is_field_value(value)) {
|
||||
msg.id = value;
|
||||
has_id = true;
|
||||
}
|
||||
} else if (field == "retry") {
|
||||
// Parse retry interval in milliseconds
|
||||
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||
@@ -4994,22 +4998,29 @@ inline void SSEClient::run_event_loop() {
|
||||
auto has_data = false;
|
||||
auto has_id = false;
|
||||
auto first_line = true;
|
||||
auto after_cr = false;
|
||||
|
||||
while (running_.load() && result.next()) {
|
||||
// What is left in the buffer has no line ending
|
||||
auto search_pos = buffer.size();
|
||||
buffer.append(result.data(), result.size());
|
||||
|
||||
// Process complete lines in the buffer
|
||||
// Process complete lines in the buffer. A line ends with \r\n, \n or \r
|
||||
// per the SSE spec
|
||||
size_t line_start = 0;
|
||||
size_t newline_pos;
|
||||
size_t eol_pos;
|
||||
|
||||
while ((newline_pos = buffer.find('\n', line_start)) !=
|
||||
while ((eol_pos = buffer.find_first_of("\r\n", search_pos)) !=
|
||||
std::string::npos) {
|
||||
auto line = buffer.substr(line_start, newline_pos - line_start);
|
||||
line_start = newline_pos + 1;
|
||||
auto line = buffer.substr(line_start, eol_pos - line_start);
|
||||
line_start = eol_pos + 1;
|
||||
search_pos = line_start;
|
||||
|
||||
// Strip the \r of a CRLF line ending so that every field, including
|
||||
// one without a colon, sees the same line
|
||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||
// The \n of a \r\n may arrive in a later chunk than its \r
|
||||
auto is_lf_of_crlf =
|
||||
after_cr && line.empty() && buffer[eol_pos] == '\n';
|
||||
after_cr = buffer[eol_pos] == '\r';
|
||||
if (is_lf_of_crlf) { continue; }
|
||||
|
||||
// A UTF-8 BOM at the start of the stream is ignored per the SSE spec
|
||||
if (first_line) {
|
||||
@@ -10312,6 +10323,17 @@ inline bool is_request_target(const std::string &s) {
|
||||
return std::all_of(s.begin(), s.end(), is_field_vchar);
|
||||
}
|
||||
|
||||
// RFC 9110 §7.2: the bytes of uri-host [ ":" port ], i.e. unreserved,
|
||||
// sub-delims, "%", ":" and the IP-literal brackets. An empty value is valid.
|
||||
inline bool is_host(const std::string &s) {
|
||||
return std::all_of(s.begin(), s.end(), [](char c) {
|
||||
return is_ascii_alnum(c) || c == '-' || c == '.' || c == '_' || c == '~' ||
|
||||
c == '!' || c == '$' || c == '&' || c == '\'' || c == '(' ||
|
||||
c == ')' || c == '*' || c == '+' || c == ',' || c == ';' ||
|
||||
c == '=' || c == '%' || c == ':' || c == '[' || c == ']';
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace fields
|
||||
|
||||
inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
||||
@@ -14570,6 +14592,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// RFC 9112 §3.2: Reject more than one Host field line or an invalid Host
|
||||
// value, so that every recipient of the request sees the same authority.
|
||||
if (req.get_header_value_count("Host") > 1 ||
|
||||
!detail::fields::is_host(req.get_header_value("Host"))) {
|
||||
connection_closed = true;
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
output_error_log(Error::InvalidHeaders, &req);
|
||||
return write_response(strm, close_connection, req, res);
|
||||
}
|
||||
|
||||
// Check if the request URI doesn't exceed the limit
|
||||
if (req.target.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) {
|
||||
connection_closed = true;
|
||||
|
||||
+105
@@ -19686,6 +19686,58 @@ TEST(RequestSmugglingTest, DuplicateContentLengthSameValues) {
|
||||
ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
TEST(HostHeaderValidationTest, AmbiguousOrInvalidHostRejected) {
|
||||
Server svr;
|
||||
svr.Get("/test", [](const Request &, Response &res) {
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
auto status_line = [&](const std::string &host_lines) {
|
||||
std::string res;
|
||||
send_request(
|
||||
5, "GET /test HTTP/1.1\r\n" + host_lines + "Connection: close\r\n\r\n",
|
||||
&res, port);
|
||||
return res.substr(0, res.find("\r\n"));
|
||||
};
|
||||
|
||||
const char *accepted[] = {
|
||||
"Host: localhost\r\n",
|
||||
"Host: a.example:8080\r\n",
|
||||
"Host: my_service\r\n",
|
||||
"Host: [::1]:8080\r\n",
|
||||
"Host: [fe80::1%25eth0]\r\n",
|
||||
"Host: \r\n", // RFC 9112 §3.2: the target URI has no authority
|
||||
};
|
||||
for (auto host_lines : accepted) {
|
||||
EXPECT_EQ("HTTP/1.1 200 OK", status_line(host_lines)) << host_lines;
|
||||
}
|
||||
|
||||
const char *rejected[] = {
|
||||
"Host: a.example\r\nHost: b.example\r\n",
|
||||
"Host: a.example\r\nHost: a.example\r\n",
|
||||
"Host: a@b.example\r\n",
|
||||
"Host: a.example/b\r\n",
|
||||
"Host: a.example\\b\r\n",
|
||||
"Host: a.example?b\r\n",
|
||||
"Host: a.example#b\r\n",
|
||||
"Host: a.example b.example\r\n",
|
||||
};
|
||||
for (auto host_lines : rejected) {
|
||||
EXPECT_EQ("HTTP/1.1 400 Bad Request", status_line(host_lines))
|
||||
<< host_lines;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
|
||||
Server svr;
|
||||
|
||||
@@ -22793,6 +22845,37 @@ TEST_F(SSEParsingTest, FieldNameOnlyWithCarriageReturn) {
|
||||
EXPECT_EQ(msgs[0].data, "");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, CarriageReturnLineEnding) {
|
||||
auto msgs = parse("event: update\rdata: hello\r\r");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].event, "update");
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, LineEndingSplitAcrossChunks) {
|
||||
// Chunk boundaries fall inside every \r\n and right before the first one
|
||||
std::vector<std::string> chunks = {"event: update", "\r", "\ndata: hello\r",
|
||||
"\n\r", "\nevent: end\ndata: end\n\n"};
|
||||
server_->Get("/split", [chunks](const Request &, Response &res) {
|
||||
auto i = std::make_shared<size_t>(0);
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream", [chunks, i](size_t /*offset*/, DataSink &sink) {
|
||||
if (*i == chunks.size()) {
|
||||
sink.done();
|
||||
} else {
|
||||
sink.write(chunks[*i].data(), chunks[*i].size());
|
||||
(*i)++;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
|
||||
auto msgs = collect("/split");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].event, "update");
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, LeadingByteOrderMarkIgnored) {
|
||||
// Only a BOM at the start of the stream is ignored
|
||||
auto msgs = parse("\xEF\xBB\xBF"
|
||||
@@ -22869,6 +22952,28 @@ TEST_F(SSEParsingTest, EmptyEventIdClearsLastEventId) {
|
||||
EXPECT_EQ(msgs[2].data, "not sent");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, UnsendableEventIdKeepsLastEventId) {
|
||||
// The first connection sets an id and then sends ids that cannot be carried
|
||||
// in a header; the second echoes back the Last-Event-ID it was reconnected
|
||||
// with
|
||||
std::atomic<int> connection_count{0};
|
||||
server_->Get("/id-unsendable", [&](const Request &req, Response &res) {
|
||||
if (connection_count++ == 0) {
|
||||
res.set_content(std::string("id: 42\n\nid: a") + '\0' +
|
||||
"b\n\nid: \x01\n\nid: 7\n\nid: 7 \n\n",
|
||||
"text/event-stream");
|
||||
} else {
|
||||
res.set_content("data: " + req.get_header_value("Last-Event-ID") +
|
||||
"\n\nevent: end\ndata: end\n\n",
|
||||
"text/event-stream");
|
||||
}
|
||||
});
|
||||
|
||||
auto msgs = collect("/id-unsendable");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "42");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
||||
auto msgs = parse("event: notification\nid: evt-42\n"
|
||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
||||
|
||||
Reference in new issue
Block a user