compat: persist cookies so consent/login survive restarts, use native UA by default

- tab webviews now share a WebContext backed by the profile dir
  (~/.local/share/RustyWolf); WebKitGTK keeps cookies in memory unless a
  persistent store is set, which made youtube.com re-show the consent
  wall on every launch and made logins impossible to keep
- default user agent is WebKit's native one instead of the malformed
  fake-Chrome + RustyWolf string that pushed sites into Chromium-only
  code paths; --ua-chrome adds a clean Chrome UA as fallback
- --private keeps using an ephemeral in-memory session
- README: document --ua-chrome and the persistent profile
This commit is contained in:
Martin Dvořák 2026-10-11 18:51:41 +02:00
commit 56f548bd1d
3 changed files with 201 additions and 91 deletions

View file

@ -44,6 +44,9 @@ cargo run -- --title "RustyWolf"
cargo run -- --private cargo run -- --private
cargo run -- --dry-run cargo run -- --dry-run
# compatibility
cargo run -- --ua-chrome # Chrome UA for sites that block WebKit
# Linux backend controls # Linux backend controls
cargo run -- --linux-backend x11 cargo run -- --linux-backend x11
cargo run -- --linux-backend wayland cargo run -- --linux-backend wayland
@ -51,6 +54,12 @@ cargo run -- --linux-disable-dmabuf
cargo run -- --linux-backend x11 --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 ## Privacy Notes
RustyWolf applies baseline hardening in page context: RustyWolf applies baseline hardening in page context:

View file

