mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0adbb4b20 | |||
| f80864ca03 | |||
| 4e75a84b39 | |||
| 77d945d3b6 | |||
| f69737a838 | |||
| a188913b02 | |||
| 02e4c53685 | |||
| 8c4370247a | |||
| 1f1a799d13 | |||
| a875292153 | |||
| f0b7d4161d |
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.31.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x001F00"
|
||||
#define CPPHTTPLIB_VERSION "0.32.0"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002000"
|
||||
|
||||
/*
|
||||
* Platform compatibility check
|
||||
@@ -1956,6 +1956,7 @@ protected:
|
||||
bool decompress_ = true;
|
||||
|
||||
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
||||
bool has_payload_max_length_ = false;
|
||||
|
||||
std::string interface_;
|
||||
|
||||
@@ -2975,98 +2976,30 @@ namespace stream {
|
||||
|
||||
class Result {
|
||||
public:
|
||||
Result() : chunk_size_(8192) {}
|
||||
|
||||
explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192)
|
||||
: handle_(std::move(handle)), chunk_size_(chunk_size) {}
|
||||
|
||||
Result(Result &&other) noexcept
|
||||
: handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
|
||||
current_size_(other.current_size_), chunk_size_(other.chunk_size_),
|
||||
finished_(other.finished_) {
|
||||
other.current_size_ = 0;
|
||||
other.finished_ = true;
|
||||
}
|
||||
|
||||
Result &operator=(Result &&other) noexcept {
|
||||
if (this != &other) {
|
||||
handle_ = std::move(other.handle_);
|
||||
buffer_ = std::move(other.buffer_);
|
||||
current_size_ = other.current_size_;
|
||||
chunk_size_ = other.chunk_size_;
|
||||
finished_ = other.finished_;
|
||||
other.current_size_ = 0;
|
||||
other.finished_ = true;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
Result();
|
||||
explicit Result(ClientImpl::StreamHandle &&handle, size_t chunk_size = 8192);
|
||||
Result(Result &&other) noexcept;
|
||||
Result &operator=(Result &&other) noexcept;
|
||||
Result(const Result &) = delete;
|
||||
Result &operator=(const Result &) = delete;
|
||||
|
||||
// Check if the result is valid (connection succeeded and response received)
|
||||
bool is_valid() const { return handle_.is_valid(); }
|
||||
explicit operator bool() const { return is_valid(); }
|
||||
|
||||
// Response status code
|
||||
int status() const {
|
||||
return handle_.response ? handle_.response->status : -1;
|
||||
}
|
||||
|
||||
// Response headers
|
||||
const Headers &headers() const {
|
||||
static const Headers empty_headers;
|
||||
return handle_.response ? handle_.response->headers : empty_headers;
|
||||
}
|
||||
|
||||
// Response info
|
||||
bool is_valid() const;
|
||||
explicit operator bool() const;
|
||||
int status() const;
|
||||
const Headers &headers() const;
|
||||
std::string get_header_value(const std::string &key,
|
||||
const char *def = "") const {
|
||||
return handle_.response ? handle_.response->get_header_value(key, def)
|
||||
: def;
|
||||
}
|
||||
const char *def = "") const;
|
||||
bool has_header(const std::string &key) const;
|
||||
Error error() const;
|
||||
Error read_error() const;
|
||||
bool has_read_error() const;
|
||||
|
||||
bool has_header(const std::string &key) const {
|
||||
return handle_.response ? handle_.response->has_header(key) : false;
|
||||
}
|
||||
|
||||
// Error information
|
||||
Error error() const { return handle_.error; }
|
||||
Error read_error() const { return handle_.get_read_error(); }
|
||||
bool has_read_error() const { return handle_.has_read_error(); }
|
||||
|
||||
// Streaming iteration API
|
||||
// Call next() to read the next chunk, then access data via data()/size()
|
||||
// Returns true if data was read, false when stream is exhausted
|
||||
bool next() {
|
||||
if (!handle_.is_valid() || finished_) { return false; }
|
||||
|
||||
if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
|
||||
|
||||
ssize_t n = handle_.read(&buffer_[0], chunk_size_);
|
||||
if (n > 0) {
|
||||
current_size_ = static_cast<size_t>(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
current_size_ = 0;
|
||||
finished_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pointer to current chunk data (valid after next() returns true)
|
||||
const char *data() const { return buffer_.data(); }
|
||||
|
||||
// Size of current chunk (valid after next() returns true)
|
||||
size_t size() const { return current_size_; }
|
||||
|
||||
// Convenience method: read all remaining data into a string
|
||||
std::string read_all() {
|
||||
std::string result;
|
||||
while (next()) {
|
||||
result.append(data(), size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
// Stream reading
|
||||
bool next();
|
||||
const char *data() const;
|
||||
size_t size() const;
|
||||
std::string read_all();
|
||||
|
||||
private:
|
||||
ClientImpl::StreamHandle handle_;
|
||||
@@ -3331,13 +3264,8 @@ struct SSEMessage {
|
||||
std::string data; // Event payload
|
||||
std::string id; // Event ID for Last-Event-ID header
|
||||
|
||||
SSEMessage() : event("message") {}
|
||||
|
||||
void clear() {
|
||||
event = "message";
|
||||
data.clear();
|
||||
id.clear();
|
||||
}
|
||||
SSEMessage();
|
||||
void clear();
|
||||
};
|
||||
|
||||
class SSEClient {
|
||||
@@ -3346,255 +3274,40 @@ public:
|
||||
using ErrorHandler = std::function<void(Error)>;
|
||||
using OpenHandler = std::function<void()>;
|
||||
|
||||
SSEClient(Client &client, const std::string &path)
|
||||
: client_(client), path_(path) {}
|
||||
|
||||
SSEClient(Client &client, const std::string &path, const Headers &headers)
|
||||
: client_(client), path_(path), headers_(headers) {}
|
||||
|
||||
~SSEClient() { stop(); }
|
||||
SSEClient(Client &client, const std::string &path);
|
||||
SSEClient(Client &client, const std::string &path, const Headers &headers);
|
||||
~SSEClient();
|
||||
|
||||
SSEClient(const SSEClient &) = delete;
|
||||
SSEClient &operator=(const SSEClient &) = delete;
|
||||
|
||||
// Event handlers
|
||||
SSEClient &on_message(MessageHandler handler) {
|
||||
on_message_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SSEClient &on_event(const std::string &type, MessageHandler handler) {
|
||||
event_handlers_[type] = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SSEClient &on_open(OpenHandler handler) {
|
||||
on_open_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SSEClient &on_error(ErrorHandler handler) {
|
||||
on_error_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SSEClient &set_reconnect_interval(int ms) {
|
||||
reconnect_interval_ms_ = ms;
|
||||
return *this;
|
||||
}
|
||||
|
||||
SSEClient &set_max_reconnect_attempts(int n) {
|
||||
max_reconnect_attempts_ = n;
|
||||
return *this;
|
||||
}
|
||||
SSEClient &on_message(MessageHandler handler);
|
||||
SSEClient &on_event(const std::string &type, MessageHandler handler);
|
||||
SSEClient &on_open(OpenHandler handler);
|
||||
SSEClient &on_error(ErrorHandler handler);
|
||||
SSEClient &set_reconnect_interval(int ms);
|
||||
SSEClient &set_max_reconnect_attempts(int n);
|
||||
|
||||
// State accessors
|
||||
bool is_connected() const { return connected_.load(); }
|
||||
const std::string &last_event_id() const { return last_event_id_; }
|
||||
bool is_connected() const;
|
||||
const std::string &last_event_id() const;
|
||||
|
||||
// Blocking start - runs event loop with auto-reconnect
|
||||
void start() {
|
||||
running_.store(true);
|
||||
run_event_loop();
|
||||
}
|
||||
void start();
|
||||
|
||||
// Non-blocking start - runs in background thread
|
||||
void start_async() {
|
||||
running_.store(true);
|
||||
async_thread_ = std::thread([this]() { run_event_loop(); });
|
||||
}
|
||||
void start_async();
|
||||
|
||||
// Stop the client (thread-safe)
|
||||
void stop() {
|
||||
running_.store(false);
|
||||
client_.stop(); // Cancel any pending operations
|
||||
if (async_thread_.joinable()) { async_thread_.join(); }
|
||||
}
|
||||
void stop();
|
||||
|
||||
private:
|
||||
// Parse a single SSE field line
|
||||
// Returns true if this line ends an event (blank line)
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty() || line == "\r") { return true; }
|
||||
|
||||
// Lines starting with ':' are comments (ignored)
|
||||
if (!line.empty() && line[0] == ':') { return false; }
|
||||
|
||||
// Find the colon separator
|
||||
auto colon_pos = line.find(':');
|
||||
if (colon_pos == std::string::npos) {
|
||||
// Line with no colon is treated as field name with empty value
|
||||
return false;
|
||||
}
|
||||
|
||||
auto field = line.substr(0, colon_pos);
|
||||
std::string value;
|
||||
|
||||
// Value starts after colon, skip optional single space
|
||||
if (colon_pos + 1 < line.size()) {
|
||||
auto value_start = colon_pos + 1;
|
||||
if (line[value_start] == ' ') { value_start++; }
|
||||
value = line.substr(value_start);
|
||||
// Remove trailing \r if present
|
||||
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
|
||||
}
|
||||
|
||||
// Handle known fields
|
||||
if (field == "event") {
|
||||
msg.event = value;
|
||||
} else if (field == "data") {
|
||||
// Multiple data lines are concatenated with newlines
|
||||
if (!msg.data.empty()) { msg.data += "\n"; }
|
||||
msg.data += value;
|
||||
} else if (field == "id") {
|
||||
// Empty id is valid (clears the last event ID)
|
||||
msg.id = value;
|
||||
} else if (field == "retry") {
|
||||
// Parse retry interval in milliseconds
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Main event loop with auto-reconnect
|
||||
void run_event_loop() {
|
||||
auto reconnect_count = 0;
|
||||
|
||||
while (running_.load()) {
|
||||
// Build headers, including Last-Event-ID if we have one
|
||||
auto request_headers = headers_;
|
||||
if (!last_event_id_.empty()) {
|
||||
request_headers.emplace("Last-Event-ID", last_event_id_);
|
||||
}
|
||||
|
||||
// Open streaming connection
|
||||
auto result = stream::Get(client_, path_, request_headers);
|
||||
|
||||
// Connection error handling
|
||||
if (!result) {
|
||||
connected_.store(false);
|
||||
if (on_error_) { on_error_(result.error()); }
|
||||
|
||||
if (!should_reconnect(reconnect_count)) { break; }
|
||||
wait_for_reconnect();
|
||||
reconnect_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.status() != 200) {
|
||||
connected_.store(false);
|
||||
// For certain errors, don't reconnect
|
||||
if (result.status() == 204 || // No Content - server wants us to stop
|
||||
result.status() == 404 || // Not Found
|
||||
result.status() == 401 || // Unauthorized
|
||||
result.status() == 403) { // Forbidden
|
||||
if (on_error_) { on_error_(Error::Connection); }
|
||||
break;
|
||||
}
|
||||
|
||||
if (on_error_) { on_error_(Error::Connection); }
|
||||
|
||||
if (!should_reconnect(reconnect_count)) { break; }
|
||||
wait_for_reconnect();
|
||||
reconnect_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Connection successful
|
||||
connected_.store(true);
|
||||
reconnect_count = 0;
|
||||
if (on_open_) { on_open_(); }
|
||||
|
||||
// Event receiving loop
|
||||
std::string buffer;
|
||||
SSEMessage current_msg;
|
||||
|
||||
while (running_.load() && result.next()) {
|
||||
buffer.append(result.data(), result.size());
|
||||
|
||||
// Process complete lines in the buffer
|
||||
size_t line_start = 0;
|
||||
size_t newline_pos;
|
||||
|
||||
while ((newline_pos = buffer.find('\n', line_start)) !=
|
||||
std::string::npos) {
|
||||
auto line = buffer.substr(line_start, newline_pos - line_start);
|
||||
line_start = newline_pos + 1;
|
||||
|
||||
// Parse the line and check if event is complete
|
||||
auto event_complete =
|
||||
parse_sse_line(line, current_msg, reconnect_interval_ms_);
|
||||
|
||||
if (event_complete && !current_msg.data.empty()) {
|
||||
// Update last_event_id for reconnection
|
||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
||||
|
||||
// Dispatch event to appropriate handler
|
||||
dispatch_event(current_msg);
|
||||
|
||||
current_msg.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep unprocessed data in buffer
|
||||
buffer.erase(0, line_start);
|
||||
}
|
||||
|
||||
// Connection ended
|
||||
connected_.store(false);
|
||||
|
||||
if (!running_.load()) { break; }
|
||||
|
||||
// Check for read errors
|
||||
if (result.has_read_error()) {
|
||||
if (on_error_) { on_error_(result.read_error()); }
|
||||
}
|
||||
|
||||
if (!should_reconnect(reconnect_count)) { break; }
|
||||
wait_for_reconnect();
|
||||
reconnect_count++;
|
||||
}
|
||||
|
||||
connected_.store(false);
|
||||
}
|
||||
|
||||
// Dispatch event to appropriate handler
|
||||
void dispatch_event(const SSEMessage &msg) {
|
||||
// Check for specific event type handler first
|
||||
auto it = event_handlers_.find(msg.event);
|
||||
if (it != event_handlers_.end()) {
|
||||
it->second(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to generic message handler
|
||||
if (on_message_) { on_message_(msg); }
|
||||
}
|
||||
|
||||
// Check if we should attempt to reconnect
|
||||
bool should_reconnect(int count) const {
|
||||
if (!running_.load()) { return false; }
|
||||
if (max_reconnect_attempts_ == 0) { return true; } // unlimited
|
||||
return count < max_reconnect_attempts_;
|
||||
}
|
||||
|
||||
// Wait for reconnect interval
|
||||
void wait_for_reconnect() {
|
||||
// Use small increments to check running_ flag frequently
|
||||
auto waited = 0;
|
||||
while (running_.load() && waited < reconnect_interval_ms_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
waited += 100;
|
||||
}
|
||||
}
|
||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms);
|
||||
void run_event_loop();
|
||||
void dispatch_event(const SSEMessage &msg);
|
||||
bool should_reconnect(int count) const;
|
||||
void wait_for_reconnect();
|
||||
|
||||
// Client and path
|
||||
Client &client_;
|
||||
@@ -3628,6 +3341,344 @@ private:
|
||||
* Implementation that will be part of the .cc file if split into .h + .cc.
|
||||
*/
|
||||
|
||||
namespace stream {
|
||||
|
||||
// stream::Result implementations
|
||||
inline Result::Result() : chunk_size_(8192) {}
|
||||
|
||||
inline Result::Result(ClientImpl::StreamHandle &&handle, size_t chunk_size)
|
||||
: handle_(std::move(handle)), chunk_size_(chunk_size) {}
|
||||
|
||||
inline Result::Result(Result &&other) noexcept
|
||||
: handle_(std::move(other.handle_)), buffer_(std::move(other.buffer_)),
|
||||
current_size_(other.current_size_), chunk_size_(other.chunk_size_),
|
||||
finished_(other.finished_) {
|
||||
other.current_size_ = 0;
|
||||
other.finished_ = true;
|
||||
}
|
||||
|
||||
inline Result &Result::operator=(Result &&other) noexcept {
|
||||
if (this != &other) {
|
||||
handle_ = std::move(other.handle_);
|
||||
buffer_ = std::move(other.buffer_);
|
||||
current_size_ = other.current_size_;
|
||||
chunk_size_ = other.chunk_size_;
|
||||
finished_ = other.finished_;
|
||||
other.current_size_ = 0;
|
||||
other.finished_ = true;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline bool Result::is_valid() const { return handle_.is_valid(); }
|
||||
inline Result::operator bool() const { return is_valid(); }
|
||||
|
||||
inline int Result::status() const {
|
||||
return handle_.response ? handle_.response->status : -1;
|
||||
}
|
||||
|
||||
inline const Headers &Result::headers() const {
|
||||
static const Headers empty_headers;
|
||||
return handle_.response ? handle_.response->headers : empty_headers;
|
||||
}
|
||||
|
||||
inline std::string Result::get_header_value(const std::string &key,
|
||||
const char *def) const {
|
||||
return handle_.response ? handle_.response->get_header_value(key, def) : def;
|
||||
}
|
||||
|
||||
inline bool Result::has_header(const std::string &key) const {
|
||||
return handle_.response ? handle_.response->has_header(key) : false;
|
||||
}
|
||||
|
||||
inline Error Result::error() const { return handle_.error; }
|
||||
inline Error Result::read_error() const { return handle_.get_read_error(); }
|
||||
inline bool Result::has_read_error() const { return handle_.has_read_error(); }
|
||||
|
||||
inline bool Result::next() {
|
||||
if (!handle_.is_valid() || finished_) { return false; }
|
||||
|
||||
if (buffer_.size() < chunk_size_) { buffer_.resize(chunk_size_); }
|
||||
|
||||
ssize_t n = handle_.read(&buffer_[0], chunk_size_);
|
||||
if (n > 0) {
|
||||
current_size_ = static_cast<size_t>(n);
|
||||
return true;
|
||||
}
|
||||
|
||||
current_size_ = 0;
|
||||
finished_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
inline const char *Result::data() const { return buffer_.data(); }
|
||||
inline size_t Result::size() const { return current_size_; }
|
||||
|
||||
inline std::string Result::read_all() {
|
||||
std::string result;
|
||||
while (next()) {
|
||||
result.append(data(), size());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace stream
|
||||
|
||||
namespace sse {
|
||||
|
||||
// SSEMessage implementations
|
||||
inline SSEMessage::SSEMessage() : event("message") {}
|
||||
|
||||
inline void SSEMessage::clear() {
|
||||
event = "message";
|
||||
data.clear();
|
||||
id.clear();
|
||||
}
|
||||
|
||||
// SSEClient implementations
|
||||
inline SSEClient::SSEClient(Client &client, const std::string &path)
|
||||
: client_(client), path_(path) {}
|
||||
|
||||
inline SSEClient::SSEClient(Client &client, const std::string &path,
|
||||
const Headers &headers)
|
||||
: client_(client), path_(path), headers_(headers) {}
|
||||
|
||||
inline SSEClient::~SSEClient() { stop(); }
|
||||
|
||||
inline SSEClient &SSEClient::on_message(MessageHandler handler) {
|
||||
on_message_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline SSEClient &SSEClient::on_event(const std::string &type,
|
||||
MessageHandler handler) {
|
||||
event_handlers_[type] = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline SSEClient &SSEClient::on_open(OpenHandler handler) {
|
||||
on_open_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline SSEClient &SSEClient::on_error(ErrorHandler handler) {
|
||||
on_error_ = std::move(handler);
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline SSEClient &SSEClient::set_reconnect_interval(int ms) {
|
||||
reconnect_interval_ms_ = ms;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline SSEClient &SSEClient::set_max_reconnect_attempts(int n) {
|
||||
max_reconnect_attempts_ = n;
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline bool SSEClient::is_connected() const { return connected_.load(); }
|
||||
|
||||
inline const std::string &SSEClient::last_event_id() const {
|
||||
return last_event_id_;
|
||||
}
|
||||
|
||||
inline void SSEClient::start() {
|
||||
running_.store(true);
|
||||
run_event_loop();
|
||||
}
|
||||
|
||||
inline void SSEClient::start_async() {
|
||||
running_.store(true);
|
||||
async_thread_ = std::thread([this]() { run_event_loop(); });
|
||||
}
|
||||
|
||||
inline void SSEClient::stop() {
|
||||
running_.store(false);
|
||||
client_.stop(); // Cancel any pending operations
|
||||
if (async_thread_.joinable()) { async_thread_.join(); }
|
||||
}
|
||||
|
||||
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||
int &retry_ms) {
|
||||
// Blank line signals end of event
|
||||
if (line.empty() || line == "\r") { return true; }
|
||||
|
||||
// Lines starting with ':' are comments (ignored)
|
||||
if (!line.empty() && line[0] == ':') { return false; }
|
||||
|
||||
// Find the colon separator
|
||||
auto colon_pos = line.find(':');
|
||||
if (colon_pos == std::string::npos) {
|
||||
// Line with no colon is treated as field name with empty value
|
||||
return false;
|
||||
}
|
||||
|
||||
auto field = line.substr(0, colon_pos);
|
||||
std::string value;
|
||||
|
||||
// Value starts after colon, skip optional single space
|
||||
if (colon_pos + 1 < line.size()) {
|
||||
auto value_start = colon_pos + 1;
|
||||
if (line[value_start] == ' ') { value_start++; }
|
||||
value = line.substr(value_start);
|
||||
// Remove trailing \r if present
|
||||
if (!value.empty() && value.back() == '\r') { value.pop_back(); }
|
||||
}
|
||||
|
||||
// Handle known fields
|
||||
if (field == "event") {
|
||||
msg.event = value;
|
||||
} else if (field == "data") {
|
||||
// Multiple data lines are concatenated with newlines
|
||||
if (!msg.data.empty()) { msg.data += "\n"; }
|
||||
msg.data += value;
|
||||
} else if (field == "id") {
|
||||
// Empty id is valid (clears the last event ID)
|
||||
msg.id = value;
|
||||
} else if (field == "retry") {
|
||||
// Parse retry interval in milliseconds
|
||||
{
|
||||
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;
|
||||
|
||||
while (running_.load()) {
|
||||
// Build headers, including Last-Event-ID if we have one
|
||||
auto request_headers = headers_;
|
||||
if (!last_event_id_.empty()) {
|
||||
request_headers.emplace("Last-Event-ID", last_event_id_);
|
||||
}
|
||||
|
||||
// Open streaming connection
|
||||
auto result = stream::Get(client_, path_, request_headers);
|
||||
|
||||
// Connection error handling
|
||||
if (!result) {
|
||||
connected_.store(false);
|
||||
if (on_error_) { on_error_(result.error()); }
|
||||
|
||||
if (!should_reconnect(reconnect_count)) { break; }
|
||||
wait_for_reconnect();
|
||||
reconnect_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result.status() != 200) {
|
||||
connected_.store(false);
|
||||
// For certain errors, don't reconnect
|
||||
if (result.status() == 204 || // No Content - server wants us to stop
|
||||
result.status() == 404 || // Not Found
|
||||
result.status() == 401 || // Unauthorized
|
||||
result.status() == 403) { // Forbidden
|
||||
if (on_error_) { on_error_(Error::Connection); }
|
||||
break;
|
||||
}
|
||||
|
||||
if (on_error_) { on_error_(Error::Connection); }
|
||||
|
||||
if (!should_reconnect(reconnect_count)) { break; }
|
||||
wait_for_reconnect();
|
||||
reconnect_count++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Connection successful
|
||||
connected_.store(true);
|
||||
reconnect_count = 0;
|
||||
if (on_open_) { on_open_(); }
|
||||
|
||||
// Event receiving loop
|
||||
std::string buffer;
|
||||
SSEMessage current_msg;
|
||||
|
||||
while (running_.load() && result.next()) {
|
||||
buffer.append(result.data(), result.size());
|
||||
|
||||
// Process complete lines in the buffer
|
||||
size_t line_start = 0;
|
||||
size_t newline_pos;
|
||||
|
||||
while ((newline_pos = buffer.find('\n', line_start)) !=
|
||||
std::string::npos) {
|
||||
auto line = buffer.substr(line_start, newline_pos - line_start);
|
||||
line_start = newline_pos + 1;
|
||||
|
||||
// Parse the line and check if event is complete
|
||||
auto event_complete =
|
||||
parse_sse_line(line, current_msg, reconnect_interval_ms_);
|
||||
|
||||
if (event_complete && !current_msg.data.empty()) {
|
||||
// Update last_event_id for reconnection
|
||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
||||
|
||||
// Dispatch event to appropriate handler
|
||||
dispatch_event(current_msg);
|
||||
|
||||
current_msg.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Keep unprocessed data in buffer
|
||||
buffer.erase(0, line_start);
|
||||
}
|
||||
|
||||
// Connection ended
|
||||
connected_.store(false);
|
||||
|
||||
if (!running_.load()) { break; }
|
||||
|
||||
// Check for read errors
|
||||
if (result.has_read_error()) {
|
||||
if (on_error_) { on_error_(result.read_error()); }
|
||||
}
|
||||
|
||||
if (!should_reconnect(reconnect_count)) { break; }
|
||||
wait_for_reconnect();
|
||||
reconnect_count++;
|
||||
}
|
||||
|
||||
connected_.store(false);
|
||||
}
|
||||
|
||||
inline void SSEClient::dispatch_event(const SSEMessage &msg) {
|
||||
// Check for specific event type handler first
|
||||
auto it = event_handlers_.find(msg.event);
|
||||
if (it != event_handlers_.end()) {
|
||||
it->second(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fall back to generic message handler
|
||||
if (on_message_) { on_message_(msg); }
|
||||
}
|
||||
|
||||
inline bool SSEClient::should_reconnect(int count) const {
|
||||
if (!running_.load()) { return false; }
|
||||
if (max_reconnect_attempts_ == 0) { return true; } // unlimited
|
||||
return count < max_reconnect_attempts_;
|
||||
}
|
||||
|
||||
inline void SSEClient::wait_for_reconnect() {
|
||||
// Use small increments to check running_ flag frequently
|
||||
auto waited = 0;
|
||||
while (running_.load() && waited < reconnect_interval_ms_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
waited += 100;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace sse
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
/*
|
||||
* TLS abstraction layer - internal function declarations
|
||||
@@ -9805,36 +9856,40 @@ inline bool Server::read_content_core(
|
||||
// are true (no Transfer-Encoding and no Content-Length), then the message
|
||||
// body length is zero (no message body is present).
|
||||
//
|
||||
// For non-SSL builds, peek into the socket to detect clients that send a
|
||||
// body without a Content-Length header (raw HTTP over TCP). If there is
|
||||
// pending data that exceeds the configured payload limit, treat this as an
|
||||
// oversized request and fail early (causing connection close). For SSL
|
||||
// builds we cannot reliably peek the decrypted application bytes, so keep
|
||||
// the original behaviour.
|
||||
// For non-SSL builds, detect clients that send a body without a
|
||||
// Content-Length header (raw HTTP over TCP). Check both the stream's
|
||||
// internal read buffer (data already read from the socket during header
|
||||
// parsing) and the socket itself for pending data. If data is found and
|
||||
// exceeds the configured payload limit, reject with 413.
|
||||
// For SSL builds we cannot reliably peek the decrypted application bytes,
|
||||
// so keep the original behaviour.
|
||||
#if !defined(CPPHTTPLIB_SSL_ENABLED)
|
||||
if (!req.has_header("Content-Length") &&
|
||||
!detail::is_chunked_transfer_encoding(req.headers)) {
|
||||
// Only peek if payload_max_length is set to a finite value
|
||||
// Only check if payload_max_length is set to a finite value
|
||||
if (payload_max_length_ > 0 &&
|
||||
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
|
||||
socket_t s = strm.socket();
|
||||
if (s != INVALID_SOCKET) {
|
||||
// Peek to check if there is any pending data
|
||||
char peekbuf[1];
|
||||
ssize_t n = ::recv(s, peekbuf, 1, MSG_PEEK);
|
||||
if (n > 0) {
|
||||
// There is data, so read it with payload limit enforcement
|
||||
auto result = detail::read_content_without_length(
|
||||
strm, payload_max_length_, out);
|
||||
if (result == detail::ReadContentResult::PayloadTooLarge) {
|
||||
res.status = StatusCode::PayloadTooLarge_413;
|
||||
return false;
|
||||
} else if (result != detail::ReadContentResult::Success) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// Check if there is data already buffered in the stream (read during
|
||||
// header parsing) or pending on the socket. Use a non-blocking socket
|
||||
// check to avoid deadlock when the client sends no body.
|
||||
bool has_data = strm.is_readable();
|
||||
if (!has_data) {
|
||||
socket_t s = strm.socket();
|
||||
if (s != INVALID_SOCKET) {
|
||||
has_data = detail::select_read(s, 0, 0) > 0;
|
||||
}
|
||||
}
|
||||
if (has_data) {
|
||||
auto result =
|
||||
detail::read_content_without_length(strm, payload_max_length_, out);
|
||||
if (result == detail::ReadContentResult::PayloadTooLarge) {
|
||||
res.status = StatusCode::PayloadTooLarge_413;
|
||||
return false;
|
||||
} else if (result != detail::ReadContentResult::Success) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -10704,6 +10759,7 @@ inline void ClientImpl::copy_settings(const ClientImpl &rhs) {
|
||||
compress_ = rhs.compress_;
|
||||
decompress_ = rhs.decompress_;
|
||||
payload_max_length_ = rhs.payload_max_length_;
|
||||
has_payload_max_length_ = rhs.has_payload_max_length_;
|
||||
interface_ = rhs.interface_;
|
||||
proxy_host_ = rhs.proxy_host_;
|
||||
proxy_port_ = rhs.proxy_port_;
|
||||
@@ -12093,7 +12149,10 @@ inline bool ClientImpl::process_request(Stream &strm, Request &req,
|
||||
|
||||
if (res.status != StatusCode::NotModified_304) {
|
||||
int dummy_status;
|
||||
if (!detail::read_content(strm, res, payload_max_length_, dummy_status,
|
||||
auto max_length = (!has_payload_max_length_ && req.content_receiver)
|
||||
? (std::numeric_limits<size_t>::max)()
|
||||
: payload_max_length_;
|
||||
if (!detail::read_content(strm, res, max_length, dummy_status,
|
||||
std::move(progress), std::move(out),
|
||||
decompress_)) {
|
||||
if (error != Error::Canceled) { error = Error::Read; }
|
||||
@@ -13044,6 +13103,7 @@ inline void ClientImpl::set_decompress(bool on) { decompress_ = on; }
|
||||
|
||||
inline void ClientImpl::set_payload_max_length(size_t length) {
|
||||
payload_max_length_ = length;
|
||||
has_payload_max_length_ = true;
|
||||
}
|
||||
|
||||
inline void ClientImpl::set_interface(const std::string &intf) {
|
||||
@@ -14689,7 +14749,8 @@ inline std::string x509_store_to_pem(X509_STORE *store) {
|
||||
std::string pem;
|
||||
auto objs = X509_STORE_get0_objects(store);
|
||||
if (!objs) return {};
|
||||
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
|
||||
auto count = sk_X509_OBJECT_num(objs);
|
||||
for (decltype(count) i = 0; i < count; i++) {
|
||||
auto obj = sk_X509_OBJECT_value(objs, i);
|
||||
if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
|
||||
auto cert = X509_OBJECT_get0_X509(obj);
|
||||
@@ -14756,7 +14817,8 @@ inline STACK_OF(X509_NAME) *
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (int i = 0; i < sk_X509_OBJECT_num(objs); i++) {
|
||||
auto count = sk_X509_OBJECT_num(objs);
|
||||
for (decltype(count) i = 0; i < count; i++) {
|
||||
auto obj = sk_X509_OBJECT_value(objs, i);
|
||||
if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
|
||||
auto cert = X509_OBJECT_get0_X509(obj);
|
||||
@@ -15201,6 +15263,9 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
|
||||
}
|
||||
|
||||
auto ssl = static_cast<SSL *>(session);
|
||||
constexpr auto max_len =
|
||||
static_cast<size_t>((std::numeric_limits<int>::max)());
|
||||
if (len > max_len) { len = max_len; }
|
||||
auto ret = SSL_read(ssl, buf, static_cast<int>(len));
|
||||
|
||||
if (ret > 0) {
|
||||
@@ -15405,18 +15470,20 @@ inline bool get_cert_validity(cert_t cert, time_t ¬_before,
|
||||
auto na = X509_get0_notAfter(x509);
|
||||
if (!nb || !na) return false;
|
||||
|
||||
// Convert ASN1_TIME to time_t
|
||||
struct tm tm_nb = {}, tm_na = {};
|
||||
if (ASN1_TIME_to_tm(nb, &tm_nb) != 1) return false;
|
||||
if (ASN1_TIME_to_tm(na, &tm_na) != 1) return false;
|
||||
ASN1_TIME *epoch = ASN1_TIME_new();
|
||||
if (!epoch) return false;
|
||||
auto se = detail::scope_exit([&] { ASN1_TIME_free(epoch); });
|
||||
|
||||
if (!ASN1_TIME_set(epoch, 0)) return false;
|
||||
|
||||
int pday, psec;
|
||||
|
||||
if (!ASN1_TIME_diff(&pday, &psec, epoch, nb)) return false;
|
||||
not_before = 86400 * (time_t)pday + psec;
|
||||
|
||||
if (!ASN1_TIME_diff(&pday, &psec, epoch, na)) return false;
|
||||
not_after = 86400 * (time_t)pday + psec;
|
||||
|
||||
#ifdef _WIN32
|
||||
not_before = _mkgmtime(&tm_nb);
|
||||
not_after = _mkgmtime(&tm_na);
|
||||
#else
|
||||
not_before = timegm(&tm_nb);
|
||||
not_after = timegm(&tm_na);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -15516,8 +15583,8 @@ inline size_t get_ca_certs(ctx_t ctx, std::vector<cert_t> &certs) {
|
||||
auto objs = X509_STORE_get0_objects(store);
|
||||
if (!objs) { return 0; }
|
||||
|
||||
int count = sk_X509_OBJECT_num(objs);
|
||||
for (int i = 0; i < count; i++) {
|
||||
auto count = sk_X509_OBJECT_num(objs);
|
||||
for (decltype(count) i = 0; i < count; i++) {
|
||||
auto obj = sk_X509_OBJECT_value(objs, i);
|
||||
if (!obj) { continue; }
|
||||
if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
|
||||
@@ -15543,8 +15610,8 @@ inline std::vector<std::string> get_ca_names(ctx_t ctx) {
|
||||
auto objs = X509_STORE_get0_objects(store);
|
||||
if (!objs) { return names; }
|
||||
|
||||
int count = sk_X509_OBJECT_num(objs);
|
||||
for (int i = 0; i < count; i++) {
|
||||
auto count = sk_X509_OBJECT_num(objs);
|
||||
for (decltype(count) i = 0; i < count; i++) {
|
||||
auto obj = sk_X509_OBJECT_value(objs, i);
|
||||
if (!obj) { continue; }
|
||||
if (X509_OBJECT_get_type(obj) == X509_LU_X509) {
|
||||
|
||||
@@ -6,13 +6,16 @@ list:
|
||||
@just --list --unsorted
|
||||
|
||||
openssl:
|
||||
@(cd test && make test && ./test)
|
||||
@(cd test && make test && LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test)
|
||||
@(cd test && make proxy)
|
||||
|
||||
mbedtls:
|
||||
@(cd test && make test_mbedtls && ./test_mbedtls)
|
||||
@(cd test && make test_mbedtls && LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./test_mbedtls)
|
||||
@(cd test && make proxy_mbedtls)
|
||||
|
||||
no_tls:
|
||||
@(cd test && make test_no_tls && ./test_no_tls)
|
||||
|
||||
fuzz:
|
||||
@(cd test && make fuzz_test)
|
||||
|
||||
|
||||
+23
-5
@@ -39,11 +39,29 @@ endif
|
||||
deps = [dependency('threads')]
|
||||
args = []
|
||||
|
||||
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('openssl'))
|
||||
if openssl_dep.found()
|
||||
deps += openssl_dep
|
||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||
if host_machine.system() == 'darwin'
|
||||
if get_option('tls').allowed()
|
||||
tls_found = false
|
||||
if get_option('tls_backend') == 'openssl'
|
||||
openssl_dep = dependency('openssl', version: '>=3.0.0', required: get_option('tls'))
|
||||
if openssl_dep.found()
|
||||
deps += openssl_dep
|
||||
args += '-DCPPHTTPLIB_OPENSSL_SUPPORT'
|
||||
tls_found = true
|
||||
endif
|
||||
else
|
||||
mbedtls_dep = dependency('mbedtls', required: get_option('tls'))
|
||||
mbedtlsx509_dep = dependency('mbedx509', required: get_option('tls'))
|
||||
mbedtlscrypto_dep = dependency('mbedcrypto', required: get_option('tls'))
|
||||
if mbedtls_dep.found() and mbedtlsx509_dep.found() and mbedtlscrypto_dep.found()
|
||||
deps += mbedtls_dep
|
||||
deps += mbedtlsx509_dep
|
||||
deps += mbedtlscrypto_dep
|
||||
args += '-DCPPHTTPLIB_MBEDTLS_SUPPORT'
|
||||
tls_found = true
|
||||
endif
|
||||
endif
|
||||
|
||||
if tls_found and host_machine.system() == 'darwin'
|
||||
macosx_keychain_dep = dependency('appleframeworks', modules: ['CFNetwork', 'CoreFoundation', 'Security'], required: get_option('macosx_keychain'))
|
||||
if macosx_keychain_dep.found()
|
||||
deps += macosx_keychain_dep
|
||||
|
||||
+3
-1
@@ -2,7 +2,8 @@
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
option('openssl', type: 'feature', value: 'auto', description: 'Enable OpenSSL support')
|
||||
option('tls', type: 'feature', description: 'Enable TLS support')
|
||||
option('tls_backend', type: 'combo', choices: ['openssl', 'mbedtls'], value: 'openssl', description: 'Which TLS library to use')
|
||||
option('zlib', type: 'feature', value: 'auto', description: 'Enable zlib support')
|
||||
option('brotli', type: 'feature', value: 'auto', description: 'Enable Brotli support')
|
||||
option('zstd', type: 'feature', value: 'auto', description: 'Enable zstd support')
|
||||
@@ -12,6 +13,7 @@ option('compile', type: 'boolean', value: false, description: 'Split the header
|
||||
option('test', type: 'boolean', value: false, description: 'Build tests')
|
||||
|
||||
# Old option names
|
||||
option('openssl', type: 'feature', deprecated: 'tls')
|
||||
option('cpp-httplib_openssl', type: 'feature', deprecated: 'openssl')
|
||||
option('cpp-httplib_zlib', type: 'feature', deprecated: 'zlib')
|
||||
option('cpp-httplib_brotli', type: 'feature', deprecated: 'brotli')
|
||||
|
||||
+334
-1
@@ -8752,6 +8752,339 @@ TEST(ClientVulnerabilityTest, PayloadMaxLengthZeroMeansNoLimit) {
|
||||
<< " bytes without truncation, but only read " << total_read << " bytes.";
|
||||
}
|
||||
|
||||
// Verify that content_receiver bypasses the default payload_max_length,
|
||||
// allowing streaming downloads larger than 100MB without requiring an explicit
|
||||
// set_payload_max_length call.
|
||||
TEST(ClientVulnerabilityTest, ContentReceiverBypassesDefaultPayloadMaxLength) {
|
||||
static constexpr size_t DATA_SIZE = 200 * 1024 * 1024; // 200MB from server
|
||||
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto server_thread = std::thread([] {
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
|
||||
#ifdef _WIN32
|
||||
reinterpret_cast<const char *>(&opt),
|
||||
#else
|
||||
&opt,
|
||||
#endif
|
||||
sizeof(opt));
|
||||
|
||||
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(srv, 1);
|
||||
|
||||
sockaddr_in cli_addr{};
|
||||
socklen_t cli_len = sizeof(cli_addr);
|
||||
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
||||
|
||||
if (cli != INVALID_SOCKET) {
|
||||
char buf[4096];
|
||||
::recv(cli, buf, sizeof(buf), 0);
|
||||
|
||||
// Response with Content-Length larger than default 100MB limit
|
||||
auto content_length = std::to_string(DATA_SIZE);
|
||||
std::string response_header = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Length: " +
|
||||
content_length +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(response_header.c_str()),
|
||||
static_cast<int>(response_header.size()),
|
||||
#else
|
||||
response_header.c_str(), response_header.size(),
|
||||
#endif
|
||||
0);
|
||||
|
||||
std::string chunk(64 * 1024, 'A');
|
||||
size_t total_sent = 0;
|
||||
|
||||
while (total_sent < DATA_SIZE) {
|
||||
auto to_send = std::min(chunk.size(), DATA_SIZE - total_sent);
|
||||
auto sent = ::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(chunk.c_str()),
|
||||
static_cast<int>(to_send),
|
||||
#else
|
||||
chunk.c_str(), to_send,
|
||||
#endif
|
||||
0);
|
||||
if (sent <= 0) break;
|
||||
total_sent += static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
detail::close_socket(cli);
|
||||
}
|
||||
detail::close_socket(srv);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
size_t total_received = 0;
|
||||
|
||||
{
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
cli.set_read_timeout(10, 0);
|
||||
// Do NOT call set_payload_max_length — use the default 100MB limit
|
||||
|
||||
auto res =
|
||||
cli.Get("/large", [&](const char * /*data*/, size_t data_length) {
|
||||
total_received += data_length;
|
||||
return true;
|
||||
});
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
server_thread.join();
|
||||
|
||||
EXPECT_EQ(total_received, DATA_SIZE)
|
||||
<< "With content_receiver, the client should read all " << DATA_SIZE
|
||||
<< " bytes despite the default 100MB payload_max_length, but only read "
|
||||
<< total_received << " bytes.";
|
||||
}
|
||||
|
||||
// Verify that an explicit set_payload_max_length smaller than the response is
|
||||
// enforced even when a content_receiver is used.
|
||||
TEST(ClientVulnerabilityTest,
|
||||
ContentReceiverRespectsExplicitPayloadMaxLength150MB) {
|
||||
static constexpr size_t DATA_SIZE = 200 * 1024 * 1024; // 200MB from server
|
||||
static constexpr size_t EXPLICIT_LIMIT = 150 * 1024 * 1024; // 150MB limit
|
||||
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto server_thread = std::thread([] {
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
|
||||
#ifdef _WIN32
|
||||
reinterpret_cast<const char *>(&opt),
|
||||
#else
|
||||
&opt,
|
||||
#endif
|
||||
sizeof(opt));
|
||||
|
||||
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(srv, 1);
|
||||
|
||||
sockaddr_in cli_addr{};
|
||||
socklen_t cli_len = sizeof(cli_addr);
|
||||
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
||||
|
||||
if (cli != INVALID_SOCKET) {
|
||||
char buf[4096];
|
||||
::recv(cli, buf, sizeof(buf), 0);
|
||||
|
||||
auto content_length = std::to_string(DATA_SIZE);
|
||||
std::string response_header = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Length: " +
|
||||
content_length +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(response_header.c_str()),
|
||||
static_cast<int>(response_header.size()),
|
||||
#else
|
||||
response_header.c_str(), response_header.size(),
|
||||
#endif
|
||||
0);
|
||||
|
||||
std::string chunk(64 * 1024, 'A');
|
||||
size_t total_sent = 0;
|
||||
|
||||
while (total_sent < DATA_SIZE) {
|
||||
auto to_send = std::min(chunk.size(), DATA_SIZE - total_sent);
|
||||
auto sent = ::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(chunk.c_str()),
|
||||
static_cast<int>(to_send),
|
||||
#else
|
||||
chunk.c_str(), to_send,
|
||||
#endif
|
||||
0);
|
||||
if (sent <= 0) break;
|
||||
total_sent += static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
detail::close_socket(cli);
|
||||
}
|
||||
detail::close_socket(srv);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
size_t total_received = 0;
|
||||
|
||||
{
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
cli.set_read_timeout(10, 0);
|
||||
cli.set_payload_max_length(EXPLICIT_LIMIT); // Explicit 150MB limit
|
||||
|
||||
auto res =
|
||||
cli.Get("/large", [&](const char * /*data*/, size_t data_length) {
|
||||
total_received += data_length;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Should fail because 200MB exceeds the explicit 150MB limit
|
||||
EXPECT_FALSE(res);
|
||||
}
|
||||
|
||||
server_thread.join();
|
||||
|
||||
EXPECT_LE(total_received, EXPLICIT_LIMIT)
|
||||
<< "Client with content_receiver should respect the explicit "
|
||||
<< "payload_max_length of " << EXPLICIT_LIMIT << " bytes, but read "
|
||||
<< total_received << " bytes.";
|
||||
}
|
||||
|
||||
// Verify that an explicit set_payload_max_length larger than the response
|
||||
// allows the content_receiver to read all data successfully.
|
||||
TEST(ClientVulnerabilityTest,
|
||||
ContentReceiverRespectsExplicitPayloadMaxLength250MB) {
|
||||
static constexpr size_t DATA_SIZE = 200 * 1024 * 1024; // 200MB from server
|
||||
static constexpr size_t EXPLICIT_LIMIT = 250 * 1024 * 1024; // 250MB limit
|
||||
|
||||
#ifndef _WIN32
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
#endif
|
||||
|
||||
auto server_thread = std::thread([] {
|
||||
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
default_socket_options(srv);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
||||
detail::set_socket_opt_time(srv, SOL_SOCKET, SO_SNDTIMEO, 5, 0);
|
||||
|
||||
sockaddr_in addr{};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(PORT + 2);
|
||||
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
||||
|
||||
int opt = 1;
|
||||
::setsockopt(srv, SOL_SOCKET, SO_REUSEADDR,
|
||||
#ifdef _WIN32
|
||||
reinterpret_cast<const char *>(&opt),
|
||||
#else
|
||||
&opt,
|
||||
#endif
|
||||
sizeof(opt));
|
||||
|
||||
::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(srv, 1);
|
||||
|
||||
sockaddr_in cli_addr{};
|
||||
socklen_t cli_len = sizeof(cli_addr);
|
||||
auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
||||
|
||||
if (cli != INVALID_SOCKET) {
|
||||
char buf[4096];
|
||||
::recv(cli, buf, sizeof(buf), 0);
|
||||
|
||||
auto content_length = std::to_string(DATA_SIZE);
|
||||
std::string response_header = "HTTP/1.1 200 OK\r\n"
|
||||
"Content-Length: " +
|
||||
content_length +
|
||||
"\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n";
|
||||
|
||||
::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(response_header.c_str()),
|
||||
static_cast<int>(response_header.size()),
|
||||
#else
|
||||
response_header.c_str(), response_header.size(),
|
||||
#endif
|
||||
0);
|
||||
|
||||
std::string chunk(64 * 1024, 'A');
|
||||
size_t total_sent = 0;
|
||||
|
||||
while (total_sent < DATA_SIZE) {
|
||||
auto to_send = std::min(chunk.size(), DATA_SIZE - total_sent);
|
||||
auto sent = ::send(cli,
|
||||
#ifdef _WIN32
|
||||
static_cast<const char *>(chunk.c_str()),
|
||||
static_cast<int>(to_send),
|
||||
#else
|
||||
chunk.c_str(), to_send,
|
||||
#endif
|
||||
0);
|
||||
if (sent <= 0) break;
|
||||
total_sent += static_cast<size_t>(sent);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
::shutdown(cli, SD_SEND);
|
||||
#else
|
||||
::shutdown(cli, SHUT_WR);
|
||||
#endif
|
||||
char drain[1024];
|
||||
while (::recv(cli, drain, sizeof(drain), 0) > 0) {}
|
||||
detail::close_socket(cli);
|
||||
}
|
||||
detail::close_socket(srv);
|
||||
});
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
size_t total_received = 0;
|
||||
|
||||
{
|
||||
Client cli("127.0.0.1", PORT + 2);
|
||||
cli.set_read_timeout(10, 0);
|
||||
cli.set_payload_max_length(EXPLICIT_LIMIT); // Explicit 250MB limit
|
||||
|
||||
auto res =
|
||||
cli.Get("/large", [&](const char * /*data*/, size_t data_length) {
|
||||
total_received += data_length;
|
||||
return true;
|
||||
});
|
||||
|
||||
ASSERT_TRUE(res);
|
||||
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||
}
|
||||
|
||||
server_thread.join();
|
||||
|
||||
EXPECT_EQ(total_received, DATA_SIZE)
|
||||
<< "With explicit payload_max_length of " << EXPLICIT_LIMIT
|
||||
<< " bytes (larger than " << DATA_SIZE
|
||||
<< " bytes response), content_receiver should read all data, but only "
|
||||
"read "
|
||||
<< total_received << " bytes.";
|
||||
}
|
||||
|
||||
#if defined(CPPHTTPLIB_ZLIB_SUPPORT) && !defined(_WIN32)
|
||||
// Regression test for "zip bomb" attack on the client side: a malicious server
|
||||
// sends a small gzip-compressed response that decompresses to a huge payload.
|
||||
@@ -8902,7 +9235,7 @@ TEST(HostAndPortPropertiesTest, SSL) {
|
||||
ASSERT_EQ(443, cli.port());
|
||||
}
|
||||
|
||||
TEST(SSLClientTest, UpdateCAStoreWithPem) {
|
||||
TEST(SSLClientTest, UpdateCAStoreWithPem_Online) {
|
||||
// Test updating CA store multiple times using PEM-based load_ca_cert_store
|
||||
std::string cert;
|
||||
read_file(CA_CERT_FILE, cert);
|
||||
|
||||
Reference in New Issue
Block a user