// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/loader/async_revalidation_manager.h"

#include <deque>
#include <string>
#include <utility>

#include "base/bind.h"
#include "base/callback.h"
#include "base/macros.h"
#include "base/memory/shared_memory_handle.h"
#include "base/pickle.h"
#include "base/run_loop.h"
#include "base/strings/string_util.h"
#include "content/browser/child_process_security_policy_impl.h"
#include "content/browser/loader/resource_dispatcher_host_impl.h"
#include "content/browser/loader/resource_message_filter.h"
#include "content/common/child_process_host_impl.h"
#include "content/common/resource_messages.h"
#include "content/public/browser/resource_context.h"
#include "content/public/common/appcache_info.h"
#include "content/public/common/process_type.h"
#include "content/public/common/resource_type.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "ipc/ipc_param_traits.h"
#include "net/base/load_flags.h"
#include "net/base/network_delegate.h"
#include "net/http/http_util.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_job.h"
#include "net/url_request/url_request_job_factory.h"
#include "net/url_request/url_request_test_job.h"
#include "net/url_request/url_request_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/page_transition_types.h"
#include "url/gurl.h"
#include "url/url_constants.h"

namespace content {

namespace {

// This class is a variation on URLRequestTestJob that
// returns ERR_IO_PENDING before every read, not just the first one.
class URLRequestTestDelayedCompletionJob : public net::URLRequestTestJob {
 public:
  URLRequestTestDelayedCompletionJob(net::URLRequest* request,
                                     net::NetworkDelegate* network_delegate,
                                     const std::string& response_headers,
                                     const std::string& response_data)
      : net::URLRequestTestJob(request,
                               network_delegate,
                               response_headers,
                               response_data,
                               false) {}

 private:
  bool NextReadAsync() override { return true; }
};

// A URLRequestJob implementation which sets the |async_revalidation_required|
// flag on the HttpResponseInfo object to true if the request has the
// LOAD_SUPPORT_ASYNC_REVALIDATION flag.
class AsyncRevalidationRequiredURLRequestTestJob
    : public net::URLRequestTestJob {
 public:
  AsyncRevalidationRequiredURLRequestTestJob(
      net::URLRequest* request,
      net::NetworkDelegate* network_delegate)
      : URLRequestTestJob(request,
                          network_delegate,
                          net::URLRequestTestJob::test_headers(),
                          std::string(),
                          false) {}

  void GetResponseInfo(net::HttpResponseInfo* info) override {
    URLRequestTestJob::GetResponseInfo(info);
    if (request()->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION)
      info->async_revalidation_required = true;
  }
};

// A URLRequestJob implementation which serves a redirect and sets the
// |async_revalidation_required| flag on the HttpResponseInfo object to true if
// the request has the LOAD_SUPPORT_ASYNC_REVALIDATION flag.
class RedirectAndRevalidateURLRequestTestJob : public net::URLRequestTestJob {
 public:
  RedirectAndRevalidateURLRequestTestJob(net::URLRequest* request,
                                         net::NetworkDelegate* network_delegate)
      : URLRequestTestJob(request,
                          network_delegate,
                          CreateRedirectHeaders(),
                          std::string(),
                          false) {}

  void GetResponseInfo(net::HttpResponseInfo* info) override {
    URLRequestTestJob::GetResponseInfo(info);
    if (request()->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION)
      info->async_revalidation_required = true;
  }

 private:
  static std::string CreateRedirectHeaders() {
    static const char kRedirectHeaders[] =
        "HTTP/1.1 302 MOVED\n"
        "Location: http://example.com/async-revalidate/from-redirect\n"
        "\n";
    return std::string(kRedirectHeaders, arraysize(kRedirectHeaders) - 1);
  }
};

class TestURLRequestJobFactory : public net::URLRequestJobFactory {
 public:
  TestURLRequestJobFactory() = default;

  // Sets the contents of the response. |headers| should have "\n" as line
  // breaks and end in "\n\n".
  void SetResponse(const std::string& headers, const std::string& data) {
    response_headers_ = headers;
    response_data_ = data;
  }

