diff options
author | wtc@chromium.org <wtc@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-02-18 21:00:32 +0000 |
---|---|---|
committer | wtc@chromium.org <wtc@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-02-18 21:00:32 +0000 |
commit | 2d2697f9ae8f588c104c017209c1e6158ab53c25 (patch) | |
tree | a9470736768a765a341455d987440650ad6d3dc0 /net/http | |
parent | 2072f00ba9832d7a3185917a70153415e0866dc1 (diff) | |
download | chromium_src-2d2697f9ae8f588c104c017209c1e6158ab53c25.zip chromium_src-2d2697f9ae8f588c104c017209c1e6158ab53c25.tar.gz chromium_src-2d2697f9ae8f588c104c017209c1e6158ab53c25.tar.bz2 |
Perform HTTP authentication over a keep-alive connection.
This is required for NTLM authentication.
R=eroman
BUG=6567,6824
Review URL: http://codereview.chromium.org/21433
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@9964 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'net/http')
-rw-r--r-- | net/http/http_network_transaction.cc | 116 | ||||
-rw-r--r-- | net/http/http_network_transaction.h | 14 | ||||
-rw-r--r-- | net/http/http_network_transaction_unittest.cc | 346 |
3 files changed, 464 insertions, 12 deletions
diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc index f7aeff6..71666fc 100644 --- a/net/http/http_network_transaction.cc +++ b/net/http/http_network_transaction.cc @@ -127,9 +127,36 @@ void HttpNetworkTransaction::PrepareForAuthRestart(HttpAuth::Target target) { auth_identity_[target].username, auth_identity_[target].password, AuthPath(target)); - next_state_ = STATE_INIT_CONNECTION; - connection_.set_socket(NULL); - connection_.Reset(); + bool keep_alive = false; + if (response_.headers->IsKeepAlive()) { + // If there is a response body of known length, we need to drain it first. + if (content_length_ > 0 || chunked_decoder_.get()) { + next_state_ = STATE_DRAIN_BODY_FOR_AUTH_RESTART; + read_buf_ = new IOBuffer(kDrainBodyBufferSize); // A bit bucket + read_buf_len_ = kDrainBodyBufferSize; + return; + } + if (content_length_ == 0) // No response body to drain. + keep_alive = true; + // content_length_ is -1 and we're not using chunked encoding. We don't + // know the length of the response body, so we can't reuse this connection + // even though the server says it's keep-alive. + } + + // We don't need to drain the response body, so we act as if we had drained + // the response body. + DidDrainBodyForAuthRestart(keep_alive); +} + +void HttpNetworkTransaction::DidDrainBodyForAuthRestart(bool keep_alive) { + if (keep_alive) { + next_state_ = STATE_WRITE_HEADERS; + reused_socket_ = true; + } else { + next_state_ = STATE_INIT_CONNECTION; + connection_.set_socket(NULL); + connection_.Reset(); + } // Reset the other member variables. ResetStateForRestart(); @@ -379,6 +406,17 @@ int HttpNetworkTransaction::DoLoop(int result) { rv = DoReadBodyComplete(rv); TRACE_EVENT_END("http.read_body", request_, request_->url.spec()); break; + case STATE_DRAIN_BODY_FOR_AUTH_RESTART: + DCHECK(rv == OK); + TRACE_EVENT_BEGIN("http.drain_body_for_auth_restart", + request_, request_->url.spec()); + rv = DoDrainBodyForAuthRestart(); + break; + case STATE_DRAIN_BODY_FOR_AUTH_RESTART_COMPLETE: + rv = DoDrainBodyForAuthRestartComplete(rv); + TRACE_EVENT_END("http.drain_body_for_auth_restart", + request_, request_->url.spec()); + break; default: NOTREACHED() << "bad state"; rv = ERR_FAILED; @@ -778,7 +816,7 @@ int HttpNetworkTransaction::DoReadBodyComplete(int result) { } } - // Clean up the HttpConnection if we are done. + // Clean up connection_ if we are done. if (done) { LogTransactionMetrics(); if (!keep_alive) @@ -794,6 +832,62 @@ int HttpNetworkTransaction::DoReadBodyComplete(int result) { return result; } +int HttpNetworkTransaction::DoDrainBodyForAuthRestart() { + // This method differs from DoReadBody only in the next_state_. So we just + // call DoReadBody and override the next_state_. Perhaps there is a more + // elegant way for these two methods to share code. + int rv = DoReadBody(); + DCHECK(next_state_ == STATE_READ_BODY_COMPLETE); + next_state_ = STATE_DRAIN_BODY_FOR_AUTH_RESTART_COMPLETE; + return rv; +} + +// TODO(wtc): The first two thirds of this method and the DoReadBodyComplete +// method are almost the same. Figure out a good way for these two methods +// to share code. +int HttpNetworkTransaction::DoDrainBodyForAuthRestartComplete(int result) { + bool unfiltered_eof = (result == 0); + + // Filter incoming data if appropriate. FilterBuf may return an error. + if (result > 0 && chunked_decoder_.get()) { + result = chunked_decoder_->FilterBuf(read_buf_->data(), result); + if (result == 0 && !chunked_decoder_->reached_eof()) { + // Don't signal completion of the Read call yet or else it'll look like + // we received end-of-file. Wait for more data. + next_state_ = STATE_DRAIN_BODY_FOR_AUTH_RESTART; + return OK; + } + } + + bool done = false, keep_alive = false; + if (result < 0) { + // Error while reading the socket. + done = true; + } else { + content_read_ += result; + if (unfiltered_eof || + (content_length_ != -1 && content_read_ >= content_length_) || + (chunked_decoder_.get() && chunked_decoder_->reached_eof())) { + done = true; + keep_alive = response_.headers->IsKeepAlive(); + // We can't reuse the connection if we read more than the advertised + // content length. + if (unfiltered_eof || + (content_length_ != -1 && content_read_ > content_length_)) + keep_alive = false; + } + } + + if (done) { + DidDrainBodyForAuthRestart(keep_alive); + } else { + // Keep draining. + next_state_ = STATE_DRAIN_BODY_FOR_AUTH_RESTART; + } + + return OK; +} + void HttpNetworkTransaction::LogTransactionMetrics() const { base::TimeDelta duration = base::Time::Now() - response_.request_time; if (60 < duration.InMinutes()) @@ -869,14 +963,6 @@ int HttpNetworkTransaction::DidReadResponseHeaders() { response_.headers = headers; response_.vary_data.Init(*request_, *response_.headers); - int rv = HandleAuthChallenge(); - if (rv == WILL_RESTART_TRANSACTION) { - DCHECK(next_state_ == STATE_INIT_CONNECTION); - return OK; - } - if (rv != OK) - return rv; - // Figure how to determine EOF: // For certain responses, we know the content length is always 0. @@ -901,6 +987,12 @@ int HttpNetworkTransaction::DidReadResponseHeaders() { } } + int rv = HandleAuthChallenge(); + if (rv == WILL_RESTART_TRANSACTION) + return OK; + if (rv != OK) + return rv; + if (using_ssl_ && !establishing_tunnel_) { SSLClientSocket* ssl_socket = reinterpret_cast<SSLClientSocket*>(connection_.socket()); diff --git a/net/http/http_network_transaction.h b/net/http/http_network_transaction.h index 9a4c619..4ebc89a 100644 --- a/net/http/http_network_transaction.h +++ b/net/http/http_network_transaction.h @@ -75,6 +75,8 @@ class HttpNetworkTransaction : public HttpTransaction { int DoReadHeadersComplete(int result); int DoReadBody(); int DoReadBodyComplete(int result); + int DoDrainBodyForAuthRestart(); + int DoDrainBodyForAuthRestartComplete(int result); // Record histogram of latency (first byte sent till last byte received) as // well as effective bandwidth used. @@ -126,6 +128,11 @@ class HttpNetworkTransaction : public HttpTransaction { // Sets up the state machine to restart the transaction with auth. void PrepareForAuthRestart(HttpAuth::Target target); + // Called when we don't need to drain the response body or have drained it. + // Resets |connection_| unless |keep_alive| is true, then calls + // ResetStateForRestart. Sets |next_state_| appropriately. + void DidDrainBodyForAuthRestart(bool keep_alive); + // Resets the members of the transaction so it can be restarted. void ResetStateForRestart(); @@ -245,6 +252,11 @@ class HttpNetworkTransaction : public HttpTransaction { // Note: |kMaxHeaderBufSize| should be a multiple of |kHeaderBufInitialSize|. enum { kMaxHeaderBufSize = 32768 }; // 32 kilobytes. + // The size in bytes of the buffer we use to drain the response body that + // we want to throw away. The response body is typically a small error + // page just a few hundred bytes long. + enum { kDrainBodyBufferSize = 1024 }; + // The position where status line starts; -1 if not found yet. int header_buf_http_offset_; @@ -281,6 +293,8 @@ class HttpNetworkTransaction : public HttpTransaction { STATE_READ_HEADERS_COMPLETE, STATE_READ_BODY, STATE_READ_BODY_COMPLETE, + STATE_DRAIN_BODY_FOR_AUTH_RESTART, + STATE_DRAIN_BODY_FOR_AUTH_RESTART_COMPLETE, STATE_NONE }; State next_state_; diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc index 1e9f6aad..52e299f 100644 --- a/net/http/http_network_transaction_unittest.cc +++ b/net/http/http_network_transaction_unittest.cc @@ -711,6 +711,352 @@ TEST_F(HttpNetworkTransactionTest, BasicAuth) { EXPECT_EQ(100, response->headers->GetContentLength()); } +// Test the request-challenge-retry sequence for basic auth, over a keep-alive +// connection. +TEST_F(HttpNetworkTransactionTest, BasicAuthKeepAlive) { + scoped_ptr<net::ProxyService> proxy_service(CreateNullProxyService()); + scoped_ptr<net::HttpTransaction> trans(new net::HttpNetworkTransaction( + CreateSession(proxy_service.get()), &mock_socket_factory)); + + net::HttpRequestInfo request; + request.method = "GET"; + request.url = GURL("http://www.google.com/"); + request.load_flags = 0; + + MockWrite data_writes1[] = { + MockWrite("GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Connection: keep-alive\r\n\r\n"), + + // After calling trans->RestartWithAuth(), this is the request we should + // be issuing -- the final header line contains the credentials. + MockWrite("GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Connection: keep-alive\r\n" + "Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), + }; + + MockRead data_reads1[] = { + MockRead("HTTP/1.1 401 Unauthorized\r\n"), + MockRead("WWW-Authenticate: Basic realm=\"MyRealm1\"\r\n"), + MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"), + MockRead("Content-Length: 14\r\n\r\n"), + MockRead("Unauthorized\r\n"), + + // Lastly, the server responds with the actual content. + MockRead("HTTP/1.1 200 OK\r\n"), + MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"), + MockRead("Content-Length: 100\r\n\r\n"), + MockRead(false, net::OK), + }; + + MockSocket data1; + data1.reads = data_reads1; + data1.writes = data_writes1; + mock_sockets[0] = &data1; + mock_sockets[1] = NULL; + + TestCompletionCallback callback1; + + int rv = trans->Start(&request, &callback1); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback1.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + const net::HttpResponseInfo* response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"www.google.com", response->auth_challenge->host); + EXPECT_EQ(L"MyRealm1", response->auth_challenge->realm); + EXPECT_EQ(L"basic", response->auth_challenge->scheme); + + TestCompletionCallback callback2; + + rv = trans->RestartWithAuth(L"foo", L"bar", &callback2); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback2.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + EXPECT_TRUE(response->auth_challenge.get() == NULL); + EXPECT_EQ(100, response->headers->GetContentLength()); +} + +// Test the request-challenge-retry sequence for basic auth, over a keep-alive +// connection and with no response body to drain. +TEST_F(HttpNetworkTransactionTest, BasicAuthKeepAliveNoBody) { + scoped_ptr<net::ProxyService> proxy_service(CreateNullProxyService()); + scoped_ptr<net::HttpTransaction> trans(new net::HttpNetworkTransaction( + CreateSession(proxy_service.get()), &mock_socket_factory)); + + net::HttpRequestInfo request; + request.method = "GET"; + request.url = GURL("http://www.google.com/"); + request.load_flags = 0; + + MockWrite data_writes1[] = { + MockWrite("GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Connection: keep-alive\r\n\r\n"), + + // After calling trans->RestartWithAuth(), this is the request we should + // be issuing -- the final header line contains the credentials. + MockWrite("GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Connection: keep-alive\r\n" + "Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), + }; + + // Respond with 5 kb of response body. + std::string large_body_string("Unauthorized"); + large_body_string.append(5 * 1024, ' '); + large_body_string.append("\r\n"); + + MockRead data_reads1[] = { + MockRead("HTTP/1.1 401 Unauthorized\r\n"), + MockRead("WWW-Authenticate: Basic realm=\"MyRealm1\"\r\n"), + MockRead("Content-Length: 0\r\n\r\n"), + + // Lastly, the server responds with the actual content. + MockRead("HTTP/1.1 200 OK\r\n"), + MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"), + MockRead("Content-Length: 100\r\n\r\n"), + MockRead(false, net::OK), + }; + + MockSocket data1; + data1.reads = data_reads1; + data1.writes = data_writes1; + mock_sockets[0] = &data1; + mock_sockets[1] = NULL; + + TestCompletionCallback callback1; + + int rv = trans->Start(&request, &callback1); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback1.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + const net::HttpResponseInfo* response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"www.google.com", response->auth_challenge->host); + EXPECT_EQ(L"MyRealm1", response->auth_challenge->realm); + EXPECT_EQ(L"basic", response->auth_challenge->scheme); + + TestCompletionCallback callback2; + + rv = trans->RestartWithAuth(L"foo", L"bar", &callback2); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback2.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + EXPECT_TRUE(response->auth_challenge.get() == NULL); + EXPECT_EQ(100, response->headers->GetContentLength()); +} + +// Test the request-challenge-retry sequence for basic auth, over a keep-alive +// connection and with a large response body to drain. +TEST_F(HttpNetworkTransactionTest, BasicAuthKeepAliveLargeBody) { + scoped_ptr<net::ProxyService> proxy_service(CreateNullProxyService()); + scoped_ptr<net::HttpTransaction> trans(new net::HttpNetworkTransaction( + CreateSession(proxy_service.get()), &mock_socket_factory)); + + net::HttpRequestInfo request; + request.method = "GET"; + request.url = GURL("http://www.google.com/"); + request.load_flags = 0; + + MockWrite data_writes1[] = { + MockWrite("GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Connection: keep-alive\r\n\r\n"), + + // After calling trans->RestartWithAuth(), this is the request we should + // be issuing -- the final header line contains the credentials. + MockWrite("GET / HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Connection: keep-alive\r\n" + "Authorization: Basic Zm9vOmJhcg==\r\n\r\n"), + }; + + // Respond with 5 kb of response body. + std::string large_body_string("Unauthorized"); + large_body_string.append(5 * 1024, ' '); + large_body_string.append("\r\n"); + + MockRead data_reads1[] = { + MockRead("HTTP/1.1 401 Unauthorized\r\n"), + MockRead("WWW-Authenticate: Basic realm=\"MyRealm1\"\r\n"), + MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"), + // 5134 = 12 + 5 * 1024 + 2 + MockRead("Content-Length: 5134\r\n\r\n"), + MockRead(true, large_body_string.data(), large_body_string.size()), + + // Lastly, the server responds with the actual content. + MockRead("HTTP/1.1 200 OK\r\n"), + MockRead("Content-Type: text/html; charset=iso-8859-1\r\n"), + MockRead("Content-Length: 100\r\n\r\n"), + MockRead(false, net::OK), + }; + + MockSocket data1; + data1.reads = data_reads1; + data1.writes = data_writes1; + mock_sockets[0] = &data1; + mock_sockets[1] = NULL; + + TestCompletionCallback callback1; + + int rv = trans->Start(&request, &callback1); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback1.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + const net::HttpResponseInfo* response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"www.google.com", response->auth_challenge->host); + EXPECT_EQ(L"MyRealm1", response->auth_challenge->realm); + EXPECT_EQ(L"basic", response->auth_challenge->scheme); + + TestCompletionCallback callback2; + + rv = trans->RestartWithAuth(L"foo", L"bar", &callback2); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback2.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + EXPECT_TRUE(response->auth_challenge.get() == NULL); + EXPECT_EQ(100, response->headers->GetContentLength()); +} + +// Test the request-challenge-retry sequence for basic auth, over a keep-alive +// proxy connection, when setting up an SSL tunnel. +TEST_F(HttpNetworkTransactionTest, BasicAuthProxyKeepAlive) { + // Configure against proxy server "myproxy:70". + scoped_ptr<net::ProxyService> proxy_service( + CreateFixedProxyService("myproxy:70")); + + scoped_refptr<net::HttpNetworkSession> session( + CreateSession(proxy_service.get())); + + scoped_ptr<net::HttpTransaction> trans(new net::HttpNetworkTransaction( + session.get(), &mock_socket_factory)); + + net::HttpRequestInfo request; + request.method = "GET"; + request.url = GURL("https://www.google.com/"); + request.load_flags = 0; + + // Since we have proxy, should try to establish tunnel. + MockWrite data_writes1[] = { + MockWrite("CONNECT www.google.com:443 HTTP/1.1\r\n" + "Host: www.google.com\r\n\r\n"), + + // After calling trans->RestartWithAuth(), this is the request we should + // be issuing -- the final header line contains the credentials. + MockWrite("CONNECT www.google.com:443 HTTP/1.1\r\n" + "Host: www.google.com\r\n" + "Proxy-Authorization: Basic Zm9vOmJheg==\r\n\r\n"), + }; + + // The proxy responds to the connect with a 407, using a persistent + // connection. + MockRead data_reads1[] = { + // No credentials. + MockRead("HTTP/1.1 407 Proxy Authentication Required\r\n"), + MockRead("Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n"), + MockRead("Content-Length: 10\r\n\r\n"), + MockRead("0123456789"), + + // Wrong credentials (wrong password). + MockRead("HTTP/1.1 407 Proxy Authentication Required\r\n"), + MockRead("Proxy-Authenticate: Basic realm=\"MyRealm1\"\r\n"), + MockRead("Content-Length: 10\r\n\r\n"), + // No response body because the test stops reading here. + MockRead(false, net::ERR_UNEXPECTED), // Should not be reached. + }; + + MockSocket data1; + data1.writes = data_writes1; + data1.reads = data_reads1; + mock_sockets[0] = &data1; + mock_sockets[1] = NULL; + + TestCompletionCallback callback1; + + int rv = trans->Start(&request, &callback1); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback1.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + const net::HttpResponseInfo* response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + EXPECT_TRUE(response->headers->IsKeepAlive()); + EXPECT_EQ(407, response->headers->response_code()); + EXPECT_EQ(10, response->headers->GetContentLength()); + EXPECT_TRUE(net::HttpVersion(1, 1) == response->headers->GetHttpVersion()); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"myproxy:70", response->auth_challenge->host); + EXPECT_EQ(L"MyRealm1", response->auth_challenge->realm); + EXPECT_EQ(L"basic", response->auth_challenge->scheme); + + TestCompletionCallback callback2; + + // Wrong password (should be "bar"). + rv = trans->RestartWithAuth(L"foo", L"baz", &callback2); + EXPECT_EQ(net::ERR_IO_PENDING, rv); + + rv = callback2.WaitForResult(); + EXPECT_EQ(net::OK, rv); + + response = trans->GetResponseInfo(); + EXPECT_FALSE(response == NULL); + + EXPECT_TRUE(response->headers->IsKeepAlive()); + EXPECT_EQ(407, response->headers->response_code()); + EXPECT_EQ(10, response->headers->GetContentLength()); + EXPECT_TRUE(net::HttpVersion(1, 1) == response->headers->GetHttpVersion()); + + // The password prompt info should have been set in response->auth_challenge. + EXPECT_FALSE(response->auth_challenge.get() == NULL); + + // TODO(eroman): this should really include the effective port (80) + EXPECT_EQ(L"myproxy:70", response->auth_challenge->host); + EXPECT_EQ(L"MyRealm1", response->auth_challenge->realm); + EXPECT_EQ(L"basic", response->auth_challenge->scheme); +} + // Test the flow when both the proxy server AND origin server require // authentication. Again, this uses basic auth for both since that is // the simplest to mock. |