1use anyhow::Result;
33use std::collections::{HashMap, hash_map::DefaultHasher};
34use std::hash::{Hash, Hasher};
35use std::rc::Rc;
36use std::time::{SystemTime, UNIX_EPOCH};
37use std::{env, io};
38use url::Url;
39use winit::event::WindowEvent;
40use winit::window::WindowId;
41
42use super::tab::FetchKind;
43use super::ui::BrowserUi;
44use super::{BrowserCommand, resource_loader::BrowserResourceLoader};
45use crate::browser::core::ui::TabId;
46use crate::engine::origin::Origin;
47use crate::platform::network::{NetworkCore, NetworkRequest};
48use crate::platform::renderer::gpu::GpuRenderer;
49use crate::platform::system::App;
50
51pub struct PendingFetches {
52 map: HashMap<usize, (WindowId, TabId, FetchKind, Url)>,
55 counter: usize,
56}
57
58impl PendingFetches {
59 pub fn new() -> Self {
60 Self {
61 map: HashMap::new(),
62 counter: 0,
63 }
64 }
65
66 pub fn insert(
68 &mut self,
69 window_id: WindowId,
70 tab_id: TabId,
71 kind: FetchKind,
72 url: Url,
73 ) -> usize {
74 self.counter += 1;
75
76 let id = self.generate_id(&url);
77
78 self.map.insert(id, (window_id, tab_id, kind, url));
79 id
80 }
81
82 fn generate_id(&self, url: &Url) -> usize {
83 let mut hasher = DefaultHasher::new();
85 url.hash(&mut hasher);
86 let url_hash = hasher.finish() as usize;
87
88 let now = SystemTime::now()
90 .duration_since(UNIX_EPOCH)
91 .expect("Time went backwards")
92 .as_nanos() as usize;
93
94 now ^ self.counter ^ url_hash
96 }
97
98 pub fn remove(&mut self, id: usize) -> Option<(WindowId, TabId, FetchKind, Url)> {
99 self.map.remove(&id)
100 }
101}
102
103pub struct BrowserApp {
131 windows: HashMap<WindowId, BrowserUi>,
133 default_window_size: (u32, u32),
135 default_window_title: String,
137 network: BrowserResourceLoader,
138 pending_fetches: PendingFetches,
139 default_ui: Option<BrowserUi>,
141}
142
143impl Default for BrowserApp {
144 fn default() -> Self {
145 Self::new((1280, 800), "Orinium Browser".to_string()).unwrap()
146 }
147}
148
149impl BrowserApp {
150 pub fn run(self) -> Result<()> {
153 if self.default_ui.is_none() {
154 anyhow::bail!("set_default_ui must be called before run()");
155 }
156 run_with_winit_backend(self)
157 }
158
159 pub fn new(
162 default_window_size: (u32, u32),
163 default_window_title: String,
164 ) -> Result<Self, io::Error> {
165 let network = BrowserResourceLoader::new(Some(Rc::new(NetworkCore::new()?)));
166
167 Ok(Self {
168 windows: HashMap::new(),
169 default_window_size,
170 default_window_title,
171 network,
172 pending_fetches: PendingFetches::new(),
173 default_ui: None,
174 })
175 }
176
177 pub fn open_window(
179 &mut self,
180 window_id: WindowId,
181 window_size: (u32, u32),
182 window_title: String,
183 scale_factor: f64,
184 mut root_ui: BrowserUi,
185 ) {
186 root_ui.set_window(window_size, scale_factor, window_title);
187 self.windows.insert(window_id, root_ui);
188 }
189
190 pub fn close_window(&mut self, window_id: WindowId) {
192 self.windows.remove(&window_id);
193 }
194
195 pub fn default_window_size(&self) -> (f32, f32) {
197 (
198 self.default_window_size.0 as f32,
199 self.default_window_size.1 as f32,
200 )
201 }
202
203 pub fn default_window_title(&self) -> String {
205 self.default_window_title.clone()
206 }
207
208 pub fn set_default_ui(&mut self, ui: BrowserUi) {
211 self.default_ui = Some(ui);
212 }
213
214 pub fn take_default_ui(&mut self) -> Option<BrowserUi> {
216 self.default_ui.take()
217 }
218
219 pub fn handle_window_event(
221 &mut self,
222 window_id: WindowId,
223 event: WindowEvent,
224 gpu: &mut GpuRenderer,
225 ) -> BrowserCommand {
226 let browser_cmd = match self.windows.get_mut(&window_id) {
227 Some(ui) => ui.handle_window_event(event, gpu),
228 None => BrowserCommand::None,
229 };
230 let cmd_from_tick = self.tick(window_id);
231 match browser_cmd {
232 BrowserCommand::None => {
233 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
234 self.redraw(window_id, gpu);
235 }
236 cmd_from_tick
237 }
238 BrowserCommand::RenameWindowTitle => {
239 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
240 self.redraw(window_id, gpu);
243 BrowserCommand::RequestRedraw
244 } else {
245 browser_cmd
246 }
247 }
248 _ => {
249 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
250 self.redraw(window_id, gpu);
251 }
252 browser_cmd
253 }
254 }
255 }
256
257 pub fn redraw(&mut self, window_id: WindowId, gpu: &mut GpuRenderer) {
259 let Some(ui) = self.windows.get_mut(&window_id) else {
260 return;
261 };
262 ui.redraw(gpu);
263 }
264
265 pub fn apply_draw_commands(&self, window_id: WindowId, gpu: &mut GpuRenderer) {
267 if let Some(ui) = self.windows.get(&window_id) {
268 ui.apply_draw_commands(gpu);
269 }
270 }
271
272 pub(crate) fn poll_window(&mut self, window_id: WindowId) -> bool {
277 matches!(self.tick(window_id), BrowserCommand::RequestRedraw)
278 }
279
280 pub fn window_size(&self, window_id: WindowId) -> (f32, f32) {
282 match self.windows.get(&window_id) {
283 Some(ui) => {
284 let (width, height) = ui.window_size();
285 (width as f32, height as f32)
286 }
287 None => (
288 self.default_window_size.0 as f32,
289 self.default_window_size.1 as f32,
290 ),
291 }
292 }
293
294 pub fn window_title(&self, window_id: WindowId) -> String {
296 match self.windows.get(&window_id) {
297 Some(ui) => ui.window_title(),
298 None => self.default_window_title.clone(),
299 }
300 }
301
302 fn tick(&mut self, window_id: WindowId) -> BrowserCommand {
304 self.handle_network_messages();
305
306 let Some(ui) = self.windows.get_mut(&window_id) else {
307 return BrowserCommand::None;
308 };
309 let outcome = ui.tick();
310
311 for fetch in outcome.fetches {
312 let url = fetch.request.url;
313 let kind = fetch.request.kind;
314 let initiator = fetch.request.origin;
315 log::info!("Fetch requested in App: url={}", url);
316 let mut request = match &kind {
317 FetchKind::JavaScript {
318 method,
319 headers,
320 body,
321 ..
322 } => NetworkRequest {
323 url: url.to_string(),
324 method: method.clone(),
325 headers: headers.clone(),
326 body: body.clone(),
327 },
328 _ => NetworkRequest::get(url.to_string()),
329 };
330 apply_fetch_headers(&mut request, &initiator, &kind);
334 let id = self
335 .pending_fetches
336 .insert(window_id, fetch.tab_id, kind, url);
337 self.network.fetch_request_async(request, id, &initiator);
339 }
340
341 if outcome.needs_redraw {
342 BrowserCommand::RequestRedraw
343 } else {
344 BrowserCommand::None
345 }
346 }
347
348 fn handle_network_messages(&mut self) {
349 let messages = self.network.try_receive();
350
351 for msg in messages {
352 log::info!("Network message received in App for fetch_id={}", msg.id);
353
354 let Some((window_id, tab_id, kind, url)) = self.pending_fetches.remove(msg.id) else {
356 log::warn!("No pending fetch found for fetch_id={}", msg.id);
357 continue;
358 };
359
360 let Some(ui) = self.windows.get_mut(&window_id) else {
362 log::warn!("There is no window called id={:?}", window_id);
363 continue;
364 };
365 ui.deliver_fetch(&tab_id, kind, url, msg.response);
366 }
367 }
368}
369
370fn apply_fetch_headers(request: &mut NetworkRequest, initiator: &Origin, kind: &FetchKind) {
379 request.headers.retain(|(name, _)| {
380 let name = name.to_ascii_lowercase();
381 name != "origin" && name != "referer"
382 });
383
384 if !request.url.starts_with("http://") && !request.url.starts_with("https://") {
385 return;
386 }
387
388 let is_cors_request = matches!(kind, FetchKind::JavaScript { .. }) || request.method != "GET";
389 if is_cors_request {
390 request
391 .headers
392 .push(("Origin".to_string(), initiator.ascii_serialization()));
393 }
394 if initiator.is_network() {
395 request
396 .headers
397 .push(("Referer".to_string(), initiator.ascii_serialization()));
398 }
399}
400
401fn run_with_winit_backend(app: BrowserApp) -> Result<()> {
402 configure_winit_backend_for_wslg();
403 if env::var_os("ORINIUM_FORCE_X11").is_some() {
404 configure_winit_backend_forced_x11();
405 }
406
407 run_event_loop(app)
408}
409
410fn run_event_loop(app: BrowserApp) -> Result<()> {
411 let event_loop = winit::event_loop::EventLoop::new()?;
412 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
413 let mut app = App::new(app);
414 event_loop.run_app(&mut app)?;
415 Ok(())
416}
417
418fn configure_winit_backend_forced_x11() {
419 let current = env::var("WINIT_UNIX_BACKEND").ok();
420 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
421
422 if should_force_x11 {
423 unsafe {
424 env::set_var("WINIT_UNIX_BACKEND", "x11");
425 env::remove_var("WAYLAND_DISPLAY");
426 }
427 log::info!("Forcing X11 (WINIT_UNIX_BACKEND=x11, WAYLAND_DISPLAY cleared)");
428 }
429}
430
431fn configure_winit_backend_for_wslg() {
432 let is_wsl = env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some();
433 if !is_wsl {
434 return;
435 }
436
437 if env::var_os("ORINIUM_PREFER_WAYLAND").is_some() {
439 return;
440 }
441
442 let current = env::var("WINIT_UNIX_BACKEND").ok();
443 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
444
445 if should_force_x11 {
446 unsafe {
447 env::set_var("WINIT_UNIX_BACKEND", "x11");
448 env::remove_var("WAYLAND_DISPLAY");
449 }
450 log::info!("WSLg detected: defaulting to X11 backend for stability");
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 fn js_fetch(method: &str) -> FetchKind {
459 FetchKind::JavaScript {
460 request_id: 1,
461 method: method.to_string(),
462 headers: Vec::new(),
463 body: Vec::new(),
464 }
465 }
466
467 fn header<'a>(request: &'a NetworkRequest, name: &str) -> Option<&'a str> {
468 request
469 .headers
470 .iter()
471 .find(|(n, _)| n.eq_ignore_ascii_case(name))
472 .map(|(_, v)| v.as_str())
473 }
474
475 #[test]
476 fn web_fetch_gets_browser_origin_and_origin_only_referer() {
477 let initiator = Origin::from_url_string("https://example.test/index.html");
478 let mut request = NetworkRequest::get("https://api.example.test/data".to_string());
479 request
480 .headers
481 .push(("Origin".to_string(), "https://evil.test".to_string()));
482 request
483 .headers
484 .push(("Referer".to_string(), "https://evil.test/leak".to_string()));
485
486 apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
487
488 assert_eq!(header(&request, "Origin"), Some("https://example.test"));
489 assert_eq!(header(&request, "Referer"), Some("https://example.test"));
490 }
491
492 #[test]
493 fn non_get_requests_carry_origin_without_requiring_fetch_kind() {
494 let initiator = Origin::from_url_string("https://example.test/");
495 let mut request = NetworkRequest {
496 url: "https://api.example.test/items".to_string(),
497 method: "POST".to_string(),
498 headers: Vec::new(),
499 body: b"{}".to_vec(),
500 };
501
502 apply_fetch_headers(
503 &mut request,
504 &initiator,
505 &FetchKind::Image {
506 source: "irrelevant".to_string(),
507 },
508 );
509
510 assert_eq!(header(&request, "Origin"), Some("https://example.test"));
511 assert_eq!(header(&request, "Referer"), Some("https://example.test"));
512 }
513
514 #[test]
515 fn plain_subresources_skip_origin_header() {
516 let initiator = Origin::from_url_string("https://example.test/");
517 let mut request = NetworkRequest::get("https://cdn.example.test/pic.png".to_string());
518
519 apply_fetch_headers(
520 &mut request,
521 &initiator,
522 &FetchKind::Image {
523 source: "pic".to_string(),
524 },
525 );
526
527 assert_eq!(header(&request, "Origin"), None);
528 assert_eq!(header(&request, "Referer"), Some("https://example.test"));
529 }
530
531 #[test]
532 fn opaque_pages_never_leak_referer_or_internal_scheme() {
533 let initiator = Origin::opaque();
534 let mut request = NetworkRequest::get("resource:///devtools/index.html".to_string());
535 request
536 .headers
537 .push(("Referer".to_string(), "resource:///secret".to_string()));
538
539 apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
540
541 assert!(
542 request.headers.is_empty(),
543 "headers = {:?}",
544 request.headers
545 );
546 }
547
548 #[test]
549 fn opaque_fetch_sends_null_origin_but_no_referer() {
550 let initiator = Origin::opaque();
551 let mut request = NetworkRequest::get("https://api.example.test/data".to_string());
552
553 apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
554
555 assert_eq!(header(&request, "Origin"), Some("null"));
556 assert_eq!(header(&request, "Referer"), None);
557 }
558}