mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-09 16:52:52 +07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00afaaed6a | ||
|
|
e803f5e413 | ||
|
|
57b8aca6da | ||
|
|
213029685a | ||
|
|
17eeb18feb | ||
|
|
edc9760005 | ||
|
|
6d1049462d | ||
|
|
4fcbc08f2d | ||
|
|
d59f3e438c | ||
|
|
09fa6820fe | ||
|
|
438319cfcb | ||
|
|
eda9a10bfe |
+1
-1
@@ -119,7 +119,7 @@ using SubProtocolSelector =
|
|||||||
std::function<std::string(const std::vector<std::string> &protocols)>;
|
std::function<std::string(const std::vector<std::string> &protocols)>;
|
||||||
```
|
```
|
||||||
|
|
||||||
The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols.
|
The `SubProtocolSelector` receives the list of subprotocols proposed by the client (from the `Sec-WebSocket-Protocol` header) and returns the selected one. Return an empty string to decline all proposed subprotocols. A returned value that the client did not propose is ignored.
|
||||||
|
|
||||||
### WebSocket (Server-side)
|
### WebSocket (Server-side)
|
||||||
|
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
|
|||||||
| Platform | Behavior | Disable (compile time) |
|
| Platform | Behavior | Disable (compile time) |
|
||||||
| :------- | :------- | :--------------------- |
|
| :------- | :------- | :--------------------- |
|
||||||
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
|
||||||
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend, with revocation checking. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend. Revocation checking is best-effort: a revoked server certificate is rejected, while one whose revocation status cannot be determined is accepted. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
|
||||||
|
|
||||||
On Windows, verification can also be disabled at runtime:
|
On Windows, verification can also be disabled at runtime:
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.59.0"
|
version = "0.60.1"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.59.0"
|
#define CPPHTTPLIB_VERSION "0.60.1"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x003b00"
|
#define CPPHTTPLIB_VERSION_NUM "0x003c01"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -4377,7 +4377,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms,
|
bool parse_sse_line(const std::string &line, SSEMessage &msg, int &retry_ms,
|
||||||
bool &has_data);
|
bool &has_data, bool &has_id);
|
||||||
void run_event_loop();
|
void run_event_loop();
|
||||||
void dispatch_event(const SSEMessage &msg);
|
void dispatch_event(const SSEMessage &msg);
|
||||||
bool should_reconnect(int count) const;
|
bool should_reconnect(int count) const;
|
||||||
@@ -4412,6 +4412,8 @@ private:
|
|||||||
|
|
||||||
namespace ws {
|
namespace ws {
|
||||||
|
|
||||||
|
class WebSocketClient;
|
||||||
|
|
||||||
enum class Opcode : uint8_t {
|
enum class Opcode : uint8_t {
|
||||||
Continuation = 0x0,
|
Continuation = 0x0,
|
||||||
Text = 0x1,
|
Text = 0x1,
|
||||||
@@ -4513,7 +4515,7 @@ public:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
friend class httplib::Server;
|
friend class httplib::Server;
|
||||||
friend class WebSocketClient;
|
friend class httplib::ws::WebSocketClient;
|
||||||
|
|
||||||
WebSocket(
|
WebSocket(
|
||||||
Stream &strm, const Request &req, bool is_server,
|
Stream &strm, const Request &req, bool is_server,
|
||||||
@@ -4887,7 +4889,8 @@ inline void SSEClient::stop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
||||||
int &retry_ms, bool &has_data) {
|
int &retry_ms, bool &has_data,
|
||||||
|
bool &has_id) {
|
||||||
// Blank line signals end of event
|
// Blank line signals end of event
|
||||||
if (line.empty()) { return true; }
|
if (line.empty()) { return true; }
|
||||||
|
|
||||||
@@ -4917,6 +4920,7 @@ inline bool SSEClient::parse_sse_line(const std::string &line, SSEMessage &msg,
|
|||||||
} else if (field == "id") {
|
} else if (field == "id") {
|
||||||
// Empty id is valid (clears the last event ID)
|
// Empty id is valid (clears the last event ID)
|
||||||
msg.id = value;
|
msg.id = value;
|
||||||
|
has_id = true;
|
||||||
} else if (field == "retry") {
|
} else if (field == "retry") {
|
||||||
// Parse retry interval in milliseconds
|
// Parse retry interval in milliseconds
|
||||||
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
// Per the SSE spec, a value that is not all ASCII digits is ignored.
|
||||||
@@ -4987,7 +4991,8 @@ inline void SSEClient::run_event_loop() {
|
|||||||
// Event receiving loop
|
// Event receiving loop
|
||||||
std::string buffer;
|
std::string buffer;
|
||||||
SSEMessage current_msg;
|
SSEMessage current_msg;
|
||||||
bool has_data = false;
|
auto has_data = false;
|
||||||
|
auto has_id = false;
|
||||||
|
|
||||||
while (running_.load() && result.next()) {
|
while (running_.load() && result.next()) {
|
||||||
buffer.append(result.data(), result.size());
|
buffer.append(result.data(), result.size());
|
||||||
@@ -5006,13 +5011,13 @@ inline void SSEClient::run_event_loop() {
|
|||||||
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
if (!line.empty() && line.back() == '\r') { line.pop_back(); }
|
||||||
|
|
||||||
// Parse the line and check if event is complete
|
// Parse the line and check if event is complete
|
||||||
auto event_complete =
|
auto event_complete = parse_sse_line(
|
||||||
parse_sse_line(line, current_msg, reconnect_interval_ms_, has_data);
|
line, current_msg, reconnect_interval_ms_, has_data, has_id);
|
||||||
|
|
||||||
if (event_complete) {
|
if (event_complete) {
|
||||||
// Update last_event_id for reconnection, even for an event that
|
// Update last_event_id for reconnection, even for an event that
|
||||||
// has no data
|
// has no data. An empty id clears it.
|
||||||
if (!current_msg.id.empty()) { last_event_id_ = current_msg.id; }
|
if (has_id) { last_event_id_ = current_msg.id; }
|
||||||
|
|
||||||
// An event without a data field is not dispatched
|
// An event without a data field is not dispatched
|
||||||
if (has_data) { dispatch_event(current_msg); }
|
if (has_data) { dispatch_event(current_msg); }
|
||||||
@@ -5020,6 +5025,7 @@ inline void SSEClient::run_event_loop() {
|
|||||||
// Reset the message for the next event either way
|
// Reset the message for the next event either way
|
||||||
current_msg.clear();
|
current_msg.clear();
|
||||||
has_data = false;
|
has_data = false;
|
||||||
|
has_id = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8268,9 +8274,11 @@ struct WebSocketUpgradeResponse {
|
|||||||
std::string selected_subprotocol;
|
std::string selected_subprotocol;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline bool read_websocket_upgrade_response(Stream &strm,
|
inline bool
|
||||||
const std::string &expected_accept,
|
read_websocket_upgrade_response(Stream &strm,
|
||||||
WebSocketUpgradeResponse &upgrade) {
|
const std::string &expected_accept,
|
||||||
|
const std::string &offered_subprotocols,
|
||||||
|
WebSocketUpgradeResponse &upgrade) {
|
||||||
// Read status line
|
// Read status line
|
||||||
const auto bufsiz = 2048;
|
const auto bufsiz = 2048;
|
||||||
char buf[bufsiz];
|
char buf[bufsiz];
|
||||||
@@ -8327,6 +8335,22 @@ inline bool read_websocket_upgrade_response(Stream &strm,
|
|||||||
upgrade.selected_subprotocol = proto_it->second;
|
upgrade.selected_subprotocol = proto_it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Verify the subprotocol is one the client offered (RFC 6455 4.1)
|
||||||
|
if (!upgrade.selected_subprotocol.empty()) {
|
||||||
|
auto was_offered = false;
|
||||||
|
split(offered_subprotocols.data(),
|
||||||
|
offered_subprotocols.data() + offered_subprotocols.size(), ',',
|
||||||
|
[&](const char *b, const char *e) {
|
||||||
|
if (std::string(b, e) == upgrade.selected_subprotocol) {
|
||||||
|
was_offered = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!was_offered) {
|
||||||
|
upgrade.error = Error::WebSocketHandshake;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8972,6 +8996,9 @@ inline bool redirect(T &cli, Request &req, Response &res,
|
|||||||
new_req.method = "GET";
|
new_req.method = "GET";
|
||||||
new_req.body.clear();
|
new_req.body.clear();
|
||||||
new_req.headers.clear();
|
new_req.headers.clear();
|
||||||
|
new_req.content_length_ = 0;
|
||||||
|
new_req.content_provider_ = nullptr;
|
||||||
|
new_req.is_chunked_content_provider_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response new_res;
|
Response new_res;
|
||||||
@@ -9885,8 +9912,10 @@ inline bool range_error(Request &req, Response &res) {
|
|||||||
last_pos = content_len;
|
last_pos = content_len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RFC 9110 14.1.2: a suffix-length longer than the representation
|
||||||
|
// selects the entire representation.
|
||||||
if (first_pos == -1) {
|
if (first_pos == -1) {
|
||||||
first_pos = content_len - last_pos;
|
first_pos = (std::max)(static_cast<ssize_t>(0), content_len - last_pos);
|
||||||
last_pos = content_len - 1;
|
last_pos = content_len - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -10321,7 +10350,10 @@ inline bool perform_websocket_handshake(Stream &strm, Request &req,
|
|||||||
|
|
||||||
// Verify 101 response and Sec-WebSocket-Accept header
|
// Verify 101 response and Sec-WebSocket-Accept header
|
||||||
auto expected_accept = websocket_accept_key(client_key);
|
auto expected_accept = websocket_accept_key(client_key);
|
||||||
return read_websocket_upgrade_response(strm, expected_accept, upgrade);
|
auto offered_subprotocols =
|
||||||
|
get_combined_header_value(req.headers, "Sec-WebSocket-Protocol");
|
||||||
|
return read_websocket_upgrade_response(strm, expected_accept,
|
||||||
|
offered_subprotocols, upgrade);
|
||||||
}
|
}
|
||||||
|
|
||||||
inline bool is_ip_address(const std::string &host) {
|
inline bool is_ip_address(const std::string &host) {
|
||||||
@@ -10826,12 +10858,6 @@ inline bool verify_cert_with_windows_schannel(
|
|||||||
auto chain_guard =
|
auto chain_guard =
|
||||||
scope_exit([&] { CertFreeCertificateChain(chain_context); });
|
scope_exit([&] { CertFreeCertificateChain(chain_context); });
|
||||||
|
|
||||||
// Check if chain has errors
|
|
||||||
if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {
|
|
||||||
out_error = chain_context->TrustStatus.dwErrorStatus;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify SSL policy
|
// Verify SSL policy
|
||||||
SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};
|
SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};
|
||||||
extra_policy_para.cbSize = sizeof(extra_policy_para);
|
extra_policy_para.cbSize = sizeof(extra_policy_para);
|
||||||
@@ -14664,6 +14690,12 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
protocols.emplace_back(b, e);
|
protocols.emplace_back(b, e);
|
||||||
});
|
});
|
||||||
selected_subprotocol = entry.sub_protocol_selector(protocols);
|
selected_subprotocol = entry.sub_protocol_selector(protocols);
|
||||||
|
|
||||||
|
// Ignore a selection the client did not offer (RFC 6455 4.2.2)
|
||||||
|
if (std::find(protocols.begin(), protocols.end(),
|
||||||
|
selected_subprotocol) == protocols.end()) {
|
||||||
|
selected_subprotocol.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15734,7 +15766,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
|||||||
if (next_host.empty()) { next_host = host_; }
|
if (next_host.empty()) { next_host = host_; }
|
||||||
if (next_path.empty()) { next_path = "/"; }
|
if (next_path.empty()) { next_path = "/"; }
|
||||||
|
|
||||||
auto path = decode_path_component(next_path) + next_query;
|
auto path = std::move(next_path) + next_query;
|
||||||
|
|
||||||
// Same host redirect - use current client
|
// Same host redirect - use current client
|
||||||
if (next_scheme == scheme && next_host == host_ && next_port == port_) {
|
if (next_scheme == scheme && next_host == host_ && next_port == port_) {
|
||||||
@@ -15989,9 +16021,9 @@ inline bool ClientImpl::write_request(Stream &strm, Request &req,
|
|||||||
// Write request line and headers
|
// Write request line and headers
|
||||||
if (detail::write_request_line(bstrm, req.method, path_with_query) < 0) {
|
if (detail::write_request_line(bstrm, req.method, path_with_query) < 0) {
|
||||||
// A rejected method (not a token, e.g. carrying CR/LF) or target (e.g.
|
// A rejected method (not a token, e.g. carrying CR/LF) or target (e.g.
|
||||||
// CR/LF smuggled in via a decoded redirect Location under
|
// CR/LF in a caller-supplied path under set_path_encode(false)) must
|
||||||
// set_path_encode(false)) must fail the request cleanly instead of
|
// fail the request cleanly instead of emitting a request-line-less,
|
||||||
// emitting a request-line-less, header-injecting request.
|
// header-injecting request.
|
||||||
error = Error::Write;
|
error = Error::Write;
|
||||||
rejected_locally = true;
|
rejected_locally = true;
|
||||||
output_error_log(error, &req);
|
output_error_log(error, &req);
|
||||||
@@ -21022,29 +21054,24 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
|||||||
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
|
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
|
||||||
auto is_ip = ip_len > 0;
|
auto is_ip = ip_len > 0;
|
||||||
|
|
||||||
// Check Subject Alternative Names (SAN)
|
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
|
||||||
// In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags
|
// in buf.tag and the raw value in buf.p / buf.len.
|
||||||
// - DNS names: raw string bytes
|
|
||||||
// - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6)
|
|
||||||
const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
|
const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
|
||||||
while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
|
while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
|
||||||
const unsigned char *p = san->buf.p;
|
const unsigned char *p = san->buf.p;
|
||||||
size_t len = san->buf.len;
|
size_t len = san->buf.len;
|
||||||
|
auto san_type = san->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK;
|
||||||
|
|
||||||
if (is_ip) {
|
if (is_ip) {
|
||||||
// For an IP host, only a matching iPAddress SAN of the same family
|
// For an IP host, only a matching iPAddress SAN of the same family
|
||||||
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
|
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
|
||||||
if (len == ip_len && memcmp(p, ip_bytes, ip_len) == 0) { return true; }
|
if (san_type == MBEDTLS_X509_SAN_IP_ADDRESS && len == ip_len &&
|
||||||
} else {
|
memcmp(p, ip_bytes, ip_len) == 0) {
|
||||||
// Check if this SAN is a DNS name (printable ASCII string)
|
return true;
|
||||||
bool is_dns = len > 0;
|
|
||||||
for (size_t i = 0; i < len && is_dns; i++) {
|
|
||||||
if (p[i] < 32 || p[i] > 126) { is_dns = false; }
|
|
||||||
}
|
|
||||||
if (is_dns) {
|
|
||||||
std::string san_name(reinterpret_cast<const char *>(p), len);
|
|
||||||
if (detail::match_hostname(san_name, host_str)) { return true; }
|
|
||||||
}
|
}
|
||||||
|
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
|
||||||
|
std::string san_name(reinterpret_cast<const char *>(p), len);
|
||||||
|
if (detail::match_hostname(san_name, host_str)) { return true; }
|
||||||
}
|
}
|
||||||
san = san->next;
|
san = san->next;
|
||||||
}
|
}
|
||||||
@@ -21123,65 +21150,45 @@ inline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
|
|||||||
const mbedtls_x509_sequence *cur = &x509->subject_alt_names;
|
const mbedtls_x509_sequence *cur = &x509->subject_alt_names;
|
||||||
while (cur != nullptr) {
|
while (cur != nullptr) {
|
||||||
if (cur->buf.len > 0) {
|
if (cur->buf.len > 0) {
|
||||||
// Mbed TLS stores SAN as ASN.1 sequences
|
|
||||||
// The tag byte indicates the type
|
|
||||||
const unsigned char *p = cur->buf.p;
|
const unsigned char *p = cur->buf.p;
|
||||||
size_t len = cur->buf.len;
|
size_t value_len = cur->buf.len;
|
||||||
|
|
||||||
// First byte is the tag
|
SanEntry entry;
|
||||||
unsigned char tag = *p;
|
switch (cur->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK) {
|
||||||
p++;
|
case MBEDTLS_X509_SAN_DNS_NAME:
|
||||||
len--;
|
entry.type = SanType::DNS;
|
||||||
|
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||||
// Parse length (simple single-byte length assumed)
|
break;
|
||||||
if (len > 0 && *p < 0x80) {
|
case MBEDTLS_X509_SAN_IP_ADDRESS:
|
||||||
size_t value_len = *p;
|
entry.type = SanType::IP;
|
||||||
p++;
|
if (value_len == 4) {
|
||||||
len--;
|
// IPv4
|
||||||
|
char buf[16];
|
||||||
if (value_len <= len) {
|
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
|
||||||
SanEntry entry;
|
entry.value = buf;
|
||||||
// ASN.1 context tags for GeneralName
|
} else if (value_len == 16) {
|
||||||
switch (tag & 0x1F) {
|
// IPv6
|
||||||
case 2: // dNSName
|
char buf[64];
|
||||||
entry.type = SanType::DNS;
|
snprintf(buf, sizeof(buf),
|
||||||
entry.value =
|
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
|
||||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||||
break;
|
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9],
|
||||||
case 7: // iPAddress
|
p[10], p[11], p[12], p[13], p[14], p[15]);
|
||||||
entry.type = SanType::IP;
|
entry.value = buf;
|
||||||
if (value_len == 4) {
|
|
||||||
// IPv4
|
|
||||||
char buf[16];
|
|
||||||
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
|
|
||||||
entry.value = buf;
|
|
||||||
} else if (value_len == 16) {
|
|
||||||
// IPv6
|
|
||||||
char buf[64];
|
|
||||||
snprintf(buf, sizeof(buf),
|
|
||||||
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
|
|
||||||
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
|
||||||
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8],
|
|
||||||
p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
|
|
||||||
entry.value = buf;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 1: // rfc822Name (email)
|
|
||||||
entry.type = SanType::EMAIL;
|
|
||||||
entry.value =
|
|
||||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
|
||||||
break;
|
|
||||||
case 6: // uniformResourceIdentifier
|
|
||||||
entry.type = SanType::URI;
|
|
||||||
entry.value =
|
|
||||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
|
||||||
break;
|
|
||||||
default: entry.type = SanType::OTHER; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
|
|
||||||
}
|
}
|
||||||
|
break;
|
||||||
|
case MBEDTLS_X509_SAN_RFC822_NAME:
|
||||||
|
entry.type = SanType::EMAIL;
|
||||||
|
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||||
|
break;
|
||||||
|
case MBEDTLS_X509_SAN_UNIFORM_RESOURCE_IDENTIFIER:
|
||||||
|
entry.type = SanType::URI;
|
||||||
|
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||||
|
break;
|
||||||
|
default: entry.type = SanType::OTHER; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
|
||||||
}
|
}
|
||||||
cur = cur->next;
|
cur = cur->next;
|
||||||
}
|
}
|
||||||
@@ -22823,9 +22830,9 @@ inline void WebSocket::start_heartbeat() {
|
|||||||
if (ping_interval_sec_ == 0) { return; }
|
if (ping_interval_sec_ == 0) { return; }
|
||||||
ping_thread_ = std::thread([this]() {
|
ping_thread_ = std::thread([this]() {
|
||||||
std::unique_lock<std::mutex> lock(ping_mutex_);
|
std::unique_lock<std::mutex> lock(ping_mutex_);
|
||||||
while (!closed_) {
|
// The predicate keeps a spurious wakeup from sending a ping early
|
||||||
ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
|
while (!ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_),
|
||||||
if (closed_) { break; }
|
[this]() { return closed_.load(); })) {
|
||||||
// If the peer has failed to respond to the previous pings, give up.
|
// If the peer has failed to respond to the previous pings, give up.
|
||||||
// RFC 6455 does not define a pong-timeout mechanism; this is an
|
// RFC 6455 does not define a pong-timeout mechanism; this is an
|
||||||
// opt-in liveness check controlled by max_missed_pongs_.
|
// opt-in liveness check controlled by max_missed_pongs_.
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ if(HTTPLIB_IS_USING_OPENSSL)
|
|||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
COMMAND_ERROR_IS_FATAL ANY
|
COMMAND_ERROR_IS_FATAL ANY
|
||||||
)
|
)
|
||||||
|
# cert_san_types.pem: the bytes of each SAN read as the other type:
|
||||||
|
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=san-types -addext subjectAltName=DNS:a.zz,IP:42.46.122.122 -out cert_san_types.pem
|
||||||
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
|
COMMAND_ERROR_IS_FATAL ANY
|
||||||
|
)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(fuzzing)
|
add_subdirectory(fuzzing)
|
||||||
|
|||||||
@@ -33,3 +33,7 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=127.0.0.1" -
|
|||||||
# different address. The SAN address must match; the CN address
|
# different address. The SAN address must match; the CN address
|
||||||
# must be ignored.
|
# must be ignored.
|
||||||
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem
|
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem
|
||||||
|
|
||||||
|
# cert_san_types.pem: the bytes of each SAN read as the other type:
|
||||||
|
# DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||||
|
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=san-types" -addext "subjectAltName=DNS:a.zz,IP:42.46.122.122" -out cert_san_types.pem
|
||||||
|
|||||||
+11
-1
@@ -137,6 +137,15 @@ cert_ipv6_pem = custom_target(
|
|||||||
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=::1', '-addext', 'subjectAltName=IP:2001:db8::1', '-out', '@OUTPUT@']
|
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=::1', '-addext', 'subjectAltName=IP:2001:db8::1', '-out', '@OUTPUT@']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# cert_san_types.pem: the bytes of each SAN read as the other type: DNS:a.zz is
|
||||||
|
# 97.46.122.122, IP:42.46.122.122 is "*.zz".
|
||||||
|
cert_san_types_pem = custom_target(
|
||||||
|
'cert_san_types_pem',
|
||||||
|
input: key_pem,
|
||||||
|
output: 'cert_san_types.pem',
|
||||||
|
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=san-types', '-addext', 'subjectAltName=DNS:a.zz,IP:42.46.122.122', '-out', '@OUTPUT@']
|
||||||
|
)
|
||||||
|
|
||||||
# Copy test files to the build directory
|
# Copy test files to the build directory
|
||||||
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
||||||
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
||||||
@@ -178,7 +187,8 @@ test(
|
|||||||
client_encrypted_pbes1_key_pem,
|
client_encrypted_pbes1_key_pem,
|
||||||
client_encrypted_cert_pem,
|
client_encrypted_cert_pem,
|
||||||
cert_ip_cn_pem,
|
cert_ip_cn_pem,
|
||||||
cert_ipv6_pem
|
cert_ipv6_pem,
|
||||||
|
cert_san_types_pem
|
||||||
],
|
],
|
||||||
workdir: meson.current_build_dir(),
|
workdir: meson.current_build_dir(),
|
||||||
timeout: 300
|
timeout: 300
|
||||||
|
|||||||
+365
-11
@@ -42,6 +42,7 @@ inline std::string u8_to_string(const char8_t *s) {
|
|||||||
#define SERVER_CERT2_FILE "./cert2.pem"
|
#define SERVER_CERT2_FILE "./cert2.pem"
|
||||||
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
|
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
|
||||||
#define SERVER_CERT_IPV6_FILE "./cert_ipv6.pem"
|
#define SERVER_CERT_IPV6_FILE "./cert_ipv6.pem"
|
||||||
|
#define SERVER_CERT_SAN_TYPES_FILE "./cert_san_types.pem"
|
||||||
#define SERVER_PRIVATE_KEY_FILE "./key.pem"
|
#define SERVER_PRIVATE_KEY_FILE "./key.pem"
|
||||||
#define CA_CERT_FILE "./ca-bundle.crt"
|
#define CA_CERT_FILE "./ca-bundle.crt"
|
||||||
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
|
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
|
||||||
@@ -7241,8 +7242,21 @@ TEST_F(ServerTest, GetStreamedWithRangeSuffix1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(ServerTest, GetStreamedWithRangeSuffix2) {
|
TEST_F(ServerTest, GetStreamedWithRangeSuffix2) {
|
||||||
|
// RFC 9110 14.1.2: a suffix-length longer than the representation selects
|
||||||
|
// the entire representation.
|
||||||
|
for (auto range : {"bytes=-8", "bytes=-9999"}) {
|
||||||
|
auto res = cli_.Get("/streamed-with-range", Headers{{"Range", range}});
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::PartialContent_206, res->status) << range;
|
||||||
|
EXPECT_EQ("7", res->get_header_value("Content-Length")) << range;
|
||||||
|
EXPECT_EQ("bytes 0-6/7", res->get_header_value("Content-Range")) << range;
|
||||||
|
EXPECT_EQ(std::string("abcdefg"), res->body) << range;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(ServerTest, GetStreamedWithRangeSuffixZero) {
|
||||||
auto res =
|
auto res =
|
||||||
cli_.Get("/streamed-with-range?error", Headers{{"Range", "bytes=-9999"}});
|
cli_.Get("/streamed-with-range?error", Headers{{"Range", "bytes=-0"}});
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
|
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
|
||||||
EXPECT_EQ("0", res->get_header_value("Content-Length"));
|
EXPECT_EQ("0", res->get_header_value("Content-Length"));
|
||||||
@@ -10209,8 +10223,7 @@ TEST(RequestLineInjectionTest, RejectsInvalidCharsInTarget) {
|
|||||||
|
|
||||||
// A target carrying CR/LF, SP or other control octets must be rejected
|
// A target carrying CR/LF, SP or other control octets must be rejected
|
||||||
// before anything reaches the wire, otherwise it splits the request line and
|
// before anything reaches the wire, otherwise it splits the request line and
|
||||||
// injects a header or a whole request. This is what a decoded redirect
|
// injects a header or a whole request.
|
||||||
// Location ("%0D%0A") turns into when path encoding is disabled.
|
|
||||||
const std::string evil_targets[] = {
|
const std::string evil_targets[] = {
|
||||||
"/a\r\nInjected: pwned",
|
"/a\r\nInjected: pwned",
|
||||||
"/a\rInjected",
|
"/a\rInjected",
|
||||||
@@ -10231,9 +10244,9 @@ TEST(RequestLineInjectionTest, RejectsInvalidCharsInTarget) {
|
|||||||
TEST(RequestLineInjectionTest, ClientRejectsCRLFTargetEndToEnd) {
|
TEST(RequestLineInjectionTest, ClientRejectsCRLFTargetEndToEnd) {
|
||||||
// End-to-end counterpart to RejectsInvalidCharsInTarget. With path encoding
|
// End-to-end counterpart to RejectsInvalidCharsInTarget. With path encoding
|
||||||
// disabled the client transmits the target verbatim, so a CR/LF-bearing
|
// disabled the client transmits the target verbatim, so a CR/LF-bearing
|
||||||
// target -- what a redirect Location "%0D%0A" decodes to -- reaches
|
// target reaches write_request. The client must fail cleanly with
|
||||||
// write_request. The client must fail cleanly with Error::Write instead of
|
// Error::Write instead of putting a request-line-less, header-injecting
|
||||||
// putting a request-line-less, header-injecting request on the wire.
|
// request on the wire.
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
svr.Get("/a", [](const Request &, Response &res) {
|
svr.Get("/a", [](const Request &, Response &res) {
|
||||||
@@ -14176,9 +14189,8 @@ TEST(SSLClientTest, WindowsCertificateVerification_ServerIntermediates_Online) {
|
|||||||
<< " ssl_backend_error=" << res.ssl_backend_error();
|
<< " ssl_backend_error=" << res.ssl_backend_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Windows, not the backend, decides on the chain: the error carries a
|
// Windows, not the backend, decides on the chain: the error is a CryptoAPI
|
||||||
// CryptoAPI trust status, which no backend error for a self-signed
|
// policy status, which no backend reports for a self-signed certificate.
|
||||||
// certificate has.
|
|
||||||
TEST(SSLClientTest, WindowsCertificateVerification_RejectsUntrustedRoot) {
|
TEST(SSLClientTest, WindowsCertificateVerification_RejectsUntrustedRoot) {
|
||||||
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||||
ASSERT_TRUE(svr.is_valid());
|
ASSERT_TRUE(svr.is_valid());
|
||||||
@@ -14198,8 +14210,7 @@ TEST(SSLClientTest, WindowsCertificateVerification_RejectsUntrustedRoot) {
|
|||||||
auto res = cli.Get("/");
|
auto res = cli.Get("/");
|
||||||
ASSERT_FALSE(res);
|
ASSERT_FALSE(res);
|
||||||
EXPECT_EQ(Error::SSLServerVerification, res.error());
|
EXPECT_EQ(Error::SSLServerVerification, res.error());
|
||||||
EXPECT_NE(0u, res.ssl_backend_error() & CERT_TRUST_IS_UNTRUSTED_ROOT)
|
EXPECT_EQ(static_cast<DWORD>(CERT_E_UNTRUSTEDROOT), res.ssl_backend_error());
|
||||||
<< "ssl_backend_error=" << res.ssl_backend_error();
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -14870,6 +14881,123 @@ TEST(SSLClientServerTest, TlsVerifyHostnameIpv6San) {
|
|||||||
EXPECT_FALSE(cn_ipv6_matched)
|
EXPECT_FALSE(cn_ipv6_matched)
|
||||||
<< "An IPv6 host must not be authenticated via the certificate CN";
|
<< "An IPv6 host must not be authenticated via the certificate CN";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A SAN entry must only match a host of its own type: the bytes of the dNSName
|
||||||
|
// "a.zz" are also 97.46.122.122, and 42.46.122.122 reads as "*.zz".
|
||||||
|
TEST(SSLClientServerTest, TlsVerifyHostnameSanType) {
|
||||||
|
using namespace httplib::tls;
|
||||||
|
|
||||||
|
// SANs: DNS:a.zz, IP:42.46.122.122
|
||||||
|
SSLServer svr(SERVER_CERT_SAN_TYPES_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||||
|
ASSERT_TRUE(svr.is_valid());
|
||||||
|
|
||||||
|
svr.Get("/test", [](const Request &, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
thread t([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
bool verify_callback_called = false;
|
||||||
|
bool dns_san_matched = false;
|
||||||
|
bool ip_san_matched = false;
|
||||||
|
bool ip_matched_via_dns_san = true;
|
||||||
|
bool dns_matched_via_ip_san = true;
|
||||||
|
|
||||||
|
SSLClient cli(HOST, port);
|
||||||
|
cli.enable_server_certificate_verification(true);
|
||||||
|
cli.set_ca_cert_path(CA_CERT_FILE);
|
||||||
|
cli.set_connection_timeout(5);
|
||||||
|
|
||||||
|
cli.set_server_certificate_verifier([&](const VerifyContext &ctx) -> bool {
|
||||||
|
verify_callback_called = true;
|
||||||
|
if (!ctx.cert) return false;
|
||||||
|
|
||||||
|
dns_san_matched = ctx.check_hostname("a.zz");
|
||||||
|
ip_san_matched = ctx.check_hostname("42.46.122.122");
|
||||||
|
|
||||||
|
ip_matched_via_dns_san = ctx.check_hostname("97.46.122.122");
|
||||||
|
dns_matched_via_ip_san = ctx.check_hostname("b.zz");
|
||||||
|
|
||||||
|
return true; // Accept for the purpose of this test
|
||||||
|
});
|
||||||
|
|
||||||
|
cli.Get("/test");
|
||||||
|
|
||||||
|
ASSERT_TRUE(verify_callback_called)
|
||||||
|
<< "Verify callback should have been called";
|
||||||
|
EXPECT_TRUE(dns_san_matched) << "verify_hostname should match a dNSName SAN";
|
||||||
|
EXPECT_TRUE(ip_san_matched)
|
||||||
|
<< "verify_hostname should match an iPAddress SAN";
|
||||||
|
EXPECT_FALSE(ip_matched_via_dns_san)
|
||||||
|
<< "An IP host must not be authenticated via a dNSName SAN";
|
||||||
|
EXPECT_FALSE(dns_matched_via_ip_san)
|
||||||
|
<< "A DNS host must not be authenticated via an iPAddress SAN";
|
||||||
|
}
|
||||||
|
|
||||||
|
// sans() must report each SAN entry under its own type.
|
||||||
|
TEST(SSLClientServerTest, TlsCertSansEntryTypes) {
|
||||||
|
using namespace httplib::tls;
|
||||||
|
|
||||||
|
// SANs: DNS:a.zz, IP:42.46.122.122
|
||||||
|
SSLServer svr(SERVER_CERT_SAN_TYPES_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||||
|
ASSERT_TRUE(svr.is_valid());
|
||||||
|
|
||||||
|
svr.Get("/test", [](const Request &, Response &res) {
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
thread t([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
bool verify_callback_called = false;
|
||||||
|
std::vector<SanEntry> sans;
|
||||||
|
|
||||||
|
SSLClient cli(HOST, port);
|
||||||
|
cli.enable_server_certificate_verification(true);
|
||||||
|
cli.set_ca_cert_path(CA_CERT_FILE);
|
||||||
|
cli.set_connection_timeout(5);
|
||||||
|
|
||||||
|
cli.set_server_certificate_verifier([&](const VerifyContext &ctx) -> bool {
|
||||||
|
verify_callback_called = true;
|
||||||
|
if (!ctx.cert) return false;
|
||||||
|
|
||||||
|
sans = ctx.sans();
|
||||||
|
|
||||||
|
return true; // Accept for the purpose of this test
|
||||||
|
});
|
||||||
|
|
||||||
|
cli.Get("/test");
|
||||||
|
|
||||||
|
ASSERT_TRUE(verify_callback_called)
|
||||||
|
<< "Verify callback should have been called";
|
||||||
|
|
||||||
|
auto has_san = [&](SanType type, const std::string &value) {
|
||||||
|
return std::any_of(sans.begin(), sans.end(), [&](const SanEntry &san) {
|
||||||
|
return san.type == type && san.value == value;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_TRUE(has_san(SanType::DNS, "a.zz"))
|
||||||
|
<< "sans() should report the dNSName SAN";
|
||||||
|
EXPECT_TRUE(has_san(SanType::IP, "42.46.122.122"))
|
||||||
|
<< "sans() should report the iPAddress SAN";
|
||||||
|
|
||||||
|
EXPECT_FALSE(has_san(SanType::IP, "97.46.122.122"))
|
||||||
|
<< "sans() must not report the dNSName SAN as an address";
|
||||||
|
EXPECT_FALSE(has_san(SanType::DNS, "*.zz"))
|
||||||
|
<< "sans() must not report the iPAddress SAN as a DNS name";
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// mbedTLS-specific callback constructor test
|
// mbedTLS-specific callback constructor test
|
||||||
@@ -17496,6 +17624,138 @@ TEST(TaskQueueTest, MaxQueuedRequests) {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(RedirectTest, SeeOtherDoesNotResendContentProviderBody) {
|
||||||
|
Server svr;
|
||||||
|
std::string method;
|
||||||
|
std::string body;
|
||||||
|
auto has_content_length = false;
|
||||||
|
auto has_transfer_encoding = false;
|
||||||
|
std::atomic<int> bad_requests{0};
|
||||||
|
|
||||||
|
// Leftover body bytes get parsed as a malformed request and answered with
|
||||||
|
// 400
|
||||||
|
svr.set_logger([&](const Request & /*req*/, const Response &res) {
|
||||||
|
if (res.status == StatusCode::BadRequest_400) { bad_requests++; }
|
||||||
|
});
|
||||||
|
|
||||||
|
svr.Post("/up", [](const Request & /*req*/, Response &res) {
|
||||||
|
res.set_redirect("/down", StatusCode::SeeOther_303);
|
||||||
|
});
|
||||||
|
svr.Get("/down", [&](const Request &req, Response &res) {
|
||||||
|
method = req.method;
|
||||||
|
body = req.body;
|
||||||
|
has_content_length = req.has_header("Content-Length");
|
||||||
|
has_transfer_encoding = req.has_header("Transfer-Encoding");
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto thread = std::thread([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
const std::string payload = "SECRET-BODY";
|
||||||
|
|
||||||
|
auto check = [&](Client &cli, const Result &res) {
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("ok", res->body);
|
||||||
|
EXPECT_EQ("GET", method);
|
||||||
|
EXPECT_TRUE(body.empty());
|
||||||
|
EXPECT_FALSE(has_content_length);
|
||||||
|
EXPECT_FALSE(has_transfer_encoding);
|
||||||
|
|
||||||
|
// Nothing of the original body may be left on the connection
|
||||||
|
auto res2 = cli.Get("/down");
|
||||||
|
ASSERT_TRUE(res2) << "Error: " << to_string(res2.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res2->status);
|
||||||
|
EXPECT_EQ("ok", res2->body);
|
||||||
|
EXPECT_EQ(0, bad_requests);
|
||||||
|
};
|
||||||
|
|
||||||
|
// With content length
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
cli.set_keep_alive(true);
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
|
||||||
|
auto res = cli.Post(
|
||||||
|
"/up", payload.size(),
|
||||||
|
[&](size_t offset, size_t length, DataSink &sink) {
|
||||||
|
return sink.write(payload.data() + offset, length);
|
||||||
|
},
|
||||||
|
"text/plain");
|
||||||
|
check(cli, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Without content length (chunked)
|
||||||
|
{
|
||||||
|
Client cli(HOST, port);
|
||||||
|
cli.set_keep_alive(true);
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
|
||||||
|
auto res = cli.Post(
|
||||||
|
"/up",
|
||||||
|
[&](size_t /*offset*/, DataSink &sink) {
|
||||||
|
sink.write(payload.data(), payload.size());
|
||||||
|
sink.done();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
"text/plain");
|
||||||
|
check(cli, res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(RedirectTest, LocationPathIsNotDecoded) {
|
||||||
|
Server svr;
|
||||||
|
std::string target;
|
||||||
|
|
||||||
|
svr.Get(R"(/start/.*)", [](const Request &req, Response &res) {
|
||||||
|
// Echo the still-encoded name back in the Location
|
||||||
|
const std::string prefix = "/start/";
|
||||||
|
res.status = StatusCode::Found_302;
|
||||||
|
res.set_header("Location", "/dest/" + req.target.substr(prefix.size()));
|
||||||
|
});
|
||||||
|
svr.Get(R"(/dest.*)", [&](const Request &req, Response &res) {
|
||||||
|
target = req.target;
|
||||||
|
res.set_content("ok", "text/plain");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port(HOST);
|
||||||
|
auto thread = std::thread([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
thread.join();
|
||||||
|
ASSERT_FALSE(svr.is_running());
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
// Decoding the Location path would turn the first four into a path
|
||||||
|
// separator, a query delimiter, a fragment delimiter and a different
|
||||||
|
// percent-encoded octet. The rest must keep reaching the server as they are.
|
||||||
|
const std::vector<std::string> names = {
|
||||||
|
"a%2Fb", "x%3Fy", "x%23y", "a%2520b", "a%20b", "%C3%BC", "a-b_c.d~e",
|
||||||
|
};
|
||||||
|
|
||||||
|
for (auto path_encode : {true, false}) {
|
||||||
|
Client cli(HOST, port);
|
||||||
|
cli.set_follow_location(true);
|
||||||
|
cli.set_path_encode(path_encode);
|
||||||
|
|
||||||
|
for (const auto &name : names) {
|
||||||
|
target.clear();
|
||||||
|
auto res = cli.Get("/start/" + name);
|
||||||
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
|
EXPECT_EQ(StatusCode::OK_200, res->status);
|
||||||
|
EXPECT_EQ("/dest/" + name, target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(RedirectTest, RedirectToUrlWithQueryParameters) {
|
TEST(RedirectTest, RedirectToUrlWithQueryParameters) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
@@ -22470,6 +22730,30 @@ TEST_F(SSEParsingTest, EventWithoutDataUpdatesLastEventId) {
|
|||||||
EXPECT_EQ(msgs[0].data, "42");
|
EXPECT_EQ(msgs[0].data, "42");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(SSEParsingTest, EmptyEventIdClearsLastEventId) {
|
||||||
|
// The first connection sets an id and then clears it with an empty one; the
|
||||||
|
// second reports whether it was reconnected with a Last-Event-ID
|
||||||
|
std::atomic<int> connection_count{0};
|
||||||
|
server_->Get("/id-clear", [&](const Request &req, Response &res) {
|
||||||
|
if (connection_count++ == 0) {
|
||||||
|
res.set_content("id: 1\ndata: first\n\nid:\ndata: second\n\n",
|
||||||
|
"text/event-stream");
|
||||||
|
} else {
|
||||||
|
res.set_content(
|
||||||
|
std::string("data: ") +
|
||||||
|
(req.has_header("Last-Event-ID") ? "sent" : "not sent") +
|
||||||
|
"\n\nevent: end\ndata: end\n\n",
|
||||||
|
"text/event-stream");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto msgs = collect("/id-clear");
|
||||||
|
ASSERT_EQ(msgs.size(), 3u);
|
||||||
|
EXPECT_EQ(msgs[0].id, "1");
|
||||||
|
EXPECT_EQ(msgs[1].id, "");
|
||||||
|
EXPECT_EQ(msgs[2].data, "not sent");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
TEST_F(SSEParsingTest, CompleteEventParsing) {
|
||||||
auto msgs = parse("event: notification\nid: evt-42\n"
|
auto msgs = parse("event: notification\nid: evt-42\n"
|
||||||
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
"data: {\"type\":\"alert\"}\nretry: 1000\n\n");
|
||||||
@@ -23906,6 +24190,18 @@ protected:
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server_->WebSocket(
|
||||||
|
"/ws-subprotocol-unoffered",
|
||||||
|
[](const Request &, ws::WebSocket &ws) {
|
||||||
|
std::string msg;
|
||||||
|
while (ws.read(msg)) {
|
||||||
|
ws.send(msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[](const std::vector<std::string> &) -> std::string {
|
||||||
|
return "admin";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void start_server() {
|
void start_server() {
|
||||||
@@ -24248,6 +24544,18 @@ TEST_F(WebSocketIntegrationTest, SubProtocolNoMatch) {
|
|||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(WebSocketIntegrationTest, SubProtocolSelectorReturnsUnoffered) {
|
||||||
|
Headers headers = {{"Sec-WebSocket-Protocol", "chat"}};
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
|
||||||
|
"/ws-subprotocol-unoffered",
|
||||||
|
headers);
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
EXPECT_TRUE(client.subprotocol().empty());
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(WebSocketIntegrationTest, SubProtocolNotRequested) {
|
TEST_F(WebSocketIntegrationTest, SubProtocolNotRequested) {
|
||||||
// Connect without requesting any subprotocol
|
// Connect without requesting any subprotocol
|
||||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
|
||||||
@@ -24846,6 +25154,52 @@ TEST(WebSocketTest, ClientRejectsResponseWithoutUpgradeToken) {
|
|||||||
EXPECT_FALSE(client.is_open());
|
EXPECT_FALSE(client.is_open());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketTest, ClientRejectsUnofferedSubprotocol) {
|
||||||
|
Server svr;
|
||||||
|
svr.Get("/ws", [](const Request &req, Response &res) {
|
||||||
|
res.status = StatusCode::SwitchingProtocol_101;
|
||||||
|
res.set_header("Upgrade", "websocket");
|
||||||
|
res.set_header("Connection", "Upgrade");
|
||||||
|
res.set_header("Sec-WebSocket-Accept",
|
||||||
|
detail::websocket_accept_key(
|
||||||
|
req.get_header_value("Sec-WebSocket-Key")));
|
||||||
|
res.set_header("Sec-WebSocket-Protocol", "admin");
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port("localhost");
|
||||||
|
std::thread t([&]() { svr.listen_after_bind(); });
|
||||||
|
auto se = detail::scope_exit([&] {
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
});
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
const auto url = "ws://localhost:" + std::to_string(port) + "/ws";
|
||||||
|
|
||||||
|
// Server selects a subprotocol the client never offered
|
||||||
|
{
|
||||||
|
Headers headers = {{"Sec-WebSocket-Protocol", "chat"}};
|
||||||
|
ws::WebSocketClient client(url, headers);
|
||||||
|
|
||||||
|
auto res = client.connect();
|
||||||
|
EXPECT_FALSE(res);
|
||||||
|
EXPECT_EQ(Error::WebSocketHandshake, res.error());
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
EXPECT_TRUE(client.subprotocol().empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client offered none but the server named one anyway
|
||||||
|
{
|
||||||
|
ws::WebSocketClient client(url);
|
||||||
|
|
||||||
|
auto res = client.connect();
|
||||||
|
EXPECT_FALSE(res);
|
||||||
|
EXPECT_EQ(Error::WebSocketHandshake, res.error());
|
||||||
|
EXPECT_FALSE(client.is_open());
|
||||||
|
EXPECT_TRUE(client.subprotocol().empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(WebSocketTest, HostHeaderOverUnixSocket) {
|
TEST(WebSocketTest, HostHeaderOverUnixSocket) {
|
||||||
// The socket path doubles as the URL host, so it must not contain '/'.
|
// The socket path doubles as the URL host, so it must not contain '/'.
|
||||||
const char *shard = getenv("GTEST_SHARD_INDEX");
|
const char *shard = getenv("GTEST_SHARD_INDEX");
|
||||||
|
|||||||
Reference in New Issue
Block a user