|
|
@@ -15,7 +15,9 @@
|
|
|
|
|
|
#include <algorithm>
|
|
|
#include <atomic>
|
|
|
+#include <cctype>
|
|
|
#include <chrono>
|
|
|
+#include <clocale>
|
|
|
#include <cstdio>
|
|
|
#include <fstream>
|
|
|
#include <future>
|
|
|
@@ -2573,6 +2575,101 @@ TEST(SpecifyServerIPAddressTest, RealHostname_Online) {
|
|
|
EXPECT_EQ(Error::Connection, res.error());
|
|
|
}
|
|
|
|
|
|
+TEST(SpecifyServerIPAddressTest, HostnameAsAddrMapValue) {
|
|
|
+ // A mapped value that is not an IP literal must be resolved. "localhost"
|
|
|
+ // resolves from the hosts file, so this test needs no external DNS.
|
|
|
+ // "target.invalid" (RFC 6761) is only a map key and the Host header value.
|
|
|
+ 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(HOST);
|
|
|
+ auto thread = std::thread([&]() { svr.listen_after_bind(); });
|
|
|
+
|
|
|
+ auto se = detail::scope_exit([&] {
|
|
|
+ svr.stop();
|
|
|
+ thread.join();
|
|
|
+ ASSERT_FALSE(svr.is_running());
|
|
|
+ });
|
|
|
+
|
|
|
+ svr.wait_until_ready();
|
|
|
+
|
|
|
+ Client cli(host, port);
|
|
|
+ cli.set_hostname_addr_map({{host, HOST}});
|
|
|
+
|
|
|
+ auto res = cli.Get("/hi");
|
|
|
+ ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
|
|
+ EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
|
+ // The mapping only redirects the connection; the identity stays host_.
|
|
|
+ EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host);
|
|
|
+}
|
|
|
+
|
|
|
+TEST(SpecifyServerIPAddressTest, IPAddressAsAddrMapValue) {
|
|
|
+ // A mapped value that is an IP literal keeps the AI_NUMERICHOST path.
|
|
|
+ auto host = "target.invalid";
|
|
|
+
|
|
|
+ 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(host, port);
|
|
|
+ cli.set_hostname_addr_map({{host, "127.0.0.1"}});
|
|
|
+
|
|
|
+ auto res = cli.Get("/hi");
|
|
|
+ ASSERT_TRUE(res) << "Error: " << to_string(res.error());
|
|
|
+ EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
|
+}
|
|
|
+
|
|
|
+TEST(SpecifyServerIPAddressTest, EmptyAddrMapValueIsIgnored) {
|
|
|
+ // An empty mapped value must leave host_ as the connection target. Without
|
|
|
+ // that guard the empty value would become the host argument, getaddrinfo
|
|
|
+ // would be called with a null node, and (no AI_PASSIVE) it would resolve to
|
|
|
+ // loopback - silently connecting somewhere the caller never asked for.
|
|
|
+ // The server listens on loopback, so such a fallback would succeed and is
|
|
|
+ // therefore observable as a failure of this test.
|
|
|
+ auto blackhole = "192.0.2.1"; // TEST-NET-1, never routable
|
|
|
+
|
|
|
+ Server svr;
|
|
|
+ svr.Get("/hi", [](const Request & /*req*/, Response &res) {
|
|
|
+ res.set_content("Hello World!", "text/plain");
|
|
|
+ });
|
|
|
+
|
|
|
+ auto port = svr.bind_to_any_port(HOST);
|
|
|
+ auto thread = std::thread([&]() { svr.listen_after_bind(); });
|
|
|
+
|
|
|
+ auto se = detail::scope_exit([&] {
|
|
|
+ svr.stop();
|
|
|
+ thread.join();
|
|
|
+ ASSERT_FALSE(svr.is_running());
|
|
|
+ });
|
|
|
+
|
|
|
+ svr.wait_until_ready();
|
|
|
+
|
|
|
+ Client cli(blackhole, port);
|
|
|
+ cli.set_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(AbsoluteRedirectTest, Redirect_Online) {
|
|
|
auto host = "httpbingo.org";
|
|
|
auto path = std::string{"/absolute-redirect/3"};
|
|
|
@@ -19298,6 +19395,47 @@ TEST(WebSocketTest, SpecifyServerIPAddress_RealHostname) {
|
|
|
t.join();
|
|
|
}
|
|
|
|
|
|
+TEST(WebSocketTest, SpecifyServerIPAddress_HostnameAsAddrMapValue) {
|
|
|
+ // A mapped value that is not an IP literal must be resolved. HOST resolves
|
|
|
+ // from the hosts file, so this test needs no external DNS. "target.invalid"
|
|
|
+ // (RFC 6761) is only a map key and the Host header value.
|
|
|
+ auto host = "target.invalid";
|
|
|
+
|
|
|
+ Server svr;
|
|
|
+ std::string received_host;
|
|
|
+ svr.WebSocket("/ws", [&](const Request &req, ws::WebSocket &ws) {
|
|
|
+ received_host = req.get_header_value("Host");
|
|
|
+ std::string msg;
|
|
|
+ while (ws.read(msg)) {}
|
|
|
+ });
|
|
|
+
|
|
|
+ auto port = svr.bind_to_any_port(HOST);
|
|
|
+ std::thread t([&]() { svr.listen_after_bind(); });
|
|
|
+
|
|
|
+ // ASSERT_* below returns from the test body, which would leave t joinable
|
|
|
+ // and make ~thread call std::terminate.
|
|
|
+ auto se = detail::scope_exit([&] {
|
|
|
+ svr.stop();
|
|
|
+ if (t.joinable()) { t.join(); }
|
|
|
+ });
|
|
|
+
|
|
|
+ svr.wait_until_ready();
|
|
|
+
|
|
|
+ ws::WebSocketClient client("ws://" + std::string(host) + ":" +
|
|
|
+ std::to_string(port) + "/ws");
|
|
|
+ client.set_hostname_addr_map({{host, HOST}});
|
|
|
+
|
|
|
+ ASSERT_TRUE(client.connect());
|
|
|
+ EXPECT_TRUE(client.is_open());
|
|
|
+ client.close();
|
|
|
+
|
|
|
+ svr.stop();
|
|
|
+ t.join();
|
|
|
+
|
|
|
+ // The mapping only redirects the connection; the identity stays host_.
|
|
|
+ EXPECT_EQ(std::string(host) + ":" + std::to_string(port), received_host);
|
|
|
+}
|
|
|
+
|
|
|
class WebSocketIntegrationTest : public ::testing::Test {
|
|
|
protected:
|
|
|
void SetUp() override {
|
|
|
@@ -19777,16 +19915,21 @@ TEST(WebSocketTest, QueryStringInHandshake) {
|
|
|
t.join();
|
|
|
}
|
|
|
|
|
|
-TEST(WebSocketTest, HostHeaderInHandshake) {
|
|
|
+// Run a handshake against a throwaway server and hand the request the server
|
|
|
+// received back to the caller, so tests can assert on the headers the client
|
|
|
+// actually put on the wire.
|
|
|
+static void capture_websocket_handshake_request(
|
|
|
+ const Headers &client_headers,
|
|
|
+ std::function<void(const Request &, int port)> verify) {
|
|
|
Server svr;
|
|
|
|
|
|
std::mutex mtx;
|
|
|
- std::string received_host;
|
|
|
+ Request received;
|
|
|
|
|
|
svr.WebSocket("/ws", [&](const Request &req, ws::WebSocket &ws) {
|
|
|
{
|
|
|
std::lock_guard<std::mutex> lock(mtx);
|
|
|
- received_host = req.get_header_value("Host");
|
|
|
+ received = req;
|
|
|
}
|
|
|
std::string msg;
|
|
|
while (ws.read(msg)) {
|
|
|
@@ -19796,11 +19939,15 @@ TEST(WebSocketTest, HostHeaderInHandshake) {
|
|
|
|
|
|
auto port = svr.bind_to_any_port("localhost");
|
|
|
std::thread t([&]() { svr.listen_after_bind(); });
|
|
|
+ auto se = detail::scope_exit([&] {
|
|
|
+ svr.stop();
|
|
|
+ t.join();
|
|
|
+ });
|
|
|
svr.wait_until_ready();
|
|
|
|
|
|
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
|
|
|
+ ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws",
|
|
|
+ client_headers);
|
|
|
ASSERT_TRUE(client.connect());
|
|
|
- // Round-trip ensures the handler has run and captured the request.
|
|
|
ASSERT_TRUE(client.send("hello"));
|
|
|
std::string msg;
|
|
|
ASSERT_TRUE(client.read(msg));
|
|
|
@@ -19808,14 +19955,162 @@ TEST(WebSocketTest, HostHeaderInHandshake) {
|
|
|
|
|
|
{
|
|
|
std::lock_guard<std::mutex> lock(mtx);
|
|
|
+ verify(received, port);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+TEST(WebSocketTest, DefaultHeadersInHandshake) {
|
|
|
+ capture_websocket_handshake_request({}, [](const Request &req, int port) {
|
|
|
// Non-default port must be present in the Host header. Default ports
|
|
|
// (80/443) are omitted; that logic is covered by
|
|
|
// MakeHostAndPortStringTest.
|
|
|
- EXPECT_EQ("localhost:" + std::to_string(port), received_host);
|
|
|
- }
|
|
|
+ EXPECT_EQ("localhost:" + std::to_string(port),
|
|
|
+ req.get_header_value("Host"));
|
|
|
+ EXPECT_EQ(std::string("cpp-httplib/") + CPPHTTPLIB_VERSION,
|
|
|
+ req.get_header_value("User-Agent"));
|
|
|
+ EXPECT_FALSE(req.has_header("Accept"));
|
|
|
+ EXPECT_FALSE(req.has_header("Accept-Encoding"));
|
|
|
+ EXPECT_FALSE(req.has_header("Content-Length"));
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+TEST(WebSocketTest, UserHeadersOverrideGeneratedOnesInHandshake) {
|
|
|
+ capture_websocket_handshake_request(
|
|
|
+ {{"Host", "example.com"},
|
|
|
+ {"User-Agent", "custom-agent"},
|
|
|
+ {"X-Custom", "value"}},
|
|
|
+ [](const Request &req, int) {
|
|
|
+ EXPECT_EQ("example.com", req.get_header_value("Host"));
|
|
|
+ EXPECT_EQ(1U, req.get_header_value_count("Host"));
|
|
|
+ EXPECT_EQ("custom-agent", req.get_header_value("User-Agent"));
|
|
|
+ EXPECT_EQ(1U, req.get_header_value_count("User-Agent"));
|
|
|
+ EXPECT_EQ("value", req.get_header_value("X-Custom"));
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+TEST(WebSocketTest, MandatoryHeadersInHandshakeAreEnforced) {
|
|
|
+ capture_websocket_handshake_request(
|
|
|
+ {{"Upgrade", "bogus"},
|
|
|
+ {"Connection", "close"},
|
|
|
+ {"Sec-WebSocket-Key", "AAAAAAAAAAAAAAAAAAAAAA=="},
|
|
|
+ {"Sec-WebSocket-Version", "8"}},
|
|
|
+ [](const Request &req, int) {
|
|
|
+ EXPECT_EQ("websocket", req.get_header_value("Upgrade"));
|
|
|
+ EXPECT_EQ(1U, req.get_header_value_count("Upgrade"));
|
|
|
+ EXPECT_EQ("Upgrade", req.get_header_value("Connection"));
|
|
|
+ EXPECT_EQ(1U, req.get_header_value_count("Connection"));
|
|
|
+ EXPECT_EQ("13", req.get_header_value("Sec-WebSocket-Version"));
|
|
|
+ EXPECT_EQ(1U, req.get_header_value_count("Sec-WebSocket-Version"));
|
|
|
+ EXPECT_EQ(1U, req.get_header_value_count("Sec-WebSocket-Key"));
|
|
|
+ EXPECT_NE("AAAAAAAAAAAAAAAAAAAAAA==",
|
|
|
+ req.get_header_value("Sec-WebSocket-Key"));
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+TEST(WebSocketTest, InvalidHeaderInHandshakeWritesNothing) {
|
|
|
+ // A header the client refuses to send must abort the handshake before any
|
|
|
+ // part of it reaches the wire; a lone request line would otherwise sit in
|
|
|
+ // the peer's buffer as a truncated request.
|
|
|
+ auto srv = ::socket(AF_INET, SOCK_STREAM, 0);
|
|
|
+ ASSERT_NE(INVALID_SOCKET, srv);
|
|
|
+ auto se_srv = detail::scope_exit([&] { detail::close_socket(srv); });
|
|
|
+
|
|
|
+ sockaddr_in addr{};
|
|
|
+ addr.sin_family = AF_INET;
|
|
|
+ addr.sin_port = 0; // ephemeral, so parallel shards don't collide
|
|
|
+ ::inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
|
|
|
+ ASSERT_EQ(0, ::bind(srv, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)));
|
|
|
+ ASSERT_EQ(0, ::listen(srv, 1));
|
|
|
+
|
|
|
+ sockaddr_in bound{};
|
|
|
+ socklen_t bound_len = sizeof(bound);
|
|
|
+ ASSERT_EQ(
|
|
|
+ 0, ::getsockname(srv, reinterpret_cast<sockaddr *>(&bound), &bound_len));
|
|
|
+ auto port = ntohs(bound.sin_port);
|
|
|
+
|
|
|
+ ssize_t received = -1;
|
|
|
+ std::thread t([&] {
|
|
|
+ // Bound every blocking call so a regression fails the test with a bad
|
|
|
+ // value instead of hanging the suite.
|
|
|
+ fd_set rfds;
|
|
|
+ FD_ZERO(&rfds);
|
|
|
+ FD_SET(srv, &rfds);
|
|
|
+ timeval tv{5, 0};
|
|
|
+ if (::select(static_cast<int>(srv + 1), &rfds, nullptr, nullptr, &tv) <=
|
|
|
+ 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ sockaddr_in cli_addr{};
|
|
|
+ socklen_t cli_len = sizeof(cli_addr);
|
|
|
+ auto cli = ::accept(srv, reinterpret_cast<sockaddr *>(&cli_addr), &cli_len);
|
|
|
+ if (cli == INVALID_SOCKET) { return; }
|
|
|
+ auto se_cli = detail::scope_exit([&] { detail::close_socket(cli); });
|
|
|
+
|
|
|
+ detail::set_socket_opt_time(cli, SOL_SOCKET, SO_RCVTIMEO, 5, 0);
|
|
|
+ char buf[4096];
|
|
|
+ received = ::recv(cli, buf, sizeof(buf), 0);
|
|
|
+ });
|
|
|
+ // The CR/LF makes the value invalid, so check_and_write_headers rejects it.
|
|
|
+ // connect() has already shut the socket down by the time it returns false,
|
|
|
+ // so the peer sees EOF without waiting for the client to be destroyed.
|
|
|
+ ws::WebSocketClient client("ws://127.0.0.1:" + std::to_string(port) + "/ws",
|
|
|
+ {{"X-Bad", "a\r\nInjected: 1"}});
|
|
|
+ EXPECT_FALSE(client.connect());
|
|
|
|
|
|
- svr.stop();
|
|
|
t.join();
|
|
|
+
|
|
|
+ // 0 means the peer saw a clean EOF without a single byte of the handshake.
|
|
|
+ EXPECT_EQ(0, received);
|
|
|
+}
|
|
|
+
|
|
|
+TEST(WebSocketTest, HostHeaderOverUnixSocket) {
|
|
|
+ // The socket path doubles as the URL host, so it must not contain '/'.
|
|
|
+ const char *shard = getenv("GTEST_SHARD_INDEX");
|
|
|
+ const std::string sock_path =
|
|
|
+ shard ? std::string("httplib-ws-") + shard + ".sock"
|
|
|
+ : std::string("httplib-ws.sock");
|
|
|
+ std::remove(sock_path.c_str());
|
|
|
+
|
|
|
+ Server svr;
|
|
|
+
|
|
|
+ std::mutex mtx;
|
|
|
+ std::string received_host;
|
|
|
+
|
|
|
+ svr.WebSocket("/ws", [&](const Request &req, ws::WebSocket &ws) {
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> lock(mtx);
|
|
|
+ received_host = req.get_header_value("Host");
|
|
|
+ }
|
|
|
+ std::string msg;
|
|
|
+ while (ws.read(msg)) {
|
|
|
+ ws.send(msg);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ svr.set_address_family(AF_UNIX);
|
|
|
+
|
|
|
+ std::thread t([&]() { ASSERT_TRUE(svr.listen(sock_path, 80)); });
|
|
|
+ auto se = detail::scope_exit([&] {
|
|
|
+ svr.stop();
|
|
|
+ t.join();
|
|
|
+ std::remove(sock_path.c_str());
|
|
|
+ });
|
|
|
+ svr.wait_until_ready();
|
|
|
+
|
|
|
+ ws::WebSocketClient client("ws://" + sock_path + "/ws");
|
|
|
+ client.set_address_family(AF_UNIX);
|
|
|
+ ASSERT_TRUE(client.connect());
|
|
|
+ ASSERT_TRUE(client.send("hello"));
|
|
|
+ std::string msg;
|
|
|
+ ASSERT_TRUE(client.read(msg));
|
|
|
+ client.close();
|
|
|
+
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> lock(mtx);
|
|
|
+ // There is no host:port for a Unix socket, so the same "localhost"
|
|
|
+ // placeholder the HTTP client uses is expected.
|
|
|
+ EXPECT_EQ("localhost", received_host);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|