| // Copyright 2019 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "net/http/http_proxy_connect_job.h" |
| |
| #include <algorithm> |
| #include <map> |
| #include <string> |
| #include <utility> |
| |
| #include "base/metrics/field_trial.h" |
| #include "base/metrics/field_trial_param_associator.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "net/base/host_port_pair.h" |
| #include "net/base/network_anonymization_key.h" |
| #include "net/base/proxy_string_util.h" |
| #include "net/base/test_proxy_delegate.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/dns/public/secure_dns_policy.h" |
| #include "net/http/http_network_session.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/nqe/network_quality_estimator_test_util.h" |
| #include "net/socket/client_socket_handle.h" |
| #include "net/socket/connect_job_test_util.h" |
| #include "net/socket/socket_test_util.h" |
| #include "net/socket/socks_connect_job.h" |
| #include "net/socket/ssl_client_socket.h" |
| #include "net/socket/ssl_connect_job.h" |
| #include "net/socket/transport_connect_job.h" |
| #include "net/spdy/spdy_test_util_common.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/gtest_util.h" |
| #include "net/test/test_data_directory.h" |
| #include "net/test/test_with_task_environment.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| #include "url/scheme_host_port.h" |
| |
| namespace net { |
| |
| namespace { |
| |
| const char kEndpointHost[] = "www.endpoint.test"; |
| |
| enum HttpProxyType { HTTP, HTTPS, SPDY }; |
| |
| const char kHttpProxyHost[] = "httpproxy.example.test"; |
| const char kHttpsProxyHost[] = "httpsproxy.example.test"; |
| |
| } // namespace |
| |
| class HttpProxyConnectJobTest : public ::testing::TestWithParam<HttpProxyType>, |
| public WithTaskEnvironment { |
| protected: |
| HttpProxyConnectJobTest() |
| : WithTaskEnvironment( |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME) { |
| // Used a mock HostResolver that does not have a cache. |
| session_deps_.host_resolver = std::make_unique<MockHostResolver>( |
| /*default_result=*/MockHostResolverBase::RuleResolver:: |
| GetLocalhostResult()); |
| |
| network_quality_estimator_ = |
| std::make_unique<TestNetworkQualityEstimator>(); |
| session_ = SpdySessionDependencies::SpdyCreateSession(&session_deps_); |
| InitCommonConnectJobParams(); |
| } |
| |
| ~HttpProxyConnectJobTest() override { |
| // Reset global field trial parameters to defaults values. |
| base::FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting(); |
| HttpProxyConnectJob::UpdateFieldTrialParametersForTesting(); |
| } |
| |
| // Initializes the field trial parameters for the field trial that determines |
| // connection timeout based on the network quality. |
| void InitAdaptiveTimeoutFieldTrialWithParams( |
| bool use_default_params, |
| int ssl_http_rtt_multiplier, |
| int non_ssl_http_rtt_multiplier, |
| base::TimeDelta min_proxy_connection_timeout, |
| base::TimeDelta max_proxy_connection_timeout) { |
| std::string trial_name = "NetAdaptiveProxyConnectionTimeout"; |
| std::string group_name = "GroupName"; |
| |
| std::map<std::string, std::string> params; |
| if (!use_default_params) { |
| params["ssl_http_rtt_multiplier"] = |
| base::NumberToString(ssl_http_rtt_multiplier); |
| params["non_ssl_http_rtt_multiplier"] = |
| base::NumberToString(non_ssl_http_rtt_multiplier); |
| params["min_proxy_connection_timeout_seconds"] = |
| base::NumberToString(min_proxy_connection_timeout.InSeconds()); |
| params["max_proxy_connection_timeout_seconds"] = |
| base::NumberToString(max_proxy_connection_timeout.InSeconds()); |
| } |
| base::FieldTrialParamAssociator::GetInstance()->ClearAllParamsForTesting(); |
| EXPECT_TRUE( |
| base::AssociateFieldTrialParams(trial_name, group_name, params)); |
| EXPECT_TRUE(base::FieldTrialList::CreateFieldTrial(trial_name, group_name)); |
| |
| // Force static global that reads the field trials to update. |
| HttpProxyConnectJob::UpdateFieldTrialParametersForTesting(); |
| } |
| |
| scoped_refptr<TransportSocketParams> CreateHttpProxyParams( |
| SecureDnsPolicy secure_dns_policy) const { |
| if (GetParam() != HTTP) |
| return nullptr; |
| return base::MakeRefCounted<TransportSocketParams>( |
| HostPortPair(kHttpProxyHost, 80), NetworkAnonymizationKey(), |
| secure_dns_policy, OnHostResolutionCallback(), |
| /*supported_alpns=*/base::flat_set<std::string>()); |
| } |
| |
| scoped_refptr<SSLSocketParams> CreateHttpsProxyParams( |
| SecureDnsPolicy secure_dns_policy) const { |
| if (GetParam() == HTTP) |
| return nullptr; |
| return base::MakeRefCounted<SSLSocketParams>( |
| base::MakeRefCounted<TransportSocketParams>( |
| HostPortPair(kHttpsProxyHost, 443), NetworkAnonymizationKey(), |
| secure_dns_policy, OnHostResolutionCallback(), |
| /*supported_alpns=*/base::flat_set<std::string>()), |
| nullptr, nullptr, HostPortPair(kHttpsProxyHost, 443), SSLConfig(), |
| PRIVACY_MODE_DISABLED, NetworkAnonymizationKey()); |
| } |
| |
| // Returns a correctly constructed HttpProxyParams for the HTTP or HTTPS |
| // proxy. |
| scoped_refptr<HttpProxySocketParams> CreateParams( |
| bool tunnel, |
| SecureDnsPolicy secure_dns_policy) { |
| return base::MakeRefCounted<HttpProxySocketParams>( |
| CreateHttpProxyParams(secure_dns_policy), |
| CreateHttpsProxyParams(secure_dns_policy), false /* is_quic */, |
| HostPortPair(kEndpointHost, tunnel ? 443 : 80), tunnel, |
| TRAFFIC_ANNOTATION_FOR_TESTS, NetworkAnonymizationKey()); |
| } |
| |
| std::unique_ptr<HttpProxyConnectJob> CreateConnectJobForHttpRequest( |
| ConnectJob::Delegate* delegate, |
| RequestPriority priority = DEFAULT_PRIORITY, |
| SecureDnsPolicy secure_dns_policy = SecureDnsPolicy::kAllow) { |
| return CreateConnectJob(CreateParams(false /* tunnel */, secure_dns_policy), |
| delegate, priority); |
| } |
| |
| std::unique_ptr<HttpProxyConnectJob> CreateConnectJobForTunnel( |
| ConnectJob::Delegate* delegate, |
| RequestPriority priority = DEFAULT_PRIORITY, |
| SecureDnsPolicy secure_dns_policy = SecureDnsPolicy::kAllow) { |
| return CreateConnectJob(CreateParams(true /* tunnel */, secure_dns_policy), |
| delegate, priority); |
| } |
| |
| std::unique_ptr<HttpProxyConnectJob> CreateConnectJob( |
| scoped_refptr<HttpProxySocketParams> http_proxy_socket_params, |
| ConnectJob::Delegate* delegate, |
| RequestPriority priority) { |
| return std::make_unique<HttpProxyConnectJob>( |
| priority, SocketTag(), common_connect_job_params_.get(), |
| std::move(http_proxy_socket_params), delegate, nullptr /* net_log */); |
| } |
| |
| // This may only be called at the start of the test, before any ConnectJobs |
| // have been created. |
| void InitProxyDelegate() { |
| proxy_delegate_ = std::make_unique<TestProxyDelegate>(); |
| InitCommonConnectJobParams(); |
| } |
| |
| // This may only be called at the start of the test, before any ConnectJobs |
| // have been created. |
| void InitCommonConnectJobParams() { |
| common_connect_job_params_ = std::make_unique<CommonConnectJobParams>( |
| session_->CreateCommonConnectJobParams()); |
| // TODO(mmenke): Consider reworking this so it can be done through |
| // |session_deps_|. |
| common_connect_job_params_->proxy_delegate = proxy_delegate_.get(); |
| common_connect_job_params_->network_quality_estimator = |
| network_quality_estimator_.get(); |
| } |
| |
| void Initialize(base::span<const MockRead> reads, |
| base::span<const MockWrite> writes, |
| base::span<const MockRead> spdy_reads, |
| base::span<const MockWrite> spdy_writes, |
| IoMode connect_and_ssl_io_mode) { |
| if (GetParam() == SPDY) { |
| data_ = std::make_unique<SequencedSocketData>(spdy_reads, spdy_writes); |
| } else { |
| data_ = std::make_unique<SequencedSocketData>(reads, writes); |
| } |
| |
| data_->set_connect_data(MockConnect(connect_and_ssl_io_mode, OK)); |
| |
| session_deps_.socket_factory->AddSocketDataProvider(data_.get()); |
| |
| if (GetParam() != HTTP) { |
| // Keep the old ssl_data in case there is a draining socket. |
| old_ssl_data_.swap(ssl_data_); |
| ssl_data_ = |
| std::make_unique<SSLSocketDataProvider>(connect_and_ssl_io_mode, OK); |
| if (GetParam() == SPDY) { |
| InitializeSpdySsl(ssl_data_.get()); |
| } |
| session_deps_.socket_factory->AddSSLSocketDataProvider(ssl_data_.get()); |
| } |
| } |
| |
| void InitializeSpdySsl(SSLSocketDataProvider* ssl_data) { |
| ssl_data->next_proto = kProtoHTTP2; |
| } |
| |
| // Return the timeout for establishing the lower layer connection. i.e., for |
| // an HTTP proxy, the TCP connection timeout, and for an HTTPS proxy, the |
| // TCP+SSL connection timeout. In many cases, this will return the return |
| // value of the "AlternateNestedConnectionTimeout()". |
| base::TimeDelta GetNestedConnectionTimeout() { |
| base::TimeDelta normal_nested_connection_timeout = |
| TransportConnectJob::ConnectionTimeout(); |
| if (GetParam() != HTTP) |
| normal_nested_connection_timeout += |
| SSLConnectJob::HandshakeTimeoutForTesting(); |
| |
| // Doesn't actually matter whether or not this is for a tunnel - the |
| // connection timeout is the same, though it probably shouldn't be the same, |
| // since tunnels need an extra round trip. |
| base::TimeDelta alternate_connection_timeout = |
| HttpProxyConnectJob::AlternateNestedConnectionTimeout( |
| *CreateParams(true /* tunnel */, SecureDnsPolicy::kAllow), |
| network_quality_estimator_.get()); |
| |
| // If there's an alternate connection timeout, and it's less than the |
| // standard TCP+SSL timeout (Which is also applied by the nested connect |
| // jobs), return the alternate connection timeout. Otherwise, return the |
| // normal timeout. |
| if (!alternate_connection_timeout.is_zero() && |
| alternate_connection_timeout < normal_nested_connection_timeout) { |
| return alternate_connection_timeout; |
| } |
| |
| return normal_nested_connection_timeout; |
| } |
| |
| protected: |
| std::unique_ptr<TestProxyDelegate> proxy_delegate_; |
| |
| std::unique_ptr<SSLSocketDataProvider> ssl_data_; |
| std::unique_ptr<SSLSocketDataProvider> old_ssl_data_; |
| std::unique_ptr<SequencedSocketData> data_; |
| SpdySessionDependencies session_deps_; |
| |
| std::unique_ptr<TestNetworkQualityEstimator> network_quality_estimator_; |
| |
| std::unique_ptr<HttpNetworkSession> session_; |
| |
| SpdyTestUtil spdy_util_; |
| |
| TestCompletionCallback callback_; |
| |
| std::unique_ptr<CommonConnectJobParams> common_connect_job_params_; |
| }; |
| |
| // All tests are run with three different proxy types: HTTP, HTTPS (non-SPDY) |
| // and SPDY. |
| INSTANTIATE_TEST_SUITE_P(HttpProxyType, |
| HttpProxyConnectJobTest, |
| ::testing::Values(HTTP, HTTPS, SPDY)); |
| |
| TEST_P(HttpProxyConnectJobTest, NoTunnel) { |
| InitProxyDelegate(); |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| base::HistogramTester histogram_tester; |
| |
| Initialize(base::span<MockRead>(), base::span<MockWrite>(), |
| base::span<MockRead>(), base::span<MockWrite>(), io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForHttpRequest(&test_delegate); |
| test_delegate.StartJobExpectingResult(connect_job.get(), OK, |
| io_mode == SYNCHRONOUS); |
| EXPECT_FALSE(proxy_delegate_->on_before_tunnel_request_called()); |
| |
| // Proxies should not set any DNS aliases. |
| EXPECT_TRUE(test_delegate.socket()->GetDnsAliases().empty()); |
| |
| bool is_secure_proxy = GetParam() == HTTPS || GetParam() == SPDY; |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Insecure.Success", |
| is_secure_proxy ? 0 : 1); |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Secure.Success", is_secure_proxy ? 1 : 0); |
| } |
| } |
| |
| // Pauses an HttpProxyConnectJob at various states, and check the value of |
| // HasEstablishedConnection(). |
| TEST_P(HttpProxyConnectJobTest, HasEstablishedConnectionNoTunnel) { |
| session_deps_.host_resolver->set_ondemand_mode(true); |
| |
| SequencedSocketData data; |
| data.set_connect_data(MockConnect(ASYNC, OK)); |
| session_deps_.socket_factory->AddSocketDataProvider(&data); |
| |
| // Set up SSL, if needed. |
| SSLSocketDataProvider ssl_data(ASYNC, OK); |
| switch (GetParam()) { |
| case HTTP: |
| // No SSL needed. |
| break; |
| case HTTPS: |
| // SSL negotiation is the last step in non-tunnel connections over HTTPS |
| // proxies, so pause there, to check the final state before completion. |
| ssl_data = SSLSocketDataProvider(SYNCHRONOUS, ERR_IO_PENDING); |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| break; |
| case SPDY: |
| InitializeSpdySsl(&ssl_data); |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| break; |
| } |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForHttpRequest(&test_delegate); |
| |
| // Connecting should run until the request hits the HostResolver. |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_TRUE(session_deps_.host_resolver->has_pending_requests()); |
| EXPECT_EQ(LOAD_STATE_RESOLVING_HOST, connect_job->GetLoadState()); |
| EXPECT_FALSE(connect_job->HasEstablishedConnection()); |
| |
| // Once the HostResolver completes, the job should start establishing a |
| // connection, which will complete asynchronously. |
| session_deps_.host_resolver->ResolveOnlyRequestNow(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_CONNECTING, connect_job->GetLoadState()); |
| EXPECT_FALSE(connect_job->HasEstablishedConnection()); |
| |
| switch (GetParam()) { |
| case HTTP: |
| case SPDY: |
| // Connection completes. Since no tunnel is established, the socket is |
| // returned immediately, and HasEstablishedConnection() is only specified |
| // to work before the ConnectJob completes. |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); |
| break; |
| case HTTPS: |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_SSL_HANDSHAKE, connect_job->GetLoadState()); |
| EXPECT_TRUE(connect_job->HasEstablishedConnection()); |
| |
| // Unfortunately, there's no API to advance the paused SSL negotiation, |
| // so just end the test here. |
| } |
| } |
| |
| // Pauses an HttpProxyConnectJob at various states, and check the value of |
| // HasEstablishedConnection(). |
| TEST_P(HttpProxyConnectJobTest, HasEstablishedConnectionTunnel) { |
| session_deps_.host_resolver->set_ondemand_mode(true); |
| |
| // HTTP proxy CONNECT request / response, with a pause during the read. |
| MockWrite http1_writes[] = { |
| MockWrite(ASYNC, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| }; |
| MockRead http1_reads[] = { |
| // Pause at first read. |
| MockRead(ASYNC, ERR_IO_PENDING, 1), |
| MockRead(ASYNC, 2, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| SequencedSocketData http1_data(http1_reads, http1_writes); |
| http1_data.set_connect_data(MockConnect(ASYNC, OK)); |
| |
| // SPDY proxy CONNECT request / response, with a pause during the read. |
| spdy::SpdySerializedFrame req(spdy_util_.ConstructSpdyConnect( |
| nullptr, 0, 1, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| MockWrite spdy_writes[] = {CreateMockWrite(req, 0)}; |
| spdy::SpdySerializedFrame resp( |
| spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1)); |
| MockRead spdy_reads[] = { |
| // Pause at first read. |
| MockRead(ASYNC, ERR_IO_PENDING, 1), |
| CreateMockRead(resp, 2, ASYNC), |
| MockRead(ASYNC, 0, 3), |
| }; |
| SequencedSocketData spdy_data(spdy_reads, spdy_writes); |
| spdy_data.set_connect_data(MockConnect(ASYNC, OK)); |
| |
| // Will point to either the HTTP/1.x or SPDY data, depending on GetParam(). |
| SequencedSocketData* sequenced_data = nullptr; |
| |
| SSLSocketDataProvider ssl_data(ASYNC, OK); |
| ssl_data.ssl_info.cert = |
| ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem"); |
| ASSERT_TRUE(ssl_data.ssl_info.cert); |
| |
| switch (GetParam()) { |
| case HTTP: |
| sequenced_data = &http1_data; |
| break; |
| case HTTPS: |
| sequenced_data = &http1_data; |
| ssl_data.next_proto = NextProto::kProtoHTTP11; |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| break; |
| case SPDY: |
| sequenced_data = &spdy_data; |
| InitializeSpdySsl(&ssl_data); |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| break; |
| } |
| |
| session_deps_.socket_factory->AddSocketDataProvider(sequenced_data); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| |
| // Connecting should run until the request hits the HostResolver. |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_TRUE(session_deps_.host_resolver->has_pending_requests()); |
| EXPECT_EQ(LOAD_STATE_RESOLVING_HOST, connect_job->GetLoadState()); |
| EXPECT_FALSE(connect_job->HasEstablishedConnection()); |
| |
| // Once the HostResolver completes, the job should start establishing a |
| // connection, which will complete asynchronously. |
| session_deps_.host_resolver->ResolveOnlyRequestNow(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_CONNECTING, connect_job->GetLoadState()); |
| EXPECT_FALSE(connect_job->HasEstablishedConnection()); |
| |
| // Run until the socket starts reading the proxy's handshake response. |
| sequenced_data->RunUntilPaused(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_ESTABLISHING_PROXY_TUNNEL, connect_job->GetLoadState()); |
| EXPECT_TRUE(connect_job->HasEstablishedConnection()); |
| |
| // Finish the read, and run the job until it's complete. |
| sequenced_data->Resume(); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); |
| |
| // Proxies should not set any DNS aliases. |
| EXPECT_TRUE(test_delegate.socket()->GetDnsAliases().empty()); |
| |
| // Although the underlying proxy connection may use TLS or negotiate ALPN, the |
| // tunnel itself is a TCP connection to the origin and should not report these |
| // values. |
| SSLInfo ssl_info; |
| EXPECT_FALSE(test_delegate.socket()->GetSSLInfo(&ssl_info)); |
| EXPECT_FALSE(test_delegate.socket()->WasAlpnNegotiated()); |
| EXPECT_EQ(test_delegate.socket()->GetNegotiatedProtocol(), |
| NextProto::kProtoUnknown); |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, ProxyDelegateExtraHeaders) { |
| InitProxyDelegate(); |
| |
| ProxyServer proxy_server( |
| GetParam() == HTTP ? ProxyServer::SCHEME_HTTP : ProxyServer::SCHEME_HTTPS, |
| HostPortPair(GetParam() == HTTP ? kHttpProxyHost : kHttpsProxyHost, |
| GetParam() == HTTP ? 80 : 443)); |
| std::string proxy_server_uri = ProxyServerToProxyUri(proxy_server); |
| |
| std::string http1_request = |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Foo: " + |
| proxy_server_uri + "\r\n\r\n"; |
| MockWrite writes[] = { |
| MockWrite(ASYNC, 0, http1_request.c_str()), |
| }; |
| |
| const char kResponseHeaderName[] = "foo"; |
| const char kResponseHeaderValue[] = "Response"; |
| std::string http1_response = base::StringPrintf( |
| "HTTP/1.1 200 Connection Established\r\n" |
| "%s: %s\r\n\r\n", |
| kResponseHeaderName, kResponseHeaderValue); |
| MockRead reads[] = { |
| MockRead(ASYNC, 1, http1_response.c_str()), |
| }; |
| |
| const char* const kExtraRequestHeaders[] = { |
| "foo", |
| proxy_server_uri.c_str(), |
| }; |
| const char* const kExtraResponseHeaders[] = { |
| kResponseHeaderName, |
| kResponseHeaderValue, |
| }; |
| spdy::SpdySerializedFrame req(spdy_util_.ConstructSpdyConnect( |
| kExtraRequestHeaders, std::size(kExtraRequestHeaders) / 2, 1, |
| HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| MockWrite spdy_writes[] = {CreateMockWrite(req, 0)}; |
| spdy::SpdySerializedFrame resp(spdy_util_.ConstructSpdyGetReply( |
| kExtraResponseHeaders, std::size(kExtraResponseHeaders) / 2, 1)); |
| MockRead spdy_reads[] = { |
| CreateMockRead(resp, 1, ASYNC), |
| MockRead(SYNCHRONOUS, ERR_IO_PENDING, 2), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, ASYNC); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| test_delegate.StartJobExpectingResult(connect_job.get(), OK, |
| false /* expect_sync_result */); |
| proxy_delegate_->VerifyOnTunnelHeadersReceived( |
| proxy_server, kResponseHeaderName, kResponseHeaderValue); |
| } |
| |
| // Test the case where auth credentials are not cached. |
| TEST_P(HttpProxyConnectJobTest, NeedAuth) { |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| MockWrite(io_mode, 5, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| // No credentials. |
| MockRead(io_mode, 1, "HTTP/1.1 407 Proxy Authentication Required\r\n"), |
| MockRead(io_mode, 2, |
| "Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n"), |
| MockRead(io_mode, 3, "Content-Length: 10\r\n\r\n"), |
| MockRead(io_mode, 4, "0123456789"), |
| MockRead(io_mode, 6, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| |
| SpdyTestUtil spdy_util; |
| spdy::SpdySerializedFrame connect(spdy_util.ConstructSpdyConnect( |
| nullptr, 0, 1, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| spdy::SpdySerializedFrame rst( |
| spdy_util.ConstructSpdyRstStream(1, spdy::ERROR_CODE_CANCEL)); |
| spdy_util.UpdateWithStreamDestruction(1); |
| |
| // After calling trans.RestartWithAuth(), this is the request we should |
| // be issuing -- the final header line contains the credentials. |
| const char* const kSpdyAuthCredentials[] = { |
| "proxy-authorization", |
| "Basic Zm9vOmJhcg==", |
| }; |
| spdy::SpdySerializedFrame connect2(spdy_util.ConstructSpdyConnect( |
| kSpdyAuthCredentials, std::size(kSpdyAuthCredentials) / 2, 3, |
| HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| |
| MockWrite spdy_writes[] = { |
| CreateMockWrite(connect, 0, io_mode), |
| CreateMockWrite(rst, 2, io_mode), |
| CreateMockWrite(connect2, 3, io_mode), |
| }; |
| |
| // The proxy responds to the connect with a 407, using a persistent |
| // connection. |
| const char kAuthStatus[] = "407"; |
| const char* const kAuthChallenge[] = { |
| "proxy-authenticate", |
| "Basic realm=\"MyRealm1\"", |
| }; |
| spdy::SpdySerializedFrame connect_auth_resp( |
| spdy_util.ConstructSpdyReplyError(kAuthStatus, kAuthChallenge, |
| std::size(kAuthChallenge) / 2, 1)); |
| |
| spdy::SpdySerializedFrame connect2_resp( |
| spdy_util.ConstructSpdyGetReply(nullptr, 0, 3)); |
| MockRead spdy_reads[] = { |
| CreateMockRead(connect_auth_resp, 1, ASYNC), |
| CreateMockRead(connect2_resp, 4, ASYNC), |
| MockRead(ASYNC, OK, 5), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| ASSERT_EQ(ERR_IO_PENDING, connect_job->Connect()); |
| // Auth callback is always invoked asynchronously when a challenge is |
| // observed. |
| EXPECT_EQ(0, test_delegate.num_auth_challenges()); |
| |
| test_delegate.WaitForAuthChallenge(1); |
| ASSERT_TRUE(test_delegate.auth_response_info().headers); |
| EXPECT_EQ(407, test_delegate.auth_response_info().headers->response_code()); |
| std::string proxy_authenticate; |
| ASSERT_TRUE(test_delegate.auth_response_info().headers->EnumerateHeader( |
| nullptr, "Proxy-Authenticate", &proxy_authenticate)); |
| EXPECT_EQ(proxy_authenticate, "Basic realm=\"MyRealm1\""); |
| ASSERT_TRUE(test_delegate.auth_controller()); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| test_delegate.auth_controller()->ResetAuth(AuthCredentials(u"foo", u"bar")); |
| test_delegate.RunAuthCallback(); |
| // Per API contract, the request can not complete synchronously. |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| EXPECT_EQ(net::OK, test_delegate.WaitForResult()); |
| EXPECT_EQ(1, test_delegate.num_auth_challenges()); |
| |
| // Close the H2 session to prevent reuse. |
| if (GetParam() == SPDY) |
| session_->CloseAllConnections(ERR_FAILED, "Very good reason"); |
| // Also need to clear the auth cache before re-running the test. |
| session_->http_auth_cache()->ClearAllEntries(); |
| } |
| } |
| |
| // Test the case where auth credentials are not cached and the first time |
| // credentials are sent, they are rejected. |
| TEST_P(HttpProxyConnectJobTest, NeedAuthTwice) { |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| MockWrite(io_mode, 2, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), |
| MockWrite(io_mode, 4, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| // No credentials. |
| MockRead(io_mode, 1, |
| "HTTP/1.1 407 Proxy Authentication Required\r\n" |
| "Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n" |
| "Content-Length: 0\r\n\r\n"), |
| MockRead(io_mode, 3, |
| "HTTP/1.1 407 Proxy Authentication Required\r\n" |
| "Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n" |
| "Content-Length: 0\r\n\r\n"), |
| MockRead(io_mode, 5, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| |
| SpdyTestUtil spdy_util; |
| spdy::SpdySerializedFrame connect(spdy_util.ConstructSpdyConnect( |
| nullptr, 0, 1, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| spdy::SpdySerializedFrame rst( |
| spdy_util.ConstructSpdyRstStream(1, spdy::ERROR_CODE_CANCEL)); |
| spdy_util.UpdateWithStreamDestruction(1); |
| |
| // After calling trans.RestartWithAuth(), this is the request we should |
| // be issuing -- the final header line contains the credentials. |
| const char* const kSpdyAuthCredentials[] = { |
| "proxy-authorization", |
| "Basic Zm9vOmJhcg==", |
| }; |
| spdy::SpdySerializedFrame connect2(spdy_util.ConstructSpdyConnect( |
| kSpdyAuthCredentials, std::size(kSpdyAuthCredentials) / 2, 3, |
| HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| spdy::SpdySerializedFrame rst2( |
| spdy_util.ConstructSpdyRstStream(3, spdy::ERROR_CODE_CANCEL)); |
| spdy_util.UpdateWithStreamDestruction(3); |
| |
| spdy::SpdySerializedFrame connect3(spdy_util.ConstructSpdyConnect( |
| kSpdyAuthCredentials, std::size(kSpdyAuthCredentials) / 2, 5, |
| HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| MockWrite spdy_writes[] = { |
| CreateMockWrite(connect, 0, io_mode), |
| CreateMockWrite(rst, 2, io_mode), |
| CreateMockWrite(connect2, 3, io_mode), |
| CreateMockWrite(rst2, 5, io_mode), |
| CreateMockWrite(connect3, 6, io_mode), |
| }; |
| |
| // The proxy responds to the connect with a 407, using a persistent |
| // connection. |
| const char kAuthStatus[] = "407"; |
| const char* const kAuthChallenge[] = { |
| "proxy-authenticate", |
| "Basic realm=\"MyRealm1\"", |
| }; |
| spdy::SpdySerializedFrame connect_auth_resp( |
| spdy_util.ConstructSpdyReplyError(kAuthStatus, kAuthChallenge, |
| std::size(kAuthChallenge) / 2, 1)); |
| spdy::SpdySerializedFrame connect2_auth_resp( |
| spdy_util.ConstructSpdyReplyError(kAuthStatus, kAuthChallenge, |
| std::size(kAuthChallenge) / 2, 3)); |
| spdy::SpdySerializedFrame connect3_resp( |
| spdy_util.ConstructSpdyGetReply(nullptr, 0, 5)); |
| MockRead spdy_reads[] = { |
| CreateMockRead(connect_auth_resp, 1, ASYNC), |
| CreateMockRead(connect2_auth_resp, 4, ASYNC), |
| CreateMockRead(connect3_resp, 7, ASYNC), |
| MockRead(ASYNC, OK, 8), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| ASSERT_EQ(ERR_IO_PENDING, connect_job->Connect()); |
| // Auth callback is always invoked asynchronously when a challenge is |
| // observed. |
| EXPECT_EQ(0, test_delegate.num_auth_challenges()); |
| |
| test_delegate.WaitForAuthChallenge(1); |
| ASSERT_TRUE(test_delegate.auth_response_info().headers); |
| EXPECT_EQ(407, test_delegate.auth_response_info().headers->response_code()); |
| std::string proxy_authenticate; |
| ASSERT_TRUE(test_delegate.auth_response_info().headers->EnumerateHeader( |
| nullptr, "Proxy-Authenticate", &proxy_authenticate)); |
| EXPECT_EQ(proxy_authenticate, "Basic realm=\"MyRealm1\""); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| test_delegate.auth_controller()->ResetAuth(AuthCredentials(u"foo", u"bar")); |
| test_delegate.RunAuthCallback(); |
| // Per API contract, the auth callback can't be invoked synchronously. |
| EXPECT_FALSE(test_delegate.auth_controller()); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| test_delegate.WaitForAuthChallenge(2); |
| ASSERT_TRUE(test_delegate.auth_response_info().headers); |
| EXPECT_EQ(407, test_delegate.auth_response_info().headers->response_code()); |
| ASSERT_TRUE(test_delegate.auth_response_info().headers->EnumerateHeader( |
| nullptr, "Proxy-Authenticate", &proxy_authenticate)); |
| EXPECT_EQ(proxy_authenticate, "Basic realm=\"MyRealm1\""); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| test_delegate.auth_controller()->ResetAuth(AuthCredentials(u"foo", u"bar")); |
| test_delegate.RunAuthCallback(); |
| // Per API contract, the request can't complete synchronously. |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| EXPECT_EQ(net::OK, test_delegate.WaitForResult()); |
| EXPECT_EQ(2, test_delegate.num_auth_challenges()); |
| |
| // Close the H2 session to prevent reuse. |
| if (GetParam() == SPDY) |
| session_->CloseAllConnections(ERR_FAILED, "Very good reason"); |
| // Also need to clear the auth cache before re-running the test. |
| session_->http_auth_cache()->ClearAllEntries(); |
| } |
| } |
| |
| // Test the case where auth credentials are cached. |
| TEST_P(HttpProxyConnectJobTest, HaveAuth) { |
| // Prepopulate auth cache. |
| const std::u16string kFoo(u"foo"); |
| const std::u16string kBar(u"bar"); |
| url::SchemeHostPort proxy_scheme_host_port( |
| GetParam() == HTTP ? GURL(std::string("http://") + kHttpProxyHost) |
| : GURL(std::string("https://") + kHttpsProxyHost)); |
| session_->http_auth_cache()->Add( |
| proxy_scheme_host_port, HttpAuth::AUTH_PROXY, "MyRealm1", |
| HttpAuth::AUTH_SCHEME_BASIC, NetworkAnonymizationKey(), |
| "Basic realm=MyRealm1", AuthCredentials(kFoo, kBar), "/"); |
| |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| MockRead(io_mode, 1, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| |
| const char* const kSpdyAuthCredentials[] = { |
| "proxy-authorization", |
| "Basic Zm9vOmJhcg==", |
| }; |
| SpdyTestUtil spdy_util; |
| spdy::SpdySerializedFrame connect(spdy_util.ConstructSpdyConnect( |
| kSpdyAuthCredentials, std::size(kSpdyAuthCredentials) / 2, 1, |
| HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| |
| MockWrite spdy_writes[] = { |
| CreateMockWrite(connect, 0, ASYNC), |
| }; |
| |
| spdy::SpdySerializedFrame connect_resp( |
| spdy_util.ConstructSpdyGetReply(nullptr, 0, 1)); |
| MockRead spdy_reads[] = { |
| // SpdySession starts trying to read from the socket as soon as it's |
| // created, so this cannot be SYNCHRONOUS. |
| CreateMockRead(connect_resp, 1, ASYNC), |
| MockRead(SYNCHRONOUS, ERR_IO_PENDING, 2), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| // SPDY operations always complete asynchronously. |
| test_delegate.StartJobExpectingResult( |
| connect_job.get(), OK, io_mode == SYNCHRONOUS && GetParam() != SPDY); |
| |
| // Close the H2 session to prevent reuse. |
| if (GetParam() == SPDY) |
| session_->CloseAllConnections(ERR_FAILED, "Very good reason"); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, HostResolutionFailure) { |
| session_deps_.host_resolver->rules()->AddSimulatedTimeoutFailure( |
| kHttpProxyHost); |
| session_deps_.host_resolver->rules()->AddSimulatedTimeoutFailure( |
| kHttpsProxyHost); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForHttpRequest(&test_delegate, DEFAULT_PRIORITY); |
| test_delegate.StartJobExpectingResult(connect_job.get(), |
| ERR_PROXY_CONNECTION_FAILED, |
| false /* expect_sync_result */); |
| EXPECT_THAT(connect_job->GetResolveErrorInfo().error, |
| test::IsError(ERR_DNS_TIMED_OUT)); |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, RequestPriority) { |
| // Make request hang during host resolution, so can observe priority there. |
| session_deps_.host_resolver->set_ondemand_mode(true); |
| |
| for (int initial_priority = MINIMUM_PRIORITY; |
| initial_priority <= MAXIMUM_PRIORITY; ++initial_priority) { |
| SCOPED_TRACE(initial_priority); |
| for (int new_priority = MINIMUM_PRIORITY; new_priority <= MAXIMUM_PRIORITY; |
| ++new_priority) { |
| SCOPED_TRACE(new_priority); |
| if (initial_priority == new_priority) |
| continue; |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = CreateConnectJobForHttpRequest( |
| &test_delegate, static_cast<RequestPriority>(initial_priority)); |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| MockHostResolverBase* host_resolver = session_deps_.host_resolver.get(); |
| size_t request_id = host_resolver->last_id(); |
| EXPECT_EQ(initial_priority, host_resolver->request_priority(request_id)); |
| |
| connect_job->ChangePriority(static_cast<RequestPriority>(new_priority)); |
| EXPECT_EQ(new_priority, host_resolver->request_priority(request_id)); |
| |
| connect_job->ChangePriority( |
| static_cast<RequestPriority>(initial_priority)); |
| EXPECT_EQ(initial_priority, host_resolver->request_priority(request_id)); |
| } |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, SecureDnsPolicy) { |
| for (auto secure_dns_policy : |
| {SecureDnsPolicy::kAllow, SecureDnsPolicy::kDisable}) { |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = CreateConnectJobForHttpRequest( |
| &test_delegate, DEFAULT_PRIORITY, secure_dns_policy); |
| |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_EQ(secure_dns_policy, |
| session_deps_.host_resolver->last_secure_dns_policy()); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, SpdySessionKeyDisableSecureDns) { |
| if (GetParam() != SPDY) |
| return; |
| |
| SSLSocketDataProvider ssl_data(ASYNC, OK); |
| InitializeSpdySsl(&ssl_data); |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| |
| // SPDY proxy CONNECT request / response, with a pause during the read. |
| spdy::SpdySerializedFrame req(spdy_util_.ConstructSpdyConnect( |
| nullptr, 0, 1, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| MockWrite spdy_writes[] = {CreateMockWrite(req, 0)}; |
| spdy::SpdySerializedFrame resp( |
| spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1)); |
| MockRead spdy_reads[] = {CreateMockRead(resp, 1), MockRead(ASYNC, 0, 2)}; |
| SequencedSocketData spdy_data(spdy_reads, spdy_writes); |
| spdy_data.set_connect_data(MockConnect(ASYNC, OK)); |
| SequencedSocketData* sequenced_data = &spdy_data; |
| session_deps_.socket_factory->AddSocketDataProvider(sequenced_data); |
| |
| TestConnectJobDelegate test_delegate; |
| auto ssl_params = base::MakeRefCounted<SSLSocketParams>( |
| base::MakeRefCounted<TransportSocketParams>( |
| HostPortPair(kHttpsProxyHost, 443), NetworkAnonymizationKey(), |
| SecureDnsPolicy::kDisable, OnHostResolutionCallback(), |
| /*supported_alpns=*/base::flat_set<std::string>()), |
| nullptr, nullptr, HostPortPair(kHttpsProxyHost, 443), SSLConfig(), |
| PRIVACY_MODE_DISABLED, NetworkAnonymizationKey()); |
| auto http_proxy_params = base::MakeRefCounted<HttpProxySocketParams>( |
| nullptr /* tcp_params */, std::move(ssl_params), false /* is_quic */, |
| HostPortPair(kEndpointHost, 443), |
| /*tunnel=*/true, TRAFFIC_ANNOTATION_FOR_TESTS, NetworkAnonymizationKey()); |
| |
| std::unique_ptr<ConnectJob> connect_job = CreateConnectJob( |
| std::move(http_proxy_params), &test_delegate, DEFAULT_PRIORITY); |
| |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); |
| EXPECT_TRUE( |
| common_connect_job_params_->spdy_session_pool->FindAvailableSession( |
| SpdySessionKey(HostPortPair(kHttpsProxyHost, 443), |
| ProxyServer::Direct(), PRIVACY_MODE_DISABLED, |
| SpdySessionKey::IsProxySession::kTrue, SocketTag(), |
| NetworkAnonymizationKey(), SecureDnsPolicy::kDisable), |
| /* enable_ip_based_pooling = */ false, |
| /* is_websocket = */ false, NetLogWithSource())); |
| EXPECT_FALSE( |
| common_connect_job_params_->spdy_session_pool->FindAvailableSession( |
| SpdySessionKey(HostPortPair(kHttpsProxyHost, 443), |
| ProxyServer::Direct(), PRIVACY_MODE_DISABLED, |
| SpdySessionKey::IsProxySession::kTrue, SocketTag(), |
| NetworkAnonymizationKey(), SecureDnsPolicy::kAllow), |
| /* enable_ip_based_pooling = */ false, |
| /* is_websocket = */ false, NetLogWithSource())); |
| } |
| |
| // Make sure that HttpProxyConnectJob does not pass on its priority to its |
| // SPDY session's socket request on Init, or on SetPriority. |
| TEST_P(HttpProxyConnectJobTest, SetSpdySessionSocketRequestPriority) { |
| if (GetParam() != SPDY) |
| return; |
| session_deps_.host_resolver->set_synchronous_mode(true); |
| |
| // The SPDY CONNECT request should have a priority of kH2QuicTunnelPriority, |
| // even though the ConnectJob's priority is set to HIGHEST after connection |
| // establishment. |
| spdy::SpdySerializedFrame req(spdy_util_.ConstructSpdyConnect( |
| nullptr /* extra_headers */, 0 /* extra_header_count */, |
| 1 /* stream_id */, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| MockWrite spdy_writes[] = {CreateMockWrite(req, 0, ASYNC)}; |
| spdy::SpdySerializedFrame resp( |
| spdy_util_.ConstructSpdyGetReply(nullptr, 0, 1)); |
| MockRead spdy_reads[] = {CreateMockRead(resp, 1, ASYNC), |
| MockRead(ASYNC, 0, 2)}; |
| |
| Initialize(base::span<MockRead>(), base::span<MockWrite>(), spdy_reads, |
| spdy_writes, SYNCHRONOUS); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate, IDLE); |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| connect_job->ChangePriority(HIGHEST); |
| |
| // Wait for tunnel to be established. If the frame has a MEDIUM priority |
| // instead of highest, the written data will not match what is expected, and |
| // the test will fail. |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, TCPError) { |
| // SPDY and HTTPS are identical, as they only differ once a connection is |
| // established. |
| if (GetParam() == SPDY) |
| return; |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| base::HistogramTester histogram_tester; |
| |
| SequencedSocketData data; |
| data.set_connect_data(MockConnect(io_mode, ERR_CONNECTION_CLOSED)); |
| session_deps_.socket_factory->AddSocketDataProvider(&data); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForHttpRequest(&test_delegate); |
| test_delegate.StartJobExpectingResult( |
| connect_job.get(), ERR_PROXY_CONNECTION_FAILED, io_mode == SYNCHRONOUS); |
| |
| bool is_secure_proxy = GetParam() == HTTPS; |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Insecure.Error", is_secure_proxy ? 0 : 1); |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Secure.Error", is_secure_proxy ? 1 : 0); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, SSLError) { |
| if (GetParam() == HTTP) |
| return; |
| |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| base::HistogramTester histogram_tester; |
| |
| SequencedSocketData data; |
| data.set_connect_data(MockConnect(io_mode, OK)); |
| session_deps_.socket_factory->AddSocketDataProvider(&data); |
| |
| SSLSocketDataProvider ssl_data(io_mode, ERR_CERT_AUTHORITY_INVALID); |
| if (GetParam() == SPDY) { |
| InitializeSpdySsl(&ssl_data); |
| } |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| test_delegate.StartJobExpectingResult(connect_job.get(), |
| ERR_PROXY_CERTIFICATE_INVALID, |
| io_mode == SYNCHRONOUS); |
| |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Secure.Error", 1); |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Insecure.Error", 0); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, TunnelUnexpectedClose) { |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| MockRead(io_mode, 1, "HTTP/1.1 200 Conn"), |
| MockRead(io_mode, ERR_CONNECTION_CLOSED, 2), |
| }; |
| spdy::SpdySerializedFrame req(SpdyTestUtil().ConstructSpdyConnect( |
| nullptr /*extra_headers */, 0 /*extra_header_count */, |
| 1 /* stream_id */, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| MockWrite spdy_writes[] = {CreateMockWrite(req, 0, io_mode)}; |
| // Sync reads don't really work with SPDY, since it constantly reads from |
| // the socket. |
| MockRead spdy_reads[] = { |
| MockRead(ASYNC, ERR_CONNECTION_CLOSED, 1), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| |
| if (GetParam() == SPDY) { |
| // SPDY cannot process a headers block unless it's complete and so it |
| // returns ERR_CONNECTION_CLOSED in this case. SPDY also doesn't return |
| // this failure synchronously. |
| test_delegate.StartJobExpectingResult(connect_job.get(), |
| ERR_CONNECTION_CLOSED, |
| false /* expect_sync_result */); |
| } else { |
| test_delegate.StartJobExpectingResult(connect_job.get(), |
| ERR_RESPONSE_HEADERS_TRUNCATED, |
| io_mode == SYNCHRONOUS); |
| } |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, Tunnel1xxResponse) { |
| // Tests that 1xx responses are rejected for a CONNECT request. |
| if (GetParam() == SPDY) { |
| // SPDY doesn't have 1xx responses. |
| return; |
| } |
| |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| MockRead(io_mode, 1, "HTTP/1.1 100 Continue\r\n\r\n"), |
| MockRead(io_mode, 2, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| |
| Initialize(reads, writes, base::span<MockRead>(), base::span<MockWrite>(), |
| io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| test_delegate.StartJobExpectingResult(connect_job.get(), |
| ERR_TUNNEL_CONNECTION_FAILED, |
| io_mode == SYNCHRONOUS); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, TunnelSetupError) { |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| MockRead(io_mode, 1, "HTTP/1.1 304 Not Modified\r\n\r\n"), |
| }; |
| SpdyTestUtil spdy_util; |
| spdy::SpdySerializedFrame req(spdy_util.ConstructSpdyConnect( |
| nullptr /* extra_headers */, 0 /* extra_header_count */, |
| 1 /* stream_id */, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair("www.endpoint.test", 443))); |
| spdy::SpdySerializedFrame rst( |
| spdy_util.ConstructSpdyRstStream(1, spdy::ERROR_CODE_CANCEL)); |
| MockWrite spdy_writes[] = { |
| CreateMockWrite(req, 0, io_mode), |
| CreateMockWrite(rst, 2, io_mode), |
| }; |
| spdy::SpdySerializedFrame resp(spdy_util.ConstructSpdyReplyError(1)); |
| // Sync reads don't really work with SPDY, since it constantly reads from |
| // the socket. |
| MockRead spdy_reads[] = { |
| CreateMockRead(resp, 1, ASYNC), |
| MockRead(ASYNC, OK, 3), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, io_mode); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate, LOW); |
| test_delegate.StartJobExpectingResult( |
| connect_job.get(), ERR_TUNNEL_CONNECTION_FAILED, |
| io_mode == SYNCHRONOUS && GetParam() != SPDY); |
| // Need to close the session to prevent reuse in the next loop iteration. |
| session_->spdy_session_pool()->CloseAllSessions(); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, SslClientAuth) { |
| if (GetParam() == HTTP) |
| return; |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| base::HistogramTester histogram_tester; |
| |
| SequencedSocketData socket_data(MockConnect(io_mode, OK), |
| base::span<const MockRead>(), |
| base::span<const MockWrite>()); |
| session_deps_.socket_factory->AddSocketDataProvider(&socket_data); |
| SSLSocketDataProvider ssl_data(io_mode, ERR_SSL_CLIENT_AUTH_CERT_NEEDED); |
| if (GetParam() == SPDY) |
| InitializeSpdySsl(&ssl_data); |
| session_deps_.socket_factory->AddSSLSocketDataProvider(&ssl_data); |
| |
| // Redirects in the HTTPS case return errors, but also return sockets. |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| test_delegate.StartJobExpectingResult(connect_job.get(), |
| ERR_SSL_CLIENT_AUTH_CERT_NEEDED, |
| io_mode == SYNCHRONOUS); |
| |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Secure.Error", 1); |
| histogram_tester.ExpectTotalCount( |
| "Net.HttpProxy.ConnectLatency.Insecure.Error", 0); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, TunnelSetupRedirect) { |
| const std::string kRedirectTarget = "https://foo.google.com/"; |
| |
| for (IoMode io_mode : {SYNCHRONOUS, ASYNC}) { |
| SCOPED_TRACE(io_mode); |
| session_deps_.host_resolver->set_synchronous_mode(io_mode == SYNCHRONOUS); |
| |
| const std::string kResponseText = |
| "HTTP/1.1 302 Found\r\n" |
| "Location: " + |
| kRedirectTarget + |
| "\r\n" |
| "Set-Cookie: foo=bar\r\n" |
| "\r\n"; |
| |
| MockWrite writes[] = { |
| MockWrite(io_mode, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| MockRead(io_mode, 1, kResponseText.c_str()), |
| }; |
| SpdyTestUtil spdy_util; |
| spdy::SpdySerializedFrame req(spdy_util.ConstructSpdyConnect( |
| nullptr /* extra_headers */, 0 /* extra_header_count */, 1, |
| DEFAULT_PRIORITY, HostPortPair(kEndpointHost, 443))); |
| spdy::SpdySerializedFrame rst( |
| spdy_util.ConstructSpdyRstStream(1, spdy::ERROR_CODE_CANCEL)); |
| |
| MockWrite spdy_writes[] = { |
| CreateMockWrite(req, 0, io_mode), |
| CreateMockWrite(rst, 3, io_mode), |
| }; |
| |
| const char* const responseHeaders[] = { |
| "location", |
| kRedirectTarget.c_str(), |
| "set-cookie", |
| "foo=bar", |
| }; |
| const int responseHeadersSize = std::size(responseHeaders) / 2; |
| spdy::SpdySerializedFrame resp(spdy_util.ConstructSpdyReplyError( |
| "302", responseHeaders, responseHeadersSize, 1)); |
| MockRead spdy_reads[] = { |
| CreateMockRead(resp, 1, ASYNC), |
| MockRead(ASYNC, 0, 2), |
| }; |
| |
| Initialize(reads, writes, spdy_reads, spdy_writes, io_mode); |
| |
| // Redirects during CONNECT returns an error. |
| TestConnectJobDelegate test_delegate( |
| TestConnectJobDelegate::SocketExpected::ON_SUCCESS_ONLY); |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| |
| // H2 never completes synchronously. |
| bool expect_sync_result = (io_mode == SYNCHRONOUS && GetParam() != SPDY); |
| |
| // We don't trust 302 responses to CONNECT from proxies. |
| test_delegate.StartJobExpectingResult( |
| connect_job.get(), ERR_TUNNEL_CONNECTION_FAILED, expect_sync_result); |
| EXPECT_FALSE(test_delegate.socket()); |
| |
| // Need to close the session to prevent reuse in the next loop iteration. |
| session_->spdy_session_pool()->CloseAllSessions(); |
| } |
| } |
| |
| // Test timeouts in the case of an auth challenge and response. |
| TEST_P(HttpProxyConnectJobTest, TestTimeoutsAuthChallenge) { |
| // Wait until this amount of time before something times out. |
| const base::TimeDelta kTinyTime = base::Microseconds(1); |
| |
| enum class TimeoutPhase { |
| CONNECT, |
| PROXY_HANDSHAKE, |
| SECOND_PROXY_HANDSHAKE, |
| |
| NONE, |
| }; |
| |
| const TimeoutPhase kTimeoutPhases[] = { |
| TimeoutPhase::CONNECT, |
| TimeoutPhase::PROXY_HANDSHAKE, |
| TimeoutPhase::SECOND_PROXY_HANDSHAKE, |
| TimeoutPhase::NONE, |
| }; |
| |
| session_deps_.host_resolver->set_ondemand_mode(true); |
| |
| MockWrite writes[] = { |
| MockWrite(ASYNC, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| MockWrite(ASYNC, 3, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| // Pause before first response is read. |
| MockRead(ASYNC, ERR_IO_PENDING, 1), |
| MockRead(ASYNC, 2, |
| "HTTP/1.1 407 Proxy Authentication Required\r\n" |
| "Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n" |
| "Content-Length: 0\r\n\r\n"), |
| |
| // Pause again before second response is read. |
| MockRead(ASYNC, ERR_IO_PENDING, 4), |
| MockRead(ASYNC, 5, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| |
| SpdyTestUtil spdy_util; |
| spdy::SpdySerializedFrame connect(spdy_util.ConstructSpdyConnect( |
| nullptr, 0, 1, HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| spdy::SpdySerializedFrame rst( |
| spdy_util.ConstructSpdyRstStream(1, spdy::ERROR_CODE_CANCEL)); |
| spdy_util.UpdateWithStreamDestruction(1); |
| |
| // After calling trans.RestartWithAuth(), this is the request we should |
| // be issuing -- the final header line contains the credentials. |
| const char* const kSpdyAuthCredentials[] = { |
| "proxy-authorization", |
| "Basic Zm9vOmJhcg==", |
| }; |
| spdy::SpdySerializedFrame connect2(spdy_util.ConstructSpdyConnect( |
| kSpdyAuthCredentials, std::size(kSpdyAuthCredentials) / 2, 3, |
| HttpProxyConnectJob::kH2QuicTunnelPriority, |
| HostPortPair(kEndpointHost, 443))); |
| // This may be sent in some tests, either when tearing down a successful |
| // connection, or on timeout. |
| spdy::SpdySerializedFrame rst2( |
| spdy_util.ConstructSpdyRstStream(3, spdy::ERROR_CODE_CANCEL)); |
| MockWrite spdy_writes[] = { |
| CreateMockWrite(connect, 0, ASYNC), |
| CreateMockWrite(rst, 3, ASYNC), |
| CreateMockWrite(connect2, 4, ASYNC), |
| CreateMockWrite(rst2, 8, ASYNC), |
| }; |
| |
| // The proxy responds to the connect with a 407, using a persistent |
| // connection. |
| const char kAuthStatus[] = "407"; |
| const char* const kAuthChallenge[] = { |
| "proxy-authenticate", |
| "Basic realm=\"MyRealm1\"", |
| }; |
| spdy::SpdySerializedFrame connect_auth_resp(spdy_util.ConstructSpdyReplyError( |
| kAuthStatus, kAuthChallenge, std::size(kAuthChallenge) / 2, 1)); |
| spdy::SpdySerializedFrame connect2_resp( |
| spdy_util.ConstructSpdyGetReply(nullptr, 0, 3)); |
| MockRead spdy_reads[] = { |
| // Pause before first response is read. |
| MockRead(ASYNC, ERR_IO_PENDING, 1), |
| CreateMockRead(connect_auth_resp, 2, ASYNC), |
| // Pause again before second response is read. |
| MockRead(ASYNC, ERR_IO_PENDING, 5), |
| CreateMockRead(connect2_resp, 6, ASYNC), |
| MockRead(ASYNC, OK, 7), |
| }; |
| |
| for (TimeoutPhase timeout_phase : kTimeoutPhases) { |
| SCOPED_TRACE(static_cast<int>(timeout_phase)); |
| |
| // Need to close the session to prevent reuse of a session from the last |
| // loop iteration. |
| session_->spdy_session_pool()->CloseAllSessions(); |
| // And clear the auth cache to prevent reusing cache entries. |
| session_->http_auth_cache()->ClearAllEntries(); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| |
| // Connecting should run until the request hits the HostResolver. |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_TRUE(session_deps_.host_resolver->has_pending_requests()); |
| EXPECT_EQ(LOAD_STATE_RESOLVING_HOST, connect_job->GetLoadState()); |
| |
| // Run until just before timeout. |
| FastForwardBy(GetNestedConnectionTimeout() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // Wait until timeout, if appropriate. |
| if (timeout_phase == TimeoutPhase::CONNECT) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| // Add mock reads for socket needed in next step. Connect phase is timed out |
| // before establishing a connection, so don't need them for |
| // TimeoutPhase::CONNECT. |
| Initialize(reads, writes, spdy_reads, spdy_writes, SYNCHRONOUS); |
| |
| // Finish resolution. |
| session_deps_.host_resolver->ResolveOnlyRequestNow(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_ESTABLISHING_PROXY_TUNNEL, |
| connect_job->GetLoadState()); |
| |
| // Wait until just before negotiation with the tunnel should time out. |
| FastForwardBy(HttpProxyConnectJob::TunnelTimeoutForTesting() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| if (timeout_phase == TimeoutPhase::PROXY_HANDSHAKE) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| data_->Resume(); |
| test_delegate.WaitForAuthChallenge(1); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // ConnectJobs cannot timeout while showing an auth dialog. |
| FastForwardBy(base::Days(1)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // Send credentials |
| test_delegate.auth_controller()->ResetAuth(AuthCredentials(u"foo", u"bar")); |
| test_delegate.RunAuthCallback(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| FastForwardBy(HttpProxyConnectJob::TunnelTimeoutForTesting() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| if (timeout_phase == TimeoutPhase::SECOND_PROXY_HANDSHAKE) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| data_->Resume(); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); |
| } |
| } |
| |
| // Same as above, except test the case the first connection cannot be reused |
| // once credentials are received. |
| TEST_P(HttpProxyConnectJobTest, TestTimeoutsAuthChallengeNewConnection) { |
| // Proxy-Connection: Close doesn't make sense with H2. |
| if (GetParam() == SPDY) |
| return; |
| |
| enum class TimeoutPhase { |
| CONNECT, |
| PROXY_HANDSHAKE, |
| SECOND_CONNECT, |
| SECOND_PROXY_HANDSHAKE, |
| |
| // This has to be last for the H2 proxy case, since success will populate |
| // the H2 session pool. |
| NONE, |
| }; |
| |
| const TimeoutPhase kTimeoutPhases[] = { |
| TimeoutPhase::CONNECT, TimeoutPhase::PROXY_HANDSHAKE, |
| TimeoutPhase::SECOND_CONNECT, TimeoutPhase::SECOND_PROXY_HANDSHAKE, |
| TimeoutPhase::NONE, |
| }; |
| |
| // Wait until this amount of time before something times out. |
| const base::TimeDelta kTinyTime = base::Microseconds(1); |
| |
| session_deps_.host_resolver->set_ondemand_mode(true); |
| |
| MockWrite writes[] = { |
| MockWrite(ASYNC, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n\r\n"), |
| }; |
| MockRead reads[] = { |
| // Pause at read. |
| MockRead(ASYNC, ERR_IO_PENDING, 1), |
| MockRead(ASYNC, 2, |
| "HTTP/1.1 407 Proxy Authentication Required\r\n" |
| "Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n" |
| "Proxy-Connection: Close\r\n" |
| "Content-Length: 0\r\n\r\n"), |
| }; |
| |
| MockWrite writes2[] = { |
| MockWrite(ASYNC, 0, |
| "CONNECT www.endpoint.test:443 HTTP/1.1\r\n" |
| "Host: www.endpoint.test:443\r\n" |
| "Proxy-Connection: keep-alive\r\n" |
| "Proxy-Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), |
| }; |
| MockRead reads2[] = { |
| // Pause at read. |
| MockRead(ASYNC, ERR_IO_PENDING, 1), |
| MockRead(ASYNC, 2, "HTTP/1.1 200 Connection Established\r\n\r\n"), |
| }; |
| |
| for (TimeoutPhase timeout_phase : kTimeoutPhases) { |
| SCOPED_TRACE(static_cast<int>(timeout_phase)); |
| |
| // Need to clear the auth cache to prevent reusing cache entries. |
| session_->http_auth_cache()->ClearAllEntries(); |
| |
| TestConnectJobDelegate test_delegate; |
| std::unique_ptr<ConnectJob> connect_job = |
| CreateConnectJobForTunnel(&test_delegate); |
| |
| // Connecting should run until the request hits the HostResolver. |
| EXPECT_THAT(connect_job->Connect(), test::IsError(ERR_IO_PENDING)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_TRUE(session_deps_.host_resolver->has_pending_requests()); |
| EXPECT_EQ(LOAD_STATE_RESOLVING_HOST, connect_job->GetLoadState()); |
| |
| // Run until just before timeout. |
| FastForwardBy(GetNestedConnectionTimeout() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // Wait until timeout, if appropriate. |
| if (timeout_phase == TimeoutPhase::CONNECT) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| // Add mock reads for socket needed in next step. Connect phase is timed out |
| // before establishing a connection, so don't need them for |
| // TimeoutPhase::CONNECT. |
| Initialize(reads, writes, base::span<MockRead>(), base::span<MockWrite>(), |
| SYNCHRONOUS); |
| |
| // Finish resolution. |
| session_deps_.host_resolver->ResolveOnlyRequestNow(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_ESTABLISHING_PROXY_TUNNEL, |
| connect_job->GetLoadState()); |
| |
| // Wait until just before negotiation with the tunnel should time out. |
| FastForwardBy(HttpProxyConnectJob::TunnelTimeoutForTesting() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| if (timeout_phase == TimeoutPhase::PROXY_HANDSHAKE) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| data_->Resume(); |
| test_delegate.WaitForAuthChallenge(1); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // ConnectJobs cannot timeout while showing an auth dialog. |
| FastForwardBy(base::Days(1)); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // Send credentials |
| test_delegate.auth_controller()->ResetAuth(AuthCredentials(u"foo", u"bar")); |
| test_delegate.RunAuthCallback(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // Since the connection was not reusable, a new connection needs to be |
| // established. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_TRUE(session_deps_.host_resolver->has_pending_requests()); |
| EXPECT_EQ(LOAD_STATE_RESOLVING_HOST, connect_job->GetLoadState()); |
| |
| // Run until just before timeout. |
| FastForwardBy(GetNestedConnectionTimeout() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| // Wait until timeout, if appropriate. |
| if (timeout_phase == TimeoutPhase::SECOND_CONNECT) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| // Add mock reads for socket needed in next step. Connect phase is timed out |
| // before establishing a connection, so don't need them for |
| // TimeoutPhase::SECOND_CONNECT. |
| Initialize(reads2, writes2, base::span<MockRead>(), base::span<MockWrite>(), |
| SYNCHRONOUS); |
| |
| // Finish resolution. |
| session_deps_.host_resolver->ResolveOnlyRequestNow(); |
| EXPECT_FALSE(test_delegate.has_result()); |
| EXPECT_EQ(LOAD_STATE_ESTABLISHING_PROXY_TUNNEL, |
| connect_job->GetLoadState()); |
| |
| // Wait until just before negotiation with the tunnel should time out. |
| FastForwardBy(HttpProxyConnectJob::TunnelTimeoutForTesting() - kTinyTime); |
| EXPECT_FALSE(test_delegate.has_result()); |
| |
| if (timeout_phase == TimeoutPhase::SECOND_PROXY_HANDSHAKE) { |
| FastForwardBy(kTinyTime); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsError(ERR_TIMED_OUT)); |
| continue; |
| } |
| |
| data_->Resume(); |
| ASSERT_TRUE(test_delegate.has_result()); |
| EXPECT_THAT(test_delegate.WaitForResult(), test::IsOk()); |
| } |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, ConnectionTimeoutNoNQE) { |
| // Doesn't actually matter whether or not this is for a tunnel - the |
| // connection timeout is the same, though it probably shouldn't be the same, |
| // since tunnels need an extra round trip. |
| base::TimeDelta alternate_connection_timeout = |
| HttpProxyConnectJob::AlternateNestedConnectionTimeout( |
| *CreateParams(true /* tunnel */, SecureDnsPolicy::kAllow), |
| nullptr /* network_quality_estimator */); |
| |
| #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_IOS) |
| // On Android and iOS, when there's no NQE, there's a hard-coded alternate |
| // proxy timeout. |
| EXPECT_EQ(base::Seconds(10), alternate_connection_timeout); |
| #else |
| // On other platforms, there is not. |
| EXPECT_EQ(base::TimeDelta(), alternate_connection_timeout); |
| #endif |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, ConnectionTimeoutMin) { |
| // Set RTT estimate to a low value. |
| base::TimeDelta rtt_estimate = base::Milliseconds(1); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| |
| EXPECT_LE(base::TimeDelta(), GetNestedConnectionTimeout()); |
| |
| // Test against a large value. |
| EXPECT_GE(base::Minutes(10), GetNestedConnectionTimeout()); |
| |
| EXPECT_EQ(base::Seconds(8), GetNestedConnectionTimeout()); |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, ConnectionTimeoutMax) { |
| // Set RTT estimate to a high value. |
| base::TimeDelta rtt_estimate = base::Seconds(100); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| |
| EXPECT_LE(base::TimeDelta(), GetNestedConnectionTimeout()); |
| |
| // Test against a large value. |
| EXPECT_GE(base::Minutes(10), GetNestedConnectionTimeout()); |
| |
| EXPECT_EQ(base::Seconds(30), GetNestedConnectionTimeout()); |
| } |
| |
| // TODO: b/327008491 - Reenable unittests with unused functionality. |
| // Cobalt does not need FieldTrial yet. |
| #if !defined(STARBOARD) |
| // Tests the connection timeout values when the field trial parameters are |
| // specified. |
| TEST_P(HttpProxyConnectJobTest, ConnectionTimeoutWithExperiment) { |
| // Timeout should be kMultiplier times the HTTP RTT estimate. |
| const int kMultiplier = 4; |
| const base::TimeDelta kMinTimeout = base::Seconds(8); |
| const base::TimeDelta kMaxTimeout = base::Seconds(20); |
| |
| InitAdaptiveTimeoutFieldTrialWithParams(false, kMultiplier, kMultiplier, |
| kMinTimeout, kMaxTimeout); |
| EXPECT_LE(base::TimeDelta(), GetNestedConnectionTimeout()); |
| |
| base::TimeDelta rtt_estimate = base::Seconds(4); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| base::TimeDelta expected_connection_timeout = kMultiplier * rtt_estimate; |
| EXPECT_EQ(expected_connection_timeout, GetNestedConnectionTimeout()); |
| |
| // Connection timeout should not exceed kMaxTimeout. |
| rtt_estimate = base::Seconds(25); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_EQ(kMaxTimeout, GetNestedConnectionTimeout()); |
| |
| // Connection timeout should not be less than kMinTimeout. |
| rtt_estimate = base::Seconds(0); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_EQ(kMinTimeout, GetNestedConnectionTimeout()); |
| } |
| |
| // Tests the connection timeout values when the field trial parameters are |
| // specified. |
| TEST_P(HttpProxyConnectJobTest, ConnectionTimeoutExperimentDifferentParams) { |
| // Timeout should be kMultiplier times the HTTP RTT estimate. |
| const int kMultiplier = 3; |
| const base::TimeDelta kMinTimeout = base::Seconds(2); |
| const base::TimeDelta kMaxTimeout = base::Seconds(30); |
| |
| InitAdaptiveTimeoutFieldTrialWithParams(false, kMultiplier, kMultiplier, |
| kMinTimeout, kMaxTimeout); |
| EXPECT_LE(base::TimeDelta(), GetNestedConnectionTimeout()); |
| |
| base::TimeDelta rtt_estimate = base::Seconds(2); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_EQ(kMultiplier * rtt_estimate, GetNestedConnectionTimeout()); |
| |
| // A change in RTT estimate should also change the connection timeout. |
| rtt_estimate = base::Seconds(7); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_EQ(kMultiplier * rtt_estimate, GetNestedConnectionTimeout()); |
| |
| // Connection timeout should not exceed kMaxTimeout. |
| rtt_estimate = base::Seconds(35); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_EQ(kMaxTimeout, GetNestedConnectionTimeout()); |
| |
| // Connection timeout should not be less than kMinTimeout. |
| rtt_estimate = base::Seconds(0); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_EQ(kMinTimeout, GetNestedConnectionTimeout()); |
| } |
| |
| TEST_P(HttpProxyConnectJobTest, ConnectionTimeoutWithConnectionProperty) { |
| const int kSecureMultiplier = 3; |
| const int kNonSecureMultiplier = 5; |
| const base::TimeDelta kMinTimeout = base::Seconds(2); |
| const base::TimeDelta kMaxTimeout = base::Seconds(30); |
| |
| InitAdaptiveTimeoutFieldTrialWithParams( |
| false, kSecureMultiplier, kNonSecureMultiplier, kMinTimeout, kMaxTimeout); |
| |
| const base::TimeDelta kRttEstimate = base::Seconds(2); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(kRttEstimate); |
| // By default, connection timeout should return the timeout for secure |
| // proxies. |
| if (GetParam() != HTTP) { |
| EXPECT_EQ(kSecureMultiplier * kRttEstimate, GetNestedConnectionTimeout()); |
| } else { |
| EXPECT_EQ(kNonSecureMultiplier * kRttEstimate, |
| GetNestedConnectionTimeout()); |
| } |
| } |
| |
| // Tests the connection timeout values when the field trial parameters are not |
| // specified. |
| TEST_P(HttpProxyConnectJobTest, ProxyPoolTimeoutWithExperimentDefaultParams) { |
| InitAdaptiveTimeoutFieldTrialWithParams(true, 0, 0, base::TimeDelta(), |
| base::TimeDelta()); |
| EXPECT_LE(base::TimeDelta(), GetNestedConnectionTimeout()); |
| |
| // Timeout should be |http_rtt_multiplier| times the HTTP RTT |
| // estimate. |
| base::TimeDelta rtt_estimate = base::Milliseconds(10); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| // Connection timeout should not be less than the HTTP RTT estimate. |
| EXPECT_LE(rtt_estimate, GetNestedConnectionTimeout()); |
| |
| // A change in RTT estimate should also change the connection timeout. |
| rtt_estimate = base::Seconds(10); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| // Connection timeout should not be less than the HTTP RTT estimate. |
| EXPECT_LE(rtt_estimate, GetNestedConnectionTimeout()); |
| |
| // Set RTT to a very large value. |
| rtt_estimate = base::Minutes(60); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_GT(rtt_estimate, GetNestedConnectionTimeout()); |
| |
| // Set RTT to a very small value. |
| rtt_estimate = base::Seconds(0); |
| network_quality_estimator_->SetStartTimeNullHttpRtt(rtt_estimate); |
| EXPECT_LT(rtt_estimate, GetNestedConnectionTimeout()); |
| } |
| #endif |
| |
| } // namespace net |