mirror of
https://github.com/yhirose/cpp-httplib
synced 2026-06-08 18:30:49 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d7c9a788d | |||
| 1cd0347ace | |||
| b3a8af80b9 | |||
| 1e97c28e36 | |||
| d279eff4db | |||
| 188035fb6d | |||
| 125272f34b | |||
| 9ced2f614d |
@@ -28,6 +28,10 @@ jobs:
|
||||
- name: Run proxy tests (OpenSSL)
|
||||
if: matrix.tls_backend == 'openssl'
|
||||
run: cd test && make proxy
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
|
||||
- name: Run proxy tests (Mbed TLS)
|
||||
if: matrix.tls_backend == 'mbedtls'
|
||||
run: cd test && make proxy_mbedtls
|
||||
run: cd test && make proxy_mbedtls
|
||||
env:
|
||||
COMPOSE_FILE: docker-compose.yml:docker-compose.ci.yml
|
||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
||||
|
||||
[site]
|
||||
title = "cpp-httplib"
|
||||
version = "0.37.1"
|
||||
version = "0.37.2"
|
||||
hostname = "https://yhirose.github.io"
|
||||
base_path = "/cpp-httplib"
|
||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||
#define CPPHTTPLIB_HTTPLIB_H
|
||||
|
||||
#define CPPHTTPLIB_VERSION "0.37.1"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002501"
|
||||
#define CPPHTTPLIB_VERSION "0.37.2"
|
||||
#define CPPHTTPLIB_VERSION_NUM "0x002502"
|
||||
|
||||
#ifdef _WIN32
|
||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||
@@ -689,6 +689,18 @@ inline from_chars_result<double> from_chars(const char *first, const char *last,
|
||||
return {first + (endptr - s.c_str()), std::errc{}};
|
||||
}
|
||||
|
||||
inline bool parse_port(const char *s, size_t len, int &port) {
|
||||
int val = 0;
|
||||
auto r = from_chars(s, s + len, val);
|
||||
if (r.ec != std::errc{} || val < 1 || val > 65535) { return false; }
|
||||
port = val;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool parse_port(const std::string &s, int &port) {
|
||||
return parse_port(s.data(), s.size(), port);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
enum SSLVerifierResponse {
|
||||
@@ -5792,9 +5804,9 @@ inline int getaddrinfo_with_timeout(const char *node, const char *service,
|
||||
memcpy((*current)->ai_addr, sockaddr_ptr, sockaddr_len);
|
||||
|
||||
// Set port if service is specified
|
||||
if (service && strlen(service) > 0) {
|
||||
int port = atoi(service);
|
||||
if (port > 0) {
|
||||
if (service && *service) {
|
||||
int port = 0;
|
||||
if (parse_port(service, strlen(service), port)) {
|
||||
if (sockaddr_ptr->sa_family == AF_INET) {
|
||||
reinterpret_cast<struct sockaddr_in *>((*current)->ai_addr)
|
||||
->sin_port = htons(static_cast<uint16_t>(port));
|
||||
@@ -6813,6 +6825,16 @@ inline bool read_headers(Stream &strm, Headers &headers) {
|
||||
header_count++;
|
||||
}
|
||||
|
||||
// RFC 9110 Section 8.6: Reject requests with multiple Content-Length
|
||||
// headers that have different values to prevent request smuggling.
|
||||
auto cl_range = headers.equal_range("Content-Length");
|
||||
if (cl_range.first != cl_range.second) {
|
||||
const auto &first_val = cl_range.first->second;
|
||||
for (auto it = std::next(cl_range.first); it != cl_range.second; ++it) {
|
||||
if (it->second != first_val) { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -11319,6 +11341,10 @@ inline bool Server::listen_internal() {
|
||||
detail::set_socket_opt_time(sock, SOL_SOCKET, SO_SNDTIMEO,
|
||||
write_timeout_sec_, write_timeout_usec_);
|
||||
|
||||
if (tcp_nodelay_) {
|
||||
detail::set_socket_opt(sock, IPPROTO_TCP, TCP_NODELAY, 1);
|
||||
}
|
||||
|
||||
if (!task_queue->enqueue(
|
||||
[this, sock]() { process_and_close_socket(sock); })) {
|
||||
output_error_log(Error::ResourceExhaustion, nullptr);
|
||||
@@ -12708,7 +12734,7 @@ inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) {
|
||||
|
||||
auto next_port = port_;
|
||||
if (!port_str.empty()) {
|
||||
next_port = std::stoi(port_str);
|
||||
if (!detail::parse_port(port_str, next_port)) { return false; }
|
||||
} else if (!next_scheme.empty()) {
|
||||
next_port = next_scheme == "https" ? 443 : 80;
|
||||
}
|
||||
@@ -12759,18 +12785,10 @@ inline bool ClientImpl::create_redirect_client(
|
||||
// Setup basic client configuration first
|
||||
setup_redirect_client(redirect_client);
|
||||
|
||||
// SSL-specific configuration for proxy environments
|
||||
if (!proxy_host_.empty() && proxy_port_ != -1) {
|
||||
// Critical: Disable SSL verification for proxy environments
|
||||
redirect_client.enable_server_certificate_verification(false);
|
||||
redirect_client.enable_server_hostname_verification(false);
|
||||
} else {
|
||||
// For direct SSL connections, copy SSL verification settings
|
||||
redirect_client.enable_server_certificate_verification(
|
||||
server_certificate_verification_);
|
||||
redirect_client.enable_server_hostname_verification(
|
||||
server_hostname_verification_);
|
||||
}
|
||||
redirect_client.enable_server_certificate_verification(
|
||||
server_certificate_verification_);
|
||||
redirect_client.enable_server_hostname_verification(
|
||||
server_hostname_verification_);
|
||||
|
||||
// Transfer CA certificate to redirect client
|
||||
if (!ca_cert_pem_.empty()) {
|
||||
@@ -14487,7 +14505,8 @@ inline Client::Client(const std::string &scheme_host_port,
|
||||
if (host.empty()) { host = m[3].str(); }
|
||||
|
||||
auto port_str = m[4].str();
|
||||
auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
|
||||
auto port = is_ssl ? 443 : 80;
|
||||
if (!port_str.empty() && !detail::parse_port(port_str, port)) { return; }
|
||||
|
||||
if (is_ssl) {
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
@@ -19900,7 +19919,8 @@ inline WebSocketClient::WebSocketClient(
|
||||
if (host_.empty()) { host_ = m[3].str(); }
|
||||
|
||||
auto port_str = m[4].str();
|
||||
port_ = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80);
|
||||
port_ = is_ssl ? 443 : 80;
|
||||
if (!port_str.empty() && !detail::parse_port(port_str, port_)) { return; }
|
||||
|
||||
path_ = m[5].str();
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
|
||||
acl SSL_ports port 443
|
||||
acl SSL_ports port 1025-65535
|
||||
acl Safe_ports port 80 # http
|
||||
acl Safe_ports port 21 # ftp
|
||||
acl Safe_ports port 443 # https
|
||||
|
||||
@@ -15,6 +15,7 @@ acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||
|
||||
acl SSL_ports port 443
|
||||
acl SSL_ports port 1025-65535
|
||||
acl Safe_ports port 80 # http
|
||||
acl Safe_ports port 21 # ftp
|
||||
acl Safe_ports port 443 # https
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
squid_basic:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
squid_digest:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
+105
@@ -2330,6 +2330,31 @@ TEST(RedirectToDifferentPort, Redirect) {
|
||||
EXPECT_EQ("Hello World!", res->body);
|
||||
}
|
||||
|
||||
TEST(RedirectToDifferentPort, OverflowPortNumber) {
|
||||
Server svr;
|
||||
svr.Get("/redir", [&](const Request & /*req*/, Response &res) {
|
||||
// Port number that overflows int — should not crash
|
||||
res.set_redirect("http://localhost:99999999999999999999/target");
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
Client cli(HOST, port);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/redir");
|
||||
// Should fail gracefully, not crash (no valid response due to bad port)
|
||||
EXPECT_FALSE(res);
|
||||
}
|
||||
|
||||
TEST(RedirectFromPageWithContent, Redirect) {
|
||||
Server svr;
|
||||
|
||||
@@ -9441,6 +9466,18 @@ TEST(HostAndPortPropertiesTest, NoSSLWithSimpleAPI) {
|
||||
ASSERT_EQ(1234, cli.port());
|
||||
}
|
||||
|
||||
TEST(HostAndPortPropertiesTest, OverflowPortNumber) {
|
||||
// Port number that overflows int — should not crash, client becomes invalid
|
||||
httplib::Client cli("http://www.google.com:99999999999999999999");
|
||||
ASSERT_FALSE(cli.is_valid());
|
||||
}
|
||||
|
||||
TEST(HostAndPortPropertiesTest, PortOutOfRange) {
|
||||
// Port 99999 exceeds valid range (1-65535) — should not crash
|
||||
httplib::Client cli("http://www.google.com:99999");
|
||||
ASSERT_FALSE(cli.is_valid());
|
||||
}
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(HostAndPortPropertiesTest, SSL) {
|
||||
httplib::SSLClient cli("www.google.com");
|
||||
@@ -13051,6 +13088,66 @@ TEST(ClientInThreadTest, Issue2068) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST(RequestSmugglingTest, DuplicateContentLengthDifferentValues) {
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &, Response &) { handled = true; });
|
||||
|
||||
thread t = thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
ASSERT_FALSE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
// Two Content-Length headers with different values — must be rejected
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"Content-Length: 10\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("HTTP/1.1 400 Bad Request",
|
||||
response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
TEST(RequestSmugglingTest, DuplicateContentLengthSameValues) {
|
||||
auto handled = false;
|
||||
|
||||
Server svr;
|
||||
svr.Post("/test", [&](const Request &, Response &res) {
|
||||
handled = true;
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t = thread([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
ASSERT_FALSE(svr.is_running());
|
||||
ASSERT_TRUE(handled);
|
||||
});
|
||||
|
||||
svr.wait_until_ready();
|
||||
|
||||
// Two Content-Length headers with same value — should be accepted (RFC 9110)
|
||||
auto req = "POST /test HTTP/1.1\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"Content-Length: 5\r\n"
|
||||
"\r\n"
|
||||
"hello";
|
||||
|
||||
std::string response;
|
||||
ASSERT_TRUE(send_request(1, req, &response));
|
||||
ASSERT_EQ("HTTP/1.1 200 OK", response.substr(0, response.find("\r\n")));
|
||||
}
|
||||
|
||||
TEST(HeaderSmugglingTest, ChunkedTrailerHeadersMerged) {
|
||||
Server svr;
|
||||
|
||||
@@ -16425,6 +16522,14 @@ TEST(WebSocketTest, InvalidURL) {
|
||||
// Missing host
|
||||
ws::WebSocketClient ws4("ws://:8080/path");
|
||||
EXPECT_FALSE(ws4.is_valid());
|
||||
|
||||
// Port number overflow — should not crash
|
||||
ws::WebSocketClient ws5("ws://localhost:99999999999999999999/path");
|
||||
EXPECT_FALSE(ws5.is_valid());
|
||||
|
||||
// Port out of range
|
||||
ws::WebSocketClient ws6("ws://localhost:99999/path");
|
||||
EXPECT_FALSE(ws6.is_valid());
|
||||
}
|
||||
|
||||
TEST(WebSocketTest, UnsupportedScheme) {
|
||||
|
||||
@@ -109,6 +109,55 @@ TEST(RedirectTest, YouTubeSSLDigest) {
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||
TEST(RedirectTest, TLSVerificationOnProxyRedirect) {
|
||||
// Untrusted HTTPS server with self-signed cert
|
||||
SSLServer untrusted_svr("cert.pem", "key.pem");
|
||||
untrusted_svr.Get("/", [](const Request &, Response &res) {
|
||||
res.set_content("MITM'd", "text/plain");
|
||||
});
|
||||
|
||||
auto untrusted_port = untrusted_svr.bind_to_any_port("0.0.0.0");
|
||||
auto t1 = thread([&]() { untrusted_svr.listen_after_bind(); });
|
||||
auto se1 = detail::scope_exit([&] {
|
||||
untrusted_svr.stop();
|
||||
t1.join();
|
||||
});
|
||||
|
||||
// HTTP server that redirects to the untrusted HTTPS server
|
||||
// Use host.docker.internal so the proxy container can reach the host
|
||||
Server redirect_svr;
|
||||
redirect_svr.Get("/", [&](const Request &, Response &res) {
|
||||
res.set_redirect(
|
||||
"https://host.docker.internal:" + to_string(untrusted_port) + "/");
|
||||
});
|
||||
|
||||
auto redirect_port = redirect_svr.bind_to_any_port("0.0.0.0");
|
||||
auto t2 = thread([&]() { redirect_svr.listen_after_bind(); });
|
||||
auto se2 = detail::scope_exit([&] {
|
||||
redirect_svr.stop();
|
||||
t2.join();
|
||||
});
|
||||
|
||||
// Wait until servers are up
|
||||
untrusted_svr.wait_until_ready();
|
||||
redirect_svr.wait_until_ready();
|
||||
|
||||
// Client with proxy + follow_location, verification ON (default)
|
||||
Client cli("host.docker.internal", redirect_port);
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
cli.set_follow_location(true);
|
||||
|
||||
auto res = cli.Get("/");
|
||||
|
||||
// Self-signed cert must be rejected
|
||||
ASSERT_TRUE(res == nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
template <typename T> void BaseAuthTestFromHTTPWatch(T &cli) {
|
||||
cli.set_proxy("localhost", 3128);
|
||||
cli.set_proxy_basic_auth("hello", "world");
|
||||
|
||||
Reference in New Issue
Block a user