Compare commits

..
5 Commits
Author SHA1 Message Date
yhirose 24e58ce5b2 Release v0.64.0 2026-10-29 17:08:34 -04:00
Kim, Hyuk a0ab09fc0a Add set_basic_auth and set_bearer_token_auth to WebSocketClient (#2652)
ClientImpl and SSLClient let callers set origin credentials with
set_basic_auth()/set_bearer_token_auth(), but WebSocketClient could only
take a fixed Authorization header through its constructor. Add both
setters: each connect() adds the Authorization header to the upgrade
request from the current credentials (Basic over Bearer), unless the
constructor headers already carry one, and never to the proxy's CONNECT
request.
2026-10-28 16:15:17 -04:00
Kim, Hyuk 290c4c0bbe Add set_tunnel_hostname_addr_map() and deprecate set_hostname_addr_map() (#2647)
Through a CONNECT proxy it is the proxy that connects to the origin, and the CONNECT target was always the original hostname, so there was no way to send the tunnel to another address. set_tunnel_hostname_addr_map() sets that target on SSLClient, Client and WebSocketClient. The CONNECT request's Host follows it, including the Digest retry, while the Host header, the SNI and the certificate check inside the tunnel keep the original hostname. The map is also copied to a redirect client.

set_hostname_addr_map() is deprecated in favor of set_direct_hostname_addr_map(), which behaves the same and names the connection it applies to. The two maps are independent and correspond to curl's --resolve and --connect-to; neither applies to the proxy's own address.
2026-10-27 21:09:25 -04:00
metsw24-max 25ba40f6fe Reject an EOF without close_notify in the Mbed TLS and wolfSSL read (#2649)
Mbed TLS and wolfSSL read a TLS EOF without close_notify as a clean end, so a response delimited by the connection close could be cut short and still come back complete. Both reads now end cleanly only on a close_notify, as on OpenSSL.
2026-10-27 18:31:55 -04:00
metsw24-max 578e4815b7 Treat ':' as a path separator in sanitize_filename on Windows (#2651)
On Windows a ':' ends a drive prefix or starts an NTFS stream name, so a filename such as "C:.." or "a.txt:stream" survived sanitize_filename and could replace the upload directory or write to an alternate data stream. Cut at ':' on Windows as well.
2026-10-27 18:16:21 -04:00
7 changed files with 712 additions and 52 deletions

No files matched your search

+22 -1
View File
@@ -200,6 +200,10 @@ void close(CloseStatus status = CloseStatus::Normal,
const std::string &reason = "");
bool is_open() const;
// Authentication - the same setters Client has (Basic wins over Bearer).
void set_basic_auth(const std::string &username, const std::string &password);
void set_bearer_token_auth(const std::string &token);
// Proxy - the same setters Client has. connect() opens a CONNECT tunnel
// through the proxy for ws:// as well as wss:// (RFC 6455 4.1); the proxy
// credentials are sent only on the CONNECT request, never through the tunnel.
@@ -233,7 +237,8 @@ void set_address_family(int family);
void set_ipv6_v6only(bool on);
void set_socket_options(SocketOptions socket_options);
void set_interface(const std::string &intf);
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
// SSL configuration (wss:// only, SSL builds only)
void set_ca_cert_path(const std::string &ca_cert_file_path,
@@ -403,6 +408,22 @@ svr.set_pre_request_handler([](const httplib::Request &req, httplib::Response &r
The server accepts a handshake from any `Origin`. A browser sends its cookies with a handshake another site starts, so if cookies authenticate the connection, also check `req.get_header_value("Origin")` against your own origins in one of these handlers (cross-site WebSocket hijacking).
### Authentication
```cpp
httplib::ws::WebSocketClient ws("wss://example.com/ws");
ws.set_bearer_token_auth("token123"); // Authorization: Bearer token123
// or
ws.set_basic_auth("user", "pass"); // Authorization: Basic dXNlcjpwYXNz
if (ws.connect()) { /* ... */ }
```
The credentials are read on every `connect()`, so a new token takes effect on
the next reconnect. An `Authorization` header passed to the constructor wins
over them. Through a proxy they are sent only inside the tunnel, never on
`CONNECT`.
### Custom Headers and Timeouts
```cpp
+33 -9
View File
@@ -742,7 +742,7 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
auto safe = httplib::sanitize_filename(file.filename);
```
This function keeps only the part after the last `/` or `\`, removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`.
This function keeps only the part after the last `/` or `\` (or, on Windows, `:`, which starts a drive prefix such as `C:` or an NTFS stream name), removes null bytes, and trims leading and trailing spaces and tabs. It returns an empty string if what is left is empty, `.` or `..`.
> [!NOTE]
> `name` and `filename` are read as quoted-strings (RFC 9110 §5.6.4), where `\"` and `\\` stand for `"` and `\`. Browsers send a backslash as it is, so from a browser, a value with `\\` in it arrives with a single `\`, and one that ends in `\` is not read correctly.
@@ -1477,17 +1477,17 @@ The same method is available on `httplib::ws::WebSocketClient`.
### Override the connection target for a hostname
`set_hostname_addr_map` redirects where the socket connects, without changing
the identity of the request. The hostname the client was constructed with keeps
supplying the `Host` header, the SNI, and the name that the server certificate
is verified against, so this is a connection-level override only, not a way to
talk to a different origin.
`set_direct_hostname_addr_map` redirects where the socket connects, without
changing the identity of the request. The hostname the client was constructed
with keeps supplying the `Host` header, the SNI, and the name that the server
certificate is verified against, so this is a connection-level override only,
not a way to talk to a different origin.
```cpp
httplib::Client cli("https://example.com");
// Connect to this IP address instead of resolving "example.com"
cli.set_hostname_addr_map({{"example.com", "192.168.1.10"}});
cli.set_direct_hostname_addr_map({{"example.com", "192.168.1.10"}});
```
A mapped value may be an IP literal or another hostname. An IP literal is used
@@ -1495,13 +1495,37 @@ as-is; anything else is resolved as a name, so a host that is only reachable
under a different name works too:
```cpp
cli.set_hostname_addr_map({{"example.com", "internal.example.lan"}});
cli.set_direct_hostname_addr_map({{"example.com", "internal.example.lan"}});
```
An empty value is ignored, leaving the original hostname as the connection
target.
The same method is available on `httplib::ws::WebSocketClient`.
The direct map applies only when the client connects to the origin itself.
Through a proxy, use `set_tunnel_hostname_addr_map`: its value becomes the
`CONNECT` target (and that request's `Host`) that the proxy is asked to connect
to (HTTPS and WebSocket). The `Host` header, the SNI and the certificate check
inside the tunnel keep the original hostname:
```cpp
httplib::Client cli("https://sni.example");
cli.set_tunnel_hostname_addr_map({{"sni.example", "server.example"}});
cli.set_proxy("proxy.example", 3128);
// CONNECT server.example:443, SNI sni.example
```
The two maps are independent; to use the same target on both paths, set both.
They correspond to curl's `--resolve` and `--connect-to`.
Neither map applies to the proxy's own address ("direct" means not through a
proxy, as in a PAC file's `DIRECT`). To connect to the proxy at a fixed
address, pass an IP literal to `set_proxy()`.
`set_hostname_addr_map` is deprecated and works the same as
`set_direct_hostname_addr_map`.
These methods are also available on `httplib::ws::WebSocketClient`.
### Automatic Path Encoding
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.63.0"
version = "0.64.0"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // The port is unused, but must not be 0
// Client
httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX);
cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
auto res = cli.Get("/");
```
+1 -1
View File
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // ポート番号は使われない(0
// クライアント
httplib::Client cli("http://localhost");
cli.set_address_family(AF_UNIX);
cli.set_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
cli.set_direct_hostname_addr_map({{"localhost", "/tmp/httplib.sock"}});
auto res = cli.Get("/");
```
+114 -19
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.63.0"
#define CPPHTTPLIB_VERSION_NUM "0x003f00"
#define CPPHTTPLIB_VERSION "0.64.0"
#define CPPHTTPLIB_VERSION_NUM "0x004000"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -2927,7 +2927,12 @@ public:
size_t is_socket_open() const;
socket_t socket() const;
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers);
@@ -3059,6 +3064,9 @@ protected:
// hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> tunnel_addr_map_;
// Default headers
Headers default_headers_;
@@ -3348,7 +3356,12 @@ public:
size_t is_socket_open() const;
socket_t socket() const;
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_default_headers(Headers headers);
@@ -4748,7 +4761,15 @@ public:
set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
void set_interface(const std::string &intf);
[[deprecated("Use set_direct_hostname_addr_map() instead.")]]
void set_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_direct_hostname_addr_map(std::map<std::string, std::string> addr_map);
void
set_tunnel_hostname_addr_map(std::map<std::string, std::string> addr_map);
void set_basic_auth(const std::string &username, const std::string &password);
void set_bearer_token_auth(const std::string &token);
void set_proxy(const std::string &host, int port);
void set_proxy_basic_auth(const std::string &username,
@@ -4820,6 +4841,13 @@ private:
// hostname; only the connection target changes, never the identity.
std::map<std::string, std::string> addr_map_;
// Hostname to CONNECT target map, used only through a proxy.
std::map<std::string, std::string> tunnel_addr_map_;
std::string basic_auth_username_;
std::string basic_auth_password_;
std::string bearer_token_auth_token_;
std::string proxy_host_;
int proxy_port_ = -1;
std::string proxy_basic_auth_username_;
@@ -11975,8 +12003,13 @@ inline std::string decode_query_component(const std::string &component,
}
inline std::string sanitize_filename(const std::string &filename) {
// Extract basename: find the last path separator (/ or \)
// Extract basename: find the last path separator (/ or \) and, on Windows,
// ':', which ends a drive prefix ("C:..") or names an NTFS stream ("a:b")
#ifdef _WIN32
auto pos = filename.find_last_of("/\\:");
#else
auto pos = filename.find_last_of("/\\");
#endif
auto result =
(pos != std::string::npos) ? filename.substr(pos + 1) : filename;
@@ -13041,6 +13074,17 @@ make_host_and_port_string_always_port(const std::string &host, int port) {
return prepare_host_string(host) + ":" + std::to_string(port);
}
// Request target of a CONNECT for `host`, with `addr_map` applied.
inline std::string
make_connect_request_target(const std::map<std::string, std::string> &addr_map,
const std::string &host, int port) {
std::string connect_host;
std::string ip;
apply_addr_map(addr_map, host, connect_host, ip);
return make_host_and_port_string_always_port(ip.empty() ? connect_host : ip,
port);
}
// Value for the Host header a client sends when the caller supplied none.
// Only the value: callers decide where in their header list it goes.
inline std::string make_default_host_header_value(const std::string &host,
@@ -16569,6 +16613,7 @@ inline void ClientImpl::setup_redirect_client(ClientType &client) {
// Copy network and socket settings
client.addr_map_ = addr_map_;
client.tunnel_addr_map_ = tunnel_addr_map_;
client.set_address_family(address_family_);
client.set_tcp_nodelay(tcp_nodelay_);
client.set_ipv6_v6only(ipv6_v6only_);
@@ -18081,9 +18126,19 @@ inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
inline void
ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
set_direct_hostname_addr_map(std::move(addr_map));
}
inline void ClientImpl::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
inline void ClientImpl::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
tunnel_addr_map_ = std::move(addr_map);
}
inline void ClientImpl::set_default_headers(Headers headers) {
default_headers_ = std::move(headers);
}
@@ -18792,7 +18847,17 @@ inline socket_t Client::socket() const { return cli_->socket(); }
inline void
Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
cli_->set_hostname_addr_map(std::move(addr_map));
cli_->set_direct_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
cli_->set_direct_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
cli_->set_tunnel_hostname_addr_map(std::move(addr_map));
}
inline void Client::set_default_headers(Headers headers) {
@@ -19160,6 +19225,10 @@ inline bool SSLClient::connect_with_proxy(
std::chrono::time_point<std::chrono::steady_clock> start_time,
Response &res, bool &success, Error &error) {
success = true;
auto connect_target =
detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
auto retargeted = connect_target !=
detail::make_host_and_port_string_always_port(host_, port_);
Response proxy_res;
if (!detail::process_client_socket(
socket.sock, read_timeout_sec_, read_timeout_usec_,
@@ -19167,8 +19236,8 @@ inline bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) {
Request req2;
req2.method = "CONNECT";
req2.path =
detail::make_host_and_port_string_always_port(host_, port_);
req2.path = connect_target;
if (retargeted) { req2.set_header("Host", connect_target); }
if (max_timeout_msec_ > 0) {
req2.start_time_ = std::chrono::steady_clock::now();
}
@@ -19207,8 +19276,8 @@ inline bool SSLClient::connect_with_proxy(
start_time, [&](Stream &strm) {
Request req3;
req3.method = "CONNECT";
req3.path = detail::make_host_and_port_string_always_port(
host_, port_);
req3.path = connect_target;
if (retargeted) { req3.set_header("Host", connect_target); }
req3.headers.insert(detail::make_digest_authentication_header(
req3, make_request_target(req3), auth, 1,
detail::random_string(10), proxy_digest_auth_username_,
@@ -21040,6 +21109,11 @@ inline ErrorCode map_mbedtls_error(int ret, int &out_errno,
out_errno = errno;
return ErrorCode::SyscallError;
}
if (ret == MBEDTLS_ERR_SSL_CONN_EOF) {
// EOF without close_notify
out_errno = 0;
return ErrorCode::SyscallError;
}
if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
// Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
// the handshake's chain verification (see set_sni()); a mismatch there
@@ -21127,7 +21201,7 @@ inline int mbedtls_net_recv_cb(void *ctx, unsigned char *buf, size_t len) {
return MBEDTLS_ERR_NET_RECV_FAILED;
}
#endif
if (ret == 0) { return MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY; }
// EOF is MBEDTLS_ERR_SSL_CONN_EOF; only a close_notify is a clean end
return static_cast<int>(ret);
}
@@ -21783,10 +21857,8 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
return static_cast<ssize_t>(ret);
}
if (ret == 0) {
err.code = ErrorCode::PeerClosed;
return 0;
}
// 0 means EOF without close_notify, which is an error as on OpenSSL
if (ret == 0) { ret = MBEDTLS_ERR_SSL_CONN_EOF; }
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
err.backend_code = static_cast<uint64_t>(-ret);
@@ -22393,6 +22465,8 @@ inline ErrorCode map_wolfssl_error(WOLFSSL *ssl, int ssl_error,
case SSL_ERROR_WANT_WRITE: return ErrorCode::WantWrite;
case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;
// EOF without close_notify
case SOCKET_PEER_CLOSED_E: out_errno = 0; return ErrorCode::SyscallError;
default:
if (ssl) {
// wolfSSL stores the low-level error code as a negative value.
@@ -22963,13 +23037,10 @@ inline ssize_t read(session_t session, void *buf, size_t len, TlsError &err) {
return static_cast<ssize_t>(ret);
}
if (ret == 0) {
err.code = ErrorCode::PeerClosed;
return 0;
}
// 0 is also returned for EOF without close_notify; get_error tells them apart
int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
if (err.code == ErrorCode::PeerClosed) { return 0; }
err.backend_code = static_cast<uint64_t>(ssl_error);
impl::wolfssl_last_error() = err.backend_code;
return -1;
@@ -23926,6 +23997,9 @@ inline void WebSocketClient::prepare_default_headers(Request &req) {
}
detail::add_default_user_agent_header(req);
detail::add_authorization_header(req, basic_auth_username_,
basic_auth_password_,
bearer_token_auth_token_, false);
}
inline Result WebSocketClient::connect() {
@@ -24051,9 +24125,19 @@ inline void WebSocketClient::set_interface(const std::string &intf) {
inline void WebSocketClient::set_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
set_direct_hostname_addr_map(std::move(addr_map));
}
inline void WebSocketClient::set_direct_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
addr_map_ = std::move(addr_map);
}
inline void WebSocketClient::set_tunnel_hostname_addr_map(
std::map<std::string, std::string> addr_map) {
tunnel_addr_map_ = std::move(addr_map);
}
inline bool WebSocketClient::is_proxy_enabled() const {
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
if (no_proxy_entries_.empty()) { return true; }
@@ -24103,7 +24187,8 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
Error &error) {
Request req;
req.method = "CONNECT";
req.path = detail::make_host_and_port_string_always_port(host_, port_);
req.path =
detail::make_connect_request_target(tunnel_addr_map_, host_, port_);
req.headers.emplace("Host", req.path);
detail::add_default_user_agent_header(req);
detail::add_authorization_header(req, proxy_basic_auth_username_,
@@ -24137,6 +24222,16 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
return true;
}
inline void WebSocketClient::set_basic_auth(const std::string &username,
const std::string &password) {
basic_auth_username_ = username;
basic_auth_password_ = password;
}
inline void WebSocketClient::set_bearer_token_auth(const std::string &token) {
bearer_token_auth_token_ = token;
}
inline void WebSocketClient::set_proxy(const std::string &host, int port) {
proxy_host_ = host;
proxy_port_ = port;
+540 -20
View File
@@ -72,6 +72,17 @@ namespace tls {
ca_store_t create_ca_store(const char *pem, size_t len);
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
std::vector<std::string> get_ca_names(ctx_t ctx);
ctx_t create_server_context();
void free_context(ctx_t ctx);
bool set_server_cert_file(ctx_t ctx, const char *cert_path,
const char *key_path, const char *password);
session_t create_session(ctx_t ctx, socket_t sock);
void free_session(session_t session);
bool accept_nonblocking(session_t session, socket_t sock, time_t timeout_sec,
time_t timeout_usec, TlsError *err);
ssize_t read(session_t session, void *buf, size_t len, TlsError &err);
ssize_t write(session_t session, const void *buf, size_t len, TlsError &err);
void shutdown(session_t session, bool graceful);
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
namespace impl {
SecTrustSettingsResult
@@ -508,6 +519,20 @@ TEST(SanitizeFilenameTest, VariousPatterns) {
EXPECT_EQ("", httplib::sanitize_filename(" "));
}
// On Windows a ':' ends a drive prefix or names an NTFS stream, so what comes
// before it is dropped; anywhere else it is an ordinary file name character.
TEST(SanitizeFilenameTest, ColonEndsPrefixOnlyOnWindows) {
#ifdef _WIN32
EXPECT_EQ("", httplib::sanitize_filename("C:.."));
EXPECT_EQ("evil.dll", httplib::sanitize_filename("C:evil.dll"));
EXPECT_EQ("stream", httplib::sanitize_filename("a.txt:stream"));
#else
EXPECT_EQ("C:..", httplib::sanitize_filename("C:.."));
EXPECT_EQ("C:evil.dll", httplib::sanitize_filename("C:evil.dll"));
EXPECT_EQ("a.txt:stream", httplib::sanitize_filename("a.txt:stream"));
#endif
}
// Forward declaration (see the base64_encode note below): split builds move the
// definition into httplib.cc, so re-declaring it here lets the test link.
namespace httplib {
@@ -3723,7 +3748,7 @@ TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) {
Client cli(host);
#endif
cli.set_hostname_addr_map({{another_host, wrong_ip}});
cli.set_direct_hostname_addr_map({{another_host, wrong_ip}});
auto res = cli.Get("/");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
@@ -3739,7 +3764,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
Client cli(host);
#endif
cli.set_hostname_addr_map({{host, wrong_ip}});
cli.set_direct_hostname_addr_map({{host, wrong_ip}});
auto res = cli.Get("/");
ASSERT_TRUE(!res);
EXPECT_EQ(Error::Connection, res.error());
@@ -3770,7 +3795,7 @@ TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) {
svr.wait_until_ready();
Client cli(host, port);
cli.set_hostname_addr_map({{host, HOST}});
cli.set_direct_hostname_addr_map({{host, HOST}});
auto res = cli.Get("/hi");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
@@ -3800,7 +3825,7 @@ TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) {
svr.wait_until_ready();
Client cli(host, port);
cli.set_hostname_addr_map({{host, "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{host, "127.0.0.1"}});
auto res = cli.Get("/hi");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
@@ -3833,13 +3858,73 @@ TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) {
svr.wait_until_ready();
Client cli(blackhole, port);
cli.set_hostname_addr_map({{blackhole, ""}});
cli.set_direct_hostname_addr_map({{blackhole, ""}});
cli.set_connection_timeout(1);
auto res = cli.Get("/hi");
EXPECT_FALSE(res) << "empty mapping must not redirect to loopback";
}
TEST(SpecifyServerIPAddressTest, DeprecatedSetHostnameAddrMapStillWorks) {
auto host = "target.invalid";
Server svr;
std::string received_host;
svr.Get("/hi", [&](const Request &req, Response &res) {
received_host = req.get_header_value("Host");
res.set_content("Hello World!", "text/plain");
});
auto port = svr.bind_to_any_port("127.0.0.1");
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);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
cli.set_hostname_addr_map({{host, "127.0.0.1"}});
#pragma GCC diagnostic pop
auto res = cli.Get("/hi");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host);
}
TEST(TunnelHostnameAddrMapTest, IgnoredOnDirectConnection) {
// 192.0.2.1 is TEST-NET-1: applying the map would fail to connect.
Server svr;
svr.Get("/hi", [](const Request & /*req*/, Response &res) {
res.set_content("Hello World!", "text/plain");
});
auto port = svr.bind_to_any_port("127.0.0.1");
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("127.0.0.1", port);
cli.set_tunnel_hostname_addr_map({{"127.0.0.1", "192.0.2.1"}});
cli.set_connection_timeout(1);
auto res = cli.Get("/hi");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
}
TEST(AbsoluteRedirectTest, Redirect_Online) {
auto host = "httpbingo.org";
auto path = std::string{"/absolute-redirect/3"};
@@ -13322,7 +13407,7 @@ TEST(SNI_AutoDetectionTest, FailedHandshakeLeavesNoSNI) {
{
// Sends SNI, then rejects the server certificate, which the CA did not sign
SSLClient cli("localhost", port);
cli.set_hostname_addr_map({{"localhost", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"localhost", "127.0.0.1"}});
cli.set_ca_cert_path(CLIENT_CA_CERT_FILE);
auto res = cli.Get("/sni");
ASSERT_FALSE(res);
@@ -16411,7 +16496,7 @@ TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) {
svr.wait_until_ready();
SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
cli.set_hostname_addr_map({{c.host, "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{c.host, "127.0.0.1"}});
cli.set_ca_cert_path(c.trusted_ca);
if (with_verifier) {
cli.set_server_certificate_verifier(
@@ -21505,6 +21590,36 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) {
detail::make_host_and_port_string("example.com", 65536, false));
}
namespace httplib {
namespace detail {
std::string
make_connect_request_target(const std::map<std::string, std::string> &addr_map,
const std::string &host, int port);
} // namespace detail
} // namespace httplib
TEST(MakeConnectRequestTargetTest, FollowsHostnameAddrMap) {
std::map<std::string, std::string> addr_map = {
{"ipv4.example", "192.0.2.10"},
{"ipv6.example", "2001:db8::1"},
{"name.example", "internal.example.lan"},
{"empty.example", ""}};
EXPECT_EQ("192.0.2.10:443",
detail::make_connect_request_target(addr_map, "ipv4.example", 443));
EXPECT_EQ("[2001:db8::1]:443",
detail::make_connect_request_target(addr_map, "ipv6.example", 443));
EXPECT_EQ("internal.example.lan:8443", detail::make_connect_request_target(
addr_map, "name.example", 8443));
// An empty or absent mapping keeps the hostname as the target
EXPECT_EQ("empty.example:443", detail::make_connect_request_target(
addr_map, "empty.example", 443));
EXPECT_EQ("other.example:443", detail::make_connect_request_target(
addr_map, "other.example", 443));
EXPECT_EQ("[::1]:443", detail::make_connect_request_target({}, "::1", 443));
}
#ifdef CPPHTTPLIB_SSL_ENABLED
TEST(SSLClientHostHeaderTest, Issue2301_Online) {
httplib::SSLClient cli("roblox.com", 443);
@@ -24053,6 +24168,115 @@ TEST(StreamBodyEndTest, BodyEndingAtConnectionClose) {
});
}
#ifdef CPPHTTPLIB_SSL_ENABLED
// Serves `response` to one request, then closes with or without a close_notify
template <typename Fn>
static void with_single_tls_response(const std::string &response,
bool close_notify, Fn fn) {
#ifndef _WIN32
signal(SIGPIPE, SIG_IGN);
#endif
std::promise<int> port_promise;
auto port_future = port_promise.get_future();
auto server_thread = std::thread([&port_promise, response, close_notify] {
auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
default_socket_options(srv);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(0);
::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
if (::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) != 0 ||
::listen(srv, 1) != 0) {
port_promise.set_value(-1);
detail::close_socket(srv);
return;
}
socklen_t addr_len = sizeof(addr);
::getsockname(srv, reinterpret_cast<sockaddr *>(&addr), &addr_len);
port_promise.set_value(static_cast<int>(ntohs(addr.sin_port)));
auto cli = ::accept(srv, nullptr, nullptr);
if (cli != INVALID_SOCKET) {
auto ctx = tls::create_server_context();
tls::set_server_cert_file(ctx, SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE,
nullptr);
auto session = tls::create_session(ctx, cli);
tls::TlsError err;
if (tls::accept_nonblocking(session, cli, 5, 0, &err)) {
// The request head first, so that closing the socket sends a FIN
// rather than an RST for the unread bytes.
std::string req;
char buf[256];
while (req.find("\r\n\r\n") == std::string::npos) {
auto n = tls::read(session, buf, sizeof(buf), err);
if (n <= 0) { break; }
req.append(buf, static_cast<size_t>(n));
}
size_t sent = 0;
while (sent < response.size()) {
auto n = tls::write(session, response.data() + sent,
response.size() - sent, err);
if (n <= 0) { break; }
sent += static_cast<size_t>(n);
}
if (close_notify) { tls::shutdown(session, true); }
}
tls::free_session(session);
detail::close_socket(cli);
tls::free_context(ctx);
}
detail::close_socket(srv);
});
auto se = detail::scope_exit([&] { server_thread.join(); });
auto port = port_future.get();
ASSERT_GT(port, 0);
SSLClient cli("127.0.0.1", port);
cli.enable_server_certificate_verification(false);
fn(cli);
}
// RFC 8446 6.1: a close-delimited body is complete only after a close_notify
TEST(SSLClientTest, UnframedBodyEndsOnlyAtCloseNotify) {
const std::string close_delimited = "HTTP/1.1 200 OK\r\n"
"Connection: close\r\n"
"\r\n"
"hello";
with_single_tls_response(close_delimited, true, [](SSLClient &cli) {
auto res = cli.Get("/");
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ("hello", res->body);
});
with_single_tls_response(close_delimited, false, [](SSLClient &cli) {
auto res = cli.Get("/");
ASSERT_FALSE(res);
EXPECT_EQ(Error::Read, res.error());
});
with_single_tls_response(close_delimited, false, [](SSLClient &cli) {
auto handle = cli.open_stream("GET", "/");
ASSERT_TRUE(handle.is_valid()) << to_string(handle.error);
read_all(handle);
EXPECT_TRUE(handle.has_read_error());
});
// A framed body ends where its framing says, with or without the alert
const std::string framed = "HTTP/1.1 200 OK\r\n"
"Content-Length: 5\r\n"
"Connection: close\r\n"
"\r\n"
"hello";
with_single_tls_response(framed, false, [](SSLClient &cli) {
auto res = cli.Get("/");
ASSERT_TRUE(res) << to_string(res.error());
EXPECT_EQ("hello", res->body);
});
}
#endif
TEST(InterimResponseTest, SkippedWithTheirHeaderFields) {
const char *responses[] = {
"HTTP/1.1 103 Early Hints\r\n"
@@ -27774,7 +27998,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_AnotherHostname) {
auto wrong_ip = "192.0.2.1";
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
client.set_hostname_addr_map({{another_host, wrong_ip}});
client.set_direct_hostname_addr_map({{another_host, wrong_ip}});
ASSERT_TRUE(client.connect());
EXPECT_TRUE(client.is_open());
@@ -27798,7 +28022,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
auto wrong_ip = "192.0.2.1";
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
client.set_hostname_addr_map({{"localhost", wrong_ip}});
client.set_direct_hostname_addr_map({{"localhost", wrong_ip}});
client.set_connection_timeout(1);
client.set_read_timeout(1);
client.set_write_timeout(1);
@@ -27838,7 +28062,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) {
ws::WebSocketClient client("ws://" + std::string(host) + ":" +
std::to_string(port) + "/ws");
client.set_hostname_addr_map({{host, HOST}});
client.set_direct_hostname_addr_map({{host, HOST}});
ASSERT_TRUE(client.connect());
EXPECT_TRUE(client.is_open());
@@ -30827,7 +31051,7 @@ private:
inline std::unique_ptr<Client> make_client(const std::string &host,
ProxyAndTargetServers &s) {
auto cli = detail::make_unique<Client>(host, s.target_port());
cli->set_hostname_addr_map({{host, "127.0.0.1"}});
cli->set_direct_hostname_addr_map({{host, "127.0.0.1"}});
cli->set_proxy("127.0.0.1", s.proxy_port());
return cli;
}
@@ -31036,7 +31260,7 @@ TEST(NoProxyTest, IPv4LiteralExactMatch) {
TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) {
ProxyAndTargetServers s;
Client cli("0:0:0:0:0:0:0:1", s.target_port());
cli.set_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"0:0:0:0:0:0:0:1", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"::1"});
@@ -31053,7 +31277,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
// host as a hostname and miss the match.
ProxyAndTargetServers s;
Client cli("fe80::1", s.target_port());
cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"fe80::/10"});
@@ -31066,7 +31290,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
ProxyAndTargetServers s;
Client cli("::1", s.target_port());
cli.set_hostname_addr_map({{"::1", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"::1", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"[::1]"});
@@ -31079,7 +31303,7 @@ TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) {
ProxyAndTargetServers s;
Client cli("fe80::1", s.target_port());
cli.set_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"fe80::1", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"[fe80::]/10"});
@@ -31095,7 +31319,7 @@ TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) {
// tricks via address-family conversion.
ProxyAndTargetServers s;
Client cli("::ffff:127.0.0.1", s.target_port());
cli.set_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"::ffff:127.0.0.1", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", s.proxy_port());
cli.set_no_proxy({"127.0.0.1"});
@@ -31311,7 +31535,7 @@ TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) {
target.listen();
Client cli("public.example", target.port());
cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy_mock.port());
cli.set_proxy_basic_auth("u", "p");
cli.set_no_proxy({"127.0.0.1"});
@@ -31347,7 +31571,7 @@ TEST(NoProxyTest, BypassedTargetReturning407DoesNotLeakProxyDigestCredentials) {
// if it doesn't, the test still routes "via proxy" to the same server and
// the Proxy-Authorization assertion below catches the leak.
Client cli("evil.example", target.port());
cli.set_hostname_addr_map({{"evil.example", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"evil.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", target.port());
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
cli.set_no_proxy({"evil.example"});
@@ -31398,7 +31622,7 @@ TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) {
bypass_server.listen();
Client cli("public.example", public_port);
cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy_mock.port());
cli.set_no_proxy({"127.0.0.1"});
cli.set_follow_location(true);
@@ -31432,7 +31656,7 @@ TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
target.listen();
Client cli("public.example", target.port());
cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
cli.set_direct_hostname_addr_map({{"public.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy_mock.port());
cli.set_keep_alive(true);
@@ -31764,6 +31988,141 @@ TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) {
EXPECT_FALSE(origin_saw_proxy_authz.load());
EXPECT_EQ(1, proxy.connect_hits());
}
TEST(ProxyTunnelTest, ConnectTargetFollowsTunnelHostnameAddrMap) {
std::string origin_sni;
std::string origin_host;
proxy_tunnel_test::ScopedSSLServer origin;
origin.svr().Get(".*", [&](const Request &req, Response &res) {
origin_sni = req.sni();
origin_host = req.get_header_value("Host");
res.set_content("ok", "text/plain");
});
origin.listen();
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
ASSERT_NE(0, proxy.port());
SSLClient cli("origin.example", origin.port());
cli.enable_server_certificate_verification(false);
cli.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy.port());
auto res = cli.Get("/x");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
auto port = std::to_string(origin.port());
auto target = "127.0.0.1:" + port;
auto connect_req = proxy.connect_request();
EXPECT_EQ(1, proxy.connect_hits());
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
EXPECT_NE(std::string::npos,
connect_req.find("\r\nHost: " + target + "\r\n"));
EXPECT_EQ("origin.example", origin_sni);
EXPECT_EQ("origin.example:" + port, origin_host);
}
TEST(ProxyTunnelTest, DigestRetryConnectTargetFollowsTunnelHostnameAddrMap) {
proxy_tunnel_test::ScopedSSLServer origin;
origin.svr().Get(".*", [&](const Request &, Response &res) {
res.set_content("ok", "text/plain");
});
origin.listen();
proxy_tunnel_test::ScopedConnectProxy proxy(
origin.port(), [](const std::string &head) -> std::string {
if (head.find(
"\r\nProxy-Authorization: Digest username=\"proxy-user\"") !=
std::string::npos) {
return proxy_tunnel_test::kTunnelEstablished;
}
return "HTTP/1.1 407 Proxy Authentication Required\r\n"
"Proxy-Authenticate: Digest realm=\"proxy\", qop=\"auth\", "
"nonce=\"abc\", algorithm=MD5\r\n"
"Content-Length: 0\r\n\r\n";
});
ASSERT_NE(0, proxy.port());
SSLClient cli("origin.example", origin.port());
cli.enable_server_certificate_verification(false);
cli.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy.port());
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
auto res = cli.Get("/x");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
// The retried CONNECT is the one the proxy saw last.
auto target = "127.0.0.1:" + std::to_string(origin.port());
auto connect_req = proxy.connect_request();
EXPECT_EQ(2, proxy.connect_hits());
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
EXPECT_NE(std::string::npos,
connect_req.find("\r\nHost: " + target + "\r\n"));
}
TEST(ProxyTunnelTest, DirectHostnameAddrMapLeavesConnectTarget) {
proxy_tunnel_test::ScopedSSLServer origin;
origin.svr().Get(".*", [&](const Request &, Response &res) {
res.set_content("ok", "text/plain");
});
origin.listen();
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
ASSERT_NE(0, proxy.port());
SSLClient cli("origin.example", origin.port());
cli.enable_server_certificate_verification(false);
cli.set_direct_hostname_addr_map({{"origin.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy.port());
auto res = cli.Get("/x");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::OK_200, res->status);
auto target = "origin.example:" + std::to_string(origin.port());
auto connect_req = proxy.connect_request();
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
EXPECT_NE(std::string::npos,
connect_req.find("\r\nHost: " + target + "\r\n"));
}
TEST(ProxyTunnelTest, RedirectKeepsTunnelHostnameAddrMap) {
proxy_tunnel_test::ScopedSSLServer origin;
auto port = std::to_string(origin.port());
origin.svr().Get("/x", [&](const Request &, Response &res) {
res.set_redirect("https://next.example:" + port + "/y");
});
origin.svr().Get("/y", [](const Request &, Response &res) {
res.set_content("ok", "text/plain");
});
origin.listen();
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
ASSERT_NE(0, proxy.port());
SSLClient cli("origin.example", origin.port());
cli.enable_server_certificate_verification(false);
cli.set_tunnel_hostname_addr_map(
{{"origin.example", "127.0.0.1"}, {"next.example", "127.0.0.1"}});
cli.set_proxy("127.0.0.1", proxy.port());
cli.set_follow_location(true);
auto res = cli.Get("/x");
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ("ok", res->body);
// The last CONNECT is the redirect client's.
ASSERT_EQ(2, proxy.connect_hits());
auto target = "127.0.0.1:" + port;
auto connect_req = proxy.connect_request();
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
EXPECT_NE(std::string::npos,
connect_req.find("\r\nHost: " + target + "\r\n"));
}
#endif
namespace proxy_tunnel_test {
@@ -31840,8 +32199,106 @@ void WebSocketCredentialsStayWithTheirHop(
<< "origin credentials must not be sent to the proxy";
}
// Like WebSocketCredentialsStayWithTheirHop(), but with the setters.
void WebSocketSetterCredentialsStayWithTheirHop(
const std::function<void(ws::WebSocketClient &)> &set_credentials,
const std::string &proxy_authz, const std::string &origin_authz) {
ScopedWebSocketOrigin origin;
ScopedConnectProxy proxy(
origin.port(), require_proxy_auth(proxy_authz, "Basic realm=\"proxy\""));
ASSERT_NE(0, proxy.port());
ws::WebSocketClient client(origin.url());
client.set_proxy("127.0.0.1", proxy.port());
set_credentials(client);
auto res = client.connect();
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
std::string msg;
ASSERT_TRUE(client.read(msg));
EXPECT_EQ("authorization:" + origin_authz + "|proxy-authorization:", msg);
client.close();
EXPECT_EQ(1, proxy.connect_hits());
EXPECT_EQ(std::string::npos,
proxy.connect_request().find("\r\nAuthorization: "))
<< "origin credentials must not be sent to the proxy";
}
} // namespace proxy_tunnel_test
namespace {
// Connects `client` and returns the Authorization header its upgrade
// request carried, or "<failed>" when the connection could not be opened.
std::string websocket_authorization(ws::WebSocketClient &client) {
if (!client.connect()) { return "<failed>"; }
std::string msg;
if (!client.read(msg)) { return "<failed>"; }
client.close();
auto prefix = std::string("authorization:");
auto end = msg.find("|proxy-authorization:");
return msg.substr(prefix.size(), end - prefix.size());
}
} // namespace
TEST(WebSocketClientAuthTest, BasicAuth) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
ws::WebSocketClient client(origin.url());
client.set_basic_auth("user", "pass");
EXPECT_EQ("Basic " + detail::base64_encode("user:pass"),
websocket_authorization(client));
}
TEST(WebSocketClientAuthTest, BasicAuthWithEmptyPasswordIsSent) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
ws::WebSocketClient client(origin.url());
client.set_basic_auth("user", "");
EXPECT_EQ("Basic " + detail::base64_encode("user:"),
websocket_authorization(client));
}
TEST(WebSocketClientAuthTest, BearerTokenAuth) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
ws::WebSocketClient client(origin.url());
client.set_bearer_token_auth("token123");
EXPECT_EQ("Bearer token123", websocket_authorization(client));
}
TEST(WebSocketClientAuthTest, BasicAuthWinsOverBearerToken) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
ws::WebSocketClient client(origin.url());
client.set_bearer_token_auth("token123");
client.set_basic_auth("user", "pass");
EXPECT_EQ("Basic " + detail::base64_encode("user:pass"),
websocket_authorization(client));
}
TEST(WebSocketClientAuthTest, ConstructorHeaderWinsOverSetter) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
ws::WebSocketClient client(origin.url(),
{{"Authorization", "Bearer from-header"}});
client.set_bearer_token_auth("from-setter");
client.set_basic_auth("user", "pass");
EXPECT_EQ("Bearer from-header", websocket_authorization(client));
}
TEST(WebSocketClientAuthTest, CredentialsApplyOnReconnect) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
ws::WebSocketClient client(origin.url());
EXPECT_EQ("", websocket_authorization(client));
client.set_bearer_token_auth("first");
EXPECT_EQ("Bearer first", websocket_authorization(client));
client.set_bearer_token_auth("second");
EXPECT_EQ("Bearer second", websocket_authorization(client));
client.set_bearer_token_auth("");
EXPECT_EQ("", websocket_authorization(client));
}
TEST(WebSocketProxyTest, TunnelsThroughConnect) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
@@ -31884,6 +32341,25 @@ TEST(WebSocketProxyTest, BearerCredentialsStayWithTheirHop) {
"Bearer proxy-token", "Bearer origin-token");
}
TEST(WebSocketProxyTest, BasicAuthSetterStaysWithItsHop) {
proxy_tunnel_test::WebSocketSetterCredentialsStayWithTheirHop(
[](ws::WebSocketClient &cli) {
cli.set_proxy_basic_auth("proxy-user", "proxy-pass");
cli.set_basic_auth("origin-user", "origin-pass");
},
"Basic " + detail::base64_encode("proxy-user:proxy-pass"),
"Basic " + detail::base64_encode("origin-user:origin-pass"));
}
TEST(WebSocketProxyTest, BearerTokenAuthSetterStaysWithItsHop) {
proxy_tunnel_test::WebSocketSetterCredentialsStayWithTheirHop(
[](ws::WebSocketClient &cli) {
cli.set_proxy_bearer_token_auth("proxy-token");
cli.set_bearer_token_auth("origin-token");
},
"Bearer proxy-token", "Bearer origin-token");
}
TEST(WebSocketProxyTest, BasicCredentialsWithEmptyPasswordAreSent) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
proxy_tunnel_test::ScopedConnectProxy proxy(
@@ -31955,6 +32431,50 @@ TEST(WebSocketProxyTest, ProxySettingAppliesOnReconnect) {
EXPECT_EQ(2, proxy.connect_hits());
}
TEST(WebSocketProxyTest, ConnectTargetFollowsTunnelHostnameAddrMap) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
ASSERT_NE(0, proxy.port());
auto port = std::to_string(origin.port());
ws::WebSocketClient client("ws://origin.example:" + port + "/ws");
client.set_tunnel_hostname_addr_map({{"origin.example", "127.0.0.1"}});
client.set_proxy("127.0.0.1", proxy.port());
auto res = client.connect();
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
std::string msg;
ASSERT_TRUE(client.read(msg));
client.close();
EXPECT_EQ(1, proxy.connect_hits());
auto target = "127.0.0.1:" + port;
auto connect_req = proxy.connect_request();
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
EXPECT_NE(std::string::npos,
connect_req.find("\r\nHost: " + target + "\r\n"));
}
TEST(WebSocketProxyTest, DirectHostnameAddrMapLeavesConnectTarget) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
ASSERT_NE(0, proxy.port());
auto port = std::to_string(origin.port());
ws::WebSocketClient client("ws://origin.example:" + port + "/ws");
client.set_direct_hostname_addr_map({{"origin.example", "127.0.0.1"}});
client.set_proxy("127.0.0.1", proxy.port());
auto res = client.connect();
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
client.close();
auto target = "origin.example:" + port;
auto connect_req = proxy.connect_request();
EXPECT_EQ(0u, connect_req.find("CONNECT " + target + " HTTP/1.1\r\n"));
EXPECT_NE(std::string::npos,
connect_req.find("\r\nHost: " + target + "\r\n"));
}
#ifdef CPPHTTPLIB_SSL_ENABLED
TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) {
proxy_tunnel_test::ScopedWebSocketOrigin origin;