mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-22 11:46:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a0ca8b35 | ||
|
|
b83b385ed4 | ||
|
|
f297616584 | ||
|
|
1e35f39675 | ||
|
|
e121385910 | ||
|
|
fc37d3eae1 | ||
|
|
3b8b9cf9d5 | ||
|
|
3599533c24 | ||
|
|
247a2abe4a |
No files matched your search
@@ -102,7 +102,7 @@ cli.enable_server_hostname_verification(false);
|
|||||||
```
|
```
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> The CA certificates are loaded when the client makes its first connection. Changing `set_ca_cert_path()` or `enable_system_ca()` after that has no effect, so make the settings before the first request, or use a new client. `set_ca_cert_store()` and `load_ca_cert_store()` take effect at once. `WebSocketClient` works the same way.
|
> The CA certificates are loaded when the client makes its first connection. Changing `set_ca_cert_path()` or `enable_system_ca()` after that has no effect, so make the settings before the first request, or use a new client. `set_ca_cert_store()` and `load_ca_cert_store()` take effect at once. `WebSocketClient` works the same way. On `Client` and `SSLClient`, a setter that changes a TLS setting also closes a connection kept alive under the old one, so the next request connects again.
|
||||||
|
|
||||||
### SSL Error Handling
|
### SSL Error Handling
|
||||||
|
|
||||||
@@ -916,6 +916,14 @@ svr.set_keep_alive_max_count(2); // Default is 100, 0 for no limit
|
|||||||
svr.set_keep_alive_timeout(10); // Default is 5
|
svr.set_keep_alive_timeout(10); // Default is 5
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Client address behind a reverse proxy
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_trusted_proxies({"10.0.0.2"});
|
||||||
|
```
|
||||||
|
|
||||||
|
A request that comes straight from one of these addresses gets `req.remote_addr` from `X-Forwarded-For`: the rightmost address in it that is not a trusted proxy. A request from anywhere else keeps the address of its connection, whatever `X-Forwarded-For` says. Addresses match as IP addresses, so an IPv4 peer that a server listening on both IPv4 and IPv6 sees as `::ffff:10.0.0.2` matches `10.0.0.2`. Host names and CIDR ranges are not supported.
|
||||||
|
|
||||||
### Timeout
|
### Timeout
|
||||||
|
|
||||||
```c++
|
```c++
|
||||||
@@ -1449,7 +1457,7 @@ res->status; // 200
|
|||||||
|
|
||||||
A URL with userinfo (`http://user:pass@host/`) is not supported, neither for a `Client` nor in a `Location`; use `set_basic_auth()` for the credentials. A `Client` constructed from a URL it cannot use, such as one with userinfo, a backslash in the authority or an invalid port, is not valid: `is_valid()` returns `false`, and every request fails with `Error::Connection`.
|
A URL with userinfo (`http://user:pass@host/`) is not supported, neither for a `Client` nor in a `Location`; use `set_basic_auth()` for the credentials. A `Client` constructed from a URL it cannot use, such as one with userinfo, a backslash in the authority or an invalid port, is not valid: `is_valid()` returns `false`, and every request fails with `Error::Connection`.
|
||||||
|
|
||||||
On a redirect to another origin (a different scheme, host or port), `Authorization`, `Cookie` and `Proxy-Authorization` are dropped along with the credentials set with `set_basic_auth()`, `set_bearer_token_auth()` and `set_digest_auth()`, and the client certificate is not presented. Other headers, the default ones included, go along (RFC 9110 §15.4), and so do the timeouts, the payload limit and the TLS verification settings. A `303` that turns a request into a `GET` drops its headers and body; only on the same origin are the default headers sent again.
|
On a redirect to another origin (a different scheme, host or port), `Authorization`, `Cookie` and `Proxy-Authorization` are dropped along with the credentials set with `set_basic_auth()`, `set_bearer_token_auth()` and `set_digest_auth()`, and the client certificate is not presented. Other headers, the default ones included, go along (RFC 9110 §15.4), and so do the timeouts, the payload limit and the TLS verification settings. The TLS settings pass only from one HTTPS hop to the next: an HTTPS hop that follows a plain HTTP one starts over with the defaults, verifying against the system CAs with no verifier set. A `303` that turns a request into a `GET` drops its headers and body; only on the same origin are the default headers sent again.
|
||||||
|
|
||||||
Up to `CPPHTTPLIB_REDIRECT_MAX_COUNT` (20 by default) redirects are followed for a request; one more fails it with `Error::ExceedRedirectCount`. Interim `1xx` responses before the final response are skipped, up to `CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT` (100 by default); one more fails the request with `Error::Read`.
|
Up to `CPPHTTPLIB_REDIRECT_MAX_COUNT` (20 by default) redirects are followed for a request; one more fails it with `Error::ExceedRedirectCount`. Interim `1xx` responses before the final response are skipped, up to `CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT` (100 by default); one more fails the request with `Error::Read`.
|
||||||
|
|
||||||
@@ -1590,6 +1598,8 @@ This holds for every kind of response, including the file-backed ones below.
|
|||||||
|
|
||||||
`Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
|
`Vary: Accept-Encoding` is added only to responses the server encoded itself. A handler that chooses between an encoded and an identity representation by reading `Accept-Encoding` should set the field itself, so that shared caches keep the two apart.
|
||||||
|
|
||||||
|
Turn this compression off with `svr.set_compress(false)`. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). A server whose responses mix the two should turn compression off, or keep such secrets out of those responses. Static file compression, below, is set separately.
|
||||||
|
|
||||||
### Static file compression
|
### Static file compression
|
||||||
|
|
||||||
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
|
Responses served from a file, whether through `set_mount_point()` or `Response::set_file_content()`, are sent as is by default. Turn compression on for them with:
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Turning it off
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_compress(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
Handler responses, buffered or streamed, are then sent as they are. Over HTTPS, the length of a compressed response can give away a secret in it, such as a CSRF token, when the same response also carries text an attacker can choose (the BREACH attack). Static file compression is set separately.
|
||||||
|
|
||||||
## Static files need to be opted in
|
## Static files need to be opted in
|
||||||
|
|
||||||
Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:
|
Files served as they are, through `set_mount_point()` or `Response::set_file_content()`, are not compressed by default. Turn it on with:
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ svr.Get("/events", [](const httplib::Request &req, httplib::Response &res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 圧縮を止める
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
svr.set_compress(false);
|
||||||
|
```
|
||||||
|
|
||||||
|
ハンドラのレスポンスは、通常のボディもストリーミングも、そのまま送られます。HTTPSでは、攻撃者が選べる文字列とCSRFトークンなどの秘密が同じレスポンスに載ると、圧縮後の長さから秘密が漏れることがあります(BREACH攻撃)。静的ファイルの圧縮は別に設定します。
|
||||||
|
|
||||||
## 静的ファイルは明示的に有効にする
|
## 静的ファイルは明示的に有効にする
|
||||||
|
|
||||||
`set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
|
`set_mount_point()`や`Response::set_file_content()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
|
||||||
|
|||||||
@@ -2357,6 +2357,8 @@ public:
|
|||||||
|
|
||||||
Server &set_payload_max_length(size_t length);
|
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(bool on);
|
||||||
Server &set_static_file_compression_min_length(size_t length);
|
Server &set_static_file_compression_min_length(size_t length);
|
||||||
Server &set_static_file_compression_max_length(size_t length);
|
Server &set_static_file_compression_max_length(size_t length);
|
||||||
@@ -2431,6 +2433,7 @@ protected:
|
|||||||
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
|
time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND;
|
||||||
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
|
time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND;
|
||||||
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
||||||
|
bool compress_ = true;
|
||||||
bool static_file_compression_ = false;
|
bool static_file_compression_ = false;
|
||||||
size_t static_file_compression_min_length_ =
|
size_t static_file_compression_min_length_ =
|
||||||
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
|
CPPHTTPLIB_STATIC_FILE_COMPRESSION_MIN_LENGTH;
|
||||||
@@ -2997,6 +3000,15 @@ protected:
|
|||||||
void close_socket(Socket &socket);
|
void close_socket(Socket &socket);
|
||||||
void disconnect(bool gracefully);
|
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;
|
std::string make_request_target(const Request &req) const;
|
||||||
bool process_request(Stream &strm, Request &req, Response &res,
|
bool process_request(Stream &strm, Request &req, Response &res,
|
||||||
bool close_connection, Error &error);
|
bool close_connection, Error &error);
|
||||||
@@ -3023,6 +3035,9 @@ protected:
|
|||||||
size_t socket_requests_in_flight_ = 0;
|
size_t socket_requests_in_flight_ = 0;
|
||||||
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
|
std::thread::id socket_requests_are_from_thread_ = std::thread::id();
|
||||||
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;
|
||||||
|
|
||||||
|
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
|
||||||
// hostname; only the connection target changes, never the identity.
|
// hostname; only the connection target changes, never the identity.
|
||||||
@@ -4019,6 +4034,7 @@ struct ChunkedDecoder {
|
|||||||
char line_buf[64];
|
char line_buf[64];
|
||||||
size_t last_chunk_total = 0;
|
size_t last_chunk_total = 0;
|
||||||
size_t last_chunk_offset = 0;
|
size_t last_chunk_offset = 0;
|
||||||
|
size_t overhead = 0;
|
||||||
|
|
||||||
explicit ChunkedDecoder(Stream &s);
|
explicit ChunkedDecoder(Stream &s);
|
||||||
|
|
||||||
@@ -8418,6 +8434,18 @@ inline void set_header(Headers &headers, const std::string &key,
|
|||||||
if (fields::is_field_valid(key, val)) { headers.emplace(key, val); }
|
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) {
|
inline bool read_headers(Stream &strm, Headers &headers) {
|
||||||
const auto bufsiz = 2048;
|
const auto bufsiz = 2048;
|
||||||
char buf[bufsiz];
|
char buf[bufsiz];
|
||||||
@@ -8895,13 +8923,6 @@ inline ssize_t write_response_line(Stream &strm, int status) {
|
|||||||
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
|
inline ssize_t write_headers(Stream &strm, const Headers &headers) {
|
||||||
ssize_t write_len = 0;
|
ssize_t write_len = 0;
|
||||||
for (const auto &x : headers) {
|
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;
|
std::string s;
|
||||||
s = x.first;
|
s = x.first;
|
||||||
s += ": ";
|
s += ": ";
|
||||||
@@ -10649,6 +10670,32 @@ inline bool is_ip_address(const std::string &host) {
|
|||||||
inet_pton(AF_INET6, host.c_str(), &addr6) == 1;
|
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
|
// Resolve where a client should connect for `host`, honoring a user-supplied
|
||||||
// hostname-to-address map. `host` itself is never rewritten, so it keeps
|
// hostname-to-address map. `host` itself is never rewritten, so it keeps
|
||||||
// supplying the Host header and SNI; only the connection target changes.
|
// supplying the Host header and SNI; only the connection target changes.
|
||||||
@@ -13802,7 +13849,11 @@ inline Server &Server::set_header_writer(
|
|||||||
|
|
||||||
inline Server &
|
inline Server &
|
||||||
Server::set_trusted_proxies(const std::vector<std::string> &proxies) {
|
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;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13848,6 +13899,11 @@ inline Server &Server::set_payload_max_length(size_t length) {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Server &Server::set_compress(bool on) {
|
||||||
|
compress_ = on;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
inline Server &Server::set_static_file_compression(bool on) {
|
inline Server &Server::set_static_file_compression(bool on) {
|
||||||
static_file_compression_ = on;
|
static_file_compression_ = on;
|
||||||
return *this;
|
return *this;
|
||||||
@@ -14003,6 +14059,10 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
need_apply_ranges = true;
|
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)) {
|
if (!detail::body_allowed_for_status(res.status)) {
|
||||||
detail::clear_content(res);
|
detail::clear_content(res);
|
||||||
}
|
}
|
||||||
@@ -14046,6 +14106,7 @@ inline bool Server::write_response_core(Stream &strm, bool close_connection,
|
|||||||
|
|
||||||
if (post_routing_handler_) {
|
if (post_routing_handler_) {
|
||||||
post_routing_handler_(req, res);
|
post_routing_handler_(req, res);
|
||||||
|
detail::erase_invalid_fields(res.headers);
|
||||||
// Again, for a body or framing headers the handler changed.
|
// Again, for a body or framing headers the handler changed.
|
||||||
detail::set_framing_headers(req, res, boundary, content_type);
|
detail::set_framing_headers(req, res, boundary, content_type);
|
||||||
}
|
}
|
||||||
@@ -14871,7 +14932,8 @@ inline void Server::apply_ranges(const Request &req, Response &res,
|
|||||||
"multipart/byteranges; boundary=" + boundary);
|
"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.body.empty()) {
|
||||||
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
|
if (res.content_length_ > 0 && req.ranges.size() == 1 &&
|
||||||
@@ -14978,12 +15040,7 @@ get_client_ip(const std::string &x_forwarded_for,
|
|||||||
// returned as the client.
|
// returned as the client.
|
||||||
for (size_t i = ip_list.size(); i-- > 0;) {
|
for (size_t i = ip_list.size(); i-- > 0;) {
|
||||||
const auto &ip = ip_list[i];
|
const auto &ip = ip_list[i];
|
||||||
|
if (!detail::is_trusted_proxy(ip, trusted_proxies)) { return ip; }
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every hop was a trusted proxy; fall back to the first entry.
|
// Every hop was a trusted proxy; fall back to the first entry.
|
||||||
@@ -15115,11 +15172,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
|
// Only honor X-Forwarded-For if the peer on the actual TCP connection is
|
||||||
// itself a trusted proxy. Otherwise any direct client could spoof
|
// itself a trusted proxy. Otherwise any direct client could spoof
|
||||||
// remote_addr simply by sending an arbitrary X-Forwarded-For header.
|
// remote_addr simply by sending an arbitrary X-Forwarded-For header.
|
||||||
auto is_trusted_peer = std::any_of(
|
if (detail::is_trusted_proxy(remote_addr, trusted_proxies_) &&
|
||||||
trusted_proxies_.begin(), trusted_proxies_.end(),
|
req.has_header("X-Forwarded-For")) {
|
||||||
[&](const std::string &proxy) { return proxy == remote_addr; });
|
|
||||||
|
|
||||||
if (is_trusted_peer && req.has_header("X-Forwarded-For")) {
|
|
||||||
// Some proxies append the address they observed as a separate
|
// Some proxies append the address they observed as a separate
|
||||||
// X-Forwarded-For field line instead of extending the one the client sent
|
// 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
|
// (e.g. HAProxy's "option forwardfor"), so the whole combined value has to
|
||||||
@@ -15148,7 +15202,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");
|
const auto &range_header_value = req.get_header_value("Range");
|
||||||
if (!detail::parse_range_header(range_header_value, req.ranges)) {
|
if (!detail::parse_range_header(range_header_value, req.ranges)) {
|
||||||
connection_closed = true;
|
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) {
|
inline bool ClientImpl::ensure_socket_connection(Socket &socket, Error &error) {
|
||||||
|
socket_tls_settings_version_ = tls_settings_version_;
|
||||||
return create_and_connect_socket(socket, error);
|
return create_and_connect_socket(socket, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15575,6 +15632,28 @@ inline void ClientImpl::disconnect(bool gracefully) {
|
|||||||
close_socket(socket_);
|
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,
|
inline bool ClientImpl::read_response_line(Stream &strm, const Request &req,
|
||||||
Response &res,
|
Response &res,
|
||||||
bool skip_100_continue) const {
|
bool skip_100_continue) const {
|
||||||
@@ -15652,25 +15731,7 @@ 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;
|
||||||
|
|
||||||
auto is_alive = false;
|
if (!socket_reusable()) {
|
||||||
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 (!ensure_socket_connection(socket_, error)) { return false; }
|
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_);
|
std::lock_guard<std::mutex> guard(socket_mutex_);
|
||||||
|
|
||||||
auto is_alive = false;
|
if (!socket_reusable()) {
|
||||||
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 (!ensure_socket_connection(socket_, handle.error)) {
|
if (!ensure_socket_connection(socket_, handle.error)) {
|
||||||
handle.response.reset();
|
handle.response.reset();
|
||||||
return handle;
|
return handle;
|
||||||
@@ -16113,6 +16161,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; }
|
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) {
|
if (chunk_len == 0) {
|
||||||
chunk_remaining = 0;
|
chunk_remaining = 0;
|
||||||
finished = true;
|
finished = true;
|
||||||
@@ -18054,11 +18111,15 @@ inline void ClientImpl::set_proxy_digest_auth(const std::string &username,
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline void ClientImpl::enable_server_certificate_verification(bool enabled) {
|
inline void ClientImpl::enable_server_certificate_verification(bool enabled) {
|
||||||
|
if (server_certificate_verification_ == enabled) { return; }
|
||||||
server_certificate_verification_ = enabled;
|
server_certificate_verification_ = enabled;
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void ClientImpl::enable_server_hostname_verification(bool enabled) {
|
inline void ClientImpl::enable_server_hostname_verification(bool enabled) {
|
||||||
|
if (server_hostname_verification_ == enabled) { return; }
|
||||||
server_hostname_verification_ = enabled;
|
server_hostname_verification_ = enabled;
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void ClientImpl::enable_system_ca(bool enabled) {
|
inline void ClientImpl::enable_system_ca(bool enabled) {
|
||||||
@@ -19217,25 +19278,32 @@ inline void SSLClient::set_ca_cert_store(tls::ca_store_t ca_cert_store) {
|
|||||||
tls::free_ca_store(ca_cert_store);
|
tls::free_ca_store(ca_cert_store);
|
||||||
}
|
}
|
||||||
custom_ca_loaded_ = true;
|
custom_ca_loaded_ = true;
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void
|
inline void
|
||||||
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
|
SSLClient::set_server_certificate_verifier(tls::VerifyCallback verifier) {
|
||||||
if (!ctx_) { return; }
|
if (!ctx_) { return; }
|
||||||
server_certificate_verifier_ = verifier;
|
server_certificate_verifier_ = verifier;
|
||||||
|
{
|
||||||
// Under the lock that creating a session from ctx_ takes
|
// Under the lock that creating a session from ctx_ takes
|
||||||
std::lock_guard<std::mutex> guard(ctx_mutex_);
|
std::lock_guard<std::mutex> guard(ctx_mutex_);
|
||||||
tls::set_verify_callback(ctx_, std::move(verifier));
|
tls::set_verify_callback(ctx_, std::move(verifier));
|
||||||
|
}
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void SSLClient::set_session_verifier(
|
inline void SSLClient::set_session_verifier(
|
||||||
std::function<SSLVerifierResponse(tls::session_t)> verifier) {
|
std::function<SSLVerifierResponse(tls::session_t)> verifier) {
|
||||||
session_verifier_ = std::move(verifier);
|
session_verifier_ = std::move(verifier);
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
#ifdef CPPHTTPLIB_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE
|
||||||
inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
|
inline void SSLClient::enable_windows_certificate_verification(bool enabled) {
|
||||||
|
if (enable_windows_cert_verification_ == enabled) { return; }
|
||||||
enable_windows_cert_verification_ = enabled;
|
enable_windows_cert_verification_ = enabled;
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -19250,6 +19318,7 @@ inline void SSLClient::load_ca_cert_store(const char *ca_cert,
|
|||||||
tls::load_ca_pem(ctx_, ca_cert, size);
|
tls::load_ca_pem(ctx_, ca_cert, size);
|
||||||
}
|
}
|
||||||
custom_ca_loaded_ = true;
|
custom_ca_loaded_ = true;
|
||||||
|
tls_settings_changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool SSLClient::load_certs() {
|
inline bool SSLClient::load_certs() {
|
||||||
@@ -21966,25 +22035,24 @@ inline ca_store_t create_ca_store(const char *pem, size_t len) {
|
|||||||
if (!detail::ensure_mbedtls_psa_crypto()) { return nullptr; }
|
if (!detail::ensure_mbedtls_psa_crypto()) { return nullptr; }
|
||||||
#endif
|
#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;
|
auto *ca_chain = new (std::nothrow) mbedtls_x509_crt;
|
||||||
if (!ca_chain) { return nullptr; }
|
if (!ca_chain) { return nullptr; }
|
||||||
|
|
||||||
mbedtls_x509_crt_init(ca_chain);
|
mbedtls_x509_crt_init(ca_chain);
|
||||||
|
|
||||||
// mbedtls_x509_crt_parse expects null-terminated PEM
|
auto ret = mbedtls_x509_crt_parse(
|
||||||
int ret = mbedtls_x509_crt_parse(ca_chain,
|
ca_chain, reinterpret_cast<const unsigned char *>(pem_str.c_str()),
|
||||||
reinterpret_cast<const unsigned char *>(pem),
|
pem_str.size() + 1);
|
||||||
len + 1); // +1 for null terminator
|
|
||||||
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) {
|
if (ret != 0) {
|
||||||
mbedtls_x509_crt_free(ca_chain);
|
mbedtls_x509_crt_free(ca_chain);
|
||||||
delete ca_chain;
|
delete ca_chain;
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return static_cast<ca_store_t>(ca_chain);
|
return static_cast<ca_store_t>(ca_chain);
|
||||||
}
|
}
|
||||||
|
|||||||
+269
@@ -2742,6 +2742,31 @@ TEST(RangeTest, FromHTTPBin_Online) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RangeTest, IgnoredForPost) {
|
||||||
|
Server svr;
|
||||||
|
svr.Post("/", [](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 : {"bytes=1-2", "bytes=x"}) {
|
||||||
|
auto res = cli.Post("/", 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
|
TEST(GetAddrInfoDanglingRefTest, LongTimeout) {
|
||||||
auto host = "unresolvableaddress.local";
|
auto host = "unresolvableaddress.local";
|
||||||
auto path = std::string{"/"};
|
auto path = std::string{"/"};
|
||||||
@@ -8073,6 +8098,25 @@ TEST_F(ServerTest, RejectsControlCharacterInChunkExtension) {
|
|||||||
"b\r\ndech\r\nf\r\nunked post body\r\n0\r\n\r\n");
|
"b\r\ndech\r\nf\r\nunked post body\r\n0\r\n\r\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The bytes around chunk data are no payload, so they cannot ride on the
|
||||||
|
// payload limit
|
||||||
|
TEST_F(ServerTest, RejectsChunkOverheadPastItsLimit) {
|
||||||
|
// Small enough that the whole request is sent before the server turns it
|
||||||
|
// down, yet past the limit by the last chunks
|
||||||
|
auto pad = std::string(CPPHTTPLIB_HEADER_MAX_LENGTH / 16, 'x');
|
||||||
|
auto zeros = std::string(CPPHTTPLIB_HEADER_MAX_LENGTH / 16, '0');
|
||||||
|
auto spaces = std::string(CPPHTTPLIB_HEADER_MAX_LENGTH / 16, ' ');
|
||||||
|
for (const auto &size_line : {"1;" + pad, zeros + "1", "1" + spaces}) {
|
||||||
|
SCOPED_TRACE(size_line.substr(0, 2));
|
||||||
|
std::string body;
|
||||||
|
for (auto c : std::string("dechunked post body")) {
|
||||||
|
body += size_line + "\r\n" + c + "\r\n";
|
||||||
|
}
|
||||||
|
body += "0\r\n\r\n";
|
||||||
|
expect_chunked_body_rejected(cli_, body.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(ServerTest, AcceptsChunkExtension) {
|
TEST_F(ServerTest, AcceptsChunkExtension) {
|
||||||
expect_chunked_body_status(cli_,
|
expect_chunked_body_status(cli_,
|
||||||
"4;name=value\r\ndech\r\n"
|
"4;name=value\r\ndech\r\n"
|
||||||
@@ -11125,6 +11169,37 @@ TEST(HeadersOrderTest, SentFieldsKeepTheirOrder) {
|
|||||||
EXPECT_EQ("second=2", res->get_header_value("Set-Cookie", "", 1));
|
EXPECT_EQ("second=2", res->get_header_value("Set-Cookie", "", 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A stream may send a byte at a time with a small extension on each chunk,
|
||||||
|
// however long it runs
|
||||||
|
TEST(ChunkOverheadTest, AcceptsSmallOverheadOnEveryChunk) {
|
||||||
|
Server svr;
|
||||||
|
svr.Post("/count", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(std::to_string(req.body.size()), "text/plain");
|
||||||
|
});
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
ASSERT_LT(0, port);
|
||||||
|
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();
|
||||||
|
|
||||||
|
std::string req = std::string("POST /count HTTP/1.1\r\nHost: ") + HOST +
|
||||||
|
"\r\nTransfer-Encoding: chunked\r\n"
|
||||||
|
"Connection: close\r\n\r\n";
|
||||||
|
for (auto i = 0; i < 4096; i++) {
|
||||||
|
req += "1;a=b\r\nx\r\n";
|
||||||
|
}
|
||||||
|
req += "0\r\n\r\n";
|
||||||
|
|
||||||
|
std::string res;
|
||||||
|
ASSERT_TRUE(send_request(5, req, &res, port));
|
||||||
|
EXPECT_EQ(0u, res.find("HTTP/1.1 200 OK\r\n"));
|
||||||
|
EXPECT_EQ("4096", res.substr(res.size() - 4));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ServerResponseSplittingTest, ChunkedTrailerCRLFInjection) {
|
TEST(ServerResponseSplittingTest, ChunkedTrailerCRLFInjection) {
|
||||||
Server svr;
|
Server svr;
|
||||||
svr.Get("/injected-trailer", [&](const Request & /*req*/, Response &res) {
|
svr.Get("/injected-trailer", [&](const Request & /*req*/, Response &res) {
|
||||||
@@ -11183,7 +11258,23 @@ TEST(ServerResponseSplittingTest, ChunkedTrailerCRLFInjection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(ServerResponseSplittingTest, ResponseHeaderCRLFInjection) {
|
TEST(ServerResponseSplittingTest, ResponseHeaderCRLFInjection) {
|
||||||
|
// A custom header writer gets only the fields the default one writes
|
||||||
|
auto verbatim_writer = [](Stream &strm, Headers &hdrs) -> ssize_t {
|
||||||
|
std::string s;
|
||||||
|
for (const auto &h : hdrs) {
|
||||||
|
s += h.first + ": " + h.second + "\r\n";
|
||||||
|
}
|
||||||
|
s += "\r\n";
|
||||||
|
return strm.write(s.data(), s.size());
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto custom_writer : {false, true}) {
|
||||||
|
SCOPED_TRACE(custom_writer ? "custom header writer" : "default writer");
|
||||||
Server svr;
|
Server svr;
|
||||||
|
if (custom_writer) { svr.set_header_writer(verbatim_writer); }
|
||||||
|
svr.set_post_routing_handler([](const Request &, Response &res) {
|
||||||
|
res.headers.emplace("X-Late", "late\r\nInjected-Late: pwned");
|
||||||
|
});
|
||||||
svr.Get("/injected-header", [&](const Request & /*req*/, Response &res) {
|
svr.Get("/injected-header", [&](const Request & /*req*/, Response &res) {
|
||||||
// res.headers is a public field an application can populate directly,
|
// res.headers is a public field an application can populate directly,
|
||||||
// bypassing set_header()'s validation (e.g. reflecting a request value).
|
// bypassing set_header()'s validation (e.g. reflecting a request value).
|
||||||
@@ -11219,12 +11310,14 @@ TEST(ServerResponseSplittingTest, ResponseHeaderCRLFInjection) {
|
|||||||
EXPECT_EQ(std::string::npos, res.find("Injected-Header"));
|
EXPECT_EQ(std::string::npos, res.find("Injected-Header"));
|
||||||
EXPECT_EQ(std::string::npos, res.find("X-Evil"));
|
EXPECT_EQ(std::string::npos, res.find("X-Evil"));
|
||||||
EXPECT_EQ(std::string::npos, res.find("Set-Cookie"));
|
EXPECT_EQ(std::string::npos, res.find("Set-Cookie"));
|
||||||
|
EXPECT_EQ(std::string::npos, res.find("Injected-Late"));
|
||||||
|
|
||||||
// Invalid headers are dropped entirely, matching set_header().
|
// Invalid headers are dropped entirely, matching set_header().
|
||||||
EXPECT_EQ(std::string::npos, res.find("X-Reflected:"));
|
EXPECT_EQ(std::string::npos, res.find("X-Reflected:"));
|
||||||
|
|
||||||
// The valid header still passes through.
|
// The valid header still passes through.
|
||||||
EXPECT_NE(std::string::npos, res.find("X-Safe: safe"));
|
EXPECT_NE(std::string::npos, res.find("X-Safe: safe"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward declaration: in split builds split.py strips `inline` and moves the
|
// Forward declaration: in split builds split.py strips `inline` and moves the
|
||||||
@@ -14842,6 +14935,45 @@ TEST(ContentEncodingTest, PreEncodedResponseIsNotCompressedAgain) {
|
|||||||
EXPECT_EQ("Hello World!", res->body);
|
EXPECT_EQ("Hello World!", res->body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set_compress(false) sends every handler response as it is, buffered or
|
||||||
|
// streamed
|
||||||
|
TEST(ContentEncodingTest, SetCompressFalseSendsIdentity) {
|
||||||
|
const std::string text(1000, 'a');
|
||||||
|
|
||||||
|
Server svr;
|
||||||
|
svr.set_compress(false);
|
||||||
|
svr.Get("/buffered", [&](const Request &, Response &res) {
|
||||||
|
res.set_content(text, "text/plain");
|
||||||
|
});
|
||||||
|
svr.Get("/streamed", [&](const Request &, Response &res) {
|
||||||
|
res.set_chunked_content_provider("text/plain", [&](size_t, DataSink &sink) {
|
||||||
|
sink.write(text.data(), text.size());
|
||||||
|
sink.done();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
ASSERT_LT(0, port);
|
||||||
|
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);
|
||||||
|
cli.set_decompress(false);
|
||||||
|
for (auto path : {"/buffered", "/streamed"}) {
|
||||||
|
SCOPED_TRACE(path);
|
||||||
|
auto res = cli.Get(path, Headers{{"Accept-Encoding", "gzip"}});
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_FALSE(res->has_header("Content-Encoding"));
|
||||||
|
EXPECT_EQ(text, res->body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A chunked provider settles its coding where the headers are written and
|
// A chunked provider settles its coding where the headers are written and
|
||||||
// reuses it at write time. Both ends of that have to hold: a handler's own
|
// reuses it at write time. Both ends of that have to hold: a handler's own
|
||||||
// Content-Encoding suppresses the coding, and a response without one is still
|
// Content-Encoding suppresses the coding, and a response without one is still
|
||||||
@@ -18238,6 +18370,94 @@ TEST(SSLClientTest, EmptyCustomCaKeepsSystemCasOut) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// A TLS setting changed between requests applies to the next one, not just to
|
||||||
|
// the next connection
|
||||||
|
TEST(SSLClientTest, TlsSettingChangeClosesKeptAliveConnection) {
|
||||||
|
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||||
|
ASSERT_TRUE(svr.is_valid());
|
||||||
|
svr.Get("/", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(std::to_string(req.remote_port), "text/plain");
|
||||||
|
});
|
||||||
|
auto port = svr.bind_to_any_port("127.0.0.1");
|
||||||
|
ASSERT_LT(0, port);
|
||||||
|
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();
|
||||||
|
|
||||||
|
SSLClient cli("127.0.0.1", port);
|
||||||
|
cli.set_keep_alive(true);
|
||||||
|
cli.set_ca_cert_path(CLIENT_CA_CERT_FILE); // does not trust cert2.pem
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
|
||||||
|
// The port the request went out from, so a new one means a new connection
|
||||||
|
auto client_port = [&] {
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
return res ? res->body : std::string();
|
||||||
|
};
|
||||||
|
auto last = client_port();
|
||||||
|
ASSERT_FALSE(last.empty());
|
||||||
|
|
||||||
|
// Setting a value it already has keeps the connection
|
||||||
|
cli.enable_server_certificate_verification(false);
|
||||||
|
cli.enable_server_hostname_verification(true);
|
||||||
|
EXPECT_EQ(last, client_port());
|
||||||
|
|
||||||
|
cli.enable_server_certificate_verification(true);
|
||||||
|
auto res = cli.Get("/");
|
||||||
|
EXPECT_FALSE(res);
|
||||||
|
EXPECT_EQ(Error::SSLServerVerification, res.error());
|
||||||
|
|
||||||
|
std::string ca;
|
||||||
|
read_file(SERVER_CERT2_FILE, ca);
|
||||||
|
cli.load_ca_cert_store(ca.c_str(), ca.size());
|
||||||
|
last = client_port();
|
||||||
|
ASSERT_FALSE(last.empty());
|
||||||
|
|
||||||
|
std::vector<std::function<void()>> setters = {
|
||||||
|
[&] { cli.enable_server_hostname_verification(false); },
|
||||||
|
[&] { cli.load_ca_cert_store(ca.c_str(), ca.size()); },
|
||||||
|
[&] {
|
||||||
|
cli.set_ca_cert_store(tls::create_ca_store(ca.c_str(), ca.size()));
|
||||||
|
},
|
||||||
|
[&] {
|
||||||
|
cli.set_server_certificate_verifier(
|
||||||
|
[](const tls::VerifyContext &) { return true; });
|
||||||
|
},
|
||||||
|
[&] {
|
||||||
|
cli.set_session_verifier(
|
||||||
|
[](tls::session_t) { return SSLVerifierResponse::NoDecisionMade; });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
for (const auto &setter : setters) {
|
||||||
|
setter();
|
||||||
|
auto next = client_port();
|
||||||
|
ASSERT_FALSE(next.empty());
|
||||||
|
EXPECT_NE(last, next);
|
||||||
|
last = next;
|
||||||
|
}
|
||||||
|
|
||||||
|
cli.set_session_verifier(
|
||||||
|
[](tls::session_t) { return SSLVerifierResponse::CertificateRejected; });
|
||||||
|
auto handle = cli.open_stream("GET", "/");
|
||||||
|
EXPECT_FALSE(handle.is_valid());
|
||||||
|
EXPECT_EQ(Error::SSLServerVerification, handle.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A PEM given to create_ca_store() needs no NUL after it
|
||||||
|
TEST(SSLClientTest, CreateCaStoreFromPemWithoutNul) {
|
||||||
|
std::string pem;
|
||||||
|
read_file(SERVER_CERT2_FILE, pem);
|
||||||
|
auto buf = pem + "X";
|
||||||
|
auto store = tls::create_ca_store(buf.data(), pem.size());
|
||||||
|
EXPECT_TRUE(store != nullptr);
|
||||||
|
SSLClient cli("localhost");
|
||||||
|
cli.set_ca_cert_store(store);
|
||||||
|
}
|
||||||
|
|
||||||
// enable_system_ca(false) prevents system CA loading entirely
|
// enable_system_ca(false) prevents system CA loading entirely
|
||||||
TEST(SSLClientTest, DisableSystemCa_Online) {
|
TEST(SSLClientTest, DisableSystemCa_Online) {
|
||||||
SSLClient cli("google.com");
|
SSLClient cli("google.com");
|
||||||
@@ -22367,6 +22587,55 @@ TEST(ForwardedHeadersTest, NoForwardedHeaders) {
|
|||||||
observed_remote_addr == "127.0.0.1");
|
observed_remote_addr == "127.0.0.1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A server listening on both IPv4 and IPv6 sees an IPv4 peer as an
|
||||||
|
// IPv4-mapped IPv6 address, which still matches the proxy's IPv4 address
|
||||||
|
TEST(ForwardedHeadersTest, IPv4MappedPeerMatchesIPv4Proxy) {
|
||||||
|
Server svr;
|
||||||
|
svr.set_trusted_proxies({"127.0.0.1"});
|
||||||
|
svr.Get("/ip", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(req.remote_addr, "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port("::");
|
||||||
|
if (port <= 0) { GTEST_SKIP() << "no IPv6 on this host"; }
|
||||||
|
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("127.0.0.1", port);
|
||||||
|
auto res = cli.Get("/ip", Headers{{"X-Forwarded-For", "198.51.100.23"}});
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ("198.51.100.23", res->body);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ForwardedHeadersTest, IPv4MappedHopMatchesIPv4Proxy) {
|
||||||
|
Server svr;
|
||||||
|
svr.set_trusted_proxies({"192.0.2.45", "::1", "127.0.0.1"});
|
||||||
|
svr.Get("/ip", [](const Request &req, Response &res) {
|
||||||
|
res.set_content(req.remote_addr, "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);
|
||||||
|
auto res = cli.Get(
|
||||||
|
"/ip", Headers{{"X-Forwarded-For", "198.51.100.23, ::ffff:192.0.2.45"}});
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ("198.51.100.23", res->body);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(ForwardedHeadersTest, SingleTrustedProxy_UsesIPBeforeTrusted) {
|
TEST(ForwardedHeadersTest, SingleTrustedProxy_UsesIPBeforeTrusted) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user