|
|
@@ -18276,3 +18276,600 @@ TEST(KeepAliveTest, DeleteWithoutContentLengthDoesNotEatNextRequest) {
|
|
|
|
|
|
EXPECT_EQ(2, delete_count.load());
|
|
|
}
|
|
|
+
|
|
|
+namespace no_proxy_test {
|
|
|
+
|
|
|
+// Server bound to 127.0.0.1:<dynamic>, listen thread spawned by listen(),
|
|
|
+// auto-stopped + joined on scope exit. Register handlers via svr() BEFORE
|
|
|
+// calling listen().
|
|
|
+class ScopedServer {
|
|
|
+public:
|
|
|
+ ScopedServer() { port_ = svr_.bind_to_any_port("127.0.0.1"); }
|
|
|
+ ~ScopedServer() {
|
|
|
+ svr_.stop();
|
|
|
+ if (th_.joinable()) { th_.join(); }
|
|
|
+ }
|
|
|
+ Server &svr() { return svr_; }
|
|
|
+ int port() const { return port_; }
|
|
|
+ void listen() {
|
|
|
+ th_ = std::thread([this] { svr_.listen_after_bind(); });
|
|
|
+ svr_.wait_until_ready();
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ Server svr_;
|
|
|
+ std::thread th_;
|
|
|
+ int port_ = 0;
|
|
|
+};
|
|
|
+
|
|
|
+class ProxyAndTargetServers {
|
|
|
+public:
|
|
|
+ ProxyAndTargetServers() {
|
|
|
+ proxy_mock_.Get(".*", [this](const Request &req, Response &res) {
|
|
|
+ proxy_hits_++;
|
|
|
+ last_had_proxy_authz_ = req.has_header("Proxy-Authorization");
|
|
|
+ res.set_content("via-proxy", "text/plain");
|
|
|
+ });
|
|
|
+ target_.Get(".*", [this](const Request &req, Response &res) {
|
|
|
+ target_hits_++;
|
|
|
+ last_had_proxy_authz_ = req.has_header("Proxy-Authorization");
|
|
|
+ res.set_content("direct", "text/plain");
|
|
|
+ });
|
|
|
+
|
|
|
+ proxy_port_ = proxy_mock_.bind_to_any_port("127.0.0.1");
|
|
|
+ target_port_ = target_.bind_to_any_port("127.0.0.1");
|
|
|
+ proxy_thread_ = std::thread([this] { proxy_mock_.listen_after_bind(); });
|
|
|
+ target_thread_ = std::thread([this] { target_.listen_after_bind(); });
|
|
|
+ proxy_mock_.wait_until_ready();
|
|
|
+ target_.wait_until_ready();
|
|
|
+ }
|
|
|
+
|
|
|
+ ~ProxyAndTargetServers() {
|
|
|
+ proxy_mock_.stop();
|
|
|
+ target_.stop();
|
|
|
+ if (proxy_thread_.joinable()) { proxy_thread_.join(); }
|
|
|
+ if (target_thread_.joinable()) { target_thread_.join(); }
|
|
|
+ }
|
|
|
+
|
|
|
+ Server &proxy_mock() { return proxy_mock_; }
|
|
|
+ Server &target() { return target_; }
|
|
|
+ int proxy_port() const { return proxy_port_; }
|
|
|
+ int target_port() const { return target_port_; }
|
|
|
+ int proxy_hits() const { return proxy_hits_.load(); }
|
|
|
+ int target_hits() const { return target_hits_.load(); }
|
|
|
+ bool last_had_proxy_authz() const { return last_had_proxy_authz_.load(); }
|
|
|
+
|
|
|
+ void reset_counters() {
|
|
|
+ proxy_hits_ = 0;
|
|
|
+ target_hits_ = 0;
|
|
|
+ last_had_proxy_authz_ = false;
|
|
|
+ }
|
|
|
+
|
|
|
+private:
|
|
|
+ Server proxy_mock_;
|
|
|
+ Server target_;
|
|
|
+ std::thread proxy_thread_;
|
|
|
+ std::thread target_thread_;
|
|
|
+ int proxy_port_ = 0;
|
|
|
+ int target_port_ = 0;
|
|
|
+ std::atomic<int> proxy_hits_{0};
|
|
|
+ std::atomic<int> target_hits_{0};
|
|
|
+ std::atomic<bool> last_had_proxy_authz_{false};
|
|
|
+};
|
|
|
+
|
|
|
+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_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ return cli;
|
|
|
+}
|
|
|
+
|
|
|
+} // namespace no_proxy_test
|
|
|
+
|
|
|
+using no_proxy_test::make_client;
|
|
|
+using no_proxy_test::ProxyAndTargetServers;
|
|
|
+
|
|
|
+TEST(NoProxyTest, ExactHostnameBypasses) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("example.com", s);
|
|
|
+ cli->set_no_proxy({"example.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, SubdomainBypasses) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("foo.example.com", s);
|
|
|
+ cli->set_no_proxy({"example.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, EvilExampleDoesNotMatchExample) {
|
|
|
+ // Regression guard: "evilexample.com" must not be considered a subdomain
|
|
|
+ // of "example.com". Without the dot-boundary rule, a naive endsWith
|
|
|
+ // check would let traffic bypass the proxy and leak credentials direct
|
|
|
+ // to the attacker host.
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("evilexample.com", s);
|
|
|
+ cli->set_no_proxy({"example.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, ExampleDotEvilDoesNotMatchExample) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("example.com.evil.com", s);
|
|
|
+ cli->set_no_proxy({"example.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, LeadingDotPatternMatchesBareDomain) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("example.com", s);
|
|
|
+ cli->set_no_proxy({".example.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, CaseInsensitiveHostname) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("Example.COM", s);
|
|
|
+ cli->set_no_proxy({"EXAMPLE.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, TrailingDotIsNormalized) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("example.com.", s);
|
|
|
+ cli->set_no_proxy({"example.com"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, TrailingDotOnEntryIsNormalized) {
|
|
|
+ // Trailing dots must be normalized on BOTH sides — host and entry.
|
|
|
+ // An implementation that only strips the host-side trailing dot would
|
|
|
+ // fail to match host "example.com" against entry "example.com." because
|
|
|
+ // a literal substring search would compare 11 chars against 12.
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("example.com", s);
|
|
|
+ cli->set_no_proxy({"example.com."});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, WildcardBypassesEverything) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("anything.invalid.test", s);
|
|
|
+ cli->set_no_proxy({"*"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, IPv4LiteralExactMatch) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"127.0.0.1"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+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_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"::1"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, BareIPv6LiteralMatchesIPv6Cidr) {
|
|
|
+ // Regression guard: a bare IPv6 host literal (no surrounding brackets)
|
|
|
+ // must still be recognized as IPv6 for CIDR matching. Implementations
|
|
|
+ // that detect IPv6 only when the host begins with '[' would parse the
|
|
|
+ // 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_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"fe80::/10"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, BracketedIPv6EntryAccepted) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("::1", s.target_port());
|
|
|
+ cli.set_hostname_addr_map({{"::1", "127.0.0.1"}});
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"[::1]"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+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_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"[fe80::]/10"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, IPv4MappedIPv6IsNotCrossMatchedAgainstIPv4Entry) {
|
|
|
+ // Policy: keep address families separate. "::ffff:1.2.3.4" must NOT
|
|
|
+ // satisfy a NO_PROXY entry of "1.2.3.4". This avoids subtle bypass
|
|
|
+ // 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_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"127.0.0.1"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, IPv4CidrMatch) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"127.0.0.0/8"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, IPv4CidrNonMatch) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"10.0.0.0/8"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, IPv4CidrPrefixZeroMatchesAll) {
|
|
|
+ // Prefix 0 must not trigger the (1u << 32) shift UB. Result: every
|
|
|
+ // IPv4 target matches.
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"0.0.0.0/0"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, IPv4CidrSingleHostNoSlash) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"127.0.0.1"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, MalformedCidrPrefixIsDropped) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"127.0.0.0/33"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, TrailingSlashCidrIsRejected) {
|
|
|
+ // Empty prefix after the slash must be rejected, not silently treated as /32.
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({"127.0.0.1/"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, ProxyAuthorizationSuppressedWhenBypassed) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("internal.corp", s);
|
|
|
+ cli->set_proxy_basic_auth("u", "p");
|
|
|
+ cli->set_no_proxy({"internal.corp"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_FALSE(s.last_had_proxy_authz())
|
|
|
+ << "Proxy-Authorization must not be sent direct to the target";
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, ProxyAuthorizationSentWhenNotBypassed) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("public.example", s);
|
|
|
+ cli->set_proxy_basic_auth("u", "p");
|
|
|
+ cli->set_no_proxy({"internal.corp"}); // does not match
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_TRUE(s.last_had_proxy_authz());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, EmptyNoProxyKeepsProxyOn) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("anything.test", s);
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, PortSpecificEntryRejected) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("example.com", s);
|
|
|
+ cli->set_no_proxy({"example.com:8080"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, EmptyAndWhitespaceEntriesDropped) {
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ auto cli = make_client("anything.test", s);
|
|
|
+ cli->set_no_proxy({"", " ", "\t"});
|
|
|
+
|
|
|
+ auto res = cli->Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(s.proxy_hits(), 1);
|
|
|
+ EXPECT_EQ(0, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, ValidEntryWithSurroundingWhitespaceStillMatches) {
|
|
|
+ // An entry with leading/trailing whitespace must still match — env-style
|
|
|
+ // values are commonly pasted with stray spaces and an implementation
|
|
|
+ // that feeds the raw token directly to inet_pton would fail to match
|
|
|
+ // valid CIDRs because of the spaces.
|
|
|
+ ProxyAndTargetServers s;
|
|
|
+ Client cli("127.0.0.1", s.target_port());
|
|
|
+ cli.set_proxy("127.0.0.1", s.proxy_port());
|
|
|
+ cli.set_no_proxy({" 127.0.0.0/8 "});
|
|
|
+
|
|
|
+ auto res = cli.Get("/");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(0, s.proxy_hits());
|
|
|
+ EXPECT_EQ(1, s.target_hits());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, RedirectToBypassedHostStripsProxyAndProxyAuth) {
|
|
|
+ // Analog of GHSA-6hrp-7fq9-3qv2: redirect to a NO_PROXY-matched host must
|
|
|
+ // go direct and must NOT carry Proxy-Authorization.
|
|
|
+ std::atomic<int> proxy_hits{0};
|
|
|
+ std::atomic<int> target_hits{0};
|
|
|
+ std::atomic<bool> target_saw_authz{false};
|
|
|
+
|
|
|
+ no_proxy_test::ScopedServer proxy_mock, target;
|
|
|
+
|
|
|
+ proxy_mock.svr().Get(".*", [&](const Request &, Response &res) {
|
|
|
+ proxy_hits++;
|
|
|
+ res.status = 302;
|
|
|
+ res.set_header("Location", "http://127.0.0.1:" +
|
|
|
+ std::to_string(target.port()) + "/landed");
|
|
|
+ });
|
|
|
+ target.svr().Get(".*", [&](const Request &req, Response &res) {
|
|
|
+ target_hits++;
|
|
|
+ if (req.has_header("Proxy-Authorization")) { target_saw_authz = true; }
|
|
|
+ res.set_content("direct", "text/plain");
|
|
|
+ });
|
|
|
+ proxy_mock.listen();
|
|
|
+ target.listen();
|
|
|
+
|
|
|
+ Client cli("public.example", target.port());
|
|
|
+ cli.set_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"});
|
|
|
+ cli.set_follow_location(true);
|
|
|
+
|
|
|
+ auto res = cli.Get("/redir");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_GE(proxy_hits.load(), 1);
|
|
|
+ EXPECT_GE(target_hits.load(), 1);
|
|
|
+ EXPECT_FALSE(target_saw_authz.load());
|
|
|
+}
|
|
|
+
|
|
|
+#ifdef CPPHTTPLIB_SSL_ENABLED
|
|
|
+TEST(NoProxyTest, BypassedTargetReturning407DoesNotLeakProxyDigestCredentials) {
|
|
|
+ // Direct origin replying 407 must not trigger the digest retry; otherwise
|
|
|
+ // proxy creds would be sent to the (possibly hostile) origin.
|
|
|
+ std::atomic<int> target_hits{0};
|
|
|
+ std::atomic<bool> target_saw_proxy_authz{false};
|
|
|
+
|
|
|
+ no_proxy_test::ScopedServer target;
|
|
|
+ target.svr().Get(".*", [&](const Request &req, Response &res) {
|
|
|
+ target_hits++;
|
|
|
+ if (req.has_header("Proxy-Authorization")) {
|
|
|
+ target_saw_proxy_authz = true;
|
|
|
+ }
|
|
|
+ res.status = StatusCode::ProxyAuthenticationRequired_407;
|
|
|
+ res.set_header("Proxy-Authenticate", "Digest realm=\"evil\", qop=\"auth\", "
|
|
|
+ "nonce=\"abc\", algorithm=MD5");
|
|
|
+ });
|
|
|
+ target.listen();
|
|
|
+
|
|
|
+ // The proxy address is set to the target's port: the bypass MUST kick in;
|
|
|
+ // 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_proxy("127.0.0.1", target.port());
|
|
|
+ cli.set_proxy_digest_auth("proxy-user", "proxy-pass");
|
|
|
+ cli.set_no_proxy({"evil.example"});
|
|
|
+
|
|
|
+ auto res = cli.Get("/x");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(StatusCode::ProxyAuthenticationRequired_407, res->status);
|
|
|
+ EXPECT_EQ(1, target_hits.load());
|
|
|
+ EXPECT_FALSE(target_saw_proxy_authz.load());
|
|
|
+}
|
|
|
+#endif
|
|
|
+
|
|
|
+TEST(NoProxyTest, MultiHopRedirectThroughBypassedHostKeepsProxy) {
|
|
|
+ // A (via proxy) → B (NO_PROXY-matched, direct) → C must re-engage the proxy.
|
|
|
+ std::atomic<int> proxy_hits{0};
|
|
|
+ std::atomic<int> bypass_hits{0};
|
|
|
+ std::atomic<bool> proxy_saw_c_url{false};
|
|
|
+
|
|
|
+ no_proxy_test::ScopedServer proxy_mock, bypass_server;
|
|
|
+
|
|
|
+ proxy_mock.svr().Get(".*", [&](const Request &req, Response &res) {
|
|
|
+ proxy_hits++;
|
|
|
+ if (req.path.find("/start") != std::string::npos) {
|
|
|
+ res.status = 302;
|
|
|
+ res.set_header("Location", "http://127.0.0.1:" +
|
|
|
+ std::to_string(bypass_server.port()) +
|
|
|
+ "/middle");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (req.path.find("/end") != std::string::npos) {
|
|
|
+ proxy_saw_c_url = true;
|
|
|
+ res.set_content("c-via-proxy", "text/plain");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ res.set_content("unexpected", "text/plain");
|
|
|
+ });
|
|
|
+ // public.example's "advertised" port is arbitrary (the request never lands
|
|
|
+ // there — it goes through the proxy), but use a dynamic value to stay
|
|
|
+ // friendly with sharded parallel runs.
|
|
|
+ int public_port = bypass_server.port();
|
|
|
+ bypass_server.svr().Get(".*", [&](const Request &, Response &res) {
|
|
|
+ bypass_hits++;
|
|
|
+ res.status = 302;
|
|
|
+ res.set_header("Location", "http://public.example:" +
|
|
|
+ std::to_string(public_port) + "/end");
|
|
|
+ });
|
|
|
+ proxy_mock.listen();
|
|
|
+ bypass_server.listen();
|
|
|
+
|
|
|
+ Client cli("public.example", public_port);
|
|
|
+ cli.set_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);
|
|
|
+
|
|
|
+ auto res = cli.Get("/start");
|
|
|
+ ASSERT_TRUE(res);
|
|
|
+ EXPECT_EQ(StatusCode::OK_200, res->status);
|
|
|
+ EXPECT_EQ("c-via-proxy", res->body);
|
|
|
+ EXPECT_GE(proxy_hits.load(), 2);
|
|
|
+ EXPECT_GE(bypass_hits.load(), 1);
|
|
|
+ EXPECT_TRUE(proxy_saw_c_url.load());
|
|
|
+}
|
|
|
+
|
|
|
+TEST(NoProxyTest, KeepAliveSocketInvalidatedOnSetNoProxy) {
|
|
|
+ // Mid-session set_no_proxy must drop any keep-alive socket so the next
|
|
|
+ // request reconnects to the correct endpoint.
|
|
|
+ std::atomic<int> proxy_hits{0};
|
|
|
+ std::atomic<int> target_hits{0};
|
|
|
+
|
|
|
+ no_proxy_test::ScopedServer proxy_mock, target;
|
|
|
+
|
|
|
+ proxy_mock.svr().Get(".*", [&](const Request &, Response &res) {
|
|
|
+ proxy_hits++;
|
|
|
+ res.set_content("via-proxy", "text/plain");
|
|
|
+ });
|
|
|
+ target.svr().Get(".*", [&](const Request &, Response &res) {
|
|
|
+ target_hits++;
|
|
|
+ res.set_content("direct", "text/plain");
|
|
|
+ });
|
|
|
+ proxy_mock.listen();
|
|
|
+ target.listen();
|
|
|
+
|
|
|
+ Client cli("public.example", target.port());
|
|
|
+ cli.set_hostname_addr_map({{"public.example", "127.0.0.1"}});
|
|
|
+ cli.set_proxy("127.0.0.1", proxy_mock.port());
|
|
|
+ cli.set_keep_alive(true);
|
|
|
+
|
|
|
+ auto res1 = cli.Get("/a");
|
|
|
+ ASSERT_TRUE(res1);
|
|
|
+ EXPECT_EQ("via-proxy", res1->body);
|
|
|
+ EXPECT_EQ(1, proxy_hits.load());
|
|
|
+ EXPECT_EQ(0, target_hits.load());
|
|
|
+
|
|
|
+ cli.set_no_proxy({"public.example"});
|
|
|
+
|
|
|
+ auto res2 = cli.Get("/b");
|
|
|
+ ASSERT_TRUE(res2);
|
|
|
+ EXPECT_EQ("direct", res2->body);
|
|
|
+ EXPECT_EQ(1, proxy_hits.load());
|
|
|
+ EXPECT_EQ(1, target_hits.load());
|
|
|
+}
|