Compare commits

...
20 Commits
Author SHA1 Message Date
yhirose a210b43e11 Give the ARM cross-compile job's build step its own time limit
apt on the runner mirror sometimes crawls for minutes
(actions/runner-images#14823), and the cross compiler is large enough
that its install alone used up the job's 10-minute limit. Loosen the job
limit to 30 minutes and limit the build step, which normally takes a few
seconds, to 2 minutes.
2026-10-22 18:00:44 -04:00
yhirose 7d28b64a03 Build BoringSSL with clang in the Linux CI job
GCC 15 on ubuntu-latest (26.04) reports a false -Wstringop-overflow in
BoringSSL's Keccak code, and BoringSSL's -Werror makes the build fail.
2026-10-22 17:40:53 -04:00
yhirose 97a972fcef Give the issue-2431 job's test step its own time limit
apt on the runner mirror sometimes crawls for minutes
(actions/runner-images#14823), which used up the job's 5-minute limit
before the test finished. Loosen the job limit to 15 minutes and limit
the test step, which normally takes about 80s, to 3 minutes.
2026-10-22 17:40:53 -04:00
metsw24-max 276cf08e84 Refuse a proxied path that does not start with a slash (#2638)
In absolute-form the path follows the authority, so a path such as "@evil.example/x" made the proxy connect to another host and forward the origin's credentials. Both handle_request and open_stream_ now refuse such a path before anything is written.
2026-10-22 17:01:58 -04:00
yhirose b4b24589ad Refuse to serve a path segment ending in a dot or a space
Windows trims a trailing '.' or space from a file name, so a mount
point there served a file under a path other than the one a pre-routing
handler checked. A mount point now serves no path with a segment
ending in '.' or a space, on every platform; "." and ".." are among
them. The README no longer lists such names among the ones the file
system can still alias.
2026-10-22 11:16:27 -04:00
yhirose 70bed02bc9 Bound how much of an unread request body the server skips
After answering, with a status below 400, a request whose body the
handler left unread, the server read past the body to reuse the
connection up to the payload limit (100MB by default), with only each
read bounded by the read timeout, so a slow sender could hold a worker
for a long time. It now skips at most
CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH (256KB, as Go's net/http does)
within the read timeout as a whole, and closes the connection
otherwise. An error response already closed the connection.
2026-10-22 10:59:57 -04:00
yhirose 5db950e1e1 Normalize the extension of a user MIME type mapping when it is set
set_file_extension_and_mimetype_mapping() kept the extension as given,
and a file's extension was looked up as it was, then in lowercase, so a
mapping given as ".pdf" never matched and one given as "PDF" matched
only files ending in ".PDF". The extension is now lowercased and loses
a leading '.' when it is set, as Apache's AddType does, and a file's
extension is looked up in lowercase only.
2026-10-22 10:21:06 -04:00
yhirose b8ae3e43d5 Answer 500 to an exception the exception handler gives no status
An exception handler that set no status sent its response with 200, or
with whatever status the throwing handler had set. The status is now
set to 500 before the exception handler runs, as ASP.NET Core does, so
it stays 500 unless the exception handler sets another one.

On a WebSocket upgrade, an exception from the pre-routing handler, the
pre-request handler or the sub-protocol selector skipped the exception
handler and dropped the connection without a response. Everything
before the protocol switches now goes through the same exception
handling as routing, so it is answered over HTTP. The WebSocket handler
itself still runs outside it.
2026-10-22 10:01:48 -04:00
yhirose 64b5c68a46 Run RequestWithoutContentLengthOrTransferEncoding on Windows too
The test was kept off Windows while reading the body of such a request
waited on the socket and could hang there. That wait was made
non-blocking soon after, and such a request no longer has its body
read at all, so nothing in it is specific to the platform any more.
2026-10-22 09:32:29 -04:00
yhirose 1c91d54b7c Read no body from an unframed request in builds without TLS too
A request with neither Content-Length nor chunked Transfer-Encoding
has no body (RFC 9112 6.3), and builds with TLS support read none.
Builds without it still read whatever followed such a request on a
connection about to close, up to EOF, as its body. Every build now
reads no body. Tests that exercised only that read, or passed whatever
the server did, are removed, and a new one checks in every build that
the bytes after such a request are not taken as its body.
2026-10-22 08:50:17 -04:00
yhirose fbd3e2cc9b Answer 501 to a request with a transfer coding besides chunked
A request whose Transfer-Encoding ended in chunked but listed more
(another coding before chunked, or chunked twice) was read with only
the chunked framing removed, so the handler got a body still in the
other coding. RFC 9112 6.1 lets a server answer 501 to a transfer
coding it does not implement. The server now accepts only a single
Transfer-Encoding field whose value is chunked, as Go and nginx do,
and answers any other list ending in chunked with 501 and closes the
connection. A list whose final coding is not chunked still gets 400.
2026-10-22 07:58:28 -04:00
yhirose 32a0ca8b35 Ignore Range on requests other than GET and HEAD
RFC 9110 14.2 defines range handling only for GET, and a server must
ignore a Range header on any other method. The server parsed it on
every method, so a POST with a Range got a 206 slice of the handler's
response, or a 416 when the Range was malformed. Range is now parsed
only for GET and HEAD; other methods get the whole response, and a
handler can still read the header itself.
2026-10-22 07:31:50 -04:00
yhirose b83b385ed4 Match trusted proxies as IP addresses
set_trusted_proxies() compared addresses as strings, so a proxy listed
as 10.0.0.2 was not trusted when a server listening on both IPv4 and
IPv6 saw it as ::ffff:10.0.0.2, and two spellings of one IPv6 address
did not match either. The listed addresses are now put in one spelling
when they are set, and so are the connection peer and each
X-Forwarded-For hop before they are looked up; an IPv4-mapped IPv6
address becomes its IPv4 address. The README now documents the setter.
2026-10-22 00:37:09 -04:00
yhirose f297616584 Note that TLS settings do not pass through a plain HTTP redirect hop
The README said a redirect keeps the TLS verification settings. They
pass only from one HTTPS hop to the next; an HTTPS hop after a plain
HTTP one starts over with the defaults.
2026-10-22 00:10:57 -04:00
yhirose 1e35f39675 Send the chunk overhead test requests whole before they are refused
RejectsChunkOverheadPastItsLimit sent about 78 KB per case, and the
server refused the request a few chunks in and closed the connection
while the client was still writing, so the send failed now and then
instead of returning the 400. Pad each size line with a sixteenth of
the limit, so each request is about 10 KB and goes past the limit only
at its last chunks.
2026-10-22 00:10:34 -04:00
yhirose e121385910 Reconnect after a TLS setting changes on a kept-alive client
TLS settings take effect as a connection is set up, so after a client
changed one (server certificate or host name verification, a CA store,
a verifier) a later request still went over the connection kept alive
under the old settings, unverified if verification had just been turned
on. Each such setter now bumps a version that the connection records
when it is set up, and a connection set up under an older version is
closed instead of reused. The setters take no lock, so they cannot wait
on a handshake in flight.
2026-10-22 00:08:44 -04:00
yhirose fc37d3eae1 Add Server::set_compress() to turn off handler response compression
A server built with compression support compressed every handler
response the client accepted, with no way to turn it off. Over HTTPS
the length of a compressed response can give away a secret in it when
the same response also carries text an attacker chooses (BREACH).
set_compress(false) sends handler responses, buffered or streamed, as
they are; compression stays on by default, and static file compression
keeps its own setting. The README and the cookbook describe the switch
and when to use it.
2026-10-21 23:34:48 -04:00
yhirose 3b8b9cf9d5 Parse a CA store PEM through a NUL-terminated copy on Mbed TLS
tls::create_ca_store() on Mbed TLS parsed the caller's buffer with one
byte more than its length, so it read past the end of the buffer, and
when that byte was not a NUL the PEM was not recognized and the call
returned NULL. Parse a NUL-terminated copy instead, as load_ca_pem()
does. DER input still parses, since its own length ends it.
2026-10-21 23:28:19 -04:00
yhirose 3599533c24 Drop invalid response header fields before any header writer
The server left response header fields with an invalid name or value,
such as a value carrying CR/LF that would split the response, for the
default header writer to skip, so a writer set with set_header_writer()
wrote them. Drop such fields from res.headers right after the handlers
that can fill it directly have run, before the server reads it for
framing and range decisions and before any header writer sees it. The
client already rejects invalid request headers before writing them.
2026-10-21 23:16:45 -04:00
yhirose 247a2abe4a Bound the bytes around chunk data in chunked transfer coding
The payload limit counts only chunk data, and a chunk-size line is only
bounded by the line length limit, so a peer could send a few bytes of
data with any amount of padding, leading zeros and chunk extensions
around them. Count those bytes as Go does: each chunk may spend 16
bytes plus twice its data on its size line and the CRLF after it, which
still lets a stream send a byte at a time, and the excess may build up
to CPPHTTPLIB_HEADER_MAX_LENGTH before the read fails. This covers
request bodies on the server and response bodies on the client.
2026-10-21 23:01:39 -04:00
7 changed files with 927 additions and 486 deletions

No files matched your search

+4 -1
View File
@@ -28,10 +28,13 @@ jobs:
test-arm32:
name: ARM 32-bit (cross-compile)
runs-on: ubuntu-latest
timeout-minutes: 10
# Loose, because apt on the runner mirror can crawl for minutes; the build
# step has its own tight limit.
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Install cross compiler
run: sudo apt-get update && sudo apt-get install -y g++-arm-linux-gnueabihf
- name: Build (ARM 32-bit)
timeout-minutes: 2
run: arm-linux-gnueabihf-g++ -std=c++11 -Wall -Wextra -Wno-psabi -Werror -c -o /dev/null test/test_32bit_build.cpp
+9 -4
View File
@@ -209,7 +209,11 @@ jobs:
git clone https://boringssl.googlesource.com/boringssl boringssl
cd boringssl
git checkout "${{ steps.boringssl-rev.outputs.sha }}"
# GCC 15 reports a false -Wstringop-overflow in BoringSSL's Keccak
# code, which its -Werror makes fatal. Clang is its primary compiler.
cmake -S . -B build \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON \
@@ -319,10 +323,9 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name) ||
(github.event_name == 'workflow_dispatch' && github.event.inputs.test_linux == 'true')
name: issue-2431 repro (Linux + ASAN)
# Bound the whole job in case anything in the test harness hangs
# unexpectedly. With the fixture in place a normal run is well under
# a minute either way (ASAN abort on broken HEAD, clean pass on fix).
timeout-minutes: 5
# Loose, because apt on the runner mirror can crawl for minutes; the test
# step has its own tight limit.
timeout-minutes: 15
env:
DNS_FIXTURE_PORT: "15353"
DNS_FIXTURE_DELAY: "3"
@@ -370,6 +373,8 @@ jobs:
- name: build test binary
run: cd test && make test
- name: run GetAddrInfoAsyncCancelTest
# A normal run takes about 80s.
timeout-minutes: 3
run: |
cd test
ARCH=$(uname -m)
+16 -6
View File
@@ -102,7 +102,7 @@ cli.enable_server_hostname_verification(false);
```
> [!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
@@ -406,7 +406,7 @@ ret = svr.remove_mount_point("/");
ret = svr.remove_mount_point("/public");
```
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows a name with a trailing `.` or space, or an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
A mount point serves a path only when it has no `.`, `..` or empty segment other than a trailing `/`, and no segment ending in `.` or a space, so a check on `req.path` in `set_pre_routing_handler()` sees the path of the file that is served. A path ending in `/`, and the mount point itself such as `/public`, serve the `index.html` in that directory. The file system can still give a file other names: another case on a case-insensitive file system, such as the default ones on macOS and Windows, and on Windows an 8.3 short name such as `PRIVAT~1`. Compare paths without regard to case on such a file system, and on Windows keep files that need a guard out of mounted directories.
```cpp
// User defined file extension and MIME type mappings
@@ -415,7 +415,7 @@ svr.set_file_extension_and_mimetype_mapping("cpp", "text/x-c");
svr.set_file_extension_and_mimetype_mapping("hh", "text/x-h");
```
Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. A user mapping is looked up by the exact extension first, then by the extension in lowercase.
Extensions match regardless of case, so `photo.JPG` is served as `image/jpeg`. The extension of a user mapping can be given in any case, with or without a leading `.`.
The following are built-in mappings:
@@ -522,7 +522,7 @@ svr.set_error_handler([](const auto& req, auto& res) {
```
### Exception handler
The exception handler gets called if a user routing handler throws an error. This covers the pre-routing handler, the file request handler, the pre-request handler, route handlers and content readers. An exception from the post-routing handler, the error handler, the `Expect: 100-continue` handler, a content provider, a logger or a WebSocket handler does not reach it: the connection is dropped, and the error logger receives `Error::UserCallbackException`.
The exception handler gets called if a user routing handler throws an error. This covers the pre-routing handler, the file request handler, the pre-request handler, route handlers, content readers and, on a WebSocket upgrade, the sub-protocol selector. The response is a 500 unless the exception handler sets another status. An exception from the post-routing handler, the error handler, the `Expect: 100-continue` handler, a content provider, a logger or a WebSocket handler does not reach it: the connection is dropped, and the error logger receives `Error::UserCallbackException`.
```cpp
svr.set_exception_handler([](const auto& req, auto& res, std::exception_ptr ep) {
@@ -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
```
### 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
```c++
@@ -928,7 +936,7 @@ svr.set_read_timeout(std::chrono::seconds(5));
svr.set_keep_alive_timeout(std::chrono::seconds(10));
```
The read timeout bounds the request line and headers as a whole, and each read of a request body.
The read timeout bounds the request line and headers as a whole, and each read of a request body. An error response closes the connection. Any other response to a request whose body was left unread, such as a redirect from a pre-routing handler, keeps the connection only when the rest of the body is at most `CPPHTTPLIB_UNREAD_PAYLOAD_MAX_LENGTH` (256KB, `0` for no limit) and arrives within the read timeout, since the server has to read past it first.
### Set maximum payload length for reading a request body
@@ -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`.
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`.
@@ -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.
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
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
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()`でファイルをそのまま返す場合、デフォルトでは圧縮されません。有効にするには次を呼びます。
+282 -204
View File
@@ -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);
+600 -271
View File
File diff suppressed because it is too large. Load diff