web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
feat: persist cookies and site state in an XDG profile dir
The browser built Servo with no `config_dir`, and Servo only reads and
writes cookie_jar.json / auth_cache.json / hsts_list.json when it has one
— so the cookie jar lived in memory and every launch started logged out
of every site. A sign-in could not outlive the window.
Servo now gets ~/.local/state/cce/browser/profile, alongside the history
and bookmarks TSVs. The directory is forced to 0700: Servo writes the jar
0644, and $HOME being 0700 is what protects it today, which is not a
property worth depending on for live session cookies.
Note the jar is plaintext JSON, and Servo writes it only when the resource
thread gets its Exit message — a clean shutdown. A killed or crashed
browser loses that session's new cookies.
Shadow-verified against a local server that sets a cookie and echoes back
what it receives: first run on an empty profile reports COOKIE ABSENT, and
after a graceful close and relaunch the same URL reports COOKIE FOUND with
the value intact; the profile dir lands 0700.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/pages.rs | 4 +++-
src/webview.rs | 21 +++++++++++++++++++++
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/src/pages.rs b/src/pages.rs
index 386034a..4c78ab5 100644
--- a/src/pages.rs
+++ b/src/pages.rs
@@ -33,7 +33,9 @@ struct Entry {
title: String,
}
-fn state_dir() -> PathBuf {
+/// `~/.local/state/cce/browser` — history and bookmarks live here directly,
+/// Servo's own persisted state in a `profile` subdirectory under it.
+pub(crate) fn state_dir() -> PathBuf {
let base = match std::env::var("XDG_STATE_HOME") {
Ok(x) if !x.is_empty() => PathBuf::from(x),
_ => PathBuf::from(std::env::var("HOME").unwrap_or_default()).join(".local/state"),
diff --git a/src/webview.rs b/src/webview.rs
index 1dc173b..b1e074b 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -293,7 +293,28 @@ impl ServoHost {
log::error!("failed to register cce: protocol: {e:?}");
}
+ // Give Servo somewhere to persist per-profile state. Without a
+ // `config_dir` it keeps the cookie jar in memory only, so every
+ // launch starts logged out of every site; with one it reads and
+ // writes cookie_jar.json (plus the auth cache and HSTS list) there.
+ // Note the jar is plaintext JSON — live sessions for signed-in
+ // accounts sit in it, so it is deliberately under the state dir
+ // rather than anywhere shared or synced.
+ let profile_dir = crate::pages::state_dir().join("profile");
+ if let Err(e) = std::fs::create_dir_all(&profile_dir) {
+ log::warn!("no browser profile dir ({e}); sessions will not persist");
+ } else {
+ // Servo writes the jar 0644. $HOME is 0700 here so that is not
+ // exposed today, but the sessions inside are worth an owner-only
+ // directory of their own rather than relying on that.
+ use std::os::unix::fs::PermissionsExt;
+ let _ = std::fs::set_permissions(&profile_dir, std::fs::Permissions::from_mode(0o700));
+ }
let servo = ServoBuilder::default()
+ .opts(servo::Opts {
+ config_dir: Some(profile_dir),
+ ..Default::default()
+ })
.event_loop_waker(Box::new(Waker(wake.clone())))
.protocol_registry(protocols)
.build();