@ -44,6 +44,12 @@ pub struct Cli {
help = "Linux only: disable WebKit dmabuf renderer (helps some NVIDIA/Wayland setups)" help = "Linux only: disable WebKit dmabuf renderer (helps some NVIDIA/Wayland setups)"
)] )]
pub linux_disable_dmabuf: bool, 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 { impl Cli {

View file

@ -19,12 +19,11 @@ use tao::platform::unix::WindowExtUnix;
use tao::window::{Icon, Window, WindowBuilder}; use tao::window::{Icon, Window, WindowBuilder};
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use wry::WebViewBuilderExtUnix; use wry::WebViewBuilderExtUnix;
use wry::{PageLoadEvent, Rect, WebView, WebViewBuilder}; use wry::{PageLoadEvent, Rect, WebContext, WebView, WebViewBuilder};
const DEFAULT_HOME: &str = "https://duckduckgo.com"; const DEFAULT_HOME: &str = "https://duckduckgo.com";
const NEW_TAB_URL: &str = "https://duckduckgo.com"; const NEW_TAB_URL: &str = "https://duckduckgo.com";
const SEARCH_URL_PREFIX: &str = "https://duckduckgo.com/?q="; 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_BASE: f64 = 88.0;
const CHROME_HEIGHT_DOWNLOADS_EXTRA: f64 = 104.0; const CHROME_HEIGHT_DOWNLOADS_EXTRA: f64 = 104.0;
const CHROME_HEIGHT_POPUP_PROMPT_EXTRA: f64 = 44.0; const CHROME_HEIGHT_POPUP_PROMPT_EXTRA: f64 = 44.0;
@ -96,6 +95,8 @@ struct BrowserState {
active: usize, active: usize,
next_id: u32, next_id: u32,
private: bool, private: bool,
user_agent: Option<String>,
web_context: Option<WebContext>,
} }
struct PendingPopup { struct PendingPopup {
@ -1111,8 +1112,13 @@ pub fn launch_webkit(args: &Cli) -> Result<()> {
if args.dry_run { if args.dry_run {
let effective_dmabuf = let effective_dmabuf =
std::env::var("WEBKIT_DISABLE_DMABUF_RENDERER").unwrap_or_else(|_| "0".to_string()); 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!( 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 args.private, args.linux_backend, args.linux_disable_dmabuf
); );
return Ok(()); return Ok(());
@ -1167,6 +1173,12 @@ pub fn launch_webkit(args: &Cli) -> Result<()> {
active: 0, active: 0,
next_id: 1, next_id: 1,
private: args.private, 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)?; 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( fn build_tab_webview(
window: &Window, window: &Window,
proxy: &EventLoopProxy<UserEvent>, proxy: &EventLoopProxy<UserEvent>,
host_mode: WebViewHostMode, host_mode: WebViewHostMode,
#[cfg(target_os = "linux")] gtk_content_container: Option<&gtk::Box>, #[cfg(target_os = "linux")] gtk_content_container: Option<&gtk::Box>,
private: bool, private: bool,
user_agent: Option<&str>,
mut web_context: Option<&mut WebContext>,
tab_id: u32, tab_id: u32,
url: &str, url: &str,
bounds: Rect, bounds: Rect,
) -> Result<(WebView, WebViewHostMode)> { ) -> Result<(WebView, WebViewHostMode)> {
let init_script = format!("{PRIVACY_JS}\n{ADBLOCK_JS}"); 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 { match host_mode {
WebViewHostMode::Window => { WebViewHostMode::Window => {
@ -1893,32 +1854,71 @@ fn build_tab_webview(
let container = gtk_content_container.ok_or_else(|| { let container = gtk_content_container.ok_or_else(|| {
anyhow!("Failed to create tab webview: missing Linux GTK content container") anyhow!("Failed to create tab webview: missing Linux GTK content container")
})?; })?;
builder() tab_webview_builder(
.build_gtk(container) proxy,
.map(|webview| (webview, WebViewHostMode::Window)) web_context,
.map_err(|err| anyhow!("Failed to create tab webview with GTK hosting: {err}")) 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"))] #[cfg(not(target_os = "linux"))]
{ {
builder() tab_webview_builder(
.build(window) proxy,
.map(|webview| (webview, WebViewHostMode::Window)) web_context,
.map_err(|err| anyhow!("Failed to create tab webview: {err}")) 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) { WebViewHostMode::Child => {
Ok(webview) => Ok((webview, WebViewHostMode::Child)), let attempt = tab_webview_builder(
Err(err) => { proxy,
let err_text = err.to_string(); web_context.as_deref_mut(),
if is_child_webview_unsupported_error(&err_text) { private,
eprintln!( user_agent,
"child tab webview unsupported on this backend; falling back to window-hosted tabs" &init_script,
); tab_id,
#[cfg(target_os = "linux")] url,
{ bounds,
if let Some(container) = gtk_content_container { )
return builder() .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) .build_gtk(container)
.map(|webview| (webview, WebViewHostMode::Window)) .map(|webview| (webview, WebViewHostMode::Window))
.map_err(|gtk_err| { .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})" "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"))] #[cfg(not(target_os = "linux"))]
{ {
return builder() return tab_webview_builder(
proxy,
web_context,
private,
user_agent,
&init_script,
tab_id,
url,
bounds,
)
.build(window) .build(window)
.map(|webview| (webview, WebViewHostMode::Window)) .map(|webview| (webview, WebViewHostMode::Window))
.map_err(|fallback_err| { .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})" "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<UserEvent>,
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<WebContext> {
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") { 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") { } else if cfg!(target_os = "windows") {
"Windows NT 10.0; Win64; x64" "Windows NT 10.0; Win64; x64"
} else { } else {
"X11; Linux x86_64" "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!( format!(
"Mozilla/5.0 ({platform}; rv:128.0) {MODERN_BROWSER_UA_BASE} RustyWolf/{}", "Mozilla/5.0 ({platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36"
env!("CARGO_PKG_VERSION")
) )
} }
@ -1982,6 +2073,8 @@ fn open_tab(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
state.gtk_content_container.as_ref(), state.gtk_content_container.as_ref(),
state.private, state.private,
state.user_agent.as_deref(),
state.web_context.as_mut(),
tab_id, tab_id,
&url, &url,
tab_bounds, tab_bounds,
@ -2080,6 +2173,8 @@ fn ensure_tab_loaded(
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
state.gtk_content_container.as_ref(), state.gtk_content_container.as_ref(),
state.private, state.private,
state.user_agent.as_deref(),
state.web_context.as_mut(),
tab_id, tab_id,
&tab_url, &tab_url,
tab_bounds, tab_bounds,