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.
|
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
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|||||||
@@ -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."
|
||||||
|
|||||||
@@ -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
@@ -13,14 +13,15 @@ ZLIB_SUPPORT = -DCPPHTTPLIB_ZLIB_SUPPORT -lz
|
|||||||
BROTLI_DIR = /usr/local/opt/brotli
|
BROTLI_DIR = /usr/local/opt/brotli
|
||||||
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
# BROTLI_SUPPORT = -DCPPHTTPLIB_BROTLI_SUPPORT -I$(BROTLI_DIR)/include -L$(BROTLI_DIR)/lib -lbrotlicommon -lbrotlienc -lbrotlidec
|
||||||
|
|
||||||
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer
|
FUZZERS = server_fuzzer url_parser_fuzzer header_parser_fuzzer client_fuzzer multipart_parser_fuzzer websocket_fuzzer sse_parser_fuzzer server_mount_fuzzer
|
||||||
|
|
||||||
# Runs all the tests and also fuzz tests against seed corpus.
|
# Runs all the tests and also fuzz tests against seed corpus.
|
||||||
all : $(FUZZERS)
|
all : $(FUZZERS)
|
||||||
./server_fuzzer corpus/*
|
./server_fuzzer corpus/*
|
||||||
|
./server_mount_fuzzer corpus_mount/*
|
||||||
|
|
||||||
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
# Fuzz target, so that you can choose which $(LIB_FUZZING_ENGINE) to use.
|
||||||
server_fuzzer : server_fuzzer.cc ../../httplib.h
|
server_fuzzer : server_fuzzer.cc fuzzed_stream.h ../../httplib.h
|
||||||
# $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
# $(CXX) $(CXXFLAGS) -o $@ $< -Wl,-Bstatic $(OPENSSL_SUPPORT) -Wl,-Bdynamic -ldl $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
zip -q -r server_fuzzer_seed_corpus.zip corpus
|
||||||
@@ -38,5 +39,15 @@ url_parser_fuzzer : url_parser_fuzzer.cc ../../httplib.h
|
|||||||
multipart_parser_fuzzer : multipart_parser_fuzzer.cc ../../httplib.h
|
multipart_parser_fuzzer : multipart_parser_fuzzer.cc ../../httplib.h
|
||||||
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
|
|
||||||
|
websocket_fuzzer : websocket_fuzzer.cc ../../httplib.h
|
||||||
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
|
|
||||||
|
sse_parser_fuzzer : sse_parser_fuzzer.cc ../../httplib.h
|
||||||
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
|
|
||||||
|
server_mount_fuzzer : server_mount_fuzzer.cc fuzzed_stream.h ../../httplib.h
|
||||||
|
$(CXX) $(CXXFLAGS) -o $@ $< $(ZLIB_SUPPORT) $(LIB_FUZZING_ENGINE) -pthread -lanl
|
||||||
|
zip -q -r server_mount_fuzzer_seed_corpus.zip corpus_mount
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
rm -f server_fuzzer pem *.0 *.o *.1 *.srl *.zip
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
HEAD /mnt/a.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
GET /mnt/a.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/index.html HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
GET /mnt/index.html HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
If-None-Match: W/"bogusetag"
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/link-in.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/link-out.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/link-sibling.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/missing.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
GET /mnt/a.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
Range: bytes=0-3
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/sub/ HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/../../../../etc/passwd HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
GET /mnt/%2e%2e/secret.txt HTTP/1.1
|
||||||
|
Host: x
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
#include <httplib.h>
|
||||||
|
|
||||||
|
class FuzzedStream : public httplib::Stream {
|
||||||
|
public:
|
||||||
|
FuzzedStream(const uint8_t *data, size_t size)
|
||||||
|
: data_(data), size_(size), read_pos_(0) {}
|
||||||
|
|
||||||
|
ssize_t read(char *ptr, size_t size) override {
|
||||||
|
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
|
||||||
|
memcpy(ptr, data_ + read_pos_, size);
|
||||||
|
read_pos_ += size;
|
||||||
|
// Like SocketStream, pull in more than was asked for, which the line
|
||||||
|
// reader then takes through buffered_data(), so both of its paths run.
|
||||||
|
buffered_end_ =
|
||||||
|
(std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096));
|
||||||
|
return static_cast<ssize_t>(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *buffered_data(size_t &size) const override {
|
||||||
|
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
|
||||||
|
return size ? reinterpret_cast<const char *>(data_ + read_pos_) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void consume_buffered(size_t size) override { read_pos_ += size; }
|
||||||
|
|
||||||
|
ssize_t write(const char *ptr, size_t size) override {
|
||||||
|
response_.append(ptr, size);
|
||||||
|
return static_cast<int>(size);
|
||||||
|
}
|
||||||
|
|
||||||
|
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
|
||||||
|
|
||||||
|
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
|
||||||
|
|
||||||
|
bool is_readable() const override { return true; }
|
||||||
|
|
||||||
|
bool wait_readable() const override { return true; }
|
||||||
|
|
||||||
|
bool wait_writable() const override { return true; }
|
||||||
|
|
||||||
|
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||||
|
ip = "127.0.0.1";
|
||||||
|
port = 8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
||||||
|
ip = "127.0.0.1";
|
||||||
|
port = 8080;
|
||||||
|
}
|
||||||
|
|
||||||
|
socket_t socket() const override { return 0; }
|
||||||
|
|
||||||
|
time_t duration() const override { return 0; };
|
||||||
|
|
||||||
|
const std::string &response() const { return response_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
const uint8_t *data_;
|
||||||
|
size_t size_;
|
||||||
|
size_t read_pos_;
|
||||||
|
size_t buffered_end_ = 0;
|
||||||
|
std::string response_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FuzzableServer : public httplib::Server {
|
||||||
|
public:
|
||||||
|
// A server that is not listening counts as shutting down, and then writes
|
||||||
|
// no file. The socket is never used.
|
||||||
|
void MarkAsRunning() { svr_sock_ = 0; }
|
||||||
|
|
||||||
|
void ProcessFuzzedRequest(FuzzedStream &stream) {
|
||||||
|
bool connection_close = false;
|
||||||
|
process_request(stream,
|
||||||
|
/*remote_addr=*/"",
|
||||||
|
/*remote_port =*/0,
|
||||||
|
/*local_addr=*/"",
|
||||||
|
/*local_port =*/0,
|
||||||
|
/*last_connection=*/false, connection_close, nullptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,79 +1,4 @@
|
|||||||
#include <cstdint>
|
#include "fuzzed_stream.h"
|
||||||
|
|
||||||
#include <httplib.h>
|
|
||||||
|
|
||||||
class FuzzedStream : public httplib::Stream {
|
|
||||||
public:
|
|
||||||
FuzzedStream(const uint8_t *data, size_t size)
|
|
||||||
: data_(data), size_(size), read_pos_(0) {}
|
|
||||||
|
|
||||||
ssize_t read(char *ptr, size_t size) override {
|
|
||||||
if (size + read_pos_ > size_) { size = size_ - read_pos_; }
|
|
||||||
memcpy(ptr, data_ + read_pos_, size);
|
|
||||||
read_pos_ += size;
|
|
||||||
// Like SocketStream, pull in more than was asked for, which the line
|
|
||||||
// reader then takes through buffered_data(), so both of its paths run.
|
|
||||||
buffered_end_ =
|
|
||||||
(std::max)(buffered_end_, (std::min)(size_, read_pos_ + 4096));
|
|
||||||
return static_cast<ssize_t>(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *buffered_data(size_t &size) const override {
|
|
||||||
size = buffered_end_ > read_pos_ ? buffered_end_ - read_pos_ : 0;
|
|
||||||
return size ? reinterpret_cast<const char *>(data_ + read_pos_) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void consume_buffered(size_t size) override { read_pos_ += size; }
|
|
||||||
|
|
||||||
ssize_t write(const char *ptr, size_t size) override {
|
|
||||||
response_.append(ptr, size);
|
|
||||||
return static_cast<int>(size);
|
|
||||||
}
|
|
||||||
|
|
||||||
ssize_t write(const char *ptr) { return write(ptr, strlen(ptr)); }
|
|
||||||
|
|
||||||
ssize_t write(const std::string &s) { return write(s.data(), s.size()); }
|
|
||||||
|
|
||||||
bool is_readable() const override { return true; }
|
|
||||||
|
|
||||||
bool wait_readable() const override { return true; }
|
|
||||||
|
|
||||||
bool wait_writable() const override { return true; }
|
|
||||||
|
|
||||||
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
|
||||||
ip = "127.0.0.1";
|
|
||||||
port = 8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
|
||||||
ip = "127.0.0.1";
|
|
||||||
port = 8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket_t socket() const override { return 0; }
|
|
||||||
|
|
||||||
time_t duration() const override { return 0; };
|
|
||||||
|
|
||||||
private:
|
|
||||||
const uint8_t *data_;
|
|
||||||
size_t size_;
|
|
||||||
size_t read_pos_;
|
|
||||||
size_t buffered_end_ = 0;
|
|
||||||
std::string response_;
|
|
||||||
};
|
|
||||||
|
|
||||||
class FuzzableServer : public httplib::Server {
|
|
||||||
public:
|
|
||||||
void ProcessFuzzedRequest(FuzzedStream &stream) {
|
|
||||||
bool connection_close = false;
|
|
||||||
process_request(stream,
|
|
||||||
/*remote_addr=*/"",
|
|
||||||
/*remote_port =*/0,
|
|
||||||
/*local_addr=*/"",
|
|
||||||
/*local_port =*/0,
|
|
||||||
/*last_connection=*/false, connection_close, nullptr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static FuzzableServer g_server;
|
static FuzzableServer g_server;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <fstream>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <utime.h>
|
||||||
|
|
||||||
|
#include "fuzzed_stream.h"
|
||||||
|
|
||||||
|
// Fixture layout, with <token> unknown to the fuzzer:
|
||||||
|
// root/mnt/{index.html,a.txt,data.bin,sub/index.html} served at /mnt
|
||||||
|
// root/mnt/link-in.txt -> a.txt (stays inside, may be served)
|
||||||
|
// root/mnt/link-out.txt -> ../outside.txt (escapes, must be refused)
|
||||||
|
// root/mnt/link-sibling.txt -> ../mnt_secret/secret.txt (shares the "mnt"
|
||||||
|
// prefix, must be refused)
|
||||||
|
// The response must never contain <token>.
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
FuzzableServer g_server;
|
||||||
|
std::string g_root;
|
||||||
|
std::string g_token;
|
||||||
|
std::vector<std::string> g_paths; // In creation order
|
||||||
|
|
||||||
|
void check(bool ok, const char *what) {
|
||||||
|
if (!ok) {
|
||||||
|
fprintf(stderr, "fixture setup failed: %s\n", what);
|
||||||
|
abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_dir(const std::string &path) {
|
||||||
|
check(mkdir(path.c_str(), 0755) == 0, path.c_str());
|
||||||
|
g_paths.push_back(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_file(const std::string &path, const std::string &content) {
|
||||||
|
{
|
||||||
|
std::ofstream out(path, std::ios::binary);
|
||||||
|
out << content;
|
||||||
|
check(out.good(), path.c_str());
|
||||||
|
}
|
||||||
|
// Fixed, so that conditional requests behave the same in every run
|
||||||
|
utimbuf times = {1700000000, 1700000000};
|
||||||
|
check(utime(path.c_str(), ×) == 0, path.c_str());
|
||||||
|
g_paths.push_back(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void make_link(const std::string &target, const std::string &path) {
|
||||||
|
check(symlink(target.c_str(), path.c_str()) == 0, path.c_str());
|
||||||
|
g_paths.push_back(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string serve(const uint8_t *data, size_t size) {
|
||||||
|
FuzzedStream stream{data, size};
|
||||||
|
g_server.ProcessFuzzedRequest(stream);
|
||||||
|
return stream.response();
|
||||||
|
}
|
||||||
|
|
||||||
|
void check_served(const char *path) {
|
||||||
|
auto req = std::string("GET ") + path + " HTTP/1.1\r\nHost: x\r\n\r\n";
|
||||||
|
auto res = serve(reinterpret_cast<const uint8_t *>(req.data()), req.size());
|
||||||
|
// Without this, a fuzzer that serves nothing would pass its check forever
|
||||||
|
check(res.find("abcdef") != std::string::npos, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove_fixture() {
|
||||||
|
for (auto it = g_paths.rbegin(); it != g_paths.rend(); ++it) {
|
||||||
|
if (unlink(it->c_str()) != 0) { rmdir(it->c_str()); }
|
||||||
|
}
|
||||||
|
rmdir(g_root.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" int LLVMFuzzerInitialize(int * /*argc*/, char *** /*argv*/) {
|
||||||
|
std::random_device rd;
|
||||||
|
char token[33];
|
||||||
|
snprintf(token, sizeof(token), "%08x%08x%08x%08x", rd(), rd(), rd(), rd());
|
||||||
|
g_token = token;
|
||||||
|
|
||||||
|
auto tmpdir = getenv("TMPDIR");
|
||||||
|
auto tmpl = std::string(tmpdir && *tmpdir ? tmpdir : "/tmp") +
|
||||||
|
"/httplib-mount-XXXXXX";
|
||||||
|
check(mkdtemp(&tmpl[0]) != nullptr, tmpl.c_str());
|
||||||
|
g_root = tmpl;
|
||||||
|
atexit(remove_fixture);
|
||||||
|
|
||||||
|
auto mnt = g_root + "/mnt";
|
||||||
|
make_dir(mnt);
|
||||||
|
make_dir(mnt + "/sub");
|
||||||
|
make_dir(g_root + "/mnt_secret");
|
||||||
|
make_file(mnt + "/index.html", "<html>index</html>");
|
||||||
|
make_file(mnt + "/a.txt", "abcdef");
|
||||||
|
make_file(mnt + "/data.bin", std::string("\x01\x02\x03\x04\x05\x06", 6));
|
||||||
|
make_file(mnt + "/sub/index.html", "<html>sub</html>");
|
||||||
|
make_file(g_root + "/outside.txt", g_token);
|
||||||
|
make_file(g_root + "/mnt_secret/secret.txt", g_token);
|
||||||
|
make_link("a.txt", mnt + "/link-in.txt");
|
||||||
|
make_link("../outside.txt", mnt + "/link-out.txt");
|
||||||
|
make_link("../mnt_secret/secret.txt", mnt + "/link-sibling.txt");
|
||||||
|
|
||||||
|
g_server.MarkAsRunning();
|
||||||
|
check(g_server.set_mount_point("/mnt", mnt), "set_mount_point");
|
||||||
|
check_served("/mnt/a.txt");
|
||||||
|
check_served("/mnt/link-in.txt");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||||
|
if (serve(data, size).find(g_token) != std::string::npos) { abort(); }
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
"GET"
|
||||||
|
"HEAD"
|
||||||
|
"HTTP/1.1"
|
||||||
|
|
||||||
|
"/mnt/"
|
||||||
|
"/mnt/index.html"
|
||||||
|
"/mnt/a.txt"
|
||||||
|
"/mnt/data.bin"
|
||||||
|
"/mnt/sub/"
|
||||||
|
"/mnt/link-in.txt"
|
||||||
|
"/mnt/link-out.txt"
|
||||||
|
"/mnt/link-sibling.txt"
|
||||||
|
".."
|
||||||
|
"%2e%2e"
|
||||||
|
"%2f"
|
||||||
|
"//"
|
||||||
|
|
||||||
|
"Range: bytes=0-"
|
||||||
|
"Range: bytes=0-3,5-"
|
||||||
|
"If-None-Match: "
|
||||||
|
"W/"
|
||||||
|
"If-Modified-Since: "
|
||||||
|
"If-Range: "
|
||||||
|
"Accept-Encoding: gzip"
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <httplib.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Feeds the stream in chunks and records every handler call in order
|
||||||
|
std::string parse_in_chunks(const std::string &stream, size_t chunk,
|
||||||
|
size_t max_event_length) {
|
||||||
|
std::string trace;
|
||||||
|
httplib::detail::SSEParser parser(
|
||||||
|
[&](const httplib::sse::SSEMessage &msg) {
|
||||||
|
trace += "event[" + msg.event + "|" + msg.data + "|" + msg.id + "]";
|
||||||
|
},
|
||||||
|
[&](const std::string &id) { trace += "id[" + id + "]"; },
|
||||||
|
[&](int retry_ms) { trace += "retry[" + std::to_string(retry_ms) + "]"; },
|
||||||
|
max_event_length);
|
||||||
|
|
||||||
|
for (size_t off = 0; off < stream.size(); off += chunk) {
|
||||||
|
if (!parser.parse(stream.data() + off,
|
||||||
|
std::min(chunk, stream.size() - off))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||||
|
if (size < 2 || size > 65536) { return 0; }
|
||||||
|
|
||||||
|
// First byte selects the chunk size, the rest is the event stream
|
||||||
|
auto chunk = static_cast<size_t>(data[0]) + 1;
|
||||||
|
std::string stream(reinterpret_cast<const char *>(data + 1), size - 1);
|
||||||
|
|
||||||
|
// Without a length limit, chunking must not change what is reported
|
||||||
|
auto whole = parse_in_chunks(stream, stream.size(), 0);
|
||||||
|
auto chunked = parse_in_chunks(stream, chunk, 0);
|
||||||
|
if (whole != chunked) { std::abort(); }
|
||||||
|
|
||||||
|
// With a limit, only check that the parser stops cleanly
|
||||||
|
parse_in_chunks(stream, chunk, 64);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
|||||||
size_t payload_size = size - 1;
|
size_t payload_size = size - 1;
|
||||||
std::string input(payload, payload_size);
|
std::string input(payload, payload_size);
|
||||||
|
|
||||||
switch (selector % 6) {
|
switch (selector % 13) {
|
||||||
case 0: {
|
case 0: {
|
||||||
// parse_query_text
|
// parse_query_text
|
||||||
httplib::Params params;
|
httplib::Params params;
|
||||||
@@ -46,6 +46,48 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
|||||||
httplib::detail::is_valid_path(input);
|
httplib::detail::is_valid_path(input);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 6: {
|
||||||
|
// parse_url
|
||||||
|
httplib::detail::UrlComponents uc;
|
||||||
|
httplib::detail::parse_url(input, uc);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 7: {
|
||||||
|
// remove_dot_segments
|
||||||
|
httplib::detail::remove_dot_segments(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 8: {
|
||||||
|
// resolve_relative_location, with the input as "<location>\n<base>"
|
||||||
|
auto nl = input.find('\n');
|
||||||
|
auto location = input.substr(0, nl);
|
||||||
|
auto base = nl == std::string::npos ? std::string("/index.html")
|
||||||
|
: input.substr(nl + 1);
|
||||||
|
httplib::detail::resolve_relative_location(location, base);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 9: {
|
||||||
|
// encode_path
|
||||||
|
httplib::detail::encode_path(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 10: {
|
||||||
|
// encode_uri_component, encode_uri
|
||||||
|
httplib::encode_uri_component(input);
|
||||||
|
httplib::encode_uri(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 11: {
|
||||||
|
// decode_uri_component, decode_uri
|
||||||
|
httplib::decode_uri_component(input);
|
||||||
|
httplib::decode_uri(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 12: {
|
||||||
|
// sanitize_filename
|
||||||
|
httplib::sanitize_filename(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <httplib.h>
|
||||||
|
|
||||||
|
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
|
||||||
|
if (size < 2) { return 0; }
|
||||||
|
|
||||||
|
auto selector = data[0];
|
||||||
|
std::string input(reinterpret_cast<const char *>(data + 1), size - 1);
|
||||||
|
|
||||||
|
switch (selector % 4) {
|
||||||
|
case 0: {
|
||||||
|
// read_websocket_frame. WebSocket::read passes the macro, close() passes
|
||||||
|
// 125, and 0 means no limit
|
||||||
|
static const size_t max_lens[] = {0, 125,
|
||||||
|
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH};
|
||||||
|
auto expect_masked = (selector / 4) % 2 != 0;
|
||||||
|
auto max_len = max_lens[(selector / 8) % 3];
|
||||||
|
httplib::detail::BufferStream strm;
|
||||||
|
strm.write(input.data(), input.size());
|
||||||
|
auto opcode = httplib::ws::Opcode::Text;
|
||||||
|
std::string payload;
|
||||||
|
auto fin = false;
|
||||||
|
httplib::ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
||||||
|
expect_masked, max_len);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 1: {
|
||||||
|
// is_valid_utf8
|
||||||
|
httplib::ws::impl::is_valid_utf8(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
// is_valid_close_payload
|
||||||
|
httplib::ws::impl::is_valid_close_payload(input);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
// read_websocket_upgrade_response, with a fixed key since
|
||||||
|
// perform_websocket_handshake draws a random one
|
||||||
|
static const auto expected_accept =
|
||||||
|
httplib::detail::websocket_accept_key("dGhlIHNhbXBsZSBub25jZQ==");
|
||||||
|
httplib::detail::BufferStream strm;
|
||||||
|
strm.write(input.data(), input.size());
|
||||||
|
httplib::detail::WebSocketUpgradeResponse upgrade;
|
||||||
|
httplib::detail::read_websocket_upgrade_response(strm, expected_accept,
|
||||||
|
"chat, stream", upgrade);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
[libfuzzer]
|
||||||
|
max_len = 70000
|
||||||
+516
-236
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user