ladybird/Libraries/LibDevTools/Actors/NetworkEventActor.cpp
Andreas Kling 0c7292e05c LibDevTools: Fix sending large messages over DevTools connection
Use blocking mode for socket writes to ensure large response bodies
(like 1MB+ HTML pages) are fully written without EAGAIN errors. The
socket is temporarily set to blocking mode during the write operation.

Also improve error handling by logging failed sends with the message
size and error details.

Additionally, when response content claims to be text but isn't valid
UTF-8, fall back to base64 encoding instead of returning empty content.
2026-01-15 20:10:19 +01:00

327 lines
11 KiB
C++

/*
* 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));
}
}