diff --git a/README.md b/README.md index 8f9c024..f716ada 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,9 @@ cargo run -- --title "RustyWolf" cargo run -- --private cargo run -- --dry-run +# compatibility +cargo run -- --ua-chrome # Chrome UA for sites that block WebKit + # Linux backend controls cargo run -- --linux-backend x11 cargo run -- --linux-backend wayland @@ -51,6 +54,12 @@ cargo run -- --linux-disable-dmabuf cargo run -- --linux-backend x11 --linux-disable-dmabuf ``` +Sessions are persistent: cookies and site data live in `~/.local/share/RustyWolf`, +so logins and consent choices survive restarts. `--private` gives an ephemeral +session instead. The default user agent is WebKit's native one (best match for +the engine); `--ua-chrome` is a last-resort fallback for sites that refuse to +run on WebKit. + ## Privacy Notes RustyWolf applies baseline hardening in page context: diff --git a/src/cli.rs b/src/cli.rs index 87c4ff7..b23d3d7 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -44,6 +44,12 @@ pub struct Cli { help = "Linux only: disable WebKit dmabuf renderer (helps some NVIDIA/Wayland setups)" )] pub linux_disable_dmabuf: bool, + + #[arg( + long, + help = "Send a Chrome user agent instead of the native WebKit one (for sites that block WebKit)" + )] + pub ua_chrome: bool, } impl Cli { diff --git a/src/webview.rs b/src/webview.rs index c8a20bf..5ce19a0 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -19,12 +19,11 @@ use tao::platform::unix::WindowExtUnix; use tao::window::{Icon, Window, WindowBuilder}; #[cfg(target_os = "linux")] use wry::WebViewBuilderExtUnix; -use wry::{PageLoadEvent, Rect, WebView, WebViewBuilder}; +use wry::{PageLoadEvent, Rect, WebContext, WebView, WebViewBuilder}; const DEFAULT_HOME: &str = "https://duckduckgo.com"; const NEW_TAB_URL: &str = "https://duckduckgo.com"; const SEARCH_URL_PREFIX: &str = "https://duckduckgo.com/?q="; -const MODERN_BROWSER_UA_BASE: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"; const CHROME_HEIGHT_BASE: f64 = 88.0; const CHROME_HEIGHT_DOWNLOADS_EXTRA: f64 = 104.0; const CHROME_HEIGHT_POPUP_PROMPT_EXTRA: f64 = 44.0; @@ -96,6 +95,8 @@ struct BrowserState { active: usize, next_id: u32, private: bool, + user_agent: Option, + web_context: Option, } struct PendingPopup { @@ -1111,8 +1112,13 @@ pub fn launch_webkit(args: &Cli) -> Result<()> { if args.dry_run { let effective_dmabuf = std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").unwrap_or_else(|_| "0".to_string()); + let ua = if args.ua_chrome { + chrome_user_agent() + } else { + "native".to_string() + }; println!( - "engine=webkit url={start_url} private={} linux_backend={:?} linux_disable_dmabuf={} effective_webkit_disable_dmabuf={effective_dmabuf}", + "engine=webkit url={start_url} private={} ua={ua} linux_backend={:?} linux_disable_dmabuf={} effective_webkit_disable_dmabuf={effective_dmabuf}", args.private, args.linux_backend, args.linux_disable_dmabuf ); return Ok(()); @@ -1167,6 +1173,12 @@ pub fn launch_webkit(args: &Cli) -> Result<()> { active: 0, next_id: 1, private: args.private, + user_agent: if args.ua_chrome { + Some(chrome_user_agent()) + } else { + None + }, + web_context: persistent_web_context(args.private), }; open_tab(&mut state, &window, &proxy, start_url)?; @@ -1820,71 +1832,20 @@ fn is_child_webview_unsupported_error(err_text: &str) -> bool { } } +#[allow(clippy::too_many_arguments)] fn build_tab_webview( window: &Window, proxy: &EventLoopProxy, host_mode: WebViewHostMode, #[cfg(target_os = "linux")] gtk_content_container: Option<>k::Box>, private: bool, + user_agent: Option<&str>, + mut web_context: Option<&mut WebContext>, tab_id: u32, url: &str, bounds: Rect, ) -> Result<(WebView, WebViewHostMode)> { let init_script = format!("{PRIVACY_JS}\n{ADBLOCK_JS}"); - let user_agent = browser_user_agent(); - - let builder = || { - let title_proxy = proxy.clone(); - let page_proxy = proxy.clone(); - let download_started_proxy = proxy.clone(); - let download_completed_proxy = proxy.clone(); - let download_total_proxy = proxy.clone(); - let new_window_proxy = proxy.clone(); - - WebViewBuilder::new() - .with_url(url) - .with_user_agent(&user_agent) - .with_incognito(private) - .with_initialization_script(&init_script) - .with_bounds(bounds) - .with_new_window_req_handler(move |target| { - let _ = new_window_proxy.send_event(UserEvent::OpenInNewTab { url: target }); - false - }) - .with_document_title_changed_handler(move |title| { - let _ = title_proxy.send_event(UserEvent::TitleChanged { tab_id, title }); - }) - .with_on_page_load_handler(move |event, url| { - if matches!(event, PageLoadEvent::Finished) { - let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, url }); - } - }) - .with_download_started_handler(move |url, destination| { - let path = ensure_unique_download_path(default_download_path_for(&url)); - if let Some(parent) = path.parent() { - let _ = std::fs::create_dir_all(parent); - } - *destination = path.clone(); - let path_string = path_to_string(&path); - fetch_download_total( - url.clone(), - path_string.clone(), - download_total_proxy.clone(), - ); - let _ = download_started_proxy.send_event(UserEvent::DownloadStarted { - url, - file_path: path_string, - }); - true - }) - .with_download_completed_handler(move |url, path, success| { - let _ = download_completed_proxy.send_event(UserEvent::DownloadCompleted { - url, - file_path: path.map(|p| path_to_string(&p)), - success, - }); - }) - }; match host_mode { WebViewHostMode::Window => { @@ -1893,32 +1854,71 @@ fn build_tab_webview( let container = gtk_content_container.ok_or_else(|| { anyhow!("Failed to create tab webview: missing Linux GTK content container") })?; - builder() - .build_gtk(container) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|err| anyhow!("Failed to create tab webview with GTK hosting: {err}")) + tab_webview_builder( + proxy, + web_context, + private, + user_agent, + &init_script, + tab_id, + url, + bounds, + ) + .build_gtk(container) + .map(|webview| (webview, WebViewHostMode::Window)) + .map_err(|err| anyhow!("Failed to create tab webview with GTK hosting: {err}")) } #[cfg(not(target_os = "linux"))] { - builder() - .build(window) - .map(|webview| (webview, WebViewHostMode::Window)) - .map_err(|err| anyhow!("Failed to create tab webview: {err}")) + tab_webview_builder( + proxy, + web_context, + private, + user_agent, + &init_script, + tab_id, + url, + bounds, + ) + .build(window) + .map(|webview| (webview, WebViewHostMode::Window)) + .map_err(|err| anyhow!("Failed to create tab webview: {err}")) } } - WebViewHostMode::Child => match builder().build_as_child(window) { - Ok(webview) => Ok((webview, WebViewHostMode::Child)), - Err(err) => { - let err_text = err.to_string(); - if is_child_webview_unsupported_error(&err_text) { - eprintln!( - "child tab webview unsupported on this backend; falling back to window-hosted tabs" - ); - #[cfg(target_os = "linux")] - { - if let Some(container) = gtk_content_container { - return builder() + WebViewHostMode::Child => { + let attempt = tab_webview_builder( + proxy, + web_context.as_deref_mut(), + private, + user_agent, + &init_script, + tab_id, + url, + bounds, + ) + .build_as_child(window); + match attempt { + Ok(webview) => Ok((webview, WebViewHostMode::Child)), + Err(err) => { + let err_text = err.to_string(); + if is_child_webview_unsupported_error(&err_text) { + eprintln!( + "child tab webview unsupported on this backend; falling back to window-hosted tabs" + ); + #[cfg(target_os = "linux")] + { + if let Some(container) = gtk_content_container { + return tab_webview_builder( + proxy, + web_context, + private, + user_agent, + &init_script, + tab_id, + url, + bounds, + ) .build_gtk(container) .map(|webview| (webview, WebViewHostMode::Window)) .map_err(|gtk_err| { @@ -1926,15 +1926,24 @@ fn build_tab_webview( "Failed to create tab webview: child failed ({err_text}); GTK fallback failed ({gtk_err})" ) }); + } + return Err(anyhow!( + "Failed to create tab webview: child failed ({err_text}); missing Linux GTK content container" + )); } - return Err(anyhow!( - "Failed to create tab webview: child failed ({err_text}); missing Linux GTK content container" - )); - } - #[cfg(not(target_os = "linux"))] - { - return builder() + #[cfg(not(target_os = "linux"))] + { + return tab_webview_builder( + proxy, + web_context, + private, + user_agent, + &init_script, + tab_id, + url, + bounds, + ) .build(window) .map(|webview| (webview, WebViewHostMode::Window)) .map_err(|fallback_err| { @@ -1942,27 +1951,109 @@ fn build_tab_webview( "Failed to create tab webview: child failed ({err_text}); window fallback failed ({fallback_err})" ) }); + } } + Err(anyhow!("Failed to create tab webview: {err_text}")) } - Err(anyhow!("Failed to create tab webview: {err_text}")) } - }, + } } } -fn browser_user_agent() -> String { +#[allow(clippy::too_many_arguments)] +fn tab_webview_builder<'a>( + proxy: &EventLoopProxy, + web_context: Option<&'a mut WebContext>, + private: bool, + user_agent: Option<&str>, + init_script: &str, + tab_id: u32, + url: &str, + bounds: Rect, +) -> WebViewBuilder<'a> { + let title_proxy = proxy.clone(); + let page_proxy = proxy.clone(); + let download_started_proxy = proxy.clone(); + let download_completed_proxy = proxy.clone(); + let download_total_proxy = proxy.clone(); + let new_window_proxy = proxy.clone(); + + let mut builder = match web_context { + Some(web_context) => WebViewBuilder::with_web_context(web_context), + None => WebViewBuilder::new(), + } + .with_url(url) + .with_incognito(private) + .with_initialization_script(init_script) + .with_bounds(bounds); + if let Some(user_agent) = user_agent { + builder = builder.with_user_agent(user_agent); + } + builder + .with_new_window_req_handler(move |target| { + let _ = new_window_proxy.send_event(UserEvent::OpenInNewTab { url: target }); + false + }) + .with_document_title_changed_handler(move |title| { + let _ = title_proxy.send_event(UserEvent::TitleChanged { tab_id, title }); + }) + .with_on_page_load_handler(move |event, url| { + if matches!(event, PageLoadEvent::Finished) { + let _ = page_proxy.send_event(UserEvent::PageLoaded { tab_id, url }); + } + }) + .with_download_started_handler(move |url, destination| { + let path = ensure_unique_download_path(default_download_path_for(&url)); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + *destination = path.clone(); + let path_string = path_to_string(&path); + fetch_download_total( + url.clone(), + path_string.clone(), + download_total_proxy.clone(), + ); + let _ = download_started_proxy.send_event(UserEvent::DownloadStarted { + url, + file_path: path_string, + }); + true + }) + .with_download_completed_handler(move |url, path, success| { + let _ = download_completed_proxy.send_event(UserEvent::DownloadCompleted { + url, + file_path: path.map(|p| path_to_string(&p)), + success, + }); + }) +} + +fn persistent_web_context(private: bool) -> Option { + if private { + return None; + } + // WebKitGTK keeps cookies in memory unless a persistent store is set, + // which would log users out and re-show consent walls on every launch. + let data_dir = dirs::data_dir() + .unwrap_or_else(std::env::temp_dir) + .join("RustyWolf"); + let _ = fs::create_dir_all(&data_dir); + Some(WebContext::new(Some(data_dir))) +} + +fn chrome_user_agent() -> String { let platform = if cfg!(target_os = "macos") { - "Macintosh; Intel Mac OS X 14_0" + "Macintosh; Intel Mac OS X 10_15_7" } else if cfg!(target_os = "windows") { "Windows NT 10.0; Win64; x64" } else { "X11; Linux x86_64" }; - // Keep broad compatibility while explicitly identifying RustyWolf. + // A consistent, well-formed Chrome UA for sites that refuse to run on WebKit. format!( - "Mozilla/5.0 ({platform}; rv:128.0) {MODERN_BROWSER_UA_BASE} RustyWolf/{}", - env!("CARGO_PKG_VERSION") + "Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36" ) } @@ -1982,6 +2073,8 @@ fn open_tab( #[cfg(target_os = "linux")] state.gtk_content_container.as_ref(), state.private, + state.user_agent.as_deref(), + state.web_context.as_mut(), tab_id, &url, tab_bounds, @@ -2080,6 +2173,8 @@ fn ensure_tab_loaded( #[cfg(target_os = "linux")] state.gtk_content_container.as_ref(), state.private, + state.user_agent.as_deref(), + state.web_context.as_mut(), tab_id, &tab_url, tab_bounds,