  net::URLRequestJob* MaybeCreateJobWithProtocolHandler(
      const std::string& scheme,
      net::URLRequest* request,
      net::NetworkDelegate* network_delegate) const override {
    std::string path = request->url().path();
    if (base::StartsWith(path, "/async-revalidate",
                         base::CompareCase::SENSITIVE)) {
      return new AsyncRevalidationRequiredURLRequestTestJob(request,
                                                            network_delegate);
    }
    if (base::StartsWith(path, "/redirect", base::CompareCase::SENSITIVE)) {
      return new RedirectAndRevalidateURLRequestTestJob(request,
                                                        network_delegate);
    }
    return new URLRequestTestDelayedCompletionJob(
        request, network_delegate, response_headers_, response_data_);
  }

  net::URLRequestJob* MaybeInterceptRedirect(
      net::URLRequest* request,
      net::NetworkDelegate* network_delegate,
      const GURL& location) const override {
    return nullptr;
  }

  net::URLRequestJob* MaybeInterceptResponse(
      net::URLRequest* request,
      net::NetworkDelegate* network_delegate) const override {
    return nullptr;
  }

  bool IsHandledProtocol(const std::string& scheme) const override {
    // If non-standard schemes need to be tested in future it will be
    // necessary to call ChildProcessSecurityPolicyImpl::
    // RegisterWebSafeScheme() for them.
    return scheme == url::kHttpScheme || scheme == url::kHttpsScheme;
  }

  bool IsHandledURL(const GURL& url) const override {
    return IsHandledProtocol(url.scheme());
  }

  bool IsSafeRedirectTarget(const GURL& location) const override {
    return false;
  }

 private:
  std::string response_headers_;
  std::string response_data_;

  DISALLOW_COPY_AND_ASSIGN(TestURLRequestJobFactory);
};

// On Windows, ResourceMsg_SetDataBuffer supplies a HANDLE which is not
// automatically released.
//
// See ResourceDispatcher::ReleaseResourcesInDataMessage.
//
// TODO(ricea): Maybe share this implementation with
// resource_dispatcher_host_unittest.cc.
void ReleaseHandlesInMessage(const IPC::Message& message) {
  if (message.type() == ResourceMsg_SetDataBuffer::ID) {
    base::PickleIterator iter(message);
    int request_id;
    CHECK(iter.ReadInt(&request_id));
    base::SharedMemoryHandle shm_handle;
    if (IPC::ParamTraits<base::SharedMemoryHandle>::Read(&message, &iter,
                                                         &shm_handle)) {
      if (base::SharedMemory::IsHandleValid(shm_handle))
        base::SharedMemory::CloseHandle(shm_handle);
    }
  }
}

// This filter just deletes any messages that are sent through it.
class BlackholeFilter : public ResourceMessageFilter {
 public:
  explicit BlackholeFilter(ResourceContext* resource_context)
      : ResourceMessageFilter(
            ChildProcessHostImpl::GenerateChildProcessUniqueId(),
            PROCESS_TYPE_RENDERER,
            nullptr,
            nullptr,
            nullptr,
            nullptr,
            nullptr,
            base::Bind(&BlackholeFilter::GetContexts, base::Unretained(this))),
        resource_context_(resource_context) {
    ChildProcessSecurityPolicyImpl::GetInstance()->Add(child_id());
  }

  bool Send(IPC::Message* msg) override {
    scoped_ptr<IPC::Message> take_ownership(msg);
    ReleaseHandlesInMessage(*msg);
    return true;
  }

 private:
  ~BlackholeFilter() override {
    ChildProcessSecurityPolicyImpl::GetInstance()->Remove(child_id());
  }

  void GetContexts(ResourceType resource_type,
                   int origin_pid,
                   ResourceContext** resource_context,
                   net::URLRequestContext** request_context) {
    *resource_context = resource_context_;
    *request_context = resource_context_->GetRequestContext();
  }

