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,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 {

View file

@ -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<String>,
web_context: Option<WebContext>,
}
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<UserEvent>,
host_mode: WebViewHostMode,
#[cfg(target_os = "linux")] gtk_content_container: Option<&gtk::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<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") {
"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,