servo/components/devtools/actors/network_event.rs
Usman Yahaya Baba 02dca0fb21
net: Send ResponseContentObj to Devtools (#38625)
Currently, the response tab for a request's detail in devtools does not
show the available data, this was due to how the content is being
structured (not the way firefox's devtools client expects it) and also
the body being discarded and not stored in the actor.
This PR stores the body in the actor , which is then retrieved in
`getResponseContent` and then use it to instantiate the new struct
`ResponseContentObj` which matches the format firefox's expects

Fixes: https://github.com/servo/servo/issues/38128

---------

Signed-off-by: uthmaniv <uthmanyahayababa@gmail.com>
Signed-off-by: Josh Matthews <josh@joshmatthews.net>
Co-authored-by: Josh Matthews <josh@joshmatthews.net>
2025-08-15 08:26:24 +00:00

723 lines
24 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Liberally derived from the [Firefox JS implementation](http://mxr.mozilla.org/mozilla-central/source/toolkit/devtools/server/actors/webconsole.js).
//! Handles interaction with the remote web console on network events (HTTP requests, responses) in Servo.
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use base64::engine::Engine;
use base64::engine::general_purpose::STANDARD;
use chrono::{Local, LocalResult, TimeZone};
use devtools_traits::{HttpRequest as DevtoolsHttpRequest, HttpResponse as DevtoolsHttpResponse};
use headers::{ContentLength, ContentType, Cookie, HeaderMapExt};
use http::{HeaderMap, Method};
use net::cookie::ServoCookie;
use net_traits::CookieSource;
use net_traits::request::Destination as RequestDestination;
use serde::Serialize;
use serde_json::{Map, Value};
use servo_url::ServoUrl;
use crate::StreamId;
use crate::actor::{Actor, ActorError, ActorRegistry};
use crate::actors::long_string::LongStringActor;
use crate::network_handler::Cause;
use crate::protocol::ClientRequest;
pub struct NetworkEventActor {
pub name: String,
pub resource_id: u64,
pub is_xhr: bool,
pub request_url: String,
pub request_method: Method,
pub request_started: SystemTime,
pub request_time_stamp: i64,
pub request_destination: RequestDestination,
pub request_headers_raw: Option<HeaderMap>,
pub request_body: Option<Vec<u8>>,
pub request_cookies: Option<RequestCookiesMsg>,
pub request_headers: Option<RequestHeadersMsg>,
pub response_headers_raw: Option<HeaderMap>,
pub response_body: Option<Vec<u8>>,
pub response_content: Option<ResponseContentMsg>,
pub response_start: Option<ResponseStartMsg>,
pub response_cookies: Option<ResponseCookiesMsg>,
pub response_headers: Option<ResponseHeadersMsg>,
pub total_time: Duration,
pub security_state: String,
pub event_timing: Option<Timings>,
pub watcher_name: String,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NetworkEventResource {
pub resource_id: u64,
pub resource_updates: Map<String, Value>,
pub browsing_context_id: u64,
pub inner_window_id: u64,
}
#[derive(Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct EventActor {
pub actor: String,
pub resource_id: u64,
pub url: String,
pub method: String,
pub started_date_time: String,
pub time_stamp: i64,
#[serde(rename = "isXHR")]
pub is_xhr: bool,
pub private: bool,
pub cause: Cause,
}
#[derive(Serialize)]
pub struct ResponseCookiesMsg {
pub cookies: Vec<ResponseCookieObj>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseStartMsg {
pub http_version: String,
pub remote_address: String,
pub remote_port: u32,
pub status: String,
pub status_text: String,
pub headers_size: usize,
pub discard_response_body: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseContentMsg {
pub mime_type: String,
pub content_size: u32,
pub transferred_size: u32,
pub discard_response_body: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ResponseHeadersMsg {
pub headers: usize,
pub headers_size: usize,
}
#[derive(Serialize)]
pub struct RequestCookiesMsg {
pub cookies: Vec<RequestCookieObj>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RequestHeadersMsg {
headers: usize,
headers_size: usize,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetRequestHeadersReply {
from: String,
headers: Vec<Header>,
header_size: usize,
raw_headers: String,
}
#[derive(Serialize)]
struct Header {
name: String,
value: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetResponseHeadersReply {
from: String,
headers: Vec<Header>,
header_size: usize,
raw_headers: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetResponseContentReply {
from: String,
content: Option<ResponseContentObj>,
content_discarded: bool,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetRequestPostDataReply {
from: String,
post_data: Option<Vec<u8>>,
post_data_discarded: bool,
}
#[derive(Serialize)]
struct GetRequestCookiesReply {
from: String,
cookies: Vec<RequestCookieObj>,
}
#[derive(Serialize)]
struct GetResponseCookiesReply {
from: String,
cookies: Vec<ResponseCookieObj>,
}
#[derive(Clone, Serialize)]
pub struct ResponseCookieObj {
pub name: String,
pub value: String,
pub path: Option<String>,
pub domain: Option<String>,
pub expires: Option<String>,
#[serde(rename = "httpOnly")]
pub http_only: Option<bool>,
pub secure: Option<bool>,
#[serde(rename = "sameSite")]
pub same_site: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ResponseContentObj {
mime_type: String,
text: Value,
body_size: usize,
decoded_body_size: usize,
size: usize,
headers_size: usize,
transferred_size: usize,
#[serde(skip_serializing_if = "Option::is_none")]
encoding: Option<String>,
}
#[derive(Clone, Serialize)]
pub struct RequestCookieObj {
pub name: String,
pub value: String,
}
#[derive(Clone, Default, Serialize)]
pub struct Timings {
blocked: u32,
dns: u32,
connect: u64,
send: u64,
wait: u32,
receive: u32,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetEventTimingsReply {
from: String,
timings: Timings,
total_time: u64,
}
#[derive(Serialize)]
struct SecurityInfo {
state: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct GetSecurityInfoReply {
from: String,
security_info: SecurityInfo,
}
impl Actor for NetworkEventActor {
fn name(&self) -> String {
self.name.clone()
}
fn handle_message(
&self,
request: ClientRequest,
registry: &ActorRegistry,
msg_type: &str,
_msg: &Map<String, Value>,
_id: StreamId,
) -> Result<(), ActorError> {
match msg_type {
"getRequestHeaders" => {
let mut headers = Vec::new();
let mut raw_headers_string = "".to_owned();
let mut headers_size = 0;
if let Some(ref headers_map) = self.request_headers_raw {
for (name, value) in headers_map.iter() {
let value = &value.to_str().unwrap().to_string();
raw_headers_string =
raw_headers_string + name.as_str() + ":" + value + "\r\n";
headers_size += name.as_str().len() + value.len();
headers.push(Header {
name: name.as_str().to_owned(),
value: value.to_owned(),
});
}
}
let msg = GetRequestHeadersReply {
from: self.name(),
headers,
header_size: headers_size,
raw_headers: raw_headers_string,
};
request.reply_final(&msg)?
},
"getRequestCookies" => {
let cookies = self
.request_cookies
.as_ref()
.map(|msg| msg.cookies.clone())
.unwrap_or_default();
let msg = GetRequestCookiesReply {
from: self.name(),
cookies,
};
request.reply_final(&msg)?
},
"getRequestPostData" => {
let msg = GetRequestPostDataReply {
from: self.name(),
post_data: self.request_body.clone(),
post_data_discarded: self.request_body.is_none(),
};
request.reply_final(&msg)?
},
"getResponseHeaders" => {
if let Some(ref response_headers) = self.response_headers_raw {
let mut headers = vec![];
let mut raw_headers_string = "".to_owned();
let mut headers_size = 0;
for (name, value) in response_headers.iter() {
headers.push(Header {
name: name.as_str().to_owned(),
value: value.to_str().unwrap().to_owned(),
});
headers_size += name.as_str().len() + value.len();
raw_headers_string.push_str(name.as_str());
raw_headers_string.push(':');
raw_headers_string.push_str(value.to_str().unwrap());
raw_headers_string.push_str("\r\n");
}
let msg = GetResponseHeadersReply {
from: self.name(),
headers,
header_size: headers_size,
raw_headers: raw_headers_string,
};
request.reply_final(&msg)?;
} else {
// FIXME: what happens when there are no response headers?
return Err(ActorError::Internal);
}
},
"getResponseCookies" => {
let cookies = self
.response_cookies
.as_ref()
.map(|msg| msg.cookies.clone())
.unwrap_or_default();
let msg = GetResponseCookiesReply {
from: self.name(),
cookies,
};
request.reply_final(&msg)?
},
"getResponseContent" => {
let content_obj = self.response_body.as_ref().map(|body| {
let mime_type = self
.response_content
.as_ref()
.map(|c| c.mime_type.clone())
.unwrap_or_default();
let headers_size = self
.response_headers
.as_ref()
.map(|h| h.headers_size)
.unwrap_or(0);
let transferred_size = self
.response_content
.as_ref()
.map(|c| c.transferred_size as usize)
.unwrap_or(0);
let body_size = body.len();
let decoded_body_size = body.len();
let size = body.len();
if Self::is_text_mime(&mime_type) {
let full_str = String::from_utf8_lossy(body).to_string();
// Queue a LongStringActor for this body
let long_string_actor = LongStringActor::new(registry, full_str);
let long_string_obj = long_string_actor.long_string_obj();
registry.register_later(Box::new(long_string_actor));
ResponseContentObj {
mime_type,
text: serde_json::to_value(long_string_obj).unwrap(),
body_size,
decoded_body_size,
size,
headers_size,
transferred_size,
encoding: None,
}
} else {
let b64 = STANDARD.encode(body);
ResponseContentObj {
mime_type,
text: serde_json::to_value(b64).unwrap(),
body_size,
decoded_body_size,
size,
headers_size,
transferred_size,
encoding: Some("base64".to_string()),
}
}
});
let msg = GetResponseContentReply {
from: self.name(),
content: content_obj,
content_discarded: self.response_body.is_none(),
};
request.reply_final(&msg)?
},
"getEventTimings" => {
// TODO: This is a fake timings msg
let timings_obj = self.event_timing.clone().unwrap_or_default();
// Might use the one on self
let total = timings_obj.connect + timings_obj.send;
// TODO: Send the correct values for all these fields.
let msg = GetEventTimingsReply {
from: self.name(),
timings: timings_obj,
total_time: total,
};
request.reply_final(&msg)?
},
"getSecurityInfo" => {
// TODO: Send the correct values for securityInfo.
let msg = GetSecurityInfoReply {
from: self.name(),
security_info: SecurityInfo {
state: "insecure".to_owned(),
},
};
request.reply_final(&msg)?
},
_ => return Err(ActorError::UnrecognizedPacketType),
};
Ok(())
}
}
impl NetworkEventActor {
pub fn new(name: String, resource_id: u64, watcher_name: String) -> NetworkEventActor {
NetworkEventActor {
name,
resource_id,
is_xhr: false,
request_url: String::new(),
request_method: Method::GET,
request_started: SystemTime::now(),
request_time_stamp: SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64,
request_destination: RequestDestination::None,
request_headers_raw: None,
request_body: None,
request_cookies: None,
request_headers: None,
response_headers_raw: None,
response_body: None,
response_content: None,
response_start: None,
response_cookies: None,
response_headers: None,
total_time: Duration::ZERO,
security_state: "insecure".to_owned(),
event_timing: None,
watcher_name,
}
}
pub fn add_request(&mut self, request: DevtoolsHttpRequest) {
self.is_xhr = request.is_xhr;
self.request_cookies = Self::request_cookies(&request);
self.request_headers = Some(Self::request_headers(&request));
self.total_time = Self::total_time(&request);
self.event_timing = Some(Self::event_timing(&request));
self.request_url = request.url.to_string();
self.request_method = request.method;
self.request_started = request.started_date_time;
self.request_time_stamp = request.time_stamp;
self.request_destination = request.destination;
self.request_body = request.body.clone();
self.request_headers_raw = Some(request.headers.clone());
}
pub fn add_response(&mut self, response: DevtoolsHttpResponse) {
self.response_headers = Some(Self::response_headers(&response));
self.response_cookies = ServoUrl::parse(&self.request_url)
.ok()
.as_ref()
.and_then(|url| Self::response_cookies(&response, url));
self.response_start = Some(Self::response_start(&response));
if let Some(response_content) = Self::response_content(self, &response) {
self.response_content = Some(response_content);
}
self.response_headers_raw = response.headers.clone();
}
pub fn event_actor(&self) -> EventActor {
// TODO: Send the correct values for startedDateTime, isXHR, private
let started_datetime_rfc3339 = match Local.timestamp_millis_opt(
self.request_started
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64,
) {
LocalResult::None => "".to_owned(),
LocalResult::Single(date_time) => date_time.to_rfc3339().to_string(),
LocalResult::Ambiguous(date_time, _) => date_time.to_rfc3339().to_string(),
};
EventActor {
actor: self.name(),
resource_id: self.resource_id,
url: self.request_url.clone(),
method: format!("{}", self.request_method),
started_date_time: started_datetime_rfc3339,
time_stamp: self.request_time_stamp,
is_xhr: self.is_xhr,
private: false,
cause: Cause {
type_: self.request_destination.as_str().to_string(),
loading_document_uri: None, // Set if available
},
}
}
pub fn response_start(response: &DevtoolsHttpResponse) -> ResponseStartMsg {
// TODO: Send the correct values for all these fields.
let h_size = response.headers.as_ref().map(|h| h.len()).unwrap_or(0);
let status = &response.status;
// TODO: Send the correct values for remoteAddress and remotePort and http_version
ResponseStartMsg {
http_version: "HTTP/1.1".to_owned(),
remote_address: "63.245.217.43".to_owned(),
remote_port: 443,
status: status.code().to_string(),
status_text: String::from_utf8_lossy(status.message()).to_string(),
headers_size: h_size,
discard_response_body: false,
}
}
pub fn response_content(
&mut self,
response: &DevtoolsHttpResponse,
) -> Option<ResponseContentMsg> {
let body = response.body.as_ref()?;
self.response_body = Some(body.clone());
let mime_type = response
.headers
.as_ref()
.and_then(|h| h.typed_get::<ContentType>())
.map(|ct| ct.to_string())
.unwrap_or_default();
let transferred_size = response
.headers
.as_ref()
.and_then(|hdrs| hdrs.typed_get::<ContentLength>())
.map(|cl| cl.0);
let content_size = response.body.as_ref().map(|body| body.len() as u64);
Some(ResponseContentMsg {
mime_type,
content_size: content_size.unwrap_or(0) as u32,
transferred_size: transferred_size.unwrap_or(0) as u32,
discard_response_body: false,
})
}
pub fn response_cookies(
response: &DevtoolsHttpResponse,
url: &ServoUrl,
) -> Option<ResponseCookiesMsg> {
let headers = response.headers.as_ref()?;
let cookies = headers
.get_all("set-cookie")
.iter()
.filter_map(|cookie| {
let cookie_str = String::from_utf8(cookie.as_bytes().to_vec()).ok()?;
ServoCookie::from_cookie_string(cookie_str, url, CookieSource::HTTP)
})
.map(|servo_cookie| {
let c = &servo_cookie.cookie;
ResponseCookieObj {
name: c.name().to_string(),
value: c.value().to_string(),
path: c.path().map(|p| p.to_string()),
domain: c.domain().map(|d| d.to_string()),
expires: c.expires().map(|dt| format!("{:?}", dt)),
http_only: c.http_only(),
secure: c.secure(),
same_site: c.same_site().map(|s| s.to_string()),
}
})
.collect::<Vec<_>>();
Some(ResponseCookiesMsg { cookies })
}
pub fn response_headers(response: &DevtoolsHttpResponse) -> ResponseHeadersMsg {
let mut header_size = 0;
let mut headers_byte_count = 0;
if let Some(ref headers) = response.headers {
for (name, value) in headers.iter() {
header_size += 1;
headers_byte_count += name.as_str().len() + value.len();
}
}
ResponseHeadersMsg {
headers: header_size,
headers_size: headers_byte_count,
}
}
pub fn request_headers(request: &DevtoolsHttpRequest) -> RequestHeadersMsg {
let size = request.headers.iter().fold(0, |acc, (name, value)| {
acc + name.as_str().len() + value.len()
});
RequestHeadersMsg {
headers: request.headers.len(),
headers_size: size,
}
}
pub fn request_cookies(request: &DevtoolsHttpRequest) -> Option<RequestCookiesMsg> {
let header_value = request.headers.typed_get::<Cookie>()?;
let cookies = header_value
.iter()
.map(|cookie| RequestCookieObj {
name: cookie.0.to_string(),
value: cookie.1.to_string(),
})
.collect::<Vec<_>>();
Some(RequestCookiesMsg { cookies })
}
pub fn total_time(request: &DevtoolsHttpRequest) -> Duration {
request.connect_time + request.send_time
}
pub fn event_timing(request: &DevtoolsHttpRequest) -> Timings {
Timings {
blocked: 0,
dns: 0,
connect: request.connect_time.as_millis() as u64,
send: request.send_time.as_millis() as u64,
wait: 0,
receive: 0,
}
}
pub fn is_text_mime(mime: &str) -> bool {
let lower = mime.to_ascii_lowercase();
lower.starts_with("text/") ||
lower.contains("json") ||
lower.contains("javascript") ||
lower.contains("xml") ||
lower.contains("csv") ||
lower.contains("html")
}
fn insert_serialized_map<T: Serialize>(map: &mut Map<String, Value>, obj: &Option<T>) {
if let Some(value) = obj {
if let Ok(Value::Object(serialized)) = serde_json::to_value(value) {
for (key, val) in serialized {
map.insert(key, val);
}
}
}
}
pub fn resource_updates(&self) -> NetworkEventResource {
let mut resource_updates = Map::new();
resource_updates.insert(
"requestCookiesAvailable".to_owned(),
Value::Bool(self.request_cookies.is_some()),
);
resource_updates.insert(
"requestHeadersAvailable".to_owned(),
Value::Bool(self.request_headers.is_some()),
);
resource_updates.insert(
"responseHeadersAvailable".to_owned(),
Value::Bool(self.response_headers.is_some()),
);
resource_updates.insert(
"responseCookiesAvailable".to_owned(),
Value::Bool(self.response_cookies.is_some()),
);
resource_updates.insert(
"responseStartAvailable".to_owned(),
Value::Bool(self.response_start.is_some()),
);
resource_updates.insert(
"responseContentAvailable".to_owned(),
Value::Bool(self.response_content.is_some()),
);
resource_updates.insert(
"totalTime".to_string(),
Value::from(self.total_time.as_secs_f64()),
);
resource_updates.insert(
"securityState".to_string(),
Value::String(self.security_state.clone()),
);
resource_updates.insert(
"eventTimingsAvailable".to_owned(),
Value::Bool(self.event_timing.is_some()),
);
Self::insert_serialized_map(&mut resource_updates, &self.response_content);
Self::insert_serialized_map(&mut resource_updates, &self.response_headers);
Self::insert_serialized_map(&mut resource_updates, &self.response_cookies);
Self::insert_serialized_map(&mut resource_updates, &self.request_headers);
Self::insert_serialized_map(&mut resource_updates, &self.request_cookies);
Self::insert_serialized_map(&mut resource_updates, &self.response_start);
Self::insert_serialized_map(&mut resource_updates, &self.event_timing);
// TODO: Set the correct values for these fields
NetworkEventResource {
resource_id: self.resource_id,
resource_updates,
browsing_context_id: 0,
inner_window_id: 0,
}
}
}