  ResourceContext* resource_context_;

  DISALLOW_COPY_AND_ASSIGN(BlackholeFilter);
};

ResourceHostMsg_Request CreateResourceRequest(const char* method,
                                              ResourceType type,
                                              const GURL& url) {
  ResourceHostMsg_Request request;
  request.method = std::string(method);
  request.url = url;
  request.first_party_for_cookies = url;  // Bypass third-party cookie blocking.
  request.referrer_policy = blink::WebReferrerPolicyDefault;
  request.load_flags = 0;
  request.origin_pid = 0;
  request.resource_type = type;
  request.request_context = 0;
  request.appcache_host_id = kAppCacheNoHostId;
  request.download_to_file = false;
  request.should_reset_appcache = false;
  request.is_main_frame = true;
  request.parent_is_main_frame = false;
  request.parent_render_frame_id = -1;
  request.transition_type = ui::PAGE_TRANSITION_LINK;
  request.allow_download = true;
  return request;
}

class AsyncRevalidationManagerTest : public ::testing::Test {
 protected:
  AsyncRevalidationManagerTest(
      scoped_ptr<net::TestNetworkDelegate> network_delegate)
      : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
        network_delegate_(std::move(network_delegate)) {
    browser_context_.reset(new TestBrowserContext());
    BrowserContext::EnsureResourceContextInitialized(browser_context_.get());
    base::RunLoop().RunUntilIdle();
    ResourceContext* resource_context = browser_context_->GetResourceContext();
    filter_ = new BlackholeFilter(resource_context);
    net::URLRequestContext* request_context =
        resource_context->GetRequestContext();
    job_factory_.reset(new TestURLRequestJobFactory);
    request_context->set_job_factory(job_factory_.get());
    request_context->set_network_delegate(network_delegate_.get());
    host_.EnableStaleWhileRevalidateForTesting();
  }

  AsyncRevalidationManagerTest()
      : AsyncRevalidationManagerTest(
            make_scoped_ptr(new net::TestNetworkDelegate)) {}

  void TearDown() override {
    host_.CancelRequestsForProcess(filter_->child_id());
    host_.Shutdown();
    host_.CancelRequestsForContext(browser_context_->GetResourceContext());
    browser_context_.reset();
    base::RunLoop().RunUntilIdle();
  }

  void SetResponse(const std::string& headers, const std::string& data) {
    job_factory_->SetResponse(headers, data);
  }

  // Creates a request using the current test object as the filter and
  // SubResource as the resource type.
  void MakeTestRequest(int render_view_id, int request_id, const GURL& url) {
    ResourceHostMsg_Request request =
        CreateResourceRequest("GET", RESOURCE_TYPE_SUB_RESOURCE, url);
    ResourceHostMsg_RequestResource msg(render_view_id, request_id, request);
    host_.OnMessageReceived(msg, filter_.get());
    base::RunLoop().RunUntilIdle();
  }

  void EnsureSchemeIsAllowed(const std::string& scheme) {
    ChildProcessSecurityPolicyImpl* policy =
        ChildProcessSecurityPolicyImpl::GetInstance();
    if (!policy->IsWebSafeScheme(scheme))
      policy->RegisterWebSafeScheme(scheme);
  }

