mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-14 19:03:30 +00:00
Compare commits
6
Commits
cc7a30f12f
...
546e821c65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
546e821c65 | ||
|
|
53a7f4964e | ||
|
|
8c5fd6aa08 | ||
|
|
a2ebab183d | ||
|
|
a39e081386 | ||
|
|
07142256ed |
No files matched your search
@@ -4542,8 +4542,6 @@ public:
|
||||
void stop();
|
||||
|
||||
private:
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms,
|
||||
bool &has_data, bool &has_id);
|
||||
void run_event_loop();
|
||||
void dispatch_event(const SSEMessage &msg);
|
||||
bool should_reconnect(int count) const;
|
||||
@@ -4578,6 +4576,40 @@ private:
|
||||
|
||||
} // namespace sse
|
||||
|
||||
namespace detail {
|
||||
|
||||
// Interprets an event stream fed in chunks of any size
|
||||
class SSEParser {
|
||||
public:
|
||||
using EventHandler = std::function<void(const sse::SSEMessage &msg)>;
|
||||
using IdHandler = std::function<void(const std::string &id)>;
|
||||
using RetryHandler = std::function<void(int retry_ms)>;
|
||||
|
||||
SSEParser(EventHandler on_event, IdHandler on_id, RetryHandler on_retry,
|
||||
size_t max_event_length = CPPHTTPLIB_SSE_MAX_EVENT_LENGTH);
|
||||
|
||||
// Returns false once the line and the event still waiting for their end
|
||||
// exceed max_event_length. 0 means no limit.
|
||||
bool parse(const char *buf, size_t n);
|
||||
|
||||
private:
|
||||
void parse_line(const std::string &line);
|
||||
|
||||
EventHandler on_event_;
|
||||
IdHandler on_id_;
|
||||
RetryHandler on_retry_;
|
||||
size_t max_event_length_;
|
||||
|
||||
std::string buffer_;
|
||||
sse::SSEMessage msg_;
|
||||
bool has_data_ = false;
|
||||
bool has_id_ = false;
|
||||
bool first_line_ = true;
|
||||
bool after_cr_ = false;
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
namespace ws {
|
||||
|
||||
class WebSocketClient;
|
||||
@@ -5023,6 +5055,110 @@ inline std::string Result::read_all() {
|
||||
|
||||
} // namespace stream
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline SSEParser::SSEParser(EventHandler on_event, IdHandler on_id,
|
||||
RetryHandler on_retry, size_t max_event_length)
|
||||
: on_event_(std::move(on_event)), on_id_(std::move(on_id)),
|
||||
on_retry_(std::move(on_retry)), max_event_length_(max_event_length) {}
|
||||
|
||||
inline bool SSEParser::parse(const char *buf, size_t n) {
|
||||
// What is left in the buffer has no line ending
|
||||
auto search_pos = buffer_.size();
|
||||
buffer_.append(buf, n);
|
||||
|
||||
// A line ends with \r\n, \n or \r per the SSE spec
|
||||
size_t line_start = 0;
|
||||
size_t eol_pos;
|
||||
|
||||
while ((eol_pos = buffer_.find_first_of("\r\n", search_pos)) !=
|
||||
std::string::npos) {
|
||||
auto line = buffer_.substr(line_start, eol_pos - line_start);
|
||||
line_start = eol_pos + 1;
|
||||
search_pos = line_start;
|
||||
|
||||
// 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_) {
|
||||
first_line_ = false;
|
||||
if (line.compare(0, 3, "\xEF\xBB\xBF") == 0) { line.erase(0, 3); }
|
||||
}
|
||||
|
||||
parse_line(line);
|
||||
}
|
||||
|
||||
// Keep unprocessed data in buffer
|
||||
buffer_.erase(0, line_start);
|
||||
|
||||
return max_event_length_ == 0 ||
|
||||
buffer_.size() + msg_.data.size() <= max_event_length_;
|
||||
}
|
||||
|
||||
inline void SSEParser::parse_line(const std::string &line) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty()) {
|
||||
// The id counts for Last-Event-ID even for an event that has no data. An
|
||||
// empty id clears it.
|
||||
if (has_id_ && on_id_) { on_id_(msg_.id); }
|
||||
|
||||
// An event without a data field is not dispatched
|
||||
if (has_data_ && on_event_) { on_event_(msg_); }
|
||||
|
||||
msg_.clear();
|
||||
has_data_ = false;
|
||||
has_id_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Lines starting with ':' are comments (ignored)
|
||||
if (line[0] == ':') { return; }
|
||||
|
||||
// Find the colon separator
|
||||
auto colon_pos = line.find(':');
|
||||
auto field = line.substr(0, colon_pos);
|
||||
std::string value;
|
||||
|
||||
// Value starts after colon, skip optional single space
|
||||
if (colon_pos != std::string::npos && colon_pos + 1 < line.size()) {
|
||||
auto value_start = colon_pos + 1;
|
||||
if (line[value_start] == ' ') { value_start++; }
|
||||
value = line.substr(value_start);
|
||||
}
|
||||
|
||||
// Handle known fields
|
||||
if (field == "event") {
|
||||
// An empty event type is dispatched as "message" per the SSE spec
|
||||
msg_.event = value.empty() ? std::string("message") : std::move(value);
|
||||
} else if (field == "data") {
|
||||
// Multiple data lines are concatenated with newlines
|
||||
if (has_data_) { msg_.data += "\n"; }
|
||||
msg_.data += value;
|
||||
has_data_ = true;
|
||||
} else if (field == "id") {
|
||||
// An id that cannot be sent back as Last-Event-ID is ignored. Empty id is
|
||||
// valid (clears the last event ID)
|
||||
if (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.
|
||||
if (is_numeric(value)) {
|
||||
int v = 0;
|
||||
auto res = from_chars(value.data(), value.data() + value.size(), v);
|
||||
if (res.ec == std::errc{} && on_retry_) { on_retry_(v); }
|
||||
}
|
||||
}
|
||||
// Unknown fields are ignored per SSE spec
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
namespace sse {
|
||||
|
||||
// SSEMessage implementations
|
||||
@@ -5104,58 +5240,6 @@ inline void SSEClient::stop() {
|
||||
if (async_thread_.joinable()) { async_thread_.join(); }
|
||||
}
|
||||
|
||||
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
int &retry_ms, bool &has_data,
|
||||
bool &has_id) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty()) { return true; }
|
||||
|
||||
// Lines starting with ':' are comments (ignored)
|
||||
if (line[0] == ':') { return false; }
|
||||
|
||||
// Find the colon separator
|
||||
auto colon_pos = line.find(':');
|
||||
auto field = line.substr(0, colon_pos);
|
||||
std::string value;
|
||||
|
||||
// Value starts after colon, skip optional single space
|
||||
if (colon_pos != std::string::npos && colon_pos + 1 < line.size()) {
|
||||
auto value_start = colon_pos + 1;
|
||||
if (line[value_start] == ' ') { value_start++; }
|
||||
value = line.substr(value_start);
|
||||
}
|
||||
|
||||
// Handle known fields
|
||||
if (field == "event") {
|
||||
// An empty event type is dispatched as "message" per the SSE spec
|
||||
msg.event = value.empty() ? std::string("message") : std::move(value);
|
||||
} else if (field == "data") {
|
||||
// Multiple data lines are concatenated with newlines
|
||||
if (has_data) { msg.data += "\n"; }
|
||||
msg.data += value;
|
||||
has_data = true;
|
||||
} else if (field == "id") {
|
||||
// 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.
|
||||
if (detail::is_numeric(value)) {
|
||||
int v = 0;
|
||||
auto res =
|
||||
detail::from_chars(value.data(), value.data() + value.size(), v);
|
||||
if (res.ec == std::errc{}) { retry_ms = v; }
|
||||
}
|
||||
}
|
||||
// Unknown fields are ignored per SSE spec
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
inline void SSEClient::run_event_loop() {
|
||||
auto reconnect_count = 0;
|
||||
|
||||
@@ -5216,72 +5300,17 @@ inline void SSEClient::run_event_loop() {
|
||||
if (on_open_) { on_open_(); }
|
||||
|
||||
// Event receiving loop
|
||||
std::string buffer;
|
||||
SSEMessage current_msg;
|
||||
auto has_data = false;
|
||||
auto has_id = false;
|
||||
auto first_line = true;
|
||||
auto after_cr = false;
|
||||
detail::SSEParser parser(
|
||||
[this](const SSEMessage &msg) { dispatch_event(msg); },
|
||||
[this](const std::string &id) {
|
||||
std::lock_guard<std::mutex> lock(last_event_id_mutex_);
|
||||
last_event_id_ = id;
|
||||
},
|
||||
[this](int retry_ms) { reconnect_interval_ms_ = retry_ms; });
|
||||
auto event_too_long = 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. A line ends with \r\n, \n or \r
|
||||
// per the SSE spec
|
||||
size_t line_start = 0;
|
||||
size_t eol_pos;
|
||||
|
||||
while ((eol_pos = buffer.find_first_of("\r\n", search_pos)) !=
|
||||
std::string::npos) {
|
||||
auto line = buffer.substr(line_start, eol_pos - line_start);
|
||||
line_start = eol_pos + 1;
|
||||
search_pos = line_start;
|
||||
|
||||
// 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) {
|
||||
first_line = false;
|
||||
if (line.compare(0, 3, "\xEF\xBB\xBF") == 0) { line.erase(0, 3); }
|
||||
}
|
||||
|
||||
// Parse the line and check if event is complete
|
||||
auto event_complete = parse_sse_line(
|
||||
line, current_msg, reconnect_interval_ms_, has_data, has_id);
|
||||
|
||||
if (event_complete) {
|
||||
// Update last_event_id for reconnection, even for an event that
|
||||
// has no data. An empty id clears it.
|
||||
if (has_id) {
|
||||
std::lock_guard<std::mutex> lock(last_event_id_mutex_);
|
||||
last_event_id_ = current_msg.id;
|
||||
}
|
||||
|
||||
// An event without a data field is not dispatched
|
||||
if (has_data) { dispatch_event(current_msg); }
|
||||
|
||||
// Reset the message for the next event either way
|
||||
current_msg.clear();
|
||||
has_data = false;
|
||||
has_id = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep unprocessed data in buffer
|
||||
buffer.erase(0, line_start);
|
||||
|
||||
// Neither an event nor a line still waiting for its end may grow
|
||||
// without bound. 0 means no limit.
|
||||
if (CPPHTTPLIB_SSE_MAX_EVENT_LENGTH > 0 &&
|
||||
buffer.size() + current_msg.data.size() >
|
||||
CPPHTTPLIB_SSE_MAX_EVENT_LENGTH) {
|
||||
if (!parser.parse(result.data(), result.size())) {
|
||||
if (on_error_) { on_error_(Error::ExceedMaxPayloadSize); }
|
||||
event_too_long = true;
|
||||
break;
|
||||
|
||||
+13
-2
@@ -13,14 +13,15 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
||||
BROTLI_DIR = /usr/local/opt/brotli
|
||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer
|
||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer websocket_fuzzer sse_parser_fuzzer server_mount_fuzzer
|
||||
|
||||
# Runs all the tests and also fuzz tests against seed corpus.
|
||||
all : $(FUZZERS)
|
||||
./server_fuzzer corpus/*
|
||||
./server_mount_fuzzer corpus_mount/*
|
||||
|
||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||
server_fuzzer : server_fuzzer.cc ../../httplib.h
|
||||
server_fuzzer : server_fuzzer.cc fuzzed_stream.h ../../httplib.h
|
||||
# $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
||||
@@ -38,5 +39,15 @@ url_parser_fuzzer : url_parser_fuzzer.cc ../../httplib.h
|
||||
multipart_parser_fuzzer : multipart_parser_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
websocket_fuzzer : websocket_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
sse_parser_fuzzer : sse_parser_fuzzer.cc ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
|
||||
server_mount_fuzzer : server_mount_fuzzer.cc fuzzed_stream.h ../../httplib.h
|
||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||
zip -q -r server_mount_fuzzer_seed_corpus.zip corpus_mount
|
||||
|
||||
clean:
|
||||
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
||||
@@ -0,0 +1,3 @@
|
||||
HEAD /mnt/a.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /mnt/a.txt HTTP/1.1
|
||||
Host: x
|
||||
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/index.html HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /mnt/index.html HTTP/1.1
|
||||
Host: x
|
||||
If-None-Match: W/"bogusetag"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/link-in.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/link-out.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/link-sibling.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/missing.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
GET /mnt/a.txt HTTP/1.1
|
||||
Host: x
|
||||
Range: bytes=0-3
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/sub/ HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/../../../../etc/passwd HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
GET /mnt/%2e%2e/secret.txt HTTP/1.1
|
||||
Host: x
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
class FuzzedStream : public httplib::Stream {
|
||||
public:
|
||||
FuzzedStream(const uint8_t *data, size_t size)
|
||||
: data_(data), size_(size), read_pos_(0) {}
|
||||
|
||||
ssize_t read(char *ptr, size_t size) override {
|
||||
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
|
||||
memcpy(ptr, data_ + read_pos_, size);
|
||||
read_pos_ += size;
|
||||
// Like SocketStream, pull in more than was asked for, which the line
|
||||
// reader then takes through buffered_data(), so both of its paths run.
|
||||
buffered_end_ =
|
||||
(std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096));
|
||||
return static_cast<ssize_t>(size);
|
||||
}
|
||||
|
||||
const char *buffered_data(size_t &size) const override {
|
||||
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
|
||||
return size ? reinterpret_cast<const char *>(data_ + read_pos_) : nullptr;
|
||||
}
|
||||
|
||||
void consume_buffered(size_t size) override { read_pos_ += size; }
|
||||
|
||||
ssize_t write(const char *ptr, size_t size) override {
|
||||
response_.append(ptr, size);
|
||||
return static_cast<int>(size);
|
||||
}
|
||||
|
||||
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
|
||||
|
||||
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
|
||||
|
||||
bool is_readable() const override { return true; }
|
||||
|
||||
bool wait_readable() const override { return true; }
|
||||
|
||||
bool wait_writable() const override { return true; }
|
||||
|
||||
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
socket_t socket() const override { return 0; }
|
||||
|
||||
time_t duration() const override { return 0; };
|
||||
|
||||
const std::string &response() const { return response_; }
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
size_t size_;
|
||||
size_t read_pos_;
|
||||
size_t buffered_end_ = 0;
|
||||
std::string response_;
|
||||
};
|
||||
|
||||
class FuzzableServer : public httplib::Server {
|
||||
public:
|
||||
// A server that is not listening counts as shutting down, and then writes
|
||||
// no file. The socket is never used.
|
||||
void MarkAsRunning() { svr_sock_ = 0; }
|
||||
|
||||
void ProcessFuzzedRequest(FuzzedStream &stream) {
|
||||
bool connection_close = false;
|
||||
process_request(stream,
|
||||
/*remote_addr=*/"",
|
||||
/*remote_port =*/0,
|
||||
/*local_addr=*/"",
|
||||
/*local_port =*/0,
|
||||
/*last_connection=*/false, connection_close, nullptr);
|
||||
}
|
||||
};
|
||||
@@ -1,79 +1,4 @@
|
||||
#include <cstdint>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
class FuzzedStream : public httplib::Stream {
|
||||
public:
|
||||
FuzzedStream(const uint8_t *data, size_t size)
|
||||
: data_(data), size_(size), read_pos_(0) {}
|
||||
|
||||
ssize_t read(char *ptr, size_t size) override {
|
||||
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
|
||||
memcpy(ptr, data_ + read_pos_, size);
|
||||
read_pos_ += size;
|
||||
// Like SocketStream, pull in more than was asked for, which the line
|
||||
// reader then takes through buffered_data(), so both of its paths run.
|
||||
buffered_end_ =
|
||||
(std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096));
|
||||
return static_cast<ssize_t>(size);
|
||||
}
|
||||
|
||||
const char *buffered_data(size_t &size) const override {
|
||||
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
|
||||
return size ? reinterpret_cast<const char *>(data_ + read_pos_) : nullptr;
|
||||
}
|
||||
|
||||
void consume_buffered(size_t size) override { read_pos_ += size; }
|
||||
|
||||
ssize_t write(const char *ptr, size_t size) override {
|
||||
response_.append(ptr, size);
|
||||
return static_cast<int>(size);
|
||||
}
|
||||
|
||||
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
|
||||
|
||||
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
|
||||
|
||||
bool is_readable() const override { return true; }
|
||||
|
||||
bool wait_readable() const override { return true; }
|
||||
|
||||
bool wait_writable() const override { return true; }
|
||||
|
||||
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
||||
ip = "127.0.0.1";
|
||||
port = 8080;
|
||||
}
|
||||
|
||||
socket_t socket() const override { return 0; }
|
||||
|
||||
time_t duration() const override { return 0; };
|
||||
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
size_t size_;
|
||||
size_t read_pos_;
|
||||
size_t buffered_end_ = 0;
|
||||
std::string response_;
|
||||
};
|
||||
|
||||
class FuzzableServer : public httplib::Server {
|
||||
public:
|
||||
void ProcessFuzzedRequest(FuzzedStream &stream) {
|
||||
bool connection_close = false;
|
||||
process_request(stream,
|
||||
/*remote_addr=*/"",
|
||||
/*remote_port =*/0,
|
||||
/*local_addr=*/"",
|
||||
/*local_port =*/0,
|
||||
/*last_connection=*/false, connection_close, nullptr);
|
||||
}
|
||||
};
|
||||
#include "fuzzed_stream.h"
|
||||
|
||||
static FuzzableServer g_server;
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <utime.h>
|
||||
|
||||
#include "fuzzed_stream.h"
|
||||
|
||||
// Fixture layout, with <token> unknown to the fuzzer:
|
||||
// root/mnt/{index.html,a.txt,data.bin,sub/index.html} served at /mnt
|
||||
// root/mnt/link-in.txt -> a.txt (stays inside, may be served)
|
||||
// root/mnt/link-out.txt -> ../outside.txt (escapes, must be refused)
|
||||
// root/mnt/link-sibling.txt -> ../mnt_secret/secret.txt (shares the "mnt"
|
||||
// prefix, must be refused)
|
||||
// The response must never contain <token>.
|
||||
|
||||
namespace {
|
||||
|
||||
FuzzableServer g_server;
|
||||
std::string g_root;
|
||||
std::string g_token;
|
||||
std::vector<std::string> g_paths; // In creation order
|
||||
|
||||
void check(bool ok, const char *what) {
|
||||
if (!ok) {
|
||||
fprintf(stderr, "fixture setup failed: %s\n", what);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
|
||||
void make_dir(const std::string &path) {
|
||||
check(mkdir(path.c_str(), 0755) == 0, path.c_str());
|
||||
g_paths.push_back(path);
|
||||
}
|
||||
|
||||
void make_file(const std::string &path, const std::string &content) {
|
||||
{
|
||||
std::ofstream out(path, std::ios::binary);
|
||||
out << content;
|
||||
check(out.good(), path.c_str());
|
||||
}
|
||||
// Fixed, so that conditional requests behave the same in every run
|
||||
utimbuf times = {1700000000, 1700000000};
|
||||
check(utime(path.c_str(), ×) == 0, path.c_str());
|
||||
g_paths.push_back(path);
|
||||
}
|
||||
|
||||
void make_link(const std::string &target, const std::string &path) {
|
||||
check(symlink(target.c_str(), path.c_str()) == 0, path.c_str());
|
||||
g_paths.push_back(path);
|
||||
}
|
||||
|
||||
std::string serve(const uint8_t *data, size_t size) {
|
||||
FuzzedStream stream{data, size};
|
||||
g_server.ProcessFuzzedRequest(stream);
|
||||
return stream.response();
|
||||
}
|
||||
|
||||
void check_served(const char *path) {
|
||||
auto req = std::string("GET ") + path + " HTTP/1.1\r\nHost: x\r\n\r\n";
|
||||
auto res = serve(reinterpret_cast<const uint8_t *>(req.data()), req.size());
|
||||
// Without this, a fuzzer that serves nothing would pass its check forever
|
||||
check(res.find("abcdef") != std::string::npos, path);
|
||||
}
|
||||
|
||||
void remove_fixture() {
|
||||
for (auto it = g_paths.rbegin(); it != g_paths.rend(); ++it) {
|
||||
if (unlink(it->c_str()) != 0) { rmdir(it->c_str()); }
|
||||
}
|
||||
rmdir(g_root.c_str());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/) {
|
||||
std::random_device rd;
|
||||
char token[33];
|
||||
snprintf(token, sizeof(token), "%08x%08x%08x%08x", rd(), rd(), rd(), rd());
|
||||
g_token = token;
|
||||
|
||||
auto tmpdir = getenv("TMPDIR");
|
||||
auto tmpl = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") +
|
||||
"/httplib-mount-XXXXXX";
|
||||
check(mkdtemp(&tmpl[0]) != nullptr, tmpl.c_str());
|
||||
g_root = tmpl;
|
||||
atexit(remove_fixture);
|
||||
|
||||
auto mnt = g_root + "/mnt";
|
||||
make_dir(mnt);
|
||||
make_dir(mnt + "/sub");
|
||||
make_dir(g_root + "/mnt_secret");
|
||||
make_file(mnt + "/index.html", "<html>index</html>");
|
||||
make_file(mnt + "/a.txt", "abcdef");
|
||||
make_file(mnt + "/data.bin", std::string("\x01\x02\x03\x04\x05\x06", 6));
|
||||
make_file(mnt + "/sub/index.html", "<html>sub</html>");
|
||||
make_file(g_root + "/outside.txt", g_token);
|
||||
make_file(g_root + "/mnt_secret/secret.txt", g_token);
|
||||
make_link("a.txt", mnt + "/link-in.txt");
|
||||
make_link("../outside.txt", mnt + "/link-out.txt");
|
||||
make_link("../mnt_secret/secret.txt", mnt + "/link-sibling.txt");
|
||||
|
||||
g_server.MarkAsRunning();
|
||||
check(g_server.set_mount_point("/mnt", mnt), "set_mount_point");
|
||||
check_served("/mnt/a.txt");
|
||||
check_served("/mnt/link-in.txt");
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (serve(data, size).find(g_token) != std::string::npos) { abort(); }
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"GET"
|
||||
"HEAD"
|
||||
"HTTP/1.1"
|
||||
|
||||
"/mnt/"
|
||||
"/mnt/index.html"
|
||||
"/mnt/a.txt"
|
||||
"/mnt/data.bin"
|
||||
"/mnt/sub/"
|
||||
"/mnt/link-in.txt"
|
||||
"/mnt/link-out.txt"
|
||||
"/mnt/link-sibling.txt"
|
||||
".."
|
||||
"%2e%2e"
|
||||
"%2f"
|
||||
"//"
|
||||
|
||||
"Range: bytes=0-"
|
||||
"Range: bytes=0-3,5-"
|
||||
"If-None-Match: "
|
||||
"W/"
|
||||
"If-Modified-Since: "
|
||||
"If-Range: "
|
||||
"Accept-Encoding: gzip"
|
||||
@@ -0,0 +1,49 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Feeds the stream in chunks and records every handler call in order
|
||||
std::string parse_in_chunks(const std::string &stream, size_t chunk,
|
||||
size_t max_event_length) {
|
||||
std::string trace;
|
||||
httplib::detail::SSEParser parser(
|
||||
[&](const httplib::sse::SSEMessage &msg) {
|
||||
trace += "event[" + msg.event + "|" + msg.data + "|" + msg.id + "]";
|
||||
},
|
||||
[&](const std::string &id) { trace += "id[" + id + "]"; },
|
||||
[&](int retry_ms) { trace += "retry[" + std::to_string(retry_ms) + "]"; },
|
||||
max_event_length);
|
||||
|
||||
for (size_t off = 0; off < stream.size(); off += chunk) {
|
||||
if (!parser.parse(stream.data() + off,
|
||||
std::min(chunk, stream.size() - off))) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size < 2 || size > 65536) { return 0; }
|
||||
|
||||
// First byte selects the chunk size, the rest is the event stream
|
||||
auto chunk = static_cast<size_t>(data[0]) + 1;
|
||||
std::string stream(reinterpret_cast<const char *>(data + 1), size - 1);
|
||||
|
||||
// Without a length limit, chunking must not change what is reported
|
||||
auto whole = parse_in_chunks(stream, stream.size(), 0);
|
||||
auto chunked = parse_in_chunks(stream, chunk, 0);
|
||||
if (whole != chunked) { std::abort(); }
|
||||
|
||||
// With a limit, only check that the parser stops cleanly
|
||||
parse_in_chunks(stream, chunk, 64);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -13,7 +13,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
size_t payload_size = size - 1;
|
||||
std::string input(payload, payload_size);
|
||||
|
||||
switch (selector % 6) {
|
||||
switch (selector % 13) {
|
||||
case 0: {
|
||||
// parse_query_text
|
||||
httplib::Params params;
|
||||
@@ -46,6 +46,48 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
httplib::detail::is_valid_path(input);
|
||||
break;
|
||||
}
|
||||
case 6: {
|
||||
// parse_url
|
||||
httplib::detail::UrlComponents uc;
|
||||
httplib::detail::parse_url(input, uc);
|
||||
break;
|
||||
}
|
||||
case 7: {
|
||||
// remove_dot_segments
|
||||
httplib::detail::remove_dot_segments(input);
|
||||
break;
|
||||
}
|
||||
case 8: {
|
||||
// resolve_relative_location, with the input as "<location>\n<base>"
|
||||
auto nl = input.find('\n');
|
||||
auto location = input.substr(0, nl);
|
||||
auto base = nl == std::string::npos ? std::string("/index.html")
|
||||
: input.substr(nl + 1);
|
||||
httplib::detail::resolve_relative_location(location, base);
|
||||
break;
|
||||
}
|
||||
case 9: {
|
||||
// encode_path
|
||||
httplib::detail::encode_path(input);
|
||||
break;
|
||||
}
|
||||
case 10: {
|
||||
// encode_uri_component, encode_uri
|
||||
httplib::encode_uri_component(input);
|
||||
httplib::encode_uri(input);
|
||||
break;
|
||||
}
|
||||
case 11: {
|
||||
// decode_uri_component, decode_uri
|
||||
httplib::decode_uri_component(input);
|
||||
httplib::decode_uri(input);
|
||||
break;
|
||||
}
|
||||
case 12: {
|
||||
// sanitize_filename
|
||||
httplib::sanitize_filename(input);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||
if (size < 2) { return 0; }
|
||||
|
||||
auto selector = data[0];
|
||||
std::string input(reinterpret_cast<const char *>(data + 1), size - 1);
|
||||
|
||||
switch (selector % 4) {
|
||||
case 0: {
|
||||
// read_websocket_frame. WebSocket::read passes the macro, close() passes
|
||||
// 125, and 0 means no limit
|
||||
static const size_t max_lens[] = {0, 125,
|
||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH};
|
||||
auto expect_masked = (selector / 4) % 2 != 0;
|
||||
auto max_len = max_lens[(selector / 8) % 3];
|
||||
httplib::detail::BufferStream strm;
|
||||
strm.write(input.data(), input.size());
|
||||
auto opcode = httplib::ws::Opcode::Text;
|
||||
std::string payload;
|
||||
auto fin = false;
|
||||
httplib::ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
||||
expect_masked, max_len);
|
||||
break;
|
||||
}
|
||||
case 1: {
|
||||
// is_valid_utf8
|
||||
httplib::ws::impl::is_valid_utf8(input);
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// is_valid_close_payload
|
||||
httplib::ws::impl::is_valid_close_payload(input);
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// read_websocket_upgrade_response, with a fixed key since
|
||||
// perform_websocket_handshake draws a random one
|
||||
static const auto expected_accept =
|
||||
httplib::detail::websocket_accept_key("dGhlIHNhbXBsZSBub25jZQ==");
|
||||
httplib::detail::BufferStream strm;
|
||||
strm.write(input.data(), input.size());
|
||||
httplib::detail::WebSocketUpgradeResponse upgrade;
|
||||
httplib::detail::read_websocket_upgrade_response(strm, expected_accept,
|
||||
"chat, stream", upgrade);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[libfuzzer]
|
||||
max_len = 70000
|
||||
+207
-236
@@ -26544,264 +26544,264 @@ protected:
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
// SSE Parsing Tests
|
||||
// SSE Parser Tests
|
||||
//==============================================================================
|
||||
|
||||
class SSEParsingTest : public SSEIntegrationTest {
|
||||
protected:
|
||||
// Feeds a raw event stream to SSEClient and returns the dispatched messages.
|
||||
// A trailing sentinel event tells when the whole stream has been parsed.
|
||||
std::vector<sse::SSEMessage> parse(const std::string &stream) {
|
||||
auto body = stream + "event: end\ndata: end\n\n";
|
||||
server_->Get("/parse", [body](const Request &, Response &res) {
|
||||
res.set_content(body, "text/event-stream");
|
||||
});
|
||||
return collect("/parse");
|
||||
}
|
||||
|
||||
// Runs SSEClient against path until an "end" event arrives and returns the
|
||||
// messages dispatched before it
|
||||
std::vector<sse::SSEMessage> collect(const std::string &path) {
|
||||
Client client(HOST, get_port());
|
||||
sse::SSEClient sse(client, path);
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::vector<sse::SSEMessage> messages;
|
||||
auto done = false;
|
||||
|
||||
sse.on_message([&](const sse::SSEMessage &msg) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
// Ignore a replay from a reconnect that races with stop()
|
||||
if (!done) { messages.push_back(msg); }
|
||||
});
|
||||
sse.on_event("end", [&](const sse::SSEMessage &) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
done = true;
|
||||
cv.notify_all();
|
||||
});
|
||||
sse.set_reconnect_interval(100);
|
||||
sse.start_async();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
cv.wait_for(lock, std::chrono::seconds(5), [&] { return done; });
|
||||
}
|
||||
sse.stop();
|
||||
|
||||
EXPECT_TRUE(done);
|
||||
return messages;
|
||||
}
|
||||
struct SSEParseResult {
|
||||
std::vector<sse::SSEMessage> events;
|
||||
std::vector<std::string> ids;
|
||||
std::vector<int> retries;
|
||||
bool ok = true;
|
||||
std::string trace; // Every handler call in order
|
||||
};
|
||||
|
||||
TEST_F(SSEParsingTest, SingleLineData) {
|
||||
auto msgs = parse("data: hello\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
EXPECT_EQ(msgs[0].event, "message");
|
||||
static SSEParseResult
|
||||
sse_parse(const std::vector<std::string> &chunks,
|
||||
size_t max_event_length = CPPHTTPLIB_SSE_MAX_EVENT_LENGTH) {
|
||||
SSEParseResult r;
|
||||
detail::SSEParser parser(
|
||||
[&](const sse::SSEMessage &msg) {
|
||||
r.events.push_back(msg);
|
||||
r.trace += "event[" + msg.event + "|" + msg.data + "|" + msg.id + "]";
|
||||
},
|
||||
[&](const std::string &id) {
|
||||
r.ids.push_back(id);
|
||||
r.trace += "id[" + id + "]";
|
||||
},
|
||||
[&](int retry_ms) {
|
||||
r.retries.push_back(retry_ms);
|
||||
r.trace += "retry[" + std::to_string(retry_ms) + "]";
|
||||
},
|
||||
max_event_length);
|
||||
for (const auto &chunk : chunks) {
|
||||
if (!parser.parse(chunk.data(), chunk.size())) {
|
||||
r.ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, MultiLineData) {
|
||||
auto msgs = parse("data: line1\ndata: line2\ndata: line3\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "line1\nline2\nline3");
|
||||
static SSEParseResult sse_parse(const std::string &stream) {
|
||||
return sse_parse(std::vector<std::string>{stream});
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, CustomEventType) {
|
||||
auto msgs = parse("event: update\ndata: payload\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].event, "update");
|
||||
EXPECT_EQ(msgs[0].data, "payload");
|
||||
TEST(SSEParserTest, SingleLineData) {
|
||||
auto r = sse_parse("data: hello\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
EXPECT_EQ(r.events[0].event, "message");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, EventIdHandling) {
|
||||
auto msgs = parse("id: 12345\ndata: test\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].id, "12345");
|
||||
TEST(SSEParserTest, MultiLineData) {
|
||||
auto r = sse_parse("data: line1\ndata: line2\ndata: line3\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "line1\nline2\nline3");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, EmptyEventId) {
|
||||
auto msgs = parse("id:\ndata: test\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].id, "");
|
||||
TEST(SSEParserTest, CustomEventType) {
|
||||
auto r = sse_parse("event: update\ndata: payload\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].event, "update");
|
||||
EXPECT_EQ(r.events[0].data, "payload");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, CommentsIgnored) {
|
||||
auto msgs = parse(": this is a comment\ndata: hello\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
EXPECT_EQ(msgs[0].event, "message");
|
||||
TEST(SSEParserTest, EventIdHandling) {
|
||||
auto r = sse_parse("id: 12345\ndata: test\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].id, "12345");
|
||||
EXPECT_EQ(r.ids, std::vector<std::string>{"12345"});
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, ColonInValue) {
|
||||
auto msgs = parse("data: hello:world:test\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "hello:world:test");
|
||||
TEST(SSEParserTest, EmptyEventId) {
|
||||
auto r = sse_parse("id:\ndata: test\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].id, "");
|
||||
EXPECT_EQ(r.ids, std::vector<std::string>{""});
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, FieldNameOnly) {
|
||||
TEST(SSEParserTest, CommentsIgnored) {
|
||||
auto r = sse_parse(": this is a comment\ndata: hello\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
EXPECT_EQ(r.events[0].event, "message");
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, ColonInValue) {
|
||||
auto r = sse_parse("data: hello:world:test\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "hello:world:test");
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, FieldNameOnly) {
|
||||
// A line without a colon is a field name with an empty value
|
||||
auto msgs = parse("data\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "");
|
||||
auto r = sse_parse("data\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, TrailingCarriageReturn) {
|
||||
auto msgs = parse("event: update\r\ndata: hello\r\n\r\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].event, "update");
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
TEST(SSEParserTest, TrailingCarriageReturn) {
|
||||
auto r = sse_parse("event: update\r\ndata: hello\r\n\r\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].event, "update");
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, FieldNameOnlyWithCarriageReturn) {
|
||||
auto msgs = parse("data\r\n\r\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "");
|
||||
TEST(SSEParserTest, FieldNameOnlyWithCarriageReturn) {
|
||||
auto r = sse_parse("data\r\n\r\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[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(SSEParserTest, CarriageReturnLineEnding) {
|
||||
auto r = sse_parse("event: update\rdata: hello\r\r");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].event, "update");
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, LineEndingSplitAcrossChunks) {
|
||||
TEST(SSEParserTest, 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");
|
||||
auto r = sse_parse(std::vector<std::string>{"event: update", "\r",
|
||||
"\ndata: hello\r", "\n\r", "\n"});
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].event, "update");
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, LeadingByteOrderMarkIgnored) {
|
||||
TEST(SSEParserTest, LeadingByteOrderMarkIgnored) {
|
||||
// Only a BOM at the start of the stream is ignored
|
||||
auto msgs = parse("\xEF\xBB\xBF"
|
||||
"data: first\n\n"
|
||||
"\xEF\xBB\xBF"
|
||||
"data: second\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "first");
|
||||
auto r = sse_parse("\xEF\xBB\xBF"
|
||||
"data: first\n\n"
|
||||
"\xEF\xBB\xBF"
|
||||
"data: second\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "first");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, UnknownFieldsIgnored) {
|
||||
auto msgs = parse("unknown: value\ndata: hello\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
EXPECT_EQ(msgs[0].event, "message");
|
||||
TEST(SSEParserTest, UnknownFieldsIgnored) {
|
||||
auto r = sse_parse("unknown: value\ndata: hello\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
EXPECT_EQ(r.events[0].event, "message");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, SpaceAfterColonOptional) {
|
||||
auto msgs = parse("data: hello\n\ndata:hello\n\n");
|
||||
ASSERT_EQ(msgs.size(), 2u);
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
EXPECT_EQ(msgs[1].data, "hello");
|
||||
TEST(SSEParserTest, SpaceAfterColonOptional) {
|
||||
auto r = sse_parse("data: hello\n\ndata:hello\n\n");
|
||||
ASSERT_EQ(r.events.size(), 2u);
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
EXPECT_EQ(r.events[1].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, EventWithoutDataResetsEventType) {
|
||||
TEST(SSEParserTest, EventWithoutDataResetsEventType) {
|
||||
// An event without data is not dispatched, and its type must not leak
|
||||
// into the next event
|
||||
auto msgs = parse("event: update\n\ndata: hello\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].event, "message");
|
||||
EXPECT_EQ(msgs[0].data, "hello");
|
||||
auto r = sse_parse("event: update\n\ndata: hello\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].event, "message");
|
||||
EXPECT_EQ(r.events[0].data, "hello");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, EmptyEventTypeIsMessage) {
|
||||
TEST(SSEParserTest, EmptyEventTypeIsMessage) {
|
||||
// Also when the empty field replaces an earlier type
|
||||
auto msgs = parse("event:\ndata: a\n\nevent: update\nevent:\ndata: b\n\n");
|
||||
ASSERT_EQ(msgs.size(), 2u);
|
||||
EXPECT_EQ(msgs[0].event, "message");
|
||||
EXPECT_EQ(msgs[1].event, "message");
|
||||
auto r = sse_parse("event:\ndata: a\n\nevent: update\nevent:\ndata: b\n\n");
|
||||
ASSERT_EQ(r.events.size(), 2u);
|
||||
EXPECT_EQ(r.events[0].event, "message");
|
||||
EXPECT_EQ(r.events[1].event, "message");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, EventWithoutDataUpdatesLastEventId) {
|
||||
// The first connection sends only an id; the second echoes back the
|
||||
// Last-Event-ID it was reconnected with
|
||||
std::atomic<int> connection_count{0};
|
||||
server_->Get("/id-only", [&](const Request &req, Response &res) {
|
||||
if (connection_count++ == 0) {
|
||||
res.set_content("id: 42\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");
|
||||
TEST(SSEParserTest, EventWithoutDataReportsId) {
|
||||
auto r = sse_parse("id: 42\n\n");
|
||||
EXPECT_TRUE(r.events.empty());
|
||||
EXPECT_EQ(r.ids, std::vector<std::string>{"42"});
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, EmptyEventIdClearsId) {
|
||||
auto r = sse_parse("id: 1\ndata: first\n\nid:\ndata: second\n\n");
|
||||
ASSERT_EQ(r.events.size(), 2u);
|
||||
EXPECT_EQ(r.events[0].id, "1");
|
||||
EXPECT_EQ(r.events[1].id, "");
|
||||
EXPECT_EQ(r.ids, (std::vector<std::string>{"1", ""}));
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, UnsendableEventIdIgnored) {
|
||||
// Ids that cannot be carried in a Last-Event-ID header
|
||||
auto r = sse_parse(std::string("id: 42\n\nid: a") + '\0' +
|
||||
"b\n\nid: \x01\n\nid: 7\n\nid: 7 \n\n");
|
||||
EXPECT_EQ(r.ids, std::vector<std::string>{"42"});
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, RetryDigitsOnly) {
|
||||
auto r = sse_parse("retry: 1000\n");
|
||||
EXPECT_EQ(r.retries, std::vector<int>{1000});
|
||||
|
||||
// A value that is not all ASCII digits, or does not fit, is ignored
|
||||
r = sse_parse("retry: -1\nretry: 1.5\nretry:\nretry: 99999999999\n");
|
||||
EXPECT_TRUE(r.retries.empty());
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, CompleteEventParsing) {
|
||||
auto r = sse_parse("event: notification\nid: evt-42\n"
|
||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].event, "notification");
|
||||
EXPECT_EQ(r.events[0].id, "evt-42");
|
||||
EXPECT_EQ(r.events[0].data, "{\"type\":\"alert\"}");
|
||||
EXPECT_EQ(r.retries, std::vector<int>{1000});
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, IncompleteEventNotReported) {
|
||||
// An event still waiting for its blank line when the stream ends is
|
||||
// discarded per the SSE spec
|
||||
auto r = sse_parse("id: 1\ndata: x\n");
|
||||
EXPECT_TRUE(r.events.empty());
|
||||
EXPECT_TRUE(r.ids.empty());
|
||||
}
|
||||
|
||||
TEST(SSEParserTest, SameResultAtEveryChunkBoundary) {
|
||||
const std::vector<std::string> streams = {
|
||||
"\xEF\xBB\xBF"
|
||||
"data: bom\r\n\r\n",
|
||||
"event: a\r\ndata: 1\r\nid: x\r\n\r\ndata: 2\rretry: 7\r\r",
|
||||
": comment\ndata\nid:\n\nid: y\n\ndata: tail\r\n\r\n",
|
||||
};
|
||||
for (const auto &stream : streams) {
|
||||
auto expected = sse_parse(stream).trace;
|
||||
ASSERT_FALSE(expected.empty());
|
||||
|
||||
for (size_t i = 0; i <= stream.size(); i++) {
|
||||
auto r = sse_parse(
|
||||
std::vector<std::string>{stream.substr(0, i), stream.substr(i)});
|
||||
EXPECT_EQ(r.trace, expected) << "split at " << i;
|
||||
}
|
||||
});
|
||||
|
||||
auto msgs = collect("/id-only");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "42");
|
||||
}
|
||||
|
||||
TEST_F(SSEParsingTest, EmptyEventIdClearsLastEventId) {
|
||||
// The first connection sets an id and then clears it with an empty one; the
|
||||
// second reports whether it was reconnected with a Last-Event-ID
|
||||
std::atomic<int> connection_count{0};
|
||||
server_->Get("/id-clear", [&](const Request &req, Response &res) {
|
||||
if (connection_count++ == 0) {
|
||||
res.set_content("id: 1\ndata: first\n\nid:\ndata: second\n\n",
|
||||
"text/event-stream");
|
||||
} else {
|
||||
res.set_content(
|
||||
std::string("data: ") +
|
||||
(req.has_header("Last-Event-ID") ? "sent" : "not sent") +
|
||||
"\n\nevent: end\ndata: end\n\n",
|
||||
"text/event-stream");
|
||||
std::vector<std::string> bytes;
|
||||
for (auto c : stream) {
|
||||
bytes.emplace_back(1, c);
|
||||
}
|
||||
});
|
||||
|
||||
auto msgs = collect("/id-clear");
|
||||
ASSERT_EQ(msgs.size(), 3u);
|
||||
EXPECT_EQ(msgs[0].id, "1");
|
||||
EXPECT_EQ(msgs[1].id, "");
|
||||
EXPECT_EQ(msgs[2].data, "not sent");
|
||||
EXPECT_EQ(sse_parse(bytes).trace, expected) << "byte by byte";
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
});
|
||||
TEST(SSEParserTest, MaxEventLength) {
|
||||
// A line still waiting for its end
|
||||
EXPECT_TRUE(sse_parse(std::vector<std::string>{"data: 12"}, 8).ok);
|
||||
EXPECT_FALSE(sse_parse(std::vector<std::string>{"data: 123"}, 8).ok);
|
||||
|
||||
auto msgs = collect("/id-unsendable");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].data, "42");
|
||||
}
|
||||
// Data accumulated over the lines of one event
|
||||
EXPECT_TRUE(
|
||||
sse_parse(std::vector<std::string>{"data: 1234\ndata: 567\n"}, 8).ok);
|
||||
EXPECT_FALSE(
|
||||
sse_parse(std::vector<std::string>{"data: 1234\ndata: 5678\n"}, 8).ok);
|
||||
|
||||
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
||||
auto msgs = parse("event: notification\nid: evt-42\n"
|
||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
||||
ASSERT_EQ(msgs.size(), 1u);
|
||||
EXPECT_EQ(msgs[0].event, "notification");
|
||||
EXPECT_EQ(msgs[0].id, "evt-42");
|
||||
EXPECT_EQ(msgs[0].data, "{\"type\":\"alert\"}");
|
||||
// 0 means no limit
|
||||
EXPECT_TRUE(sse_parse(std::vector<std::string>{"data: 123"}, 0).ok);
|
||||
|
||||
// Events completed in the same chunk are still reported
|
||||
auto r = sse_parse(std::vector<std::string>{"data: a\n\ndata: 123456789"}, 8);
|
||||
EXPECT_FALSE(r.ok);
|
||||
ASSERT_EQ(r.events.size(), 1u);
|
||||
EXPECT_EQ(r.events[0].data, "a");
|
||||
}
|
||||
|
||||
TEST(SSEMessageTest, Clear) {
|
||||
@@ -27426,35 +27426,6 @@ TEST_F(SSEIntegrationTest, StreamEndingAtConnectionCloseIsNotAnError) {
|
||||
EXPECT_EQ(0, error_count.load());
|
||||
}
|
||||
|
||||
// Test: A retry field that is not all ASCII digits is ignored
|
||||
TEST_F(SSEIntegrationTest, NonDigitRetryFieldIgnored) {
|
||||
std::atomic<int> connection_count{0};
|
||||
|
||||
server_->Get("/bad-retry",
|
||||
[&connection_count](const Request &, Response &res) {
|
||||
connection_count.fetch_add(1);
|
||||
res.set_chunked_content_provider(
|
||||
"text/event-stream", [](size_t offset, DataSink &sink) {
|
||||
if (offset == 0) {
|
||||
std::string event = "retry: -1\ndata: hello\n\n";
|
||||
sink.write(event.data(), event.size());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
});
|
||||
|
||||
Client client("localhost", get_port());
|
||||
sse::SSEClient sse(client, "/bad-retry");
|
||||
|
||||
sse.set_reconnect_interval(10000);
|
||||
sse.start_async();
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
sse.stop();
|
||||
|
||||
EXPECT_EQ(connection_count.load(), 1);
|
||||
}
|
||||
|
||||
// Test: A retry field of all ASCII digits sets the reconnection time
|
||||
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
||||
std::mutex mutex;
|
||||
|
||||
Reference in new issue
Block a user