mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-30 13:00:42 +00:00
Compare commits
5
Commits
252dbaa505
...
v0.64.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24e58ce5b2 | ||
|
|
a0ab09fc0a | ||
|
|
290c4c0bbe | ||
|
|
25ba40f6fe | ||
|
|
578e4815b7 |
No files matched your search
+22
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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("/");
|
||||
```
|
||||
|
||||
@@ -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("/");
|
||||
```
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user