mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-11-14 19:03:30 +00:00
Compare commits
7
Commits
latest
...
546e821c65
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
546e821c65 | ||
|
|
53a7f4964e | ||
|
|
8c5fd6aa08 | ||
|
|
a2ebab183d | ||
|
|
a39e081386 | ||
|
|
07142256ed | ||
|
|
cc7a30f12f |
No files matched your search
@@ -1344,7 +1344,7 @@ public:
|
|||||||
size_type erase(const std::string &key) {
|
size_type erase(const std::string &key) {
|
||||||
auto before = entries_.size();
|
auto before = entries_.size();
|
||||||
entries_.erase(std::remove_if(entries_.begin(), entries_.end(),
|
entries_.erase(std::remove_if(entries_.begin(), entries_.end(),
|
||||||
[&](const value_type &entry) {
|
[key](const value_type &entry) {
|
||||||
return keys_equal(entry.first, key);
|
return keys_equal(entry.first, key);
|
||||||
}),
|
}),
|
||||||
entries_.end());
|
entries_.end());
|
||||||
@@ -4542,8 +4542,6 @@ public:
|
|||||||
void stop();
|
void stop();
|
||||||
|
|
||||||
private:
|
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 run_event_loop();
|
||||||
void dispatch_event(const SSEMessage &msg);
|
void dispatch_event(const SSEMessage &msg);
|
||||||
bool should_reconnect(int count) const;
|
bool should_reconnect(int count) const;
|
||||||
@@ -4578,6 +4576,40 @@ private:
|
|||||||
|
|
||||||
} // namespace sse
|
} // 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 {
|
namespace ws {
|
||||||
|
|
||||||
class WebSocketClient;
|
class WebSocketClient;
|
||||||
@@ -5023,6 +5055,110 @@ inline std::string Result::read_all() {
|
|||||||
|
|
||||||
} // namespace stream
|
} // 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 {
|
namespace sse {
|
||||||
|
|
||||||
// SSEMessage implementations
|
// SSEMessage implementations
|
||||||
@@ -5104,58 +5240,6 @@ inline void SSEClient::stop() {
|
|||||||
if (async_thread_.joinable()) { async_thread_.join(); }
|
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() {
|
inline void SSEClient::run_event_loop() {
|
||||||
auto reconnect_count = 0;
|
auto reconnect_count = 0;
|
||||||
|
|
||||||
@@ -5216,72 +5300,17 @@ inline void SSEClient::run_event_loop() {
|
|||||||
if (on_open_) { on_open_(); }
|
if (on_open_) { on_open_(); }
|
||||||
|
|
||||||
// Event receiving loop
|
// Event receiving loop
|
||||||
std::string buffer;
|
detail::SSEParser parser(
|
||||||
SSEMessage current_msg;
|
[this](const SSEMessage &msg) { dispatch_event(msg); },
|
||||||
auto has_data = false;
|
[this](const std::string &id) {
|
||||||
auto has_id = false;
|
std::lock_guard<std::mutex> lock(last_event_id_mutex_);
|
||||||
auto first_line = true;
|
last_event_id_ = id;
|
||||||
auto after_cr = false;
|
},
|
||||||
|
[this](int retry_ms) { reconnect_interval_ms_ = retry_ms; });
|
||||||
auto event_too_long = false;
|
auto event_too_long = false;
|
||||||
|
|
||||||
while (running_.load() && result.next()) {
|
while (running_.load() && result.next()) {
|
||||||
// What is left in the buffer has no line ending
|
if (!parser.parse(result.data(), result.size())) {
|
||||||
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 (on_error_) { on_error_(Error::ExceedMaxPayloadSize); }
|
if (on_error_) { on_error_(Error::ExceedMaxPayloadSize); }
|
||||||
event_too_long = true;
|
event_too_long = true;
|
||||||
break;
|
break;
|
||||||
|
|||||||
+13
-2
@@ -13,14 +13,15 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
|||||||
BROTLI_DIR = /usr/local/opt/brotli
|
BROTLI_DIR = /usr/local/opt/brotli
|
||||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
# 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.
|
# Runs all the tests and also fuzz tests against seed corpus.
|
||||||
all : $(FUZZERS)
|
all : $(FUZZERS)
|
||||||
./server_fuzzer corpus/*
|
./server_fuzzer corpus/*
|
||||||
|
./server_mount_fuzzer corpus_mount/*
|
||||||
|
|
||||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
# 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 $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
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
|
multipart_parser_fuzzer : multipart_parser_fuzzer.cc ../../httplib.h
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
$(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:
|
clean:
|
||||||
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
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 "fuzzed_stream.h"
|
||||||
|
|
||||||
#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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static FuzzableServer g_server;
|
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;
|
size_t payload_size = size - 1;
|
||||||
std::string input(payload, payload_size);
|
std::string input(payload, payload_size);
|
||||||
|
|
||||||
switch (selector % 6) {
|
switch (selector % 13) {
|
||||||
case 0: {
|
case 0: {
|
||||||
// parse_query_text
|
// parse_query_text
|
||||||
httplib::Params params;
|
httplib::Params params;
|
||||||
@@ -46,6 +46,48 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
|||||||
httplib::detail::is_valid_path(input);
|
httplib::detail::is_valid_path(input);
|
||||||
break;
|
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;
|
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
|
||||||
+214
-236
@@ -2392,6 +2392,13 @@ TEST(HeadersOrderTest, ErasingByNameKeepsTheRemainingOrder) {
|
|||||||
EXPECT_EQ("B=2 C=4 ", entries_to_string(headers));
|
EXPECT_EQ("B=2 C=4 ", entries_to_string(headers));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(HeadersOrderTest, ErasingAnAliasedNameKeepsTheRemainingOrder) {
|
||||||
|
Headers headers = {{"A", "1"}, {"B", "2"}, {"a", "3"}, {"B", "4"}};
|
||||||
|
|
||||||
|
EXPECT_EQ(2U, headers.erase(headers.begin()->first));
|
||||||
|
EXPECT_EQ("B=2 B=4 ", entries_to_string(headers));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(HeadersOrderTest, EmplaceFrontPrepends) {
|
TEST(HeadersOrderTest, EmplaceFrontPrepends) {
|
||||||
Headers headers = {{"Accept", "*/*"}, {"User-Agent", "test"}};
|
Headers headers = {{"Accept", "*/*"}, {"User-Agent", "test"}};
|
||||||
headers.emplace_front("Host", "example.com");
|
headers.emplace_front("Host", "example.com");
|
||||||
@@ -26537,264 +26544,264 @@ protected:
|
|||||||
};
|
};
|
||||||
|
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
// SSE Parsing Tests
|
// SSE Parser Tests
|
||||||
//==============================================================================
|
//==============================================================================
|
||||||
|
|
||||||
class SSEParsingTest : public SSEIntegrationTest {
|
struct SSEParseResult {
|
||||||
protected:
|
std::vector<sse::SSEMessage> events;
|
||||||
// Feeds a raw event stream to SSEClient and returns the dispatched messages.
|
std::vector<std::string> ids;
|
||||||
// A trailing sentinel event tells when the whole stream has been parsed.
|
std::vector<int> retries;
|
||||||
std::vector<sse::SSEMessage> parse(const std::string &stream) {
|
bool ok = true;
|
||||||
auto body = stream + "event: end\ndata: end\n\n";
|
std::string trace; // Every handler call in order
|
||||||
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;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, SingleLineData) {
|
static SSEParseResult
|
||||||
auto msgs = parse("data: hello\n\n");
|
sse_parse(const std::vector<std::string> &chunks,
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
size_t max_event_length = CPPHTTPLIB_SSE_MAX_EVENT_LENGTH) {
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
SSEParseResult r;
|
||||||
EXPECT_EQ(msgs[0].event, "message");
|
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) {
|
static SSEParseResult sse_parse(const std::string &stream) {
|
||||||
auto msgs = parse("data: line1\ndata: line2\ndata: line3\n\n");
|
return sse_parse(std::vector<std::string>{stream});
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
|
||||||
EXPECT_EQ(msgs[0].data, "line1\nline2\nline3");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, CustomEventType) {
|
TEST(SSEParserTest, SingleLineData) {
|
||||||
auto msgs = parse("event: update\ndata: payload\n\n");
|
auto r = sse_parse("data: hello\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].event, "update");
|
EXPECT_EQ(r.events[0].data, "hello");
|
||||||
EXPECT_EQ(msgs[0].data, "payload");
|
EXPECT_EQ(r.events[0].event, "message");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, EventIdHandling) {
|
TEST(SSEParserTest, MultiLineData) {
|
||||||
auto msgs = parse("id: 12345\ndata: test\n\n");
|
auto r = sse_parse("data: line1\ndata: line2\ndata: line3\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].id, "12345");
|
EXPECT_EQ(r.events[0].data, "line1\nline2\nline3");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, EmptyEventId) {
|
TEST(SSEParserTest, CustomEventType) {
|
||||||
auto msgs = parse("id:\ndata: test\n\n");
|
auto r = sse_parse("event: update\ndata: payload\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].id, "");
|
EXPECT_EQ(r.events[0].event, "update");
|
||||||
|
EXPECT_EQ(r.events[0].data, "payload");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, CommentsIgnored) {
|
TEST(SSEParserTest, EventIdHandling) {
|
||||||
auto msgs = parse(": this is a comment\ndata: hello\n\n");
|
auto r = sse_parse("id: 12345\ndata: test\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
EXPECT_EQ(r.events[0].id, "12345");
|
||||||
EXPECT_EQ(msgs[0].event, "message");
|
EXPECT_EQ(r.ids, std::vector<std::string>{"12345"});
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, ColonInValue) {
|
TEST(SSEParserTest, EmptyEventId) {
|
||||||
auto msgs = parse("data: hello:world:test\n\n");
|
auto r = sse_parse("id:\ndata: test\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].data, "hello:world:test");
|
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
|
// A line without a colon is a field name with an empty value
|
||||||
auto msgs = parse("data\n\n");
|
auto r = sse_parse("data\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].data, "");
|
EXPECT_EQ(r.events[0].data, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, TrailingCarriageReturn) {
|
TEST(SSEParserTest, TrailingCarriageReturn) {
|
||||||
auto msgs = parse("event: update\r\ndata: hello\r\n\r\n");
|
auto r = sse_parse("event: update\r\ndata: hello\r\n\r\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].event, "update");
|
EXPECT_EQ(r.events[0].event, "update");
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
EXPECT_EQ(r.events[0].data, "hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, FieldNameOnlyWithCarriageReturn) {
|
TEST(SSEParserTest, FieldNameOnlyWithCarriageReturn) {
|
||||||
auto msgs = parse("data\r\n\r\n");
|
auto r = sse_parse("data\r\n\r\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].data, "");
|
EXPECT_EQ(r.events[0].data, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, CarriageReturnLineEnding) {
|
TEST(SSEParserTest, CarriageReturnLineEnding) {
|
||||||
auto msgs = parse("event: update\rdata: hello\r\r");
|
auto r = sse_parse("event: update\rdata: hello\r\r");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].event, "update");
|
EXPECT_EQ(r.events[0].event, "update");
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
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
|
// Chunk boundaries fall inside every \r\n and right before the first one
|
||||||
std::vector<std::string> chunks = {"event: update", "\r", "\ndata: hello\r",
|
auto r = sse_parse(std::vector<std::string>{"event: update", "\r",
|
||||||
"\n\r", "\nevent: end\ndata: end\n\n"};
|
"\ndata: hello\r", "\n\r", "\n"});
|
||||||
server_->Get("/split", [chunks](const Request &, Response &res) {
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
auto i = std::make_shared<size_t>(0);
|
EXPECT_EQ(r.events[0].event, "update");
|
||||||
res.set_chunked_content_provider(
|
EXPECT_EQ(r.events[0].data, "hello");
|
||||||
"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) {
|
TEST(SSEParserTest, LeadingByteOrderMarkIgnored) {
|
||||||
// Only a BOM at the start of the stream is ignored
|
// Only a BOM at the start of the stream is ignored
|
||||||
auto msgs = parse("\xEF\xBB\xBF"
|
auto r = sse_parse("\xEF\xBB\xBF"
|
||||||
"data: first\n\n"
|
"data: first\n\n"
|
||||||
"\xEF\xBB\xBF"
|
"\xEF\xBB\xBF"
|
||||||
"data: second\n\n");
|
"data: second\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].data, "first");
|
EXPECT_EQ(r.events[0].data, "first");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, UnknownFieldsIgnored) {
|
TEST(SSEParserTest, UnknownFieldsIgnored) {
|
||||||
auto msgs = parse("unknown: value\ndata: hello\n\n");
|
auto r = sse_parse("unknown: value\ndata: hello\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
EXPECT_EQ(r.events[0].data, "hello");
|
||||||
EXPECT_EQ(msgs[0].event, "message");
|
EXPECT_EQ(r.events[0].event, "message");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, SpaceAfterColonOptional) {
|
TEST(SSEParserTest, SpaceAfterColonOptional) {
|
||||||
auto msgs = parse("data: hello\n\ndata:hello\n\n");
|
auto r = sse_parse("data: hello\n\ndata:hello\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 2u);
|
ASSERT_EQ(r.events.size(), 2u);
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
EXPECT_EQ(r.events[0].data, "hello");
|
||||||
EXPECT_EQ(msgs[1].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
|
// An event without data is not dispatched, and its type must not leak
|
||||||
// into the next event
|
// into the next event
|
||||||
auto msgs = parse("event: update\n\ndata: hello\n\n");
|
auto r = sse_parse("event: update\n\ndata: hello\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
EXPECT_EQ(msgs[0].event, "message");
|
EXPECT_EQ(r.events[0].event, "message");
|
||||||
EXPECT_EQ(msgs[0].data, "hello");
|
EXPECT_EQ(r.events[0].data, "hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, EmptyEventTypeIsMessage) {
|
TEST(SSEParserTest, EmptyEventTypeIsMessage) {
|
||||||
// Also when the empty field replaces an earlier type
|
// Also when the empty field replaces an earlier type
|
||||||
auto msgs = parse("event:\ndata: a\n\nevent: update\nevent:\ndata: b\n\n");
|
auto r = sse_parse("event:\ndata: a\n\nevent: update\nevent:\ndata: b\n\n");
|
||||||
ASSERT_EQ(msgs.size(), 2u);
|
ASSERT_EQ(r.events.size(), 2u);
|
||||||
EXPECT_EQ(msgs[0].event, "message");
|
EXPECT_EQ(r.events[0].event, "message");
|
||||||
EXPECT_EQ(msgs[1].event, "message");
|
EXPECT_EQ(r.events[1].event, "message");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, EventWithoutDataUpdatesLastEventId) {
|
TEST(SSEParserTest, EventWithoutDataReportsId) {
|
||||||
// The first connection sends only an id; the second echoes back the
|
auto r = sse_parse("id: 42\n\n");
|
||||||
// Last-Event-ID it was reconnected with
|
EXPECT_TRUE(r.events.empty());
|
||||||
std::atomic<int> connection_count{0};
|
EXPECT_EQ(r.ids, std::vector<std::string>{"42"});
|
||||||
server_->Get("/id-only", [&](const Request &req, Response &res) {
|
}
|
||||||
if (connection_count++ == 0) {
|
|
||||||
res.set_content("id: 42\n\n", "text/event-stream");
|
TEST(SSEParserTest, EmptyEventIdClearsId) {
|
||||||
} else {
|
auto r = sse_parse("id: 1\ndata: first\n\nid:\ndata: second\n\n");
|
||||||
res.set_content("data: " + req.get_header_value("Last-Event-ID") +
|
ASSERT_EQ(r.events.size(), 2u);
|
||||||
"\n\nevent: end\ndata: end\n\n",
|
EXPECT_EQ(r.events[0].id, "1");
|
||||||
"text/event-stream");
|
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");
|
std::vector<std::string> bytes;
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
for (auto c : stream) {
|
||||||
EXPECT_EQ(msgs[0].data, "42");
|
bytes.emplace_back(1, c);
|
||||||
}
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
});
|
EXPECT_EQ(sse_parse(bytes).trace, expected) << "byte by byte";
|
||||||
|
}
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, UnsendableEventIdKeepsLastEventId) {
|
TEST(SSEParserTest, MaxEventLength) {
|
||||||
// The first connection sets an id and then sends ids that cannot be carried
|
// A line still waiting for its end
|
||||||
// in a header; the second echoes back the Last-Event-ID it was reconnected
|
EXPECT_TRUE(sse_parse(std::vector<std::string>{"data: 12"}, 8).ok);
|
||||||
// with
|
EXPECT_FALSE(sse_parse(std::vector<std::string>{"data: 123"}, 8).ok);
|
||||||
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");
|
// Data accumulated over the lines of one event
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
EXPECT_TRUE(
|
||||||
EXPECT_EQ(msgs[0].data, "42");
|
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) {
|
// 0 means no limit
|
||||||
auto msgs = parse("event: notification\nid: evt-42\n"
|
EXPECT_TRUE(sse_parse(std::vector<std::string>{"data: 123"}, 0).ok);
|
||||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
|
||||||
ASSERT_EQ(msgs.size(), 1u);
|
// Events completed in the same chunk are still reported
|
||||||
EXPECT_EQ(msgs[0].event, "notification");
|
auto r = sse_parse(std::vector<std::string>{"data: a\n\ndata: 123456789"}, 8);
|
||||||
EXPECT_EQ(msgs[0].id, "evt-42");
|
EXPECT_FALSE(r.ok);
|
||||||
EXPECT_EQ(msgs[0].data, "{\"type\":\"alert\"}");
|
ASSERT_EQ(r.events.size(), 1u);
|
||||||
|
EXPECT_EQ(r.events[0].data, "a");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(SSEMessageTest, Clear) {
|
TEST(SSEMessageTest, Clear) {
|
||||||
@@ -27419,35 +27426,6 @@ TEST_F(SSEIntegrationTest, StreamEndingAtConnectionCloseIsNotAnError) {
|
|||||||
EXPECT_EQ(0, error_count.load());
|
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: A retry field of all ASCII digits sets the reconnection time
|
||||||
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
TEST_F(SSEIntegrationTest, DigitRetryFieldApplied) {
|
||||||
std::mutex mutex;
|
std::mutex mutex;
|
||||||
|
|||||||
Reference in new issue
Block a user