// Copyright (c) 2019 GitHub, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.

#include "shell/browser/api/electron_api_protocol.h"

#include <memory>
#include <utility>
#include <vector>

#include "base/stl_util.h"
#include "content/public/browser/child_process_security_policy.h"
#include "shell/browser/browser.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/net/asar/asar_url_loader.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
#include "shell/common/gin_helper/promise.h"
#include "shell/common/options_switches.h"
#include "shell/common/process_util.h"
#include "url/url_util.h"

namespace {

// List of registered custom standard schemes.
std::vector<std::string> g_standard_schemes;

struct SchemeOptions {
  bool standard = false;
  bool secure = false;
  bool bypassCSP = false;
  bool allowServiceWorkers = false;
  bool supportFetchAPI = false;
  bool corsEnabled = false;
};

struct CustomScheme {
  std::string scheme;
  SchemeOptions options;
};

// Provide support for accessing asar archives in file:// protocol.
class AsarURLLoaderFactory : public network::mojom::URLLoaderFactory {
 public:
  AsarURLLoaderFactory() {}

 private:
  // network::mojom::URLLoaderFactory:
  void CreateLoaderAndStart(
      mojo::PendingReceiver<network::mojom::URLLoader> loader,
      int32_t routing_id,
      int32_t request_id,
      uint32_t options,
      const network::ResourceRequest& request,
      mojo::PendingRemote<network::mojom::URLLoaderClient> client,
      const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
      override {
    asar::CreateAsarURLLoader(request, std::move(loader), std::move(client),
                              new net::HttpResponseHeaders(""));
  }

  void Clone(
      mojo::PendingReceiver<network::mojom::URLLoaderFactory> loader) override {
    receivers_.Add(this, std::move(loader));
  }

  mojo::ReceiverSet<network::mojom::URLLoaderFactory> receivers_;
};

}  // namespace

namespace gin {

template <>
struct Converter<CustomScheme> {
  static bool FromV8(v8::Isolate* isolate,
                     v8::Local<v8::Value> val,
                     CustomScheme* out) {
    gin::Dictionary dict(isolate);
    if (!ConvertFromV8(isolate, val, &dict))
      return false;
    if (!dict.Get("scheme", &(out->scheme)))
      return false;
    gin::Dictionary opt(isolate);
    // options are optional. Default values specified in SchemeOptions are used
    if (dict.Get("privileges", &opt)) {
      opt.Get("standard", &(out->options.standard));
      opt.Get("supportFetchAPI", &(out->options.supportFetchAPI));
      opt.Get("secure", &(out->options.secure));
      opt.Get("bypassCSP", &(out->options.bypassCSP));
      opt.Get("allowServiceWorkers", &(out->options.allowServiceWorkers));
      opt.Get("supportFetchAPI", &(out->options.supportFetchAPI));
      opt.Get("corsEnabled", &(out->options.corsEnabled));
    }
    return true;
  }
};

}  // namespace gin

namespace electron {
namespace api {

std::vector<std::string> GetStandardSchemes() {
  return g_standard_schemes;
}

void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
                                 v8::Local<v8::Value> val) {
  std::vector<CustomScheme> custom_schemes;
  if (!gin::ConvertFromV8(thrower.isolate(), val, &custom_schemes)) {
    thrower.ThrowError("Argument must be an array of custom schemes.");
    return;
  }

  std::vector<std::string> secure_schemes, cspbypassing_schemes, fetch_schemes,
      service_worker_schemes, cors_schemes;
  for (const auto& custom_scheme : custom_schemes) {
    // Register scheme to privileged list (https, wss, data, chrome-extension)
    if (custom_scheme.options.standard) {
      auto* policy = content::ChildProcessSecurityPolicy::GetInstance();
      url::AddStandardScheme(custom_scheme.scheme.c_str(),
                             url::SCHEME_WITH_HOST);
      g_standard_schemes.push_back(custom_scheme.scheme);
      policy->RegisterWebSafeScheme(custom_scheme.scheme);
    }
    if (custom_scheme.options.secure) {
      secure_schemes.push_back(custom_scheme.scheme);
      url::AddSecureScheme(custom_scheme.scheme.c_str());
    }
    if (custom_scheme.options.bypassCSP) {
      cspbypassing_schemes.push_back(custom_scheme.scheme);
      url::AddCSPBypassingScheme(custom_scheme.scheme.c_str());
    }
    if (custom_scheme.options.corsEnabled) {
      cors_schemes.push_back(custom_scheme.scheme);
      url::AddCorsEnabledScheme(custom_scheme.scheme.c_str());
    }
    if (custom_scheme.options.supportFetchAPI) {
      fetch_schemes.push_back(custom_scheme.scheme);
    }
    if (custom_scheme.options.allowServiceWorkers) {
      service_worker_schemes.push_back(custom_scheme.scheme);
    }
  }

  const auto AppendSchemesToCmdLine = [](const char* switch_name,
                                         std::vector<std::string> schemes) {
    // Add the schemes to command line switches, so child processes can also
    // register them.
    base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
        switch_name, base::JoinString(schemes, ","));
  };

