Compare commits

...
16 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
userandastunm cc7a30f12f Preserve aliased keys when erasing entries (#2659)
Co-authored-by: astunm <astunm@users.noreply.github.com>
2026-11-11 14:32:09 -05:00
yhirose cdcd2ac6e6 Release v0.66.0 2026-11-10 17:19:39 -05:00
yhirose bd23a7b179 Keep the job queued when a dynamic worker thread cannot be created
ThreadPool::enqueue constructed the dynamic thread without handling
std::system_error. When thread creation failed (e.g. EAGAIN under a
pids limit), the exception escaped the accept loop and destroying the
pool's joinable threads called std::terminate. Treat the failure as
"no spare capacity" and leave the job for an existing worker.
2026-11-10 16:54:53 -05:00
metsw24-max 6d8dec3c3e Keep a redirect on the unix socket in ClientImpl::redirect (#2658)
A client using set_address_family(AF_UNIX) took the host of an absolute Location as a socket path, so a redirect to http://localhost/next failed and one to http://other.sock/next went to ./other.sock. Every hop now stays on the client's socket, as with curl's --unix-socket.

The same host is still decided from the Location, as curl does: only localhost (the Host this client sends) counts as this client, so a redirect to any other host goes through the cross-host client and drops Authorization and Cookie. That client reaches the original socket through addr_map_, and a host the caller mapped with set_direct_hostname_addr_map keeps its mapping. An https Location runs TLS over the socket with the Location's host.

The mapped-target lookup shared with make_connect_request_target is factored into detail::addr_map_target.
2026-11-07 10:22:00 -05:00
yhirose 7e5d0683ac Release v0.65.0 2026-11-05 07:39:47 -05:00
yhirose 6df971434e Do not carry Request::params over to a redirect's Location
A request sent with send(Request) and params got those params appended
to the Location of a redirect that had no query, on the same origin or
another one. The Location alone names the next target, as it already
did for Get/Post(path, params).
2026-11-05 07:04:31 -05:00
Eminy 39c98d4671 Ignore a Range header with a unit other than bytes (#2657)
RFC 9110 14.2 says an origin server MUST ignore a Range header field
with a range unit it does not understand. parse_range_header() accepts
only bytes=, so a request such as "Range: items=1-2" got a 416 instead
of the full response. The unit is now checked first, and only a bytes
range is parsed and answered with a 416 when it is invalid.
2026-11-04 22:44:22 -05:00
Wei-Lun Liandyhirose a1ba73a256 Cap a send() or wolfSSL_read() length at INT_MAX (#2655)
* Cap a send() or wolfSSL_read() length at INT_MAX

On 64-bit Windows SocketStream::write() handed size to send(), whose
length is an int, so a single write of 4 GiB or more went out as its low
32 bits: a 4 GiB + 1 byte write sent one byte, and write_data() then
retried the remaining 4 GiB as zero-byte sends forever. The INT_MAX cap
that read() has was limited to 32-bit Windows in df20c27 (#1294), when
the body was still written with one unchecked write(); write_data() now
sends the rest, so the cap applies to every Windows build again.

The wolfSSL tls::read() had the same cast, so a read into a buffer over
INT_MAX failed. Cap it as the OpenSSL backend has done since f80864c.

* Drop the 4 GiB write test and shorten comments

The Windows-only test allocated 4 GiB and took about 5 s, which is too
heavy for CI.

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-04 18:06:30 -05:00
metsw24-maxandyhirose 453b1b8613 keep a relative Location on its host in resolve_relative_location (#2654)
* keep a relative Location on its host in resolve_relative_location

* Shorten the comment on on_this_host in resolve_relative_location

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
2026-11-02 13:12:31 -05:00
Yoshiki Matsuda a18895f42e Let ClientImpl::stop() interrupt a request waiting on Windows (#2653)
stop() shuts the socket down to end a request in flight, but on Windows
that does not wake a read waiting in WSAPoll (#1396). The request then
waited until the server answered, the read timeout passed, or the OS
dropped the connection.

stop() now also sets a flag, and send_() gives the request's stream an
interrupt check that reads it, as SSEClient::stop() does since 801bf2e.
The wait for data then ends within 100 ms on every platform. A request
started after stop() clears the flag. A redirect or a digest retry sent
inside the request keeps it.

select_read_interruptible() now rounds its last wait up to a whole
millisecond. poll() takes milliseconds, and the wait ended up to 1 ms
before the deadline, which MaxTimeoutTest caught once every client
request waited through this function.
2026-10-30 13:40:50 -04:00
25 changed files with 1172 additions and 452 deletions

No files matched your search

+2
View File
@@ -1716,6 +1716,8 @@ cli.set_address_family(AF_UNIX);
This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior. This library automatically sets the Host header to "localhost" for Unix socket connections, similar to curl's behavior.
With `set_follow_location(true)`, a redirect is followed over the same socket, whatever host the `Location` names, as curl's `--unix-socket` does. A host mapped with `set_direct_hostname_addr_map` goes to its mapped socket instead. Only a `Location` naming `localhost` counts as the same host; any other host is treated as cross-host, so `Authorization` and `Cookie` are not sent to it.
URI Encoding/Decoding Utilities URI Encoding/Decoding Utilities
------------------------------- -------------------------------
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site] [site]
title = "cpp-httplib" title = "cpp-httplib"
version = "0.64.0" version = "0.66.0"
hostname = "https://yhirose.github.io" hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib" base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved." footer_message = "© 2026 Yuji Hirose. All rights reserved."
+227 -136
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H #ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.64.0" #define CPPHTTPLIB_VERSION "0.66.0"
#define CPPHTTPLIB_VERSION_NUM "0x004000" #define CPPHTTPLIB_VERSION_NUM "0x004200"
#ifdef _WIN32 #ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00 #if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -1029,18 +1029,27 @@ inline std::string resolve_relative_location(const std::string &location,
// A ':' in the first segment means the value has a scheme. // A ':' in the first segment means the value has a scheme.
if (location.find(':') < location.find_first_of("/?#")) { return location; } if (location.find(':') < location.find_first_of("/?#")) { return location; }
if (location[0] == '#') { return base.substr(0, base.find('#')) + location; } // A path starting with "//" would be re-parsed as an authority. Prefix "/."
// (as the URL Standard does); remove_dot_segments strips it later.
auto on_this_host = [](std::string path) -> std::string {
if (path.compare(0, 2, "//") == 0) { path.insert(0, "/."); }
return path;
};
if (location[0] == '#') {
return on_this_host(base.substr(0, base.find('#')) + location);
}
auto base_path = base.substr(0, base.find_first_of("?#")); auto base_path = base.substr(0, base.find_first_of("?#"));
if (location[0] == '?') { return base_path + location; } if (location[0] == '?') { return on_this_host(base_path + location); }
if (base_path.empty() || base_path[0] != '/') { base_path = "/"; } if (base_path.empty() || base_path[0] != '/') { base_path = "/"; }
auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location; auto merged = base_path.substr(0, base_path.rfind('/') + 1) + location;
// Remove "." and ".." segments from the merged path. // Remove "." and ".." segments from the merged path.
auto path_end = (std::min)(merged.find_first_of("?#"), merged.size()); auto path_end = (std::min)(merged.find_first_of("?#"), merged.size());
return remove_dot_segments(merged.substr(0, path_end)) + return on_this_host(remove_dot_segments(merged.substr(0, path_end)) +
merged.substr(path_end); merged.substr(path_end));
} }
} // namespace detail } // namespace detail
@@ -1335,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());
@@ -3058,6 +3067,10 @@ protected:
bool socket_should_be_closed_when_request_is_done_ = false; bool socket_should_be_closed_when_request_is_done_ = false;
size_t socket_tls_settings_version_ = 0; size_t socket_tls_settings_version_ = 0;
// Set by stop() while a request is in flight. The request's reads poll it,
// because shutting the socket down does not wake a read on Windows.
std::atomic<bool> stop_requested_{false};
std::atomic<size_t> tls_settings_version_{0}; std::atomic<size_t> tls_settings_version_{0};
// Hostname to connection target map. The value is an IP literal or another // Hostname to connection target map. The value is an IP literal or another
@@ -4529,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;
@@ -4565,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;
@@ -5010,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
@@ -5091,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;
@@ -5203,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;
@@ -6840,10 +6882,14 @@ select_read_interruptible(socket_t sock, time_t sec, time_t usec,
while (!interrupted()) { while (!interrupted()) {
auto left = std::chrono::duration_cast<std::chrono::microseconds>( auto left = std::chrono::duration_cast<std::chrono::microseconds>(
deadline - std::chrono::steady_clock::now()); deadline - std::chrono::steady_clock::now());
auto wait = auto last = left < slice;
(std::max)((std::min)(left, slice), std::chrono::microseconds(0)); // poll() takes whole milliseconds, so round the last wait up to keep it
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait.count())); // from ending before the deadline.
if (ret != 0 || wait < slice) { return ret; } auto wait_usec = last ? (std::max)((left.count() + 999) / 1000 * 1000,
static_cast<decltype(left.count())>(0))
: slice.count();
auto ret = select_impl(sock, POLLIN, 0, static_cast<time_t>(wait_usec));
if (ret != 0 || last) { return ret; }
} }
return 0; return 0;
} }
@@ -9335,6 +9381,7 @@ inline bool redirect(T &cli, Request &req, Response &res,
Error &error) { Error &error) {
Request new_req = req; Request new_req = req;
new_req.path = path; new_req.path = path;
new_req.params.clear();
new_req.redirect_count_ -= 1; new_req.redirect_count_ -= 1;
if (res.status == StatusCode::SeeOther_303 && if (res.status == StatusCode::SeeOther_303 &&
@@ -12620,7 +12667,16 @@ inline bool ThreadPool::enqueue(std::function<void()> fn) {
if (jobs_.size() > idle_thread_count_ && if (jobs_.size() > idle_thread_count_ &&
threads_.size() + dynamic_threads_.size() < max_thread_count_) { threads_.size() + dynamic_threads_.size() < max_thread_count_) {
cleanup_finished_threads(); cleanup_finished_threads();
dynamic_threads_.emplace_back(std::thread([this]() { worker(true); })); #ifndef CPPHTTPLIB_NO_EXCEPTIONS
try {
#endif
dynamic_threads_.emplace_back(std::thread([this]() { worker(true); }));
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
} catch (const std::system_error &) {
// No thread budget left: keep the job queued for an existing worker
// instead of letting the exception escape the accept loop.
}
#endif
} }
} }
@@ -12859,7 +12915,8 @@ inline ssize_t SocketStream::read(char *ptr, size_t size) {
inline ssize_t SocketStream::write(const char *ptr, size_t size) { inline ssize_t SocketStream::write(const char *ptr, size_t size) {
if (!wait_writable()) { return -1; } if (!wait_writable()) { return -1; }
#if defined(_WIN32) && !defined(_WIN64) // Winsock's send() takes an int length, even on 64-bit Windows
#ifdef _WIN32
size = size =
(std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)())); (std::min)(size, static_cast<size_t>((std::numeric_limits<int>::max)()));
#endif #endif
@@ -13074,14 +13131,22 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
return prepare_host_string(host) + ":" + std::to_string(port); return prepare_host_string(host) + ":" + std::to_string(port);
} }
// Where a client connects for `host`, as one string: the mapped address or
// name, or `host` itself.
inline std::string
addr_map_target(const std::map<std::string, std::string> &addr_map,
const std::string &host) {
std::string connect_host;
std::string ip;
apply_addr_map(addr_map, host, connect_host, ip);
return ip.empty() ? connect_host : ip;
}
// Request target of a CONNECT for `host`, with `addr_map` applied. // Request target of a CONNECT for `host`, with `addr_map` applied.
inline std::string inline std::string
make_connect_request_target(const std::map<std::string, std::string> &addr_map, make_connect_request_target(const std::map<std::string, std::string> &addr_map,
const std::string &host, int port) { const std::string &host, int port) {
std::string connect_host; return make_host_and_port_string_always_port(addr_map_target(addr_map, host),
std::string ip;
apply_addr_map(addr_map, host, connect_host, ip);
return make_host_and_port_string_always_port(ip.empty() ? connect_host : ip,
port); port);
} }
@@ -15274,7 +15339,10 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
if ((req.method == "GET" || req.method == "HEAD") && if ((req.method == "GET" || req.method == "HEAD") &&
req.has_header("Range")) { req.has_header("Range")) {
const auto &range_header_value = req.get_header_value("Range"); const auto &range_header_value = req.get_header_value("Range");
if (!detail::parse_range_header(range_header_value, req.ranges)) { // RFC 9110 14.2: a range unit other than bytes must be ignored
auto unit = range_header_value.substr(0, range_header_value.find('='));
if (detail::case_ignore::equal(unit, "bytes") &&
!detail::parse_range_header(range_header_value, req.ranges)) {
connection_closed = true; connection_closed = true;
res.status = StatusCode::RangeNotSatisfiable_416; res.status = StatusCode::RangeNotSatisfiable_416;
output_error_log(Error::InvalidRangeHeader, &req); output_error_log(Error::InvalidRangeHeader, &req);
@@ -15803,6 +15871,9 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
// socket. // socket.
socket_should_be_closed_when_request_is_done_ = false; socket_should_be_closed_when_request_is_done_ = false;
// A redirect or a retry sent inside a request keeps its stop()
if (socket_requests_in_flight_ == 0) { stop_requested_ = false; }
if (!socket_reusable()) { if (!socket_reusable()) {
if (!ensure_socket_connection(socket_, error)) { return false; } if (!ensure_socket_connection(socket_, error)) { return false; }
@@ -15850,6 +15921,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
}); });
ret = process_socket(socket_, req.start_time_, [&](Stream &strm) { ret = process_socket(socket_, req.start_time_, [&](Stream &strm) {
strm.set_read_interrupt([this] { return stop_requested_.load(); });
return handle_request(strm, req, res, close_connection, error); return handle_request(strm, req, res, close_connection, error);
}); });
@@ -16462,8 +16534,13 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
auto path = std::move(next_path) + next_query; auto path = std::move(next_path) + next_query;
// A Unix socket client sends "Host: localhost", so a Location naming
// localhost names this client too.
auto is_same_host = next_host == host_ ||
(address_family_ == AF_UNIX && next_host == "localhost");
// Same host redirect - use current client // Same host redirect - use current client
if (next_scheme == scheme && next_host == host_ && next_port == port_) { if (next_scheme == scheme && is_same_host && next_port == port_) {
return detail::redirect(*this, req, res, path, location, error); return detail::redirect(*this, req, res, path, location, error);
} }
@@ -16614,6 +16691,14 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
// Copy network and socket settings // Copy network and socket settings
client.addr_map_ = addr_map_; client.addr_map_ = addr_map_;
client.tunnel_addr_map_ = tunnel_addr_map_; client.tunnel_addr_map_ = tunnel_addr_map_;
// A Unix socket client reaches every host through its own socket, as curl's
// --unix-socket does, unless the caller mapped that host elsewhere. The
// Location's host is never taken as a socket path.
if (address_family_ == AF_UNIX) {
auto &target = client.addr_map_[client.host()];
if (target.empty()) { target = detail::addr_map_target(addr_map_, host_); }
}
client.set_address_family(address_family_); client.set_address_family(address_family_);
client.set_tcp_nodelay(tcp_nodelay_); client.set_tcp_nodelay(tcp_nodelay_);
client.set_ipv6_v6only(ipv6_v6only_); client.set_ipv6_v6only(ipv6_v6only_);
@@ -18068,6 +18153,9 @@ inline void ClientImpl::stop() {
// are not thread-safe. // are not thread-safe.
if (socket_requests_in_flight_ > 0) { if (socket_requests_in_flight_ > 0) {
shutdown_socket(socket_); shutdown_socket(socket_);
// shutdown() does not wake a read waiting in WSAPoll on Windows, so the
// request's reads also give up once they see this flag.
stop_requested_ = true;
// Aside from that, we set a flag for the socket to be closed when we're // Aside from that, we set a flag for the socket to be closed when we're
// done. // done.
@@ -23030,6 +23118,9 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
} }
auto wsession = static_cast<impl::WolfSSLSession *>(session); auto wsession = static_cast<impl::WolfSSLSession *>(session);
constexpr auto max_len =
static_cast<size_t>((std::numeric_limits<int>::max)());
if (len > max_len) { len = max_len; }
int ret = wolfSSL_read(wsession->ssl, buf, static_cast<int>(len)); int ret = wolfSSL_read(wsession->ssl, buf, static_cast<int>(len));
if (ret > 0) { if (ret > 0) {
+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
+516 -236
View File
@@ -291,6 +291,76 @@ TEST_F(UnixSocketTest, HostHeaderAutoSet) {
EXPECT_EQ(received_host_header, "localhost"); EXPECT_EQ(received_host_header, "localhost");
} }
TEST_F(UnixSocketTest, RedirectStaysOnSocket) {
// Another socket in the working directory, which a Location can name as
// its host
const auto other_path = "other-" + pathname_.substr(2);
httplib::Server svr;
svr.Get("/redirect", [&](const httplib::Request &, httplib::Response &res) {
// An absolute Location naming the Host this client sends
res.set_redirect("http://localhost" + pattern_);
});
svr.Get("/cross", [&](const httplib::Request &, httplib::Response &res) {
res.set_redirect("http://" + other_path + pattern_);
});
svr.Get("/mapped", [&](const httplib::Request &, httplib::Response &res) {
res.set_redirect("http://mapped.test" + pattern_);
});
svr.Get(pattern_, [&](const httplib::Request &req, httplib::Response &res) {
res.set_header("X-Auth", req.get_header_value("Authorization"));
res.set_content(content_, "text/plain");
});
httplib::Server other;
other.Get(pattern_, [&](const httplib::Request &, httplib::Response &res) {
res.set_content("other", "text/plain");
});
std::thread t{[&] {
ASSERT_TRUE(svr.set_address_family(AF_UNIX).listen(pathname_, 80));
}};
std::thread t2{[&] {
ASSERT_TRUE(other.set_address_family(AF_UNIX).listen(other_path, 80));
}};
auto se = detail::scope_exit([&] {
svr.stop();
other.stop();
t.join();
t2.join();
std::remove(other_path.c_str());
});
svr.wait_until_ready();
other.wait_until_ready();
httplib::Client cli{pathname_};
cli.set_address_family(AF_UNIX);
cli.set_follow_location(true);
cli.set_bearer_token_auth("token");
// Another host: the same socket, without the credentials
auto res = cli.Get("/cross");
ASSERT_TRUE(res) << "error: " << res.error();
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(content_, res->body);
EXPECT_EQ("", res->get_header_value("X-Auth"));
// localhost is this client's own host
res = cli.Get("/redirect");
ASSERT_TRUE(res) << "error: " << res.error();
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(content_, res->body);
EXPECT_EQ("Bearer token", res->get_header_value("X-Auth"));
// A host the caller mapped to a socket goes there
cli.set_direct_hostname_addr_map({{"mapped.test", other_path}});
res = cli.Get("/mapped");
ASSERT_TRUE(res) << "error: " << res.error();
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ("other", res->body);
}
#ifndef _WIN32 #ifndef _WIN32
TEST(SocketStream, wait_writable_UNIX) { TEST(SocketStream, wait_writable_UNIX) {
int fds[2]; int fds[2];
@@ -2322,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");
@@ -2818,6 +2895,35 @@ TEST(RangeTest, IgnoredForPost) {
} }
} }
TEST(RangeTest, IgnoredForUnknownUnit) {
Server svr;
svr.Get("/", [](const Request &, Response &res) {
res.set_content("abcdefg", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
ASSERT_GT(port, 0);
thread t = thread([&] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
t.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
Client cli(HOST, port);
for (auto range : {"items=1-2", "none=x"}) {
auto res = cli.Get("/", Headers{{"Range", range}});
ASSERT_TRUE(res) << range << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << range;
EXPECT_EQ("abcdefg", res->body) << range;
}
auto res = cli.Get("/", Headers{{"Range", "bytes=x"}});
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
}
TEST(GetAddrInfoDanglingRefTest, LongTimeout) { TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
auto host = "unresolvableaddress.local"; auto host = "unresolvableaddress.local";
auto path = std::string{"/"}; auto path = std::string{"/"};
@@ -3445,6 +3551,55 @@ TEST(CancelTest, WithCancelLargePayloadDelete) {
EXPECT_EQ(Error::Canceled, res.error()); EXPECT_EQ(Error::Canceled, res.error());
} }
// One stop() from another thread ends a request that waits for the response
TEST(ClientStopTest, StopEndsWaitForResponse) {
Server svr;
std::atomic<bool> received{false};
std::atomic<bool> done{false};
svr.Get("/wait", [&](const Request & /*req*/, Response &res) {
received.store(true);
while (!done.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
res.set_content("Hello World!", "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
auto thread = std::thread([&]() { svr.listen_after_bind(); });
Client cli(HOST, port);
cli.set_read_timeout(std::chrono::seconds(30));
std::future<Result> request;
auto se = detail::scope_exit([&] {
done.store(true);
svr.stop();
thread.join();
ASSERT_FALSE(svr.is_running());
});
svr.wait_until_ready();
request = std::async(std::launch::async, [&]() { return cli.Get("/wait"); });
for (int i = 0; i < 500 && !received.load(); ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
ASSERT_TRUE(received.load());
// Let the client block in the wait for the response
std::this_thread::sleep_for(std::chrono::milliseconds(100));
auto start = std::chrono::steady_clock::now();
cli.stop();
ASSERT_EQ(std::future_status::ready,
request.wait_for(std::chrono::seconds(5)));
EXPECT_LT(std::chrono::steady_clock::now() - start, std::chrono::seconds(2));
auto res = request.get();
ASSERT_TRUE(!res);
EXPECT_EQ(Error::Read, res.error());
}
static std::string remove_whitespace(const std::string &input) { static std::string remove_whitespace(const std::string &input) {
std::string output; std::string output;
output.reserve(input.size()); output.reserve(input.size());
@@ -4248,6 +4403,62 @@ TEST(RedirectToSamePort, ForwardCookie) {
EXPECT_EQ("session_id=SECRET", captured_cookie); EXPECT_EQ("session_id=SECRET", captured_cookie);
} }
TEST(RedirectTest, DoNotCarryRequestParamsToLocation) {
// The Location alone names the next target, on any origin.
Server target_svr;
std::string cross_target;
target_svr.Get("/landing", [&](const Request &req, Response &res) {
cross_target = req.target;
res.set_content("OK", "text/plain");
});
auto target_port = target_svr.bind_to_any_port(HOST);
auto target_thread = std::thread([&]() { target_svr.listen_after_bind(); });
Server origin_svr;
std::string same_target;
origin_svr.Get("/cross", [&](const Request &, Response &res) {
res.set_redirect(
"http://localhost:" + std::to_string(target_port) + "/landing", 302);
});
origin_svr.Get("/same", [&](const Request &, Response &res) {
res.set_redirect("/landing", 302);
});
origin_svr.Get("/landing", [&](const Request &req, Response &res) {
same_target = req.target;
res.set_content("OK", "text/plain");
});
auto origin_port = origin_svr.bind_to_any_port(HOST);
auto origin_thread = std::thread([&]() { origin_svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
origin_svr.stop();
origin_thread.join();
target_svr.stop();
target_thread.join();
});
target_svr.wait_until_ready();
origin_svr.wait_until_ready();
Client cli("localhost", origin_port);
cli.set_follow_location(true);
Request req;
req.method = "GET";
req.params = {{"api_key", "SECRET"}};
req.path = "/cross";
auto res = cli.send(req);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("/landing", cross_target);
req.path = "/same";
res = cli.send(req);
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("/landing", same_target);
}
TEST(RedirectToDifferentPort, ForwardDefaultHeadersButNotCredentials) { TEST(RedirectToDifferentPort, ForwardDefaultHeadersButNotCredentials) {
std::string authorization; std::string authorization;
std::string trace_id; std::string trace_id;
@@ -20676,6 +20887,13 @@ TEST(RedirectTest, ResolveRelativeLocation) {
detail::resolve_relative_location(c.first, "/b/c/d;p?q")) detail::resolve_relative_location(c.first, "/b/c/d;p?q"))
<< c.first; << c.first;
} }
// A result starting with "//" is still a path, not a network-path reference
EXPECT_EQ("/.//g",
detail::resolve_relative_location("../../..//g", "/b/c/d;p?q"));
EXPECT_EQ("/.//g/x", detail::resolve_relative_location("x", "//g/h"));
EXPECT_EQ("/.//g/h?y", detail::resolve_relative_location("?y", "//g/h"));
EXPECT_EQ("/.//g/h#s", detail::resolve_relative_location("#s", "//g/h"));
} }
TEST(RedirectTest, RemoveDotSegments) { TEST(RedirectTest, RemoveDotSegments) {
@@ -20763,6 +20981,54 @@ TEST(RedirectTest, RelativeLocationWithoutLeadingSlash) {
} }
} }
// "..//host:port/x" resolves to the path "//host:port/x" on the same host
// (RFC 3986 5.2), as curl and the URL Standard have it, not to another host.
TEST(RedirectTest, RelativeLocationStaysOnHost) {
Server other;
std::atomic<int> other_hits{0};
other.Get(".*", [&](const Request & /*req*/, Response &res) {
other_hits++;
res.set_content("other", "text/plain");
});
auto other_port = other.bind_to_any_port("127.0.0.1");
auto other_thread = std::thread([&]() { other.listen_after_bind(); });
Server svr;
svr.Get("/dir/page", [](const Request &req, Response &res) {
res.set_redirect(req.get_param_value("to"));
});
svr.Get(".*", [](const Request &req, Response &res) {
res.set_content(req.path, "text/plain");
});
auto port = svr.bind_to_any_port(HOST);
auto thread = std::thread([&]() { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
thread.join();
other.stop();
other_thread.join();
ASSERT_FALSE(svr.is_running());
ASSERT_FALSE(other.is_running());
});
svr.wait_until_ready();
other.wait_until_ready();
auto authority = "127.0.0.1:" + std::to_string(other_port);
for (const auto &to :
{"..//" + authority + "/x", "../..//" + authority + "/x"}) {
Client cli(HOST, port);
cli.set_follow_location(true);
auto res = cli.Get("/dir/page?to=" + encode_query_component(to));
ASSERT_TRUE(res) << to << ": " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status) << to;
EXPECT_EQ("//" + authority + "/x", res->body) << to;
}
EXPECT_EQ(0, other_hits.load());
}
#ifdef CPPHTTPLIB_SSL_ENABLED #ifdef CPPHTTPLIB_SSL_ENABLED
TEST(RedirectTest, Issue2185_Online) { TEST(RedirectTest, Issue2185_Online) {
SSLClient client("github.com"); SSLClient client("github.com");
@@ -24952,6 +25218,49 @@ TEST(SSLTest, ResponseBodyTerminatedByConnectionClose) {
EXPECT_EQ(StatusCode::OK_200, res->status); EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(expected_body, res->body); EXPECT_EQ(expected_body, res->body);
} }
// Without Content-Length, the full buffer size reaches tls::read().
TEST(SSLTest, ReadIntoBufferOverIntMax) {
if (sizeof(size_t) <= sizeof(int)) {
GTEST_SKIP() << "size_t cannot exceed INT_MAX here";
}
SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
svr.Get("/no-content-length", [&](const Request & /*req*/, Response &res) {
res.set_content_provider("text/plain",
[&](size_t offset, DataSink &sink) -> bool {
if (offset == 0) { sink.write("Hello", 5); }
sink.done();
return true;
});
});
auto port = svr.bind_to_any_port(HOST);
auto listen_thread = std::thread([&svr] { svr.listen_after_bind(); });
auto se = detail::scope_exit([&] {
svr.stop();
listen_thread.join();
});
svr.wait_until_ready();
SSLClient cli(HOST, port);
cli.enable_server_certificate_verification(false);
auto handle = cli.open_stream("GET", "/no-content-length");
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
const auto len = static_cast<size_t>((std::numeric_limits<int>::max)()) + 1;
std::unique_ptr<char[]> buf(new char[len]);
std::string body;
ssize_t n;
while ((n = handle.read(buf.get(), len)) > 0) {
body.append(buf.get(), static_cast<size_t>(n));
}
EXPECT_EQ(0, n);
EXPECT_EQ("Hello", body);
}
#endif // CPPHTTPLIB_SSL_ENABLED #endif // CPPHTTPLIB_SSL_ENABLED
//============================================================================== //==============================================================================
@@ -26235,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) {
@@ -27117,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;