/* 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, pub request_body: Option>, pub request_cookies: Option, pub request_headers: Option, pub response_headers_raw: Option, pub response_body: Option>, pub response_content: Option, pub response_start: Option, pub response_cookies: Option, pub response_headers: Option, pub total_time: Duration, pub security_state: String, pub event_timing: Option, pub watcher_name: String, } #[derive(Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct NetworkEventResource { pub resource_id: u64, pub resource_updates: Map, 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, } #[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, } #[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_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_size: usize, raw_headers: String, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct GetResponseContentReply { from: String, content: Option, content_discarded: bool, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct GetRequestPostDataReply { from: String, post_data: Option>, post_data_discarded: bool, } #[derive(Serialize)] struct GetRequestCookiesReply { from: String, cookies: Vec, } #[derive(Serialize)] struct GetResponseCookiesReply { from: String, cookies: Vec, } #[derive(Clone, Serialize)] pub struct ResponseCookieObj { pub name: String, pub value: String, pub path: Option, pub domain: Option, pub expires: Option, #[serde(rename = "httpOnly")] pub http_only: Option, pub secure: Option, #[serde(rename = "sameSite")] pub same_site: Option, } #[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, } #[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, _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 { 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::()) .map(|ct| ct.to_string()) .unwrap_or_default(); let transferred_size = response .headers .as_ref() .and_then(|hdrs| hdrs.typed_get::()) .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 { 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::>(); 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 { let header_value = request.headers.typed_get::()?; let cookies = header_value .iter() .map(|cookie| RequestCookieObj { name: cookie.0.to_string(), value: cookie.1.to_string(), }) .collect::>(); 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(map: &mut Map, obj: &Option) { 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, } } }