ladybird/Libraries/LibDevTools/Actors/NetworkEventActor.cpp

328 lines
11 KiB
C++
Raw Normal View History

/*
* Copyright (c) 2025, the Ladybird developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Base64.h>
#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, ByteBuffer request_body, Optional<String> initiator_type)
{
m_url = move(url);
m_method = move(method);
m_start_time = start_time;
m_request_headers = move(request_headers);
m_request_body = move(request_body);
m_initiator_type = move(initiator_type);
}
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::append_response_body(ByteBuffer data)
{
// Limit response body size to prevent memory issues
if (m_response_body.size() >= MAX_RESPONSE_BODY_SIZE)
return;
auto remaining_capacity = MAX_RESPONSE_BODY_SIZE - m_response_body.size();
auto bytes_to_append = min(data.size(), remaining_capacity);
if (bytes_to_append > 0)
m_response_body.append(data.bytes().slice(0, bytes_to_append));
}
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
{
// Determine if this is an XHR/fetch request (Firefox groups both under "XHR" filter)
bool is_xhr = m_initiator_type.has_value()
&& (m_initiator_type.value() == "xmlhttprequest"sv || m_initiator_type.value() == "fetch"sv);
// Map initiator type to Firefox DevTools cause type
StringView cause_type = "document"sv;
if (m_initiator_type.has_value()) {
auto const& type = m_initiator_type.value();
if (type == "xmlhttprequest"sv)
cause_type = "xhr"sv;
else if (type == "css"sv)
cause_type = "stylesheet"sv;
else if (type == "img"sv || type == "image"sv)
cause_type = "image"sv;
else
cause_type = type;
}
JsonObject cause;
cause.set("type"sv, cause_type);
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);
event.set("isXHR"sv, is_xhr);
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, MUST(String::from_utf8(m_request_body)));
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)
{
// Get MIME type from Content-Type header
String mime_type = "application/octet-stream"_string;
for (auto const& header : m_response_headers) {
if (header.name.equals_ignoring_ascii_case("content-type"sv)) {
auto content_type = StringView { header.value };
// Extract just the MIME type, ignoring charset etc.
if (auto semicolon = content_type.find(';'); semicolon.has_value())
content_type = content_type.substring_view(0, *semicolon);
mime_type = MUST(String::from_utf8(content_type.trim_whitespace()));
break;
}
}
bool content_discarded = m_response_body.size() >= MAX_RESPONSE_BODY_SIZE;
// Check if content is text-based (can be displayed as UTF-8)
bool is_text = mime_type.starts_with_bytes("text/"sv)
|| mime_type == "application/json"sv
|| mime_type == "application/javascript"sv
|| mime_type == "application/xml"sv
|| mime_type.ends_with_bytes("+xml"sv)
|| mime_type.ends_with_bytes("+json"sv);
JsonObject content;
if (is_text) {
// Try to interpret as UTF-8, fall back to base64 if invalid
auto text_or_error = String::from_utf8(m_response_body);
if (!text_or_error.is_error()) {
content.set("text"sv, text_or_error.release_value());
content.set("encoding"sv, JsonValue {});
} else {
// Content claims to be text but isn't valid UTF-8, base64 encode it
content.set("text"sv, MUST(encode_base64(m_response_body)));
content.set("encoding"sv, "base64"sv);
}
} else {
// Base64 encode binary content
content.set("text"sv, MUST(encode_base64(m_response_body)));
content.set("encoding"sv, "base64"sv);
}
content.set("mimeType"sv, mime_type);
content.set("size"sv, static_cast<i64>(m_body_size));
JsonObject response;
response.set("content"sv, move(content));
response.set("contentDiscarded"sv, content_discarded);
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));
}
}