blob: bccb05627132a6c6fca2da899413310bff2761c6 [file] [log] [blame] [edit]
// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/test/chromedriver/server/http_handler.h"
#include <stddef.h>
#include <algorithm>
#include <memory>
#include <optional>
#include <string_view>
#include <utility>
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_forward.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/logging.h" // For CHECK macros.
#include "base/memory/scoped_refptr.h"
#include "base/notimplemented.h"
#include "base/strings/strcat.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/system/sys_info.h"
#include "base/task/bind_post_task.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/single_thread_task_runner.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/test/chromedriver/alert_commands.h"
#include "chrome/test/chromedriver/chrome/adb_impl.h"
#include "chrome/test/chromedriver/chrome/device_manager.h"
#include "chrome/test/chromedriver/chrome/status.h"
#include "chrome/test/chromedriver/command.h"
#include "chrome/test/chromedriver/commands.h"
#include "chrome/test/chromedriver/connection_session_map.h"
#include "chrome/test/chromedriver/constants/version.h"
#include "chrome/test/chromedriver/fedcm_commands.h"
#include "chrome/test/chromedriver/net/url_request_context_getter.h"
#include "chrome/test/chromedriver/server/http_server.h"
#include "chrome/test/chromedriver/session.h"
#include "chrome/test/chromedriver/session_thread_map.h"
#include "chrome/test/chromedriver/util.h"
#include "chrome/test/chromedriver/webauthn_commands.h"
#include "chrome/test/chromedriver/window_commands.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/server/http_server_request_info.h"
#include "net/server/http_server_response_info.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/mojom/url_loader_factory.mojom.h"
#include "services/network/transitional_url_loader_factory_owner.h"
#include "url/url_util.h"
#if BUILDFLAG(IS_MAC)
#include "base/apple/scoped_nsautorelease_pool.h"
#endif
const char kCreateWebSocketPath[] =
"session/:sessionId/chromium/create_websocket";
const char kSendCommandFromWebSocket[] =
"session/:sessionId/chromium/send_command_from_websocket";
namespace {
const char kLocalStorage[] = "localStorage";
const char kSessionStorage[] = "sessionStorage";
const char kShutdownPath[] = "shutdown";
// The commands are in the order as they ordered in the WebDriver BiDi
// specification.
base::flat_set<std::string> kKnownBidiSessionCommands = {
// session
"session.end",
"session.subscribe",
"session.unsubscribe",
// browsingContext
"browsingContext.activate",
"browsingContext.captureScreenshot",
"browsingContext.close",
"browsingContext.create",
"browsingContext.getTree",
"browsingContext.handleUserPropmpt",
"browsingContext.navigate",
"browsingContext.print",
"browsingContext.reload",
"browsingContext.setViewport",
// network
"network.addIntercept",
"network.continueRequest",
"network.continueResponse",
"network.continueWithAuth",
"network.failRequest",
"network.provideResponse",
"network.removeIntercept",
// script
"script.addPreloadScript",
"script.disown",
"script.callFunction",
"script.evaluate",
"script.getRealms",
"script.removePreloadScript",
// input
"input.performActions",
"input.releaseActions",
};
std::optional<base::Value> Clone(const std::optional<base::Value>& original) {
if (!original.has_value()) {
return std::nullopt;
}
return std::make_optional(original->Clone());
}
bool w3cMode(const std::string& session_id,
const SessionThreadMap& session_thread_map) {
if (session_id.length() > 0 && session_thread_map.count(session_id) > 0)
return session_thread_map.at(session_id)->w3cMode();
return kW3CDefault;
}
net::HttpServerResponseInfo CreateWebSocketRejectResponse(
net::HttpStatusCode code,
const std::string& msg) {
net::HttpServerResponseInfo response(code);
response.AddHeader("X-WebSocket-Reject-Reason", msg);
return response;
}
void AddBidiConnectionOnSessionThread(int connection_id,
SendTextFunc send_response,
CloseFunc close_connection) {
Session* session = GetThreadLocalSession();
// session == nullptr is a valid case: ExecuteQuit has already been handled
// in the session thread but the following
// TerminateSessionThreadOnCommandThread has not yet been executed (the latter
// destroys the session thread) The connection has already been accepted by
// the CMD thread but soon it will be closed. We don't need to do anything.
if (session != nullptr) {
session->AddBidiConnection(connection_id, std::move(send_response),
std::move(close_connection));
}
}
void RemoveBidiConnectionOnSessionThread(int connection_id) {
Session* session = GetThreadLocalSession();
// session == nullptr is a valid case: ExecuteQuit has already been handled
// in the session thread but the following
// TerminateSessionThreadOnCommandThread has not yet been executed (the latter
// destroys the session thread)
if (session != nullptr) {
session->RemoveBidiConnection(connection_id);
}
}
bool MatchesMethod(HttpMethod command_method, const std::string& method) {
std::string lower_method = base::ToLowerASCII(method);
switch (command_method) {
case kGet:
return lower_method == "get";
case kPost:
return lower_method == "post" || lower_method == "put";
case kDelete:
return lower_method == "delete";
}
return false;
}
} // namespace
// WrapperURLLoaderFactory subclasses mojom::URLLoaderFactory as non-mojo, cross
// thread class. It basically posts ::CreateLoaderAndStart calls over to the UI
// thread, to call them on the real mojo object.
class WrapperURLLoaderFactory : public network::mojom::URLLoaderFactory {
public:
explicit WrapperURLLoaderFactory(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
: url_loader_factory_(std::move(url_loader_factory)),
network_task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) {}
WrapperURLLoaderFactory(const WrapperURLLoaderFactory&) = delete;
WrapperURLLoaderFactory& operator=(const WrapperURLLoaderFactory&) = delete;
void CreateLoaderAndStart(
mojo::PendingReceiver<network::mojom::URLLoader> loader,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
mojo::PendingRemote<network::mojom::URLLoaderClient> client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation)
override {
if (network_task_runner_->RunsTasksInCurrentSequence()) {
url_loader_factory_->CreateLoaderAndStart(
std::move(loader), request_id, options, request, std::move(client),
traffic_annotation);
} else {
network_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&WrapperURLLoaderFactory::CreateLoaderAndStart,
base::Unretained(this), std::move(loader), request_id,
options, request, std::move(client),
traffic_annotation));
}
}
void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory> factory)
override {
NOTIMPLEMENTED();
}
private:
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
// Runner for URLRequestContextGetter network thread.
scoped_refptr<base::SequencedTaskRunner> network_task_runner_;
};
CommandMapping::CommandMapping(HttpMethod method,
const std::string& path_pattern,
const Command& command)
: method(method), path_pattern(path_pattern), command(command) {}
CommandMapping::CommandMapping(const CommandMapping& other) = default;
CommandMapping::~CommandMapping() = default;
// Create a command mapping with a prefixed HTTP path (.e.g goog/).
CommandMapping VendorPrefixedSessionCommandMapping(HttpMethod method,
std::string_view path_suffix,
const Command& command) {
return CommandMapping(
method,
base::StrCat({"session/:sessionId/", kChromeDriverCompanyPrefix, "/",
path_suffix}),
command);
}
HttpHandler::HttpHandler(const std::string& url_base)
: url_base_(url_base),
received_shutdown_(false),
command_map_(new CommandMap()) {
session_connection_map_.emplace("", std::vector<int>());
}
HttpHandler::HttpHandler(
const base::RepeatingClosure& quit_func,
const scoped_refptr<base::SingleThreadTaskRunner> io_task_runner,
const scoped_refptr<base::SingleThreadTaskRunner> cmd_task_runner,
const std::string& url_base,
int adb_port)
: quit_func_(quit_func),
io_task_runner_(io_task_runner),
cmd_task_runner_(cmd_task_runner),
url_base_(url_base),
received_shutdown_(false) {
#if BUILDFLAG(IS_MAC)
base::apple::ScopedNSAutoreleasePool autorelease_pool;
#endif
context_getter_ = new URLRequestContextGetter(io_task_runner_);
socket_factory_ = CreateSyncWebSocketFactory(context_getter_.get());
adb_ = std::make_unique<AdbImpl>(io_task_runner_, adb_port);
device_manager_ = std::make_unique<DeviceManager>(adb_.get());
url_loader_factory_owner_ =
std::make_unique<network::TransitionalURLLoaderFactoryOwner>(
context_getter_.get());
wrapper_url_loader_factory_ = std::make_unique<WrapperURLLoaderFactory>(
url_loader_factory_owner_->GetURLLoaderFactory());
session_connection_map_.emplace("", std::vector<int>());
auto terminate_on_cmd = base::BindRepeating(&HttpHandler::OnSessionTerminated,
weak_ptr_factory_.GetWeakPtr());
Command init_session_cmd = WrapToCommand(
"InitSession",
base::BindRepeating(
&ExecuteInitSession,
InitSessionParams(wrapper_url_loader_factory_.get(), socket_factory_,
device_manager_.get(), cmd_task_runner,
terminate_on_cmd)));
Command create_and_init_session = base::BindRepeating(
&ExecuteCreateSession, &session_thread_map_, init_session_cmd);
CommandMapping commands[] = {
//
// W3C standard endpoints
//
CommandMapping(kPost, internal::kNewSessionPathPattern,
WrapCreateNewSessionCommand(create_and_init_session)),
CommandMapping(kDelete, "session/:sessionId",
base::BindRepeating(
&ExecuteSessionCommand, &session_thread_map_, "Quit",
base::BindRepeating(&ExecuteQuit, false), true, true)),
CommandMapping(kGet, "status", base::BindRepeating(&ExecuteGetStatus)),
CommandMapping(kGet, "session/:sessionId/timeouts",
WrapToCommand("GetTimeouts",
base::BindRepeating(&ExecuteGetTimeouts))),
CommandMapping(kPost, "session/:sessionId/timeouts",
WrapToCommand("SetTimeouts",
base::BindRepeating(&ExecuteSetTimeouts))),
CommandMapping(
kPost, "session/:sessionId/url",
WrapToCommand("Navigate", base::BindRepeating(&ExecuteGet))),
CommandMapping(
kGet, "session/:sessionId/url",
WrapToCommand("GetUrl", base::BindRepeating(&ExecuteGetCurrentUrl))),
CommandMapping(
kPost, "session/:sessionId/back",
WrapToCommand("GoBack", base::BindRepeating(&ExecuteGoBack))),
CommandMapping(
kPost, "session/:sessionId/forward",
WrapToCommand("GoForward", base::BindRepeating(&ExecuteGoForward))),
CommandMapping(
kPost, "session/:sessionId/refresh",
WrapToCommand("Refresh", base::BindRepeating(&ExecuteRefresh))),
CommandMapping(
kGet, "session/:sessionId/title",
WrapToCommand("GetTitle", base::BindRepeating(&ExecuteGetTitle))),
CommandMapping(
kGet, "session/:sessionId/window",
WrapToCommand("GetWindow",
base::BindRepeating(&ExecuteGetCurrentWindowHandle))),
CommandMapping(
kDelete, "session/:sessionId/window",
WrapToCommand("CloseWindow", base::BindRepeating(&ExecuteClose))),
CommandMapping(
kPost, "session/:sessionId/window/new",
WrapToCommand("NewWindow", base::BindRepeating(&ExecuteNewWindow))),
CommandMapping(
kPost, "session/:sessionId/window",
WrapToCommand("SwitchToWindow",
base::BindRepeating(&ExecuteSwitchToWindow))),
CommandMapping(
kGet, "session/:sessionId/window/handles",
WrapToCommand("GetWindows",
base::BindRepeating(&ExecuteGetWindowHandles))),
CommandMapping(kPost, "session/:sessionId/frame",
WrapToCommand("SwitchToFrame",
base::BindRepeating(&ExecuteSwitchToFrame))),
CommandMapping(
kPost, "session/:sessionId/frame/parent",
WrapToCommand("SwitchToParentFrame",
base::BindRepeating(&ExecuteSwitchToParentFrame))),
CommandMapping(kGet, "session/:sessionId/window/rect",
WrapToCommand("GetWindowRect",
base::BindRepeating(&ExecuteGetWindowRect))),
CommandMapping(kPost, "session/:sessionId/window/rect",
WrapToCommand("SetWindowRect",
base::BindRepeating(&ExecuteSetWindowRect))),
CommandMapping(
kPost, "session/:sessionId/window/maximize",
WrapToCommand("MaximizeWindow",
base::BindRepeating(&ExecuteMaximizeWindow))),
CommandMapping(
kPost, "session/:sessionId/window/minimize",
WrapToCommand("MinimizeWindow",
base::BindRepeating(&ExecuteMinimizeWindow))),
CommandMapping(
kPost, "session/:sessionId/window/fullscreen",
WrapToCommand("FullscreenWindow",
base::BindRepeating(&ExecuteFullScreenWindow))),
CommandMapping(
kGet, "session/:sessionId/element/active",
WrapToCommand("GetActiveElement",
base::BindRepeating(&ExecuteGetActiveElement))),
CommandMapping(
kGet, "session/:sessionId/element/:id/shadow",
WrapToCommand("GetElementShadowRoot",
base::BindRepeating(&ExecuteGetElementShadowRoot))),
CommandMapping(
kPost, "session/:sessionId/shadow/:id/element",
WrapToCommand(
"FindChildElementFromShadowRoot",
base::BindRepeating(&ExecuteFindChildElementFromShadowRoot, 50))),
CommandMapping(
kPost, "session/:sessionId/shadow/:id/elements",
WrapToCommand("FindChildElementsFromShadowRoot",
base::BindRepeating(
&ExecuteFindChildElementsFromShadowRoot, 50))),
CommandMapping(
kPost, "session/:sessionId/element",
WrapToCommand("FindElement",
base::BindRepeating(&ExecuteFindElement, 50))),
CommandMapping(
kPost, "session/:sessionId/elements",
WrapToCommand("FindElements",
base::BindRepeating(&ExecuteFindElements, 50))),
CommandMapping(
kPost, "session/:sessionId/element/:id/element",
WrapToCommand("FindChildElement",
base::BindRepeating(&ExecuteFindChildElement, 50))),
CommandMapping(
kPost, "session/:sessionId/element/:id/elements",
WrapToCommand("FindChildElements",
base::BindRepeating(&ExecuteFindChildElements, 50))),
CommandMapping(
kGet, "session/:sessionId/element/:id/selected",
WrapToCommand("IsElementSelected",
base::BindRepeating(&ExecuteIsElementSelected))),
CommandMapping(
kGet, "session/:sessionId/element/:id/attribute/:name",
WrapToCommand("GetElementAttribute",
base::BindRepeating(&ExecuteGetElementAttribute))),
CommandMapping(
kGet, "session/:sessionId/element/:id/property/:name",
WrapToCommand("GetElementProperty",
base::BindRepeating(&ExecuteGetElementProperty))),
CommandMapping(kGet, "session/:sessionId/element/:id/css/:propertyName",
WrapToCommand("GetElementCSSProperty",
base::BindRepeating(
&ExecuteGetElementValueOfCSSProperty))),
CommandMapping(
kGet, "session/:sessionId/element/:id/text",
WrapToCommand("GetElementText",
base::BindRepeating(&ExecuteGetElementText))),
CommandMapping(
kGet, "session/:sessionId/element/:id/name",
WrapToCommand("GetElementTagName",
base::BindRepeating(&ExecuteGetElementTagName))),
CommandMapping(
kGet, "session/:sessionId/element/:id/rect",
WrapToCommand("GetElementRect",
base::BindRepeating(&ExecuteGetElementRect))),
CommandMapping(
kGet, "session/:sessionId/element/:id/enabled",
WrapToCommand("IsElementEnabled",
base::BindRepeating(&ExecuteIsElementEnabled))),
CommandMapping(
kGet, "session/:sessionId/element/:id/computedlabel",
WrapToCommand("GetComputedLabel",
base::BindRepeating(&ExecuteGetComputedLabel))),
CommandMapping(
kGet, "session/:sessionId/element/:id/computedrole",
WrapToCommand("GetComputedRole",
base::BindRepeating(&ExecuteGetComputedRole))),
CommandMapping(kPost, "session/:sessionId/element/:id/click",
WrapToCommand("ClickElement",
base::BindRepeating(&ExecuteClickElement))),
CommandMapping(kPost, "session/:sessionId/element/:id/clear",
WrapToCommand("ClearElement",
base::BindRepeating(&ExecuteClearElement))),
CommandMapping(
kPost, "session/:sessionId/element/:id/value",
WrapToCommand("TypeElement",
base::BindRepeating(&ExecuteSendKeysToElement))),
CommandMapping(kGet, "session/:sessionId/source",
WrapToCommand("GetSource",
base::BindRepeating(&ExecuteGetPageSource))),
CommandMapping(kPost, "session/:sessionId/execute/sync",
WrapToCommand("ExecuteScript",
base::BindRepeating(&ExecuteExecuteScript))),
CommandMapping(
kPost, "session/:sessionId/execute/async",
WrapToCommand("ExecuteAsyncScript",
base::BindRepeating(&ExecuteExecuteAsyncScript))),
CommandMapping(
kGet, "session/:sessionId/cookie",
WrapToCommand("GetCookies", base::BindRepeating(&ExecuteGetCookies))),
CommandMapping(
kGet, "session/:sessionId/cookie/:name",
WrapToCommand("GetNamedCookie",
base::BindRepeating(&ExecuteGetNamedCookie))),
CommandMapping(
kPost, "session/:sessionId/cookie",
WrapToCommand("AddCookie", base::BindRepeating(&ExecuteAddCookie))),
CommandMapping(kDelete, "session/:sessionId/cookie/:name",
WrapToCommand("DeleteCookie",
base::BindRepeating(&ExecuteDeleteCookie))),
CommandMapping(
kDelete, "session/:sessionId/cookie",
WrapToCommand("DeleteAllCookies",
base::BindRepeating(&ExecuteDeleteAllCookies))),
CommandMapping(
kPost, "session/:sessionId/actions",
WrapToCommand("PerformActions",
base::BindRepeating(&ExecutePerformActions))),
CommandMapping(
kDelete, "session/:sessionId/actions",
WrapToCommand("ReleaseActions",
base::BindRepeating(&ExecuteReleaseActions))),
CommandMapping(
kPost, "session/:sessionId/alert/dismiss",
WrapToCommand(
"DismissAlert",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteDismissAlert)))),
CommandMapping(
kPost, "session/:sessionId/alert/accept",
WrapToCommand(
"AcceptAlert",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteAcceptAlert)))),
CommandMapping(
kGet, "session/:sessionId/alert/text",
WrapToCommand(
"GetAlertMessage",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteGetAlertText)))),
CommandMapping(
kPost, "session/:sessionId/alert/text",
WrapToCommand(
"SetAlertPrompt",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteSetAlertText)))),
CommandMapping(
kGet, "session/:sessionId/screenshot",
WrapToCommand("Screenshot", base::BindRepeating(&ExecuteScreenshot))),
CommandMapping(
kGet, "session/:sessionId/element/:id/screenshot",
WrapToCommand("ElementScreenshot",
base::BindRepeating(&ExecuteElementScreenshot))),
CommandMapping(
kGet, "session/:sessionId/screenshot/full",
WrapToCommand("FullPageScreenshot",
base::BindRepeating(&ExecuteFullPageScreenshot))),
CommandMapping(
kPost, "session/:sessionId/print",
WrapToCommand("Print", base::BindRepeating(&ExecutePrint))),
//
// Json wire protocol endpoints
//
// No W3C equivalent.
CommandMapping(
kGet, "sessions",
base::BindRepeating(
&ExecuteGetSessions,
WrapToCommand("GetSessions", base::BindRepeating(
&ExecuteGetSessionCapabilities)),
&session_thread_map_)),
// No W3C equivalent.
CommandMapping(
kGet, "session/:sessionId",
WrapToCommand("GetSessionCapabilities",
base::BindRepeating(&ExecuteGetSessionCapabilities),
false /*w3c_standard_command*/)),
// Subset of W3C POST /session/:sessionId/timeouts.
CommandMapping(kPost, "session/:sessionId/timeouts/implicit_wait",
WrapToCommand("SetImplicitWait",
base::BindRepeating(&ExecuteImplicitlyWait),
false /*w3c_standard_command*/)),
// Subset of W3C POST /session/:sessionId/timeouts.
CommandMapping(
kPost, "session/:sessionId/timeouts/async_script",
WrapToCommand("SetScriptTimeout",
base::BindRepeating(&ExecuteSetScriptTimeout),
false /*w3c_standard_command*/)),
// Similar to W3C GET /session/:sessionId/window.
CommandMapping(
kGet, "session/:sessionId/window_handle",
WrapToCommand("GetWindow",
base::BindRepeating(&ExecuteGetCurrentWindowHandle),
false /*w3c_standard_command*/)),
// Similar to W3C GET /session/:sessionId/window/handles
CommandMapping(
kGet, "session/:sessionId/window_handles",
WrapToCommand("GetWindows",
base::BindRepeating(&ExecuteGetWindowHandles),
false /*w3c_standard_command*/)),
// Similar to W3C POST /session/:sessionId/execute/sync.
CommandMapping(kPost, "session/:sessionId/execute",
WrapToCommand("ExecuteScript",
base::BindRepeating(&ExecuteExecuteScript),
false /*w3c_standard_command*/)),
// Similar to W3C POST /session/:sessionId/execute/async.
CommandMapping(
kPost, "session/:sessionId/execute_async",
WrapToCommand("ExecuteAsyncScript",
base::BindRepeating(&ExecuteExecuteAsyncScript),
false /*w3c_standard_command*/)),
// Subset of W3C POST /session/:sessionId/window/rect.
CommandMapping(kPost, "session/:sessionId/window/:windowHandle/size",
WrapToCommand("SetWindowSize",
base::BindRepeating(&ExecuteSetWindowSize),
false /*w3c_standard_command*/)),
// Subset of W3C GET /session/:sessionId/window/rect.
CommandMapping(kGet, "session/:sessionId/window/:windowHandle/size",
WrapToCommand("GetWindowSize",
base::BindRepeating(&ExecuteGetWindowSize),
false /*w3c_standard_command*/)),
// Subset of W3C POST /session/:sessionId/window/rect.
CommandMapping(
kPost, "session/:sessionId/window/:windowHandle/position",
WrapToCommand("SetWindowPosition",
base::BindRepeating(&ExecuteSetWindowPosition),
false /*w3c_standard_command*/)),
// Subset of W3C GET /session/:sessionId/window/rect.
CommandMapping(
kGet, "session/:sessionId/window/:windowHandle/position",
WrapToCommand("GetWindowPosition",
base::BindRepeating(&ExecuteGetWindowPosition),
false /*w3c_standard_command*/)),
// Similar to W3C POST /session/:sessionId/window/maximize.
CommandMapping(kPost, "session/:sessionId/window/:windowHandle/maximize",
WrapToCommand("MaximizeWindow",
base::BindRepeating(&ExecuteMaximizeWindow),
false /*w3c_standard_command*/)),
// Similar to W3C GET /session/:sessionId/element/active, but is POST.
CommandMapping(
kPost, "session/:sessionId/element/active",
WrapToCommand("GetActiveElement",
base::BindRepeating(&ExecuteGetActiveElement),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kPost, "session/:sessionId/element/:id/submit",
WrapToCommand("SubmitElement",
base::BindRepeating(&ExecuteSubmitElement),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(
kPost, "session/:sessionId/keys",
WrapToCommand("Type",
base::BindRepeating(&ExecuteSendKeysToActiveElement),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/element/:id/equals/:other",
WrapToCommand("IsElementEqual",
base::BindRepeating(&ExecuteElementEquals),
false /*w3c_standard_command*/)),
// No W3C equivalent. Allowed in W3C mode due to active usage by some APIs
// and the difficulty for clients to provide an equivalent implementation.
// This endpoint is mentioned in an appendix of W3C spec
// (https://www.w3.org/TR/webdriver/#element-displayedness).
CommandMapping(
kGet, "session/:sessionId/element/:id/displayed",
WrapToCommand("IsElementDisplayed",
base::BindRepeating(&ExecuteIsElementDisplayed))),
// No W3C equivalent.
CommandMapping(
kGet, "session/:sessionId/element/:id/location",
WrapToCommand("GetElementLocation",
base::BindRepeating(&ExecuteGetElementLocation),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(
kGet, "session/:sessionId/element/:id/location_in_view",
WrapToCommand("GetElementLocationInView",
base::BindRepeating(
&ExecuteGetElementLocationOnceScrolledIntoView),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/element/:id/size",
WrapToCommand("GetElementSize",
base::BindRepeating(&ExecuteGetElementSize),
false /*w3c_standard_command*/)),
// Similar to W3C GET /session/:sessionId/alert/text.
CommandMapping(
kGet, "session/:sessionId/alert_text",
WrapToCommand(
"GetAlertMessage",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteGetAlertText)),
false /*w3c_standard_command*/)),
// Similar to W3C POST /session/:sessionId/alert/text.
CommandMapping(
kPost, "session/:sessionId/alert_text",
WrapToCommand(
"SetAlertPrompt",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteSetAlertText)),
false /*w3c_standard_command*/)),
// Similar to W3C POST /session/:sessionId/alert/accept.
CommandMapping(
kPost, "session/:sessionId/accept_alert",
WrapToCommand(
"AcceptAlert",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteAcceptAlert)),
false /*w3c_standard_command*/)),
// Similar to W3C POST /session/:sessionId/alert/dismiss.
CommandMapping(
kPost, "session/:sessionId/dismiss_alert",
WrapToCommand(
"DismissAlert",
base::BindRepeating(&ExecuteAlertCommand,
base::BindRepeating(&ExecuteDismissAlert)),
false /*w3c_standard_command*/)),
// The following set of commands form a subset of W3C Actions API.
CommandMapping(
kPost, "session/:sessionId/moveto",
WrapToCommand("MouseMove", base::BindRepeating(&ExecuteMouseMoveTo),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/click",
WrapToCommand("Click", base::BindRepeating(&ExecuteMouseClick))),
CommandMapping(kPost, "session/:sessionId/buttondown",
WrapToCommand("MouseDown",
base::BindRepeating(&ExecuteMouseButtonDown),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/buttonup",
WrapToCommand("MouseUp", base::BindRepeating(&ExecuteMouseButtonUp),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/doubleclick",
WrapToCommand("DoubleClick",
base::BindRepeating(&ExecuteMouseDoubleClick),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/touch/click",
WrapToCommand("Tap", base::BindRepeating(&ExecuteTouchSingleTap),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/touch/down",
WrapToCommand("TouchDown", base::BindRepeating(&ExecuteTouchDown),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/touch/up",
WrapToCommand("TouchUp", base::BindRepeating(&ExecuteTouchUp),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/touch/move",
WrapToCommand("TouchMove", base::BindRepeating(&ExecuteTouchMove),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/touch/scroll",
WrapToCommand("TouchScroll", base::BindRepeating(&ExecuteTouchScroll),
false /*w3c_standard_command*/)),
CommandMapping(kPost, "session/:sessionId/touch/doubleclick",
WrapToCommand("TouchDoubleTap",
base::BindRepeating(&ExecuteTouchDoubleTap),
false /*w3c_standard_command*/)),
CommandMapping(kPost, "session/:sessionId/touch/longclick",
WrapToCommand("TouchLongPress",
base::BindRepeating(&ExecuteTouchLongPress),
false /*w3c_standard_command*/)),
CommandMapping(
kPost, "session/:sessionId/touch/flick",
WrapToCommand("TouchFlick", base::BindRepeating(&ExecuteFlick),
false /*w3c_standard_command*/)),
// No W3C equivalent, see .https://crbug.com/chromedriver/3180
CommandMapping(kGet, "session/:sessionId/location",
WrapToCommand("GetGeolocation",
base::BindRepeating(&ExecuteGetLocation))),
// No W3C equivalent, see .https://crbug.com/chromedriver/3180
CommandMapping(kPost, "session/:sessionId/location",
WrapToCommand("SetGeolocation",
base::BindRepeating(&ExecuteSetLocation))),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/local_storage",
WrapToCommand("GetLocalStorageKeys",
base::BindRepeating(&ExecuteGetStorageKeys,
kLocalStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kPost, "session/:sessionId/local_storage",
WrapToCommand("SetLocalStorageKeys",
base::BindRepeating(&ExecuteSetStorageItem,
kLocalStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kDelete, "session/:sessionId/local_storage",
WrapToCommand("ClearLocalStorage",
base::BindRepeating(&ExecuteClearStorage,
kLocalStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/local_storage/key/:key",
WrapToCommand("GetLocalStorageItem",
base::BindRepeating(&ExecuteGetStorageItem,
kLocalStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(
kDelete, "session/:sessionId/local_storage/key/:key",
WrapToCommand(
"RemoveLocalStorageItem",
base::BindRepeating(&ExecuteRemoveStorageItem, kLocalStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/local_storage/size",
WrapToCommand("GetLocalStorageSize",
base::BindRepeating(&ExecuteGetStorageSize,
kLocalStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/session_storage",
WrapToCommand("GetSessionStorageKeys",
base::BindRepeating(&ExecuteGetStorageKeys,
kSessionStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kPost, "session/:sessionId/session_storage",
WrapToCommand("SetSessionStorageItem",
base::BindRepeating(&ExecuteSetStorageItem,
kSessionStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kDelete, "session/:sessionId/session_storage",
WrapToCommand("ClearSessionStorage",
base::BindRepeating(&ExecuteClearStorage,
kSessionStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/session_storage/key/:key",
WrapToCommand("GetSessionStorageItem",
base::BindRepeating(&ExecuteGetStorageItem,
kSessionStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(
kDelete, "session/:sessionId/session_storage/key/:key",
WrapToCommand(
"RemoveSessionStorageItem",
base::BindRepeating(&ExecuteRemoveStorageItem, kSessionStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/session_storage/size",
WrapToCommand("GetSessionStorageSize",
base::BindRepeating(&ExecuteGetStorageSize,
kSessionStorage),
false /*w3c_standard_command*/)),
// No W3C equivalent, temporarily enabled until clients start using
// "session/:sessionId/se/log".
// Superseded by "session/:sessionId/se/log".
CommandMapping(
kPost, "session/:sessionId/log",
WrapToCommand("GetLog", base::BindRepeating(&ExecuteGetLog))),
// No W3C equivalent. Superseded by "session/:sessionId/se/log/types".
CommandMapping(
kGet, "session/:sessionId/log/types",
WrapToCommand("GetLogTypes",
base::BindRepeating(&ExecuteGetAvailableLogTypes),
false /*w3c_standard_command*/)),
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/application_cache/status",
base::BindRepeating(&ExecuteGetStatus)),
//
// Extension commands from other specs.
//
// Extension for Reporting API:
// https://w3c.github.io/reporting/#generate-test-report-command
CommandMapping(
kPost, "session/:sessionId/reporting/generate_test_report",
WrapToCommand("GenerateTestReport",
base::BindRepeating(&ExecuteGenerateTestReport))),
// Extensions from Mobile JSON Wire Protocol:
// https://github.com/SeleniumHQ/mobile-spec/blob/master/spec-draft.md
CommandMapping(
kGet, "session/:sessionId/network_connection",
WrapToCommand("GetNetworkConnection",
base::BindRepeating(&ExecuteGetNetworkConnection))),
CommandMapping(
kPost, "session/:sessionId/network_connection",
WrapToCommand("SetNetworkConnection",
base::BindRepeating(&ExecuteSetNetworkConnection))),
// Extension for WebAuthn API:
// https://w3c.github.io/webauthn/#sctn-automation
CommandMapping(kPost, "session/:sessionId/webauthn/authenticator",
WrapToCommand("AddVirtualAuthenticator",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(
&ExecuteAddVirtualAuthenticator)))),
CommandMapping(
kDelete, "session/:sessionId/webauthn/authenticator/:authenticatorId",
WrapToCommand(
"RemoveVirtualAuthenticator",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteRemoveVirtualAuthenticator)))),
CommandMapping(
kPost,
"session/:sessionId/webauthn/authenticator/:authenticatorId/"
"credential",
WrapToCommand(
"AddCredential",
base::BindRepeating(&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteAddCredential)))),
CommandMapping(
kGet,
"session/:sessionId/webauthn/authenticator/:authenticatorId/"
"credentials",
WrapToCommand("GetCredentials",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteGetCredentials)))),
CommandMapping(
kDelete,
"session/:sessionId/webauthn/authenticator/:authenticatorId/"
"credentials/:credentialId",
WrapToCommand("RemoveCredential",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteRemoveCredential)))),
CommandMapping(
kDelete,
"session/:sessionId/webauthn/authenticator/:authenticatorId/"
"credentials",
WrapToCommand(
"RemoveAllCredentials",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteRemoveAllCredentials)))),
CommandMapping(
kPost,
"session/:sessionId/webauthn/authenticator/:authenticatorId/uv",
WrapToCommand("SetUserVerified",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteSetUserVerified)))),
CommandMapping(
kPost,
"session/:sessionId/webauthn/authenticator/:authenticatorId/"
"credentials/:credentialId/props",
WrapToCommand(
"SetCredentialProperties",
base::BindRepeating(
&ExecuteWebAuthnCommand,
base::BindRepeating(&ExecuteSetCredentialProperties)))),
// Extensions for Secure Payment Confirmation API:
// https://w3c.github.io/secure-payment-confirmation/#sctn-automation
CommandMapping(
kPost, "session/:sessionId/secure-payment-confirmation/set-mode",
WrapToCommand("SetSPCTransactionMode",
base::BindRepeating(&ExecuteSetSPCTransactionMode))),
// Extensions for the Federated Credential Management API:
// https://fedidcg.github.io/FedCM/#automation
CommandMapping(kPost, "session/:sessionId/fedcm/canceldialog",
WrapToCommand("CancelDialog",
base::BindRepeating(&ExecuteCancelDialog))),
CommandMapping(kPost, "session/:sessionId/fedcm/selectaccount",
WrapToCommand("SelectAccount",
base::BindRepeating(&ExecuteSelectAccount))),
CommandMapping(
kPost, "session/:sessionId/fedcm/clickdialogbutton",
WrapToCommand("ClickDialogButton",
base::BindRepeating(&ExecuteClickDialogButton))),
CommandMapping(kGet, "session/:sessionId/fedcm/accountlist",
WrapToCommand("GetAccounts",
base::BindRepeating(&ExecuteGetAccounts))),
CommandMapping(kGet, "session/:sessionId/fedcm/gettitle",
WrapToCommand("GetFedCmTitle",
base::BindRepeating(&ExecuteGetFedCmTitle))),
CommandMapping(kGet, "session/:sessionId/fedcm/getdialogtype",
WrapToCommand("GetDialogType",
base::BindRepeating(&ExecuteGetDialogType))),
CommandMapping(
kPost, "session/:sessionId/fedcm/setdelayenabled",
WrapToCommand("SetDelayEnabled",
base::BindRepeating(&ExecuteSetDelayEnabled))),
CommandMapping(kPost, "session/:sessionId/fedcm/resetcooldown",
WrapToCommand("ResetCooldown",
base::BindRepeating(&ExecuteResetCooldown))),
// Extensions for Navigational Tracking Mitigations:
// https://privacycg.github.io/nav-tracking-mitigations
CommandMapping(
kDelete, "session/:sessionId/storage/run_bounce_tracking_mitigations",
WrapToCommand(
"RunBounceTrackingMitigations",
base::BindRepeating(&ExecuteRunBounceTrackingMitigations))),
// Extensions for Protected Audience KAnonymity support:
// https://wicg.github.io/turtledove/#kanonymity-automation
CommandMapping(
kPost, "session/:sessionId/protected_audience/set_k_anonymity",
WrapToCommand(
"SetProtectedAudienceKAnonymity",
base::BindRepeating(&ExecuteSetProtectedAudienceKAnonymity))),
// Extensions for Custom Handlers API:
// https://html.spec.whatwg.org/multipage/system-state.html#rph-automation
CommandMapping(
kPost, "session/:sessionId/custom-handlers/set-mode",
WrapToCommand("SetRPHRegistrationMode",
base::BindRepeating(&ExecuteSetRPHRegistrationMode))),
// https://w3c.github.io/sensors/#automation
CommandMapping(
kPost, "session/:sessionId/sensor",
WrapToCommand("CreateVirtualSensor",
base::BindRepeating(&ExecuteCreateVirtualSensor))),
CommandMapping(
kPost, "session/:sessionId/sensor/:type",
WrapToCommand("UpdateVirtualSensor",
base::BindRepeating(&ExecuteUpdateVirtualSensor))),
CommandMapping(
kDelete, "session/:sessionId/sensor/:type",
WrapToCommand("RemoveVirtualSensor",
base::BindRepeating(&ExecuteRemoveVirtualSensor))),
CommandMapping(kGet, "session/:sessionId/sensor/:type",
WrapToCommand("GetVirtualSensorInformation",
base::BindRepeating(
&ExecuteGetVirtualSensorInformation))),
// Extension for Permissions Standard Automation "set permission" command:
// https://w3c.github.io/permissions/#set-permission-command
CommandMapping(kPost, "session/:sessionId/permissions",
WrapToCommand("SetPermission",
base::BindRepeating(&ExecuteSetPermission))),
// Extensions for Device Posture API:
// https://w3c.github.io/device-posture/#automation
CommandMapping(
kPost, "session/:sessionId/deviceposture",
WrapToCommand("SetDevicePosture",
base::BindRepeating(&ExecuteSetDevicePosture))),
CommandMapping(
kDelete, "session/:sessionId/deviceposture",
WrapToCommand("ClearDevicePosture",
base::BindRepeating(&ExecuteClearDevicePosture))),
// Extensions for Viewport Segments API:
// https://drafts.csswg.org/css-viewport-1/#automation-of-the-segments-property
CommandMapping(
kPost, "session/:sessionId/displayfeatures",
WrapToCommand("SetDisplayFeatures",
base::BindRepeating(&ExecuteSetDisplayFeatures))),
CommandMapping(
kDelete, "session/:sessionId/displayfeatures",
WrapToCommand("ClearDisplayFeatures",
base::BindRepeating(&ExecuteClearDisplayFeatures))),
// Extensions for Compute Pressure API:
// https://w3c.github.io/compute-pressure/#automation
CommandMapping(kPost, "session/:sessionId/pressuresource",
WrapToCommand("CreateVirtualPressureSource",
base::BindRepeating(
&ExecuteCreateVirtualPressureSource))),
CommandMapping(kPost, "session/:sessionId/pressuresource/:type",
WrapToCommand("UpdateVirtualPressureSource",
base::BindRepeating(
&ExecuteUpdateVirtualPressureSource))),
CommandMapping(kDelete, "session/:sessionId/pressuresource/:type",
WrapToCommand("RemoveVirtualPressureSource",
base::BindRepeating(
&ExecuteRemoveVirtualPressureSource))),
//
// Non-standard extension commands
//
// Commands to access Chrome logs, defined by agreement with Selenium.
// Using "se" prefix for "Selenium".
CommandMapping(
kPost, "session/:sessionId/se/log",
WrapToCommand("GetLog", base::BindRepeating(&ExecuteGetLog))),
CommandMapping(
kGet, "session/:sessionId/se/log/types",
WrapToCommand("GetLogTypes",
base::BindRepeating(&ExecuteGetAvailableLogTypes))),
// Command is used by Selenium Java tests
CommandMapping(
kPost, "session/:sessionId/file",
WrapToCommand("UploadFile", base::BindRepeating(&ExecuteUploadFile))),
// Command is used by Ruby OSS mode
// No W3C equivalent.
CommandMapping(kGet, "session/:sessionId/element/:id/value",
WrapToCommand("GetElementValue",
base::BindRepeating(&ExecuteGetElementValue),
false /*w3c_standard_command*/)),
// Command is used by Selenium Java tests
CommandMapping(
kGet, kShutdownPath,
base::BindRepeating(
&ExecuteQuitAll,
WrapToCommand("QuitAll", base::BindRepeating(&ExecuteQuit, true)),
&session_thread_map_)),
// Command is used by Selenium Java tests
CommandMapping(
kPost, kShutdownPath,
base::BindRepeating(
&ExecuteQuitAll,
WrapToCommand("QuitAll", base::BindRepeating(&ExecuteQuit, true)),
&session_thread_map_)),
// Set Time Zone command
CommandMapping(kPost, "session/:sessionId/time_zone",
WrapToCommand("SetTimeZone",
base::BindRepeating(&ExecuteSetTimeZone))),
//
// ChromeDriver specific extension commands.
//
CommandMapping(
kGet, "session/:sessionId/chromium/heap_snapshot",
WrapToCommand("HeapSnapshot",
base::BindRepeating(&ExecuteTakeHeapSnapshot))),
CommandMapping(
kGet, "session/:sessionId/chromium/network_conditions",
WrapToCommand("GetNetworkConditions",
base::BindRepeating(&ExecuteGetNetworkConditions))),
CommandMapping(
kPost, "session/:sessionId/chromium/network_conditions",
WrapToCommand("SetNetworkConditions",
base::BindRepeating(&ExecuteSetNetworkConditions))),
CommandMapping(
kDelete, "session/:sessionId/chromium/network_conditions",
WrapToCommand("DeleteNetworkConditions",
base::BindRepeating(&ExecuteDeleteNetworkConditions))),
CommandMapping(kPost, "session/:sessionId/chromium/send_command",
WrapToCommand("SendCommand",
base::BindRepeating(&ExecuteSendCommand))),
VendorPrefixedSessionCommandMapping(
kPost, "cdp/execute",
WrapToCommand("ExecuteCDP",
base::BindRepeating(&ExecuteSendCommandAndGetResult))),
CommandMapping(
kPost, "session/:sessionId/chromium/send_command_and_get_result",
WrapToCommand("SendCommandAndGetResult",
base::BindRepeating(&ExecuteSendCommandAndGetResult))),
VendorPrefixedSessionCommandMapping(
kPost, "page/freeze",
WrapToCommand("Freeze", base::BindRepeating(&ExecuteFreeze))),
VendorPrefixedSessionCommandMapping(
kPost, "page/resume",
WrapToCommand("Resume", base::BindRepeating(&ExecuteResume))),
VendorPrefixedSessionCommandMapping(
kPost, "cast/set_sink_to_use",
WrapToCommand("SetSinkToUse",
base::BindRepeating(&ExecuteSetSinkToUse))),
VendorPrefixedSessionCommandMapping(
kPost, "cast/start_desktop_mirroring",
WrapToCommand("StartDesktopMirroring",
base::BindRepeating(&ExecuteStartDesktopMirroring))),
VendorPrefixedSessionCommandMapping(
kPost, "cast/start_tab_mirroring",
WrapToCommand("StartTabMirroring",
base::BindRepeating(&ExecuteStartTabMirroring))),
VendorPrefixedSessionCommandMapping(
kPost, "cast/stop_casting",
WrapToCommand("StopCasting",
base::BindRepeating(&ExecuteStopCasting))),
VendorPrefixedSessionCommandMapping(
kGet, "cast/get_sinks",
WrapToCommand("GetSinks", base::BindRepeating(&ExecuteGetSinks))),
VendorPrefixedSessionCommandMapping(
kGet, "cast/get_issue_message",
WrapToCommand("GetIssueMessage",
base::BindRepeating(&ExecuteGetIssueMessage))),
//
// Commands used for internal testing only.
// They are used in run_py_tests.py
//
CommandMapping(kGet, "session/:sessionId/alert",
WrapToCommand("IsAlertOpen",
base::BindRepeating(
&ExecuteAlertCommand,
base::BindRepeating(&ExecuteGetAlert)))),
CommandMapping(
kGet, "session/:sessionId/is_loading",
WrapToCommand("IsLoading", base::BindRepeating(&ExecuteIsLoading))),
//
// Special commands used by internal implementation
// Client apps should never use this over a normal
// WebDriver http connection
//
CommandMapping(
kPost, kSendCommandFromWebSocket,
WrapToCommand("SendCommandFromWebSocket",
base::BindRepeating(&ExecuteSendCommandFromWebSocket))),
};
command_map_ =
std::make_unique<CommandMap>(std::begin(commands), std::end(commands));
static_bidi_command_map_.emplace(
"session.status", base::BindRepeating(&ExecuteBidiSessionStatus));
static_bidi_command_map_.emplace(
"session.new",
base::BindRepeating(&ExecuteBidiSessionNew, &session_thread_map_,
init_session_cmd));
session_bidi_command_map_.emplace(
"session.end",
base::BindRepeating(&ExecuteSessionCommand, &session_thread_map_, "Quit",
base::BindRepeating(&ExecuteBidiSessionEnd), true,
true));
forward_session_command_ = WrapToCommand(
"ForwardBidiCommand", base::BindRepeating(&ForwardBidiCommand));
}
HttpHandler::~HttpHandler() = default;
void HttpHandler::Handle(const net::HttpServerRequestInfo& request,
const HttpResponseSenderFunc& send_response_func) {
CHECK(thread_checker_.CalledOnValidThread());
if (received_shutdown_)
return;
std::string path = request.path;
if (!base::StartsWith(path, url_base_, base::CompareCase::SENSITIVE)) {
std::unique_ptr<net::HttpServerResponseInfo> response(
new net::HttpServerResponseInfo(net::HTTP_BAD_REQUEST));
response->SetBody("unhandled request", "text/plain");
send_response_func.Run(std::move(response));
return;
}
path.erase(0, url_base_.length());
HandleCommand(request, path, send_response_func);
if (path == kShutdownPath)
received_shutdown_ = true;
}
base::WeakPtr<HttpHandler> HttpHandler::WeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
Command HttpHandler::WrapToCommand(const char* name,
const SessionCommand& session_command,
bool w3c_standard_command) {
return base::BindRepeating(&ExecuteSessionCommand, &session_thread_map_, name,
session_command, w3c_standard_command, false);
}
Command HttpHandler::WrapToCommand(const char* name,
const WindowCommand& window_command,
bool w3c_standard_command) {
return WrapToCommand(
name, base::BindRepeating(&ExecuteWindowCommand, window_command),
w3c_standard_command);
}
Command HttpHandler::WrapToCommand(const char* name,
const ElementCommand& element_command,
bool w3c_standard_command) {
return WrapToCommand(
name, base::BindRepeating(&ExecuteElementCommand, element_command),
w3c_standard_command);
}
void HttpHandler::HandleCommand(
const net::HttpServerRequestInfo& request,
const std::string& trimmed_path,
const HttpResponseSenderFunc& send_response_func) {
base::Value::Dict params;
std::string session_id;
CommandMap::const_iterator iter = command_map_->begin();
while (true) {
if (iter == command_map_->end()) {
if (w3cMode(session_id, session_thread_map_)) {
PrepareResponse(
trimmed_path, send_response_func,
Status(kUnknownCommand, "unknown command: " + trimmed_path),
nullptr, session_id, true);
} else {
std::unique_ptr<net::HttpServerResponseInfo> response(
new net::HttpServerResponseInfo(net::HTTP_NOT_FOUND));
response->SetBody("unknown command: " + trimmed_path, "text/plain");
send_response_func.Run(std::move(response));
}
return;
}
if (internal::MatchesCommand(
request.method, trimmed_path, *iter, &session_id, &params)) {
break;
}
++iter;
}
if (request.data.length()) {
std::optional<base::Value> parsed_body =
base::JSONReader::Read(request.data);
base::Value::Dict* body_params =
parsed_body ? parsed_body->GetIfDict() : nullptr;
if (!body_params) {
if (w3cMode(session_id, session_thread_map_)) {
PrepareResponse(trimmed_path, send_response_func,
Status(kInvalidArgument, "missing command parameters"),
nullptr, session_id, true);
} else {
std::unique_ptr<net::HttpServerResponseInfo> response(
new net::HttpServerResponseInfo(net::HTTP_BAD_REQUEST));
response->SetBody("missing command parameters", "text/plain");
send_response_func.Run(std::move(response));
}
return;
}
params.Merge(std::move(*body_params));
} else if (iter->method == kPost &&
w3cMode(session_id, session_thread_map_)) {
// Data in JSON format is required for POST requests. See step 5 of
// https://www.w3.org/TR/2018/REC-webdriver1-20180605/#processing-model.
PrepareResponse(trimmed_path, send_response_func,
Status(kInvalidArgument, "missing command parameters"),
nullptr, session_id, true);
return;
}
// Pass host instead for potential WebSocketUrl if it's a new session
iter->command.Run(params,
internal::IsNewSession(*iter)
? request.GetHeaderValue("host")
: session_id,
base::BindRepeating(&HttpHandler::PrepareResponse,
weak_ptr_factory_.GetWeakPtr(),
trimmed_path, send_response_func));
}
void HttpHandler::PrepareResponse(
const std::string& trimmed_path,
const HttpResponseSenderFunc& send_response_func,
const Status& status,
std::unique_ptr<base::Value> value,
const std::string& session_id,
bool w3c_compliant) {
CHECK(thread_checker_.CalledOnValidThread());
std::unique_ptr<net::HttpServerResponseInfo> response;
if (w3c_compliant)
response = PrepareStandardResponse(
trimmed_path, status, std::move(value), session_id);
else
response = PrepareLegacyResponse(trimmed_path,
status,
std::move(value),
session_id);
send_response_func.Run(std::move(response));
if (trimmed_path == kShutdownPath)
quit_func_.Run();
}
std::unique_ptr<net::HttpServerResponseInfo> HttpHandler::PrepareLegacyResponse(
const std::string& trimmed_path,
const Status& status,
std::unique_ptr<base::Value> value,
const std::string& session_id) {
if (status.code() == kUnknownCommand) {
std::unique_ptr<net::HttpServerResponseInfo> response(
new net::HttpServerResponseInfo(net::HTTP_NOT_IMPLEMENTED));
response->SetBody("unimplemented command: " + trimmed_path, "text/plain");
return response;
}
if (status.IsError()) {
Status full_status(status);
full_status.AddDetails(base::StringPrintf(
"Driver info: %s=%s,platform=%s %s %s",
base::ToLowerASCII(kChromeDriverProductShortName).c_str(),
kChromeDriverVersion, base::SysInfo::OperatingSystemName().c_str(),
base::SysInfo::OperatingSystemVersion().c_str(),
base::SysInfo::OperatingSystemArchitecture().c_str()));
base::Value::Dict error;
error.Set("message", full_status.message());
value = std::make_unique<base::Value>(std::move(error));
}
if (!value)
value = std::make_unique<base::Value>();
base::Value::Dict body_params;
body_params.Set("status", status.code());
body_params.Set("value", base::Value::FromUniquePtrValue(std::move(value)));
body_params.Set("sessionId", session_id);
std::string body;
base::JSONWriter::WriteWithOptions(
body_params, base::JSONWriter::OPTIONS_OMIT_DOUBLE_TYPE_PRESERVATION,
&body);
std::unique_ptr<net::HttpServerResponseInfo> response(
new net::HttpServerResponseInfo(net::HTTP_OK));
response->SetBody(body, "application/json; charset=utf-8");
return response;
}
std::unique_ptr<net::HttpServerResponseInfo>
HttpHandler::PrepareStandardResponse(
const std::string& trimmed_path,
const Status& status,
std::unique_ptr<base::Value> value,
const std::string& session_id) {
std::unique_ptr<net::HttpServerResponseInfo> response;
switch (status.code()) {
case kOk:
response = std::make_unique<net::HttpServerResponseInfo>(net::HTTP_OK);
break;
// error codes
case kElementClickIntercepted:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kElementNotInteractable:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kInvalidArgument:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kInvalidCookieDomain:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kInvalidElementState:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kInvalidSelector:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kInvalidSessionId:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kJavaScriptError:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kMoveTargetOutOfBounds:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kNoSuchAlert:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kNoSuchCookie:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kNoSuchElement:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kNoSuchFrame:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kNoSuchWindow:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kScriptTimeout:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kSessionNotCreated:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kStaleElementReference:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kTimeout:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kUnableToSetCookie:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kUnexpectedAlertOpen:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kUnknownCommand:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kUnknownError:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kUnsupportedOperation:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kTargetDetached:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
// TODO(kereliuk): evaluate the usage of these as they relate to the spec
case kElementNotVisible:
case kXPathLookupError:
case kNoSuchExecutionContext:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_BAD_REQUEST);
break;
case kDisconnected:
case kTabCrashed:
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
case kNoSuchShadowRoot:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
case kDetachedShadowRoot:
response =
std::make_unique<net::HttpServerResponseInfo>(net::HTTP_NOT_FOUND);
break;
default:
DCHECK(false);
// Examples of unexpected codes:
// * kChromeNotReachable: kSessionNotCreated must be returned instead;
// * kAbortedByNavigation: kUnknownError must be returned
// instead.
response = std::make_unique<net::HttpServerResponseInfo>(
net::HTTP_INTERNAL_SERVER_ERROR);
break;
}
if (!value)
value = std::make_unique<base::Value>();
base::Value::Dict body_params;
if (status.IsError()){
base::Value::Dict* inner_params = body_params.EnsureDict("value");
inner_params->Set("error", StatusCodeToString(status.code()));
inner_params->Set("message", status.message());
inner_params->Set("stacktrace", status.stack_trace());
// According to
// https://www.w3.org/TR/2018/REC-webdriver1-20180605/#dfn-annotated-unexpected-alert-open-error
// error UnexpectedAlertOpen should contain 'data.text' with alert text
if (status.code() == kUnexpectedAlertOpen) {
const std::string& message = status.message();
auto first = message.find("{");
auto last = message.find_last_of("}");
if (first == std::string::npos || last == std::string::npos) {
inner_params->SetByDottedPath("data.text", "");
} else {
std::string alert_text = message.substr(first, last - first);
auto colon = alert_text.find(":");
if (colon != std::string::npos && alert_text.size() > (colon + 2))
alert_text = alert_text.substr(colon + 2);
inner_params->SetByDottedPath("data.text", alert_text);
}
}
} else {
body_params.Set("value", base::Value::FromUniquePtrValue(std::move(value)));
}
std::string body;
base::JSONWriter::WriteWithOptions(
body_params, base::JSONWriter::OPTIONS_OMIT_DOUBLE_TYPE_PRESERVATION,
&body);
response->SetBody(body, "application/json; charset=utf-8");
response->AddHeader("cache-control", "no-cache");
return response;
}
void HttpHandler::OnWebSocketRequest(HttpServerInterface* http_server,
int connection_id,
const net::HttpServerRequestInfo& info) {
std::string path = info.path;
std::vector<std::string> path_parts = base::SplitString(
path, "/", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
if (path_parts.size() == 1 && path_parts[0] == "session") {
OnWebSocketUnboundConnectionRequest(http_server, connection_id, info);
return;
}
if (path_parts.size() == 2 && path_parts[0] == "session") {
std::string session_id = path_parts[1];
OnWebSocketAttachToSessionRequest(http_server, connection_id, session_id,
info);
return;
}
std::string err_msg = "bad request received path " + path;
VLOG(0) << "HttpHandler WebSocketRequest error " << err_msg;
SendWebSocketRejectResponse(
base::BindRepeating(&HttpServerInterface::SendResponse,
base::Unretained(http_server)),
connection_id, net::HTTP_BAD_REQUEST, err_msg);
}
void HttpHandler::CloseConnectionOnCommandThread(
HttpServerInterface* http_server,
int connection_id) {
auto close_connection_on_io_func = base::BindRepeating(
&HttpServerInterface::Close, base::Unretained(http_server));
io_task_runner_->PostTask(
FROM_HERE, base::BindOnce(close_connection_on_io_func, connection_id));
}
void HttpHandler::SendForwardedResponseOnCommandThread(
HttpServerInterface* http_server,
int connection_id,
std::string message) {
auto send_response_on_io_func = base::BindRepeating(
[](HttpServerInterface* http_server, int connection_id,
std::string data) {
http_server->SendOverWebSocket(connection_id, data);
},
base::Unretained(http_server));
io_task_runner_->PostTask(
FROM_HERE, base::BindOnce(send_response_on_io_func, connection_id,
std::move(message)));
}
void HttpHandler::OnWebSocketAttachToSessionRequest(
HttpServerInterface* http_server,
int connection_id,
const std::string& session_id,
const net::HttpServerRequestInfo& info) {
auto it = session_connection_map_.find(session_id);
if (it == session_connection_map_.end()) {
std::string err_msg = "bad request invalid session id " + session_id;
VLOG(0) << "HttpHandler WebSocketRequest error " << err_msg;
SendWebSocketRejectResponse(
base::BindRepeating(&HttpServerInterface::SendResponse,
base::Unretained(http_server)),
connection_id, net::HTTP_BAD_REQUEST, err_msg);
return;
}
session_connection_map_[session_id].push_back(connection_id);
connection_session_map_[connection_id] = session_id;
auto thread_it = session_thread_map_.find(session_id);
// check first that the session thread is still alive
if (thread_it != session_thread_map_.end()) {
auto reply_on_command_thread = base::BindRepeating(
&HttpHandler::SendForwardedResponseOnCommandThread,
weak_ptr_factory_.GetWeakPtr(), http_server, connection_id);
auto close_on_command_thread = base::BindRepeating(
&HttpHandler::CloseConnectionOnCommandThread,
weak_ptr_factory_.GetWeakPtr(), http_server, connection_id);
thread_it->second->thread()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&AddBidiConnectionOnSessionThread, connection_id,
base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
std::move(reply_on_command_thread)),
base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
std::move(close_on_command_thread))));
io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpServerInterface::AcceptWebSocket,
base::Unretained(http_server), connection_id, info));
} else {
std::string err_msg = "session not found session_id=" + session_id;
VLOG(0) << "HttpHandler WebSocketRequest error " << err_msg;
SendWebSocketRejectResponse(
base::BindRepeating(&HttpServerInterface::SendResponse,
base::Unretained(http_server)),
connection_id, net::HTTP_BAD_REQUEST, err_msg);
}
}
void HttpHandler::OnWebSocketUnboundConnectionRequest(
HttpServerInterface* http_server,
int connection_id,
const net::HttpServerRequestInfo& info) {
auto it = connection_session_map_.find(connection_id);
if (it != connection_session_map_.end()) {
// This should never happen. The block exists just for diagnostics purposes.
std::string err_msg =
"connection is already bound to session_id=" + it->second;
VLOG(0) << "HttpHandler WebSocketRequest error " << err_msg;
SendWebSocketRejectResponse(
base::BindRepeating(&HttpServerInterface::SendResponse,
base::Unretained(http_server)),
connection_id, net::HTTP_BAD_REQUEST, err_msg);
return;
}
session_connection_map_[""].push_back(connection_id);
connection_session_map_[connection_id] = "";
io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpServerInterface::AcceptWebSocket,
base::Unretained(http_server), connection_id, info));
}
void HttpHandler::SendResponseOverWebSocket(
HttpServerInterface* http_server,
int connection_id,
const std::optional<base::Value>& maybe_id,
const Status& status,
std::unique_ptr<base::Value> result,
const std::string& session_id,
bool w3c) {
base::Value::Dict response;
if (status.IsOk()) {
if (!result) {
return;
}
response.Set("type", "success");
if (maybe_id.has_value()) {
response.Set("id", maybe_id->Clone());
}
response.Set("result", std::move(*result));
} else {
response = internal::CreateBidiErrorResponse(status, Clone(maybe_id));
}
std::string message;
if (base::JSONWriter::Write(response, &message)) {
io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpServerInterface::SendOverWebSocket,
base::Unretained(http_server), connection_id, message));
} else {
LOG(WARNING) << "unable to serialize BiDi response";
}
}
Command HttpHandler::WrapCreateNewSessionCommand(Command command) {
using CommandCallbackWrapper = base::RepeatingCallback<void(
const CommandCallback&, const Status&, std::unique_ptr<base::Value>,
const std::string&, bool)>;
return base::BindRepeating(
[](Command create_and_init, CommandCallbackWrapper callback_to_prepend,
const base::Value::Dict& params, const std::string& session_id,
const CommandCallback& callback) {
create_and_init.Run(params, session_id,
base::BindRepeating(callback_to_prepend, callback));
},
command,
base::BindRepeating(&HttpHandler::OnNewSessionCreated,
weak_ptr_factory_.GetWeakPtr()));
}
void HttpHandler::OnNewSessionCreated(const CommandCallback& next_callback,
const Status& status,
std::unique_ptr<base::Value> result,
const std::string& session_id,
bool w3c) {
base::Value::Dict* dict = result ? result->GetIfDict() : nullptr;
if (status.IsOk() && dict &&
dict->FindByDottedPath("capabilities.webSocketUrl")) {
session_connection_map_.emplace(session_id, std::vector<int>{});
}
next_callback.Run(status, std::move(result), session_id, w3c);
}
void HttpHandler::OnSessionTerminated(std::string session_id) {
session_thread_map_.erase(session_id);
session_connection_map_.erase(session_id);
}
void HttpHandler::OnNewBidiSessionOnCmdThread(
HttpServerInterface* http_server,
int connection_id,
const std::optional<base::Value>& maybe_id,
const Status& status,
std::unique_ptr<base::Value> result,
const std::string& session_id,
bool w3c) {
std::vector<int>& unbound_connections = session_connection_map_[""];
auto conn_it = std::find(unbound_connections.begin(),
unbound_connections.end(), connection_id);
if (conn_it != unbound_connections.end()) {
unbound_connections.erase(conn_it);
}
session_connection_map_.emplace(session_id, std::vector<int>{connection_id});
connection_session_map_.insert_or_assign(connection_id, session_id);
auto reply_on_command_thread = base::BindRepeating(
&HttpHandler::SendForwardedResponseOnCommandThread,
weak_ptr_factory_.GetWeakPtr(), http_server, connection_id);
auto close_on_command_thread = base::BindRepeating(
&HttpHandler::CloseConnectionOnCommandThread,
weak_ptr_factory_.GetWeakPtr(), http_server, connection_id);
auto thread_it = session_thread_map_.find(session_id);
if (thread_it != session_thread_map_.end()) {
thread_it->second->thread()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&AddBidiConnectionOnSessionThread, connection_id,
base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
std::move(reply_on_command_thread)),
base::BindPostTask(
base::SingleThreadTaskRunner::GetCurrentDefault(),
std::move(close_on_command_thread))));
} else {
VLOG(0) << "session thread is not found";
}
SendResponseOverWebSocket(http_server, connection_id, Clone(maybe_id), status,
std::move(result), session_id, w3c);
}
void HttpHandler::OnWebSocketMessage(HttpServerInterface* http_server,
int connection_id,
const std::string& data) {
base::Value::Dict parsed;
Status status = internal::ParseBidiCommand(data, parsed);
auto it = connection_session_map_.find(connection_id);
base::Value* maybe_id_as_value = parsed.Find("id");
std::optional<base::Value> maybe_id =
maybe_id_as_value ? std::make_optional(maybe_id_as_value->Clone())
: std::nullopt;
if (it == connection_session_map_.end()) {
// Session was terminated but the connection is not yet closed
Status invalid_session_error{kInvalidSessionId, "session not found"};
SendResponseOverWebSocket(http_server, connection_id, std::move(maybe_id),
invalid_session_error, nullptr, "", true);
return;
}
std::string* method = parsed.FindString("method");
// Invalid session id must be handled first and it has been.
// Now we can handle other errors.
if (status.IsError()) {
SendResponseOverWebSocket(http_server, connection_id, std::move(maybe_id),
status, nullptr, it->second, true);
return;
}
std::string session_id = it->second;
// Static command is handled first.
auto cmd_it = static_bidi_command_map_.find(*method);
if (cmd_it != static_bidi_command_map_.end()) {
CommandCallback callback = base::BindRepeating(
&HttpHandler::SendResponseOverWebSocket, weak_ptr_factory_.GetWeakPtr(),
http_server, connection_id, Clone(maybe_id));
if (*method == "session.new") {
callback = base::BindRepeating(&HttpHandler::OnNewBidiSessionOnCmdThread,
weak_ptr_factory_.GetWeakPtr(),
base::Unretained(http_server),
connection_id, std::move(maybe_id));
}
cmd_it->second.Run(parsed, session_id, std::move(callback));
return;
}
// The case #6 "Match parsed against the remote end definition" of
// https://w3c.github.io/webdriver-bidi/#handle-an-incoming-message is
// conducted in ChromeDriver only if there is no active session.
// Otherwise it is delegated to BiDiMapper.
if (session_id.empty()) {
if (kKnownBidiSessionCommands.contains(*method)) {
Status invalid_session_error{kInvalidSessionId, "session not found"};
SendResponseOverWebSocket(http_server, connection_id, std::move(maybe_id),
invalid_session_error, nullptr, "", true);
} else {
Status unknown_static_command = {kUnknownCommand, *method};
SendResponseOverWebSocket(http_server, connection_id, std::move(maybe_id),
unknown_static_command, nullptr, session_id,
true);
}
return;
}
cmd_it = session_bidi_command_map_.find(*method);
if (cmd_it != session_bidi_command_map_.end()) {
CommandCallback callback = base::BindRepeating(
&HttpHandler::SendResponseOverWebSocket, weak_ptr_factory_.GetWeakPtr(),
http_server, connection_id, std::move(maybe_id));
cmd_it->second.Run(parsed, session_id, std::move(callback));
return;
}
// Session command handling is delegated to BiDiMapper.
base::Value::Dict params;
params.Set("bidiCommand", std::move(parsed));
params.Set("connectionId", connection_id);
forward_session_command_.Run(
params, session_id,
base::BindRepeating(&HttpHandler::SendResponseOverWebSocket,
weak_ptr_factory_.GetWeakPtr(), http_server,
connection_id, std::move(maybe_id)));
}
void HttpHandler::OnWebSocketResponseOnCmdThread(
HttpServerInterface* http_server,
int connection_id,
const std::string& data) {
io_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpServerInterface::SendOverWebSocket,
base::Unretained(http_server), connection_id, data));
}
void HttpHandler::OnWebSocketResponseOnSessionThread(
HttpServerInterface* http_server,
int connection_id,
const std::string& data) {
cmd_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&HttpHandler::OnWebSocketResponseOnCmdThread, WeakPtr(),
base::Unretained(http_server), connection_id, data));
}
void HttpHandler::OnClose(HttpServerInterface* http_server, int connection_id) {
auto it = connection_session_map_.find(connection_id);
if (it == connection_session_map_.end()) {
return;
}
std::string session_id = it->second;
auto ses_it = session_connection_map_.find(session_id);
// This situation can never happen: the session related entry is removed from
// the session_connection_map_ only after all connections have been closed
// either by the client or by the session thread.
// Therefore if the session related entry is missing in the
// session_connection_map_ the corresponding connection entry must miss in the
// connection_session_map_. This situation is handled above.
// We leave this check just to be on the safe side.
if (ses_it == session_connection_map_.end()) {
VLOG(logging::LOGGING_WARNING)
<< "Session related entry is missing in session_connection_map_.";
return;
}
std::vector<int>& bucket = ses_it->second;
auto bucket_it = std::ranges::find(bucket, connection_id);
// The case when it can happen:
// The session thread has sent a response (e.g. Quit command) to the client.
// After that the session thread preempted before closing all connections.
// The client has handled the response and closed all connections.
// The command thread has handled the connection close requests initiated by
// the client. Therefore the connection is no longer in the bucket.
// The session thread wakes up and posts a request to close all connections.
// The request arrives to the CMD thread but some or all connections don't
// exist any longer.
// TODO (crbug.com/chromedriver/4597): Fix this by callback chaining.
// The reproducer is testConnectionIsClosedIfSessionIsDestroyed that flakes
// from time to time.
if (bucket_it == bucket.end()) {
return;
}
bucket.erase(bucket_it);
connection_session_map_.erase(it);
auto thread_it = session_thread_map_.find(session_id);
// check first that the session thread is still alive
if (thread_it != session_thread_map_.end()) {
thread_it->second->thread()->task_runner()->PostTask(
FROM_HERE,
base::BindOnce(&RemoveBidiConnectionOnSessionThread, connection_id));
}
}
void HttpHandler::SendWebSocketRejectResponse(
base::RepeatingCallback<void(int,
const net::HttpServerResponseInfo&,
const net::NetworkTrafficAnnotationTag&)>
send_http_response,
int connection_id,
net::HttpStatusCode code,
const std::string& msg) {
io_task_runner_->PostTask(
FROM_HERE, base::BindOnce(std::move(send_http_response), connection_id,
CreateWebSocketRejectResponse(code, msg),
TRAFFIC_ANNOTATION_FOR_TESTS));
}
const char internal::kNewSessionPathPattern[] = "session";
bool internal::MatchesCommand(const std::string& method,
const std::string& path,
const CommandMapping& command,
std::string* session_id,
base::Value::Dict* out_params) {
if (!MatchesMethod(command.method, method))
return false;
std::vector<std::string> path_parts = base::SplitString(
path, "/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
std::vector<std::string> command_path_parts = base::SplitString(
command.path_pattern, "/", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if (path_parts.size() != command_path_parts.size())
return false;
base::Value::Dict params;
for (size_t i = 0; i < path_parts.size(); ++i) {
CHECK(command_path_parts[i].length());
if (command_path_parts[i][0] == ':') {
std::string name = command_path_parts[i];
name.erase(0, 1);
CHECK(name.length());
url::RawCanonOutputT<char16_t> output;
url::DecodeURLEscapeSequences(
path_parts[i], url::DecodeURLMode::kUTF8OrIsomorphic, &output);
std::string decoded = base::UTF16ToASCII(output.view());
// Due to crbug.com/533361, the url decoding libraries decodes all of the
// % escape sequences except for %%. We need to handle this case manually.
// So, replacing all the instances of "%%" with "%".
base::ReplaceSubstringsAfterOffset(&decoded, 0 , "%%" , "%");
if (name == "sessionId")
*session_id = decoded;
else
params.Set(name, decoded);
} else if (command_path_parts[i] != path_parts[i]) {
return false;
}
}
out_params->Merge(std::move(params));
return true;
}
bool internal::IsNewSession(const CommandMapping& command) {
return command.method == kPost &&
command.path_pattern == internal::kNewSessionPathPattern;
}
Status internal::ParseBidiCommand(const std::string& data,
base::Value::Dict& parsed) {
Status status{kOk};
std::optional<base::Value> maybe_bidi_command = base::JSONReader::Read(data);
if (!maybe_bidi_command.has_value()) {
return Status{kInvalidArgument, "unable to parse BiDi command: " + data};
}
if (!maybe_bidi_command->is_dict()) {
return Status(kInvalidArgument,
"a JSON dictionary is expected as a BiDi command: " + data);
}
parsed = std::move(maybe_bidi_command->GetDict());
std::optional<double> maybe_id = parsed.FindDouble("id");
if (!maybe_id) {
return Status(kInvalidArgument,
"BiDi command has no 'id' of type js-uint: " + data);
}
std::string* maybe_method = parsed.FindString("method");
if (!maybe_method) {
return Status(kInvalidArgument,
"BiDi command has no 'method' of type string: " + data);
}
base::Value::Dict* maybe_params = parsed.FindDict("params");
if (!maybe_params) {
return Status(kInvalidArgument,
"BiDi command has no 'params' of type dictionary: " + data);
}
return status;
}
base::Value::Dict internal::CreateBidiErrorResponse(
Status status,
std::optional<base::Value> maybe_id) {
base::Value::Dict ret;
// Error is generated by ChromeDriver
ret.Set("type", "error");
ret.Set("message", status.message());
ret.Set("error", StatusCodeToString(status.code()));
if (maybe_id.has_value()) {
ret.Set("id", std::move(*maybe_id));
}
return ret;
}