1use std::cell::Cell;
6use std::convert::TryInto;
7use std::ops::Deref;
8use std::sync::LazyLock;
9
10use base::generic_channel;
11use dom_struct::dom_struct;
12use embedder_traits::{EmbedderMsg, ProtocolHandlerUpdateRegistration, RegisterOrUnregister};
13use headers::HeaderMap;
14use http::header::{self, HeaderValue};
15use js::rust::MutableHandleValue;
16use net_traits::request::{
17 CredentialsMode, Destination, RequestBuilder, RequestId, RequestMode,
18 is_cors_safelisted_request_content_type,
19};
20use net_traits::{FetchMetadata, NetworkError, ResourceFetchTiming};
21use regex::Regex;
22use servo_config::pref;
23use servo_url::ServoUrl;
24
25use crate::body::Extractable;
26use crate::dom::bindings::cell::DomRefCell;
27use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
28use crate::dom::bindings::codegen::Bindings::WindowBinding::Window_Binding::WindowMethods;
29use crate::dom::bindings::codegen::Bindings::XMLHttpRequestBinding::BodyInit;
30use crate::dom::bindings::error::{Error, Fallible};
31use crate::dom::bindings::refcounted::Trusted;
32use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object};
33use crate::dom::bindings::root::{DomRoot, MutNullableDom};
34use crate::dom::bindings::str::{DOMString, USVString};
35use crate::dom::bindings::utils::to_frozen_array;
36#[cfg(feature = "bluetooth")]
37use crate::dom::bluetooth::Bluetooth;
38use crate::dom::clipboard::Clipboard;
39use crate::dom::credentialmanagement::credentialscontainer::CredentialsContainer;
40use crate::dom::csp::{GlobalCspReporting, Violation};
41#[cfg(feature = "gamepad")]
42use crate::dom::gamepad::Gamepad;
43#[cfg(feature = "gamepad")]
44use crate::dom::gamepad::gamepadevent::GamepadEventType;
45use crate::dom::geolocation::Geolocation;
46use crate::dom::globalscope::GlobalScope;
47use crate::dom::mediadevices::MediaDevices;
48use crate::dom::mediasession::MediaSession;
49use crate::dom::mimetypearray::MimeTypeArray;
50use crate::dom::navigatorinfo;
51use crate::dom::performance::performanceresourcetiming::InitiatorType;
52use crate::dom::permissions::Permissions;
53use crate::dom::pluginarray::PluginArray;
54use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
55use crate::dom::servointernals::ServoInternals;
56#[cfg(feature = "webgpu")]
57use crate::dom::webgpu::gpu::GPU;
58use crate::dom::window::Window;
59#[cfg(feature = "webxr")]
60use crate::dom::xrsystem::XRSystem;
61use crate::fetch::RequestWithGlobalScope;
62use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
63use crate::script_runtime::{CanGc, JSContext};
64
65pub(super) fn hardware_concurrency() -> u64 {
66 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
67
68 *CPUS
69}
70
71static SAFELISTED_SCHEMES: [&str; 24] = [
73 "bitcoin",
74 "ftp",
75 "ftps",
76 "geo",
77 "im",
78 "irc",
79 "ircs",
80 "magnet",
81 "mailto",
82 "matrix",
83 "mms",
84 "news",
85 "nntp",
86 "openpgp4fpr",
87 "sftp",
88 "sip",
89 "sms",
90 "smsto",
91 "ssh",
92 "tel",
93 "urn",
94 "webcal",
95 "wtai",
96 "xmpp",
97];
98
99fn matches_web_plus_protocol(scheme: &str) -> bool {
101 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
102 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
103
104 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
105}
106
107#[dom_struct]
108pub(crate) struct Navigator {
109 reflector_: Reflector,
110 #[cfg(feature = "bluetooth")]
111 bluetooth: MutNullableDom<Bluetooth>,
112 credentials: MutNullableDom<CredentialsContainer>,
113 plugins: MutNullableDom<PluginArray>,
114 mime_types: MutNullableDom<MimeTypeArray>,
115 service_worker: MutNullableDom<ServiceWorkerContainer>,
116 #[cfg(feature = "webxr")]
117 xr: MutNullableDom<XRSystem>,
118 mediadevices: MutNullableDom<MediaDevices>,
119 #[cfg(feature = "gamepad")]
121 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
122 permissions: MutNullableDom<Permissions>,
123 mediasession: MutNullableDom<MediaSession>,
124 clipboard: MutNullableDom<Clipboard>,
125 #[cfg(feature = "webgpu")]
126 gpu: MutNullableDom<GPU>,
127 #[cfg(feature = "gamepad")]
129 has_gamepad_gesture: Cell<bool>,
130 servo_internals: MutNullableDom<ServoInternals>,
131}
132
133impl Navigator {
134 fn new_inherited() -> Navigator {
135 Navigator {
136 reflector_: Reflector::new(),
137 #[cfg(feature = "bluetooth")]
138 bluetooth: Default::default(),
139 credentials: Default::default(),
140 plugins: Default::default(),
141 mime_types: Default::default(),
142 service_worker: Default::default(),
143 #[cfg(feature = "webxr")]
144 xr: Default::default(),
145 mediadevices: Default::default(),
146 #[cfg(feature = "gamepad")]
147 gamepads: Default::default(),
148 permissions: Default::default(),
149 mediasession: Default::default(),
150 clipboard: Default::default(),
151 #[cfg(feature = "webgpu")]
152 gpu: Default::default(),
153 #[cfg(feature = "gamepad")]
154 has_gamepad_gesture: Cell::new(false),
155 servo_internals: Default::default(),
156 }
157 }
158
159 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
160 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
161 }
162
163 #[cfg(feature = "webxr")]
164 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
165 self.xr.get()
166 }
167
168 #[cfg(feature = "gamepad")]
169 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
170 self.gamepads.borrow().get(index).and_then(|g| g.get())
171 }
172
173 #[cfg(feature = "gamepad")]
174 pub(crate) fn set_gamepad(&self, index: usize, gamepad: &Gamepad, can_gc: CanGc) {
175 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
176 gamepad_to_set.set(Some(gamepad));
177 }
178 if self.has_gamepad_gesture.get() {
179 gamepad.set_exposed(true);
180 if self.global().as_window().Document().is_fully_active() {
181 gamepad.notify_event(GamepadEventType::Connected, can_gc);
182 }
183 }
184 }
185
186 #[cfg(feature = "gamepad")]
187 pub(crate) fn remove_gamepad(&self, index: usize) {
188 if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
189 gamepad_to_remove.set(None);
190 }
191 self.shrink_gamepads_list();
192 }
193
194 #[cfg(feature = "gamepad")]
196 pub(crate) fn select_gamepad_index(&self) -> u32 {
197 let mut gamepad_list = self.gamepads.borrow_mut();
198 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
199 index as u32
200 } else {
201 let len = gamepad_list.len();
202 gamepad_list.resize_with(len + 1, Default::default);
203 len as u32
204 }
205 }
206
207 #[cfg(feature = "gamepad")]
208 fn shrink_gamepads_list(&self) {
209 let mut gamepad_list = self.gamepads.borrow_mut();
210 for i in (0..gamepad_list.len()).rev() {
211 if gamepad_list.get(i).is_none() {
212 gamepad_list.remove(i);
213 } else {
214 break;
215 }
216 }
217 }
218
219 #[cfg(feature = "gamepad")]
220 pub(crate) fn has_gamepad_gesture(&self) -> bool {
221 self.has_gamepad_gesture.get()
222 }
223
224 #[cfg(feature = "gamepad")]
225 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
226 self.has_gamepad_gesture.set(has_gamepad_gesture);
227 }
228
229 fn normalize_protocol_handler_parameters(
231 &self,
232 scheme: DOMString,
233 url: USVString,
234 ) -> Fallible<(String, ServoUrl)> {
235 let scheme = scheme.to_ascii_lowercase();
237 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
240 return Err(Error::Security(None));
241 }
242 if !url.contains("%s") {
244 return Err(Error::Syntax(Some(
245 "Missing replacement string %s in URL".to_owned(),
246 )));
247 }
248 let environment = self.global();
250 let window = environment.as_window();
252 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
253 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
255 };
256 if !matches!(url.scheme(), "http" | "https") {
259 return Err(Error::Security(None));
260 }
261 let environment_origin = environment.origin().immutable().clone();
262 if url.origin() != environment_origin {
263 return Err(Error::Security(None));
264 }
265 assert!(url.is_potentially_trustworthy());
267 Ok((scheme, url))
269 }
270
271 fn send_protocol_update_registration_to_embedder(
272 &self,
273 registration: ProtocolHandlerUpdateRegistration,
274 ) {
275 let global = self.global();
276 let window = global.as_window();
277 let (sender, _) = generic_channel::channel().unwrap();
278 let _ = global
279 .script_to_embedder_chan()
280 .send(EmbedderMsg::AllowProtocolHandlerRequest(
281 window.webview_id(),
282 registration,
283 sender,
284 ));
285 }
286}
287
288impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
289 fn Product(&self) -> DOMString {
291 navigatorinfo::Product()
292 }
293
294 fn ProductSub(&self) -> DOMString {
296 navigatorinfo::ProductSub()
297 }
298
299 fn Vendor(&self) -> DOMString {
301 navigatorinfo::Vendor()
302 }
303
304 fn VendorSub(&self) -> DOMString {
306 navigatorinfo::VendorSub()
307 }
308
309 fn TaintEnabled(&self) -> bool {
311 navigatorinfo::TaintEnabled()
312 }
313
314 fn AppName(&self) -> DOMString {
316 navigatorinfo::AppName()
317 }
318
319 fn AppCodeName(&self) -> DOMString {
321 navigatorinfo::AppCodeName()
322 }
323
324 fn Platform(&self) -> DOMString {
326 navigatorinfo::Platform()
327 }
328
329 fn UserAgent(&self) -> DOMString {
331 navigatorinfo::UserAgent(&pref!(user_agent))
332 }
333
334 fn AppVersion(&self) -> DOMString {
336 navigatorinfo::AppVersion()
337 }
338
339 #[cfg(feature = "bluetooth")]
341 fn Bluetooth(&self) -> DomRoot<Bluetooth> {
342 self.bluetooth
343 .or_init(|| Bluetooth::new(&self.global(), CanGc::note()))
344 }
345
346 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
348 self.credentials
349 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::note()))
350 }
351
352 fn Geolocation(&self) -> DomRoot<Geolocation> {
354 Geolocation::new(&self.global(), CanGc::note())
355 }
356
357 fn Language(&self) -> DOMString {
359 navigatorinfo::Language()
360 }
361
362 fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
364 to_frozen_array(&[self.Language()], cx, retval, can_gc)
365 }
366
367 fn OnLine(&self) -> bool {
369 true
370 }
371
372 fn Plugins(&self) -> DomRoot<PluginArray> {
374 self.plugins
375 .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
376 }
377
378 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
380 self.mime_types
381 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
382 }
383
384 fn JavaEnabled(&self) -> bool {
386 false
387 }
388
389 fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
391 self.service_worker
392 .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
393 }
394
395 fn CookieEnabled(&self) -> bool {
397 true
398 }
399
400 #[cfg(feature = "gamepad")]
402 fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
403 let global = self.global();
404 let window = global.as_window();
405 let doc = window.Document();
406
407 if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
409 return Vec::new();
410 }
411
412 self.gamepads.borrow().iter().map(|g| g.get()).collect()
413 }
414 fn Permissions(&self) -> DomRoot<Permissions> {
416 self.permissions
417 .or_init(|| Permissions::new(&self.global(), CanGc::note()))
418 }
419
420 #[cfg(feature = "webxr")]
422 fn Xr(&self) -> DomRoot<XRSystem> {
423 self.xr
424 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
425 }
426
427 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
429 self.mediadevices
430 .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
431 }
432
433 fn MediaSession(&self) -> DomRoot<MediaSession> {
435 self.mediasession.or_init(|| {
436 let global = self.global();
444 let window = global.as_window();
445 MediaSession::new(window, CanGc::note())
446 })
447 }
448
449 #[cfg(feature = "webgpu")]
451 fn Gpu(&self) -> DomRoot<GPU> {
452 self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
453 }
454
455 fn HardwareConcurrency(&self) -> u64 {
457 hardware_concurrency()
458 }
459
460 fn Clipboard(&self) -> DomRoot<Clipboard> {
462 self.clipboard
463 .or_init(|| Clipboard::new(&self.global(), CanGc::note()))
464 }
465
466 fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
468 let global = self.global();
469 let base = global.api_base_url();
471 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
479 return Err(Error::Type("Cannot parse URL".to_owned()));
480 };
481 if !matches!(url.scheme(), "http" | "https") {
482 return Err(Error::Type("URL is not http(s)".to_owned()));
483 }
484 let mut request_body = None;
485 let mut headers = HeaderMap::with_capacity(1);
487 let mut cors_mode = RequestMode::NoCors;
489 if let Some(data) = data {
491 let extracted_body = data.extract(&global, true, can_gc)?;
494 if let Some(total_bytes) = extracted_body.total_bytes {
498 let in_flight_keep_alive_bytes =
499 global.total_size_of_in_flight_keep_alive_records();
500 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
501 return Ok(false);
502 }
503 }
504 if let Some(content_type) = extracted_body.content_type.as_ref() {
506 cors_mode = RequestMode::CorsMode;
508 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
511 cors_mode = RequestMode::NoCors;
512 }
513 headers.insert(
518 header::CONTENT_TYPE,
519 HeaderValue::from_str(&content_type.str()).unwrap(),
520 );
521 }
522 request_body = Some(extracted_body.into_net_request_body().0);
523 }
524 let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
526 .mode(cors_mode)
527 .destination(Destination::None)
528 .with_global_scope(&global)
529 .method(http::Method::POST)
530 .body(request_body)
531 .keep_alive(true)
532 .credentials_mode(CredentialsMode::Include)
533 .headers(headers);
534 global.fetch(
536 request,
537 BeaconFetchListener {
538 url,
539 global: Trusted::new(&global),
540 },
541 global.task_manager().networking_task_source().into(),
542 );
543 Ok(true)
546 }
547
548 fn Servo(&self) -> DomRoot<ServoInternals> {
550 self.servo_internals
551 .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
552 }
553
554 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
556 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
559 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
571 scheme,
572 url,
573 register_or_unregister: RegisterOrUnregister::Register,
574 });
575 Ok(())
576 }
577
578 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
580 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
583 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
585 scheme,
586 url,
587 register_or_unregister: RegisterOrUnregister::Unregister,
588 });
589 Ok(())
590 }
591}
592
593struct BeaconFetchListener {
594 url: ServoUrl,
596 global: Trusted<GlobalScope>,
598}
599
600impl FetchResponseListener for BeaconFetchListener {
601 fn process_request_body(&mut self, _: RequestId) {}
602
603 fn process_request_eof(&mut self, _: RequestId) {}
604
605 fn process_response(
606 &mut self,
607 _: RequestId,
608 fetch_metadata: Result<FetchMetadata, NetworkError>,
609 ) {
610 _ = fetch_metadata;
611 }
612
613 fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
614 _ = chunk;
615 }
616
617 fn process_response_eof(
618 self,
619 _: RequestId,
620 response: Result<(), NetworkError>,
621 timing: ResourceFetchTiming,
622 ) {
623 submit_timing(&self, &response, &timing, CanGc::note());
624 }
625
626 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
627 let global = self.resource_timing_global();
628 global.report_csp_violations(violations, None, None);
629 }
630}
631
632impl ResourceTimingListener for BeaconFetchListener {
633 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
634 (InitiatorType::Beacon, self.url.clone())
635 }
636
637 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
638 self.global.root()
639 }
640}