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};
41use crate::dom::gamepad::Gamepad;
42use crate::dom::gamepad::gamepadevent::GamepadEventType;
43use crate::dom::geolocation::Geolocation;
44use crate::dom::globalscope::GlobalScope;
45use crate::dom::mediadevices::MediaDevices;
46use crate::dom::mediasession::MediaSession;
47use crate::dom::mimetypearray::MimeTypeArray;
48use crate::dom::navigatorinfo;
49use crate::dom::performance::performanceresourcetiming::InitiatorType;
50use crate::dom::permissions::Permissions;
51use crate::dom::pluginarray::PluginArray;
52use crate::dom::serviceworkercontainer::ServiceWorkerContainer;
53use crate::dom::servointernals::ServoInternals;
54#[cfg(feature = "webgpu")]
55use crate::dom::webgpu::gpu::GPU;
56use crate::dom::window::Window;
57#[cfg(feature = "webxr")]
58use crate::dom::xrsystem::XRSystem;
59use crate::network_listener::{FetchResponseListener, ResourceTimingListener, submit_timing};
60use crate::script_runtime::{CanGc, JSContext};
61
62pub(super) fn hardware_concurrency() -> u64 {
63 static CPUS: LazyLock<u64> = LazyLock::new(|| num_cpus::get().try_into().unwrap_or(1));
64
65 *CPUS
66}
67
68static SAFELISTED_SCHEMES: [&str; 24] = [
70 "bitcoin",
71 "ftp",
72 "ftps",
73 "geo",
74 "im",
75 "irc",
76 "ircs",
77 "magnet",
78 "mailto",
79 "matrix",
80 "mms",
81 "news",
82 "nntp",
83 "openpgp4fpr",
84 "sftp",
85 "sip",
86 "sms",
87 "smsto",
88 "ssh",
89 "tel",
90 "urn",
91 "webcal",
92 "wtai",
93 "xmpp",
94];
95
96fn matches_web_plus_protocol(scheme: &str) -> bool {
98 static WEB_PLUS_SCHEME_GRAMMAR: LazyLock<Regex> =
99 LazyLock::new(|| Regex::new(r#"^web\+[a-z]+$"#).unwrap());
100
101 WEB_PLUS_SCHEME_GRAMMAR.is_match(scheme)
102}
103
104#[dom_struct]
105pub(crate) struct Navigator {
106 reflector_: Reflector,
107 #[cfg(feature = "bluetooth")]
108 bluetooth: MutNullableDom<Bluetooth>,
109 credentials: MutNullableDom<CredentialsContainer>,
110 plugins: MutNullableDom<PluginArray>,
111 mime_types: MutNullableDom<MimeTypeArray>,
112 service_worker: MutNullableDom<ServiceWorkerContainer>,
113 #[cfg(feature = "webxr")]
114 xr: MutNullableDom<XRSystem>,
115 mediadevices: MutNullableDom<MediaDevices>,
116 gamepads: DomRefCell<Vec<MutNullableDom<Gamepad>>>,
118 permissions: MutNullableDom<Permissions>,
119 mediasession: MutNullableDom<MediaSession>,
120 clipboard: MutNullableDom<Clipboard>,
121 #[cfg(feature = "webgpu")]
122 gpu: MutNullableDom<GPU>,
123 has_gamepad_gesture: Cell<bool>,
125 servo_internals: MutNullableDom<ServoInternals>,
126}
127
128impl Navigator {
129 fn new_inherited() -> Navigator {
130 Navigator {
131 reflector_: Reflector::new(),
132 #[cfg(feature = "bluetooth")]
133 bluetooth: Default::default(),
134 credentials: Default::default(),
135 plugins: Default::default(),
136 mime_types: Default::default(),
137 service_worker: Default::default(),
138 #[cfg(feature = "webxr")]
139 xr: Default::default(),
140 mediadevices: Default::default(),
141 gamepads: Default::default(),
142 permissions: Default::default(),
143 mediasession: Default::default(),
144 clipboard: Default::default(),
145 #[cfg(feature = "webgpu")]
146 gpu: Default::default(),
147 has_gamepad_gesture: Cell::new(false),
148 servo_internals: Default::default(),
149 }
150 }
151
152 pub(crate) fn new(window: &Window, can_gc: CanGc) -> DomRoot<Navigator> {
153 reflect_dom_object(Box::new(Navigator::new_inherited()), window, can_gc)
154 }
155
156 #[cfg(feature = "webxr")]
157 pub(crate) fn xr(&self) -> Option<DomRoot<XRSystem>> {
158 self.xr.get()
159 }
160
161 pub(crate) fn get_gamepad(&self, index: usize) -> Option<DomRoot<Gamepad>> {
162 self.gamepads.borrow().get(index).and_then(|g| g.get())
163 }
164
165 pub(crate) fn set_gamepad(&self, index: usize, gamepad: &Gamepad, can_gc: CanGc) {
166 if let Some(gamepad_to_set) = self.gamepads.borrow().get(index) {
167 gamepad_to_set.set(Some(gamepad));
168 }
169 if self.has_gamepad_gesture.get() {
170 gamepad.set_exposed(true);
171 if self.global().as_window().Document().is_fully_active() {
172 gamepad.notify_event(GamepadEventType::Connected, can_gc);
173 }
174 }
175 }
176
177 pub(crate) fn remove_gamepad(&self, index: usize) {
178 if let Some(gamepad_to_remove) = self.gamepads.borrow_mut().get(index) {
179 gamepad_to_remove.set(None);
180 }
181 self.shrink_gamepads_list();
182 }
183
184 pub(crate) fn select_gamepad_index(&self) -> u32 {
186 let mut gamepad_list = self.gamepads.borrow_mut();
187 if let Some(index) = gamepad_list.iter().position(|g| g.get().is_none()) {
188 index as u32
189 } else {
190 let len = gamepad_list.len();
191 gamepad_list.resize_with(len + 1, Default::default);
192 len as u32
193 }
194 }
195
196 fn shrink_gamepads_list(&self) {
197 let mut gamepad_list = self.gamepads.borrow_mut();
198 for i in (0..gamepad_list.len()).rev() {
199 if gamepad_list.get(i).is_none() {
200 gamepad_list.remove(i);
201 } else {
202 break;
203 }
204 }
205 }
206
207 pub(crate) fn has_gamepad_gesture(&self) -> bool {
208 self.has_gamepad_gesture.get()
209 }
210
211 pub(crate) fn set_has_gamepad_gesture(&self, has_gamepad_gesture: bool) {
212 self.has_gamepad_gesture.set(has_gamepad_gesture);
213 }
214
215 fn normalize_protocol_handler_parameters(
217 &self,
218 scheme: DOMString,
219 url: USVString,
220 ) -> Fallible<(String, ServoUrl)> {
221 let scheme = scheme.to_ascii_lowercase();
223 if !SAFELISTED_SCHEMES.contains(&scheme.as_ref()) && !matches_web_plus_protocol(&scheme) {
226 return Err(Error::Security(None));
227 }
228 if !url.contains("%s") {
230 return Err(Error::Syntax(Some(
231 "Missing replacement string %s in URL".to_owned(),
232 )));
233 }
234 let environment = self.global();
236 let window = environment.as_window();
238 let Ok(url) = window.Document().encoding_parse_a_url(&url) else {
239 return Err(Error::Syntax(Some("Cannot parse URL".to_owned())));
241 };
242 if !matches!(url.scheme(), "http" | "https") {
245 return Err(Error::Security(None));
246 }
247 let environment_origin = environment.origin().immutable().clone();
248 if url.origin() != environment_origin {
249 return Err(Error::Security(None));
250 }
251 assert!(url.is_potentially_trustworthy());
253 Ok((scheme, url))
255 }
256
257 fn send_protocol_update_registration_to_embedder(
258 &self,
259 registration: ProtocolHandlerUpdateRegistration,
260 ) {
261 let global = self.global();
262 let window = global.as_window();
263 let (sender, _) = generic_channel::channel().unwrap();
264 let _ = global
265 .script_to_embedder_chan()
266 .send(EmbedderMsg::AllowProtocolHandlerRequest(
267 window.webview_id(),
268 registration,
269 sender,
270 ));
271 }
272}
273
274impl NavigatorMethods<crate::DomTypeHolder> for Navigator {
275 fn Product(&self) -> DOMString {
277 navigatorinfo::Product()
278 }
279
280 fn ProductSub(&self) -> DOMString {
282 navigatorinfo::ProductSub()
283 }
284
285 fn Vendor(&self) -> DOMString {
287 navigatorinfo::Vendor()
288 }
289
290 fn VendorSub(&self) -> DOMString {
292 navigatorinfo::VendorSub()
293 }
294
295 fn TaintEnabled(&self) -> bool {
297 navigatorinfo::TaintEnabled()
298 }
299
300 fn AppName(&self) -> DOMString {
302 navigatorinfo::AppName()
303 }
304
305 fn AppCodeName(&self) -> DOMString {
307 navigatorinfo::AppCodeName()
308 }
309
310 fn Platform(&self) -> DOMString {
312 navigatorinfo::Platform()
313 }
314
315 fn UserAgent(&self) -> DOMString {
317 navigatorinfo::UserAgent(&pref!(user_agent))
318 }
319
320 fn AppVersion(&self) -> DOMString {
322 navigatorinfo::AppVersion()
323 }
324
325 #[cfg(feature = "bluetooth")]
327 fn Bluetooth(&self) -> DomRoot<Bluetooth> {
328 self.bluetooth
329 .or_init(|| Bluetooth::new(&self.global(), CanGc::note()))
330 }
331
332 fn Credentials(&self) -> DomRoot<CredentialsContainer> {
334 self.credentials
335 .or_init(|| CredentialsContainer::new(&self.global(), CanGc::note()))
336 }
337
338 fn Geolocation(&self) -> DomRoot<Geolocation> {
340 Geolocation::new(&self.global(), CanGc::note())
341 }
342
343 fn Language(&self) -> DOMString {
345 navigatorinfo::Language()
346 }
347
348 fn Languages(&self, cx: JSContext, can_gc: CanGc, retval: MutableHandleValue) {
350 to_frozen_array(&[self.Language()], cx, retval, can_gc)
351 }
352
353 fn OnLine(&self) -> bool {
355 true
356 }
357
358 fn Plugins(&self) -> DomRoot<PluginArray> {
360 self.plugins
361 .or_init(|| PluginArray::new(&self.global(), CanGc::note()))
362 }
363
364 fn MimeTypes(&self) -> DomRoot<MimeTypeArray> {
366 self.mime_types
367 .or_init(|| MimeTypeArray::new(&self.global(), CanGc::note()))
368 }
369
370 fn JavaEnabled(&self) -> bool {
372 false
373 }
374
375 fn ServiceWorker(&self) -> DomRoot<ServiceWorkerContainer> {
377 self.service_worker
378 .or_init(|| ServiceWorkerContainer::new(&self.global(), CanGc::note()))
379 }
380
381 fn CookieEnabled(&self) -> bool {
383 true
384 }
385
386 fn GetGamepads(&self) -> Vec<Option<DomRoot<Gamepad>>> {
388 let global = self.global();
389 let window = global.as_window();
390 let doc = window.Document();
391
392 if !doc.is_fully_active() || !self.has_gamepad_gesture.get() {
394 return Vec::new();
395 }
396
397 self.gamepads.borrow().iter().map(|g| g.get()).collect()
398 }
399 fn Permissions(&self) -> DomRoot<Permissions> {
401 self.permissions
402 .or_init(|| Permissions::new(&self.global(), CanGc::note()))
403 }
404
405 #[cfg(feature = "webxr")]
407 fn Xr(&self) -> DomRoot<XRSystem> {
408 self.xr
409 .or_init(|| XRSystem::new(self.global().as_window(), CanGc::note()))
410 }
411
412 fn MediaDevices(&self) -> DomRoot<MediaDevices> {
414 self.mediadevices
415 .or_init(|| MediaDevices::new(&self.global(), CanGc::note()))
416 }
417
418 fn MediaSession(&self) -> DomRoot<MediaSession> {
420 self.mediasession.or_init(|| {
421 let global = self.global();
429 let window = global.as_window();
430 MediaSession::new(window, CanGc::note())
431 })
432 }
433
434 #[cfg(feature = "webgpu")]
436 fn Gpu(&self) -> DomRoot<GPU> {
437 self.gpu.or_init(|| GPU::new(&self.global(), CanGc::note()))
438 }
439
440 fn HardwareConcurrency(&self) -> u64 {
442 hardware_concurrency()
443 }
444
445 fn Clipboard(&self) -> DomRoot<Clipboard> {
447 self.clipboard
448 .or_init(|| Clipboard::new(&self.global(), CanGc::note()))
449 }
450
451 fn SendBeacon(&self, url: USVString, data: Option<BodyInit>, can_gc: CanGc) -> Fallible<bool> {
453 let global = self.global();
454 let base = global.api_base_url();
456 let origin = global.origin().immutable().clone();
458 let Ok(url) = ServoUrl::parse_with_base(Some(&base), &url) else {
462 return Err(Error::Type("Cannot parse URL".to_owned()));
463 };
464 if !matches!(url.scheme(), "http" | "https") {
465 return Err(Error::Type("URL is not http(s)".to_owned()));
466 }
467 let mut request_body = None;
468 let mut headers = HeaderMap::with_capacity(1);
470 let mut cors_mode = RequestMode::NoCors;
472 if let Some(data) = data {
474 let extracted_body = data.extract(&global, can_gc)?;
477 if let Some(total_bytes) = extracted_body.total_bytes {
481 if total_bytes > 64 * 1024 {
482 return Ok(false);
483 }
484 }
485 if let Some(content_type) = extracted_body.content_type.as_ref() {
487 cors_mode = RequestMode::CorsMode;
489 if is_cors_safelisted_request_content_type(content_type.as_bytes().deref()) {
492 cors_mode = RequestMode::NoCors;
493 }
494 headers.insert(
499 header::CONTENT_TYPE,
500 HeaderValue::from_str(&content_type.str()).unwrap(),
501 );
502 }
503 request_body = Some(extracted_body.into_net_request_body().0);
504 }
505 let request = RequestBuilder::new(None, url.clone(), global.get_referrer())
507 .mode(cors_mode)
508 .destination(Destination::None)
509 .policy_container(global.policy_container())
510 .insecure_requests_policy(global.insecure_requests_policy())
511 .has_trustworthy_ancestor_origin(global.has_trustworthy_ancestor_or_current_origin())
512 .method(http::Method::POST)
513 .body(request_body)
514 .origin(origin)
515 .credentials_mode(CredentialsMode::Include)
517 .headers(headers);
518 global.fetch(
520 request,
521 BeaconFetchListener {
522 url,
523 global: Trusted::new(&global),
524 },
525 global.task_manager().networking_task_source().into(),
526 );
527 Ok(true)
530 }
531
532 fn Servo(&self) -> DomRoot<ServoInternals> {
534 self.servo_internals
535 .or_init(|| ServoInternals::new(&self.global(), CanGc::note()))
536 }
537
538 fn RegisterProtocolHandler(&self, scheme: DOMString, url: USVString) -> Fallible<()> {
540 let (scheme, url) = self.normalize_protocol_handler_parameters(scheme, url)?;
543 self.send_protocol_update_registration_to_embedder(ProtocolHandlerUpdateRegistration {
555 scheme,
556 url,
557 register_or_unregister: RegisterOrUnregister::Register,
558 });
559 Ok(())
560 }
561
562 fn UnregisterProtocolHandler(&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 {
569 scheme,
570 url,
571 register_or_unregister: RegisterOrUnregister::Unregister,
572 });
573 Ok(())
574 }
575}
576
577struct BeaconFetchListener {
578 url: ServoUrl,
580 global: Trusted<GlobalScope>,
582}
583
584impl FetchResponseListener for BeaconFetchListener {
585 fn process_request_body(&mut self, _: RequestId) {}
586
587 fn process_request_eof(&mut self, _: RequestId) {}
588
589 fn process_response(
590 &mut self,
591 _: RequestId,
592 fetch_metadata: Result<FetchMetadata, NetworkError>,
593 ) {
594 _ = fetch_metadata;
595 }
596
597 fn process_response_chunk(&mut self, _: RequestId, chunk: Vec<u8>) {
598 _ = chunk;
599 }
600
601 fn process_response_eof(
602 self,
603 _: RequestId,
604 response: Result<ResourceFetchTiming, NetworkError>,
605 ) {
606 if let Ok(response) = response {
607 submit_timing(&self, &response, CanGc::note());
608 }
609 }
610
611 fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
612 let global = self.resource_timing_global();
613 global.report_csp_violations(violations, None, None);
614 }
615}
616
617impl ResourceTimingListener for BeaconFetchListener {
618 fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
619 (InitiatorType::Beacon, self.url.clone())
620 }
621
622 fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
623 self.global.root()
624 }
625}