chromium/fuchsia_web/webengine/browser/web_engine_devtools_controller.cc

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

#include "fuchsia_web/webengine/browser/web_engine_devtools_controller.h"

#include <fuchsia/web/cpp/fidl.h>
#include <lib/fidl/cpp/interface_ptr_set.h>
#include <lib/sys/cpp/component_context.h>

#include <optional>
#include <vector>

#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/fuchsia/process_context.h"
#include "base/fuchsia/scoped_service_binding.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_tokenizer.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/devtools_socket_factory.h"
#include "content/public/common/content_switches.h"
#include "fuchsia_web/webengine/switches.h"
#include "net/base/net_errors.h"
#include "net/base/port_util.h"
#include "net/socket/tcp_server_socket.h"

namespace {

using OnDevToolsPortChanged = base::OnceCallback<void(uint16_t)>;

class DevToolsSocketFactory : public content::DevToolsSocketFactory {
 public:
  DevToolsSocketFactory(OnDevToolsPortChanged on_devtools_port,
                        net::IPEndPoint ip_end_point)
      : on_devtools_port_(std::move(on_devtools_port)),
        ip_end_point_(std::move(ip_end_point)) {}
  ~DevToolsSocketFactory() override = default;

  DevToolsSocketFactory(const DevToolsSocketFactory&) = delete;
  DevToolsSocketFactory& operator=(const DevToolsSocketFactory&) = delete;

  // content::DevToolsSocketFactory implementation.
  std::unique_ptr<net::ServerSocket> CreateForHttpServer() override {
    const int kTcpListenBackLog = 5;
    auto socket =
        std::make_unique<net::TCPServerSocket>(nullptr, net::NetLogSource());
    int error = socket->Listen(ip_end_point_, kTcpListenBackLog,
                               /*ipv6_only=*/std::nullopt);
    if (error != net::OK) {
      LOG(WARNING) << "Failed to start the HTTP debugger service. "
                   << net::ErrorToString(error);
      std::move(on_devtools_port_).Run(0);
      return nullptr;
    }

    net::IPEndPoint end_point;
    socket->GetLocalAddress(&end_point);
    std::move(on_devtools_port_).Run(end_point.port());
    return socket;
  }

  std::unique_ptr<net::ServerSocket> CreateForTethering(
      std::string* out_name) override {
    return nullptr;
  }

 private:
  OnDevToolsPortChanged on_devtools_port_;
  net::IPEndPoint ip_end_point_;
};

void StartRemoteDebuggingServer(OnDevToolsPortChanged on_devtools_port,
                                net::IPEndPoint ip_end_point) {
  const base::FilePath kDisableActivePortOutputDirectory{};
  const base::FilePath kDisableDebugOutput{};

  content::DevToolsAgentHost::StartRemoteDebuggingServer(
      std::make_unique<DevToolsSocketFactory>(std::move(on_devtools_port),
                                              ip_end_point),
      kDisableActivePortOutputDirectory, kDisableDebugOutput);
}

class NoopController : public WebEngineDevToolsController {
 public:
  NoopController() = default;
  ~NoopController() override = default;

  NoopController(const NoopController&) = delete;
  NoopController& operator=(const NoopController&) = delete;

  // WebEngineDevToolsController implementation:
  bool OnFrameCreated(content::WebContents* contents,
                      bool user_debugging) override {
    return !user_debugging;
  }
  void OnFrameLoaded(content::WebContents* contents) override {}
  void OnFrameDestroyed(content::WebContents* contents) override {}
  content::DevToolsAgentHost::List RemoteDebuggingTargets() override {
    return {};
  }
  void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override {
    std::move(callback).Run(0);
  }
};

// "User-mode" makes DevTools accessible to remote devices for Frames specified
// by the web_instance owner. The controller, which starts DevTools when the
// first Frame is created, and shuts it down when no debuggable Frames remain.
class UserModeController : public WebEngineDevToolsController {
 public:
  explicit UserModeController(uint16_t server_port)
      : ip_endpoint_(net::IPAddress::IPv6AllZeros(), server_port) {}
  ~UserModeController() override {
    if (is_remote_debugging_started_) {
      content::DevToolsAgentHost::StopRemoteDebuggingServer();
    }
  }

  UserModeController(const UserModeController&) = delete;
  UserModeController& operator=(const UserModeController&) = delete;

