Prevent CancelAuth from doing things twice.

Previously, CancelAuth would cause the URLRequestHttpJob to resume at a
point earlier than where the caller was informed of the auth challenge
resulting in doing a number of things twice (Setting cookies, calling
into the network delegate, logging a histogram, setting various
headers received times).

This CL fixes that, causing the URLRequestHttpJob to resume at the step
right after it informed the called of the auth challenge.

Bug: 971836
Change-Id: I8db5f759075799ed338ac0ce8dcc941c9d07e777
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1649078
Reviewed-by: Asanka Herath <[email protected]>
Commit-Queue: Matt Menke <[email protected]>
Cr-Commit-Position: refs/heads/master@{#668126}
diff --git a/net/url_request/url_request.cc b/net/url_request/url_request.cc
index d11742c..f5711c7 100644
--- a/net/url_request/url_request.cc
+++ b/net/url_request/url_request.cc
@@ -862,6 +862,10 @@
   if (status.status() != URLRequestStatus::SUCCESS)
     set_status(status);
 
+  // |status_| should not be ERR_IO_PENDING when calling into the
+  // URLRequest::Delegate().
+  DCHECK(!status_.is_io_pending());
+
   int net_error = OK;
   if (!status_.is_success())
     net_error = status_.error();
diff --git a/net/url_request/url_request_http_job.cc b/net/url_request/url_request_http_job.cc
index 80c73f7..8d6d3f13 100644
--- a/net/url_request/url_request_http_job.cc
+++ b/net/url_request/url_request_http_job.cc
@@ -1236,7 +1236,6 @@
 }
 
 void URLRequestHttpJob::CancelAuth() {
-  // Proxy gets set first, then WWW.
   if (proxy_auth_state_ == AUTH_STATE_NEED_AUTH) {
     proxy_auth_state_ = AUTH_STATE_CANCELED;
   } else {
@@ -1244,26 +1243,18 @@
     server_auth_state_ = AUTH_STATE_CANCELED;
   }
 
-  // These will be reset in OnStartCompleted.
-  response_info_ = nullptr;
-  receive_headers_end_ = base::TimeTicks::Now();
-  // TODO(davidben,mmenke): We should either reset override_response_headers_
-  // here or not call NotifyHeadersReceived a second time on the same response
-  // headers. See https://crbug.com/810063.
+  // The above lines should ensure this is the case.
+  DCHECK(!NeedsAuth());
 
-  ResetTimer();
-
-  // OK, let the consumer read the error page...
+  // Let the consumer read the HTTP error page. NeedsAuth() should now return
+  // false, so NotifyHeadersComplete() should not request auth from the client
+  // again.
   //
-  // Because we set the AUTH_STATE_CANCELED flag, NeedsAuth will return false,
-  // which will cause the consumer to receive OnResponseStarted instead of
-  // OnAuthRequired.
-  //
-  // We have to do this via InvokeLater to avoid "recursing" the consumer.
-  //
+  // Have to do this via PostTask to avoid re-entrantly calling into the
+  // consumer.
   base::ThreadTaskRunnerHandle::Get()->PostTask(
-      FROM_HERE, base::BindOnce(&URLRequestHttpJob::OnStartCompleted,
-                                weak_factory_.GetWeakPtr(), OK));
+      FROM_HERE, base::BindOnce(&URLRequestHttpJob::NotifyFinalHeadersReceived,
+                                weak_factory_.GetWeakPtr()));
 }
 
 void URLRequestHttpJob::ContinueWithCertificate(
diff --git a/net/url_request/url_request_job.cc b/net/url_request/url_request_job.cc
index 3a7efea..18d56f1e 100644
--- a/net/url_request/url_request_job.cc
+++ b/net/url_request/url_request_job.cc
@@ -433,6 +433,22 @@
     }
   }
 
+  NotifyFinalHeadersReceived();
+  // |this| may be destroyed at this point.
+}
+
+void URLRequestJob::NotifyFinalHeadersReceived() {
+  DCHECK(!NeedsAuth() || !GetAuthChallengeInfo());
+
+  if (has_handled_response_)
+    return;
+
+  // While the request's status is normally updated in NotifyHeadersComplete(),
+  // URLRequestHttpJob::CancelAuth() posts a task to invoke this method
+  // directly, which bypasses that logic.
+  if (request_->status().is_io_pending())
+    request_->set_status(URLRequestStatus());
+
   has_handled_response_ = true;
   if (request_->status().is_success()) {
     DCHECK(!source_stream_);
@@ -463,7 +479,6 @@
   }
 
   request_->NotifyResponseStarted(URLRequestStatus());
-
   // |this| may be destroyed at this point.
 }
 
