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.
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.
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.
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.
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.
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.
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.
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.
The OpenSSL client finished the handshake with SSL_VERIFY_NONE and
checked the server certificate only afterwards, so it presented its
client certificate to a server that then failed verification. With a
server certificate verifier that accepted, wolfSSL did the same. Verify
the chain, and the host name, during the handshake on every backend,
keeping the built-in verdict when a verifier runs alongside it. On
OpenSSL a session verifier, which may accept a certificate the built-in
check rejects, still decides after the handshake.
A certificate that fails the built-in verification now reports
CertVerifyFailed, or HostnameMismatch for its name, in ssl_error() on
every backend, where OpenSSL left it 0.
tls::create_ca_store() parses certificates, which on Mbed TLS 4.x needs
PSA Crypto. A context readies it when it is created, but a store can be
made before any context exists, and then a valid PEM failed to parse
and the call returned NULL. Ready PSA Crypto first, as the contexts do.
Under the default system CA policy, a custom CA given to a client keeps
the system CAs out. SSLClient and WebSocketClient only noted the custom
CA when set_ca_cert_store() got a store or load_ca_cert_store() got some
bytes, so a NULL store, such as the one tls::create_ca_store() returns
for a PEM it cannot parse, or an empty PEM left the client trusting
every system CA instead. Note the custom CA on every call, so that such
a client fails verification. A redirect still passes a CA on only when
the original client had one.
A mount point left ".", ".." and empty path segments for the file
system to resolve, while the pre-routing handler, where the README has
applications guard static files, saw the path as it was sent. A file
could then be reached under a spelling that a check on req.path did not
match. Serve a path from a mount point only when it has none of those
segments other than a trailing '/', so that the file served is the one
req.path names; any other path now gets 404. The README also notes the
other names a file system can give a file.
SSLServer::update_certs_pem() loaded the client CA it was given but
left client certificate verification as it was, so on a server built
without a client CA it returned true while clients without a
certificate still got in. Turn verification on, as the constructors do
when they are given a client CA. A client CA passed to
update_certs_pem() now always makes client certificates required.
The Mbed TLS server keeps the SNI from the ClientHello in a
thread-local until the handshake succeeds, and its SNI callback only
runs for a ClientHello that carries one. A handshake that failed left
its SNI behind, so the next connection served by the same thread
without SNI reported the earlier client's name in req.sni(). Start
each handshake with the thread-local cleared.
SSLServer::PemMemory takes a length of 0 to mean a NUL-terminated
string for cert_pem and key_pem, but skipped client_ca_pem when its
length was 0, so a server given its client CA that way started without
requiring client certificates. Read the client CA under the same rule
as the other fields. An empty client CA now fails to load instead of
turning the check off, and the README documents the length-0 rule.
open_stream(), and with it stream::Get() and SSEClient, wrote its own
request head and left out what a buffered request gets: the
Authorization from set_basic_auth() or set_bearer_token_auth(), and
through a plain HTTP proxy the absolute-form target (RFC 9112 3.2.2)
and Proxy-Authorization. A proxy refused such a request, and an origin
that needs credentials answered 401.
Move the two decisions out of write_request() and handle_request() into
add_credential_headers() and proxy_target_prefix(), and use them in
open_stream() too, after connecting as a buffered request does. A
Digest challenge or a redirect is still handed to the caller.
A failed request could reach the client's error logger twice: once from
the step that failed, without a request when that was the TLS setup or
the proxy CONNECT, and again from send_() with the request. A content
receiver or progress callback that canceled was reported twice too, a
CONNECT answered with 407 only without a request, and a stream that
failed to open not at all unless TLS failed.
Report a failure in one place instead. The outermost send() reports the
request's error once, with the request the caller made, also when the
failure happened on a redirect (a redirect client no longer gets the
error logger) and when an answer carries an error, such as a proxy's
407. open_stream() reports a stream that fails to open the same way.
Only an upload body compressed before send() still reports where it
fails.
A buffered request whose response body went over set_payload_max_length(),
also once decompressed, failed with Error::Read, while the Stream API
reports the same limit as Error::ExceedMaxPayloadSize. read_content()
already marks it with a 413 status, so map that status to
ExceedMaxPayloadSize next to the 415 and 500 the client maps already.
696b94b built and ran test_websocket_heartbeat in the Windows jobs to
cover the pong timeout wake, which added more than a minute to each of
them. Only its two pong timeout tests exercise what differs by
platform, and both work with the ping interval set at runtime. Move
them into test.cc, where every job runs them in its shards, and drop
the Windows heartbeat build and step.
The heartbeat file keeps CompileTimeClientReadTimeoutIsFail, whose
fixture is renamed WebSocketSilentPeerTest after the peer it sets up.
When max_missed_pongs ran out, the heartbeat thread shut down the read
side of the socket to end a read() blocked on the silent peer. On
Windows that does not wake the wait, so read() stayed blocked until its
read timeout and then returned Timeout. The heartbeat test never ran on
Windows, so CI did not catch it.
The heartbeat now sets a flag that the stream's read interrupt checks,
the mechanism SSEClient::stop() uses, and WebSocketSSLStream takes the
interrupt too. It is set only when a pong timeout can happen, so other
connections wait as before. read() reports a timeout on a connection
that has closed as Fail, since Timeout means it is still usable.
Build and run the heartbeat test in the Windows jobs, and add a TLS
pong timeout test.
The event loop reads through open_stream(), which takes the socket out
of the Client, so the client_.stop() in stop() could not reach it.
stop(), and the destructor with it, waited until the server sent
something or the read timeout passed, 300 seconds by default.
Shutting the socket down from stop() does not wake a read waiting in
WSAPoll on Windows, so 3744227 was reverted. A stream now takes an
interrupt check instead: while one is set, it waits for data in 100 ms
slices and gives up once the check returns true, as the server's
keep-alive wait does with its flag. SSEClient sets one that turns true
when stop() is called. Streams without one wait as before.
SSEIntegrationTest.ManualStop now ends its stream with the fixture's
flag. Its server outlives the test body, and the faster stop() let the
stream read a local of the body that had returned.
This reverts commit 3744227de2.
Shutting down the socket does not wake a read waiting in WSAPoll on
Windows while the peer stays silent, so stop() still waited out the read
timeout there and the new tests failed. Faster stop() on the other
platforms also let SSEIntegrationTest.ManualStop return while its server
handler still read a local of the test body (stack-use-after-return).
The fix will come back built on a wait that works on every platform.
An upgrade request goes through the pre-routing handler before the
WebSocket routes are matched. When none matched, it was routed as a
plain HTTP request, and routing() ran the handler a second time. Skip
it there for an upgrade request.
An `event:` line without a value left the type empty, so the event
never reached on_event("message"), and on_message saw an empty type.
The HTML standard dispatches an event with an empty type as
"message".
StreamHandle::read() parsed the trailers only on the path without a
decompressor, so response->trailers stayed empty for a compressed
chunked response. Parse them in one place for both paths, and only
after the last chunk: after a failed read it waited another read
timeout for a trailer line, and parse_trailers_if_needed() called
before the end read body bytes as trailers.
A response with neither Content-Length nor chunked encoding ends when
the connection closes (RFC 9112 6.3), but the stream API set
read_error() to ConnectionClosed on reaching that end, and SSEClient
passed it on to on_error. Only a body shorter than its Content-Length
is cut short there.
With follow_location on, every 3xx was taken for a redirect, so a 304
Not Modified, or any 3xx without a Location, failed with Error::Unknown.
Follow a 3xx only when it has a Location, as curl does, and return the
others as the response, as curl, Go, requests and urllib3 do. Response
handlers and content receivers now get such a response too.
With built-in verification on, a server certificate verifier returning
true made Mbed TLS accept a certificate its own check had rejected,
such as a self-signed one, while OpenSSL and wolfSSL still failed with
SSLServerVerification. On a client, keep the flags of Mbed TLS's own
verification, so the verifier is an additional check as on the other
backends. With verification off, the verifier is still the only check.
A server still clears them: there all three backends let an accepting
callback admit a client certificate.
The event loop reads through open_stream(), which takes the socket out
of the Client, so the client_.stop() in stop() could not reach it.
stop(), and the destructor with it, waited until the server sent
something or the read timeout passed, 300 seconds by default.
The loop now hands the stream's socket to stop() while it reads, and
withdraws it under the same lock before the socket is closed. stop()
shuts down the read side only: a TLS backend answers the EOF with an
alert, and writing it to a socket closed for writing raises SIGPIPE.
SSLServer, SSLClient and WebSocketClient take a PemMemory with
cert_pem_len and key_pem_len, but the lengths were never read: every
backend read the PEM up to a NUL, so a buffer holding exactly the PEM,
such as a std::vector<char> read from a file, was read past its end.
Hand on a NUL-terminated copy of the given number of bytes instead.
A length of 0 still means a NUL-terminated string, which is what
callers got before. The client CA already honored its length.
Bring the READMEs and the docs pages (en and ja) in line with the
current header:
- Fix the client sample that does not compile, the Transfer-Encoding
statement for default headers, the verifier callback comment, the
thread pool spawn rule, the sanitize_filename description, and the
inverted Accept-Encoding sentence.
- Describe recent behaviour: TLS errors behind Read/Write failures,
Host validation, Connection: close for a body without length,
content_reader rules, refused CONNECT, WebSocketClient proxies,
CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT, the SSE event size limit, and
the payload limit of the Stream API.
- Complete the API references of WebSocket, WebSocketClient and the
stream:: helpers, and the SSE reconnect rules.
- Fix cookbook pages: keychain loading on every backend, received Close
status, a multipart sample that trusted the client's file name, an
echo sample that sent binary as text, SNI, pre-request handler scope,
stop() with WebSocket handlers, and the thread pool notes.
- Restore the mutex in the LLM app's Chapter 5 code.
CURLOPT_READDATA takes a pointer, but ServerClosesConnection passed
the read_data struct itself through curl_easy_setopt()'s varargs, so
the read callback never got its state and never produced a body.
time_t is 32 bits wide on 32-bit builds, so the millisecond counts in
set_max_timeout() and the streams' duration() narrowed into it
implicitly. Cast them as duration_to_sec_and_usec() already does. In
the tests, six content receivers took a uint64_t length where
ContentReceiver passes size_t, the Expect: 100-continue test kept
10 GiB in a size_t, and the Range checks compared ssize_t fields with
unsigned literals, so they now compare whole Ranges.
DoesNotReserveTheClaimedLength shifted a size_t by 41. It now claims
std::string().max_size(), the most the client accepts, and still
fails if the reserve cap is removed.
BoringSSL counts stacks in size_t, and takes the protocol version as
uint16_t and the error code as uint32_t. Walk the certificate stacks
with the decltype(count) loop used elsewhere, pass the version as
uint16_t, and cast the error code to what ERR_get_error() returns.
MSVC takes the send() length as int, so the raw send() calls left in
the tests go through detail::send_socket().
ThreadPool::enqueue only spawned a dynamic thread when no thread was
idle. Threads already notified for an earlier job still counted as
idle until they took it, so a burst of connections could leave jobs
queued with no thread while the pool was below max_threads. With
keep-alive, such a connection waited until another connection closed.
Fix#2633
from_chars() mixed its int digit and base into the arithmetic of an
unsigned T, a sign conversion since Content-Length is parsed as size_t.
Convert both to T first.
Comparing a uint64_t with SIZE_MAX is always false on 64-bit builds,
so the WebSocket frame length and mmap size checks tripped
-Wtautological-type-limit-compare. fits_in_size_t() checks with a
round trip instead, and the Windows branch of mmap::open() uses it too.
The raw ::send() calls in the WebSocket tests passed an int length,
which POSIX takes as size_t. Use detail::send_socket(), which handles
both.
This reverts commit 28f3922. Keep the path encoding settled in #2082
(#2170) and #2608 until a concrete report needs more: user input
belongs in encode_path_component(), and an embedded NUL is invalid
input.
encode_path() sent '"', '<', '>', '\', '^', '`', '{', '|', '}', '['
and ']' raw, although RFC 3986 §3.3 doesn't allow them in a path.
Tomcat, Jetty and Undertow answer each of them with 400 by default, but
accept the percent-encoded form. It also stopped at an embedded NUL, so
"/a\0b" went out as "/a". Encode every byte outside pchar, keeping '/'
and '%' so that escapes pass through, and still encode '+', ',', ';'
and '\'' as before.
encode_request_target() now passes only the path to encode_path(). The
"http://host" of a target sent to a proxy and a CONNECT "host:port" are
sent as they are, so the brackets of an IPv6 host stay.
On BoringSSL, SSL_get1_session() after a TLS 1.3 handshake gives a
session that cannot be resumed; the tickets come only through the
new-session callback, which its documentation recommends. Store the
session from that callback, so the resumption tests resume there too.
ThrowingVerifyCallbackRejectsTheCertificate and
ErrorAndExceptionHandlersReplaceProvider throw from a callback, which
does not compile with CPPHTTPLIB_NO_EXCEPTIONS and -fno-exceptions.
windows.h defines max() as a macro, which broke the Windows builds at
the two time_point::max() calls the handshake deadline added. Wrap them
in parentheses, as the other max() calls are.
Both sides of the merge tested the same behavior. Keep one test of
each: ClientCertStillRequired* for a server keeping client certificates
required, which covers a cleared callback too, VerifierStaysWithItsClient
for per-client verifiers across threads, and the Keep* redirect tests,
which share one helper. KeepSessionVerifier now also checks the error,
as the dropped SessionVerifier did.
Both sides keep TLS verify callbacks per context. Keep this side's
storage, which lets a handshake keep the callback it is running and
takes an exception from it as a rejection, and take the other side's
apply_verify_mode(), which works out the verify mode in one place on
every backend, with Mbed TLS and wolfSSL client contexts verifying by
default. Mbed TLS installs its verify callback on every session, and
each session runs its own copy of the user's callback. The redirect
client setup that both sides added is kept once.
name and filename are read as RFC 9110 quoted-strings, while browsers
send a backslash as it is, so from a browser a value with "\\" in it
arrives with a single backslash, and one ending in "\" is misread.
Through a plain-HTTP proxy the request line carries the absolute form,
while the Digest uri stays in origin form, as curl sends it. The comment
cited RFC 7616 3.4, which asks for the effective request URI.
A resumed handshake does not show the client certificate again, so since
OpenSSL mTLS servers resume sessions, a verify callback, such as one
checking a deny list, was skipped for as long as the ticket was good
(7200 s by default). Turn off session tickets, TLS 1.3 ones included,
and the session cache on a context with a callback, as on wolfSSL.
A server sending "102 Processing" without end kept the client reading
them unless max_timeout was set. Skip at most
CPPHTTPLIB_INTERIM_RESPONSE_MAX_COUNT (100) and fail with Error::Read
past it.
Client took any string with "//" in it that it could not use as a URL
for a bad URL, so a Unix socket path such as /run/app//api.sock gave an
invalid client. Only a string that starts with a scheme and "://", or
with "//", is taken for a URL now.
set_content_provider() and file content added their Content-Type next to
the one an earlier set_content() had set, so the response went out with
two, and clients took the first. Each content setter now replaces it. A
Content-Type set after set_file_content() is used for the file.
A content provider without a length or chunked coding sends a body that
only the end of the connection delimits (RFC 9112 6.3), yet the response
offered keep-alive, so a keep-alive client waited for the server's idle
timeout. Send Connection: close and close after it, as Go's net/http
does. A Content-Length set by the handler is still dropped; the
overload that takes a length frames such a body.
An exception from a verify callback unwound through the TLS library,
which is C: OpenSSL leaked the objects of the handshake, and a server
worker thread ended the process. It also left the handshake count that
update_certs_pem() waits on raised, so with a task queue that caught the
exception, every later update waited forever. Take the exception as a
rejection, and lower the count on every way out of the handshake.
Replacing a verify callback, as set_server_certificate_verifier() does,
deleted the one that a handshake on another thread could be running, on
OpenSSL and wolfSSL. Handshakes now share the callback and take their
reference under a lock, and SSLClient replaces it under the lock that
guards creating a session from its context.
tls::set_verify_callback() set the verify mode anew, so on a server that
required client certificates it made them optional: OpenSSL and wolfSSL
dropped SSL_VERIFY_FAIL_IF_NO_PEER_CERT, and Mbed TLS went from REQUIRED
to OPTIONAL. A client without a certificate got through. Keep what the
server requires.
update_server_client_ca() rotated the session ticket keys with 80 bytes,
which only OpenSSL takes. BoringSSL and LibreSSL take 48, refused them,
and the result was not checked, so a client went on resuming with a
ticket from before the client CA changed. Use the size each library
takes, and fail the update when the keys cannot be set.
wolfSSL shares its session cache between contexts, so a session resumed
on a server that checks client certificates could carry a certificate
checked against another server's CA, or against a client CA replaced
since. Such a server no longer resumes sessions.
Without a session id context, OpenSSL answers a client resuming its
session with a fatal alert once client certificates are verified, which
curl and browsers run into on their second connection. Set one on every
server context. A changed client CA still stops resumption, as fcf7568
flushes the cache and rotates the ticket keys.
With set_ca_cert_store() after load_ca_cert_store(), the redirect client
trusted the earlier PEM too, though the store had replaced it, and of
several load_ca_cert_store() calls it kept only the last. Take the CAs
from the context when a store was set, and from every PEM loaded
otherwise.
Hostname verification on these backends fell back to the subject CN
even when the certificate had a DNS SAN, unlike X509_check_host and
RFC 6125 6.4.4. It does so only without one now. Mbed TLS also reads
the CN by its OID instead of searching the formatted DN for "CN=", and
no longer stops at an empty SAN.
The Digest uri and A2 took the raw path, while the request line carried
it encoded, with the params and without the fragment, so a server
comparing the two rejected the credentials. Build the target in one
place for both.
set_keep_alive_max_count(0) tripped an assert, a negative WebSocket ping
interval sent pings in a tight loop, and the form length, multipart file
count and range count macros rejected every request at 0. Make 0 mean
no limit for those, and a ping interval of 0 or less turn pings off. The
URI, header and line bounds cannot be lifted, so a static_assert keeps
them positive.
An event stream has no end, but the client's payload_max_length, 100MB
by default, cut it and made the client reconnect. SSEClient lifts that
limit for its stream; each event stays bounded by
CPPHTTPLIB_SSE_MAX_EVENT_LENGTH.
Files have been read with pread() since 44658e6, so the mapping went
unused. Keep only the descriptor, open it close-on-exec and without
waiting on a FIFO, and accept only a regular file, so that a device is
no longer served as an empty body.
A quoted boundary kept its quoted-pairs, as only the quotes were taken
off, and the multipart writer sent a backslash as it was, so a name
ending in one escaped the closing quote for this library's own parser.
Unescape \" and \\ in media type parameters as in Content-Disposition,
and write a backslash as \\.
append_query_params() added the parameters after a '#', where the
fragment, which is not sent, took them along, and write_request() read a
query that came after one. Insert them before the fragment, and leave
the fragment out of the query.
A response framed by Transfer-Encoding next to a Content-Length, or an
HTTP/1.0 response with Transfer-Encoding, left the connection open for
the next request, though RFC 9112 6.1 has such framing treated as faulty
and the connection closed after it. Close it, with the check the server
makes for requests now shared.
get_ca_certs() handed out heap copies of the context's CA certificates,
but free_cert() frees nothing on Mbed TLS, so every copy leaked. Hand
out the context's own certificates instead, as get_peer_certs() does
the session's, and say on the declaration that they last only as long
as the context's CA certificates stay as they are.
This showed up on every redirect from a client given a store with
set_ca_cert_store(), which passes the store on through these
certificates.
A content provider that wrote past the length it was given put the
extra bytes after the framed body, where a keep-alive peer read them as
the next message. Write only what fits and fail the write, on both the
server and the client.
Content-Length and Transfer-Encoding a handler set were sent next to
the ones the library worked out, for ranges and compression, and a
post-routing handler could change the body after Content-Length was
set. Work both out in one place, from the body about to be written,
again after the post-routing handler; a HEAD response keeps the length
its handler gave it. A 1xx, 204 or 304 response sends no body and,
following RFC 9110 8.6, no Content-Length.
The client still sends a Content-Length or Transfer-Encoding its caller
passes, as curl and urllib3 do, and the README says these have to match
the body.
The read timeout applied to each read of the request line and headers,
so a client that sent them a byte now and then held a worker for as
long as it liked, and enough such clients left no worker for anyone
else. Bound the whole head by the read timeout, as the TLS handshake
already is, through a new Stream::set_max_timeout() that the socket
streams implement with their existing max timeout.
A request body is still bounded per read, and a WebSocket holds its
worker for its whole life. The README says so, and points to a
reverse proxy and the pre-request handler for untrusted clients.
A retry or redirect after an early answer to Expect: 100-continue now
opens a new connection, interim responses are also skipped while
waiting for 100 Continue, and a URL Client cannot use, such as one with
userinfo, gives an invalid client. Only a 407 is passed on from a
refused CONNECT, WebSocket::close() sends only a valid payload, and the
SSE client stops on an oversized event rather than reconnect to it.
Media type parameter names and user extension mappings are matched
without case, file content is read in larger chunks from a buffer of
its own, the streaming decompression limit reports
ExceedMaxPayloadSize, and the tests use the existing single-response
server helpers. Comments and the README are brought in line.
The server fuzzer's stream exposed no buffered data, so the fast path of
stream_line_reader::getline was never fuzzed. Hand out what a read
pulled in beyond the bytes asked for, as SocketStream does.
Over TLS a file is copied out of its mapping in this process, so one
truncated while it was being served raised SIGBUS and took the whole
server down. Read it with pread, which comes up short instead.
Credentials and the client certificate stay behind; other headers,
default ones included, go along as RFC 9110 15.4 has it, and so do the
limits and the TLS settings.
A wss client could set its CAs but had no way to pin a certificate. Add
set_server_certificate_verifier() and set_session_verifier(), as
SSLClient has.
INDEX.HTML got the default type, a REMOTE_ADDR spelled in lower case got
past the guard on prohibited header names, and Multipart/Form-Data was
not taken for multipart, though RFC 9110 makes field names and media
types case-insensitive and web servers match extensions so.
OpenSSL allowed client-initiated renegotiation, which runs a handshake
update_certs_pem() does not wait for, and kept sessions and ticket keys
across a change of client CAs, so a client verified under the old ones
could resume. wolfSSL added new client CAs to the old instead of
replacing them, and a bad certificate or key left Mbed TLS and wolfSSL
half updated.
Turn renegotiation off, forget the sessions and ticket keys on a client
CA change, replace wolfSSL's CAs, and check new material before swapping
it in. Note that the update must not be called from a TLS callback.
The Mbed TLS and wolfSSL backends matched *.com, a bare *, vic*.test and
names with empty labels, which OpenSSL rejects. Require two labels after
a wildcard, none in an IDNA label and no empty label, and ignore a
trailing dot on the host, so every backend accepts the same names.
On Mbed TLS and wolfSSL the verification mode is a setting of the
context, and it was changed outside the lock that guards creating a
session from that context.
Any Close payload was echoed back, a 1-byte one, a code that may not be
sent and a reason that is not UTF-8 included. Echo a valid one and
answer the others with 1002, from one place for both read loops.
A non-200 answer to CONNECT was returned as the response, so a proxy, or
anyone on the plain leg to it, could answer an HTTPS request as if the
origin had. Fail the request with Error::ProxyConnection for anything
but a 4xx or 5xx refusal.
Only 100 Continue was skipped, and only without header fields, so a 103
Early Hints was taken for the final response and a 100 with fields threw
the parsing off. Skip every 1xx but 101, along with its header fields.
When the final response came before 100 Continue, the body was never
sent but the connection was kept, so the server could read the next
request as that body. Close it after the response.
parse_url took http://user@evil.com/ for the host "user@evil.com", where
parsers that honour userinfo, or read a backslash as '/', see evil.com.
userinfo is not supported, so reject both.
When the real path of a file under a mount point could not be resolved,
the check that it stays inside the mount point was skipped and the file
was served. Do not serve it from there, and refuse a mount point whose
directory cannot be resolved.
decode_path_component dropped a %u escape of a surrogate, joining what
was on either side of it, so /ad%uD800min became /admin after a filter
had seen something else. Keep such an escape as it is.
Any run of spaces or tabs separated the parts of the request line, a
fragment in the target was stripped, and an HTTP/1.1 request without
Host was served, so a server or a cache in front that follows RFC 9112
could see another request. Require a single SP between the parts, and
answer a target with '#' and an HTTP/1.1 request without Host with 400.
The client no longer sends the fragment of a URL.
RFC 9112 6.1 has a server close the connection after answering a
request with both Transfer-Encoding and Content-Length, or with
Transfer-Encoding in HTTP/1.0. Close it, and send Connection: close
whenever the connection closes after a response.
split_unquoted ended a quoted-string at an escaped quote, so
name="a\"; filename=evil.php" came apart into a name and a filename that
a parser in front following RFC 9110 would not see. Skip quoted-pairs
when splitting, and unescape \" and \\ in Content-Disposition values,
leaving other backslashes, such as those of a Windows path, as they are.
A part header arriving a few bytes at a time was searched for its CRLF
from the start on every read, which made parsing it quadratic. Remember
how far the search got.
The SSE client buffered a line, or the data of an event, with no limit
of its own, and with payload_max_length(0) with none at all. Add
CPPHTTPLIB_SSE_MAX_EVENT_LENGTH (16MB) and end the connection with
ExceedMaxPayloadSize past it.
Every continuation frame could be as long as the whole message limit,
and the total was checked only after appending it, so a fragmented
message could take about twice the limit. Read each fragment against
what is left, with room for a control frame between them.
The client reserved what Content-Length claimed, up to
payload_max_length, before any of the body arrived, and without a limit
nothing bounded it. Reserve at most 1 MiB up front and let a longer body
grow as it is read.
An unsupported scheme without exceptions, or a port out of range, left
Client with no implementation, so the first request after it went
through a null pointer. Put a client in its place that reports itself
invalid and fails every request with Error::Connection.
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH = 0 rejected every frame with a
payload, while 0 means no limit for payload_max_length. Make it mean the
same here, and refuse a length that size_t cannot hold.
set_payload_max_length(0) means no limit, but StreamHandle compared the
decompressed size against 0, so every compressed response read through
the streaming API failed.