  // WebEngineDevToolsController implementation:
  bool OnFrameCreated(content::WebContents* contents,
                      bool user_debugging) override {
    if (user_debugging) {
      debuggable_contents_.insert(contents);
      UpdateRemoteDebuggingServer();
    }
    return true;
  }
  void OnFrameLoaded(content::WebContents* contents) override {}
  void OnFrameDestroyed(content::WebContents* contents) override {
    debuggable_contents_.erase(contents);
    UpdateRemoteDebuggingServer();
  }
  content::DevToolsAgentHost::List RemoteDebuggingTargets() override {
    DCHECK(is_remote_debugging_started_);

    content::DevToolsAgentHost::List enabled_hosts;
    for (content::WebContents* contents : debuggable_contents_) {
      enabled_hosts.push_back(
          content::DevToolsAgentHost::GetOrCreateFor(contents));
    }
    return enabled_hosts;
  }
  void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override {
    get_port_callbacks_.emplace_back(std::move(callback));
    MaybeNotifyGetPortCallbacks();
  }

 private:
  // Starts or stops the remote debugging server, if necessary
  void UpdateRemoteDebuggingServer() {
    bool need_remote_debugging = !debuggable_contents_.empty();
    if (need_remote_debugging == is_remote_debugging_started_)
      return;
    is_remote_debugging_started_ = need_remote_debugging;

    if (need_remote_debugging) {
      StartRemoteDebuggingServer(
          base::BindOnce(&UserModeController::OnDevToolsPortChanged,
                         base::Unretained(this)),
          ip_endpoint_);
    } else {
      content::DevToolsAgentHost::StopRemoteDebuggingServer();
      devtools_port_.reset();
    }
  }

  void OnDevToolsPortChanged(uint16_t port) {
    devtools_port_ = port;
    MaybeNotifyGetPortCallbacks();
  }

  void MaybeNotifyGetPortCallbacks() {
    if (!devtools_port_.has_value())
      return;
    for (auto& callback : get_port_callbacks_)
      std::move(callback).Run(devtools_port_.value());
    get_port_callbacks_.clear();
  }

  const net::IPEndPoint ip_endpoint_;

  // True if the remote debugging server is started.
  bool is_remote_debugging_started_ = false;

  // Currently active DevTools port. Set to 0 on service startup error.
  std::optional<uint16_t> devtools_port_;

  // Set of Frames' content::WebContents which are remotely debuggable.
  base::flat_set<content::WebContents*> debuggable_contents_;

  std::vector<base::OnceCallback<void(uint16_t)>> get_port_callbacks_;
};

// "Debug-mode" is used for on-device testing, and makes all Frames available
// for debugging by clients on the same device. DevTools is only reported when
// the first Frame finishes loading its main document, so that the
// DevToolsPerContextListeners can start interacting with it immediately.
class DebugModeController : public WebEngineDevToolsController,
                            public fuchsia::web::Debug {
 public:
  DebugModeController()
      : DebugModeController(
            net::IPEndPoint(net::IPAddress::IPv4Localhost(), 0)) {}
  ~DebugModeController() override {
    content::DevToolsAgentHost::StopRemoteDebuggingServer();
  }

  DebugModeController(const DebugModeController&) = delete;
  DebugModeController& operator=(const DebugModeController&) = delete;

  // DevToolsController implementation:
  bool OnFrameCreated(content::WebContents* contents,
                      bool user_debugging) override {
    return !user_debugging;
  }
  void OnFrameLoaded(content::WebContents* contents) override {
    if (!frame_loaded_) {
      frame_loaded_ = true;
      MaybeSendRemoteDebuggingCallbacks();
    }
  }
  void OnFrameDestroyed(content::WebContents* contents) override {}
  content::DevToolsAgentHost::List RemoteDebuggingTargets() override {
    return content::DevToolsAgentHost::GetOrCreateAll();
  }
  void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override {
    std::move(callback).Run(0);
  }

 protected:
  explicit DebugModeController(net::IPEndPoint ip_endpoint)
      : ip_endpoint_(std::move(ip_endpoint)),
        binding_(base::ComponentContextForProcess()->outgoing().get(), this) {
    // Immediately start the service.
    StartRemoteDebuggingServer(
        base::BindOnce(&DebugModeController::OnDevToolsPortChanged,
                       base::Unretained(this)),
        ip_endpoint_);
  }

  virtual void OnDevToolsPortChanged(uint16_t port) {
    devtools_port_ = port;
    MaybeSendRemoteDebuggingCallbacks();
  }

  // Currently active DevTools port. Set to 0 on service startup error.
  std::optional<uint16_t> devtools_port_;

 private:
  // fuchsia::web::Debug implementation.
  void EnableDevTools(
      fidl::InterfaceHandle<fuchsia::web::DevToolsListener> listener_handle,
      EnableDevToolsCallback callback) override {
    // Each web-instance has a single DevTools "context", so create a new
    // per-context channel, and pass it to |listener| immediately.
    fuchsia::web::DevToolsPerContextListenerPtr context_listener;
    auto listener = listener_handle.Bind();
    listener->OnContextDevToolsAvailable(context_listener.NewRequest());

    // Notify the per-context listener immediately, if the port is ready.
    if (frame_loaded_ && devtools_port_)
      context_listener->OnHttpPortOpen(devtools_port_.value());

    devtools_listeners_.AddInterfacePtr(std::move(context_listener));
  }

  void MaybeSendRemoteDebuggingCallbacks() {
    if (!frame_loaded_ || !devtools_port_)
      return;

    // If |devtools_port_| is zero then DevTools failed to initialize, and
    // all listener connections should be closed to indicate failure.
    if (devtools_port_.value() == 0) {
      devtools_listeners_.CloseAll();
      return;
    }

    for (const auto& listener : devtools_listeners_.ptrs()) {
      listener->get()->OnHttpPortOpen(devtools_port_.value());
    }
  }

  const net::IPEndPoint ip_endpoint_;

  bool frame_loaded_ = false;

  fidl::InterfacePtrSet<fuchsia::web::DevToolsPerContextListener>
      devtools_listeners_;

  const base::ScopedServiceBinding<fuchsia::web::Debug> binding_;
};

// "Mixed-mode" is used when both user and debug remote debugging are active at
// the same time. The service is enabled for the lifespan of the web_instance.
class MixedModeController : public DebugModeController {
 public:
  explicit MixedModeController(uint16_t server_port)
      : DebugModeController(
            net::IPEndPoint(net::IPAddress::IPv6AllZeros(), server_port)) {}
  ~MixedModeController() override = default;

