From 6565d982bd6db7c96cc398b3d1567c3b8eacbde0 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Sat, 30 Aug 2025 12:51:58 -0400 Subject: [PATCH] servoshell: Support runtime preference manipulation (#38159) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These changes add a custom servo:preferences URL that allows modifying selected preferences at runtime. The goal of this work is to make it easy to test pages while toggling experimental web platform features, and support quickly changing the User-Agent header. Testing: Manually verified that spacex.com loads correctly after changing the user agent, and that https://polygon.io/ displays grid elements correctly and no console errors with the experimental prefs enabled. Fixes: #35862 Screenshot 2025-07-18 at 1 06 23 AM --------- Signed-off-by: Josh Matthews --- components/config/prefs.rs | 2 +- components/constellation/constellation.rs | 8 ++ components/constellation/pipeline.rs | 6 + components/net/http_cache.rs | 4 + components/net/protocols/mod.rs | 19 +++ components/script/dom/servointernals.rs | 56 +++++++- components/script/script_thread.rs | 9 ++ .../webidls/ServoInternals.webidl | 7 + components/servo/lib.rs | 3 + components/shared/script/lib.rs | 2 + ports/servoshell/desktop/protocols/servo.rs | 52 +++++++- ports/servoshell/prefs.rs | 42 +++--- resources/resource_protocol/preferences.html | 123 ++++++++++++++++++ 13 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 resources/resource_protocol/preferences.html diff --git a/components/config/prefs.rs b/components/config/prefs.rs index 8b3c5e86788..271bc6ec8e7 100644 --- a/components/config/prefs.rs +++ b/components/config/prefs.rs @@ -452,7 +452,7 @@ pub enum UserAgentPlatform { impl UserAgentPlatform { /// Return the default `UserAgentPlatform` for this platform. This is /// not an implementation of `Default` so that it can be `const`. - const fn default() -> Self { + pub const fn default() -> Self { if cfg!(target_os = "android") { Self::Android } else if cfg!(target_env = "ohos") { diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index e898933ada8..9ec81cdfc93 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -482,6 +482,9 @@ pub struct Constellation { /// When in single-process mode, join handles for script-threads. script_join_handles: HashMap>, + + /// A list of URLs that can access privileged internal APIs. + privileged_urls: Vec, } /// State needed to construct a constellation. @@ -535,6 +538,9 @@ pub struct InitialConstellationState { /// User content manager pub user_content_manager: UserContentManager, + /// A list of URLs that can access privileged internal APIs. + pub privileged_urls: Vec, + /// The async runtime. pub async_runtime: Box, } @@ -732,6 +738,7 @@ where process_manager: ProcessManager::new(state.mem_profiler_chan), async_runtime: state.async_runtime, script_join_handles: Default::default(), + privileged_urls: state.privileged_urls, }; constellation.run(); @@ -1003,6 +1010,7 @@ where player_context: WindowGLContext::get(), rippy_data: self.rippy_data.clone(), user_content_manager: self.user_content_manager.clone(), + privileged_urls: self.privileged_urls.clone(), }); let pipeline = match result { diff --git a/components/constellation/pipeline.rs b/components/constellation/pipeline.rs index 83db415e84c..78924513bbe 100644 --- a/components/constellation/pipeline.rs +++ b/components/constellation/pipeline.rs @@ -204,6 +204,9 @@ pub struct InitialPipelineState { /// User content manager pub user_content_manager: UserContentManager, + + /// A list of URLs that can access privileged internal APIs. + pub privileged_urls: Vec, } pub struct NewPipeline { @@ -305,6 +308,7 @@ impl Pipeline { rippy_data: state.rippy_data, user_content_manager: state.user_content_manager, lifeline_sender: None, + privileged_urls: state.privileged_urls, }; // Spawn the child process. @@ -497,6 +501,7 @@ pub struct UnprivilegedPipelineContent { rippy_data: Vec, user_content_manager: UserContentManager, lifeline_sender: Option>, + privileged_urls: Vec, } impl UnprivilegedPipelineContent { @@ -543,6 +548,7 @@ impl UnprivilegedPipelineContent { player_context: self.player_context.clone(), inherited_secure_context: self.load_data.inherited_secure_context, user_content_manager: self.user_content_manager, + privileged_urls: self.privileged_urls, }, layout_factory, Arc::new(self.system_font_service.to_proxy()), diff --git a/components/net/http_cache.rs b/components/net/http_cache.rs index 94a6331bb30..8ff8e808d82 100644 --- a/components/net/http_cache.rs +++ b/components/net/http_cache.rs @@ -514,6 +514,10 @@ impl HttpCache { request: &Request, done_chan: &mut DoneChannel, ) -> Option { + if pref!(network_http_cache_disabled) { + return None; + } + // TODO: generate warning headers as appropriate debug!("trying to construct cache response for {:?}", request.url()); if request.method != Method::GET { diff --git a/components/net/protocols/mod.rs b/components/net/protocols/mod.rs index 6dc58ceab64..a4d420bf2a5 100644 --- a/components/net/protocols/mod.rs +++ b/components/net/protocols/mod.rs @@ -30,6 +30,13 @@ use file::FileProtocolHander; static FORBIDDEN_SCHEMES: [&str; 4] = ["http", "https", "chrome", "about"]; pub trait ProtocolHandler: Send + Sync { + /// A list of schema-less URLs that can be resolved against this handler's + /// scheme. These URLs will be granted access to the `navigator.servo` + /// interface to perform privileged operations that manipulate Servo internals. + fn privileged_paths(&self) -> &'static [&'static str] { + &[] + } + /// Triggers the load of a resource for this protocol and returns a future /// that will produce a Response. Even if the protocol is not backed by a /// http endpoint, it is recommended to a least provide: @@ -132,6 +139,18 @@ impl ProtocolRegistry { .get(scheme) .is_some_and(|handler| handler.is_secure()) } + + pub fn privileged_urls(&self) -> Vec { + self.handlers + .iter() + .flat_map(|(scheme, handler)| { + let paths = handler.privileged_paths(); + paths + .iter() + .filter_map(move |path| ServoUrl::parse(&format!("{scheme}:{path}")).ok()) + }) + .collect() + } } /// Test if the URL is potentially trustworthy or the custom protocol is registered as secure diff --git a/components/script/dom/servointernals.rs b/components/script/dom/servointernals.rs index b104238a8af..58b572a3eb6 100644 --- a/components/script/dom/servointernals.rs +++ b/components/script/dom/servointernals.rs @@ -8,11 +8,13 @@ use constellation_traits::ScriptToConstellationMessage; use dom_struct::dom_struct; use js::rust::HandleObject; use profile_traits::mem::MemoryReportResult; +use script_bindings::error::{Error, Fallible}; use script_bindings::interfaces::ServoInternalsHelpers; use script_bindings::script_runtime::JSContext; +use script_bindings::str::USVString; +use servo_config::prefs::{self, PrefValue}; use crate::dom::bindings::codegen::Bindings::ServoInternalsBinding::ServoInternalsMethods; -use crate::dom::bindings::error::Error; use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object}; use crate::dom::bindings::root::DomRoot; use crate::dom::globalscope::GlobalScope; @@ -20,6 +22,7 @@ use crate::dom::promise::Promise; use crate::realms::{AlreadyInRealm, InRealm}; use crate::routed_promise::{RoutedPromiseListener, route_promise}; use crate::script_runtime::CanGc; +use crate::script_thread::ScriptThread; #[dom_struct] pub(crate) struct ServoInternals { @@ -55,6 +58,51 @@ impl ServoInternalsMethods for ServoInternals { } promise } + + /// + fn GetBoolPreference(&self, name: USVString) -> Fallible { + if let PrefValue::Bool(b) = prefs::get().get_value(&name) { + return Ok(b); + } + Err(Error::TypeMismatch) + } + + /// + fn GetIntPreference(&self, name: USVString) -> Fallible { + if let PrefValue::Int(i) = prefs::get().get_value(&name) { + return Ok(i); + } + Err(Error::TypeMismatch) + } + + /// + fn GetStringPreference(&self, name: USVString) -> Fallible { + if let PrefValue::Str(s) = prefs::get().get_value(&name) { + return Ok(s.into()); + } + Err(Error::TypeMismatch) + } + + /// + fn SetBoolPreference(&self, name: USVString, value: bool) { + let mut current_prefs = prefs::get().clone(); + current_prefs.set_value(&name, value.into()); + prefs::set(current_prefs); + } + + /// + fn SetIntPreference(&self, name: USVString, value: i64) { + let mut current_prefs = prefs::get().clone(); + current_prefs.set_value(&name, value.into()); + prefs::set(current_prefs); + } + + /// + fn SetStringPreference(&self, name: USVString, value: USVString) { + let mut current_prefs = prefs::get().clone(); + current_prefs.set_value(&name, value.0.into()); + prefs::set(current_prefs); + } } impl RoutedPromiseListener for ServoInternals { @@ -66,14 +114,16 @@ impl RoutedPromiseListener for ServoInternals { } impl ServoInternalsHelpers for ServoInternals { - /// The navigator.servo api is only exposed to about: pages except about:blank + /// The navigator.servo api is exposed to about: pages except about:blank, as + /// well as any URLs provided by embedders that register new protocol handlers. #[allow(unsafe_code)] fn is_servo_internal(cx: JSContext, _global: HandleObject) -> bool { unsafe { let in_realm_proof = AlreadyInRealm::assert_for_cx(cx); let global_scope = GlobalScope::from_context(*cx, InRealm::Already(&in_realm_proof)); let url = global_scope.get_url(); - url.scheme() == "about" && url.as_str() != "about:blank" + (url.scheme() == "about" && url.as_str() != "about:blank") || + ScriptThread::is_servo_privileged(url) } } } diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 2d98acfbea9..9e2388d2b5a 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -346,6 +346,10 @@ pub struct ScriptThread { needs_rendering_update: Arc, debugger_global: Dom, + + /// A list of URLs that can access privileged internal APIs. + #[no_trace] + privileged_urls: Vec, } struct BHMExitSignal { @@ -1016,6 +1020,7 @@ impl ScriptThread { scheduled_update_the_rendering: Default::default(), needs_rendering_update: Arc::new(AtomicBool::new(false)), debugger_global: debugger_global.as_traced(), + privileged_urls: state.privileged_urls, } } @@ -4030,6 +4035,10 @@ impl ScriptThread { }; document.event_handler().handle_refresh_cursor(); } + + pub(crate) fn is_servo_privileged(url: ServoUrl) -> bool { + with_script_thread(|script_thread| script_thread.privileged_urls.contains(&url)) + } } impl Drop for ScriptThread { diff --git a/components/script_bindings/webidls/ServoInternals.webidl b/components/script_bindings/webidls/ServoInternals.webidl index 609d49180e4..94bb8b286c7 100644 --- a/components/script_bindings/webidls/ServoInternals.webidl +++ b/components/script_bindings/webidls/ServoInternals.webidl @@ -11,6 +11,13 @@ Func="ServoInternals::is_servo_internal"] interface ServoInternals { Promise reportMemory(); + + [Throws] USVString getStringPreference(USVString name); + [Throws] long long getIntPreference(USVString name); + [Throws] boolean getBoolPreference(USVString name); + undefined setStringPreference(USVString name, USVString value); + undefined setIntPreference(USVString name, long long value); + undefined setBoolPreference(USVString name, boolean value); }; partial interface Navigator { diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 1ab513151f6..eef2b8813cd 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -1149,6 +1149,8 @@ fn create_constellation( let bluetooth_thread: IpcSender = BluetoothThreadFactory::new(embedder_proxy.clone()); + let privileged_urls = protocols.privileged_urls(); + let (public_resource_threads, private_resource_threads, async_runtime) = new_resource_threads( devtools_sender.clone(), time_profiler_chan.clone(), @@ -1191,6 +1193,7 @@ fn create_constellation( wgpu_image_map, user_content_manager, async_runtime, + privileged_urls, }; let layout_factory = Arc::new(LayoutFactoryImpl()); diff --git a/components/shared/script/lib.rs b/components/shared/script/lib.rs index 6e03d0cbb38..e5051a069af 100644 --- a/components/shared/script/lib.rs +++ b/components/shared/script/lib.rs @@ -354,6 +354,8 @@ pub struct InitialScriptState { pub player_context: WindowGLContext, /// User content manager pub user_content_manager: UserContentManager, + /// A list of URLs that can access privileged internal APIs. + pub privileged_urls: Vec, } /// Errors from executing a paint worklet diff --git a/ports/servoshell/desktop/protocols/servo.rs b/ports/servoshell/desktop/protocols/servo.rs index 465d71a9e08..c6ad9aa0733 100644 --- a/ports/servoshell/desktop/protocols/servo.rs +++ b/ports/servoshell/desktop/protocols/servo.rs @@ -4,22 +4,37 @@ //! Loads resources using a mapping from well-known shortcuts to resource: urls. //! Recognized shortcuts: +//! - servo:default-user-agent +//! - servo:experimental-preferences //! - servo:newtab +//! - servo:preferences use std::future::Future; use std::pin::Pin; +use headers::{ContentType, HeaderMapExt}; use net::fetch::methods::{DoneChannel, FetchContext}; use net::protocols::ProtocolHandler; +use net_traits::ResourceFetchTiming; use net_traits::request::Request; -use net_traits::response::Response; +use net_traits::response::{Response, ResponseBody}; +use servo::config::prefs::UserAgentPlatform; use crate::desktop::protocols::resource::ResourceProtocolHandler; +use crate::prefs::EXPERIMENTAL_PREFS; #[derive(Default)] pub struct ServoProtocolHandler {} impl ProtocolHandler for ServoProtocolHandler { + fn privileged_paths(&self) -> &'static [&'static str] { + &["preferences"] + } + + fn is_fetchable(&self) -> bool { + true + } + fn load( &self, request: &mut Request, @@ -35,9 +50,44 @@ impl ProtocolHandler for ServoProtocolHandler { context, "/newtab.html", ), + + "preferences" => ResourceProtocolHandler::response_for_path( + request, + done_chan, + context, + "/preferences.html", + ), + + "experimental-preferences" => { + let pref_list = EXPERIMENTAL_PREFS + .iter() + .map(|pref| format!("\"{pref}\"")) + .collect::>() + .join(","); + json_response(request, format!("[{pref_list}]")) + }, + + "default-user-agent" => { + let user_agent = UserAgentPlatform::default().to_user_agent_string(); + json_response(request, format!("\"{user_agent}\"")) + }, + _ => Box::pin(std::future::ready(Response::network_internal_error( "Invalid shortcut", ))), } } } + +fn json_response( + request: &Request, + body: String, +) -> Pin + Send>> { + let mut response = Response::new( + request.current_url(), + ResourceFetchTiming::new(request.timing_type()), + ); + response.headers.typed_insert(ContentType::json()); + *response.body.lock().unwrap() = ResponseBody::Done(body.into_bytes()); + Box::pin(std::future::ready(response)) +} diff --git a/ports/servoshell/prefs.rs b/ports/servoshell/prefs.rs index 9c312331c32..4311562074c 100644 --- a/ports/servoshell/prefs.rs +++ b/ports/servoshell/prefs.rs @@ -20,6 +20,25 @@ use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; use url::Url; +pub(crate) static EXPERIMENTAL_PREFS: &[&str] = &[ + "dom_async_clipboard_enabled", + "dom_fontface_enabled", + "dom_intersection_observer_enabled", + "dom_mouse_event_which_enabled", + "dom_navigator_sendbeacon_enabled", + "dom_notification_enabled", + "dom_offscreen_canvas_enabled", + "dom_permissions_enabled", + "dom_resize_observer_enabled", + "dom_trusted_types_enabled", + "dom_webgl2_enabled", + "dom_webgpu_enabled", + "dom_xpath_enabled", + "layout_columns_enabled", + "layout_container_queries_enabled", + "layout_grid_enabled", +]; + #[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))] #[derive(Clone)] pub(crate) struct ServoShellPreferences { @@ -587,26 +606,9 @@ pub(crate) fn parse_command_line_arguments(args: Vec) -> ArgumentParsing .collect(); if opt_match.opt_present("enable-experimental-web-platform-features") { - vec![ - "dom_async_clipboard_enabled", - "dom_fontface_enabled", - "dom_intersection_observer_enabled", - "dom_mouse_event_which_enabled", - "dom_navigator_sendbeacon_enabled", - "dom_notification_enabled", - "dom_offscreen_canvas_enabled", - "dom_permissions_enabled", - "dom_resize_observer_enabled", - "dom_trusted_types_enabled", - "dom_webgl2_enabled", - "dom_webgpu_enabled", - "dom_xpath_enabled", - "layout_columns_enabled", - "layout_container_queries_enabled", - "layout_grid_enabled", - ] - .iter() - .for_each(|pref| preferences.set_value(pref, PrefValue::Bool(true))); + for pref in EXPERIMENTAL_PREFS { + preferences.set_value(pref, PrefValue::Bool(true)); + } } // Handle all command-line preferences overrides. diff --git a/resources/resource_protocol/preferences.html b/resources/resource_protocol/preferences.html new file mode 100644 index 00000000000..65ce6f73bba --- /dev/null +++ b/resources/resource_protocol/preferences.html @@ -0,0 +1,123 @@ + + + + servo:preferences + + + +

Preferences

+ +
Loading...
+ + +