LibDevTools: Add Firefox DevTools network monitoring support

Hook ResourceLoader to emit network request lifecycle events through
IPC to the UI process, where FrameActor creates NetworkEventActor
instances that serialize requests using Firefox's Remote Debug Protocol.

The Network panel now shows requests with method, URL, status, MIME
type, size, and timing information. Several features remain stubbed
(POST data, response content, cause detection) marked with FIXMEs.
This commit is contained in:
Andreas Kling
2026-01-14 18:43:48 +01:00
committed by Andreas Kling
parent affad6c85d
commit cf010885d5
Notes: github-actions[bot] 2026-01-15 19:12:01 +00:00
23 changed files with 762 additions and 7 deletions

View File

@@ -12,6 +12,7 @@
#include <LibDevTools/Actors/ConsoleActor.h>
#include <LibDevTools/Actors/FrameActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/NetworkEventActor.h>
#include <LibDevTools/Actors/StyleSheetsActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/ThreadActor.h>
@@ -53,13 +54,30 @@ FrameActor::FrameActor(DevToolsServer& devtools, String name, WeakPtr<TabActor>
async_handler<FrameActor>({}, [](auto& self, auto style_sheets, auto& response) {
self.style_sheets_available(response, move(style_sheets));
}));
devtools.delegate().listen_for_network_events(
tab->description(),
[weak_self = make_weak_ptr<FrameActor>()](DevToolsDelegate::NetworkRequestData data) {
if (auto self = weak_self.strong_ref())
self->on_network_request_started(move(data));
},
[weak_self = make_weak_ptr<FrameActor>()](DevToolsDelegate::NetworkResponseData data) {
if (auto self = weak_self.strong_ref())
self->on_network_response_headers_received(move(data));
},
[weak_self = make_weak_ptr<FrameActor>()](DevToolsDelegate::NetworkRequestCompleteData data) {
if (auto self = weak_self.strong_ref())
self->on_network_request_finished(move(data));
});
}
}
FrameActor::~FrameActor()
{
if (auto tab = m_tab.strong_ref())
if (auto tab = m_tab.strong_ref()) {
devtools().delegate().stop_listening_for_console_messages(tab->description());
devtools().delegate().stop_listening_for_network_events(tab->description());
}
}
void FrameActor::handle_message(Message const& message)
@@ -348,4 +366,129 @@ void FrameActor::request_console_messages()
}
}
void FrameActor::on_network_request_started(DevToolsDelegate::NetworkRequestData data)
{
auto& actor = devtools().register_actor<NetworkEventActor>(data.request_id);
actor.set_request_info(move(data.url), move(data.method), data.start_time, move(data.request_headers));
m_network_events.set(data.request_id, actor);
JsonArray events;
events.must_append(actor.serialize_initial_event());
JsonArray network_event;
network_event.must_append("network-event"sv);
network_event.must_append(move(events));
JsonArray array;
array.must_append(move(network_event));
JsonObject message;
message.set("type"sv, "resources-available-array"sv);
message.set("array"sv, move(array));
send_message(move(message));
}
void FrameActor::on_network_response_headers_received(DevToolsDelegate::NetworkResponseData data)
{
auto it = m_network_events.find(data.request_id);
if (it == m_network_events.end())
return;
auto& actor = *it->value;
actor.set_response_start(data.status_code, data.reason_phrase);
// Extract Content-Type before moving headers
String mime_type;
i64 headers_size = 0;
for (auto const& header : data.response_headers) {
headers_size += static_cast<i64>(header.name.bytes().size() + header.value.bytes().size() + 4);
if (header.name.equals_ignoring_ascii_case("content-type"sv))
mime_type = MUST(String::from_byte_string(header.value));
}
actor.set_response_headers(move(data.response_headers));
// Build resource updates object
JsonObject resource_updates;
resource_updates.set("status"sv, String::number(data.status_code));
resource_updates.set("statusText"sv, data.reason_phrase.value_or(String {}));
resource_updates.set("headersSize"sv, headers_size);
resource_updates.set("mimeType"sv, mime_type);
// FIXME: Get actual HTTP version from response
resource_updates.set("httpVersion"sv, "HTTP/1.1"sv);
// FIXME: Get actual remote address and port from connection
resource_updates.set("remoteAddress"sv, String {});
resource_updates.set("remotePort"sv, 0);
// FIXME: Calculate actual waiting time (time between request sent and first byte received)
resource_updates.set("waitingTime"sv, 0);
// Mark headers as available
resource_updates.set("responseHeadersAvailable"sv, true);
JsonObject update_entry;
update_entry.set("resourceId"sv, static_cast<i64>(data.request_id));
update_entry.set("resourceType"sv, "network-event"sv);
update_entry.set("resourceUpdates"sv, move(resource_updates));
update_entry.set("browsingContextID"sv, 1);
update_entry.set("innerWindowId"sv, 1);
JsonArray updates;
updates.must_append(move(update_entry));
JsonArray network_event_updates;
network_event_updates.must_append("network-event"sv);
network_event_updates.must_append(move(updates));
JsonArray array;
array.must_append(move(network_event_updates));
JsonObject message;
message.set("type"sv, "resources-updated-array"sv);
message.set("array"sv, move(array));
send_message(move(message));
}
void FrameActor::on_network_request_finished(DevToolsDelegate::NetworkRequestCompleteData data)
{
auto it = m_network_events.find(data.request_id);
if (it == m_network_events.end())
return;
auto& actor = *it->value;
actor.set_request_complete(data.body_size, data.timing_info, data.network_error);
// Calculate total time in milliseconds
auto total_time = (data.timing_info.response_end_microseconds - data.timing_info.request_start_microseconds) / 1000;
// Build resource updates object with content and timing info
JsonObject resource_updates;
resource_updates.set("contentSize"sv, static_cast<i64>(data.body_size));
resource_updates.set("transferredSize"sv, static_cast<i64>(data.body_size));
resource_updates.set("totalTime"sv, total_time);
// Mark as complete
resource_updates.set("responseContentAvailable"sv, true);
resource_updates.set("eventTimingsAvailable"sv, true);
JsonObject update_entry;
update_entry.set("resourceId"sv, static_cast<i64>(data.request_id));
update_entry.set("resourceType"sv, "network-event"sv);
update_entry.set("resourceUpdates"sv, move(resource_updates));
update_entry.set("browsingContextID"sv, 1);
update_entry.set("innerWindowId"sv, 1);
JsonArray updates;
updates.must_append(move(update_entry));
JsonArray network_event_updates;
network_event_updates.must_append("network-event"sv);
network_event_updates.must_append(move(updates));
JsonArray array;
array.must_append(move(network_event_updates));
JsonObject message;
message.set("type"sv, "resources-updated-array"sv);
message.set("array"sv, move(array));
send_message(move(message));
}
}

