|
|
|
@@ -142,6 +142,12 @@
|
|
|
|
|
#define CPPHTTPLIB_PAYLOAD_MAX_LENGTH (100 * 1024 * 1024) // 100MB
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// How much of an unread request body the server skips to reuse the connection,
|
|
|
|
|
// 0 for no limit
|
|
|
|
|
#ifndef CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH
|
|
|
|
|
#define CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH (256 * 1024) // 256KB
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
#ifndef CPPHTTPLIB_SSE_MAX_EVENT_LENGTH
|
|
|
|
|
#define CPPHTTPLIB_SSE_MAX_EVENT_LENGTH (16 * 1024 * 1024) // 16MB
|
|
|
|
|
#endif
|
|
|
|
@@ -2357,6 +2363,8 @@ public:
|
|
|
|
|
|
|
|
|
|
Server &set_payload_max_length(size_t length);
|
|
|
|
|
|
|
|
|
|
Server &set_compress(bool on);
|
|
|
|
|
|
|
|
|
|
Server &set_static_file_compression(bool on);
|
|
|
|
|
Server &set_static_file_compression_min_length(size_t length);
|
|
|
|
|
Server &set_static_file_compression_max_length(size_t length);
|
|
|
|
@@ -2392,10 +2400,11 @@ protected:
|
|
|
|
|
// Runs the per-connection serving loop and stops an exception thrown by a
|
|
|
|
|
// user callback from escaping the worker thread.
|
|
|
|
|
//
|
|
|
|
|
// process_request() wraps only routing() in a try/catch. Content providers,
|
|
|
|
|
// the post-routing, error, logging and expect-100 handlers and WebSocket
|
|
|
|
|
// handlers all run outside it, and the task queue calls the job without a
|
|
|
|
|
// catch, so an exception from any of those would terminate the process.
|
|
|
|
|
// process_request() wraps only routing() and the hooks before a WebSocket
|
|
|
|
|
// upgrade in a try/catch. Content providers, the post-routing, error,
|
|
|
|
|
// logging and expect-100 handlers and WebSocket handlers all run outside
|
|
|
|
|
// it, and the task queue calls the job without a catch, so an exception
|
|
|
|
|
// from any of those would terminate the process.
|
|
|
|
|
//
|
|
|
|
|
// No 500 is possible here: by the time a content provider runs, the status
|
|
|
|
|
// line and headers are already on the wire. Report it through the error
|
|
|
|
@@ -2431,6 +2440,7 @@ protected:
|
|
|
|
|
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
|
|
|
|
|
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
|
|
|
|
|
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
|
|
|
|
bool compress_ = true;
|
|
|
|
|
bool static_file_compression_ = false;
|
|
|
|
|
size_t static_file_compression_min_length_ =
|
|
|
|
|
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
|
|
|
|
@@ -2481,6 +2491,9 @@ private:
|
|
|
|
|
|
|
|
|
|
bool routing(Request &req, Response &res, Stream &strm,
|
|
|
|
|
bool skip_pre_routing);
|
|
|
|
|
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
|
|
|
|
bool handle_exception(Request &req, Response &res);
|
|
|
|
|
#endif
|
|
|
|
|
bool handle_file_request(Request &req, Response &res);
|
|
|
|
|
bool check_if_not_modified(const Request &req, Response &res,
|
|
|
|
|
const std::string &etag, time_t mtime) const;
|
|
|
|
@@ -2997,6 +3010,15 @@ protected:
|
|
|
|
|
void close_socket(Socket &socket);
|
|
|
|
|
void disconnect(bool gracefully);
|
|
|
|
|
|
|
|
|
|
// Under socket_mutex_: whether the connection kept alive can take another
|
|
|
|
|
// request. One the peer has closed, or one set up under TLS settings changed
|
|
|
|
|
// since, is closed instead.
|
|
|
|
|
bool socket_reusable();
|
|
|
|
|
|
|
|
|
|
// TLS settings apply as a connection is set up, so a change makes the
|
|
|
|
|
// connection kept alive unfit for the next request.
|
|
|
|
|
void tls_settings_changed() { tls_settings_version_++; }
|
|
|
|
|
|
|
|
|
|
std::string make_request_target(const Request &req) const;
|
|
|
|
|
bool process_request(Stream &strm, Request &req, Response &res,
|
|
|
|
|
bool close_connection, Error &error);
|
|
|
|
@@ -3023,6 +3045,9 @@ protected:
|
|
|
|
|
size_t socket_requests_in_flight_ = 0;
|
|
|
|
|
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
|
|
|
|
|
bool socket_should_be_closed_when_request_is_done_ = false;
|
|
|
|
|
size_t socket_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; only the connection target changes, never the identity.
|
|
|
|
@@ -4019,6 +4044,7 @@ struct ChunkedDecoder {
|
|
|
|
|
char line_buf[64];
|
|
|
|
|
size_t last_chunk_total = 0;
|
|
|
|
|
size_t last_chunk_offset = 0;
|
|
|
|
|
size_t overhead = 0;
|
|
|
|
|
|
|
|
|
|
explicit ChunkedDecoder(Stream &s);
|
|
|
|
|
|
|
|
|
@@ -5962,9 +5988,10 @@ inline bool is_valid_close_payload(const std::string &payload) {
|
|
|
|
|
|
|
|
|
|
namespace detail {
|
|
|
|
|
|
|
|
|
|
// A path a mount point serves: absolute, with no ".", ".." or empty segment
|
|
|
|
|
// except a trailing '/', so the file served is the one that req.path names to
|
|
|
|
|
// the handlers that ran before it.
|
|
|
|
|
// A path a mount point serves: absolute, with no empty segment except a
|
|
|
|
|
// trailing '/' and no segment ending in '.' or a space (which covers "." and
|
|
|
|
|
// ".." and names Windows trims into another file's name), so the file served
|
|
|
|
|
// is the one that req.path names to the handlers that ran before it.
|
|
|
|
|
inline bool is_valid_path(const std::string &path) {
|
|
|
|
|
if (path.empty() || path[0] != '/') { return false; }
|
|
|
|
|
|
|
|
|
@@ -5975,8 +6002,8 @@ inline bool is_valid_path(const std::string &path) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
auto len = i - beg;
|
|
|
|
|
if ((len == 0 && i < path.size()) || (len == 1 && path[beg] == '.') ||
|
|
|
|
|
(len == 2 && path[beg] == '.' && path[beg + 1] == '.')) {
|
|
|
|
|
if ((len == 0 && i < path.size()) ||
|
|
|
|
|
(len > 0 && (path[i - 1] == '.' || path[i - 1] == ' '))) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
beg = i + 1;
|
|
|
|
@@ -7728,13 +7755,10 @@ inline std::string
|
|
|
|
|
find_content_type(const std::string &path,
|
|
|
|
|
const std::map<std::string, std::string> &user_data,
|
|
|
|
|
const std::string &default_content_type) {
|
|
|
|
|
auto ext = file_extension(path);
|
|
|
|
|
// Extensions are matched regardless of case, as web servers do
|
|
|
|
|
auto ext = case_ignore::to_lower(file_extension(path));
|
|
|
|
|
|
|
|
|
|
// Extensions are matched regardless of case, as web servers do, with an
|
|
|
|
|
// exact match to a user mapping first
|
|
|
|
|
auto lower_ext = case_ignore::to_lower(ext);
|
|
|
|
|
auto it = user_data.find(ext);
|
|
|
|
|
if (it == user_data.end()) { it = user_data.find(lower_ext); }
|
|
|
|
|
if (it != user_data.end()) { return it->second; }
|
|
|
|
|
|
|
|
|
|
static const struct {
|
|
|
|
@@ -7793,7 +7817,7 @@ find_content_type(const std::string &path,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const auto &t : types) {
|
|
|
|
|
if (lower_ext == t.ext) { return t.type; }
|
|
|
|
|
if (ext == t.ext) { return t.type; }
|
|
|
|
|
}
|
|
|
|
|
return default_content_type;
|
|
|
|
|
}
|
|
|
|
@@ -8418,6 +8442,18 @@ inline void set_header(Headers &headers, const std::string &key,
|
|
|
|
|
if (fields::is_field_valid(key, val)) { headers.emplace(key, val); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Drops the fields set_header() would refuse, from headers an application can
|
|
|
|
|
// fill directly: CR/LF in one would split the message.
|
|
|
|
|
inline void erase_invalid_fields(Headers &headers) {
|
|
|
|
|
for (auto it = headers.begin(); it != headers.end();) {
|
|
|
|
|
if (fields::is_field_valid(it->first, it->second)) {
|
|
|
|
|
++it;
|
|
|
|
|
} else {
|
|
|
|
|
it = headers.erase(it);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool read_headers(Stream &strm, Headers &headers) {
|
|
|
|
|
const auto bufsiz = 2048;
|
|
|
|
|
char buf[bufsiz];
|
|
|
|
@@ -8895,13 +8931,6 @@ inline ssize_t write_response_line(Stream &strm, int status) {
|
|
|
|
|
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
|
|
|
|
|
ssize_t write_len = 0;
|
|
|
|
|
for (const auto &x : headers) {
|
|
|
|
|
// Skip fields with invalid names or values to prevent response splitting
|
|
|
|
|
// via CR/LF injection, matching set_header(). The client validates request
|
|
|
|
|
// headers up front in check_and_write_headers, but the server passes
|
|
|
|
|
// res.headers straight to this writer, and res.headers is a public field
|
|
|
|
|
// an application can populate directly with request-derived values.
|
|
|
|
|
if (!fields::is_field_valid(x.first, x.second)) { continue; }
|
|
|
|
|
|
|
|
|
|
std::string s;
|
|
|
|
|
s = x.first;
|
|
|
|
|
s += ": ";
|
|
|
|
@@ -10377,15 +10406,6 @@ inline bool has_framed_body(const Request &req) {
|
|
|
|
|
req.get_header_value_u64("Content-Length") > 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool is_connection_persistent(const Request &req) {
|
|
|
|
|
if (has_header_token(req.headers, "Connection", "close")) { return false; }
|
|
|
|
|
if (req.version == "HTTP/1.0" &&
|
|
|
|
|
!has_header_token(req.headers, "Connection", "keep-alive")) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool expect_content(const Request &req) {
|
|
|
|
|
if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" ||
|
|
|
|
|
req.method == "DELETE") {
|
|
|
|
@@ -10649,6 +10669,32 @@ inline bool is_ip_address(const std::string &host) {
|
|
|
|
|
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// One spelling for an IPv6 address, and the IPv4 one for an IPv4-mapped IPv6
|
|
|
|
|
// address, which is how a dual-stack socket reports an IPv4 peer. Anything
|
|
|
|
|
// else, including an address with a zone ID, is returned as it is.
|
|
|
|
|
inline std::string canonical_ip(const std::string &addr) {
|
|
|
|
|
struct in6_addr addr6;
|
|
|
|
|
if (addr.find('%') != std::string::npos ||
|
|
|
|
|
inet_pton(AF_INET6, addr.c_str(), &addr6) != 1) {
|
|
|
|
|
return addr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char buf[INET6_ADDRSTRLEN];
|
|
|
|
|
if (IN6_IS_ADDR_V4MAPPED(&addr6)) {
|
|
|
|
|
return inet_ntop(AF_INET, &addr6.s6_addr[12], buf, sizeof(buf)) ? buf
|
|
|
|
|
: addr;
|
|
|
|
|
}
|
|
|
|
|
return inet_ntop(AF_INET6, &addr6, buf, sizeof(buf)) ? buf : addr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// `trusted_proxies` holds canonical_ip() spellings.
|
|
|
|
|
inline bool is_trusted_proxy(const std::string &addr,
|
|
|
|
|
const std::vector<std::string> &trusted_proxies) {
|
|
|
|
|
if (trusted_proxies.empty()) { return false; }
|
|
|
|
|
return std::find(trusted_proxies.begin(), trusted_proxies.end(),
|
|
|
|
|
canonical_ip(addr)) != trusted_proxies.end();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve where a client should connect for `host`, honoring a user-supplied
|
|
|
|
|
// hostname-to-address map. `host` itself is never rewritten, so it keeps
|
|
|
|
|
// supplying the Host header and SNI; only the connection target changes.
|
|
|
|
@@ -13694,7 +13740,10 @@ inline bool Server::remove_mount_point(const std::string &mount_point) {
|
|
|
|
|
inline Server &
|
|
|
|
|
Server::set_file_extension_and_mimetype_mapping(const std::string &ext,
|
|
|
|
|
const std::string &mime) {
|
|
|
|
|
file_extension_and_mimetype_map_[ext] = mime;
|
|
|
|
|
// Keyed as find_content_type() looks it up
|
|
|
|
|
auto key = detail::case_ignore::to_lower(ext);
|
|
|
|
|
if (!key.empty() && key[0] == '.') { key.erase(0, 1); }
|
|
|
|
|
file_extension_and_mimetype_map_[key] = mime;
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -13802,7 +13851,11 @@ inline Server &Server::set_header_writer(
|
|
|
|
|
|
|
|
|
|
inline Server &
|
|
|
|
|
Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
|
|
|
|
|
trusted_proxies_ = proxies;
|
|
|
|
|
trusted_proxies_.clear();
|
|
|
|
|
trusted_proxies_.reserve(proxies.size());
|
|
|
|
|
for (const auto &proxy : proxies) {
|
|
|
|
|
trusted_proxies_.push_back(detail::canonical_ip(proxy));
|
|
|
|
|
}
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -13848,6 +13901,11 @@ inline Server &Server::set_payload_max_length(size_t length) {
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline Server &Server::set_compress(bool on) {
|
|
|
|
|
compress_ = on;
|
|
|
|
|
return *this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline Server &Server::set_static_file_compression(bool on) {
|
|
|
|
|
static_file_compression_ = on;
|
|
|
|
|
return *this;
|
|
|
|
@@ -14003,6 +14061,10 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|
|
|
|
need_apply_ranges = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Before anything below reads res.headers, which the handlers may have
|
|
|
|
|
// filled directly
|
|
|
|
|
detail::erase_invalid_fields(res.headers);
|
|
|
|
|
|
|
|
|
|
if (!detail::body_allowed_for_status(res.status)) {
|
|
|
|
|
detail::clear_content(res);
|
|
|
|
|
}
|
|
|
|
@@ -14046,6 +14108,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|
|
|
|
|
|
|
|
|
if (post_routing_handler_) {
|
|
|
|
|
post_routing_handler_(req, res);
|
|
|
|
|
detail::erase_invalid_fields(res.headers);
|
|
|
|
|
// Again, for a body or framing headers the handler changed.
|
|
|
|
|
detail::set_framing_headers(req, res, boundary, content_type);
|
|
|
|
|
}
|
|
|
|
@@ -14258,38 +14321,10 @@ inline bool Server::read_content_core(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RFC 9112 §6: no Transfer-Encoding and no Content-Length means no body.
|
|
|
|
|
// For non-SSL builds we still scan non-persistent connections for stray
|
|
|
|
|
// body bytes so the payload limit is enforced (413). On keep-alive,
|
|
|
|
|
// pending bytes may be the next request (issue #2450), so skip.
|
|
|
|
|
#if !defined(CPPHTTPLIB_SSL_ENABLED)
|
|
|
|
|
if (!req.has_header("Content-Length") &&
|
|
|
|
|
!detail::is_chunked_transfer_encoding(req.headers)) {
|
|
|
|
|
if (!detail::is_connection_persistent(req) && payload_max_length_ > 0 &&
|
|
|
|
|
payload_max_length_ < (std::numeric_limits<size_t>::max)()) {
|
|
|
|
|
auto has_data = strm.is_readable();
|
|
|
|
|
if (!has_data) {
|
|
|
|
|
auto s = strm.socket();
|
|
|
|
|
if (s != INVALID_SOCKET) {
|
|
|
|
|
has_data = detail::select_read(s, 0, 0) > 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (has_data) {
|
|
|
|
|
// Route through the same decompressing reader used by the
|
|
|
|
|
// length-framed and chunked paths below, so payload_max_length_ is
|
|
|
|
|
// enforced on the decompressed size here too instead of only on the
|
|
|
|
|
// compressed wire bytes.
|
|
|
|
|
return detail::read_content(strm, req, payload_max_length_, res.status,
|
|
|
|
|
nullptr, out, true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
if (!req.has_header("Content-Length") &&
|
|
|
|
|
!detail::is_chunked_transfer_encoding(req.headers)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
// A second read would start where the first one stopped, or in the next
|
|
|
|
|
// request.
|
|
|
|
@@ -14636,6 +14671,17 @@ inline bool Server::listen_internal() {
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifndef CPPHTTPLIB_NO_EXCEPTIONS
|
|
|
|
|
// Call from a catch block. The response is a 500 unless the exception handler
|
|
|
|
|
// sets another status. Returns whether the exception handler ran.
|
|
|
|
|
inline bool Server::handle_exception(Request &req, Response &res) {
|
|
|
|
|
res.status = StatusCode::InternalServerError_500;
|
|
|
|
|
if (!exception_handler_) { return false; }
|
|
|
|
|
exception_handler_(req, res, std::current_exception());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
inline bool Server::routing(Request &req, Response &res, Stream &strm,
|
|
|
|
|
bool skip_pre_routing) {
|
|
|
|
|
if (!skip_pre_routing && pre_routing_handler_ &&
|
|
|
|
@@ -14871,7 +14917,8 @@ inline void Server::apply_ranges(const Request &req, Response &res,
|
|
|
|
|
"multipart/byteranges; boundary=" + boundary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto type = detail::encoding_type(req, res);
|
|
|
|
|
auto type =
|
|
|
|
|
compress_ ? detail::encoding_type(req, res) : detail::EncodingType::None;
|
|
|
|
|
|
|
|
|
|
if (res.body.empty()) {
|
|
|
|
|
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
|
|
|
|
@@ -14978,12 +15025,7 @@ get_client_ip(const std::string &x_forwarded_for,
|
|
|
|
|
// returned as the client.
|
|
|
|
|
for (size_t i = ip_list.size(); i-- > 0;) {
|
|
|
|
|
const auto &ip = ip_list[i];
|
|
|
|
|
|
|
|
|
|
auto is_trusted_proxy =
|
|
|
|
|
std::any_of(trusted_proxies.begin(), trusted_proxies.end(),
|
|
|
|
|
[&](const std::string &proxy) { return ip == proxy; });
|
|
|
|
|
|
|
|
|
|
if (!is_trusted_proxy) { return ip; }
|
|
|
|
|
if (!detail::is_trusted_proxy(ip, trusted_proxies)) { return ip; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Every hop was a trusted proxy; fall back to the first entry.
|
|
|
|
@@ -15003,7 +15045,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
|
|
|
|
|
// The read timeout bounds the request line and headers as a whole, so a
|
|
|
|
|
// client sending them a byte at a time cannot hold the worker any longer.
|
|
|
|
|
strm.set_max_timeout(read_timeout_sec_ * 1000 + read_timeout_usec_ / 1000);
|
|
|
|
|
auto read_timeout_msec = read_timeout_sec_ * 1000 + read_timeout_usec_ / 1000;
|
|
|
|
|
strm.set_max_timeout(read_timeout_msec);
|
|
|
|
|
|
|
|
|
|
// Connection has been closed on client
|
|
|
|
|
if (!line_reader.getline()) { return false; }
|
|
|
|
@@ -15078,6 +15121,16 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
return write_response(strm, close_connection, req, res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RFC 9112 6.1: a lone "chunked" is the only transfer coding implemented
|
|
|
|
|
if (req.has_header("Transfer-Encoding") &&
|
|
|
|
|
(req.get_header_value_count("Transfer-Encoding") != 1 ||
|
|
|
|
|
!detail::case_ignore::equal(req.get_header_value("Transfer-Encoding"),
|
|
|
|
|
"chunked"))) {
|
|
|
|
|
connection_closed = true;
|
|
|
|
|
res.status = StatusCode::NotImplemented_501;
|
|
|
|
|
return write_response(strm, close_connection, req, res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RFC 9112 §6.1: a Transfer-Encoding alongside a Content-Length, or in an
|
|
|
|
|
// HTTP/1.0 request, is honoured, but the connection closes after the
|
|
|
|
|
// response.
|
|
|
|
@@ -15115,11 +15168,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
// Only honor X-Forwarded-For if the peer on the actual TCP connection is
|
|
|
|
|
// itself a trusted proxy. Otherwise any direct client could spoof
|
|
|
|
|
// remote_addr simply by sending an arbitrary X-Forwarded-For header.
|
|
|
|
|
auto is_trusted_peer = std::any_of(
|
|
|
|
|
trusted_proxies_.begin(), trusted_proxies_.end(),
|
|
|
|
|
[&](const std::string &proxy) { return proxy == remote_addr; });
|
|
|
|
|
|
|
|
|
|
if (is_trusted_peer && req.has_header("X-Forwarded-For")) {
|
|
|
|
|
if (detail::is_trusted_proxy(remote_addr, trusted_proxies_) &&
|
|
|
|
|
req.has_header("X-Forwarded-For")) {
|
|
|
|
|
// Some proxies append the address they observed as a separate
|
|
|
|
|
// X-Forwarded-For field line instead of extending the one the client sent
|
|
|
|
|
// (e.g. HAProxy's "option forwardfor"), so the whole combined value has to
|
|
|
|
@@ -15148,7 +15198,9 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.has_header("Range")) {
|
|
|
|
|
// RFC 9110 14.2: range handling is defined only for GET (and so for HEAD)
|
|
|
|
|
if ((req.method == "GET" || req.method == "HEAD") &&
|
|
|
|
|
req.has_header("Range")) {
|
|
|
|
|
const auto &range_header_value = req.get_header_value("Range");
|
|
|
|
|
if (!detail::parse_range_header(range_header_value, req.ranges)) {
|
|
|
|
|
connection_closed = true;
|
|
|
|
@@ -15204,27 +15256,22 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
auto close_if_answered =
|
|
|
|
|
close_after_response || detail::has_framed_body(req);
|
|
|
|
|
|
|
|
|
|
if (pre_routing_handler_ &&
|
|
|
|
|
pre_routing_handler_(req, res) == HandlerResponse::Handled) {
|
|
|
|
|
if (res.status == -1) { res.status = StatusCode::OK_200; }
|
|
|
|
|
return write_response_with_content(strm, close_if_answered, req, res);
|
|
|
|
|
}
|
|
|
|
|
// Find matching WebSocket handler
|
|
|
|
|
for (const auto &entry : websocket_handlers_) {
|
|
|
|
|
if (entry.matcher->match(req)) {
|
|
|
|
|
const WebSocketHandlerEntry *ws_route = nullptr;
|
|
|
|
|
std::string selected_subprotocol;
|
|
|
|
|
auto answer_before_upgrade = [&]() -> bool {
|
|
|
|
|
if (pre_routing_handler_ &&
|
|
|
|
|
pre_routing_handler_(req, res) == HandlerResponse::Handled) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
for (const auto &entry : websocket_handlers_) {
|
|
|
|
|
if (!entry.matcher->match(req)) { continue; }
|
|
|
|
|
req.matched_route = entry.matcher->pattern();
|
|
|
|
|
if (pre_request_handler_ &&
|
|
|
|
|
pre_request_handler_(req, res) == HandlerResponse::Handled) {
|
|
|
|
|
if (res.status == -1) { res.status = StatusCode::OK_200; }
|
|
|
|
|
return write_response_with_content(strm, close_if_answered, req, res);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
ws_route = &entry;
|
|
|
|
|
|
|
|
|
|
// Compute accept key
|
|
|
|
|
auto client_key = req.get_header_value("Sec-WebSocket-Key");
|
|
|
|
|
auto accept_key = detail::websocket_accept_key(client_key);
|
|
|
|
|
|
|
|
|
|
// Negotiate subprotocol
|
|
|
|
|
std::string selected_subprotocol;
|
|
|
|
|
if (entry.sub_protocol_selector) {
|
|
|
|
|
auto protocol_header = detail::get_combined_header_value(
|
|
|
|
|
req.headers, "Sec-WebSocket-Protocol");
|
|
|
|
@@ -15244,57 +15291,78 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Send 101 Switching Protocols
|
|
|
|
|
std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n"
|
|
|
|
|
"Upgrade: websocket\r\n"
|
|
|
|
|
"Connection: Upgrade\r\n"
|
|
|
|
|
"Sec-WebSocket-Accept: " +
|
|
|
|
|
accept_key + "\r\n";
|
|
|
|
|
if (!selected_subprotocol.empty()) {
|
|
|
|
|
if (!detail::fields::is_field_value(selected_subprotocol)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
handshake_response +=
|
|
|
|
|
"Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n";
|
|
|
|
|
}
|
|
|
|
|
handshake_response += "\r\n";
|
|
|
|
|
if (strm.write(handshake_response.data(), handshake_response.size()) <
|
|
|
|
|
0) {
|
|
|
|
|
auto answered = false;
|
|
|
|
|
#ifdef CPPHTTPLIB_NO_EXCEPTIONS
|
|
|
|
|
answered = answer_before_upgrade();
|
|
|
|
|
#else
|
|
|
|
|
try {
|
|
|
|
|
answered = answer_before_upgrade();
|
|
|
|
|
} catch (...) {
|
|
|
|
|
if (!handle_exception(req, res)) {
|
|
|
|
|
return write_response(strm, close_if_answered, req, res);
|
|
|
|
|
}
|
|
|
|
|
answered = true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
if (answered) {
|
|
|
|
|
if (res.status == -1) { res.status = StatusCode::OK_200; }
|
|
|
|
|
return write_response_with_content(strm, close_if_answered, req, res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ws_route) {
|
|
|
|
|
auto client_key = req.get_header_value("Sec-WebSocket-Key");
|
|
|
|
|
auto accept_key = detail::websocket_accept_key(client_key);
|
|
|
|
|
|
|
|
|
|
// Send 101 Switching Protocols
|
|
|
|
|
std::string handshake_response = "HTTP/1.1 101 Switching Protocols\r\n"
|
|
|
|
|
"Upgrade: websocket\r\n"
|
|
|
|
|
"Connection: Upgrade\r\n"
|
|
|
|
|
"Sec-WebSocket-Accept: " +
|
|
|
|
|
accept_key + "\r\n";
|
|
|
|
|
if (!selected_subprotocol.empty()) {
|
|
|
|
|
if (!detail::fields::is_field_value(selected_subprotocol)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
handshake_response +=
|
|
|
|
|
"Sec-WebSocket-Protocol: " + selected_subprotocol + "\r\n";
|
|
|
|
|
}
|
|
|
|
|
handshake_response += "\r\n";
|
|
|
|
|
if (strm.write(handshake_response.data(), handshake_response.size()) <
|
|
|
|
|
0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connection_closed = true;
|
|
|
|
|
if (websocket_upgraded) { *websocket_upgraded = true; }
|
|
|
|
|
connection_closed = true;
|
|
|
|
|
if (websocket_upgraded) { *websocket_upgraded = true; }
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
|
|
|
if (req.ssl) {
|
|
|
|
|
// wss: the heartbeat ping thread and the read path enter the same
|
|
|
|
|
// TLS session from different threads. Hand the WebSocket a stream
|
|
|
|
|
// that serializes every TLS call, so the shared SSLSocketStream on
|
|
|
|
|
// the plain HTTP/HTTPS paths stays untouched.
|
|
|
|
|
auto ws_strm =
|
|
|
|
|
std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
|
|
|
|
strm.socket(), const_cast<tls::session_t>(req.ssl),
|
|
|
|
|
CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
|
|
|
|
|
write_timeout_sec_, write_timeout_usec_));
|
|
|
|
|
ws::WebSocket ws(std::move(ws_strm), req, true,
|
|
|
|
|
websocket_ping_interval_sec_,
|
|
|
|
|
websocket_max_missed_pongs_);
|
|
|
|
|
entry.handler(req, ws);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
// Use WebSocket-specific read timeout instead of HTTP timeout
|
|
|
|
|
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND,
|
|
|
|
|
0);
|
|
|
|
|
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
|
|
|
|
|
websocket_max_missed_pongs_);
|
|
|
|
|
entry.handler(req, ws);
|
|
|
|
|
}
|
|
|
|
|
if (req.ssl) {
|
|
|
|
|
// wss: the heartbeat ping thread and the read path enter the same
|
|
|
|
|
// TLS session from different threads. Hand the WebSocket a stream
|
|
|
|
|
// that serializes every TLS call, so the shared SSLSocketStream on
|
|
|
|
|
// the plain HTTP/HTTPS paths stays untouched.
|
|
|
|
|
auto ws_strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
|
|
|
|
strm.socket(), const_cast<tls::session_t>(req.ssl),
|
|
|
|
|
CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
|
|
|
|
|
write_timeout_sec_, write_timeout_usec_));
|
|
|
|
|
ws::WebSocket ws(std::move(ws_strm), req, true,
|
|
|
|
|
websocket_ping_interval_sec_,
|
|
|
|
|
websocket_max_missed_pongs_);
|
|
|
|
|
ws_route->handler(req, ws);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
// Use WebSocket-specific read timeout instead of HTTP timeout
|
|
|
|
|
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0);
|
|
|
|
|
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
|
|
|
|
|
websocket_max_missed_pongs_);
|
|
|
|
|
ws_route->handler(req, ws);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
// No WebSocket route: route it as a plain HTTP request
|
|
|
|
|
}
|
|
|
|
@@ -15306,23 +15374,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
#else
|
|
|
|
|
try {
|
|
|
|
|
routed = routing(req, res, strm, is_upgrade);
|
|
|
|
|
} catch (std::exception &) {
|
|
|
|
|
if (exception_handler_) {
|
|
|
|
|
auto ep = std::current_exception();
|
|
|
|
|
exception_handler_(req, res, ep);
|
|
|
|
|
routed = true;
|
|
|
|
|
} else {
|
|
|
|
|
res.status = StatusCode::InternalServerError_500;
|
|
|
|
|
}
|
|
|
|
|
} catch (...) {
|
|
|
|
|
if (exception_handler_) {
|
|
|
|
|
auto ep = std::current_exception();
|
|
|
|
|
exception_handler_(req, res, ep);
|
|
|
|
|
routed = true;
|
|
|
|
|
} else {
|
|
|
|
|
res.status = StatusCode::InternalServerError_500;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (...) { routed = handle_exception(req, res); }
|
|
|
|
|
#endif
|
|
|
|
|
auto ret = false;
|
|
|
|
|
if (routed) {
|
|
|
|
@@ -15378,14 +15430,18 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|
|
|
|
// keep-alive. Without framing there is no body to drain — reading would
|
|
|
|
|
// consume the next request (issue #2450). If the response has committed the
|
|
|
|
|
// connection to close, there is no next request to protect. A body read in
|
|
|
|
|
// part cannot be drained from where it stopped, so the connection closes.
|
|
|
|
|
// part cannot be drained from where it stopped, so the connection closes,
|
|
|
|
|
// as it does when the body is longer than the drain limit or does not
|
|
|
|
|
// arrive within the read timeout.
|
|
|
|
|
if (!req.body_consumed_ && detail::has_framed_body(req) &&
|
|
|
|
|
!detail::has_header_token(res.headers, "Connection", "close")) {
|
|
|
|
|
strm.set_max_timeout(read_timeout_msec);
|
|
|
|
|
int dummy_status;
|
|
|
|
|
if (req.body_read_started_ ||
|
|
|
|
|
!detail::read_content(
|
|
|
|
|
strm, req, payload_max_length_, dummy_status, nullptr,
|
|
|
|
|
[](const char *, size_t, size_t, size_t) { return true; }, false)) {
|
|
|
|
|
strm, req, CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH, dummy_status,
|
|
|
|
|
nullptr, [](const char *, size_t, size_t, size_t) { return true; },
|
|
|
|
|
false)) {
|
|
|
|
|
connection_closed = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -15526,6 +15582,7 @@ inline bool ClientImpl::create_and_connect_socket(Socket &socket,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
|
|
|
|
|
socket_tls_settings_version_ = tls_settings_version_;
|
|
|
|
|
return create_and_connect_socket(socket, error);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -15575,6 +15632,28 @@ inline void ClientImpl::disconnect(bool gracefully) {
|
|
|
|
|
close_socket(socket_);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool ClientImpl::socket_reusable() {
|
|
|
|
|
if (!socket_.is_open()) { return false; }
|
|
|
|
|
|
|
|
|
|
auto is_alive = detail::is_socket_alive(socket_.sock);
|
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
|
|
|
if (is_alive && is_ssl() && tls::is_peer_closed(socket_.ssl, socket_.sock)) {
|
|
|
|
|
is_alive = false;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
if (!is_alive) {
|
|
|
|
|
// Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
|
|
|
|
|
disconnect(/*gracefully=*/false);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_ssl() && socket_tls_settings_version_ != tls_settings_version_) {
|
|
|
|
|
disconnect(/*gracefully=*/true);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
|
|
|
|
|
Response &res,
|
|
|
|
|
bool skip_100_continue) const {
|
|
|
|
@@ -15652,25 +15731,7 @@ inline bool ClientImpl::send_(Request &req, Response &res, Error &error) {
|
|
|
|
|
// socket.
|
|
|
|
|
socket_should_be_closed_when_request_is_done_ = false;
|
|
|
|
|
|
|
|
|
|
auto is_alive = false;
|
|
|
|
|
if (socket_.is_open()) {
|
|
|
|
|
is_alive = detail::is_socket_alive(socket_.sock);
|
|
|
|
|
|
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
|
|
|
if (is_alive && is_ssl()) {
|
|
|
|
|
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
|
|
|
|
|
is_alive = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
if (!is_alive) {
|
|
|
|
|
// Peer seems gone — non-graceful shutdown to avoid SIGPIPE.
|
|
|
|
|
disconnect(/*gracefully=*/false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_alive) {
|
|
|
|
|
if (!socket_reusable()) {
|
|
|
|
|
if (!ensure_socket_connection(socket_, error)) { return false; }
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
@@ -15833,20 +15894,7 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
|
|
|
|
|
{
|
|
|
|
|
std::lock_guard<std::mutex> guard(socket_mutex_);
|
|
|
|
|
|
|
|
|
|
auto is_alive = false;
|
|
|
|
|
if (socket_.is_open()) {
|
|
|
|
|
is_alive = detail::is_socket_alive(socket_.sock);
|
|
|
|
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
|
|
|
if (is_alive && is_ssl()) {
|
|
|
|
|
if (tls::is_peer_closed(socket_.ssl, socket_.sock)) {
|
|
|
|
|
is_alive = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
if (!is_alive) { disconnect(/*gracefully=*/false); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!is_alive) {
|
|
|
|
|
if (!socket_reusable()) {
|
|
|
|
|
if (!ensure_socket_connection(socket_, handle.error)) {
|
|
|
|
|
handle.response.reset();
|
|
|
|
|
return handle;
|
|
|
|
@@ -15894,8 +15942,11 @@ inline ClientImpl::StreamHandle ClientImpl::open_stream_(Request &req) {
|
|
|
|
|
{
|
|
|
|
|
detail::BufferStream bstrm;
|
|
|
|
|
|
|
|
|
|
if (detail::write_request_line(bstrm, req.method,
|
|
|
|
|
proxy_target_prefix() + req.path) < 0) {
|
|
|
|
|
// The path follows the authority in absolute-form, so one that does not
|
|
|
|
|
// begin with '/' (e.g. "@evil.example/x") would name another host.
|
|
|
|
|
auto prefix = proxy_target_prefix();
|
|
|
|
|
if ((!prefix.empty() && req.path[0] != '/') ||
|
|
|
|
|
detail::write_request_line(bstrm, req.method, prefix + req.path) < 0) {
|
|
|
|
|
handle.error = Error::Write;
|
|
|
|
|
handle.response.reset();
|
|
|
|
|
return handle;
|
|
|
|
@@ -16113,6 +16164,15 @@ inline ssize_t ChunkedDecoder::read_payload(char *buf, size_t len,
|
|
|
|
|
if (!is_space_or_tab(*p) && !fields::is_field_vchar(*p)) { return -1; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The bytes around the data are no payload, so they are counted on their
|
|
|
|
|
// own, as Go does: each chunk may spend 16 bytes and twice its data on
|
|
|
|
|
// them, which lets a stream send a byte at a time, and what goes past that
|
|
|
|
|
// in padding, leading zeros and extensions builds up to one header field.
|
|
|
|
|
overhead += lr.size() + 2;
|
|
|
|
|
overhead = overhead > 16 ? overhead - 16 : 0;
|
|
|
|
|
overhead = chunk_len > overhead / 2 ? 0 : overhead - 2 * chunk_len;
|
|
|
|
|
if (overhead > CPPHTTPLIB_HEADER_MAX_LENGTH) { return -1; }
|
|
|
|
|
|
|
|
|
|
if (chunk_len == 0) {
|
|
|
|
|
chunk_remaining = 0;
|
|
|
|
|
finished = true;
|
|
|
|
@@ -16191,6 +16251,13 @@ inline bool ClientImpl::handle_request(Stream &strm, Request &req,
|
|
|
|
|
|
|
|
|
|
auto prefix = proxy_target_prefix();
|
|
|
|
|
if (!prefix.empty()) {
|
|
|
|
|
// The path follows the authority in absolute-form, so one that does not
|
|
|
|
|
// begin with '/' (e.g. "@evil.example/x") would name another host.
|
|
|
|
|
if (req.path[0] != '/') {
|
|
|
|
|
error = Error::Write;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto req2 = req;
|
|
|
|
|
req2.path = prefix + req.path;
|
|
|
|
|
ret = process_request(strm, req2, res, close_connection, error);
|
|
|
|
@@ -18054,11 +18121,15 @@ inline void ClientImpl::set_proxy_digest_auth(const std::string &username,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void ClientImpl::enable_server_certificate_verification(bool enabled) {
|
|
|
|
|
if (server_certificate_verification_ == enabled) { return; }
|
|
|
|
|
server_certificate_verification_ = enabled;
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void ClientImpl::enable_server_hostname_verification(bool enabled) {
|
|
|
|
|
if (server_hostname_verification_ == enabled) { return; }
|
|
|
|
|
server_hostname_verification_ = enabled;
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void ClientImpl::enable_system_ca(bool enabled) {
|
|
|
|
@@ -19217,25 +19288,32 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
|
|
|
|
|
tls::free_ca_store(ca_cert_store);
|
|
|
|
|
}
|
|
|
|
|
custom_ca_loaded_ = true;
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void
|
|
|
|
|
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
|
|
|
|
|
if (!ctx_) { return; }
|
|
|
|
|
server_certificate_verifier_ = verifier;
|
|
|
|
|
// Under the lock that creating a session from ctx_ takes
|
|
|
|
|
std::lock_guard<std::mutex> guard(ctx_mutex_);
|
|
|
|
|
tls::set_verify_callback(ctx_, std::move(verifier));
|
|
|
|
|
{
|
|
|
|
|
// Under the lock that creating a session from ctx_ takes
|
|
|
|
|
std::lock_guard<std::mutex> guard(ctx_mutex_);
|
|
|
|
|
tls::set_verify_callback(ctx_, std::move(verifier));
|
|
|
|
|
}
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline void SSLClient::set_session_verifier(
|
|
|
|
|
std::function<SSLVerifierResponse(tls::session_t)> verifier) {
|
|
|
|
|
session_verifier_ = std::move(verifier);
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
|
|
|
|
inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
|
|
|
|
|
if (enable_windows_cert_verification_ == enabled) { return; }
|
|
|
|
|
enable_windows_cert_verification_ = enabled;
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
@@ -19250,6 +19328,7 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert,
|
|
|
|
|
tls::load_ca_pem(ctx_, ca_cert, size);
|
|
|
|
|
}
|
|
|
|
|
custom_ca_loaded_ = true;
|
|
|
|
|
tls_settings_changed();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inline bool SSLClient::load_certs() {
|
|
|
|
@@ -21966,24 +22045,23 @@ inline ca_store_t create_ca_store(const char *pem, size_t len) {
|
|
|
|
|
if (!detail::ensure_mbedtls_psa_crypto()) { return nullptr; }
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
if (!pem) { return nullptr; }
|
|
|
|
|
|
|
|
|
|
// mbedtls_x509_crt_parse expects null-terminated PEM
|
|
|
|
|
std::string pem_str(pem, len);
|
|
|
|
|
|
|
|
|
|
auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;
|
|
|
|
|
if (!ca_chain) { return nullptr; }
|
|
|
|
|
|
|
|
|
|
mbedtls_x509_crt_init(ca_chain);
|
|
|
|
|
|
|
|
|
|
// mbedtls_x509_crt_parse expects null-terminated PEM
|
|
|
|
|
int ret = mbedtls_x509_crt_parse(ca_chain,
|
|
|
|
|
reinterpret_cast<const unsigned char *>(pem),
|
|
|
|
|
len + 1); // +1 for null terminator
|
|
|
|
|
auto ret = mbedtls_x509_crt_parse(
|
|
|
|
|
ca_chain, reinterpret_cast<const unsigned char *>(pem_str.c_str()),
|
|
|
|
|
pem_str.size() + 1);
|
|
|
|
|
if (ret != 0) {
|
|
|
|
|
// Try without +1 in case PEM is already null-terminated
|
|
|
|
|
ret = mbedtls_x509_crt_parse(
|
|
|
|
|
ca_chain, reinterpret_cast<const unsigned char *>(pem), len);
|
|
|
|
|
if (ret != 0) {
|
|
|
|
|
mbedtls_x509_crt_free(ca_chain);
|
|
|
|
|
delete ca_chain;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
mbedtls_x509_crt_free(ca_chain);
|
|
|
|
|
delete ca_chain;
|
|
|
|
|
return nullptr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return static_cast<ca_store_t>(ca_chain);
|
|
|
|
|