  content::TestBrowserThreadBundle thread_bundle_;
  scoped_ptr<TestBrowserContext> browser_context_;
  scoped_ptr<TestURLRequestJobFactory> job_factory_;
  scoped_refptr<BlackholeFilter> filter_;
  scoped_ptr<net::TestNetworkDelegate> network_delegate_;
  ResourceDispatcherHostImpl host_;
};

TEST_F(AsyncRevalidationManagerTest, SupportsAsyncRevalidation) {
  SetResponse(net::URLRequestTestJob::test_headers(), "delay complete");
  MakeTestRequest(0, 1, GURL("http://example.com/baz"));

  net::URLRequest* url_request(
      host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
  ASSERT_TRUE(url_request);

  EXPECT_TRUE(url_request->load_flags() & net::LOAD_SUPPORT_ASYNC_REVALIDATION);
}

TEST_F(AsyncRevalidationManagerTest, AsyncRevalidationNotSupportedForPOST) {
  SetResponse(net::URLRequestTestJob::test_headers(), "delay complete");
  // Create POST request.
  ResourceHostMsg_Request request = CreateResourceRequest(
      "POST", RESOURCE_TYPE_SUB_RESOURCE, GURL("http://example.com/baz.php"));
  ResourceHostMsg_RequestResource msg(0, 1, request);
  host_.OnMessageReceived(msg, filter_.get());
  base::RunLoop().RunUntilIdle();

  net::URLRequest* url_request(
      host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
  ASSERT_TRUE(url_request);

  EXPECT_FALSE(url_request->load_flags() &
               net::LOAD_SUPPORT_ASYNC_REVALIDATION);
}

TEST_F(AsyncRevalidationManagerTest,
       AsyncRevalidationNotSupportedAfterRedirect) {
  static const char kRedirectHeaders[] =
      "HTTP/1.1 302 MOVED\n"
      "Location: http://example.com/var\n"
      "\n";
  SetResponse(kRedirectHeaders, "");

  MakeTestRequest(0, 1, GURL("http://example.com/baz"));

  net::URLRequest* url_request(
      host_.GetURLRequest(GlobalRequestID(filter_->child_id(), 1)));
  ASSERT_TRUE(url_request);

  EXPECT_FALSE(url_request->load_flags() &
               net::LOAD_SUPPORT_ASYNC_REVALIDATION);
}

// A NetworkDelegate that records the URLRequests as they are created.
class URLRequestRecordingNetworkDelegate : public net::TestNetworkDelegate {
 public:
  URLRequestRecordingNetworkDelegate() : requests_() {}

  net::URLRequest* NextRequest() {
    EXPECT_FALSE(requests_.empty());
    if (requests_.empty())
      return nullptr;
    net::URLRequest* request = requests_.front();
    EXPECT_TRUE(request);
    requests_.pop_front();
    return request;
  }

  bool NextRequestWasDestroyed() {
    net::URLRequest* request = requests_.front();
    requests_.pop_front();
    return request == nullptr;
  }

  bool IsEmpty() const { return requests_.empty(); }

  int OnBeforeURLRequest(net::URLRequest* request,
                         const net::CompletionCallback& callback,
                         GURL* new_url) override {
    requests_.push_back(request);
    return TestNetworkDelegate::OnBeforeURLRequest(request, callback, new_url);
  }

  void OnURLRequestDestroyed(net::URLRequest* request) override {
    for (auto& recorded_request : requests_) {
      if (recorded_request == request)
        recorded_request = nullptr;
    }
    net::TestNetworkDelegate::OnURLRequestDestroyed(request);
  }

 private:
  std::deque<net::URLRequest*> requests_;

  DISALLOW_COPY_AND_ASSIGN(URLRequestRecordingNetworkDelegate);
};

class AsyncRevalidationManagerRecordingTest
    : public AsyncRevalidationManagerTest {
 public:
  AsyncRevalidationManagerRecordingTest()
      : AsyncRevalidationManagerTest(
            make_scoped_ptr(new URLRequestRecordingNetworkDelegate)) {}

  void TearDown() override {
    EXPECT_TRUE(IsEmpty());
    AsyncRevalidationManagerTest::TearDown();
  }

  URLRequestRecordingNetworkDelegate* recording_network_delegate() const {
    return static_cast<URLRequestRecordingNetworkDelegate*>(
        network_delegate_.get());
  }

  bool NextRequestWasDestroyed() {
    return recording_network_delegate()->NextRequestWasDestroyed();
  }

  net::URLRequest* NextRequest() {
    return recording_network_delegate()->NextRequest();
  }

  bool IsEmpty() const { return recording_network_delegate()->IsEmpty(); }
};

// Verify that an async revalidation is actually created when needed.
TEST_F(AsyncRevalidationManagerRecordingTest, Issued) {
  // Create the original request.
  MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));

  net::URLRequest* initial_request = NextRequest();
  ASSERT_TRUE(initial_request);
  EXPECT_TRUE(initial_request->load_flags() &
              net::LOAD_SUPPORT_ASYNC_REVALIDATION);

  net::URLRequest* async_request = NextRequest();
  ASSERT_TRUE(async_request);
}

// Verify the the URL of the async revalidation matches the original request.
TEST_F(AsyncRevalidationManagerRecordingTest, URLMatches) {
  // Create the original request.
  MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate/u"));

  // Discard the original request.
  NextRequest();

  net::URLRequest* async_request = NextRequest();
  ASSERT_TRUE(async_request);
  EXPECT_EQ(GURL("http://example.com/async-revalidate/u"),
            async_request->url());
}

TEST_F(AsyncRevalidationManagerRecordingTest,
       AsyncRevalidationsDoNotSupportAsyncRevalidation) {
  // Create the original request.
  MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));

