Everywhere: Hoist the Services folder to the top-level

This commit is contained in:
Timothy Flynn 2024-11-09 12:13:18 -05:00 committed by Andreas Kling
commit 22e0eeada2
Notes: github-actions[bot] 2024-11-10 11:52:06 +00:00
68 changed files with 41 additions and 41 deletions

View file

@ -0,0 +1,16 @@
set(SOURCES
${LADYBIRD_SOURCE_DIR}/Ladybird/Utilities.cpp
Client.cpp
Session.cpp
WebContentConnection.cpp
main.cpp
)
add_executable(WebDriver ${SOURCES})
target_include_directories(WebDriver PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../..)
target_include_directories(WebDriver PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/../..)
target_include_directories(WebDriver PRIVATE ${LADYBIRD_SOURCE_DIR}/Userland)
target_include_directories(WebDriver PRIVATE ${LADYBIRD_SOURCE_DIR}/Services)
target_link_libraries(WebDriver PRIVATE LibCore LibFileSystem LibGfx LibIPC LibJS LibMain LibWeb LibWebSocket LibWebView)

View file

@ -0,0 +1,877 @@
/*
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Debug.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Timer.h>
#include <LibWeb/WebDriver/Capabilities.h>
#include <WebDriver/Client.h>
namespace WebDriver {
Atomic<unsigned> Client::s_next_session_id;
HashMap<unsigned, NonnullRefPtr<Session>> Client::s_sessions;
ErrorOr<NonnullRefPtr<Client>> Client::try_create(NonnullOwnPtr<Core::BufferedTCPSocket> socket, LaunchBrowserCallbacks callbacks, Core::EventReceiver* parent)
{
if (!callbacks.launch_browser || !callbacks.launch_headless_browser)
return Error::from_string_literal("All callbacks to launch a browser must be provided");
TRY(socket->set_blocking(true));
return adopt_nonnull_ref_or_enomem(new (nothrow) Client(move(socket), move(callbacks), parent));
}
Client::Client(NonnullOwnPtr<Core::BufferedTCPSocket> socket, LaunchBrowserCallbacks callbacks, Core::EventReceiver* parent)
: Web::WebDriver::Client(move(socket), parent)
, m_callbacks(move(callbacks))
{
}
Client::~Client() = default;
ErrorOr<NonnullRefPtr<Session>, Web::WebDriver::Error> Client::find_session_with_id(StringView session_id, AllowInvalidWindowHandle allow_invalid_window_handle)
{
auto session_id_or_error = session_id.to_number<unsigned>();
if (!session_id_or_error.has_value())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidSessionId, "Invalid session id");
if (auto session = s_sessions.get(*session_id_or_error); session.has_value()) {
if (allow_invalid_window_handle == AllowInvalidWindowHandle::No)
TRY(session.value()->ensure_current_window_handle_is_valid());
return *session.release_value();
}
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidSessionId, "Invalid session id");
}
void Client::close_session(unsigned session_id)
{
if (s_sessions.remove(session_id))
dbgln_if(WEBDRIVER_DEBUG, "Shut down session {}", session_id);
else
dbgln_if(WEBDRIVER_DEBUG, "Unable to shut down session {}: Not found", session_id);
}
// 8.1 New Session, https://w3c.github.io/webdriver/#dfn-new-sessions
// POST /session
Web::WebDriver::Response Client::new_session(Web::WebDriver::Parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session");
// FIXME: 1. If the maximum active sessions is equal to the length of the list of active sessions,
// return error with error code session not created.
// FIXME: 2. If the remote end is an intermediary node, take implementation-defined steps that either
// result in returning an error with error code session not created, or in returning a
// success with data that is isomorphic to that returned by remote ends according to the
// rest of this algorithm. If an error is not returned, the intermediary node must retain a
// reference to the session created on the upstream node as the associated session such
// that commands may be forwarded to this associated session on subsequent commands.
// FIXME: 3. If the maximum active sessions is equal to the length of the list of active sessions,
// return error with error code session not created.
// 4. Let capabilities be the result of trying to process capabilities with parameters as an argument.
auto capabilities = TRY(Web::WebDriver::process_capabilities(payload));
// 5. If capabilitiess is null, return error with error code session not created.
if (capabilities.is_null())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::SessionNotCreated, "Could not match capabilities"sv);
// 6. Let session id be the result of generating a UUID.
// FIXME: Actually create a UUID.
auto session_id = Client::s_next_session_id++;
// 7. Let session be a new session with the session ID of session id.
Web::WebDriver::LadybirdOptions options { capabilities.as_object() };
auto session = make_ref_counted<Session>(session_id, *this, move(options));
if (auto start_result = session->start(m_callbacks); start_result.is_error())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::SessionNotCreated, ByteString::formatted("Failed to start session: {}", start_result.error()));
auto& web_content_connection = session->web_content_connection();
// FIXME: 8. Set the current session to session.
// FIXME: 9. Run any WebDriver new session algorithm defined in external specifications,
// with arguments session and capabilities.
// 10. Append session to active sessions.
s_sessions.set(session_id, session);
// NOTE: We do step 12 before 11 because step 12 mutates the capabilities we set in step 11.
// 12. Initialize the following from capabilities:
session->initialize_from_capabilities(capabilities.as_object());
// 11. Let body be a JSON Object initialized with:
JsonObject body;
// "sessionId"
// session id
body.set("sessionId", ByteString::number(session_id));
// "capabilities"
// capabilities
body.set("capabilities", move(capabilities));
// 13. Set the webdriver-active flag to true.
web_content_connection.async_set_is_webdriver_active(true);
// FIXME: 14. Set the current top-level browsing context for session with the top-level browsing context
// of the UAs current browsing context.
// FIXME: 15. Set the request queue to a new queue.
// 16. Return success with data body.
return JsonValue { move(body) };
}
// 8.2 Delete Session, https://w3c.github.io/webdriver/#dfn-delete-session
// DELETE /session/{session id}
Web::WebDriver::Response Client::delete_session(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling DELETE /session/<session_id>");
// 1. If the current session is an active session, try to close the session.
if (auto session = find_session_with_id(parameters[0], AllowInvalidWindowHandle::Yes); !session.is_error())
close_session(session.value()->session_id());
// 2. Return success with data null.
return JsonValue {};
}
// 8.3 Status, https://w3c.github.io/webdriver/#dfn-status
// GET /status
Web::WebDriver::Response Client::get_status(Web::WebDriver::Parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /status");
// 1. Let body be a new JSON Object with the following properties:
// "ready"
// The remote ends readiness state.
// "message"
// An implementation-defined string explaining the remote ends readiness state.
// FIXME: Report if we are somehow not ready.
JsonObject body;
body.set("ready", true);
body.set("message", "Ready to start some sessions!");
// 2. Return success with data body.
return JsonValue { body };
}
// 9.1 Get Timeouts, https://w3c.github.io/webdriver/#dfn-get-timeouts
// GET /session/{session id}/timeouts
Web::WebDriver::Response Client::get_timeouts(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session id>/timeouts");
auto session = TRY(find_session_with_id(parameters[0]));
return session->web_content_connection().get_timeouts();
}
// 9.2 Set Timeouts, https://w3c.github.io/webdriver/#dfn-set-timeouts
// POST /session/{session id}/timeouts
Web::WebDriver::Response Client::set_timeouts(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session id>/timeouts");
auto session = TRY(find_session_with_id(parameters[0]));
return session->set_timeouts(move(payload));
}
// 10.1 Navigate To, https://w3c.github.io/webdriver/#dfn-navigate-to
// POST /session/{session id}/url
Web::WebDriver::Response Client::navigate_to(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/url");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.navigate_to(move(payload));
});
}
// 10.2 Get Current URL, https://w3c.github.io/webdriver/#dfn-get-current-url
// GET /session/{session id}/url
Web::WebDriver::Response Client::get_current_url(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/url");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_current_url();
});
}
// 10.3 Back, https://w3c.github.io/webdriver/#dfn-back
// POST /session/{session id}/back
Web::WebDriver::Response Client::back(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/back");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.back();
});
}
// 10.4 Forward, https://w3c.github.io/webdriver/#dfn-forward
// POST /session/{session id}/forward
Web::WebDriver::Response Client::forward(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/forward");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.forward();
});
}
// 10.5 Refresh, https://w3c.github.io/webdriver/#dfn-refresh
// POST /session/{session id}/refresh
Web::WebDriver::Response Client::refresh(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/refresh");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.refresh();
});
}
// 10.6 Get Title, https://w3c.github.io/webdriver/#dfn-get-title
// GET /session/{session id}/title
Web::WebDriver::Response Client::get_title(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/title");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_title();
});
}
// 11.1 Get Window Handle, https://w3c.github.io/webdriver/#get-window-handle
// GET /session/{session id}/window
Web::WebDriver::Response Client::get_window_handle(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/window");
auto session = TRY(find_session_with_id(parameters[0]));
// 1. If the current top-level browsing context is no longer open, return error with error code no such window.
TRY(session->web_content_connection().ensure_top_level_browsing_context_is_open());
// 2. Return success with data being the window handle associated with the current top-level browsing context.
return JsonValue { session->current_window_handle() };
}
// 11.2 Close Window, https://w3c.github.io/webdriver/#dfn-close-window
// DELETE /session/{session id}/window
Web::WebDriver::Response Client::close_window(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling DELETE /session/<session_id>/window");
auto session = TRY(find_session_with_id(parameters[0]));
return session->close_window();
}
// 11.3 Switch to Window, https://w3c.github.io/webdriver/#dfn-switch-to-window
// POST /session/{session id}/window
Web::WebDriver::Response Client::switch_to_window(Web::WebDriver::Parameters parameters, AK::JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window");
auto session = TRY(find_session_with_id(parameters[0], AllowInvalidWindowHandle::Yes));
if (!payload.is_object())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "Payload is not a JSON object");
// 1. Let handle be the result of getting the property "handle" from the parameters argument.
auto handle = payload.as_object().get("handle"sv);
// 2. If handle is undefined, return error with error code invalid argument.
if (!handle.has_value())
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::InvalidArgument, "No property called 'handle' present");
return session->switch_to_window(handle->as_string());
}
// 11.4 Get Window Handles, https://w3c.github.io/webdriver/#dfn-get-window-handles
// GET /session/{session id}/window/handles
Web::WebDriver::Response Client::get_window_handles(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/window/handles");
auto session = TRY(find_session_with_id(parameters[0], AllowInvalidWindowHandle::Yes));
return session->get_window_handles();
}
// 11.5 New Window, https://w3c.github.io/webdriver/#dfn-new-window
// POST /session/{session id}/window/new
Web::WebDriver::Response Client::new_window(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window/new");
auto session = TRY(find_session_with_id(parameters[0]));
auto handle = TRY(session->perform_async_action([&](auto& connection) {
return connection.new_window(move(payload));
}));
static constexpr u32 CONNECTION_TIMEOUT_MS = 5000;
auto timeout_fired = false;
auto timer = Core::Timer::create_single_shot(CONNECTION_TIMEOUT_MS, [&timeout_fired] { timeout_fired = true; });
timer->start();
Core::EventLoop::current().spin_until([&session, &timeout_fired, handle = handle.as_object().get("handle"sv)->as_string()]() {
return session->has_window_handle(handle) || timeout_fired;
});
if (timeout_fired)
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::Timeout, "Timed out waiting for window handle");
return handle;
}
// 11.6 Switch To Frame, https://w3c.github.io/webdriver/#dfn-switch-to-frame
// POST /session/{session id}/frame
Web::WebDriver::Response Client::switch_to_frame(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/frame");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.switch_to_frame(move(payload));
});
}
// 11.7 Switch To Parent Frame, https://w3c.github.io/webdriver/#dfn-switch-to-parent-frame
// POST /session/{session id}/frame/parent
Web::WebDriver::Response Client::switch_to_parent_frame(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/frame/parent");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.switch_to_parent_frame(move(payload));
});
}
// 11.8.1 Get Window Rect, https://w3c.github.io/webdriver/#dfn-get-window-rect
// GET /session/{session id}/window/rect
Web::WebDriver::Response Client::get_window_rect(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/window/rect");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_window_rect();
});
}
// 11.8.2 Set Window Rect, https://w3c.github.io/webdriver/#dfn-set-window-rect
// POST /session/{session id}/window/rect
Web::WebDriver::Response Client::set_window_rect(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window/rect");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.set_window_rect(move(payload));
});
}
// 11.8.3 Maximize Window, https://w3c.github.io/webdriver/#dfn-maximize-window
// POST /session/{session id}/window/maximize
Web::WebDriver::Response Client::maximize_window(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window/maximize");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.maximize_window();
});
}
// 11.8.4 Minimize Window, https://w3c.github.io/webdriver/#minimize-window
// POST /session/{session id}/window/minimize
Web::WebDriver::Response Client::minimize_window(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window/minimize");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.minimize_window();
});
}
// 11.8.5 Fullscreen Window, https://w3c.github.io/webdriver/#dfn-fullscreen-window
// POST /session/{session id}/window/fullscreen
Web::WebDriver::Response Client::fullscreen_window(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window/fullscreen");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.fullscreen_window();
});
}
// Extension: Consume User Activation, https://html.spec.whatwg.org/multipage/interaction.html#user-activation-user-agent-automation
// POST /session/{session id}/window/consume-user-activation
Web::WebDriver::Response Client::consume_user_activation(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/window/consume-user-activation");
auto session = TRY(find_session_with_id(parameters[0]));
return session->web_content_connection().consume_user_activation();
}
// 12.3.2 Find Element, https://w3c.github.io/webdriver/#dfn-find-element
// POST /session/{session id}/element
Web::WebDriver::Response Client::find_element(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.find_element(move(payload));
});
}
// 12.3.3 Find Elements, https://w3c.github.io/webdriver/#dfn-find-elements
// POST /session/{session id}/elements
Web::WebDriver::Response Client::find_elements(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/elements");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.find_elements(move(payload));
});
}
// 12.3.4 Find Element From Element, https://w3c.github.io/webdriver/#dfn-find-element-from-element
// POST /session/{session id}/element/{element id}/element
Web::WebDriver::Response Client::find_element_from_element(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/element");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.find_element_from_element(move(payload), move(parameters[1]));
});
}
// 12.3.5 Find Elements From Element, https://w3c.github.io/webdriver/#dfn-find-elements-from-element
// POST /session/{session id}/element/{element id}/elements
Web::WebDriver::Response Client::find_elements_from_element(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/elements");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.find_elements_from_element(move(payload), move(parameters[1]));
});
}
// 12.3.6 Find Element From Shadow Root, https://w3c.github.io/webdriver/#find-element-from-shadow-root
// POST /session/{session id}/shadow/{shadow id}/element
Web::WebDriver::Response Client::find_element_from_shadow_root(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/shadow/<shadow_id>/element");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.find_element_from_shadow_root(move(payload), move(parameters[1]));
});
}
// 12.3.7 Find Elements From Shadow Root, https://w3c.github.io/webdriver/#find-elements-from-shadow-root
// POST /session/{session id}/shadow/{shadow id}/elements
Web::WebDriver::Response Client::find_elements_from_shadow_root(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/shadow/<shadow_id>/elements");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.find_elements_from_shadow_root(move(payload), move(parameters[1]));
});
}
// 12.3.8 Get Active Element, https://w3c.github.io/webdriver/#get-active-element
// GET /session/{session id}/element/active
Web::WebDriver::Response Client::get_active_element(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/active");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_active_element();
});
}
// 12.3.9 Get Element Shadow Root, https://w3c.github.io/webdriver/#get-element-shadow-root
// GET /session/{session id}/element/{element id}/shadow
Web::WebDriver::Response Client::get_element_shadow_root(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/shadow");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_shadow_root(move(parameters[1]));
});
}
// 12.4.1 Is Element Selected, https://w3c.github.io/webdriver/#dfn-is-element-selected
// GET /session/{session id}/element/{element id}/selected
Web::WebDriver::Response Client::is_element_selected(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/selected");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.is_element_selected(move(parameters[1]));
});
}
// 12.4.2 Get Element Attribute, https://w3c.github.io/webdriver/#dfn-get-element-attribute
// GET /session/{session id}/element/{element id}/attribute/{name}
Web::WebDriver::Response Client::get_element_attribute(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/attribute/<name>");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_attribute(move(parameters[1]), move(parameters[2]));
});
}
// 12.4.3 Get Element Property, https://w3c.github.io/webdriver/#dfn-get-element-property
// GET /session/{session id}/element/{element id}/property/{name}
Web::WebDriver::Response Client::get_element_property(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/property/<name>");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_property(move(parameters[1]), move(parameters[2]));
});
}
// 12.4.4 Get Element CSS Value, https://w3c.github.io/webdriver/#dfn-get-element-css-value
// GET /session/{session id}/element/{element id}/css/{property name}
Web::WebDriver::Response Client::get_element_css_value(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/css/<property_name>");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_css_value(move(parameters[1]), move(parameters[2]));
});
}
// 12.4.5 Get Element Text, https://w3c.github.io/webdriver/#dfn-get-element-text
// GET /session/{session id}/element/{element id}/text
Web::WebDriver::Response Client::get_element_text(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/text");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_text(move(parameters[1]));
});
}
// 12.4.6 Get Element Tag Name, https://w3c.github.io/webdriver/#dfn-get-element-tag-name
// GET /session/{session id}/element/{element id}/name
Web::WebDriver::Response Client::get_element_tag_name(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/name");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_tag_name(move(parameters[1]));
});
}
// 12.4.7 Get Element Rect, https://w3c.github.io/webdriver/#dfn-get-element-rect
// GET /session/{session id}/element/{element id}/rect
Web::WebDriver::Response Client::get_element_rect(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/rect");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_element_rect(move(parameters[1]));
});
}
// 12.4.8 Is Element Enabled, https://w3c.github.io/webdriver/#dfn-is-element-enabled
// GET /session/{session id}/element/{element id}/enabled
Web::WebDriver::Response Client::is_element_enabled(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/enabled");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.is_element_enabled(move(parameters[1]));
});
}
// 12.4.9 https://w3c.github.io/webdriver/#dfn-get-computed-role
// GET /session/{session id}/element/{element id}/computedrole
Web::WebDriver::Response Client::get_computed_role(Web::WebDriver::Parameters parameters, AK::JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session id>/element/<element id>/computedrole");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_computed_role(move(parameters[1]));
});
}
// 12.4.10 Get Computed Label, https://w3c.github.io/webdriver/#get-computed-label
// GET /session/{session id}/element/{element id}/computedlabel
Web::WebDriver::Response Client::get_computed_label(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session id>/element/<element id>/computedlabel");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_computed_label(move(parameters[1]));
});
}
// 12.5.1 Element Click, https://w3c.github.io/webdriver/#element-click
// POST /session/{session id}/element/{element id}/click
Web::WebDriver::Response Client::element_click(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/click");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.element_click(move(parameters[1]));
});
}
// 12.5.2 Element Clear, https://w3c.github.io/webdriver/#dfn-element-clear
// POST /session/{session id}/element/{element id}/clear
Web::WebDriver::Response Client::element_clear(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/clear");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.element_clear(move(parameters[1]));
});
}
// 12.5.3 Element Send Keys, https://w3c.github.io/webdriver/#dfn-element-send-keys
// POST /session/{session id}/element/{element id}/value
Web::WebDriver::Response Client::element_send_keys(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/element/<element_id>/value");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.element_send_keys(move(parameters[1]), move(payload));
});
}
// 13.1 Get Page Source, https://w3c.github.io/webdriver/#dfn-get-page-source
// GET /session/{session id}/source
Web::WebDriver::Response Client::get_source(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/source");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_source();
});
}
// 13.2.1 Execute Script, https://w3c.github.io/webdriver/#dfn-execute-script
// POST /session/{session id}/execute/sync
Web::WebDriver::Response Client::execute_script(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/execute/sync");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.execute_script(move(payload));
});
}
// 13.2.2 Execute Async Script, https://w3c.github.io/webdriver/#dfn-execute-async-script
// POST /session/{session id}/execute/async
Web::WebDriver::Response Client::execute_async_script(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/execute/async");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.execute_async_script(move(payload));
});
}
// 14.1 Get All Cookies, https://w3c.github.io/webdriver/#dfn-get-all-cookies
// GET /session/{session id}/cookie
Web::WebDriver::Response Client::get_all_cookies(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/cookie");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_all_cookies();
});
}
// 14.2 Get Named Cookie, https://w3c.github.io/webdriver/#dfn-get-named-cookie
// GET /session/{session id}/cookie/{name}
Web::WebDriver::Response Client::get_named_cookie(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/cookie/<name>");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.get_named_cookie(move(parameters[1]));
});
}
// 14.3 Add Cookie, https://w3c.github.io/webdriver/#dfn-adding-a-cookie
// POST /session/{session id}/cookie
Web::WebDriver::Response Client::add_cookie(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/cookie");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.add_cookie(move(payload));
});
}
// 14.4 Delete Cookie, https://w3c.github.io/webdriver/#dfn-delete-cookie
// DELETE /session/{session id}/cookie/{name}
Web::WebDriver::Response Client::delete_cookie(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling DELETE /session/<session_id>/cookie/<name>");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.delete_cookie(move(parameters[1]));
});
}
// 14.5 Delete All Cookies, https://w3c.github.io/webdriver/#dfn-delete-all-cookies
// DELETE /session/{session id}/cookie
Web::WebDriver::Response Client::delete_all_cookies(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling DELETE /session/<session_id>/cookie");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.delete_all_cookies();
});
}
// 15.7 Perform Actions, https://w3c.github.io/webdriver/#perform-actions
// POST /session/{session id}/actions
Web::WebDriver::Response Client::perform_actions(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/actions");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.perform_actions(move(payload));
});
}
// 15.8 Release Actions, https://w3c.github.io/webdriver/#release-actions
// DELETE /session/{session id}/actions
Web::WebDriver::Response Client::release_actions(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling DELETE /session/<session_id>/actions");
auto session = TRY(find_session_with_id(parameters[0]));
return session->web_content_connection().release_actions();
}
// 16.1 Dismiss Alert, https://w3c.github.io/webdriver/#dismiss-alert
// POST /session/{session id}/alert/dismiss
Web::WebDriver::Response Client::dismiss_alert(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/alert/dismiss");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.dismiss_alert();
});
}
// 16.2 Accept Alert, https://w3c.github.io/webdriver/#accept-alert
// POST /session/{session id}/alert/accept
Web::WebDriver::Response Client::accept_alert(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/alert/accept");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.accept_alert();
});
}
// 16.3 Get Alert Text, https://w3c.github.io/webdriver/#get-alert-text
// GET /session/{session id}/alert/text
Web::WebDriver::Response Client::get_alert_text(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/alert/text");
auto session = TRY(find_session_with_id(parameters[0]));
return session->web_content_connection().get_alert_text();
}
// 16.4 Send Alert Text, https://w3c.github.io/webdriver/#send-alert-text
// POST /session/{session id}/alert/text
Web::WebDriver::Response Client::send_alert_text(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session_id>/alert/text");
auto session = TRY(find_session_with_id(parameters[0]));
return session->web_content_connection().send_alert_text(payload);
}
// 17.1 Take Screenshot, https://w3c.github.io/webdriver/#take-screenshot
// GET /session/{session id}/screenshot
Web::WebDriver::Response Client::take_screenshot(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/screenshot");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.take_screenshot();
});
}
// 17.2 Take Element Screenshot, https://w3c.github.io/webdriver/#dfn-take-element-screenshot
// GET /session/{session id}/element/{element id}/screenshot
Web::WebDriver::Response Client::take_element_screenshot(Web::WebDriver::Parameters parameters, JsonValue)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling GET /session/<session_id>/element/<element_id>/screenshot");
auto session = TRY(find_session_with_id(parameters[0]));
return session->perform_async_action([&](auto& connection) {
return connection.take_element_screenshot(move(parameters[1]));
});
}
// 18.1 Print Page, https://w3c.github.io/webdriver/#dfn-print-page
// POST /session/{session id}/print
Web::WebDriver::Response Client::print_page(Web::WebDriver::Parameters parameters, JsonValue payload)
{
dbgln_if(WEBDRIVER_DEBUG, "Handling POST /session/<session id>/print");
auto session = TRY(find_session_with_id(parameters[0]));
return session->web_content_connection().print_page(move(payload));
}
}

113
Services/WebDriver/Client.h Normal file
View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022, Tim Flynn <trflynn89@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Function.h>
#include <AK/NonnullRefPtr.h>
#include <LibCore/EventReceiver.h>
#include <LibWeb/WebDriver/Client.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWeb/WebDriver/Response.h>
#include <WebDriver/Session.h>
namespace WebDriver {
struct LaunchBrowserCallbacks {
Function<ErrorOr<pid_t>(ByteString const&)> launch_browser;
Function<ErrorOr<pid_t>(ByteString const&)> launch_headless_browser;
};
class Client final : public Web::WebDriver::Client {
C_OBJECT_ABSTRACT(Client);
public:
static ErrorOr<NonnullRefPtr<Client>> try_create(NonnullOwnPtr<Core::BufferedTCPSocket>, LaunchBrowserCallbacks, Core::EventReceiver* parent);
virtual ~Client() override;
void close_session(unsigned session_id);
private:
Client(NonnullOwnPtr<Core::BufferedTCPSocket>, LaunchBrowserCallbacks, Core::EventReceiver* parent);
enum class AllowInvalidWindowHandle {
No,
Yes,
};
ErrorOr<NonnullRefPtr<Session>, Web::WebDriver::Error> find_session_with_id(StringView session_id, AllowInvalidWindowHandle = AllowInvalidWindowHandle::No);
virtual Web::WebDriver::Response new_session(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response delete_session(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_status(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_timeouts(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response set_timeouts(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response navigate_to(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_current_url(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response back(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response forward(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response refresh(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_title(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_window_handle(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response close_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response switch_to_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_window_handles(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response new_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response switch_to_frame(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response switch_to_parent_frame(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_window_rect(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response set_window_rect(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response maximize_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response minimize_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response fullscreen_window(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response consume_user_activation(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_element(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_elements(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_element_from_element(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_elements_from_element(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_element_from_shadow_root(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response find_elements_from_shadow_root(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_active_element(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_shadow_root(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response is_element_selected(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_attribute(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_property(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_css_value(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_text(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_tag_name(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_element_rect(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response is_element_enabled(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_computed_role(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_computed_label(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response element_click(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response element_clear(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response element_send_keys(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_source(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response execute_script(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response execute_async_script(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_all_cookies(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_named_cookie(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response add_cookie(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response delete_cookie(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response delete_all_cookies(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response perform_actions(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response release_actions(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response dismiss_alert(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response accept_alert(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response get_alert_text(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response send_alert_text(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response take_screenshot(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response take_element_screenshot(Web::WebDriver::Parameters parameters, JsonValue payload) override;
virtual Web::WebDriver::Response print_page(Web::WebDriver::Parameters parameters, JsonValue payload) override;
static HashMap<unsigned, NonnullRefPtr<Session>> s_sessions;
static Atomic<unsigned> s_next_session_id;
LaunchBrowserCallbacks m_callbacks;
};
}

View file

@ -0,0 +1,256 @@
/*
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
* Copyright (c) 2022, Sam Atkins <atkinssj@serenityos.org>
* Copyright (c) 2022, Tobias Christiansen <tobyase@serenityos.org>
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "Session.h"
#include "Client.h"
#include <AK/JsonObject.h>
#include <LibCore/LocalServer.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibWeb/WebDriver/TimeoutsConfiguration.h>
#include <unistd.h>
namespace WebDriver {
Session::Session(unsigned session_id, NonnullRefPtr<Client> client, Web::WebDriver::LadybirdOptions options)
: m_client(move(client))
, m_options(move(options))
, m_id(session_id)
{
}
// https://w3c.github.io/webdriver/#dfn-close-the-session
Session::~Session()
{
if (!m_started)
return;
// 1. Perform the following substeps based on the remote ends type:
// NOTE: We perform the "Remote end is an endpoint node" steps in the WebContent process.
for (auto& it : m_windows) {
it.value.web_content_connection->close_session();
}
// 2. Remove the current session from active sessions.
// NOTE: We are in a session destruction which means it is already removed
// from active sessions
// 3. Perform any implementation-specific cleanup steps.
if (m_browser_pid.has_value()) {
MUST(Core::System::kill(*m_browser_pid, SIGTERM));
m_browser_pid = {};
}
if (m_web_content_socket_path.has_value()) {
MUST(Core::System::unlink(*m_web_content_socket_path));
m_web_content_socket_path = {};
}
}
// Step 12 of https://w3c.github.io/webdriver/#dfn-new-sessions
void Session::initialize_from_capabilities(JsonObject& capabilities)
{
auto& connection = web_content_connection();
// 1. Let strategy be the result of getting property "pageLoadStrategy" from capabilities.
auto strategy = capabilities.get_byte_string("pageLoadStrategy"sv);
// 2. If strategy is a string, set the current sessions page loading strategy to strategy. Otherwise, set the page loading strategy to normal and set a property of capabilities with name "pageLoadStrategy" and value "normal".
if (strategy.has_value()) {
m_page_load_strategy = Web::WebDriver::page_load_strategy_from_string(*strategy);
connection.async_set_page_load_strategy(m_page_load_strategy);
} else {
capabilities.set("pageLoadStrategy"sv, "normal"sv);
}
// 3. Let strictFileInteractability be the result of getting property "strictFileInteractability" from capabilities.
auto strict_file_interactiblity = capabilities.get_bool("strictFileInteractability"sv);
// 4. If strictFileInteractability is a boolean, set the current sessions strict file interactability to strictFileInteractability. Otherwise set the current sessions strict file interactability to false.
if (strict_file_interactiblity.has_value()) {
m_strict_file_interactiblity = *strict_file_interactiblity;
connection.async_set_strict_file_interactability(m_strict_file_interactiblity);
} else {
capabilities.set("strictFileInteractability"sv, false);
}
// FIXME: 5. Let proxy be the result of getting property "proxy" from capabilities and run the substeps of the first matching statement:
// FIXME: proxy is a proxy configuration object
// FIXME: Take implementation-defined steps to set the user agent proxy using the extracted proxy configuration. If the defined proxy cannot be configured return error with error code session not created.
// FIXME: Otherwise
// FIXME: Set a property of capabilities with name "proxy" and a value that is a new JSON Object.
// 6. If capabilities has a property with the key "timeouts":
if (auto timeouts = capabilities.get_object("timeouts"sv); timeouts.has_value()) {
// a. Let timeouts be the result of trying to JSON deserialize as a timeouts configuration the value of the "timeouts" property.
// NOTE: This happens on the remote end.
// b. Make the session timeouts the new timeouts.
MUST(set_timeouts(*timeouts));
} else {
// 7. Set a property on capabilities with name "timeouts" and value that of the JSON deserialization of the session timeouts.
capabilities.set("timeouts"sv, Web::WebDriver::timeouts_object({}));
}
// 8. Apply changes to the user agent for any implementation-defined capabilities selected during the capabilities processing step.
if (auto behavior = capabilities.get_byte_string("unhandledPromptBehavior"sv); behavior.has_value()) {
m_unhandled_prompt_behavior = Web::WebDriver::unhandled_prompt_behavior_from_string(*behavior);
connection.async_set_unhandled_prompt_behavior(m_unhandled_prompt_behavior);
} else {
capabilities.set("unhandledPromptBehavior"sv, "dismiss and notify"sv);
}
}
ErrorOr<NonnullRefPtr<Core::LocalServer>> Session::create_server(NonnullRefPtr<ServerPromise> promise)
{
static_assert(IsSame<IPC::Transport, IPC::TransportSocket>, "Need to handle other IPC transports here");
dbgln("Listening for WebDriver connection on {}", *m_web_content_socket_path);
(void)Core::System::unlink(*m_web_content_socket_path);
auto server = TRY(Core::LocalServer::try_create());
server->listen(*m_web_content_socket_path);
server->on_accept = [this, promise](auto client_socket) {
auto maybe_connection = adopt_nonnull_ref_or_enomem(new (nothrow) WebContentConnection(IPC::Transport(move(client_socket))));
if (maybe_connection.is_error()) {
promise->resolve(maybe_connection.release_error());
return;
}
dbgln("WebDriver is connected to WebContent socket");
auto web_content_connection = maybe_connection.release_value();
auto maybe_window_handle = web_content_connection->get_window_handle();
if (maybe_window_handle.is_error()) {
promise->reject(Error::from_string_literal("Window was closed immediately"));
return;
}
auto window_handle = MUST(String::from_byte_string(maybe_window_handle.value().as_string()));
web_content_connection->on_close = [this, window_handle]() {
dbgln_if(WEBDRIVER_DEBUG, "Window {} was closed remotely.", window_handle);
m_windows.remove(window_handle);
if (m_windows.is_empty())
m_client->close_session(session_id());
};
web_content_connection->async_set_page_load_strategy(m_page_load_strategy);
web_content_connection->async_set_strict_file_interactability(m_strict_file_interactiblity);
web_content_connection->async_set_unhandled_prompt_behavior(m_unhandled_prompt_behavior);
if (m_timeouts_configuration.has_value())
web_content_connection->async_set_timeouts(*m_timeouts_configuration);
m_windows.set(window_handle, Session::Window { window_handle, move(web_content_connection) });
if (m_current_window_handle.is_empty())
m_current_window_handle = window_handle;
promise->resolve({});
};
server->on_accept_error = [promise](auto error) {
promise->resolve(move(error));
};
return server;
}
ErrorOr<void> Session::start(LaunchBrowserCallbacks const& callbacks)
{
auto promise = TRY(ServerPromise::try_create());
m_web_content_socket_path = ByteString::formatted("{}/webdriver/session_{}_{}", TRY(Core::StandardPaths::runtime_directory()), getpid(), m_id);
m_web_content_server = TRY(create_server(promise));
if (m_options.headless)
m_browser_pid = TRY(callbacks.launch_headless_browser(*m_web_content_socket_path));
else
m_browser_pid = TRY(callbacks.launch_browser(*m_web_content_socket_path));
// FIXME: Allow this to be more asynchronous. For now, this at least allows us to propagate
// errors received while accepting the Browser and WebContent sockets.
TRY(TRY(promise->await()));
m_started = true;
return {};
}
Web::WebDriver::Response Session::set_timeouts(JsonValue payload)
{
m_timeouts_configuration = TRY(web_content_connection().set_timeouts(move(payload)));
return JsonValue {};
}
// 11.2 Close Window, https://w3c.github.io/webdriver/#dfn-close-window
Web::WebDriver::Response Session::close_window()
{
// 3. Close the current top-level browsing context.
TRY(perform_async_action([&](auto& connection) {
return connection.close_window();
}));
{
// Defer removing the window handle from this session until after we know we are done with its connection.
ScopeGuard guard { [this] { m_windows.remove(m_current_window_handle); m_current_window_handle = "NoSuchWindowPleaseSelectANewOne"_string; } };
// 4. If there are no more open top-level browsing contexts, then close the session.
if (m_windows.size() == 1)
m_client->close_session(session_id());
}
// 5. Return the result of running the remote end steps for the Get Window Handles command.
return get_window_handles();
}
// 11.3 Switch to Window, https://w3c.github.io/webdriver/#dfn-switch-to-window
Web::WebDriver::Response Session::switch_to_window(StringView handle)
{
// 4. If handle is equal to the associated window handle for some top-level browsing context in the
// current session, let context be the that browsing context, and set the current top-level
// browsing context with context.
// Otherwise, return error with error code no such window.
if (auto it = m_windows.find(handle); it != m_windows.end())
m_current_window_handle = it->key;
else
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found");
// 5. Update any implementation-specific state that would result from the user selecting the current
// browsing context for interaction, without altering OS-level focus.
TRY(web_content_connection().switch_to_window(m_current_window_handle));
// 6. Return success with data null.
return JsonValue {};
}
// 11.4 Get Window Handles, https://w3c.github.io/webdriver/#dfn-get-window-handles
Web::WebDriver::Response Session::get_window_handles() const
{
// 1. Let handles be a JSON List.
JsonArray handles {};
// 2. For each top-level browsing context in the remote end, push the associated window handle onto handles.
for (auto const& window_handle : m_windows.keys()) {
handles.must_append(JsonValue(window_handle));
}
// 3. Return success with data handles.
return JsonValue { move(handles) };
}
ErrorOr<void, Web::WebDriver::Error> Session::ensure_current_window_handle_is_valid() const
{
if (auto current_window = m_windows.get(m_current_window_handle); current_window.has_value())
return {};
return Web::WebDriver::Error::from_code(Web::WebDriver::ErrorCode::NoSuchWindow, "Window not found"sv);
}
}

View file

@ -0,0 +1,108 @@
/*
* Copyright (c) 2022, Florent Castelli <florent.castelli@gmail.com>
* Copyright (c) 2022, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/Error.h>
#include <AK/JsonValue.h>
#include <AK/RefCounted.h>
#include <AK/RefPtr.h>
#include <AK/ScopeGuard.h>
#include <AK/String.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Promise.h>
#include <LibWeb/WebDriver/Capabilities.h>
#include <LibWeb/WebDriver/Error.h>
#include <LibWeb/WebDriver/Response.h>
#include <WebDriver/WebContentConnection.h>
#include <unistd.h>
namespace WebDriver {
struct LaunchBrowserCallbacks;
class Session : public RefCounted<Session> {
public:
Session(unsigned session_id, NonnullRefPtr<Client> client, Web::WebDriver::LadybirdOptions options);
~Session();
void initialize_from_capabilities(JsonObject&);
unsigned session_id() const { return m_id; }
struct Window {
String handle;
NonnullRefPtr<WebContentConnection> web_content_connection;
};
WebContentConnection& web_content_connection() const
{
auto current_window = m_windows.get(m_current_window_handle);
VERIFY(current_window.has_value());
return current_window->web_content_connection;
}
String const& current_window_handle() const
{
return m_current_window_handle;
}
bool has_window_handle(StringView handle) const { return m_windows.contains(handle); }
ErrorOr<void> start(LaunchBrowserCallbacks const&);
Web::WebDriver::Response set_timeouts(JsonValue);
Web::WebDriver::Response close_window();
Web::WebDriver::Response switch_to_window(StringView);
Web::WebDriver::Response get_window_handles() const;
ErrorOr<void, Web::WebDriver::Error> ensure_current_window_handle_is_valid() const;
template<typename Action>
Web::WebDriver::Response perform_async_action(Action&& action)
{
Optional<Web::WebDriver::Response> response;
auto& connection = web_content_connection();
ScopeGuard guard { [&]() { connection.on_driver_execution_complete = nullptr; } };
connection.on_driver_execution_complete = [&](auto result) { response = move(result); };
TRY(action(connection));
Core::EventLoop::current().spin_until([&]() {
return response.has_value();
});
return response.release_value();
}
private:
using ServerPromise = Core::Promise<ErrorOr<void>>;
ErrorOr<NonnullRefPtr<Core::LocalServer>> create_server(NonnullRefPtr<ServerPromise> promise);
NonnullRefPtr<Client> m_client;
Web::WebDriver::LadybirdOptions m_options;
bool m_started { false };
unsigned m_id { 0 };
HashMap<String, Window> m_windows;
String m_current_window_handle;
Optional<ByteString> m_web_content_socket_path;
Optional<pid_t> m_browser_pid;
RefPtr<Core::LocalServer> m_web_content_server;
Web::WebDriver::PageLoadStrategy m_page_load_strategy { Web::WebDriver::PageLoadStrategy::Normal };
Web::WebDriver::UnhandledPromptBehavior m_unhandled_prompt_behavior { Web::WebDriver::UnhandledPromptBehavior::DismissAndNotify };
Optional<JsonValue> m_timeouts_configuration;
bool m_strict_file_interactiblity { false };
};
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <WebDriver/Client.h>
#include <WebDriver/WebContentConnection.h>
namespace WebDriver {
WebContentConnection::WebContentConnection(IPC::Transport transport)
: IPC::ConnectionFromClient<WebDriverClientEndpoint, WebDriverServerEndpoint>(*this, move(transport), 1)
{
}
void WebContentConnection::die()
{
if (on_close)
on_close();
}
void WebContentConnection::driver_execution_complete(Web::WebDriver::Response const& response)
{
if (on_driver_execution_complete)
on_driver_execution_complete(response);
}
}

View file

@ -0,0 +1,33 @@
/*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibIPC/ConnectionFromClient.h>
#include <LibIPC/Transport.h>
#include <WebContent/WebDriverClientEndpoint.h>
#include <WebContent/WebDriverServerEndpoint.h>
namespace WebDriver {
class Client;
class WebContentConnection
: public IPC::ConnectionFromClient<WebDriverClientEndpoint, WebDriverServerEndpoint> {
C_OBJECT_ABSTRACT(WebContentConnection)
public:
explicit WebContentConnection(IPC::Transport transport);
Function<void()> on_close;
Function<void(Web::WebDriver::Response)> on_driver_execution_complete;
private:
virtual void die() override;
virtual void driver_execution_complete(Web::WebDriver::Response const&) override;
};
}

142
Services/WebDriver/main.cpp Normal file
View file

@ -0,0 +1,142 @@
/*
* Copyright (c) 2022-2024, Tim Flynn <trflynn89@laybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Platform.h>
#include <Ladybird/Utilities.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/Directory.h>
#include <LibCore/EventLoop.h>
#include <LibCore/Process.h>
#include <LibCore/StandardPaths.h>
#include <LibCore/System.h>
#include <LibCore/TCPServer.h>
#include <LibMain/Main.h>
#include <LibWeb/WebDriver/Capabilities.h>
#include <WebDriver/Client.h>
static Vector<ByteString> certificates;
static ErrorOr<pid_t> launch_process(StringView application, ReadonlySpan<ByteString> arguments)
{
auto paths = TRY(get_paths_for_helper_process(application));
ErrorOr<pid_t> result = -1;
for (auto const& path : paths) {
auto path_view = path.view();
result = Core::Process::spawn(path_view, arguments, {}, Core::Process::KeepAsChild::Yes);
if (!result.is_error())
break;
}
return result;
}
static Vector<ByteString> create_arguments(ByteString const& socket_path, bool force_cpu_painting)
{
Vector<ByteString> arguments {
"--webdriver-content-path"sv,
socket_path,
};
Vector<ByteString> certificate_args;
for (auto const& certificate : certificates) {
certificate_args.append(ByteString::formatted("--certificate={}", certificate));
arguments.append(certificate_args.last().view().characters_without_null_termination());
}
arguments.append("--allow-popups"sv);
arguments.append("--force-new-process"sv);
arguments.append("--enable-autoplay"sv);
if (force_cpu_painting)
arguments.append("--force-cpu-painting"sv);
arguments.append("about:blank"sv);
return arguments;
}
static ErrorOr<pid_t> launch_browser(ByteString const& socket_path, bool force_cpu_painting)
{
auto arguments = create_arguments(socket_path, force_cpu_painting);
return launch_process("Ladybird"sv, arguments.span());
}
static ErrorOr<pid_t> launch_headless_browser(ByteString const& socket_path, bool force_cpu_painting)
{
auto arguments = create_arguments(socket_path, force_cpu_painting);
return launch_process("headless-browser"sv, arguments.span());
}
ErrorOr<int> serenity_main(Main::Arguments arguments)
{
AK::set_rich_debug_enabled(true);
auto listen_address = "0.0.0.0"sv;
int port = 8000;
bool force_cpu_painting = false;
bool headless = false;
Core::ArgsParser args_parser;
args_parser.add_option(listen_address, "IP address to listen on", "listen-address", 'l', "listen_address");
args_parser.add_option(port, "Port to listen on", "port", 'p', "port");
args_parser.add_option(certificates, "Path to a certificate file", "certificate", 'C', "certificate");
args_parser.add_option(force_cpu_painting, "Launch browser with GPU painting disabled", "force-cpu-painting");
args_parser.add_option(headless, "Launch browser without a graphical interface", "headless");
args_parser.parse(arguments);
auto ipv4_address = IPv4Address::from_string(listen_address);
if (!ipv4_address.has_value()) {
warnln("Invalid listen address: {}", listen_address);
return 1;
}
if ((u16)port != port) {
warnln("Invalid port number: {}", port);
return 1;
}
platform_init();
Web::WebDriver::set_default_interface_mode(headless ? Web::WebDriver::InterfaceMode::Headless : Web::WebDriver::InterfaceMode::Graphical);
auto webdriver_socket_path = ByteString::formatted("{}/webdriver", TRY(Core::StandardPaths::runtime_directory()));
TRY(Core::Directory::create(webdriver_socket_path, Core::Directory::CreateDirectories::Yes));
Core::EventLoop loop;
auto server = TRY(Core::TCPServer::try_create());
// FIXME: Propagate errors
server->on_ready_to_accept = [&] {
auto maybe_client_socket = server->accept();
if (maybe_client_socket.is_error()) {
warnln("Failed to accept the client: {}", maybe_client_socket.error());
return;
}
auto maybe_buffered_socket = Core::BufferedTCPSocket::create(maybe_client_socket.release_value());
if (maybe_buffered_socket.is_error()) {
warnln("Could not obtain a buffered socket for the client: {}", maybe_buffered_socket.error());
return;
}
auto launch_browser_callback = [&](ByteString const& socket_path) {
return launch_browser(socket_path, force_cpu_painting);
};
auto launch_headless_browser_callback = [&](ByteString const& socket_path) {
return launch_headless_browser(socket_path, force_cpu_painting);
};
auto maybe_client = WebDriver::Client::try_create(maybe_buffered_socket.release_value(), { move(launch_browser_callback), move(launch_headless_browser_callback) }, server);
if (maybe_client.is_error()) {
warnln("Could not create a WebDriver client: {}", maybe_client.error());
return;
}
};
TRY(server->listen(ipv4_address.value(), port, Core::TCPServer::AllowAddressReuse::Yes));
outln("Listening on {}:{}", ipv4_address.value(), port);
return loop.exec();
}