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