web browser (Servo)
git clone https://git.lucas.co/cce-browser.git
feat: cce://cookies, a confirm-then-clear page for site data
Cookies became persistent earlier today, which means the browser could
only ever accumulate state — no way to sign out of everything, and no way
to recover a site whose cookie state has wedged.
Deliberately a page with a link rather than a chord that acts: persistent
logins make an accidental keystroke expensive. Ctrl+Shift+Delete (and
about:cookies) opens the page; the clear happens only on following the
link, and a confirmation page replaces it.
The plumbing is the interesting part. The cce: protocol handler runs on
Servo's fetch threads and cannot reach the Servo instance, so
cce://cookies/clear raises a shared flag and returns the confirmation
page; the app's next pump sees the flag and calls
SiteDataManager::clear_cookies. Same shape as the other internal pages,
which own Arc'd stores rather than engine handles.
Shadow-verified against a local server that echoes the Cookie header it
receives: cookie present and sent -> chord opens the page -> follow the
link -> reload the site -> COOKIE ABSENT.
Co-Authored-By: Claude Opus 5 <[email protected]>
src/main.rs | 10 ++++++++++
src/pages.rs | 30 ++++++++++++++++++++++++++++++
src/webview.rs | 9 +++++++++
3 files changed, 49 insertions(+)
diff --git a/src/main.rs b/src/main.rs
index 5c57f0f..400a10a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -202,6 +202,9 @@ fn parse_url_input(input: &str, search_prefix: &str) -> Option<Url> {
if s.eq_ignore_ascii_case("about:downloads") {
return Url::parse("cce://downloads").ok();
}
+ if s.eq_ignore_ascii_case("about:cookies") {
+ return Url::parse("cce://cookies").ok();
+ }
if let Ok(u) = Url::parse(s) {
if matches!(u.scheme(), "http" | "https" | "file" | "data" | "about" | "cce") {
return Some(u);
@@ -810,6 +813,13 @@ impl Application for BrowserApp {
*needs_rebuild = true;
return self.close_tab(self.host.active_index());
}
+ // Ctrl+Shift+Delete opens the cookie page rather than
+ // clearing outright; the page asks first.
+ Key::Named(NamedKey::Delete) if event.shift => {
+ self.open_internal_page("cce://cookies");
+ *needs_rebuild = true;
+ return None;
+ }
Key::Character(c) if c == "h" || c == "b" || c == "j" => {
let page = match c.as_str() {
"h" => "cce://history",
diff --git a/src/pages.rs b/src/pages.rs
index 4c78ab5..8c60d83 100644
--- a/src/pages.rs
+++ b/src/pages.rs
@@ -257,6 +257,31 @@ pub struct CceProtocol {
pub history: Arc<History>,
pub bookmarks: Arc<Bookmarks>,
pub downloads: Arc<crate::downloads::Downloads>,
+ /// Raised by cce://cookies/clear. The handler runs on fetch threads and
+ /// cannot reach Servo, so it flags the request and the app's next pump
+ /// performs the clear through the SiteDataManager.
+ pub clear_cookies: Arc<std::sync::atomic::AtomicBool>,
+}
+
+/// Confirmation page for clearing cookies. Deliberately a page with a link
+/// rather than a chord that acts immediately: logins persist now, so an
+/// accidental keystroke would sign the user out of everything.
+fn cookies_page() -> String {
+ page(
+ "Cookies",
+ "Signed-in sessions live here",
+ "<div class=e><span class=w></span><span class=u>Clearing cookies signs you out of every site and cannot be undone. Bookmarks and history are untouched.</span></div> <div class=e><span class=w></span> <a class=rm href=\"cce://cookies/clear\">Clear all cookies</a></div>",
+ "",
+ )
+}
+
+fn cookies_cleared_page() -> String {
+ page(
+ "Cookies",
+ "Cleared",
+ "<div class=e><span class=w></span><span class=u>All cookies were cleared. Sites you were signed in to will ask you to sign in again.</span></div>",
+ "",
+ )
}
impl ProtocolHandler for CceProtocol {
@@ -289,6 +314,11 @@ impl ProtocolHandler for CceProtocol {
self.downloads.clear_finished();
Some(self.downloads.html())
}
+ "cookies" => Some(cookies_page()),
+ "cookies/clear" => {
+ self.clear_cookies.store(true, std::sync::atomic::Ordering::SeqCst);
+ Some(cookies_cleared_page())
+ }
_ => None,
};
let response = match body {
diff --git a/src/webview.rs b/src/webview.rs
index 2a3c909..3d342d5 100644
--- a/src/webview.rs
+++ b/src/webview.rs
@@ -242,6 +242,8 @@ pub struct ServoHost {
force_dark: bool,
/// Deadlines for pending reloads, earliest last (popped off the back).
reload_at: Vec<std::time::Instant>,
+ /// Raised by the cce://cookies/clear page; acted on here in `pump`.
+ clear_cookies: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl ServoHost {
@@ -312,11 +314,13 @@ impl ServoHost {
let history = std::sync::Arc::new(History::load());
let bookmarks = std::sync::Arc::new(Bookmarks::load());
let downloads = std::sync::Arc::new(Downloads::default());
+ let clear_cookies = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut protocols = ProtocolRegistry::default();
let handler = CceProtocol {
history: history.clone(),
bookmarks: bookmarks.clone(),
downloads: downloads.clone(),
+ clear_cookies: clear_cookies.clone(),
};
if let Err(e) = protocols.register("cce", handler) {
log::error!("failed to register cce: protocol: {e:?}");
@@ -394,6 +398,7 @@ impl ServoHost {
force_dark_sheet,
force_dark,
reload_at: Vec::new(),
+ clear_cookies,
};
host.open_tab(url);
host
@@ -510,6 +515,10 @@ impl ServoHost {
/// tab if it produced a frame. Returns (new frame, any state change).
pub fn pump(&mut self) -> (bool, bool) {
self.servo.spin_event_loop();
+ if self.clear_cookies.swap(false, std::sync::atomic::Ordering::SeqCst) {
+ self.servo.site_data_manager().clear_cookies(None);
+ log::info!("cleared all cookies");
+ }
if self.reload_at.last().is_some_and(|at| std::time::Instant::now() >= *at) {
self.reload_at.pop();
for tab in &self.tabs {