  AppendSchemesToCmdLine(electron::switches::kSecureSchemes, secure_schemes);
  AppendSchemesToCmdLine(electron::switches::kBypassCSPSchemes,
                         cspbypassing_schemes);
  AppendSchemesToCmdLine(electron::switches::kCORSSchemes, cors_schemes);
  AppendSchemesToCmdLine(electron::switches::kFetchSchemes, fetch_schemes);
  AppendSchemesToCmdLine(electron::switches::kServiceWorkerSchemes,
                         service_worker_schemes);
  AppendSchemesToCmdLine(electron::switches::kStandardSchemes,
                         g_standard_schemes);
}

namespace {

const char* kBuiltinSchemes[] = {
    "about", "file", "http", "https", "data", "filesystem",
};

// Convert error code to string.
std::string ErrorCodeToString(ProtocolError error) {
  switch (error) {
    case ProtocolError::REGISTERED:
      return "The scheme has been registered";
    case ProtocolError::NOT_REGISTERED:
      return "The scheme has not been registered";
    case ProtocolError::INTERCEPTED:
      return "The scheme has been intercepted";
    case ProtocolError::NOT_INTERCEPTED:
      return "The scheme has not been intercepted";
    default:
      return "Unexpected error";
  }
}

}  // namespace

Protocol::Protocol(v8::Isolate* isolate,
                   ElectronBrowserContext* browser_context) {
  Init(isolate);
  AttachAsUserData(browser_context);
}

Protocol::~Protocol() = default;

void Protocol::RegisterURLLoaderFactories(
    URLLoaderFactoryType type,
    content::ContentBrowserClient::NonNetworkURLLoaderFactoryMap* factories) {
  // Override the default FileURLLoaderFactory to support asar archives.
  if (type == URLLoaderFactoryType::kNavigation) {
    // Always allow navigating to file:// URLs.
    //
    // Note that Chromium calls |emplace| to create the default file factory
    // after this call, so it won't override our asar factory.
    DCHECK(!base::Contains(*factories, url::kFileScheme));
    factories->emplace(url::kFileScheme,
                       std::make_unique<AsarURLLoaderFactory>());
  } else if (type == URLLoaderFactoryType::kDocumentSubResource) {
    // Only support requesting file:// subresource URLs when Chromium does so,
    // it is usually supported under file:// or about:blank documents.
    auto file_factory = factories->find(url::kFileScheme);
    if (file_factory != factories->end())
      file_factory->second = std::make_unique<AsarURLLoaderFactory>();
  }

  for (const auto& it : handlers_) {
    factories->emplace(it.first, std::make_unique<ElectronURLLoaderFactory>(
                                     it.second.first, it.second.second));
  }
}

ProtocolError Protocol::RegisterProtocol(ProtocolType type,
                                         const std::string& scheme,
                                         const ProtocolHandler& handler) {
  const bool added = base::TryEmplace(handlers_, scheme, type, handler).second;
  return added ? ProtocolError::OK : ProtocolError::REGISTERED;
}

void Protocol::UnregisterProtocol(const std::string& scheme,
                                  gin::Arguments* args) {
  const bool removed = handlers_.erase(scheme) != 0;
  const auto error =
      removed ? ProtocolError::OK : ProtocolError::NOT_REGISTERED;
  HandleOptionalCallback(args, error);
}

bool Protocol::IsProtocolRegistered(const std::string& scheme) {
  return base::Contains(handlers_, scheme);
}

ProtocolError Protocol::InterceptProtocol(ProtocolType type,
                                          const std::string& scheme,
                                          const ProtocolHandler& handler) {
  const bool added =
      base::TryEmplace(intercept_handlers_, scheme, type, handler).second;
  return added ? ProtocolError::OK : ProtocolError::INTERCEPTED;
}

void Protocol::UninterceptProtocol(const std::string& scheme,
                                   gin::Arguments* args) {
  const bool removed = intercept_handlers_.erase(scheme) != 0;
  const auto error =
      removed ? ProtocolError::OK : ProtocolError::NOT_INTERCEPTED;
  HandleOptionalCallback(args, error);
}

bool Protocol::IsProtocolIntercepted(const std::string& scheme) {
  return base::Contains(intercept_handlers_, scheme);
}

v8::Local<v8::Promise> Protocol::IsProtocolHandled(const std::string& scheme,
                                                   gin::Arguments* args) {
  node::Environment* env = node::Environment::GetCurrent(args->isolate());
  EmitWarning(env,
              "The protocol.isProtocolHandled API is deprecated, use "
              "protocol.isProtocolRegistered or protocol.isProtocolIntercepted "
              "instead.",
              "ProtocolDeprecateIsProtocolHandled");
  return gin_helper::Promise<bool>::ResolvedPromise(
      isolate(), IsProtocolRegistered(scheme) ||
                     IsProtocolIntercepted(scheme) ||
                     // The |isProtocolHandled| should return true for builtin
                     // schemes, however with NetworkService it is impossible to
                     // know which schemes are registered until a real network
                     // request is sent.
                     // So we have to test against a hard-coded builtin schemes
                     // list make it work with old code. We should deprecate
                     // this API with the new |isProtocolRegistered| API.
                     base::Contains(kBuiltinSchemes, scheme));
}

void Protocol::HandleOptionalCallback(gin::Arguments* args,
                                      ProtocolError error) {
  CompletionCallback callback;
  if (args->GetNext(&callback)) {
    node::Environment* env = node::Environment::GetCurrent(args->isolate());
    EmitWarning(
        env,
        "The callback argument of protocol module APIs is no longer needed.",
        "ProtocolDeprecateCallback");
    if (error == ProtocolError::OK)
      callback.Run(v8::Null(args->isolate()));
    else
      callback.Run(v8::Exception::Error(
          gin::StringToV8(isolate(), ErrorCodeToString(error))));
  }
}

// static
gin::Handle<Protocol> Protocol::Create(
    v8::Isolate* isolate,
    ElectronBrowserContext* browser_context) {
  return gin::CreateHandle(isolate, new Protocol(isolate, browser_context));
}

// static
void Protocol::BuildPrototype(v8::Isolate* isolate,
                              v8::Local<v8::FunctionTemplate> prototype) {
  prototype->SetClassName(gin::StringToV8(isolate, "Protocol"));
  gin_helper::ObjectTemplateBuilder(isolate, prototype->PrototypeTemplate())
      .SetMethod("registerStringProtocol",
                 &Protocol::RegisterProtocolFor<ProtocolType::kString>)
      .SetMethod("registerBufferProtocol",
                 &Protocol::RegisterProtocolFor<ProtocolType::kBuffer>)
      .SetMethod("registerFileProtocol",
                 &Protocol::RegisterProtocolFor<ProtocolType::kFile>)
      .SetMethod("registerHttpProtocol",
                 &Protocol::RegisterProtocolFor<ProtocolType::kHttp>)
      .SetMethod("registerStreamProtocol",
                 &Protocol::RegisterProtocolFor<ProtocolType::kStream>)
      .SetMethod("registerProtocol",
                 &Protocol::RegisterProtocolFor<ProtocolType::kFree>)
      .SetMethod("unregisterProtocol", &Protocol::UnregisterProtocol)
      .SetMethod("isProtocolRegistered", &Protocol::IsProtocolRegistered)
      .SetMethod("isProtocolHandled", &Protocol::IsProtocolHandled)
      .SetMethod("interceptStringProtocol",
                 &Protocol::InterceptProtocolFor<ProtocolType::kString>)
      .SetMethod("interceptBufferProtocol",
                 &Protocol::InterceptProtocolFor<ProtocolType::kBuffer>)
      .SetMethod("interceptFileProtocol",
                 &Protocol::InterceptProtocolFor<ProtocolType::kFile>)
      .SetMethod("interceptHttpProtocol",
                 &Protocol::InterceptProtocolFor<ProtocolType::kHttp>)
      .SetMethod("interceptStreamProtocol",
                 &Protocol::InterceptProtocolFor<ProtocolType::kStream>)
      .SetMethod("interceptProtocol",
                 &Protocol::InterceptProtocolFor<ProtocolType::kFree>)
      .SetMethod("uninterceptProtocol", &Protocol::UninterceptProtocol)
      .SetMethod("isProtocolIntercepted", &Protocol::IsProtocolIntercepted);
}

}  // namespace api
}  // namespace electron

namespace {

void RegisterSchemesAsPrivileged(gin_helper::ErrorThrower thrower,
                                 v8::Local<v8::Value> val) {
  if (electron::Browser::Get()->is_ready()) {
    thrower.ThrowError(
        "protocol.registerSchemesAsPrivileged should be called before "
        "app is ready");
    return;
  }

  electron::api::RegisterSchemesAsPrivileged(thrower, val);
}

void Initialize(v8::Local<v8::Object> exports,
                v8::Local<v8::Value> unused,
                v8::Local<v8::Context> context,
                void* priv) {
  v8::Isolate* isolate = context->GetIsolate();
  gin_helper::Dictionary dict(isolate, exports);
  dict.SetMethod("registerSchemesAsPrivileged", &RegisterSchemesAsPrivileged);
  dict.SetMethod("getStandardSchemes", &electron::api::GetStandardSchemes);
}

}  // namespace

NODE_LINKED_MODULE_CONTEXT_AWARE(atom_browser_protocol, Initialize)