  // WebEngineDevToolsController overrides:
  bool OnFrameCreated(content::WebContents* contents,
                      bool user_debugging) override {
    return true;
  }
  void GetDevToolsPort(base::OnceCallback<void(uint16_t)> callback) override {
    get_port_callbacks_.emplace_back(std::move(callback));
    MaybeNotifyGetPortCallbacks();
  }

  // DebugModeController overrides:
  void OnDevToolsPortChanged(uint16_t port) override {
    DebugModeController::OnDevToolsPortChanged(port);
    MaybeNotifyGetPortCallbacks();
  }

  void MaybeNotifyGetPortCallbacks() {
    if (!devtools_port_)
      return;
    for (auto& callback : get_port_callbacks_)
      std::move(callback).Run(devtools_port_.value());
    get_port_callbacks_.clear();
  }

  std::vector<base::OnceCallback<void(uint16_t)>> get_port_callbacks_;
};

}  //  namespace

// static
std::unique_ptr<WebEngineDevToolsController>
WebEngineDevToolsController::CreateFromCommandLine(
    const base::CommandLine& command_line) {
  std::optional<uint16_t> devtools_port;
  if (command_line.HasSwitch(switches::kRemoteDebuggingPort)) {
    // Set up DevTools to listen on all network routes on the command-line
    // provided port.
    std::string command_line_port_value =
        command_line.GetSwitchValueASCII(switches::kRemoteDebuggingPort);
    int parsed_port = 0;

    // The command-line option can only be provided by the ContextProvider
    // process, it should not fail to parse to an int.
    bool parsed = base::StringToInt(command_line_port_value, &parsed_port);
    DCHECK(parsed);

    if (parsed_port != 0 &&
        (!net::IsPortValid(parsed_port) || net::IsWellKnownPort(parsed_port))) {
      LOG(WARNING) << "Invalid HTTP debugger service port number "
                   << command_line_port_value;
    } else {
      devtools_port = parsed_port;
    }
  }

  bool enable_debug_mode =
      command_line.HasSwitch(switches::kEnableRemoteDebugMode);
  if (devtools_port) {
    if (enable_debug_mode) {
      return std::make_unique<MixedModeController>(devtools_port.value());
    } else {
      return std::make_unique<UserModeController>(devtools_port.value());
    }
  } else if (enable_debug_mode) {
    return std::make_unique<DebugModeController>();
  } else {
    return std::make_unique<NoopController>();
  }
}