Compare commits

..
6 Commits
Author SHA1 Message Date
yhirose 546e821c65 Add server_mount_fuzzer for static file serving (#2666)
Fuzz Server::handle_file_request through a mount point backed by a temporary directory that holds symlinks escaping the mount root, one of them into a sibling directory sharing its name prefix. Abort if a token stored outside the root appears in a response. The seeds are based on those of #2661.

Move FuzzedStream into fuzzed_stream.h so that both server fuzzers share it, and add FuzzableServer::MarkAsRunning(), without which a server that is not listening writes no file.
2026-11-12 21:52:33 -05:00
yhirose 53a7f4964e Add sse_parser_fuzzer for detail::SSEParser (#2665)
Feed the event stream to the parser in chunks and abort if the reported events, ids and retries differ from parsing it in one piece. Also run with a length limit to cover the early-stop path.
2026-11-12 21:11:48 -05:00
Arthur Chan 8c5fd6aa08 OSS-Fuzz: Improve url_parser_fuzzer with more cases (#2664)
Add cases for relative location resolution, dot segment removal, URI encoding and decoding, and filename sanitization.
2026-11-12 20:47:26 -05:00
Arthur Chan a2ebab183d OSS-Fuzz: Add new fuzzer/options targets websocket (#2663)
Add websocket_fuzzer covering frame reading, UTF-8 and close payload validation, and the upgrade response parser. Add websocket_fuzzer.options to raise max_len above the 64 KiB frame length boundary.
2026-11-12 20:47:22 -05:00
yhirose a39e081386 Test SSEParser directly instead of through a server
Move the SSE parsing tests from a fixture that started a server and an
SSEClient for every case to unit tests of detail::SSEParser, and turn
NonDigitRetryFieldIgnored, which slept for 500ms, into one of them. Add
tests that feed each stream split at every position and byte by byte,
and that cover the max event length and an event left incomplete at the
end of the stream.
2026-11-12 19:07:54 -05:00
yhirose 07142256ed Extract SSE event stream parsing into detail::SSEParser
Move the line splitting and event assembly out of
SSEClient::run_event_loop() into a parser that is fed chunks and reports
events, ids and retry values through handlers. The event loop is left
with I/O and reconnection, and the parser can be driven without a
server, which the following test and fuzzer changes rely on.

No behavior change.
2026-11-12 18:27:58 -05:00
23 changed files with 779 additions and 432 deletions

No files matched your search

+146 -117
View File
@@ -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
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
HEAD /mnt/a.txt HTTP/1.1
Host: x
+4
View File
@@ -0,0 +1,4 @@
GET /mnt/a.txt HTTP/1.1
Host: x
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/index.html HTTP/1.1
Host: x
+4
View File
@@ -0,0 +1,4 @@
GET /mnt/index.html HTTP/1.1
Host: x
If-None-Match: W/"bogusetag"
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/link-in.txt HTTP/1.1
Host: x
+3
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
GET /mnt/missing.txt HTTP/1.1
Host: x
+4
View File
@@ -0,0 +1,4 @@
GET /mnt/a.txt HTTP/1.1
Host: x
Range: bytes=0-3
+3
View File
@@ -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
+84
View File
@@ -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 -76
View File
@@ -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;
+117
View File
@@ -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(), &times) == 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;
}
+24
View File
@@ -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"
+49
View File
@@ -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;
}
+43 -1
View File
@@ -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;
+54
View File
@@ -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;
}
+2
View File
@@ -0,0 +1,2 @@
[libfuzzer]
max_len = 70000
+207 -236
View File
@@ -26544,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) {
@@ -27426,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;