1use anyhow::Result;
33use std::collections::{HashMap, hash_map::DefaultHasher};
34use std::hash::{Hash, Hasher};
35use std::time::{SystemTime, UNIX_EPOCH};
36use std::{env, io};
37use url::Url;
38use winit::event::WindowEvent;
39use winit::window::WindowId;
40
41use super::tab::FetchKind;
42use super::ui::{BrowserUi, FetchParams};
43use super::{BrowserCommand, resource_loader::BrowserResourceLoader};
44use crate::browser::core::ui::TabId;
45use crate::engine::origin::Origin;
46use crate::engine::permissions::Permissions;
47use crate::platform::network::{NetworkCore, NetworkRequest};
48use crate::platform::renderer::draw_sink::DrawSink;
49use crate::platform::system::App;
50use crate::platform::system::shell::{BrowserHost, ShellCommand, WindowGeometry};
51
52pub(crate) struct PendingFetch {
54 window_id: WindowId,
55 tab_id: TabId,
56 kind: FetchKind,
57 url: Url,
58 generation: u64,
59 origin: Origin,
61 permissions: Permissions,
64}
65
66pub(crate) struct PendingFetches {
67 map: HashMap<usize, PendingFetch>,
69 counter: usize,
70}
71
72impl PendingFetches {
73 pub fn new() -> Self {
74 Self {
75 map: HashMap::new(),
76 counter: 0,
77 }
78 }
79 #[allow(clippy::too_many_arguments)]
81 pub fn insert(
82 &mut self,
83 window_id: WindowId,
84 tab_id: TabId,
85 kind: FetchKind,
86 url: Url,
87 generation: u64,
88 origin: Origin,
89 permissions: Permissions,
90 ) -> usize {
91 self.counter += 1;
92
93 let id = self.generate_id(&url);
94
95 self.map.insert(
96 id,
97 PendingFetch {
98 window_id,
99 tab_id,
100 kind,
101 url,
102 generation,
103 origin,
104 permissions,
105 },
106 );
107 id
108 }
109
110 fn generate_id(&self, url: &Url) -> usize {
111 let mut hasher = DefaultHasher::new();
113 url.hash(&mut hasher);
114 let url_hash = hasher.finish() as usize;
115
116 let now = SystemTime::now()
118 .duration_since(UNIX_EPOCH)
119 .expect("Time went backwards")
120 .as_nanos() as usize;
121
122 now ^ self.counter ^ url_hash
124 }
125
126 pub fn remove(&mut self, id: usize) -> Option<PendingFetch> {
127 self.map.remove(&id)
128 }
129}
130
131fn fetch_is_stale(current_generation: u64, fetch_generation: u64) -> bool {
139 current_generation != fetch_generation
140}
141
142pub struct BrowserApp {
170 windows: HashMap<WindowId, BrowserUi>,
172 default_window_size: (u32, u32),
174 default_window_title: String,
176 network: BrowserResourceLoader,
177 pending_fetches: PendingFetches,
178 default_ui: Option<BrowserUi>,
180}
181
182impl Default for BrowserApp {
183 fn default() -> Self {
184 Self::new((1280, 800), "Orinium Browser".to_string()).unwrap()
185 }
186}
187
188impl BrowserApp {
189 pub fn run(self) -> Result<()> {
192 if self.default_ui.is_none() {
193 anyhow::bail!("set_default_ui must be called before run()");
194 }
195 run_with_winit_backend(self)
196 }
197
198 pub fn new(
201 default_window_size: (u32, u32),
202 default_window_title: String,
203 ) -> Result<Self, io::Error> {
204 let network = BrowserResourceLoader::with_network(NetworkCore::new()?);
205
206 Ok(Self {
207 windows: HashMap::new(),
208 default_window_size,
209 default_window_title,
210 network,
211 pending_fetches: PendingFetches::new(),
212 default_ui: None,
213 })
214 }
215
216 pub fn open_window_with_ui(
223 &mut self,
224 window_id: WindowId,
225 geometry: WindowGeometry,
226 root_ui: BrowserUi,
227 ) {
228 self.register_window(window_id, &geometry, root_ui);
229 }
230
231 fn register_window(
233 &mut self,
234 window_id: WindowId,
235 geometry: &WindowGeometry,
236 mut root_ui: BrowserUi,
237 ) {
238 root_ui.set_window(geometry.size, geometry.scale_factor, geometry.title.clone());
239 self.windows.insert(window_id, root_ui);
240 }
241
242 pub fn close_window(&mut self, window_id: WindowId) {
244 self.windows.remove(&window_id);
245 }
246
247 pub fn set_default_ui(&mut self, ui: BrowserUi) {
250 self.default_ui = Some(ui);
251 }
252
253 pub fn handle_window_event(
255 &mut self,
256 window_id: WindowId,
257 event: WindowEvent,
258 sink: &mut dyn DrawSink,
259 ) -> BrowserCommand {
260 let browser_cmd = match self.windows.get_mut(&window_id) {
261 Some(ui) => ui.handle_window_event(event, sink),
262 None => BrowserCommand::None,
263 };
264 let cmd_from_tick = self.tick(window_id);
265 match browser_cmd {
266 BrowserCommand::None => {
267 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
268 self.redraw(window_id, sink);
269 }
270 cmd_from_tick
271 }
272 BrowserCommand::RenameWindowTitle => {
273 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
274 self.redraw(window_id, sink);
277 BrowserCommand::RequestRedraw
278 } else {
279 browser_cmd
280 }
281 }
282 _ => {
283 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
284 self.redraw(window_id, sink);
285 }
286 browser_cmd
287 }
288 }
289 }
290
291 pub fn redraw(&mut self, window_id: WindowId, sink: &mut dyn DrawSink) {
293 let Some(ui) = self.windows.get_mut(&window_id) else {
294 return;
295 };
296 ui.redraw(sink);
297 }
298
299 pub fn apply_draw_commands(&self, window_id: WindowId, sink: &mut dyn DrawSink) {
301 if let Some(ui) = self.windows.get(&window_id) {
302 ui.apply_draw_commands(sink);
303 }
304 }
305
306 pub(crate) fn poll_window(&mut self, window_id: WindowId) -> bool {
311 matches!(self.tick(window_id), BrowserCommand::RequestRedraw)
312 }
313
314 pub fn window_size(&self, window_id: WindowId) -> (f32, f32) {
316 match self.windows.get(&window_id) {
317 Some(ui) => {
318 let (width, height) = ui.window_size();
319 (width as f32, height as f32)
320 }
321 None => (
322 self.default_window_size.0 as f32,
323 self.default_window_size.1 as f32,
324 ),
325 }
326 }
327
328 pub fn window_title(&self, window_id: WindowId) -> String {
330 match self.windows.get(&window_id) {
331 Some(ui) => ui.window_title(),
332 None => self.default_window_title.clone(),
333 }
334 }
335
336 fn tick(&mut self, window_id: WindowId) -> BrowserCommand {
338 self.handle_network_messages();
339
340 let Some(ui) = self.windows.get_mut(&window_id) else {
341 return BrowserCommand::None;
342 };
343 let outcome = ui.tick();
344
345 for fetch in outcome.fetches {
346 let url = fetch.request.url;
347 let kind = fetch.request.kind;
348 let initiator = fetch.request.origin;
349 let permissions = fetch.request.permissions;
350 log::info!("Fetch requested in App: url={}", url);
351 let mut request = match &kind {
352 FetchKind::JavaScript {
353 method,
354 headers,
355 body,
356 ..
357 } => NetworkRequest {
358 url: url.to_string(),
359 method: method.clone(),
360 headers: headers.clone(),
361 body: body.clone(),
362 },
363 _ => NetworkRequest::get(url.to_string()),
364 };
365 apply_fetch_headers(&mut request, &initiator, &kind);
369 let id = self.pending_fetches.insert(
370 window_id,
371 fetch.tab_id,
372 kind,
373 url,
374 fetch.request.generation,
375 initiator.clone(),
376 permissions,
377 );
378 self.network
382 .fetch_request_async(request, id, &initiator, permissions);
383 }
384
385 if outcome.needs_redraw {
386 BrowserCommand::RequestRedraw
387 } else {
388 BrowserCommand::None
389 }
390 }
391
392 fn handle_network_messages(&mut self) {
393 let messages = self.network.try_receive();
394
395 for msg in messages {
396 log::info!("Network message received in App for fetch_id={}", msg.id);
397
398 let Some(pending) = self.pending_fetches.remove(msg.id) else {
400 log::warn!("No pending fetch found for fetch_id={}", msg.id);
401 continue;
402 };
403 let PendingFetch {
404 window_id,
405 tab_id,
406 kind,
407 url,
408 generation,
409 origin,
410 permissions,
411 } = pending;
412
413 let Some(ui) = self.windows.get_mut(&window_id) else {
414 log::warn!("There is no window called id={:?}", window_id);
415 continue;
416 };
417 if tab_id.0 != 0 {
418 let Some(tab) = ui.tab(&tab_id) else {
419 log::warn!("Tab was closed before fetch completed: tab_id={}", tab_id);
420 continue;
421 };
422 let current_generation = tab.document_generation();
423 if fetch_is_stale(current_generation, generation) {
424 log::info!(
425 "Dropping stale fetch result for tab_id={} generation={} (current={})",
426 tab_id,
427 generation,
428 current_generation
429 );
430 continue;
431 }
432 }
433 ui.deliver_fetch(
434 &tab_id,
435 FetchParams {
436 kind,
437 url,
438 origin,
439 permissions,
440 response: msg.response,
441 generation,
442 },
443 );
444 }
445 }
446}
447
448impl BrowserHost for BrowserApp {
453 fn default_window_size(&self) -> (u32, u32) {
454 self.default_window_size
455 }
456
457 fn default_window_title(&self) -> String {
458 self.default_window_title.clone()
459 }
460
461 fn open_window(&mut self, window_id: WindowId, geometry: WindowGeometry) {
462 let root_ui = self
465 .default_ui
466 .take()
467 .unwrap_or_else(|| BrowserUi::with_tab(super::Tab::default()));
468 self.open_window_with_ui(window_id, geometry, root_ui);
469 }
470
471 fn has_pending_default_window(&self) -> bool {
472 self.default_ui.is_some()
473 }
474
475 fn close_window(&mut self, window_id: WindowId) {
476 self.windows.remove(&window_id);
477 }
478
479 fn window_title(&self, window_id: WindowId) -> String {
480 match self.windows.get(&window_id) {
481 Some(ui) => ui.window_title(),
482 None => self.default_window_title.clone(),
483 }
484 }
485
486 fn handle_window_event(
487 &mut self,
488 window_id: WindowId,
489 event: WindowEvent,
490 sink: &mut dyn DrawSink,
491 ) -> ShellCommand {
492 BrowserApp::handle_window_event(self, window_id, event, sink)
493 }
494
495 fn apply_draw_commands(&mut self, window_id: WindowId, sink: &mut dyn DrawSink) {
496 BrowserApp::apply_draw_commands(self, window_id, sink);
497 }
498
499 fn poll_window(&mut self, window_id: WindowId) -> bool {
500 self.poll_window(window_id)
501 }
502}
503
504fn apply_fetch_headers(request: &mut NetworkRequest, initiator: &Origin, kind: &FetchKind) {
513 request.headers.retain(|(name, _)| {
514 let name = name.to_ascii_lowercase();
515 name != "origin" && name != "referer"
516 });
517
518 if !request.url.starts_with("http://") && !request.url.starts_with("https://") {
519 return;
520 }
521
522 let is_cors_request = matches!(kind, FetchKind::JavaScript { .. }) || request.method != "GET";
523 if is_cors_request {
524 request
525 .headers
526 .push(("Origin".to_string(), initiator.ascii_serialization()));
527 }
528 if initiator.is_network() {
529 request
530 .headers
531 .push(("Referer".to_string(), initiator.ascii_serialization()));
532 }
533}
534
535fn run_with_winit_backend(app: BrowserApp) -> Result<()> {
536 configure_winit_backend_for_wslg();
537 if env::var_os("ORINIUM_FORCE_X11").is_some() {
538 configure_winit_backend_forced_x11();
539 }
540
541 run_event_loop(app)
542}
543
544fn run_event_loop(app: BrowserApp) -> Result<()> {
545 let event_loop = winit::event_loop::EventLoop::new()?;
546 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
547 let mut app = App::new(Box::new(app));
548 event_loop.run_app(&mut app)?;
549 Ok(())
550}
551
552fn configure_winit_backend_forced_x11() {
553 let current = env::var("WINIT_UNIX_BACKEND").ok();
554 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
555
556 if should_force_x11 {
557 unsafe {
558 env::set_var("WINIT_UNIX_BACKEND", "x11");
559 env::remove_var("WAYLAND_DISPLAY");
560 }
561 log::info!("Forcing X11 (WINIT_UNIX_BACKEND=x11, WAYLAND_DISPLAY cleared)");
562 }
563}
564
565fn configure_winit_backend_for_wslg() {
566 let is_wsl = env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some();
567 if !is_wsl {
568 return;
569 }
570
571 if env::var_os("ORINIUM_PREFER_WAYLAND").is_some() {
573 return;
574 }
575
576 let current = env::var("WINIT_UNIX_BACKEND").ok();
577 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
578
579 if should_force_x11 {
580 unsafe {
581 env::set_var("WINIT_UNIX_BACKEND", "x11");
582 env::remove_var("WAYLAND_DISPLAY");
583 }
584 log::info!("WSLg detected: defaulting to X11 backend for stability");
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591 use crate::test_support::assert_send;
592
593 #[test]
610 fn resource_path_is_send() {
611 assert_send::<BrowserResourceLoader>();
612 assert_send::<super::PendingFetches>();
613 }
614
615 fn js_fetch(method: &str) -> FetchKind {
616 FetchKind::JavaScript {
617 request_id: 1,
618 method: method.to_string(),
619 headers: Vec::new(),
620 body: Vec::new(),
621 }
622 }
623
624 fn header<'a>(request: &'a NetworkRequest, name: &str) -> Option<&'a str> {
625 request
626 .headers
627 .iter()
628 .find(|(n, _)| n.eq_ignore_ascii_case(name))
629 .map(|(_, v)| v.as_str())
630 }
631
632 #[test]
633 fn web_fetch_gets_browser_origin_and_origin_only_referer() {
634 let initiator = Origin::from_url_string("https://example.test/index.html");
635 let mut request = NetworkRequest::get("https://api.example.test/data".to_string());
636 request
637 .headers
638 .push(("Origin".to_string(), "https://evil.test".to_string()));
639 request
640 .headers
641 .push(("Referer".to_string(), "https://evil.test/leak".to_string()));
642
643 apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
644
645 assert_eq!(header(&request, "Origin"), Some("https://example.test"));
646 assert_eq!(header(&request, "Referer"), Some("https://example.test"));
647 }
648
649 #[test]
650 fn non_get_requests_carry_origin_without_requiring_fetch_kind() {
651 let initiator = Origin::from_url_string("https://example.test/");
652 let mut request = NetworkRequest {
653 url: "https://api.example.test/items".to_string(),
654 method: "POST".to_string(),
655 headers: Vec::new(),
656 body: b"{}".to_vec(),
657 };
658
659 apply_fetch_headers(
660 &mut request,
661 &initiator,
662 &FetchKind::Image {
663 source: "irrelevant".to_string(),
664 },
665 );
666
667 assert_eq!(header(&request, "Origin"), Some("https://example.test"));
668 assert_eq!(header(&request, "Referer"), Some("https://example.test"));
669 }
670
671 #[test]
672 fn plain_subresources_skip_origin_header() {
673 let initiator = Origin::from_url_string("https://example.test/");
674 let mut request = NetworkRequest::get("https://cdn.example.test/pic.png".to_string());
675
676 apply_fetch_headers(
677 &mut request,
678 &initiator,
679 &FetchKind::Image {
680 source: "pic".to_string(),
681 },
682 );
683
684 assert_eq!(header(&request, "Origin"), None);
685 assert_eq!(header(&request, "Referer"), Some("https://example.test"));
686 }
687
688 #[test]
689 fn opaque_pages_never_leak_referer_or_internal_scheme() {
690 let initiator = Origin::opaque();
691 let mut request = NetworkRequest::get("resource:///devtools/index.html".to_string());
692 request
693 .headers
694 .push(("Referer".to_string(), "resource:///secret".to_string()));
695
696 apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
697
698 assert!(
699 request.headers.is_empty(),
700 "headers = {:?}",
701 request.headers
702 );
703 }
704
705 #[test]
706 fn opaque_fetch_sends_null_origin_but_no_referer() {
707 let initiator = Origin::opaque();
708 let mut request = NetworkRequest::get("https://api.example.test/data".to_string());
709
710 apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
711
712 assert_eq!(header(&request, "Origin"), Some("null"));
713 assert_eq!(header(&request, "Referer"), None);
714 }
715
716 #[test]
717 fn only_a_matching_generation_is_fresh() {
718 assert!(fetch_is_stale(1, 0));
719 assert!(fetch_is_stale(2, 1));
720 assert!(fetch_is_stale(0, 1));
721 assert!(!fetch_is_stale(0, 0));
722 assert!(!fetch_is_stale(7, 7));
723 }
724
725 fn pending_request(generation: u64, permissions: Permissions) -> (usize, PendingFetches) {
726 let mut pending = PendingFetches::new();
727 let id = pending.insert(
728 WindowId::from(0u64),
729 TabId(1),
730 FetchKind::Image {
731 source: "pic".to_string(),
732 },
733 Url::parse("https://example.test/pic.png").unwrap(),
734 generation,
735 Origin::from_url_string("https://example.test/"),
736 permissions,
737 );
738 (id, pending)
739 }
740
741 #[test]
742 fn pending_fetch_round_trips_origin_permissions_and_generation() {
743 let origin = Origin::from_url_string("https://example.test/");
744 let permissions = Permissions::content_for_str("https://example.test/");
745 let (id, mut pending) = pending_request(3, permissions);
746
747 let fetch = pending.remove(id).expect("just inserted");
748 assert_eq!(fetch.tab_id, TabId(1));
749 assert_eq!(fetch.generation, 3);
750 assert_eq!(fetch.origin, origin);
751 assert_eq!(fetch.permissions, permissions);
752
753 assert!(
754 pending.remove(id).is_none(),
755 "a fetch id must be consumed exactly once"
756 );
757 }
758
759 #[test]
760 fn pending_fetch_snapshots_privilege_at_request_time() {
761 let (id, mut pending) = pending_request(0, Permissions::chrome());
764 let fetch = pending.remove(id).expect("just inserted");
765 assert!(fetch.permissions.is_chrome());
766 }
767}