// Copyright 2014 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 "components/domain_reliability/monitor.h"

#include <utility>

#include "base/command_line.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/profiler/scoped_tracker.h"
#include "base/single_thread_task_runner.h"
#include "base/task_runner.h"
#include "components/domain_reliability/baked_in_configs.h"
#include "components/domain_reliability/google_configs.h"
#include "components/domain_reliability/header.h"
#include "components/domain_reliability/quic_error_mapping.h"
#include "net/base/ip_endpoint.h"
#include "net/base/load_flags.h"
#include "net/base/net_errors.h"
#include "net/http/http_response_headers.h"
#include "net/url_request/url_request.h"
#include "net/url_request/url_request_context.h"
#include "net/url_request/url_request_context_getter.h"

namespace domain_reliability {

namespace {

int URLRequestStatusToNetError(const net::URLRequestStatus& status) {
  switch (status.status()) {
    case net::URLRequestStatus::SUCCESS:
      return net::OK;
    case net::URLRequestStatus::IO_PENDING:
      return net::ERR_IO_PENDING;
    case net::URLRequestStatus::CANCELED:
      return net::ERR_ABORTED;
    case net::URLRequestStatus::FAILED:
      return status.error();
    default:
      NOTREACHED();
      return net::ERR_UNEXPECTED;
  }
}

// Creates a new beacon based on |beacon_template| but fills in the status,
// chrome_error, and server_ip fields based on the endpoint and result of
// |attempt|.
//
// If there is no matching status for the result, returns false (which
// means the attempt should not result in a beacon being reported).
std::unique_ptr<DomainReliabilityBeacon> CreateBeaconFromAttempt(
    const DomainReliabilityBeacon& beacon_template,
    const net::ConnectionAttempt& attempt) {
  std::string status;
  if (!GetDomainReliabilityBeaconStatus(
          attempt.result, beacon_template.http_response_code, &status)) {
    return std::unique_ptr<DomainReliabilityBeacon>();
  }

  std::unique_ptr<DomainReliabilityBeacon> beacon(
      new DomainReliabilityBeacon(beacon_template));
  beacon->status = status;
  beacon->chrome_error = attempt.result;
  if (!attempt.endpoint.address().empty())
    beacon->server_ip = attempt.endpoint.ToString();
  else
    beacon->server_ip = "";
  return beacon;
}

const char* kDomainReliabilityHeaderName = "NEL";

}  // namespace

DomainReliabilityMonitor::DomainReliabilityMonitor(
    const std::string& upload_reporter_string,
    const scoped_refptr<base::SingleThreadTaskRunner>& pref_thread,
    const scoped_refptr<base::SingleThreadTaskRunner>& network_thread)
    : time_(new ActualTime()),
      upload_reporter_string_(upload_reporter_string),
      scheduler_params_(
          DomainReliabilityScheduler::Params::GetFromFieldTrialsOrDefaults()),
      dispatcher_(time_.get()),
      context_manager_(this),
      pref_task_runner_(pref_thread),
      network_task_runner_(network_thread),
      moved_to_network_thread_(false),
      discard_uploads_set_(false),
      weak_factory_(this) {
  DCHECK(OnPrefThread());
  net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
}

DomainReliabilityMonitor::DomainReliabilityMonitor(
    const std::string& upload_reporter_string,
    const scoped_refptr<base::SingleThreadTaskRunner>& pref_thread,
    const scoped_refptr<base::SingleThreadTaskRunner>& network_thread,
    std::unique_ptr<MockableTime> time)
    : time_(std::move(time)),
      upload_reporter_string_(upload_reporter_string),
      scheduler_params_(
          DomainReliabilityScheduler::Params::GetFromFieldTrialsOrDefaults()),
      dispatcher_(time_.get()),
      context_manager_(this),
      pref_task_runner_(pref_thread),
      network_task_runner_(network_thread),
      moved_to_network_thread_(false),
      discard_uploads_set_(false),
      weak_factory_(this) {
  DCHECK(OnPrefThread());
  net::NetworkChangeNotifier::AddNetworkChangeObserver(this);
}

DomainReliabilityMonitor::~DomainReliabilityMonitor() {
  if (moved_to_network_thread_)
    DCHECK(OnNetworkThread());
  else
    DCHECK(OnPrefThread());

  net::NetworkChangeNotifier::RemoveNetworkChangeObserver(this);
}

void DomainReliabilityMonitor::MoveToNetworkThread() {
  DCHECK(OnPrefThread());
  DCHECK(!moved_to_network_thread_);

  moved_to_network_thread_ = true;
}

void DomainReliabilityMonitor::InitURLRequestContext(
    net::URLRequestContext* url_request_context) {
  DCHECK(OnNetworkThread());
  DCHECK(moved_to_network_thread_);

  scoped_refptr<net::URLRequestContextGetter> url_request_context_getter =
      new net::TrivialURLRequestContextGetter(url_request_context,
                                              network_task_runner_);
  InitURLRequestContext(url_request_context_getter);
}

void DomainReliabilityMonitor::InitURLRequestContext(
    const scoped_refptr<net::URLRequestContextGetter>&
        url_request_context_getter) {
  DCHECK(OnNetworkThread());
  DCHECK(moved_to_network_thread_);

  // Make sure the URLRequestContext actually lives on what was declared to be
  // the network thread.
  DCHECK(url_request_context_getter->GetNetworkTaskRunner()->
         RunsTasksOnCurrentThread());

  uploader_ = DomainReliabilityUploader::Create(time_.get(),
                                                url_request_context_getter);
}

void DomainReliabilityMonitor::AddBakedInConfigs() {
  DCHECK(OnNetworkThread());
  DCHECK(moved_to_network_thread_);

  for (size_t i = 0; kBakedInJsonConfigs[i]; ++i) {
    base::StringPiece json(kBakedInJsonConfigs[i]);
    std::unique_ptr<const DomainReliabilityConfig> config =
        DomainReliabilityConfig::FromJSON(json);
    if (!config) {
      DLOG(WARNING) << "Baked-in Domain Reliability config failed to parse: "
                    << json;
      continue;
    }
    context_manager_.AddContextForConfig(std::move(config));
  }

  std::vector<std::unique_ptr<DomainReliabilityConfig>> google_configs;
  GetAllGoogleConfigs(&google_configs);
  for (auto& google_config : google_configs)
    context_manager_.AddContextForConfig(std::move(google_config));
}

void DomainReliabilityMonitor::SetDiscardUploads(bool discard_uploads) {
  DCHECK(OnNetworkThread());
  DCHECK(moved_to_network_thread_);
  DCHECK(uploader_);

  uploader_->set_discard_uploads(discard_uploads);
  discard_uploads_set_ = true;
}

void DomainReliabilityMonitor::OnBeforeRedirect(net::URLRequest* request) {
  DCHECK(OnNetworkThread());
  DCHECK(discard_uploads_set_);

  // Record the redirect itself in addition to the final request.
  OnRequestLegComplete(RequestInfo(*request));
}

void DomainReliabilityMonitor::OnCompleted(net::URLRequest* request,
                                           bool started) {
  DCHECK(OnNetworkThread());
  DCHECK(discard_uploads_set_);

  if (!started)
    return;
  RequestInfo request_info(*request);
  OnRequestLegComplete(request_info);

  if (request_info.response_info.network_accessed) {
    // A request was just using the network, so now is a good time to run any
    // pending and eligible uploads.
    dispatcher_.RunEligibleTasks();
  }
}

void DomainReliabilityMonitor::OnNetworkChanged(
    net::NetworkChangeNotifier::ConnectionType type) {
  last_network_change_time_ = time_->NowTicks();
}

void DomainReliabilityMonitor::ClearBrowsingData(
   DomainReliabilityClearMode mode,
   const base::Callback<bool(const GURL&)>& origin_filter) {
  DCHECK(OnNetworkThread());

  switch (mode) {
    case CLEAR_BEACONS:
      context_manager_.ClearBeacons(origin_filter);
      break;
    case CLEAR_CONTEXTS:
      context_manager_.RemoveContexts(origin_filter);
      break;
    case MAX_CLEAR_MODE:
      NOTREACHED();
  }
}

std::unique_ptr<base::Value> DomainReliabilityMonitor::GetWebUIData() const {
  DCHECK(OnNetworkThread());

  std::unique_ptr<base::DictionaryValue> data_value(
      new base::DictionaryValue());
  data_value->Set("contexts", context_manager_.GetWebUIData());
  return std::move(data_value);
}

DomainReliabilityContext* DomainReliabilityMonitor::AddContextForTesting(
    std::unique_ptr<const DomainReliabilityConfig> config) {
  DCHECK(OnNetworkThread());

  return context_manager_.AddContextForConfig(std::move(config));
}

std::unique_ptr<DomainReliabilityContext>
DomainReliabilityMonitor::CreateContextForConfig(
    std::unique_ptr<const DomainReliabilityConfig> config) {
  DCHECK(OnNetworkThread());
  DCHECK(config);
  DCHECK(config->IsValid());

  return base::MakeUnique<DomainReliabilityContext>(
      time_.get(), scheduler_params_, upload_reporter_string_,
      &last_network_change_time_, &dispatcher_, uploader_.get(),
      std::move(config));
}

DomainReliabilityMonitor::RequestInfo::RequestInfo() {}

DomainReliabilityMonitor::RequestInfo::RequestInfo(
    const net::URLRequest& request)
    : url(request.url()),
      status(request.status()),
      response_info(request.response_info()),
      load_flags(request.load_flags()),
      upload_depth(
          DomainReliabilityUploader::GetURLRequestUploadDepth(request)) {
  request.GetLoadTimingInfo(&load_timing_info);
  request.GetConnectionAttempts(&connection_attempts);
  request.PopulateNetErrorDetails(&details);
  if (!request.GetRemoteEndpoint(&remote_endpoint))
    remote_endpoint = net::IPEndPoint();
}

DomainReliabilityMonitor::RequestInfo::RequestInfo(const RequestInfo& other) =
    default;

DomainReliabilityMonitor::RequestInfo::~RequestInfo() {}

// static
bool DomainReliabilityMonitor::RequestInfo::ShouldReportRequest(
    const DomainReliabilityMonitor::RequestInfo& request) {
  // Don't report requests that weren't supposed to send cookies.
  if (request.load_flags & net::LOAD_DO_NOT_SEND_COOKIES)
    return false;

  // Report requests that accessed the network or failed with an error code
  // that Domain Reliability is interested in.
  if (request.response_info.network_accessed)
    return true;
  if (URLRequestStatusToNetError(request.status) != net::OK)
    return true;
  if (request.details.quic_port_migration_detected)
    return true;

  return false;
}

void DomainReliabilityMonitor::OnRequestLegComplete(
    const RequestInfo& request) {
  // Check these again because unit tests call this directly.
  DCHECK(OnNetworkThread());
  DCHECK(discard_uploads_set_);

  MaybeHandleHeader(request);

  if (!RequestInfo::ShouldReportRequest(request))
    return;

  int response_code;
  if (request.response_info.headers.get())
    response_code = request.response_info.headers->response_code();
  else
    response_code = -1;

  net::ConnectionAttempt url_request_attempt(
      request.remote_endpoint, URLRequestStatusToNetError(request.status));

  DomainReliabilityBeacon beacon_template;
  if (request.response_info.connection_info !=
      net::HttpResponseInfo::CONNECTION_INFO_UNKNOWN) {
    beacon_template.protocol =
        GetDomainReliabilityProtocol(request.response_info.connection_info,
                                     request.response_info.ssl_info.is_valid());
  } else {
    // Use the connection info from the network error details if the response
    // is unavailable.
    beacon_template.protocol =
        GetDomainReliabilityProtocol(request.details.connection_info,
                                     request.response_info.ssl_info.is_valid());
  }
  GetDomainReliabilityBeaconQuicError(request.details.quic_connection_error,
                                      &beacon_template.quic_error);
  beacon_template.http_response_code = response_code;
  beacon_template.start_time = request.load_timing_info.request_start;
  beacon_template.elapsed = time_->NowTicks() - beacon_template.start_time;
  beacon_template.was_proxied = request.response_info.was_fetched_via_proxy;
  beacon_template.url = request.url;
  beacon_template.upload_depth = request.upload_depth;
  beacon_template.details = request.details;

  // This is not foolproof -- it's possible that we'll see the same error twice
  // (e.g. an SSL error during connection on one attempt, and then an error
  // that maps to the same code during a read).
  // TODO(juliatuttle): Find a way for this code to reliably tell whether we
  // eventually established a connection or not.
  bool url_request_attempt_is_duplicate = false;
  for (const auto& attempt : request.connection_attempts) {
    if (attempt.result == url_request_attempt.result)
      url_request_attempt_is_duplicate = true;

    std::unique_ptr<DomainReliabilityBeacon> beacon =
        CreateBeaconFromAttempt(beacon_template, attempt);
    if (beacon)
      context_manager_.RouteBeacon(std::move(beacon));
  }

  if (url_request_attempt_is_duplicate)
    return;

  std::unique_ptr<DomainReliabilityBeacon> beacon =
      CreateBeaconFromAttempt(beacon_template, url_request_attempt);
  if (beacon)
    context_manager_.RouteBeacon(std::move(beacon));
}

void DomainReliabilityMonitor::MaybeHandleHeader(
    const RequestInfo& request) {
  if (!request.response_info.headers.get())
    return;

  size_t iter = 0;
  std::string kHeaderNameString(kDomainReliabilityHeaderName);

  std::string header_value;
  if (!request.response_info.headers->EnumerateHeader(
          &iter, kHeaderNameString, &header_value)) {
    // No header found.
    return;
  }

  std::string ignored_header_value;
  if (request.response_info.headers->EnumerateHeader(
          &iter, kHeaderNameString, &ignored_header_value)) {
    LOG(WARNING) << "Request to " << request.url << " had (at least) two "
                 << kHeaderNameString << " headers: \"" << header_value
                 << "\" and \"" << ignored_header_value << "\".";
    return;
  }

  std::unique_ptr<DomainReliabilityHeader> parsed =
      DomainReliabilityHeader::Parse(header_value);
  GURL origin = request.url.GetOrigin();
  switch (parsed->status()) {
    case DomainReliabilityHeader::PARSE_SET_CONFIG:
      {
        base::TimeDelta max_age = parsed->max_age();
        context_manager_.SetConfig(origin, parsed->ReleaseConfig(), max_age);
      }
      break;
    case DomainReliabilityHeader::PARSE_CLEAR_CONFIG:
      context_manager_.ClearConfig(origin);
      break;
    case DomainReliabilityHeader::PARSE_ERROR:
      LOG(WARNING) << "Request to " << request.url << " had invalid "
                   << kHeaderNameString << " header \"" << header_value
                   << "\".";
      break;
  }
}

base::WeakPtr<DomainReliabilityMonitor>
DomainReliabilityMonitor::MakeWeakPtr() {
  return weak_factory_.GetWeakPtr();
}

}  // namespace domain_reliability
