// Copyright 2017 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/public/browser/network_service_instance.h"

#include <map>
#include <memory>
#include <string>
#include <utility>

#include "base/deferred_sequenced_task_runner.h"
#include "base/environment.h"
#include "base/feature_list.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "build/build_config.h"
#include "content/browser/browser_main_loop.h"
#include "content/browser/network_service_client.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/network_service_util.h"
#include "content/public/common/service_manager_connection.h"
#include "content/public/common/service_names.mojom.h"
#include "net/log/net_log_util.h"
#include "services/network/network_service.h"
#include "services/network/public/cpp/features.h"
#include "services/network/public/cpp/network_switches.h"
#include "services/network/public/mojom/net_log.mojom.h"
#include "services/network/public/mojom/network_change_manager.mojom.h"
#include "services/service_manager/public/cpp/connector.h"

namespace content {

namespace {

network::mojom::NetworkServicePtr* g_network_service_ptr = nullptr;
network::NetworkConnectionTracker* g_network_connection_tracker;
network::NetworkService* g_network_service;

network::mojom::NetworkServiceParamsPtr CreateNetworkServiceParams() {
  network::mojom::NetworkServiceParamsPtr network_service_params =
      network::mojom::NetworkServiceParams::New();
  network_service_params->initial_connection_type =
      network::mojom::ConnectionType(
          net::NetworkChangeNotifier::GetConnectionType());
  network_service_params->initial_connection_subtype =
      network::mojom::ConnectionSubtype(
          net::NetworkChangeNotifier::GetConnectionSubtype());
  return network_service_params;
}

void CreateNetworkServiceOnIO(network::mojom::NetworkServiceRequest request) {
  if (g_network_service) {
    // GetNetworkServiceImpl() was already called and created the object, so
    // just bind it.
    g_network_service->Bind(std::move(request));
    return;
  }

  g_network_service = new network::NetworkService(
      nullptr, std::move(request), GetContentClient()->browser()->GetNetLog());
}

void BindNetworkChangeManagerRequest(
    network::mojom::NetworkChangeManagerRequest request) {
  GetNetworkService()->GetNetworkChangeManager(std::move(request));
}

base::CallbackList<void()>& GetCrashHandlersList() {
  static base::NoDestructor<base::CallbackList<void()>> s_list;
  return *s_list;
}

void OnNetworkServiceCrash() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(g_network_service_ptr);
  DCHECK(g_network_service_ptr->is_bound());
  DCHECK(g_network_service_ptr->encountered_error());
  GetCrashHandlersList().Notify();
}

}  // namespace

network::mojom::NetworkService* GetNetworkService() {
  service_manager::Connector* connector =
      base::FeatureList::IsEnabled(network::features::kNetworkService)
          ? ServiceManagerConnection::GetForProcess()->GetConnector()
          : nullptr;
  return GetNetworkServiceFromConnector(connector);
}

CONTENT_EXPORT network::mojom::NetworkService* GetNetworkServiceFromConnector(
    service_manager::Connector* connector) {
  const bool is_network_service_enabled =
      base::FeatureList::IsEnabled(network::features::kNetworkService);
  // The DCHECK for thread is only done without network service enabled. This is
  // because the connector and the pre-existing |g_network_service_ptr| are
  // bound to the right thread in the network service case, and this allows
  // Android to instantiate the NetworkService before UI thread is promoted to
  // BrowserThread::UI.
  if (!is_network_service_enabled)
    DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  if (!g_network_service_ptr)
    g_network_service_ptr = new network::mojom::NetworkServicePtr;
  static NetworkServiceClient* g_client;
  if (!g_network_service_ptr->is_bound() ||
      g_network_service_ptr->encountered_error()) {
    if (is_network_service_enabled) {
      connector->BindInterface(mojom::kNetworkServiceName,
                               g_network_service_ptr);
      g_network_service_ptr->set_connection_error_handler(
          base::BindOnce(&OnNetworkServiceCrash));
    } else {
      DCHECK(!g_network_service_ptr->is_bound());
      base::PostTaskWithTraits(
          FROM_HERE, {BrowserThread::IO},
          base::BindOnce(CreateNetworkServiceOnIO,
                         mojo::MakeRequest(g_network_service_ptr)));
    }

    network::mojom::NetworkServiceClientPtr client_ptr;
    auto client_request = mojo::MakeRequest(&client_ptr);
    // Call SetClient before creating NetworkServiceClient, as the latter might
    // make requests to NetworkService that depend on initialization.
    (*g_network_service_ptr)
        ->SetClient(std::move(client_ptr), CreateNetworkServiceParams());

    delete g_client;  // In case we're recreating the network service.
    g_client = new NetworkServiceClient(std::move(client_request));

      const base::CommandLine* command_line =
          base::CommandLine::ForCurrentProcess();
      if (is_network_service_enabled) {
        if (command_line->HasSwitch(network::switches::kLogNetLog)) {
          base::FilePath log_path =
              command_line->GetSwitchValuePath(network::switches::kLogNetLog);

          base::DictionaryValue client_constants =
              GetContentClient()->GetNetLogConstants();

          base::File file(log_path, base::File::FLAG_CREATE_ALWAYS |
                                        base::File::FLAG_WRITE);
          if (!file.IsValid()) {
            LOG(ERROR) << "Failed opening: " << log_path.value();
          } else {
            // TODO(mmenke): Get capture mode from the command line.
            (*g_network_service_ptr)
                ->StartNetLog(std::move(file),
                              network::mojom::NetLogCaptureMode::DEFAULT,
                              std::move(client_constants));
          }
        }
      }

      if (command_line->HasSwitch(network::switches::kSSLKeyLogFile)) {
        base::FilePath log_path =
            command_line->GetSwitchValuePath(network::switches::kSSLKeyLogFile);
        LOG_IF(WARNING, log_path.empty())
            << "ssl-key-log-file argument missing";
        if (!log_path.empty())
          (*g_network_service_ptr)->SetSSLKeyLogFile(log_path);
      }

      std::unique_ptr<base::Environment> env(base::Environment::Create());
      std::string env_str;
      if (env->GetVar("SSLKEYLOGFILE", &env_str)) {
#if defined(OS_WIN)
        // base::Environment returns environment variables in UTF-8 on Windows.
        base::FilePath log_path(base::UTF8ToUTF16(env_str));
#else
        base::FilePath log_path(env_str);
#endif
        if (!log_path.empty())
          (*g_network_service_ptr)->SetSSLKeyLogFile(log_path);
      }

      GetContentClient()->browser()->OnNetworkServiceCreated(
          g_network_service_ptr->get());
  }
  return g_network_service_ptr->get();
}

