mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-30 13:00:42 +00:00
Compare commits
5
Commits
| 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 = "");
|
const std::string &reason = "");
|
||||||
bool is_open() const;
|
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
|
// 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
|
// 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.
|
// 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_ipv6_v6only(bool on);
|
||||||
void set_socket_options(SocketOptions socket_options);
|
void set_socket_options(SocketOptions socket_options);
|
||||||
void set_interface(const std::string &intf);
|
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)
|
// SSL configuration (wss:// only, SSL builds only)
|
||||||
void set_ca_cert_path(const std::string &ca_cert_file_path,
|
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).
|
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
|
### Custom Headers and Timeouts
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -742,7 +742,7 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
|||||||
auto safe = httplib::sanitize_filename(file.filename);
|
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]
|
> [!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.
|
> `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
|
### Override the connection target for a hostname
|
||||||
|
|
||||||
`set_hostname_addr_map` redirects where the socket connects, without changing
|
`set_direct_hostname_addr_map` redirects where the socket connects, without
|
||||||
the identity of the request. The hostname the client was constructed with keeps
|
changing the identity of the request. The hostname the client was constructed
|
||||||
supplying the `Host` header, the SNI, and the name that the server certificate
|
with keeps supplying the `Host` header, the SNI, and the name that the server
|
||||||
is verified against, so this is a connection-level override only, not a way to
|
certificate is verified against, so this is a connection-level override only,
|
||||||
talk to a different origin.
|
not a way to talk to a different origin.
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
httplib::Client cli("https://example.com");
|
httplib::Client cli("https://example.com");
|
||||||
|
|
||||||
// Connect to this IP address instead of resolving "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
|
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:
|
under a different name works too:
|
||||||
|
|
||||||
```cpp
|
```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
|
An empty value is ignored, leaving the original hostname as the connection
|
||||||
target.
|
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
|
### Automatic Path Encoding
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
|
|||||||
|
|
||||||
[site]
|
[site]
|
||||||
title = "cpp-httplib"
|
title = "cpp-httplib"
|
||||||
version = "0.63.0"
|
version = "0.64.0"
|
||||||
hostname = "https://yhirose.github.io"
|
hostname = "https://yhirose.github.io"
|
||||||
base_path = "/cpp-httplib"
|
base_path = "/cpp-httplib"
|
||||||
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
footer_message = "© 2026 Yuji Hirose. All rights reserved."
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // The port is unused, but must not be 0
|
|||||||
// Client
|
// Client
|
||||||
httplib::Client cli("http://localhost");
|
httplib::Client cli("http://localhost");
|
||||||
cli.set_address_family(AF_UNIX);
|
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("/");
|
auto res = cli.Get("/");
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ svr.listen("/tmp/httplib.sock", 80); // ポート番号は使われない(0
|
|||||||
// クライアント
|
// クライアント
|
||||||
httplib::Client cli("http://localhost");
|
httplib::Client cli("http://localhost");
|
||||||
cli.set_address_family(AF_UNIX);
|
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("/");
|
auto res = cli.Get("/");
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
#ifndef CPPHTTPLIB_HTTPLIB_H
|
#ifndef CPPHTTPLIB_HTTPLIB_H
|
||||||
#define CPPHTTPLIB_HTTPLIB_H
|
#define CPPHTTPLIB_HTTPLIB_H
|
||||||
|
|
||||||
#define CPPHTTPLIB_VERSION "0.63.0"
|
#define CPPHTTPLIB_VERSION "0.64.0"
|
||||||
#define CPPHTTPLIB_VERSION_NUM "0x003f00"
|
#define CPPHTTPLIB_VERSION_NUM "0x004000"
|
||||||
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
|
||||||
@@ -2927,7 +2927,12 @@ public:
|
|||||||
size_t is_socket_open() const;
|
size_t is_socket_open() const;
|
||||||
socket_t socket() 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_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);
|
void set_default_headers(Headers headers);
|
||||||
|
|
||||||
@@ -3059,6 +3064,9 @@ protected:
|
|||||||
// hostname; only the connection target changes, never the identity.
|
// hostname; only the connection target changes, never the identity.
|
||||||
std::map<std::string, std::string> addr_map_;
|
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
|
// Default headers
|
||||||
Headers default_headers_;
|
Headers default_headers_;
|
||||||
|
|
||||||
@@ -3348,7 +3356,12 @@ public:
|
|||||||
size_t is_socket_open() const;
|
size_t is_socket_open() const;
|
||||||
socket_t socket() 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_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);
|
void set_default_headers(Headers headers);
|
||||||
|
|
||||||
@@ -4748,7 +4761,15 @@ public:
|
|||||||
set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
|
set_connection_timeout(const std::chrono::duration<Rep, Period> &duration);
|
||||||
|
|
||||||
void set_interface(const std::string &intf);
|
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_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(const std::string &host, int port);
|
||||||
void set_proxy_basic_auth(const std::string &username,
|
void set_proxy_basic_auth(const std::string &username,
|
||||||
@@ -4820,6 +4841,13 @@ private:
|
|||||||
// hostname; only the connection target changes, never the identity.
|
// hostname; only the connection target changes, never the identity.
|
||||||
std::map<std::string, std::string> addr_map_;
|
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_;
|
std::string proxy_host_;
|
||||||
int proxy_port_ = -1;
|
int proxy_port_ = -1;
|
||||||
std::string proxy_basic_auth_username_;
|
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) {
|
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("/\\");
|
auto pos = filename.find_last_of("/\\");
|
||||||
|
#endif
|
||||||
auto result =
|
auto result =
|
||||||
(pos != std::string::npos) ? filename.substr(pos + 1) : filename;
|
(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);
|
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.
|
// 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.
|
// Only the value: callers decide where in their header list it goes.
|
||||||
inline std::string make_default_host_header_value(const std::string &host,
|
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
|
// Copy network and socket settings
|
||||||
client.addr_map_ = addr_map_;
|
client.addr_map_ = addr_map_;
|
||||||
|
client.tunnel_addr_map_ = tunnel_addr_map_;
|
||||||
client.set_address_family(address_family_);
|
client.set_address_family(address_family_);
|
||||||
client.set_tcp_nodelay(tcp_nodelay_);
|
client.set_tcp_nodelay(tcp_nodelay_);
|
||||||
client.set_ipv6_v6only(ipv6_v6only_);
|
client.set_ipv6_v6only(ipv6_v6only_);
|
||||||
@@ -18081,9 +18126,19 @@ inline void ClientImpl::set_path_encode(bool on) { path_encode_ = on; }
|
|||||||
|
|
||||||
inline void
|
inline void
|
||||||
ClientImpl::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
|
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);
|
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) {
|
inline void ClientImpl::set_default_headers(Headers headers) {
|
||||||
default_headers_ = std::move(headers);
|
default_headers_ = std::move(headers);
|
||||||
}
|
}
|
||||||
@@ -18792,7 +18847,17 @@ inline socket_t Client::socket() const { return cli_->socket(); }
|
|||||||
|
|
||||||
inline void
|
inline void
|
||||||
Client::set_hostname_addr_map(std::map<std::string, std::string> addr_map) {
|
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) {
|
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,
|
std::chrono::time_point<std::chrono::steady_clock> start_time,
|
||||||
Response &res, bool &success, Error &error) {
|
Response &res, bool &success, Error &error) {
|
||||||
success = true;
|
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;
|
Response proxy_res;
|
||||||
if (!detail::process_client_socket(
|
if (!detail::process_client_socket(
|
||||||
socket.sock, read_timeout_sec_, read_timeout_usec_,
|
socket.sock, read_timeout_sec_, read_timeout_usec_,
|
||||||
@@ -19167,8 +19236,8 @@ inline bool SSLClient::connect_with_proxy(
|
|||||||
start_time, [&](Stream &strm) {
|
start_time, [&](Stream &strm) {
|
||||||
Request req2;
|
Request req2;
|
||||||
req2.method = "CONNECT";
|
req2.method = "CONNECT";
|
||||||
req2.path =
|
req2.path = connect_target;
|
||||||
detail::make_host_and_port_string_always_port(host_, port_);
|
if (retargeted) { req2.set_header("Host", connect_target); }
|
||||||
if (max_timeout_msec_ > 0) {
|
if (max_timeout_msec_ > 0) {
|
||||||
req2.start_time_ = std::chrono::steady_clock::now();
|
req2.start_time_ = std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
@@ -19207,8 +19276,8 @@ inline bool SSLClient::connect_with_proxy(
|
|||||||
start_time, [&](Stream &strm) {
|
start_time, [&](Stream &strm) {
|
||||||
Request req3;
|
Request req3;
|
||||||
req3.method = "CONNECT";
|
req3.method = "CONNECT";
|
||||||
req3.path = detail::make_host_and_port_string_always_port(
|
req3.path = connect_target;
|
||||||
host_, port_);
|
if (retargeted) { req3.set_header("Host", connect_target); }
|
||||||
req3.headers.insert(detail::make_digest_authentication_header(
|
req3.headers.insert(detail::make_digest_authentication_header(
|
||||||
req3, make_request_target(req3), auth, 1,
|
req3, make_request_target(req3), auth, 1,
|
||||||
detail::random_string(10), proxy_digest_auth_username_,
|
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;
|
out_errno = errno;
|
||||||
return ErrorCode::SyscallError;
|
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) {
|
if (ret == MBEDTLS_ERR_X509_CERT_VERIFY_FAILED) {
|
||||||
// Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
|
// Unlike OpenSSL/wolfSSL, Mbed TLS folds the CN/SAN identity check into
|
||||||
// the handshake's chain verification (see set_sni()); a mismatch there
|
// 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;
|
return MBEDTLS_ERR_NET_RECV_FAILED;
|
||||||
}
|
}
|
||||||
#endif
|
#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);
|
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);
|
return static_cast<ssize_t>(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret == 0) {
|
// 0 means EOF without close_notify, which is an error as on OpenSSL
|
||||||
err.code = ErrorCode::PeerClosed;
|
if (ret == 0) { ret = MBEDTLS_ERR_SSL_CONN_EOF; }
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
|
err.code = impl::map_mbedtls_error(ret, err.sys_errno, 0);
|
||||||
err.backend_code = static_cast<uint64_t>(-ret);
|
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_WANT_WRITE: return ErrorCode::WantWrite;
|
||||||
case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
|
case SSL_ERROR_ZERO_RETURN: return ErrorCode::PeerClosed;
|
||||||
case SSL_ERROR_SYSCALL: out_errno = errno; return ErrorCode::SyscallError;
|
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:
|
default:
|
||||||
if (ssl) {
|
if (ssl) {
|
||||||
// wolfSSL stores the low-level error code as a negative value.
|
// 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);
|
return static_cast<ssize_t>(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret == 0) {
|
// 0 is also returned for EOF without close_notify; get_error tells them apart
|
||||||
err.code = ErrorCode::PeerClosed;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
|
int ssl_error = wolfSSL_get_error(wsession->ssl, ret);
|
||||||
err.code = impl::map_wolfssl_error(wsession->ssl, ssl_error, err.sys_errno);
|
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);
|
err.backend_code = static_cast<uint64_t>(ssl_error);
|
||||||
impl::wolfssl_last_error() = err.backend_code;
|
impl::wolfssl_last_error() = err.backend_code;
|
||||||
return -1;
|
return -1;
|
||||||
@@ -23926,6 +23997,9 @@ inline void WebSocketClient::prepare_default_headers(Request &req) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
detail::add_default_user_agent_header(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() {
|
inline Result WebSocketClient::connect() {
|
||||||
@@ -24051,9 +24125,19 @@ inline void WebSocketClient::set_interface(const std::string &intf) {
|
|||||||
|
|
||||||
inline void WebSocketClient::set_hostname_addr_map(
|
inline void WebSocketClient::set_hostname_addr_map(
|
||||||
std::map<std::string, std::string> 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);
|
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 {
|
inline bool WebSocketClient::is_proxy_enabled() const {
|
||||||
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
|
if (proxy_host_.empty() || proxy_port_ == -1) { return false; }
|
||||||
if (no_proxy_entries_.empty()) { return true; }
|
if (no_proxy_entries_.empty()) { return true; }
|
||||||
@@ -24103,7 +24187,8 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
|
|||||||
Error &error) {
|
Error &error) {
|
||||||
Request req;
|
Request req;
|
||||||
req.method = "CONNECT";
|
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);
|
req.headers.emplace("Host", req.path);
|
||||||
detail::add_default_user_agent_header(req);
|
detail::add_default_user_agent_header(req);
|
||||||
detail::add_authorization_header(req, proxy_basic_auth_username_,
|
detail::add_authorization_header(req, proxy_basic_auth_username_,
|
||||||
@@ -24137,6 +24222,16 @@ inline bool WebSocketClient::connect_with_proxy(Response &proxy_res,
|
|||||||
return true;
|
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) {
|
inline void WebSocketClient::set_proxy(const std::string &host, int port) {
|
||||||
proxy_host_ = host;
|
proxy_host_ = host;
|
||||||
proxy_port_ = port;
|
proxy_port_ = port;
|
||||||
|
|||||||
+540
-20
@@ -72,6 +72,17 @@ namespace tls {
|
|||||||
ca_store_t create_ca_store(const char *pem, size_t len);
|
ca_store_t create_ca_store(const char *pem, size_t len);
|
||||||
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
bool set_verify_callback(ctx_t ctx, VerifyCallback callback);
|
||||||
std::vector<std::string> get_ca_names(ctx_t ctx);
|
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
|
#ifdef CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN
|
||||||
namespace impl {
|
namespace impl {
|
||||||
SecTrustSettingsResult
|
SecTrustSettingsResult
|
||||||
@@ -508,6 +519,20 @@ TEST(SanitizeFilenameTest, VariousPatterns) {
|
|||||||
EXPECT_EQ("", httplib::sanitize_filename(" "));
|
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
|
// 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.
|
// definition into httplib.cc, so re-declaring it here lets the test link.
|
||||||
namespace httplib {
|
namespace httplib {
|
||||||
@@ -3723,7 +3748,7 @@ TEST(SpecifyServerIPAddressTest, AnotherHostname_Online) {
|
|||||||
Client cli(host);
|
Client cli(host);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
cli.set_hostname_addr_map({{another_host, wrong_ip}});
|
cli.set_direct_hostname_addr_map({{another_host, wrong_ip}});
|
||||||
auto res = cli.Get("/");
|
auto res = cli.Get("/");
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
|
ASSERT_EQ(StatusCode::MovedPermanently_301, res->status);
|
||||||
@@ -3739,7 +3764,7 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
|
|||||||
Client cli(host);
|
Client cli(host);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
cli.set_hostname_addr_map({{host, wrong_ip}});
|
cli.set_direct_hostname_addr_map({{host, wrong_ip}});
|
||||||
auto res = cli.Get("/");
|
auto res = cli.Get("/");
|
||||||
ASSERT_TRUE(!res);
|
ASSERT_TRUE(!res);
|
||||||
EXPECT_EQ(Error::Connection, res.error());
|
EXPECT_EQ(Error::Connection, res.error());
|
||||||
@@ -3770,7 +3795,7 @@ TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) {
|
|||||||
svr.wait_until_ready();
|
svr.wait_until_ready();
|
||||||
|
|
||||||
Client cli(host, port);
|
Client cli(host, port);
|
||||||
cli.set_hostname_addr_map({{host, HOST}});
|
cli.set_direct_hostname_addr_map({{host, HOST}});
|
||||||
|
|
||||||
auto res = cli.Get("/hi");
|
auto res = cli.Get("/hi");
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
@@ -3800,7 +3825,7 @@ TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) {
|
|||||||
svr.wait_until_ready();
|
svr.wait_until_ready();
|
||||||
|
|
||||||
Client cli(host, port);
|
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");
|
auto res = cli.Get("/hi");
|
||||||
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
||||||
@@ -3833,13 +3858,73 @@ TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) {
|
|||||||
svr.wait_until_ready();
|
svr.wait_until_ready();
|
||||||
|
|
||||||
Client cli(blackhole, port);
|
Client cli(blackhole, port);
|
||||||
cli.set_hostname_addr_map({{blackhole, ""}});
|
cli.set_direct_hostname_addr_map({{blackhole, ""}});
|
||||||
cli.set_connection_timeout(1);
|
cli.set_connection_timeout(1);
|
||||||
|
|
||||||
auto res = cli.Get("/hi");
|
auto res = cli.Get("/hi");
|
||||||
EXPECT_FALSE(res) << "empty mapping must not redirect to loopback";
|
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) {
|
TEST(AbsoluteRedirectTest, Redirect_Online) {
|
||||||
auto host = "httpbingo.org";
|
auto host = "httpbingo.org";
|
||||||
auto path = std::string{"/absolute-redirect/3"};
|
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
|
// Sends SNI, then rejects the server certificate, which the CA did not sign
|
||||||
SSLClient cli("localhost", port);
|
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);
|
cli.set_ca_cert_path(CLIENT_CA_CERT_FILE);
|
||||||
auto res = cli.Get("/sni");
|
auto res = cli.Get("/sni");
|
||||||
ASSERT_FALSE(res);
|
ASSERT_FALSE(res);
|
||||||
@@ -16411,7 +16496,7 @@ TEST(SSLClientServerTest, ClientCertNotSentToUnverifiedServer) {
|
|||||||
svr.wait_until_ready();
|
svr.wait_until_ready();
|
||||||
|
|
||||||
SSLClient cli(c.host, port, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
|
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);
|
cli.set_ca_cert_path(c.trusted_ca);
|
||||||
if (with_verifier) {
|
if (with_verifier) {
|
||||||
cli.set_server_certificate_verifier(
|
cli.set_server_certificate_verifier(
|
||||||
@@ -21505,6 +21590,36 @@ TEST(MakeHostAndPortStringTest, VariousPatterns) {
|
|||||||
detail::make_host_and_port_string("example.com", 65536, false));
|
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
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(SSLClientHostHeaderTest, Issue2301_Online) {
|
TEST(SSLClientHostHeaderTest, Issue2301_Online) {
|
||||||
httplib::SSLClient cli("roblox.com", 443);
|
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) {
|
TEST(InterimResponseTest, SkippedWithTheirHeaderFields) {
|
||||||
const char *responses[] = {
|
const char *responses[] = {
|
||||||
"HTTP/1.1 103 Early Hints\r\n"
|
"HTTP/1.1 103 Early Hints\r\n"
|
||||||
@@ -27774,7 +27998,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_AnotherHostname) {
|
|||||||
auto wrong_ip = "192.0.2.1";
|
auto wrong_ip = "192.0.2.1";
|
||||||
|
|
||||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
|
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());
|
ASSERT_TRUE(client.connect());
|
||||||
EXPECT_TRUE(client.is_open());
|
EXPECT_TRUE(client.is_open());
|
||||||
@@ -27798,7 +28022,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
|
|||||||
auto wrong_ip = "192.0.2.1";
|
auto wrong_ip = "192.0.2.1";
|
||||||
|
|
||||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
|
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_connection_timeout(1);
|
||||||
client.set_read_timeout(1);
|
client.set_read_timeout(1);
|
||||||
client.set_write_timeout(1);
|
client.set_write_timeout(1);
|
||||||
@@ -27838,7 +28062,7 @@ TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) {
|
|||||||
|
|
||||||
ws::WebSocketClient client("ws://" + std::string(host) + ":" +
|
ws::WebSocketClient client("ws://" + std::string(host) + ":" +
|
||||||
std::to_string(port) + "/ws");
|
std::to_string(port) + "/ws");
|
||||||
client.set_hostname_addr_map({{host, HOST}});
|
client.set_direct_hostname_addr_map({{host, HOST}});
|
||||||
|
|
||||||
ASSERT_TRUE(client.connect());
|
ASSERT_TRUE(client.connect());
|
||||||
EXPECT_TRUE(client.is_open());
|
EXPECT_TRUE(client.is_open());
|
||||||
@@ -30827,7 +31051,7 @@ private:
|
|||||||
inline std::unique_ptr<Client> make_client(const std::string &host,
|
inline std::unique_ptr<Client> make_client(const std::string &host,
|
||||||
ProxyAndTargetServers &s) {
|
ProxyAndTargetServers &s) {
|
||||||
auto cli = detail::make_unique<Client>(host, s.target_port());
|
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());
|
cli->set_proxy("127.0.0.1", s.proxy_port());
|
||||||
return cli;
|
return cli;
|
||||||
}
|
}
|
||||||
@@ -31036,7 +31260,7 @@ TEST(NoProxyTest, IPv4LiteralExactMatch) {
|
|||||||
TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) {
|
TEST(NoProxyTest, IPv6LiteralExactMatchAcrossEquivalentForms) {
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
Client cli("0:0:0:0:0:0:0:1", s.target_port());
|
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_proxy("127.0.0.1", s.proxy_port());
|
||||||
cli.set_no_proxy({"::1"});
|
cli.set_no_proxy({"::1"});
|
||||||
|
|
||||||
@@ -31053,7 +31277,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
|
|||||||
// host as a hostname and miss the match.
|
// host as a hostname and miss the match.
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
Client cli("fe80::1", s.target_port());
|
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_proxy("127.0.0.1", s.proxy_port());
|
||||||
cli.set_no_proxy({"fe80::/10"});
|
cli.set_no_proxy({"fe80::/10"});
|
||||||
|
|
||||||
@@ -31066,7 +31290,7 @@ TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
|
|||||||
TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
|
TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
Client cli("::1", s.target_port());
|
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_proxy("127.0.0.1", s.proxy_port());
|
||||||
cli.set_no_proxy({"[::1]"});
|
cli.set_no_proxy({"[::1]"});
|
||||||
|
|
||||||
@@ -31079,7 +31303,7 @@ TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
|
|||||||
TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) {
|
TEST(NoProxyTest, BracketedIPv6CidrEntryAccepted) {
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
Client cli("fe80::1", s.target_port());
|
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_proxy("127.0.0.1", s.proxy_port());
|
||||||
cli.set_no_proxy({"[fe80::]/10"});
|
cli.set_no_proxy({"[fe80::]/10"});
|
||||||
|
|
||||||
@@ -31095,7 +31319,7 @@ TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) {
|
|||||||
// tricks via address-family conversion.
|
// tricks via address-family conversion.
|
||||||
ProxyAndTargetServers s;
|
ProxyAndTargetServers s;
|
||||||
Client cli("::ffff:127.0.0.1", s.target_port());
|
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_proxy("127.0.0.1", s.proxy_port());
|
||||||
cli.set_no_proxy({"127.0.0.1"});
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
|
|
||||||
@@ -31311,7 +31535,7 @@ TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) {
|
|||||||
target.listen();
|
target.listen();
|
||||||
|
|
||||||
Client cli("public.example", target.port());
|
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("127.0.0.1", proxy_mock.port());
|
||||||
cli.set_proxy_basic_auth("u", "p");
|
cli.set_proxy_basic_auth("u", "p");
|
||||||
cli.set_no_proxy({"127.0.0.1"});
|
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
|
// if it doesn't, the test still routes "via proxy" to the same server and
|
||||||
// the Proxy-Authorization assertion below catches the leak.
|
// the Proxy-Authorization assertion below catches the leak.
|
||||||
Client cli("evil.example", target.port());
|
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("127.0.0.1", target.port());
|
||||||
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
|
cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
|
||||||
cli.set_no_proxy({"evil.example"});
|
cli.set_no_proxy({"evil.example"});
|
||||||
@@ -31398,7 +31622,7 @@ TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) {
|
|||||||
bypass_server.listen();
|
bypass_server.listen();
|
||||||
|
|
||||||
Client cli("public.example", public_port);
|
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_proxy("127.0.0.1", proxy_mock.port());
|
||||||
cli.set_no_proxy({"127.0.0.1"});
|
cli.set_no_proxy({"127.0.0.1"});
|
||||||
cli.set_follow_location(true);
|
cli.set_follow_location(true);
|
||||||
@@ -31432,7 +31656,7 @@ TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
|
|||||||
target.listen();
|
target.listen();
|
||||||
|
|
||||||
Client cli("public.example", target.port());
|
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("127.0.0.1", proxy_mock.port());
|
||||||
cli.set_keep_alive(true);
|
cli.set_keep_alive(true);
|
||||||
|
|
||||||
@@ -31764,6 +31988,141 @@ TEST(ProxyTunnelTest, OriginReturning407InsideTunnelDoesNotLeakProxyDigest) {
|
|||||||
EXPECT_FALSE(origin_saw_proxy_authz.load());
|
EXPECT_FALSE(origin_saw_proxy_authz.load());
|
||||||
EXPECT_EQ(1, proxy.connect_hits());
|
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
|
#endif
|
||||||
|
|
||||||
namespace proxy_tunnel_test {
|
namespace proxy_tunnel_test {
|
||||||
@@ -31840,8 +32199,106 @@ void WebSocketCredentialsStayWithTheirHop(
|
|||||||
<< "origin credentials must not be sent to the proxy";
|
<< "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 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) {
|
TEST(WebSocketProxyTest, TunnelsThroughConnect) {
|
||||||
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
|
proxy_tunnel_test::ScopedConnectProxy proxy(origin.port());
|
||||||
@@ -31884,6 +32341,25 @@ TEST(WebSocketProxyTest, BearerCredentialsStayWithTheirHop) {
|
|||||||
"Bearer proxy-token", "Bearer origin-token");
|
"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) {
|
TEST(WebSocketProxyTest, BasicCredentialsWithEmptyPasswordAreSent) {
|
||||||
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
proxy_tunnel_test::ScopedConnectProxy proxy(
|
proxy_tunnel_test::ScopedConnectProxy proxy(
|
||||||
@@ -31955,6 +32431,50 @@ TEST(WebSocketProxyTest, ProxySettingAppliesOnReconnect) {
|
|||||||
EXPECT_EQ(2, proxy.connect_hits());
|
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
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) {
|
TEST(WebSocketProxyTest, DigestRetriesConnectOnFreshSocket) {
|
||||||
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
proxy_tunnel_test::ScopedWebSocketOrigin origin;
|
||||||
|
|||||||
Reference in new issue
Block a user