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;
56use crate::dom::types::UserActivation;
57#[cfg(feature = "webgpu")]
58use crate::dom::webgpu::gpu::GPU;
59use crate::dom::window::Window;
60#[cfg(feature = "webxr")]
61use crate::dom::xrsystem::XRSystem;
62use crate::fetch::RequestWithGlobalScope;
63use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
64use crate::script_runtime::{CanGc, JSContext};
65
66pub(super) fn hardware_concurrency() -> u64 {
67 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
68
69 *CPUS
70}
71
72static SAFELISTED_SCHEMES: [&str; 24] = [
74 "bitcoin",
75 "ftp",
76 "ftps",
77 "geo",
78 "im",
79 "irc",
80 "ircs",
81 "magnet",
82 "mailto",
83 "matrix",
84 "mms",
85 "news",
86 "nntp",
87 "openpgp4fpr",
88 "sftp",
89 "sip",
90 "sms",
91 "smsto",
92 "ssh",
93 "tel",
94 "urn",
95 "webcal",
96 "wtai",
97 "xmpp",
98];
99
100fn matches_web_plus_protocol(scheme: &str) -> bool {
102 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
103 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
104
105 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
106}
107
108#[dom_struct]
109pub(crate) struct Navigator {
110 reflector_: Reflector,
111 #[cfg(feature = "bluetooth")]
112 bluetooth: MutNullableDom<Bluetooth>,
113 credentials: MutNullableDom<CredentialsContainer>,
114 plugins: MutNullableDom<PluginArray>,
115 mime_types: MutNullableDom<MimeTypeArray>,
116 service_worker: MutNullableDom<ServiceWorkerContainer>,
117 #[cfg(feature = "webxr")]
118 xr: MutNullableDom<XRSystem>,
119 mediadevices: MutNullableDom<MediaDevices>,
120 #[cfg(feature = "gamepad")]
122 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
123 permissions: MutNullableDom<Permissions>,
124 mediasession: MutNullableDom<MediaSession>,
125 clipboard: MutNullableDom<Clipboard>,
126 #[cfg(feature = "webgpu")]
127 gpu: MutNullableDom<GPU>,
128 #[cfg(feature = "gamepad")]
130 has_gamepad_gesture: Cell<bool>,
131 servo_internals: MutNullableDom<ServoInternals>,
132 user_activation: MutNullableDom<UserActivation>,
133}
134
135impl Navigator {
136 fn new_inherited() -> Navigator {
137 Navigator {
138 reflector_: Reflector::new(),
139 #[cfg(feature = "bluetooth")]
140 bluetooth: Default::default(),
141 credentials: Default::default(),
142 plugins: Default::default(),
143 mime_types: Default::default(),
144 service_worker: Default::default(),
145 #[cfg(feature = "webxr")]
146 xr: Default::default(),
147 mediadevices: Default::default(),
148 #[cfg(feature = "gamepad")]
149 gamepads: Default::default(),
150 permissions: Default::default(),
151 mediasession: Default::default(),
152 clipboard: Default::default(),
153 #[cfg(feature = "webgpu")]
154 gpu: Default::default(),
155 #[cfg(feature = "gamepad")]
156 has_gamepad_gesture: Cell::new(false),
157 servo_internals: Default::default(),
158 user_activation: Default::default(),
159 }
160 }
161
162 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
163 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
164 }
165
166 #[cfg(feature = "webxr")]
167 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
168 self.xr.get()
169 }
170
171 #[cfg(feature = "gamepad")]
172 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
173 self.gamepads.borrow().get(index).and_then(|g| g.get())
174 }
175
176 #[cfg(feature = "gamepad")]
177 pub(crate) fn set_gamepad(&self, index: usize, gamepad: &Gamepad, can_gc: CanGc) {
178 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
179 gamepad_to_set.set(Some(gamepad));
180 }
181 if self.has_gamepad_gesture.get() {
182 gamepad.set_exposed(true);
183 if self.global().as_window().Document().is_fully_active() {
184 gamepad.notify_event(GamepadEventType::Connected, can_gc);
185 }
186 }
187 }
188
189 #[cfg(feature = "gamepad")]
190 pub(crate) fn remove_gamepad(&self, index: usize) {
191 if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
192 gamepad_to_remove.set(None);
193 }
194 self.shrink_gamepads_list();
195 }
196
197 #[cfg(feature = "gamepad")]
199 pub(crate) fn select_gamepad_index(&self) -> u32 {
200 let mut gamepad_list = self.gamepads.borrow_mut();
201 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
202 index as u32
203 } else {
204 let len = gamepad_list.len();
205 gamepad_list.resize_with(len + 1, Default::default);
206 len as u32
207 }
208 }
209
210 #[cfg(feature = "gamepad")]
211 fn shrink_gamepads_list(&self) {
212 let mut gamepad_list = self.gamepads.borrow_mut();
213 for i in (0..gamepad_list.len()).rev() {
214 if gamepad_list.get(i).is_none() {
215 gamepad_list.remove(i);
216 } else {
217 break;
218 }
219 }
220 }
221
222 #[cfg(feature = "gamepad")]
223 pub(crate) fn has_gamepad_gesture(&self) -> bool {
224 self.has_gamepad_gesture.get()
225 }
226
227 #[cfg(feature = "gamepad")]
228 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
229 self.has_gamepad_gesture.set(has_gamepad_gesture);
230 }
231
232 fn normalize_protocol_handler_parameters(
234 &self,
235 scheme: DOMString,
236 url: USVString,
237 ) -> Fallible<(String, ServoUrl)> {
238 let scheme = scheme.to_ascii_lowercase();
240 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
243 return Err(Error::Security(None));
244 }
245 if !url.contains("%s") {
247 return Err(Error::Syntax(Some(
248 "Missing replacement string %s in URL".to_owned(),
249 )));
250 }
251 let environment = self.global();
253 let window = environment.as_window();
255 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
256 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
258 };
259 if !matches!(url.scheme(), "http" | "https") {
262 return Err(Error::Security(None));
263 }
264 let environment_origin = environment.origin().immutable().clone();
265 if url.origin() != environment_origin {
266 return Err(Error::Security(None));
267 }
268 assert!(url.is_potentially_trustworthy());
270 Ok((scheme, url))
272 }
273
274 fn send_protocol_update_registration_to_embedder(
275 &self,
276 registration: ProtocolHandlerUpdateRegistration,
277 ) {
278 let global = self.global();
279 let window = global.as_window();
280 let (sender, _) = generic_channel::channel().unwrap();
281 let _ = global
282 .script_to_embedder_chan()
283 .send(EmbedderMsg::AllowProtocolHandlerRequest(
284 window.webview_id(),
285 registration,
286 sender,
287 ));
288 }
289}
290
291impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
292 fn Product(&self) -> DOMString {
294 navigatorinfo::Product()
295 }
296
297 fn ProductSub(&self) -> DOMString {
299 navigatorinfo::ProductSub()
300 }
301
302 fn Vendor(&self) -> DOMString {
304 navigatorinfo::Vendor()
305 }
306
307 fn VendorSub(&self) -> DOMString {
309 navigatorinfo::VendorSub()
310 }
311
312 fn TaintEnabled(&self) -> bool {
314 navigatorinfo::TaintEnabled()
315 }
316
317 fn AppName(&self) -> DOMString {
319 navigatorinfo::AppName()
320 }
321
322 fn AppCodeName(&self) -> DOMString {
324 navigatorinfo::AppCodeName()
325 }
326
327 fn Platform(&self) -> DOMString {
329 navigatorinfo::Platform()
330 }
331
332 fn UserAgent(&self) -> DOMString {
334 navigatorinfo::UserAgent(&pref!(user_agent))
335 }
336
337 fn AppVersion(&self) -> DOMString {
339 navigatorinfo::AppVersion()
340 }
341
342 #[cfg(feature = "bluetooth")]
344 fn Bluetooth(&self) -> DomRoot<Bluetooth> {
345 self.bluetooth
346 .or_init(|| Bluetooth::new(&self.global(), CanGc::note()))
347 }
348
349 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
351 self.credentials
352 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::note()))
353 }
354
355 fn Geolocation(&self) -> DomRoot<Geolocation> {
357 Geolocation::new(&self.global(), CanGc::note())
358 }
359
360 fn Language(&self) -> DOMString {
362 navigatorinfo::Language()
363 }
364
365 fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
367 to_frozen_array(&[self.Language()], cx, retval, can_gc)
368 }
369
370 fn OnLine(&self) -> bool {
372 true
373 }
374
375 fn Plugins(&self) -> DomRoot<PluginArray> {
377 self.plugins
378 .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
379 }
380
381 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
383 self.mime_types
384 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
385 }
386
387 fn JavaEnabled(&self) -> bool {
389 false
390 }
391
392 fn PdfViewerEnabled(&self) -> bool {
394 false
395 }
396
397 fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
399 self.service_worker
400 .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
401 }
402
403 fn CookieEnabled(&self) -> bool {
405 true
406 }
407
408 #[cfg(feature = "gamepad")]
410 fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
411 let global = self.global();
412 let window = global.as_window();
413 let doc = window.Document();
414
415 if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
417 return Vec::new();
418 }
419
420 self.gamepads.borrow().iter().map(|g| g.get()).collect()
421 }
422 fn Permissions(&self) -> DomRoot<Permissions> {
424 self.permissions
425 .or_init(|| Permissions::new(&self.global(), CanGc::note()))
426 }
427
428 #[cfg(feature = "webxr")]
430 fn Xr(&self) -> DomRoot<XRSystem> {
431 self.xr
432 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
433 }
434
435 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
437 self.mediadevices
438 .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
439 }
440
441 fn MediaSession(&self) -> DomRoot<MediaSession> {
443 self.mediasession.or_init(|| {
444 let global = self.global();
452 let window = global.as_window();
453 MediaSession::new(window, CanGc::note())
454 })
455 }
456
457 #[cfg(feature = "webgpu")]
459 fn Gpu(&self) -> DomRoot<GPU> {
460 self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
461 }
462
463 fn HardwareConcurrency(&self) -> u64 {
465 hardware_concurrency()
466 }
467
468 fn Clipboard(&self) -> DomRoot<Clipboard> {
470 self.clipboard
471 .or_init(|| Clipboard::new(&self.global(), CanGc::note()))
472 }
473
474 fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
476 let global = self.global();
477 let base = global.api_base_url();
479 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
487 return Err(Error::Type(c"Cannot parse URL".to_owned()));
488 };
489 if !matches!(url.scheme(), "http" | "https") {
490 return Err(Error::Type(c"URL is not http(s)".to_owned()));
491 }
492 let mut request_body = None;
493 let mut headers = HeaderMap::with_capacity(1);
495 let mut cors_mode = RequestMode::NoCors;
497 if let Some(data) = data {
499 let extracted_body = data.extract(&global, true, can_gc)?;
502 if let Some(total_bytes) = extracted_body.total_bytes {
506 let in_flight_keep_alive_bytes =
507 global.total_size_of_in_flight_keep_alive_records();
508 if total_bytes as u64 + in_flight_keep_alive_bytes > 64 * 1024 {
509 return Ok(false);
510 }
511 }
512 if let Some(content_type) = extracted_body.content_type.as_ref() {
514 cors_mode = RequestMode::CorsMode;
516 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
519 cors_mode = RequestMode::NoCors;
520 }
521 headers.insert(
526 header::CONTENT_TYPE,
527 HeaderValue::from_str(&content_type.str()).unwrap(),
528 );
529 }
530 request_body = Some(extracted_body.into_net_request_body().0);
531 }
532 let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
534 .mode(cors_mode)
535 .destination(Destination::None)
536 .with_global_scope(&global)
537 .method(http::Method::POST)
538 .body(request_body)
539 .keep_alive(true)
540 .credentials_mode(CredentialsMode::Include)
541 .headers(headers);
542 global.fetch(
544 request,
545 BeaconFetchListener {
546 url,
547 global: Trusted::new(&global),
548 },
549 global.task_manager().networking_task_source().into(),
550 );
551 Ok(true)
554 }
555
556 fn Servo(&self) -> DomRoot<ServoInternals> {
558 self.servo_internals
559 .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
560 }
561
562 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
564 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
567 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
579 scheme,
580 url,
581 register_or_unregister: RegisterOrUnregister::Register,
582 });
583 Ok(())
584 }
585
586 fn UnregisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
588 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
591 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
593 scheme,
594 url,
595 register_or_unregister: RegisterOrUnregister::Unregister,
596 });
597 Ok(())
598 }
599
600 fn UserActivation(&self, can_gc: CanGc) -> DomRoot<UserActivation> {
602 self.user_activation
603 .or_init(|| UserActivation::new(&self.global(), can_gc))
604 }
605}
606
607struct BeaconFetchListener {
608 url: ServoUrl,
610 global: Trusted<GlobalScope>,
612}
613
614impl FetchResponseListener for BeaconFetchListener {
615 fn process_request_body(&mut self, _: RequestId) {}
616
617 fn process_request_eof(&mut self, _: RequestId) {}
618
619 fn process_response(
620 &mut self,
621 _: RequestId,
622 fetch_metadata: Result<FetchMetadata, NetworkError>,
623 ) {
624 _ = fetch_metadata;
625 }
626
627 fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
628 _ = chunk;
629 }
630
631 fn process_response_eof(
632 self,
633 cx: &mut js::context::JSContext,
634 _: RequestId,
635 response: Result<(), NetworkError>,
636 timing: ResourceFetchTiming,
637 ) {
638 submit_timing(&self, &response, &timing, CanGc::from_cx(cx));
639 }
640
641 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
642 let global = self.resource_timing_global();
643 global.report_csp_violations(violations, None, None);
644 }
645}
646
647impl ResourceTimingListener for BeaconFetchListener {
648 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
649 (InitiatorType::Beacon, self.url.clone())
650 }
651
652 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
653 self.global.root()
654 }
655}