View File

@@ -6,10 +6,12 @@
#pragma once
#include <AK/HashMap.h>
#include <AK/NonnullRefPtr.h>
#include <AK/Types.h>
#include <AK/Vector.h>
#include <LibDevTools/Actor.h>
#include <LibDevTools/DevToolsDelegate.h>
#include <LibDevTools/Forward.h>
#include <LibWeb/Forward.h>
#include <LibWebView/Forward.h>
@@ -38,6 +40,10 @@ private:
void console_messages_received(i32 start_index, Vector<WebView::ConsoleOutput>);
void request_console_messages();
void on_network_request_started(DevToolsDelegate::NetworkRequestData);
void on_network_response_headers_received(DevToolsDelegate::NetworkResponseData);
void on_network_request_finished(DevToolsDelegate::NetworkRequestCompleteData);
WeakPtr<TabActor> m_tab;
WeakPtr<CSSPropertiesActor> m_css_properties;
@@ -50,6 +56,8 @@ private:
i32 m_highest_notified_message_index { -1 };
i32 m_highest_received_message_index { -1 };
bool m_waiting_for_messages { false };
HashMap<u64, NonnullRefPtr<NetworkEventActor>> m_network_events;
};
}

View File

@@ -0,0 +1,259 @@
/*
* Copyright (c) 2025, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/NetworkEventActor.h>
#include <LibDevTools/DevToolsServer.h>
namespace DevTools {
NonnullRefPtr<NetworkEventActor> NetworkEventActor::create(DevToolsServer& devtools, String name, u64 request_id)
{
return adopt_ref(*new NetworkEventActor(devtools, move(name), request_id));
}
NetworkEventActor::NetworkEventActor(DevToolsServer& devtools, String name, u64 request_id)
: Actor(devtools, move(name))
, m_request_id(request_id)
{
}
NetworkEventActor::~NetworkEventActor() = default;
void NetworkEventActor::set_request_info(String url, String method, UnixDateTime start_time, Vector<HTTP::Header> request_headers)
{
m_url = move(url);
m_method = move(method);
m_start_time = start_time;
m_request_headers = move(request_headers);
}
void NetworkEventActor::set_response_start(u32 status_code, Optional<String> reason_phrase)
{
m_status_code = status_code;
m_reason_phrase = move(reason_phrase);
}
void NetworkEventActor::set_response_headers(Vector<HTTP::Header> response_headers)
{
m_response_headers = move(response_headers);
}
void NetworkEventActor::set_request_complete(u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error)
{
m_body_size = body_size;
m_timing_info = timing_info;
m_network_error = network_error;
m_complete = true;
}
JsonObject NetworkEventActor::serialize_initial_event() const
{
// FIXME: Detect actual cause type (xhr, fetch, script, stylesheet, image, etc.)
JsonObject cause;
cause.set("type"sv, "document"sv);
JsonObject event;
event.set("resourceType"sv, "network-event"sv);
event.set("resourceId"sv, static_cast<i64>(m_request_id));
event.set("actor"sv, name());
event.set("startedDateTime"sv, MUST(m_start_time.to_string("%Y-%m-%dT%H:%M:%S.000Z"sv)));
event.set("timeStamp"sv, m_start_time.milliseconds_since_epoch());
event.set("url"sv, m_url);
event.set("method"sv, m_method);
// FIXME: Detect if request is XHR/fetch
event.set("isXHR"sv, false);
event.set("cause"sv, move(cause));
event.set("private"sv, false);
// FIXME: Detect if response is from cache
event.set("fromCache"sv, false);
event.set("fromServiceWorker"sv, false);
event.set("isThirdPartyTrackingResource"sv, false);
// FIXME: Get actual referrer policy from request
event.set("referrerPolicy"sv, "strict-origin-when-cross-origin"sv);
event.set("blockedReason"sv, 0);
event.set("blockingExtension"sv, JsonValue {});
event.set("channelId"sv, static_cast<i64>(m_request_id));
// FIXME: Get actual browsing context ID from the page
event.set("browsingContextID"sv, 1);
// FIXME: Get actual inner window ID
event.set("innerWindowId"sv, 1);
// FIXME: Get request priority
event.set("priority"sv, 0);
// FIXME: Detect if this is a navigation request
event.set("isNavigationRequest"sv, false);
event.set("chromeContext"sv, false);
return event;
}
void NetworkEventActor::handle_message(Message const& message)
{
if (message.type == "getRequestHeaders"sv) {
get_request_headers(message);
return;
}
if (message.type == "getRequestCookies"sv) {
get_request_cookies(message);
return;
}
if (message.type == "getRequestPostData"sv) {
get_request_post_data(message);
return;
}
if (message.type == "getResponseHeaders"sv) {
get_response_headers(message);
return;
}
if (message.type == "getResponseCookies"sv) {
get_response_cookies(message);
return;
}
if (message.type == "getResponseContent"sv) {
get_response_content(message);
return;
}
if (message.type == "getEventTimings"sv) {
get_event_timings(message);
return;
}
if (message.type == "getSecurityInfo"sv) {
get_security_info(message);
return;
}
send_unrecognized_packet_type_error(message);
}
void NetworkEventActor::get_request_headers(Message const& message)
{
JsonArray headers;
i64 header_size = 0;
for (auto const& header : m_request_headers) {
JsonObject header_obj;
header_obj.set("name"sv, MUST(String::from_byte_string(header.name)));
header_obj.set("value"sv, MUST(String::from_byte_string(header.value)));
headers.must_append(move(header_obj));
header_size += static_cast<i64>(header.name.bytes().size() + header.value.bytes().size() + 4); // ": " and "\r\n"
}
JsonObject response;
response.set("headers"sv, move(headers));
response.set("headersSize"sv, header_size);
response.set("rawHeaders"sv, String {});
send_response(message, move(response));
}
void NetworkEventActor::get_request_cookies(Message const& message)
{
JsonObject response;
response.set("cookies"sv, JsonArray {});
send_response(message, move(response));
}
void NetworkEventActor::get_request_post_data(Message const& message)
{
JsonObject post_data;
post_data.set("text"sv, String {});
JsonObject response;
response.set("postData"sv, move(post_data));
response.set("postDataDiscarded"sv, false);
send_response(message, move(response));
}
void NetworkEventActor::get_response_headers(Message const& message)
{
JsonArray headers;
i64 header_size = 0;
for (auto const& header : m_response_headers) {
JsonObject header_obj;
header_obj.set("name"sv, MUST(String::from_byte_string(header.name)));
header_obj.set("value"sv, MUST(String::from_byte_string(header.value)));
headers.must_append(move(header_obj));
header_size += static_cast<i64>(header.name.bytes().size() + header.value.bytes().size() + 4);
}
JsonObject response;
response.set("headers"sv, move(headers));
response.set("headersSize"sv, header_size);
response.set("rawHeaders"sv, String {});
send_response(message, move(response));
}
void NetworkEventActor::get_response_cookies(Message const& message)
{
JsonObject response;
response.set("cookies"sv, JsonArray {});
send_response(message, move(response));
}
void NetworkEventActor::get_response_content(Message const& message)
{
// FIXME: Store and return actual response body content
JsonObject content;
content.set("text"sv, String {});
// FIXME: Get actual MIME type from response headers
content.set("mimeType"sv, "text/html"sv);
content.set("size"sv, static_cast<i64>(m_body_size));
JsonObject response;
response.set("content"sv, move(content));
response.set("contentDiscarded"sv, true);
send_response(message, move(response));
}
void NetworkEventActor::get_event_timings(Message const& message)
{
// Convert microseconds to milliseconds for HAR format
auto dns_time = (m_timing_info.domain_lookup_end_microseconds - m_timing_info.domain_lookup_start_microseconds) / 1000;
auto connect_time = (m_timing_info.connect_end_microseconds - m_timing_info.connect_start_microseconds) / 1000;
auto ssl_time = m_timing_info.secure_connect_start_microseconds > 0
? (m_timing_info.connect_end_microseconds - m_timing_info.secure_connect_start_microseconds) / 1000
: 0;
auto send_time = (m_timing_info.response_start_microseconds - m_timing_info.request_start_microseconds) / 1000;
// FIXME: Calculate actual time waiting for server response (TTFB)
auto wait_time = 0;
auto receive_time = (m_timing_info.response_end_microseconds - m_timing_info.response_start_microseconds) / 1000;
JsonObject timings;
timings.set("blocked"sv, 0);
timings.set("dns"sv, dns_time);
timings.set("connect"sv, connect_time);
timings.set("ssl"sv, ssl_time);
timings.set("send"sv, send_time);
timings.set("wait"sv, wait_time);
timings.set("receive"sv, receive_time);
auto total_time = dns_time + connect_time + send_time + wait_time + receive_time;
JsonObject response;
response.set("timings"sv, move(timings));
response.set("totalTime"sv, total_time);
response.set("offsets"sv, JsonObject {});
send_response(message, move(response));
}
void NetworkEventActor::get_security_info(Message const& message)
{
// FIXME: Get actual TLS/SSL security information from the connection
JsonObject response;
response.set("securityInfo"sv, JsonObject {});
response.set("state"sv, "insecure"sv);
send_response(message, move(response));
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2025, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <AK/NonnullRefPtr.h>
#include <AK/Time.h>
#include <LibDevTools/Actor.h>
#include <LibDevTools/Forward.h>
#include <LibHTTP/Header.h>
#include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h>
namespace DevTools {
class DEVTOOLS_API NetworkEventActor final : public Actor {
public:
static constexpr auto base_name = "netEvent"sv;
static NonnullRefPtr<NetworkEventActor> create(DevToolsServer&, String name, u64 request_id);
virtual ~NetworkEventActor() override;
u64 request_id() const { return m_request_id; }
void set_request_info(String url, String method, UnixDateTime start_time, Vector<HTTP::Header> request_headers);
void set_response_start(u32 status_code, Optional<String> reason_phrase);
void set_response_headers(Vector<HTTP::Header> response_headers);
void set_request_complete(u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error);
JsonObject serialize_initial_event() const;
private:
NetworkEventActor(DevToolsServer&, String name, u64 request_id);
virtual void handle_message(Message const&) override;
void get_request_headers(Message const&);
void get_request_cookies(Message const&);
void get_request_post_data(Message const&);
void get_response_headers(Message const&);
void get_response_cookies(Message const&);
void get_response_content(Message const&);
void get_event_timings(Message const&);
void get_security_info(Message const&);
u64 m_request_id { 0 };
String m_url;
String m_method;
UnixDateTime m_start_time;
Vector<HTTP::Header> m_request_headers;
Optional<u32> m_status_code;
Optional<String> m_reason_phrase;
Vector<HTTP::Header> m_response_headers;
u64 m_body_size { 0 };
Requests::RequestTimingInfo m_timing_info {};
Optional<Requests::NetworkError> m_network_error;
bool m_complete { false };
};
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/NetworkParentActor.h>
#include <LibDevTools/DevToolsServer.h>
namespace DevTools {
NonnullRefPtr<NetworkParentActor> NetworkParentActor::create(DevToolsServer& devtools, String name)
{
return adopt_ref(*new NetworkParentActor(devtools, move(name)));
}
NetworkParentActor::NetworkParentActor(DevToolsServer& devtools, String name)
: Actor(devtools, move(name))
{
}
NetworkParentActor::~NetworkParentActor() = default;
void NetworkParentActor::handle_message(Message const& message)
{
JsonObject response;
if (message.type == "setPersist"sv) {
// FIXME: Implement persist functionality
send_response(message, move(response));
return;
}
if (message.type == "setNetworkThrottling"sv) {
// FIXME: Implement network throttling
send_response(message, move(response));
return;
}
if (message.type == "getNetworkThrottling"sv) {
response.set("state"sv, JsonValue {});
send_response(message, move(response));
return;
}
if (message.type == "clearNetworkThrottling"sv) {
send_response(message, move(response));
return;
}
if (message.type == "setSaveRequestAndResponseBodies"sv) {
// FIXME: Implement saving request/response bodies
send_response(message, move(response));
return;
}
if (message.type == "setBlockedUrls"sv) {
// FIXME: Implement URL blocking
send_response(message, move(response));
return;
}
if (message.type == "getBlockedUrls"sv) {
response.set("urls"sv, JsonArray {});
send_response(message, move(response));
return;
}
if (message.type == "blockRequest"sv) {
// FIXME: Implement request blocking
send_response(message, move(response));
return;
}
if (message.type == "unblockRequest"sv) {
// FIXME: Implement request unblocking
send_response(message, move(response));
return;
}
if (message.type == "override"sv) {
// FIXME: Implement request override
send_response(message, move(response));
return;
}
if (message.type == "removeOverride"sv) {
// FIXME: Implement remove override
send_response(message, move(response));
return;
}
send_unrecognized_packet_type_error(message);
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright (c) 2025, Tim Flynn <trflynn89@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include <LibDevTools/Actor.h>
namespace DevTools {
class DEVTOOLS_API NetworkParentActor final : public Actor {
public:
static constexpr auto base_name = "networkParent"sv;
static NonnullRefPtr<NetworkParentActor> create(DevToolsServer&, String name);
private:
NetworkParentActor(DevToolsServer&, String name);
virtual ~NetworkParentActor() override;
virtual void handle_message(Message const&) override;
};
}

View File

@@ -25,7 +25,7 @@ NonnullRefPtr<RootActor> RootActor::create(DevToolsServer& devtools, String name
traits.set("sources"sv, false);
traits.set("highlightable"sv, true);
traits.set("customHighlighters"sv, true);
traits.set("networkMonitor"sv, false);
traits.set("networkMonitor"sv, true);
JsonObject message;
message.set("applicationType"sv, "browser"sv);

View File

@@ -12,6 +12,7 @@
#include <LibDevTools/Actors/ConsoleActor.h>
#include <LibDevTools/Actors/FrameActor.h>
#include <LibDevTools/Actors/InspectorActor.h>
#include <LibDevTools/Actors/NetworkParentActor.h>
#include <LibDevTools/Actors/StyleSheetsActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Actors/TargetConfigurationActor.h>
@@ -39,6 +40,15 @@ void WatcherActor::handle_message(Message const& message)
{
JsonObject response;
if (message.type == "getNetworkParentActor"sv) {
if (!m_network_parent)
m_network_parent = devtools().register_actor<NetworkParentActor>();
response.set("network"sv, m_network_parent->name());
send_response(message, move(response));
return;
}
if (message.type == "getParentBrowsingContextID"sv) {
auto browsing_context_id = get_required_parameter<u64>(message, "browsingContextID"sv);
if (!browsing_context_id.has_value())
@@ -66,7 +76,6 @@ void WatcherActor::handle_message(Message const& message)
send_response(message, move(response));
return;
}
if (message.type == "watchResources"sv) {
auto resource_types = get_required_parameter<JsonArray>(message, "resourceTypes"sv);
if (!resource_types.has_value())
@@ -132,7 +141,7 @@ JsonObject WatcherActor::serialize_description() const
resources.set("jstracer-trace"sv, false);
resources.set("last-private-context-exit"sv, false);
resources.set("local-storage"sv, false);
resources.set("network-event"sv, false);
resources.set("network-event"sv, true);
resources.set("network-event-stacktrace"sv, false);
resources.set("platform-message"sv, false);
resources.set("reflow"sv, false);

View File

@@ -30,6 +30,7 @@ private:
WeakPtr<Actor> m_target;
WeakPtr<TargetConfigurationActor> m_target_configuration;
WeakPtr<ThreadConfigurationActor> m_thread_configuration;
WeakPtr<NetworkParentActor> m_network_parent;
};
}

View File

@@ -10,6 +10,8 @@ set(SOURCES
Actors/HighlighterActor.cpp
Actors/InspectorActor.cpp
Actors/LayoutInspectorActor.cpp
Actors/NetworkEventActor.cpp
Actors/NetworkParentActor.cpp
Actors/NodeActor.cpp
Actors/PageStyleActor.cpp
Actors/ParentAccessibilityActor.cpp

View File

@@ -9,11 +9,15 @@
#include <AK/Error.h>
#include <AK/Function.h>
#include <AK/JsonValue.h>
#include <AK/Time.h>
#include <AK/Vector.h>
#include <LibDevTools/Actors/CSSPropertiesActor.h>
#include <LibDevTools/Actors/PageStyleActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/Forward.h>
#include <LibHTTP/Header.h>
#include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h>
#include <LibWeb/CSS/Selector.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/Forward.h>
@@ -77,6 +81,34 @@ public:
virtual void listen_for_console_messages(TabDescription const&, OnConsoleMessageAvailable, OnReceivedConsoleMessages) const { }
virtual void stop_listening_for_console_messages(TabDescription const&) const { }
virtual void request_console_messages(TabDescription const&, i32) const { }
struct NetworkRequestData {
u64 request_id { 0 };
String url;
String method;
UnixDateTime start_time;
Vector<HTTP::Header> request_headers;
};
struct NetworkResponseData {
u64 request_id { 0 };
u32 status_code { 0 };
Optional<String> reason_phrase;
Vector<HTTP::Header> response_headers;
};
struct NetworkRequestCompleteData {
u64 request_id { 0 };
u64 body_size { 0 };
Requests::RequestTimingInfo timing_info;
Optional<Requests::NetworkError> network_error;
};
using OnNetworkRequestStarted = Function<void(NetworkRequestData)>;
using OnNetworkResponseHeadersReceived = Function<void(NetworkResponseData)>;
using OnNetworkRequestFinished = Function<void(NetworkRequestCompleteData)>;
virtual void listen_for_network_events(TabDescription const&, OnNetworkRequestStarted, OnNetworkResponseHeadersReceived, OnNetworkRequestFinished) const { }
virtual void stop_listening_for_network_events(TabDescription const&) const { }
};
}

View File

@@ -24,6 +24,8 @@ class FrameActor;
class HighlighterActor;
class InspectorActor;
class LayoutInspectorActor;
class NetworkEventActor;
class NetworkParentActor;
class NodeActor;
class PageStyleActor;
class ParentAccessibilityActor;

View File

@@ -417,8 +417,12 @@ void ResourceLoader::load(LoadRequest& request, GC::Root<OnHeadersReceived> on_h
return;
}
auto protocol_headers_received = [this, on_headers_received = move(on_headers_received), request](auto const& response_headers, auto status_code, auto const& reason_phrase) {
auto protocol_headers_received = [this, on_headers_received = move(on_headers_received), request, request_id = protocol_request->id()](auto const& response_headers, auto status_code, auto const& reason_phrase) {
handle_network_response_headers(request, response_headers);
if (auto page = request.page())
page->client().page_did_receive_network_response_headers(request_id, status_code.value_or(0), reason_phrase, response_headers->headers());
on_headers_received->function()(response_headers, move(status_code), reason_phrase);
};
@@ -426,9 +430,12 @@ void ResourceLoader::load(LoadRequest& request, GC::Root<OnHeadersReceived> on_h
on_data_received->function()(data);
};
auto protocol_complete = [this, on_complete = move(on_complete), request, &protocol_request = *protocol_request](u64, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error) {
auto protocol_complete = [this, on_complete = move(on_complete), request, &protocol_request = *protocol_request](u64 total_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error) {
finish_network_request(protocol_request);
if (auto page = request.page())
page->client().page_did_finish_network_request(protocol_request.id(), total_size, timing_info, network_error);
if (!network_error.has_value()) {
log_success(request);
on_complete->function()(true, timing_info, {});
@@ -464,6 +471,9 @@ RefPtr<Requests::Request> ResourceLoader::start_network_request(LoadRequest cons
return {};
};
if (auto page = request.page())
page->client().page_did_start_network_request(protocol_request->id(), request.url().value(), request.method(), request.headers().headers());
++m_pending_loads;
if (on_load_counter_change)
on_load_counter_change();

View File

@@ -18,7 +18,10 @@
#include <LibGfx/Rect.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibGfx/Size.h>
#include <LibHTTP/Header.h>
#include <LibIPC/Forward.h>
#include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h>
#include <LibURL/URL.h>
#include <LibWeb/Bindings/AgentType.h>
#include <LibWeb/CSS/PreferredColorScheme.h>
@@ -416,6 +419,10 @@ public:
virtual void page_did_change_audio_play_state(HTML::AudioPlayState) { }
virtual void page_did_start_network_request([[maybe_unused]] u64 request_id, [[maybe_unused]] URL::URL const& url, [[maybe_unused]] ByteString const& method, [[maybe_unused]] Vector<HTTP::Header> const& request_headers) { }
virtual void page_did_receive_network_response_headers([[maybe_unused]] u64 request_id, [[maybe_unused]] u32 status_code, [[maybe_unused]] Optional<String> reason_phrase, [[maybe_unused]] Vector<HTTP::Header> const& response_headers) { }
virtual void page_did_finish_network_request([[maybe_unused]] u64 request_id, [[maybe_unused]] u64 body_size, [[maybe_unused]] Requests::RequestTimingInfo const& timing_info, [[maybe_unused]] Optional<Requests::NetworkError> const& network_error) { }
virtual IPC::File request_worker_agent([[maybe_unused]] Web::Bindings::AgentType worker_type) { return IPC::File {}; }
virtual void page_did_mutate_dom([[maybe_unused]] FlyString const& type, [[maybe_unused]] DOM::Node const& target, [[maybe_unused]] DOM::NodeList& added_nodes, [[maybe_unused]] DOM::NodeList& removed_nodes, [[maybe_unused]] GC::Ptr<DOM::Node> previous_sibling, [[maybe_unused]] GC::Ptr<DOM::Node> next_sibling, [[maybe_unused]] Optional<String> const& attribute_name) { }

View File

@@ -1307,4 +1307,34 @@ void Application::request_console_messages(DevTools::TabDescription const& descr
view->js_console_request_messages(start_index);
}
void Application::listen_for_network_events(DevTools::TabDescription const& description, OnNetworkRequestStarted on_request_started, OnNetworkResponseHeadersReceived on_response_headers, OnNetworkRequestFinished on_request_finished) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value())
return;
view->on_network_request_started = [on_request_started = move(on_request_started)](u64 request_id, URL::URL const& url, ByteString const& method, Vector<HTTP::Header> const& headers) {
on_request_started({ request_id, url.to_string(), MUST(String::from_byte_string(method)), UnixDateTime::now(), headers });
};
view->on_network_response_headers_received = [on_response_headers = move(on_response_headers)](u64 request_id, u32 status_code, Optional<String> const& reason_phrase, Vector<HTTP::Header> const& headers) {
on_response_headers({ request_id, status_code, reason_phrase, headers });
};
view->on_network_request_finished = [on_request_finished = move(on_request_finished)](u64 request_id, u64 body_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error) {
on_request_finished({ request_id, body_size, timing_info, network_error });
};
}
void Application::stop_listening_for_network_events(DevTools::TabDescription const& description) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value())
return;
view->on_network_request_started = nullptr;
view->on_network_response_headers_received = nullptr;
view->on_network_request_finished = nullptr;
}
}

View File

@@ -191,6 +191,8 @@ private:
virtual void listen_for_console_messages(DevTools::TabDescription const&, OnConsoleMessageAvailable, OnReceivedConsoleMessages) const override;
virtual void stop_listening_for_console_messages(DevTools::TabDescription const&) const override;
virtual void request_console_messages(DevTools::TabDescription const&, i32) const override;
virtual void listen_for_network_events(DevTools::TabDescription const&, OnNetworkRequestStarted, OnNetworkResponseHeadersReceived, OnNetworkRequestFinished) const override;
virtual void stop_listening_for_network_events(DevTools::TabDescription const&) const override;
static Application* s_the;

View File

@@ -18,6 +18,9 @@
#include <LibCore/Promise.h>
#include <LibGfx/Cursor.h>
#include <LibGfx/Forward.h>
#include <LibHTTP/Header.h>
#include <LibRequests/Forward.h>
#include <LibRequests/NetworkError.h>
#include <LibWeb/Forward.h>
#include <LibWeb/HTML/ActivateTab.h>
#include <LibWeb/HTML/AudioPlayState.h>
@@ -201,6 +204,9 @@ public:
Function<void(JsonValue)> on_received_js_console_result;
Function<void(i32 message_id)> on_console_message_available;
Function<void(i32 start_index, Vector<ConsoleOutput>)> on_received_console_messages;
Function<void(u64 request_id, URL::URL const&, ByteString const&, Vector<HTTP::Header> const&)> on_network_request_started;
Function<void(u64 request_id, u32 status_code, Optional<String> const&, Vector<HTTP::Header> const&)> on_network_response_headers_received;
Function<void(u64 request_id, u64 body_size, Requests::RequestTimingInfo const&, Optional<Requests::NetworkError> const&)> on_network_request_finished;
Function<void(i32 count_waiting)> on_resource_status_change;
Function<void()> on_restore_window;
Function<void(Gfx::IntPoint)> on_reposition_window;

View File

@@ -434,6 +434,30 @@ void WebContentClient::did_get_js_console_messages(u64 page_id, i32 start_index,
}
}
void WebContentClient::did_start_network_request(u64 page_id, u64 request_id, URL::URL url, ByteString method, Vector<HTTP::Header> request_headers)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_network_request_started)
view->on_network_request_started(request_id, url, method, request_headers);
}
}
void WebContentClient::did_receive_network_response_headers(u64 page_id, u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header> response_headers)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_network_response_headers_received)
view->on_network_response_headers_received(request_id, status_code, reason_phrase, response_headers);
}
}
void WebContentClient::did_finish_network_request(u64 page_id, u64 request_id, u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_network_request_finished)
view->on_network_request_finished(request_id, body_size, timing_info, network_error);
}
}
void WebContentClient::did_request_alert(u64 page_id, String message)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View File

@@ -8,8 +8,11 @@
#include <AK/HashMap.h>
#include <AK/SourceLocation.h>
#include <LibHTTP/Header.h>
#include <LibIPC/ConnectionToServer.h>
#include <LibIPC/Transport.h>
#include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/StyleSheetIdentifier.h>
#include <LibWeb/HTML/ActivateTab.h>
@@ -94,6 +97,9 @@ private:
virtual void did_execute_js_console_input(u64 page_id, JsonValue) override;
virtual void did_output_js_console_message(u64 page_id, i32 message_index) override;
virtual void did_get_js_console_messages(u64 page_id, i32 start_index, Vector<ConsoleOutput>) override;
virtual void did_start_network_request(u64 page_id, u64 request_id, URL::URL, ByteString method, Vector<HTTP::Header>) override;
virtual void did_receive_network_response_headers(u64 page_id, u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header>) override;
virtual void did_finish_network_request(u64 page_id, u64 request_id, u64 body_size, Requests::RequestTimingInfo, Optional<Requests::NetworkError>) override;
virtual void did_change_favicon(u64 page_id, Gfx::ShareableBitmap) override;
virtual void did_request_alert(u64 page_id, String) override;
virtual void did_request_confirm(u64 page_id, String) override;

View File

@@ -30,7 +30,7 @@ target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${CMAKE_CU
target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${LADYBIRD_SOURCE_DIR}>)
target_include_directories(webcontentservice PUBLIC $<BUILD_INTERFACE:${LADYBIRD_SOURCE_DIR}/Services/>)
target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibFileSystem LibGfx LibIPC LibJS LibMain LibMedia LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC)
target_link_libraries(webcontentservice PUBLIC LibCore LibCrypto LibFileSystem LibGfx LibHTTP LibIPC LibJS LibMain LibMedia LibWeb LibWebSocket LibRequests LibWebView LibImageDecoderClient LibGC)
target_link_libraries(webcontentservice PRIVATE OpenSSL::Crypto OpenSSL::SSL)
target_link_libraries(webcontentservice PRIVATE SDL3::SDL3)

View File

@@ -751,6 +751,21 @@ void PageClient::received_message_from_web_ui(String const& name, JS::Value data
m_web_ui->received_message_from_web_ui(name, data);
}
void PageClient::page_did_start_network_request(u64 request_id, URL::URL const& url, ByteString const& method, Vector<HTTP::Header> const& request_headers)
{
client().async_did_start_network_request(m_id, request_id, url, method, request_headers);
}
void PageClient::page_did_receive_network_response_headers(u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header> const& response_headers)
{
client().async_did_receive_network_response_headers(m_id, request_id, status_code, move(reason_phrase), response_headers);
}
void PageClient::page_did_finish_network_request(u64 request_id, u64 body_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error)
{
client().async_did_finish_network_request(m_id, request_id, body_size, timing_info, network_error);
}
void PageClient::initialize_js_console(Web::DOM::Document& document)
{
if (document.is_temporary_document_for_fragment_parsing())

View File

@@ -181,6 +181,9 @@ private:
virtual void page_did_paint(Gfx::IntRect const& content_rect, i32 bitmap_id) override;
virtual void page_did_take_screenshot(Gfx::ShareableBitmap const& screenshot) override;
virtual void received_message_from_web_ui(String const& name, JS::Value data) override;
virtual void page_did_start_network_request(u64 request_id, URL::URL const&, ByteString const&, Vector<HTTP::Header> const&) override;
virtual void page_did_receive_network_response_headers(u64 request_id, u32 status_code, Optional<String>, Vector<HTTP::Header> const&) override;
virtual void page_did_finish_network_request(u64 request_id, u64 body_size, Requests::RequestTimingInfo const&, Optional<Requests::NetworkError> const&) override;
void setup_palette();
ConnectionFromClient& client() const;

View File

@@ -2,6 +2,9 @@
#include <LibGfx/Color.h>
#include <LibGfx/Cursor.h>
#include <LibGfx/ShareableBitmap.h>
#include <LibHTTP/Header.h>
#include <LibRequests/NetworkError.h>
#include <LibRequests/RequestTimingInfo.h>
#include <LibURL/URL.h>
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/Clipboard/SystemClipboard.h>
@@ -113,6 +116,10 @@ endpoint WebContentClient
did_output_js_console_message(u64 page_id, i32 message_index) =|
did_get_js_console_messages(u64 page_id, i32 start_index, Vector<WebView::ConsoleOutput> console_output) =|
did_start_network_request(u64 page_id, u64 request_id, URL::URL url, ByteString method, Vector<HTTP::Header> request_headers) =|
did_receive_network_response_headers(u64 page_id, u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header> response_headers) =|
did_finish_network_request(u64 page_id, u64 request_id, u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error) =|
did_finish_test(u64 page_id, String text) =|
did_set_test_timeout(u64 page_id, double milliseconds) =|
did_receive_reference_test_metadata(u64 page_id, JsonValue result) =|