std::unique_ptr<base::CallbackList<void()>::Subscription>
RegisterNetworkServiceCrashHandler(base::RepeatingClosure handler) {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
  DCHECK(!handler.is_null());

  if (base::FeatureList::IsEnabled(network::features::kNetworkService))
    return GetCrashHandlersList().Add(std::move(handler));

  return nullptr;
}

network::NetworkService* GetNetworkServiceImpl() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO));
  DCHECK(!base::FeatureList::IsEnabled(network::features::kNetworkService));
  if (!g_network_service) {
    g_network_service = new network::NetworkService(
        nullptr, nullptr, GetContentClient()->browser()->GetNetLog());
  }

  return g_network_service;
}

net::NetworkChangeNotifier* GetNetworkChangeNotifier() {
  return BrowserMainLoop::GetInstance()->network_change_notifier();
}

void FlushNetworkServiceInstanceForTesting() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));

  if (g_network_service_ptr)
    g_network_service_ptr->FlushForTesting();
}

network::NetworkConnectionTracker* GetNetworkConnectionTracker() {
  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI) ||
         !BrowserThread::IsThreadInitialized(BrowserThread::UI));
  if (!g_network_connection_tracker) {
    g_network_connection_tracker = new network::NetworkConnectionTracker(
        base::BindRepeating(&BindNetworkChangeManagerRequest));
  }
  return g_network_connection_tracker;
}

void GetNetworkConnectionTrackerFromUIThread(
    base::OnceCallback<void(network::NetworkConnectionTracker*)> callback) {
  // TODO(fdoray): Investigate why this is needed. The IO thread is supposed to
  // be initialized by the time the UI thread starts running tasks.
  //
  // GetNetworkConnectionTracker() will call CreateNetworkServiceOnIO(). Here it
  // makes sure the IO thread is running when CreateNetworkServiceOnIO() is
  // called.
  if (!content::BrowserThread::IsThreadInitialized(
          content::BrowserThread::IO)) {
    // IO thread is not yet initialized. Try again in the next message pump.
    bool task_posted = base::ThreadTaskRunnerHandle::Get()->PostTask(
        FROM_HERE, base::BindOnce(&GetNetworkConnectionTrackerFromUIThread,
                                  std::move(callback)));
    DCHECK(task_posted);
    return;
  }

  base::PostTaskWithTraitsAndReplyWithResult(
      FROM_HERE, {BrowserThread::UI, base::TaskPriority::BEST_EFFORT},
      base::BindOnce(&GetNetworkConnectionTracker), std::move(callback));
}

network::NetworkConnectionTrackerAsyncGetter
CreateNetworkConnectionTrackerAsyncGetter() {
  return base::BindRepeating(&content::GetNetworkConnectionTrackerFromUIThread);
}

void SetNetworkConnectionTrackerForTesting(
    network::NetworkConnectionTracker* network_connection_tracker) {
  if (g_network_connection_tracker != network_connection_tracker) {
    DCHECK(!g_network_connection_tracker || !network_connection_tracker);
    g_network_connection_tracker = network_connection_tracker;
  }
}

scoped_refptr<base::DeferredSequencedTaskRunner> GetNetworkTaskRunner() {
  DCHECK(IsInProcessNetworkService());
  static base::NoDestructor<scoped_refptr<base::DeferredSequencedTaskRunner>>
      instance(new base::DeferredSequencedTaskRunner());
  return instance->get();
}

}  // namespace content
