Close HTTP/1.1 sockets when blocked by CORB or CORP.
BUG=1154250
Change-Id: Iff9b03523b6265cb7e5ccc0bbbc43dd911ac9b9b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2575014
Reviewed-by: Shivani Sharma <[email protected]>
Reviewed-by: Ćukasz Anforowicz <[email protected]>
Commit-Queue: Matt Menke <[email protected]>
Cr-Commit-Position: refs/heads/master@{#834909}
diff --git a/net/http/http_cache_transaction.cc b/net/http/http_cache_transaction.cc
index 3ee8cba..27f9663 100644
--- a/net/http/http_cache_transaction.cc
+++ b/net/http/http_cache_transaction.cc
@@ -615,6 +615,14 @@
network_transaction_info_.old_connection_attempts.end());
}
+void HttpCache::Transaction::CloseConnectionOnDestruction() {
+ if (network_trans_) {
+ network_trans_->CloseConnectionOnDestruction();
+ } else if (InWriters()) {
+ entry_->writers->CloseConnectionOnDestruction();
+ }
+}
+
void HttpCache::Transaction::SetValidatingCannotProceed() {
DCHECK(!reading_);
// Ensure this transaction is waiting for a callback.
diff --git a/net/http/http_cache_transaction.h b/net/http/http_cache_transaction.h
index 1d76d0197..63baba5 100644
--- a/net/http/http_cache_transaction.h
+++ b/net/http/http_cache_transaction.h
@@ -160,6 +160,7 @@
void SetResponseHeadersCallback(ResponseHeadersCallback callback) override;
int ResumeNetworkStart() override;
void GetConnectionAttempts(ConnectionAttempts* out) const override;
+ void CloseConnectionOnDestruction() override;
// Invoked when parallel validation cannot proceed due to response failure
// and this transaction needs to be restarted.
diff --git a/net/http/http_cache_writers.cc b/net/http/http_cache_writers.cc
index 9459509..a80dc43 100644
--- a/net/http/http_cache_writers.cc
+++ b/net/http/http_cache_writers.cc
@@ -225,6 +225,11 @@
}
}
+void HttpCache::Writers::CloseConnectionOnDestruction() {
+ if (network_transaction_)
+ network_transaction_->CloseConnectionOnDestruction();
+}
+
bool HttpCache::Writers::ContainsOnlyIdleWriters() const {
return waiting_for_read_.empty() && !active_transaction_;
}
diff --git a/net/http/http_cache_writers.h b/net/http/http_cache_writers.h
index c638aeb..4da9f1e 100644
--- a/net/http/http_cache_writers.h
+++ b/net/http/http_cache_writers.h
@@ -125,6 +125,8 @@
return network_transaction_.get();
}
+ void CloseConnectionOnDestruction();
+
// Returns the load state of the |network_transaction_| if present else
// returns LOAD_STATE_IDLE.
LoadState GetLoadState() const;
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc
index af1020cc..e526512 100644
--- a/net/http/http_network_transaction.cc
+++ b/net/http/http_network_transaction.cc
@@ -148,7 +148,8 @@
// TODO(mbelshe): The stream_ should be able to compute whether or not the
// stream should be kept alive. No reason to compute here
// and pass it in.
- if (!stream_->CanReuseConnection() || next_state_ != STATE_NONE) {
+ if (!stream_->CanReuseConnection() || next_state_ != STATE_NONE ||
+ close_connection_on_destruction_) {
stream_->Close(true /* not reusable */);
} else if (stream_->IsResponseBodyComplete()) {
// If the response body is complete, we can just reuse the socket.
@@ -542,6 +543,10 @@
return DoLoop(OK);
}
+void HttpNetworkTransaction::CloseConnectionOnDestruction() {
+ close_connection_on_destruction_ = true;
+}
+
void HttpNetworkTransaction::OnStreamReady(const SSLConfig& used_ssl_config,
const ProxyInfo& used_proxy_info,
std::unique_ptr<HttpStream> stream) {
diff --git a/net/http/http_network_transaction.h b/net/http/http_network_transaction.h
index c0fc39f..f95473ea 100644
--- a/net/http/http_network_transaction.h
+++ b/net/http/http_network_transaction.h
@@ -88,8 +88,8 @@
void SetConnectedCallback(const ConnectedCallback& callback) override;
void SetRequestHeadersCallback(RequestHeadersCallback callback) override;
void SetResponseHeadersCallback(ResponseHeadersCallback callback) override;
-
int ResumeNetworkStart() override;
+ void CloseConnectionOnDestruction() override;
// HttpStreamRequest::Delegate methods:
void OnStreamReady(const SSLConfig& used_ssl_config,
@@ -435,6 +435,8 @@
// Number of times the transaction was restarted via a RestartWith* call.
size_t num_restarts_;
+ bool close_connection_on_destruction_ = false;
+
DISALLOW_COPY_AND_ASSIGN(HttpNetworkTransaction);
};
diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc
index a63c0c08..df4fda9 100644
--- a/net/http/http_network_transaction_unittest.cc
+++ b/net/http/http_network_transaction_unittest.cc
@@ -9390,6 +9390,105 @@
EXPECT_EQ(1, GetIdleSocketCountInTransportSocketPool(session.get()));
}
+TEST_F(HttpNetworkTransactionTest, CloseConnectionOnDestruction) {
+ enum class TestCase {
+ kReadHeaders,
+ kReadPartOfBodyRead,
+ kReadAllOfBody,
+ };
+
+ for (auto test_case : {TestCase::kReadHeaders, TestCase::kReadPartOfBodyRead,
+ TestCase::kReadAllOfBody}) {
+ SCOPED_TRACE(testing::Message()
+ << "Test case: " << static_cast<int>(test_case));
+ for (bool close_connection : {false, true}) {
+ if (test_case != TestCase::kReadAllOfBody || close_connection == false)
+ continue;
+ SCOPED_TRACE(testing::Message()
+ << "Close connection: " << close_connection);
+
+ HttpRequestInfo request;
+ request.method = "GET";
+ request.url = GURL("http://foo.test/");
+ request.traffic_annotation =
+ net::MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+
+ std::unique_ptr<HttpNetworkSession> session(
+ CreateSession(&session_deps_));
+
+ std::unique_ptr<HttpNetworkTransaction> trans =
+ std::make_unique<HttpNetworkTransaction>(DEFAULT_PRIORITY,
+ session.get());
+
+ MockRead data_reads[] = {
+ // A part of the response body is received with the response headers.
+ MockRead("HTTP/1.1 200 OK\r\n"
+ "Content-Length: 11\r\n\r\n"
+ "hello world"),
+ MockRead(SYNCHRONOUS, OK),
+ };
+
+ StaticSocketDataProvider data(data_reads, base::span<MockWrite>());
+ session_deps_.socket_factory->AddSocketDataProvider(&data);
+
+ TestCompletionCallback callback;
+
+ int rv = trans->Start(&request, callback.callback(), NetLogWithSource());
+ EXPECT_THAT(callback.GetResult(rv), IsOk());
+
+ const HttpResponseInfo* response = trans->GetResponseInfo();
+ ASSERT_TRUE(response);
+
+ EXPECT_TRUE(response->headers);
+ std::string status_line = response->headers->GetStatusLine();
+ EXPECT_EQ("HTTP/1.1 200 OK", status_line);
+
+ EXPECT_EQ(0, GetIdleSocketCountInTransportSocketPool(session.get()));
+
+ std::string response_data;
+ switch (test_case) {
+ case TestCase::kReadHeaders: {
+ // Already read the headers, nothing else to do.
+ break;
+ }
+
+ case TestCase::kReadPartOfBodyRead: {
+ scoped_refptr<IOBuffer> buf = base::MakeRefCounted<IOBuffer>(5);
+ rv = trans->Read(buf.get(), 5, callback.callback());
+ ASSERT_EQ(5, callback.GetResult(rv));
+ response_data.assign(buf->data(), 5);
+ EXPECT_EQ("hello", response_data);
+ break;
+ }
+
+ case TestCase::kReadAllOfBody: {
+ rv = ReadTransaction(trans.get(), &response_data);
+ EXPECT_THAT(rv, IsOk());
+ EXPECT_EQ("hello world", response_data);
+ break;
+ }
+ }
+
+ if (close_connection)
+ trans->CloseConnectionOnDestruction();
+ trans.reset();
+
+ // Wait for the socket to be drained and added to the socket pool or
+ // destroyed.
+ base::RunLoop().RunUntilIdle();
+
+ // In the case all the body was read, the socket will have been released
+ // before the CloseConnectionOnDestruction() call, so will not be
+ // destroyed.
+ if (close_connection && test_case != TestCase::kReadAllOfBody) {
+ EXPECT_EQ(0, GetIdleSocketCountInTransportSocketPool(session.get()));
+ } else {
+ EXPECT_EQ(1, GetIdleSocketCountInTransportSocketPool(session.get()));
+ }
+ }
+ }
+}
+
// Grab a socket, use it, and put it back into the pool. Then, make
// low memory notification and ensure the socket pool is flushed.
TEST_F(HttpNetworkTransactionTest, FlushSocketPoolOnLowMemoryNotifications) {
diff --git a/net/http/http_transaction.h b/net/http/http_transaction.h
index 19e4d4d..e3a44d98 100644
--- a/net/http/http_transaction.h
+++ b/net/http/http_transaction.h
@@ -206,6 +206,19 @@
virtual int ResumeNetworkStart() = 0;
virtual void GetConnectionAttempts(ConnectionAttempts* out) const = 0;
+
+ // Configures the transaction to close the network connection, if any, on
+ // destruction. Intended for cases where keeping the socket alive may leak
+ // data. Does not immediately close the socket. If multiple transactions are
+ // using the same socket, only closes it once all transactions have completed.
+ //
+ // Does not close H2/H3 sessions, but does close H1 tunnels on top of H2/H3
+ // sessions.
+ //
+ // Only applies to currently in-use connections. Does nothing after the last
+ // byte of the response body has been read, as the connection is no longer in
+ // use at that point.
+ virtual void CloseConnectionOnDestruction() = 0;
};
} // namespace net
diff --git a/net/http/http_transaction_test_util.cc b/net/http/http_transaction_test_util.cc
index 2e4b0ef..01f0271e8 100644
--- a/net/http/http_transaction_test_util.cc
+++ b/net/http/http_transaction_test_util.cc
@@ -572,6 +572,10 @@
NOTIMPLEMENTED();
}
+void MockNetworkTransaction::CloseConnectionOnDestruction() {
+ NOTIMPLEMENTED();
+}
+
void MockNetworkTransaction::CallbackLater(CompletionOnceCallback callback,
int result) {
base::ThreadTaskRunnerHandle::Get()->PostTask(
diff --git a/net/http/http_transaction_test_util.h b/net/http/http_transaction_test_util.h
index 89a5826..aa7be5c3 100644
--- a/net/http/http_transaction_test_util.h
+++ b/net/http/http_transaction_test_util.h
@@ -250,6 +250,8 @@
void GetConnectionAttempts(ConnectionAttempts* out) const override;
+ void CloseConnectionOnDestruction() override;
+
CreateHelper* websocket_handshake_stream_create_helper() {
return websocket_handshake_stream_create_helper_;
}
diff --git a/net/url_request/url_request.cc b/net/url_request/url_request.cc
index b8409e8..db6f60d 100644
--- a/net/url_request/url_request.cc
+++ b/net/url_request/url_request.cc
@@ -866,6 +866,14 @@
job_->ContinueDespiteLastError();
}
+void URLRequest::AbortAndCloseConnection() {
+ DCHECK_EQ(OK, status_);
+ DCHECK(!has_notified_completion_);
+ DCHECK(job_);
+ job_->CloseConnectionOnDestruction();
+ job_.reset();
+}
+
void URLRequest::PrepareToRestart() {
DCHECK(job_.get());
diff --git a/net/url_request/url_request.h b/net/url_request/url_request.h
index eed0ae9..a640343 100644
--- a/net/url_request/url_request.h
+++ b/net/url_request/url_request.h
@@ -618,6 +618,24 @@
// cancel the request instead, call Cancel().
void ContinueDespiteLastError();
+ // Aborts the request (without invoking any completion callbacks) and closes
+ // the current connection, rather than returning it to the socket pool. Only
+ // affects HTTP/1.1 connections and tunnels.
+ //
+ // Intended to be used in cases where socket reuse can potentially leak data
+ // across sites.
+ //
+ // May only be called after Delegate::OnResponseStarted() has been invoked
+ // with net::OK, but before the body has been completely read. After the last
+ // body has been read, the socket may have already been handed off to another
+ // consumer.
+ //
+ // Due to transactions potentially being shared by multiple URLRequests in
+ // some cases, it is possible the socket may not be immediately closed, but
+ // will instead be closed when all URLRequests sharing the socket have been
+ // destroyed.
+ void AbortAndCloseConnection();
+
// Used to specify the context (cookie store, cache) for this request.
const URLRequestContext* context() const;
diff --git a/net/url_request/url_request_context_builder.cc b/net/url_request/url_request_context_builder.cc
index d19aaf4..d6f1215 100644
--- a/net/url_request/url_request_context_builder.cc
+++ b/net/url_request/url_request_context_builder.cc
@@ -541,6 +541,10 @@
HttpNetworkSession::Context network_session_context;
SetHttpNetworkSessionComponents(context.get(), &network_session_context);
+ // Unlike the other fields of HttpNetworkSession::Context,
+ // |client_socket_factory| is not mirrored in URLRequestContext.
+ network_session_context.client_socket_factory =
+ client_socket_factory_for_testing_;
storage->set_http_network_session(std::make_unique<HttpNetworkSession>(
http_network_session_params_, network_session_context));
diff --git a/net/url_request/url_request_context_builder.h b/net/url_request/url_request_context_builder.h
index 713c74c0..26fcf0e 100644
--- a/net/url_request/url_request_context_builder.h
+++ b/net/url_request/url_request_context_builder.h
@@ -51,6 +51,7 @@
namespace net {
class CertVerifier;
+class ClientSocketFactory;
class CookieStore;
class CTPolicyEnforcer;
class HttpAuthHandlerFactory;
@@ -311,6 +312,13 @@
CreateHttpTransactionFactoryCallback
create_http_network_transaction_factory);
+ // Sets a ClientSocketFactory so a test can mock out sockets. The
+ // ClientSocketFactory must be destroyed after the creates URLRequestContext.
+ void set_client_socket_factory_for_testing(
+ ClientSocketFactory* client_socket_factory_for_testing) {
+ client_socket_factory_for_testing_ = client_socket_factory_for_testing;
+ }
+
// Creates a mostly self-contained URLRequestContext. May only be called once
// per URLRequestContextBuilder. After this is called, the Builder can be
// safely destroyed.
@@ -378,6 +386,8 @@
std::map<std::string, std::unique_ptr<URLRequestJobFactory::ProtocolHandler>>
protocol_handlers_;
+ ClientSocketFactory* client_socket_factory_for_testing_ = nullptr;
+
DISALLOW_COPY_AND_ASSIGN(URLRequestContextBuilder);
};
diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc
index 2c1daf73..cc0608a 100644
--- a/net/url_request/url_request_http_job.cc
+++ b/net/url_request/url_request_http_job.cc
@@ -305,6 +305,11 @@
out->clear();
}
+void URLRequestHttpJob::CloseConnectionOnDestruction() {
+ DCHECK(transaction_);
+ transaction_->CloseConnectionOnDestruction();
+}
+
int URLRequestHttpJob::NotifyConnectedCallback(const TransportInfo& info) {
return URLRequestJob::NotifyConnected(info);
}
diff --git a/net/url_request/url_request_http_job.h b/net/url_request/url_request_http_job.h
index 274314b..c026834 100644
--- a/net/url_request/url_request_http_job.h
+++ b/net/url_request/url_request_http_job.h
@@ -63,6 +63,7 @@
void Start() override;
void Kill() override;
void GetConnectionAttempts(ConnectionAttempts* out) const override;
+ void CloseConnectionOnDestruction() override;
std::unique_ptr<SourceStream> SetUpSourceStream() override;
RequestPriority priority() const {
diff --git a/net/url_request/url_request_job.cc b/net/url_request/url_request_job.cc
index 1f669a4..9eaf309 100644
--- a/net/url_request/url_request_job.cc
+++ b/net/url_request/url_request_job.cc
@@ -270,6 +270,8 @@
out->clear();
}
+void URLRequestJob::CloseConnectionOnDestruction() {}
+
namespace {
// Assuming |url| has already been stripped for use as a referrer, if
diff --git a/net/url_request/url_request_job.h b/net/url_request/url_request_job.h
index fe4dc8ea..065794b 100644
--- a/net/url_request/url_request_job.h
+++ b/net/url_request/url_request_job.h
@@ -240,6 +240,10 @@
// from the remote party with the actual response headers recieved.
virtual void SetResponseHeadersCallback(ResponseHeadersCallback callback) {}
+ // Causes the current transaction always close its active socket on
+ // destruction. Does not close H2/H3 sessions.
+ virtual void CloseConnectionOnDestruction();
+
// Given |policy|, |original_referrer|, and |destination|, returns the
// referrer URL mandated by |request|'s referrer policy.
//
diff --git a/services/network/throttling/throttling_network_transaction.cc b/services/network/throttling/throttling_network_transaction.cc
index 589ec47..ea61af7 100644
--- a/services/network/throttling/throttling_network_transaction.cc
+++ b/services/network/throttling/throttling_network_transaction.cc
@@ -304,4 +304,8 @@
network_transaction_->GetConnectionAttempts(out);
}
+void ThrottlingNetworkTransaction::CloseConnectionOnDestruction() {
+ network_transaction_->CloseConnectionOnDestruction();
+}
+
} // namespace network
diff --git a/services/network/throttling/throttling_network_transaction.h b/services/network/throttling/throttling_network_transaction.h
index a9adb44..83b510a8 100644
--- a/services/network/throttling/throttling_network_transaction.h
+++ b/services/network/throttling/throttling_network_transaction.h
@@ -85,6 +85,7 @@
net::ResponseHeadersCallback callback) override;
int ResumeNetworkStart() override;
void GetConnectionAttempts(net::ConnectionAttempts* out) const override;
+ void CloseConnectionOnDestruction() override;
protected:
friend class ThrottlingControllerTestHelper;
diff --git a/services/network/url_loader.cc b/services/network/url_loader.cc
index 3273379d..c92b8a07 100644
--- a/services/network/url_loader.cc
+++ b/services/network/url_loader.cc
@@ -1122,6 +1122,18 @@
coep_reporter_)) {
CompleteBlockedResponse(net::ERR_BLOCKED_BY_RESPONSE, false,
blocked_reason);
+ // TODO(https://crbug.com/1154250): Close the socket here.
+ // For more details see https://crbug.com/1154250#c17.
+ // Item 2 discusses redirect handling.
+ //
+ // "url_request_->AbortAndCloseConnection()" should ideally close the
+ // socket, but unfortunately, URLRequestHttpJob caches redirects in a way
+ // that ignores their response bodies, since they'll never be read. It does
+ // this by calling HttpCache::Transaction::StopCaching(), which also has the
+ // effect of detaching the HttpNetworkTransaction, which owns the socket,
+ // from the HttpCache::Transaction. To fix this, we'd either need to call
+ // StopCaching() later in the process, or make the HttpCache::Transaction
+ // continue to hang onto the HttpNetworkTransaction after this call.
DeleteSelf();
return;
}
@@ -1352,6 +1364,9 @@
coep_reporter_)) {
CompleteBlockedResponse(net::ERR_BLOCKED_BY_RESPONSE, false,
blocked_reason);
+ // Close the socket associated with the request, to prevent leaking
+ // information.
+ url_request_->AbortAndCloseConnection();
DeleteSelf();
return;
}
@@ -2070,6 +2085,11 @@
// Ask the caller to continue processing the request.
return kContinueRequest;
}
+
+ // Close the socket associated with the request, to prevent leaking
+ // information.
+ url_request_->AbortAndCloseConnection();
+
// Delete self and cancel the request - the caller doesn't need to continue.
//
// DeleteSelf is posted asynchronously, to make sure that the callers (e.g.
diff --git a/services/network/url_loader_unittest.cc b/services/network/url_loader_unittest.cc
index bc2edbde..817637f8 100644
--- a/services/network/url_loader_unittest.cc
+++ b/services/network/url_loader_unittest.cc
@@ -57,6 +57,7 @@
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_response_info.h"
#include "net/proxy_resolution/configured_proxy_resolution_service.h"
+#include "net/socket/socket_test_util.h"
#include "net/ssl/client_cert_identity_test_util.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
@@ -492,13 +493,19 @@
net::GetTestCertsDirectory().AppendASCII("quic-root.pem"));
net::QuicSimpleTestServer::Start();
+ net::URLRequestFailedJob::AddUrlHandler();
+ }
+ ~URLLoaderTest() override {
+ net::URLRequestFilter::GetInstance()->ClearHandlers();
+ }
+
+ void SetUp() override {
net::HttpNetworkSession::Params params;
auto quic_context = std::make_unique<net::QuicContext>();
quic_context->params()->origins_to_force_quic_on.insert(
net::HostPortPair(net::QuicSimpleTestServer::GetHost(),
net::QuicSimpleTestServer::GetPort()));
params.enable_quic = true;
-
net::URLRequestContextBuilder context_builder;
context_builder.set_http_network_session_params(params);
context_builder.set_quic_context(std::move(quic_context));
@@ -507,17 +514,12 @@
auto test_network_delegate = std::make_unique<net::TestNetworkDelegate>();
unowned_test_network_delegate_ = test_network_delegate.get();
context_builder.set_network_delegate(std::move(test_network_delegate));
+ context_builder.set_client_socket_factory_for_testing(GetSocketFactory());
context_ = context_builder.Build();
resource_scheduler_client_ = base::MakeRefCounted<ResourceSchedulerClient>(
kProcessId, kRouteId, &resource_scheduler_,
context_->network_quality_estimator());
- net::URLRequestFailedJob::AddUrlHandler();
- }
- ~URLLoaderTest() override {
- net::URLRequestFilter::GetInstance()->ClearHandlers();
- }
- void SetUp() override {
test_server_.AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("services/test/data")));
// This Unretained is safe because test_server_ is owned by |this|.
@@ -534,18 +536,33 @@
mock_resolver_proc.get());
}
- void TearDown() override { net::QuicSimpleTestServer::Shutdown(); }
+ void TearDown() override {
+ context_.reset();
+ net::QuicSimpleTestServer::Shutdown();
+ }
- // Attempts to load |url| and returns the resulting error code. If |body| is
- // non-NULL, also attempts to read the response body. The advantage of using
- // |body| instead of calling ReadBody() after Load is that it will load the
- // response body before URLLoader is complete, so URLLoader completion won't
- // block on trying to write the body buffer.
+ // Attempts to load |url| and returns the resulting error code.
int Load(const GURL& url, std::string* body = nullptr) WARN_UNUSED_RESULT {
DCHECK(!ran_);
ResourceRequest request =
CreateResourceRequest(!request_body_ ? "GET" : "POST", url);
+
+ if (request_body_)
+ request.request_body = request_body_;
+
+ request.trusted_params->client_security_state.Swap(
+ &request_client_security_state_);
+ return LoadRequest(request, body);
+ }
+
+ // Attempts to load |request| and returns the resulting error code. If |body|
+ // is non-NULL, also attempts to read the response body. The advantage of
+ // using |body| instead of calling ReadBody() after Load is that it will load
+ // the response body before URLLoader is complete, so URLLoader completion
+ // won't block on trying to write the body buffer.
+ int LoadRequest(const ResourceRequest& request,
+ std::string* body = nullptr) WARN_UNUSED_RESULT {
uint32_t options = mojom::kURLLoadOptionNone;
if (send_ssl_with_response_)
options |= mojom::kURLLoadOptionSendSSLInfoWithResponse;
@@ -562,22 +579,16 @@
ignore_last_upload_file_);
}
- if (request_body_)
- request.request_body = request_body_;
-
- request.trusted_params->client_security_state.Swap(
- &request_client_security_state_);
-
base::RunLoop delete_run_loop;
mojo::Remote<mojom::URLLoader> loader;
std::unique_ptr<URLLoader> url_loader;
mojom::URLLoaderFactoryParams params;
params.process_id = mojom::kBrowserProcessId;
- params.is_corb_enabled = false;
+ params.is_corb_enabled = corb_enabled_;
params.client_security_state.Swap(&factory_client_security_state_);
- url::Origin origin = url::Origin::Create(url);
+ url::Origin origin = url::Origin::Create(request.url);
params.isolation_info =
net::IsolationInfo::CreateForInternalRequest(origin);
params.is_trusted = true;
@@ -858,6 +869,9 @@
// execute once a request reaches the test server.
virtual void OnServerReceivedRequest(const net::test_server::HttpRequest&) {}
+ // Lets subclasses inject a mock ClientSocketFactory.
+ virtual net::ClientSocketFactory* GetSocketFactory() { return nullptr; }
+
protected:
void Monitor(const net::test_server::HttpRequest& request) {
sent_request_ = request;
@@ -885,6 +899,8 @@
mojom::ClientSecurityStatePtr request_client_security_state_;
scoped_refptr<ResourceRequestBody> request_body_;
+ bool corb_enabled_ = false;
+
// Used to ensure that methods are called either before or after a request is
// made, since the test fixture is meant to be used only once.
bool ran_ = false;
@@ -892,6 +908,20 @@
TestURLLoaderClient client_;
};
+class URLLoaderMockSocketTest : public URLLoaderTest {
+ public:
+ URLLoaderMockSocketTest() = default;
+ ~URLLoaderMockSocketTest() override = default;
+
+ // Lets subclasses inject mock ClientSocketFactories.
+ net::ClientSocketFactory* GetSocketFactory() override {
+ return &socket_factory_;
+ }
+
+ protected:
+ net::MockClientSocketFactory socket_factory_;
+};
+
constexpr int URLLoaderTest::kProcessId;
constexpr int URLLoaderTest::kRouteId;
@@ -6073,4 +6103,221 @@
ASSERT_FALSE(network_service_client.client_security_state());
}
+TEST_F(URLLoaderMockSocketTest,
+ CorbDoesNotCloseSocketsWhenResourcesNotBlocked) {
+ corb_enabled_ = true;
+
+ const net::MockWrite kWrites[] = {
+ net::MockWrite(net::SYNCHRONOUS, 0,
+ "GET / HTTP/1.1\r\n"
+ "Host: origin.test\r\n"
+ "Connection: keep-alive\r\n"
+ "User-Agent: \r\n"
+ "Accept-Encoding: gzip, deflate\r\n\r\n"),
+ };
+ net::MockRead kReads[] = {
+ net::MockRead(net::SYNCHRONOUS, 1,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: keep-alive\r\n"
+ "Content-Length: 5\r\n\r\n"),
+ net::MockRead(net::SYNCHRONOUS, 2, "Hello"),
+ };
+
+ net::SequencedSocketData socket_data(kReads, kWrites);
+ socket_factory_.AddSocketDataProvider(&socket_data);
+
+ GURL url("http://origin.test/");
+ url::Origin initiator =
+ url::Origin::Create(GURL("http://other-origin.test/"));
+
+ ResourceRequest request = CreateResourceRequest("GET", url);
+ request.mode = mojom::RequestMode::kCors;
+ request.request_initiator = initiator;
+ std::string body;
+ EXPECT_EQ(net::OK, LoadRequest(request, &body));
+ EXPECT_EQ(body, "Hello");
+
+ // Socket should still be alive, in the socket pool.
+ EXPECT_TRUE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, CorbClosesSocketOnReceivingHeaders) {
+ corb_enabled_ = true;
+
+ const net::MockWrite kWrites[] = {
+ net::MockWrite(net::SYNCHRONOUS, 0,
+ "GET / HTTP/1.1\r\n"
+ "Host: origin.test\r\n"
+ "Connection: keep-alive\r\n"
+ "User-Agent: \r\n"
+ "Accept-Encoding: gzip, deflate\r\n\r\n"),
+ };
+ net::MockRead kReads[] = {
+ net::MockRead(net::SYNCHRONOUS, 1,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: keep-alive\r\n"
+ "Cross-Origin-Resource-Policy: same-origin\r\n"
+ "Content-Length: 23\r\n\r\n"),
+ net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"),
+ };
+
+ net::SequencedSocketData socket_data(kReads, kWrites);
+ socket_factory_.AddSocketDataProvider(&socket_data);
+
+ GURL url("http://origin.test/");
+ url::Origin initiator =
+ url::Origin::Create(GURL("http://other-origin.test/"));
+
+ ResourceRequest request = CreateResourceRequest("GET", url);
+ request.mode = mojom::RequestMode::kCors;
+ request.request_initiator = initiator;
+ std::string body;
+ EXPECT_EQ(net::OK, LoadRequest(request, &body));
+ EXPECT_TRUE(body.empty());
+
+ // Socket should have been destroyed, so it will not be reused.
+ EXPECT_FALSE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest,
+ CorbDoesNotCloseSocketsWhenResourcesNotBlockedAfterSniffingMimeType) {
+ corb_enabled_ = true;
+
+ const net::MockWrite kWrites[] = {
+ net::MockWrite(net::SYNCHRONOUS, 0,
+ "GET / HTTP/1.1\r\n"
+ "Host: origin.test\r\n"
+ "Connection: keep-alive\r\n"
+ "User-Agent: \r\n"
+ "Accept-Encoding: gzip, deflate\r\n\r\n"),
+ };
+ net::MockRead kReads[] = {
+ net::MockRead(net::SYNCHRONOUS, 1,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: keep-alive\r\n"
+ "Content-Type: application/json\r\n"
+ "Content-Length: 17\r\n\r\n"),
+ net::MockRead(net::SYNCHRONOUS, 2, "Not actually JSON"),
+ };
+
+ net::SequencedSocketData socket_data(kReads, kWrites);
+ socket_factory_.AddSocketDataProvider(&socket_data);
+
+ GURL url("http://origin.test/");
+ url::Origin initiator =
+ url::Origin::Create(GURL("http://other-origin.test/"));
+
+ ResourceRequest request = CreateResourceRequest("GET", url);
+ request.mode = mojom::RequestMode::kCors;
+ request.request_initiator = initiator;
+ std::string body;
+ EXPECT_EQ(net::OK, LoadRequest(request, &body));
+ EXPECT_EQ("Not actually JSON", body);
+
+ // Socket should still be alive, in the socket pool.
+ EXPECT_TRUE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, CorbClosesSocketOnSniffingMimeType) {
+ corb_enabled_ = true;
+
+ const net::MockWrite kWrites[] = {
+ net::MockWrite(net::SYNCHRONOUS, 0,
+ "GET / HTTP/1.1\r\n"
+ "Host: origin.test\r\n"
+ "Connection: keep-alive\r\n"
+ "User-Agent: \r\n"
+ "Accept-Encoding: gzip, deflate\r\n\r\n"),
+ };
+ net::MockRead kReads[] = {
+ net::MockRead(net::SYNCHRONOUS, 1,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: keep-alive\r\n"
+ "Content-Type: application/json\r\n"
+ "Content-Length: 9\r\n\r\n"),
+ net::MockRead(net::SYNCHRONOUS, 2, "{\"x\" : 3}"),
+ };
+
+ net::SequencedSocketData socket_data(kReads, kWrites);
+ socket_factory_.AddSocketDataProvider(&socket_data);
+
+ GURL url("http://origin.test/");
+ url::Origin initiator =
+ url::Origin::Create(GURL("http://other-origin.test/"));
+
+ ResourceRequest request = CreateResourceRequest("GET", url);
+ request.mode = mojom::RequestMode::kCors;
+ request.request_initiator = initiator;
+ std::string body;
+ EXPECT_EQ(net::OK, LoadRequest(request, &body));
+ EXPECT_TRUE(body.empty());
+
+ // Socket should have been destroyed, so it will not be reused.
+ EXPECT_FALSE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, CorpClosesSocket) {
+ auto client_security_state = NewSecurityState();
+ client_security_state->cross_origin_embedder_policy.value =
+ mojom::CrossOriginEmbedderPolicyValue::kRequireCorp;
+ client_security_state->private_network_request_policy =
+ mojom::PrivateNetworkRequestPolicy::kAllow;
+ set_factory_client_security_state(std::move(client_security_state));
+
+ const net::MockWrite kWrites[] = {
+ net::MockWrite(net::SYNCHRONOUS, 0,
+ "GET / HTTP/1.1\r\n"
+ "Host: origin.test\r\n"
+ "Connection: keep-alive\r\n"
+ "User-Agent: \r\n"
+ "Accept-Encoding: gzip, deflate\r\n\r\n"),
+ };
+ net::MockRead kReads[] = {
+ net::MockRead(net::SYNCHRONOUS, 1,
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: keep-alive\r\n"
+ "Content-Type: test/plain\r\n"
+ "Content-Length: 23\r\n\r\n"),
+ net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"),
+ };
+
+ net::SequencedSocketData socket_data(kReads, kWrites);
+ socket_factory_.AddSocketDataProvider(&socket_data);
+
+ GURL url("http://origin.test/");
+ url::Origin initiator =
+ url::Origin::Create(GURL("http://other-origin.test/"));
+
+ ResourceRequest request = CreateResourceRequest("GET", url);
+ request.mode = mojom::RequestMode::kNoCors;
+ request.request_initiator = initiator;
+ EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE, LoadRequest(request));
+
+ // Socket should have been destroyed, so it will not be reused.
+ EXPECT_FALSE(socket_data.socket());
+}
+
+TEST_F(URLLoaderMockSocketTest, PrivateNetworkRequestPolicyClosesSocket) {
+ auto client_security_state = NewSecurityState();
+ client_security_state->private_network_request_policy =
+ mojom::PrivateNetworkRequestPolicy::kBlockFromInsecureToMorePrivate;
+ set_factory_client_security_state(std::move(client_security_state));
+
+ // No data should be read or written. Trying to do so will assert.
+ net::SequencedSocketData socket_data;
+ socket_factory_.AddSocketDataProvider(&socket_data);
+
+ GURL url("http://origin.test/");
+ url::Origin initiator =
+ url::Origin::Create(GURL("http://other-origin.test/"));
+
+ ResourceRequest request = CreateResourceRequest("GET", url);
+ request.mode = mojom::RequestMode::kNoCors;
+ request.request_initiator = initiator;
+ EXPECT_EQ(net::ERR_FAILED, LoadRequest(request));
+
+ // Socket should have been destroyed.
+ EXPECT_FALSE(socket_data.socket());
+}
+
} // namespace network