diff --git a/net/url_request/url_request_job.h b/net/url_request/url_request_job.h
index 5e8e307..df3eda2 100644
--- a/net/url_request/url_request_job.h
+++ b/net/url_request/url_request_job.h
@@ -276,6 +276,10 @@
   // Notifies the job that headers have been received.
   void NotifyHeadersComplete();
 
+  // Called when the final set headers have been received (no more redirects to
+  // follow, and no more auth challenges that will be responded to).
+  void NotifyFinalHeadersReceived();
+
   // Notifies the request that a start error has occurred.
   void NotifyStartError(const URLRequestStatus& status);
 
diff --git a/net/url_request/url_request_unittest.cc b/net/url_request/url_request_unittest.cc
index ecea7cb..3b23b56 100644
--- a/net/url_request/url_request_unittest.cc
+++ b/net/url_request/url_request_unittest.cc
@@ -43,6 +43,7 @@
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
+#include "base/test/bind_test_util.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/threading/thread_task_runner_handle.h"
@@ -290,6 +291,19 @@
 }
 #endif
 
+CookieList GetAllCookies(URLRequestContext* request_context) {
+  CookieList cookie_list;
+  base::RunLoop run_loop;
+  request_context->cookie_store()->GetAllCookiesAsync(
+      base::BindLambdaForTesting([&](const CookieList& cookies,
+                                     const CookieStatusList& excluded_list) {
+        cookie_list = cookies;
+        run_loop.Quit();
+      }));
+  run_loop.Run();
+  return cookie_list;
+}
+
 void TestLoadTimingCacheHitNoNetwork(
     const LoadTimingInfo& load_timing_info) {
   EXPECT_FALSE(load_timing_info.socket_reused);
@@ -8539,6 +8553,51 @@
   }
 }
 
+TEST_F(URLRequestTestHTTP, BasicAuthWithCookiesCancelAuth) {
+  ASSERT_TRUE(http_test_server()->Start());
+
+  GURL url_requiring_auth =
+      http_test_server()->GetURL("/auth-basic?set-cookie-if-challenged");
+
+  // Request a page that will give a 401 containing a Set-Cookie header.
+  // Verify that cookies are set before credentials are provided, and then
+  // cancelling auth does not result in setting the cookies again.
+  TestNetworkDelegate network_delegate;  // Must outlive URLRequest.
+  TestURLRequestContext context(true);
+  context.set_network_delegate(&network_delegate);
+  context.Init();
+
+  TestDelegate d;
+
+  EXPECT_TRUE(GetAllCookies(&context).empty());
+
+  std::unique_ptr<URLRequest> r(context.CreateRequest(
+      url_requiring_auth, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS));
+  r->set_site_for_cookies(url_requiring_auth);
+  r->Start();
+  d.RunUntilAuthRequired();
+
+  // Cookie should have been set.
+  EXPECT_EQ(1, network_delegate.set_cookie_count());
+  CookieList cookies = GetAllCookies(&context);
+  ASSERT_EQ(1u, cookies.size());
+  EXPECT_EQ("got_challenged", cookies[0].Name());
+  EXPECT_EQ("true", cookies[0].Value());
+
+  // Delete cookie.
+  context.cookie_store()->DeleteAllAsync(CookieStore::DeleteCallback());
+
+  // Cancel auth and continue the request.
+  r->CancelAuth();
+  d.RunUntilComplete();
+  ASSERT_TRUE(r->response_headers());
+  EXPECT_EQ(401, r->response_headers()->response_code());
+
+  // Cookie should not have been set again.
+  EXPECT_TRUE(GetAllCookies(&context).empty());
+  EXPECT_EQ(1, network_delegate.set_cookie_count());
+}
+
 TEST_F(URLRequestTest, CatchFilteredCookies) {
   HttpTestServer test_server;
   ASSERT_TRUE(test_server.Start());