  // Discard the original request.
  NextRequest();

  // Get the async revalidation request.
  net::URLRequest* async_request = NextRequest();
  ASSERT_TRUE(async_request);
  EXPECT_FALSE(async_request->load_flags() &
               net::LOAD_SUPPORT_ASYNC_REVALIDATION);
}

TEST_F(AsyncRevalidationManagerRecordingTest, AsyncRevalidationsNotDuplicated) {
  // Create the original request.
  MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));

  // Discard the original request.
  NextRequest();

  // Get the async revalidation request.
  net::URLRequest* async_request = NextRequest();
  EXPECT_TRUE(async_request);

  // Start a second request to the same URL.
  MakeTestRequest(0, 2, GURL("http://example.com/async-revalidate"));

  // Discard the second request.
  NextRequest();

  // There should not be another async revalidation request.
  EXPECT_TRUE(IsEmpty());
}

// Async revalidation to different URLs should not be treated as duplicates.
TEST_F(AsyncRevalidationManagerRecordingTest,
       AsyncRevalidationsToSeparateURLsAreSeparate) {
  // Create two requests to two URLs.
  MakeTestRequest(0, 1, GURL("http://example.com/async-revalidate"));
  MakeTestRequest(0, 2, GURL("http://example.com/async-revalidate2"));

  net::URLRequest* initial_request = NextRequest();
  ASSERT_TRUE(initial_request);
  net::URLRequest* initial_async_revalidation = NextRequest();
  ASSERT_TRUE(initial_async_revalidation);
  net::URLRequest* second_request = NextRequest();
  ASSERT_TRUE(second_request);
  net::URLRequest* second_async_revalidation = NextRequest();
  ASSERT_TRUE(second_async_revalidation);

  EXPECT_EQ("http://example.com/async-revalidate",
            initial_request->url().spec());
  EXPECT_EQ("http://example.com/async-revalidate",
            initial_async_revalidation->url().spec());
  EXPECT_EQ("http://example.com/async-revalidate2",
            second_request->url().spec());
  EXPECT_EQ("http://example.com/async-revalidate2",
            second_async_revalidation->url().spec());
}

// Nothing after the first redirect leg has stale-while-revalidate applied.
// TODO(ricea): s-w-r should work with redirects. Change this test when it does.
TEST_F(AsyncRevalidationManagerRecordingTest, NoSWRAfterFirstRedirectLeg) {
  MakeTestRequest(0, 1, GURL("http://example.com/redirect"));

  net::URLRequest* initial_request = NextRequest();
  EXPECT_TRUE(initial_request);

  EXPECT_FALSE(initial_request->load_flags() &
               net::LOAD_SUPPORT_ASYNC_REVALIDATION);

  // An async revalidation happens for the redirect. It has no body, so it has
  // already completed.
  EXPECT_TRUE(NextRequestWasDestroyed());

  // But no others.
  EXPECT_TRUE(IsEmpty());
}

}  // namespace

}  // namespace content
