mirror of
https://github.com/therealaleph/MasterHttpRelayVPN-RUST.git
synced 2026-05-17 21:24:48 +03:00
Closes #1145. LibreWolf users were getting `MOZILLA_PKIX_ERROR_MITM_DETECTED` when visiting HSTS-protected sites (bing.com, youtube.com, …) through MasterHttpRelayVPN's MITM mode. HSTS gives no "Add Exception" affordance, so users were fully locked out of those sites despite the OS-level CA install having succeeded. **Root cause**: `cert_installer.rs` only scanned Firefox profile roots (`~/.mozilla/firefox`, the snap variant, `%APPDATA%\Mozilla\Firefox\Profiles`, `~/Library/Application Support/Firefox/Profiles`). LibreWolf is a Firefox fork with strict privacy defaults; it shares Firefox's NSS DB layout and respects the same `security.enterprise_roots.enabled` pref, but stores its profile tree under its own app dir. Neither the per-profile `certutil -A` install nor the `user.js` enterprise-roots auto-trust fallback ever touched LibreWolf, so the browser never trusted our CA. Same failure mode behind already-closed #955 and #959 (Firefox-fork users reporting the identical "secure connection could not be established" symptom). **Fix**: extend Mozilla-family profile discovery to cover LibreWolf on every supported platform. No behavioural change for Firefox installs. ## Changes (`src/cert_installer.rs`-only) - Renamed `firefox_profile_dirs()` → `mozilla_family_profile_dirs()`. Same flat-vec return type so all five call sites read identically; the rename is signposting only. - Extracted `mozilla_family_profile_roots(os, home, appdata, xdg_config_home)`: returns the union of Firefox + LibreWolf profile root directories, per-OS: - **Linux**: `~/.mozilla/firefox`, snap variant, `~/.librewolf`, `$XDG_CONFIG_HOME/librewolf` (LibreWolf respects XDG by default). - **macOS**: `~/Library/Application Support/Firefox/Profiles`, `~/Library/Application Support/LibreWolf/Profiles`. - **Windows**: `%APPDATA%\Mozilla\Firefox\Profiles`, `%APPDATA%\LibreWolf\Profiles`. - All five existing call sites (per-profile install, enterprise-roots fallback, uninstall, dry-run reporter, test-mode reporter) read from the renamed function without further changes. ## Verified locally (on top of v1.9.24) - `cargo test --lib --release`: **239/239** ✅ (was 231; this PR adds 8 new tests covering LibreWolf-path discovery on each OS). - `cargo build --release --features ui --bin mhrv-rs-ui`: clean ✅ ## Will combine with #1143 PR #1143 already pre-baked the v1.9.25 release files (Cargo.toml + changelog). This PR doesn't touch either, so the squash-merge will land cleanly alongside #1143's changes. Will edit v1.9.25's changelog to include #1159 as a second bullet before tagging. Reviewed via Anthropic Claude. Co-Authored-By: dazzling-no-more <noreply@github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+285
-48
@@ -54,7 +54,7 @@ impl RemovalOutcome {
|
|||||||
/// When running as root via `sudo`, the process's `HOME` / `USER`
|
/// When running as root via `sudo`, the process's `HOME` / `USER`
|
||||||
/// environment reflects **root**, not the user who invoked the command.
|
/// environment reflects **root**, not the user who invoked the command.
|
||||||
/// That breaks every user-scoped cert path this module touches —
|
/// That breaks every user-scoped cert path this module touches —
|
||||||
/// `data_dir()` resolves to root's config dir, `firefox_profile_dirs()`
|
/// `data_dir()` resolves to root's config dir, `mozilla_family_profile_dirs()`
|
||||||
/// scans root's profiles, macOS `login.keychain-db` is root's. The
|
/// scans root's profiles, macOS `login.keychain-db` is root's. The
|
||||||
/// removal then operates on paths that probably don't exist, reports
|
/// removal then operates on paths that probably don't exist, reports
|
||||||
/// success, and leaves the real user's CA trusted.
|
/// success, and leaves the real user's CA trusted.
|
||||||
@@ -840,10 +840,10 @@ fn remove_windows() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- NSS (Firefox + Chrome/Chromium on Linux) ----------
|
// ---------- NSS (Firefox + LibreWolf + Chrome/Chromium on Linux) ----------
|
||||||
|
|
||||||
/// Best-effort install of the CA into all discovered NSS stores:
|
/// Best-effort install of the CA into all discovered NSS stores:
|
||||||
/// 1. Every Firefox profile (each has its own cert9.db).
|
/// 1. Every Firefox/LibreWolf profile (each has its own cert9.db).
|
||||||
/// 2. On Linux, the shared Chrome/Chromium NSS DB at ~/.pki/nssdb —
|
/// 2. On Linux, the shared Chrome/Chromium NSS DB at ~/.pki/nssdb —
|
||||||
/// this is the one update-ca-certificates does NOT populate, and
|
/// this is the one update-ca-certificates does NOT populate, and
|
||||||
/// missing it was the real blocker for Chrome users who'd installed
|
/// missing it was the real blocker for Chrome users who'd installed
|
||||||
@@ -851,18 +851,19 @@ fn remove_windows() {
|
|||||||
/// Silently no-ops if `certutil` (from libnss3-tools) isn't on PATH.
|
/// Silently no-ops if `certutil` (from libnss3-tools) isn't on PATH.
|
||||||
/// Browsers must be closed during install for changes to take effect.
|
/// Browsers must be closed during install for changes to take effect.
|
||||||
fn install_nss_stores(cert_path: &str) {
|
fn install_nss_stores(cert_path: &str) {
|
||||||
// First, try to make Firefox pick up the OS-level CA automatically by
|
// First, try to make Firefox/LibreWolf pick up the OS-level CA
|
||||||
// flipping the `security.enterprise_roots.enabled` pref in user.js of
|
// automatically by flipping the `security.enterprise_roots.enabled`
|
||||||
// every Firefox profile we find. This is the cleanest cross-platform
|
// pref in user.js of every Mozilla-family profile we find. This is
|
||||||
// fix because it doesn't depend on whether NSS certutil is installed
|
// the cleanest cross-platform fix because it doesn't depend on
|
||||||
// — Firefox just starts trusting whatever the OS trusts. Especially
|
// whether NSS certutil is installed — the browser just starts
|
||||||
// important on Windows where NSS certutil isn't on PATH.
|
// trusting whatever the OS trusts. Especially important on Windows
|
||||||
enable_firefox_enterprise_roots();
|
// where NSS certutil isn't on PATH.
|
||||||
|
enable_mozilla_enterprise_roots();
|
||||||
|
|
||||||
if !has_nss_certutil() {
|
if !has_nss_certutil() {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"NSS certutil not found — Firefox will still trust the CA via the \
|
"NSS certutil not found — Firefox/LibreWolf will still trust the CA via \
|
||||||
`security.enterprise_roots.enabled` user.js pref (flipped above). \
|
the `security.enterprise_roots.enabled` user.js pref (flipped above). \
|
||||||
For Chrome/Chromium on Linux, install `libnss3-tools` (Debian/Ubuntu) \
|
For Chrome/Chromium on Linux, install `libnss3-tools` (Debian/Ubuntu) \
|
||||||
or `nss-tools` (Fedora/RHEL), or import ca.crt manually via \
|
or `nss-tools` (Fedora/RHEL), or import ca.crt manually via \
|
||||||
chrome://settings/certificates → Authorities."
|
chrome://settings/certificates → Authorities."
|
||||||
@@ -873,8 +874,8 @@ fn install_nss_stores(cert_path: &str) {
|
|||||||
let mut ok = 0;
|
let mut ok = 0;
|
||||||
let mut tried = 0;
|
let mut tried = 0;
|
||||||
|
|
||||||
// 1. Firefox profiles.
|
// 1. Firefox/LibreWolf profiles.
|
||||||
for p in firefox_profile_dirs() {
|
for p in mozilla_family_profile_dirs() {
|
||||||
tried += 1;
|
tried += 1;
|
||||||
if install_nss_in_profile(&p, cert_path) {
|
if install_nss_in_profile(&p, cert_path) {
|
||||||
ok += 1;
|
ok += 1;
|
||||||
@@ -910,36 +911,36 @@ fn install_nss_stores(cert_path: &str) {
|
|||||||
tracing::info!("CA installed in {}/{} NSS store(s).", ok, tried);
|
tracing::info!("CA installed in {}/{} NSS store(s).", ok, tried);
|
||||||
} else if tried > 0 {
|
} else if tried > 0 {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"NSS install: 0/{} stores updated. If Firefox/Chrome was running, close \
|
"NSS install: 0/{} stores updated. If Firefox/LibreWolf/Chrome was running, \
|
||||||
them and retry. Otherwise, import ca.crt manually via browser settings.",
|
close them and retry. Otherwise, import ca.crt manually via browser settings.",
|
||||||
tried
|
tried
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write `user_pref("security.enterprise_roots.enabled", true);` to every
|
/// Write `user_pref("security.enterprise_roots.enabled", true);` to every
|
||||||
/// discovered Firefox profile's user.js. This makes Firefox trust the OS
|
/// discovered Firefox/LibreWolf profile's user.js. This makes the browser
|
||||||
/// trust store on next startup — so our already-successful system-level
|
/// trust the OS trust store on next startup — so our already-successful
|
||||||
/// CA install automatically propagates. Critical on Windows where Firefox
|
/// system-level CA install automatically propagates. Critical on Windows
|
||||||
/// keeps its own NSS DB independent of Windows cert store, and NSS
|
/// where the browser keeps its own NSS DB independent of the Windows
|
||||||
/// certutil isn't typically installed so the certutil-based path doesn't
|
/// cert store, and NSS certutil isn't typically installed so the
|
||||||
/// fire there.
|
/// certutil-based path doesn't fire there.
|
||||||
///
|
///
|
||||||
/// We tag the block we write with a sentinel marker comment on the line
|
/// We tag the block we write with a sentinel marker comment on the line
|
||||||
/// above the pref, so uninstall can prove ownership before removing it —
|
/// above the pref, so uninstall can prove ownership before removing it —
|
||||||
/// the user may have had `security.enterprise_roots.enabled = true`
|
/// the user may have had `security.enterprise_roots.enabled = true`
|
||||||
/// before this app existed, and we must not silently revoke their
|
/// before this app existed, and we must not silently revoke their
|
||||||
/// setting. Idempotent.
|
/// setting. Idempotent.
|
||||||
fn enable_firefox_enterprise_roots() {
|
fn enable_mozilla_enterprise_roots() {
|
||||||
let mut touched = 0;
|
let mut touched = 0;
|
||||||
for profile in firefox_profile_dirs() {
|
for profile in mozilla_family_profile_dirs() {
|
||||||
let user_js = profile.join("user.js");
|
let user_js = profile.join("user.js");
|
||||||
let existing = std::fs::read_to_string(&user_js).unwrap_or_default();
|
let existing = std::fs::read_to_string(&user_js).unwrap_or_default();
|
||||||
match add_enterprise_roots_block(&existing) {
|
match add_enterprise_roots_block(&existing) {
|
||||||
EnterpriseRootsEdit::AddedBlock(new) => {
|
EnterpriseRootsEdit::AddedBlock(new) => {
|
||||||
if let Err(e) = std::fs::write(&user_js, new) {
|
if let Err(e) = std::fs::write(&user_js, new) {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"firefox profile {}: user.js write failed: {}",
|
"mozilla profile {}: user.js write failed: {}",
|
||||||
profile.display(),
|
profile.display(),
|
||||||
e
|
e
|
||||||
);
|
);
|
||||||
@@ -950,7 +951,7 @@ fn enable_firefox_enterprise_roots() {
|
|||||||
EnterpriseRootsEdit::AlreadyOurs => {}
|
EnterpriseRootsEdit::AlreadyOurs => {}
|
||||||
EnterpriseRootsEdit::UserOwned => {
|
EnterpriseRootsEdit::UserOwned => {
|
||||||
tracing::debug!(
|
tracing::debug!(
|
||||||
"firefox profile {} already has a user-owned enterprise_roots pref; leaving alone",
|
"mozilla profile {} already has a user-owned enterprise_roots pref; leaving alone",
|
||||||
profile.display()
|
profile.display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -958,7 +959,7 @@ fn enable_firefox_enterprise_roots() {
|
|||||||
}
|
}
|
||||||
if touched > 0 {
|
if touched > 0 {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"enabled Firefox enterprise_roots in {} profile(s) — restart Firefox for it to take effect",
|
"enabled enterprise_roots in {} Firefox/LibreWolf profile(s) — restart the browser for it to take effect",
|
||||||
touched
|
touched
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1054,7 +1055,7 @@ fn contains_our_block(existing: &str) -> bool {
|
|||||||
/// True iff `existing` has our exact pref line but NOT inside our
|
/// True iff `existing` has our exact pref line but NOT inside our
|
||||||
/// marker+pref block — i.e. an orphan `security.enterprise_roots.enabled
|
/// marker+pref block — i.e. an orphan `security.enterprise_roots.enabled
|
||||||
/// = true` whose provenance we can't prove. Used by
|
/// = true` whose provenance we can't prove. Used by
|
||||||
/// `disable_firefox_enterprise_roots` to surface a one-line hint on
|
/// `disable_mozilla_enterprise_roots` to surface a one-line hint on
|
||||||
/// uninstall so users upgrading from pre-v1.2.13 installs know their
|
/// uninstall so users upgrading from pre-v1.2.13 installs know their
|
||||||
/// Firefox user.js still has a cosmetic orphan pref from the old app
|
/// Firefox user.js still has a cosmetic orphan pref from the old app
|
||||||
/// (not broken, just left in place because we can't distinguish it
|
/// (not broken, just left in place because we can't distinguish it
|
||||||
@@ -1173,13 +1174,13 @@ impl NssReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn remove_nss_stores() -> NssReport {
|
fn remove_nss_stores() -> NssReport {
|
||||||
disable_firefox_enterprise_roots();
|
disable_mozilla_enterprise_roots();
|
||||||
|
|
||||||
if !has_nss_certutil() {
|
if !has_nss_certutil() {
|
||||||
// Only warn if there's actually an NSS store we can see — if the
|
// Only warn if there's actually an NSS store we can see — if the
|
||||||
// user never ran Firefox/Chrome on this machine there's nothing
|
// user never ran Firefox/Chrome on this machine there's nothing
|
||||||
// to clean up either way.
|
// to clean up either way.
|
||||||
let profiles = firefox_profile_dirs();
|
let profiles = mozilla_family_profile_dirs();
|
||||||
let chrome_present: bool;
|
let chrome_present: bool;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -1195,9 +1196,9 @@ fn remove_nss_stores() -> NssReport {
|
|||||||
if stores_present {
|
if stores_present {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"NSS certutil not found — cannot automatically remove CA from \
|
"NSS certutil not found — cannot automatically remove CA from \
|
||||||
Firefox/Chrome NSS stores. Remove `MasterHttpRelayVPN` manually \
|
Firefox/LibreWolf/Chrome NSS stores. Remove `MasterHttpRelayVPN` \
|
||||||
via each browser's certificate settings, or install NSS tools \
|
manually via each browser's certificate settings, or install NSS \
|
||||||
(`libnss3-tools` on Debian/Ubuntu, `nss-tools` on Fedora/RHEL) \
|
tools (`libnss3-tools` on Debian/Ubuntu, `nss-tools` on Fedora/RHEL) \
|
||||||
and re-run --remove-cert."
|
and re-run --remove-cert."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1210,7 +1211,7 @@ fn remove_nss_stores() -> NssReport {
|
|||||||
|
|
||||||
let mut report = NssReport::default();
|
let mut report = NssReport::default();
|
||||||
|
|
||||||
for p in firefox_profile_dirs() {
|
for p in mozilla_family_profile_dirs() {
|
||||||
report.tried += 1;
|
report.tried += 1;
|
||||||
if remove_nss_in_profile(&p) {
|
if remove_nss_in_profile(&p) {
|
||||||
report.ok += 1;
|
report.ok += 1;
|
||||||
@@ -1239,7 +1240,7 @@ fn remove_nss_stores() -> NssReport {
|
|||||||
tracing::info!("Removed CA from {} NSS store(s).", report.ok);
|
tracing::info!("Removed CA from {} NSS store(s).", report.ok);
|
||||||
} else {
|
} else {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
"NSS cleanup partial: {}/{} stores updated. If Firefox/Chrome \
|
"NSS cleanup partial: {}/{} stores updated. If Firefox/LibreWolf/Chrome \
|
||||||
was running, close it and re-run --remove-cert. Otherwise \
|
was running, close it and re-run --remove-cert. Otherwise \
|
||||||
remove `MasterHttpRelayVPN` manually via each browser's cert \
|
remove `MasterHttpRelayVPN` manually via each browser's cert \
|
||||||
settings.",
|
settings.",
|
||||||
@@ -1329,12 +1330,12 @@ fn remove_nss_in_profile(profile: &Path) -> bool {
|
|||||||
remove_nss_in_dir(&dir_arg)
|
remove_nss_in_dir(&dir_arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Undo `enable_firefox_enterprise_roots`: for each profile, strip the
|
/// Undo `enable_mozilla_enterprise_roots`: for each profile, strip the
|
||||||
/// marker+pref block if (and only if) we wrote it. If the user owns
|
/// marker+pref block if (and only if) we wrote it. If the user owns
|
||||||
/// their own `enterprise_roots` pref — indicated by the absence of our
|
/// their own `enterprise_roots` pref — indicated by the absence of our
|
||||||
/// marker line — leave user.js alone entirely.
|
/// marker line — leave user.js alone entirely.
|
||||||
fn disable_firefox_enterprise_roots() {
|
fn disable_mozilla_enterprise_roots() {
|
||||||
for profile in firefox_profile_dirs() {
|
for profile in mozilla_family_profile_dirs() {
|
||||||
let user_js = profile.join("user.js");
|
let user_js = profile.join("user.js");
|
||||||
let Ok(existing) = std::fs::read_to_string(&user_js) else {
|
let Ok(existing) = std::fs::read_to_string(&user_js) else {
|
||||||
continue;
|
continue;
|
||||||
@@ -1351,10 +1352,10 @@ fn disable_firefox_enterprise_roots() {
|
|||||||
// leftovers feel like half-done removals.
|
// leftovers feel like half-done removals.
|
||||||
if has_bare_enterprise_roots(&existing) {
|
if has_bare_enterprise_roots(&existing) {
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"Firefox profile {}: `security.enterprise_roots.enabled` pref \
|
"Mozilla profile {}: `security.enterprise_roots.enabled` pref \
|
||||||
present without our marker — left in place. If it was written \
|
present without our marker — left in place. If it was written \
|
||||||
by a pre-v1.2.13 install it's a cosmetic orphan (harmless, \
|
by a pre-v1.2.13 install it's a cosmetic orphan (harmless, the \
|
||||||
Firefox falls back to its built-in root store); remove it \
|
browser falls back to its built-in root store); remove it \
|
||||||
manually from user.js if it bothers you. If you set it \
|
manually from user.js if it bothers you. If you set it \
|
||||||
yourself, leave it.",
|
yourself, leave it.",
|
||||||
profile.display()
|
profile.display()
|
||||||
@@ -1363,16 +1364,53 @@ fn disable_firefox_enterprise_roots() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn firefox_profile_dirs() -> Vec<std::path::PathBuf> {
|
/// Candidate root directories under which Mozilla-family browser profile
|
||||||
use std::path::PathBuf;
|
/// directories (each containing cert9.db / cert8.db) live. Pure helper —
|
||||||
|
/// OS / HOME / APPDATA / XDG_CONFIG_HOME come in as args so the
|
||||||
|
/// per-platform layout can be asserted in unit tests without touching
|
||||||
|
/// env or the filesystem.
|
||||||
|
///
|
||||||
|
/// LibreWolf (issue #1145) is a Firefox fork with strict privacy
|
||||||
|
/// defaults that shares Firefox's NSS DB layout and respects the same
|
||||||
|
/// `security.enterprise_roots.enabled` pref, but stores its profile tree
|
||||||
|
/// under its own app dir — so the original Firefox-only scan missed it
|
||||||
|
/// and the MITM CA never reached LibreWolf's trust store. HSTS-protected
|
||||||
|
/// sites (bing.com, youtube.com, …) then failed with
|
||||||
|
/// MOZILLA_PKIX_ERROR_MITM_DETECTED with no add-exception path the user
|
||||||
|
/// could take.
|
||||||
|
///
|
||||||
|
/// On Linux we have to scan five candidate Mozilla-fork layouts:
|
||||||
|
/// * `~/.librewolf` — LibreWolf legacy Firefox-style layout (still
|
||||||
|
/// present on pre-migration installs).
|
||||||
|
/// * `${XDG_CONFIG_HOME:-~/.config}/librewolf/librewolf` — LibreWolf
|
||||||
|
/// current XDG layout.
|
||||||
|
/// * Both LibreWolf paths again under
|
||||||
|
/// `~/.var/app/io.gitlab.librewolf-community/` for the Flatpak
|
||||||
|
/// sandbox, which redirects HOME inside the container.
|
||||||
|
/// * `~/.mozilla/icecat` — GNU IceCat (Firefox fork shipped by
|
||||||
|
/// Trisquel / Parabola / Guix / Debian). Same NSS DB format and
|
||||||
|
/// `security.enterprise_roots.enabled` semantics as Firefox; only
|
||||||
|
/// the binary's branded profile dir differs. Windows/macOS builds
|
||||||
|
/// are not officially distributed, so we don't list paths there.
|
||||||
|
/// Non-existent roots silently no-op via `read_dir` failure, so listing
|
||||||
|
/// all of them costs nothing on installs that only have one.
|
||||||
|
fn mozilla_family_profile_roots(
|
||||||
|
os: &str,
|
||||||
|
home: &str,
|
||||||
|
appdata: Option<&str>,
|
||||||
|
xdg_config_home: Option<&str>,
|
||||||
|
) -> Vec<PathBuf> {
|
||||||
let mut roots: Vec<PathBuf> = Vec::new();
|
let mut roots: Vec<PathBuf> = Vec::new();
|
||||||
let home = std::env::var("HOME").unwrap_or_default();
|
match os {
|
||||||
match std::env::consts::OS {
|
|
||||||
"macos" => {
|
"macos" => {
|
||||||
roots.push(PathBuf::from(format!(
|
roots.push(PathBuf::from(format!(
|
||||||
"{}/Library/Application Support/Firefox/Profiles",
|
"{}/Library/Application Support/Firefox/Profiles",
|
||||||
home
|
home
|
||||||
)));
|
)));
|
||||||
|
roots.push(PathBuf::from(format!(
|
||||||
|
"{}/Library/Application Support/LibreWolf/Profiles",
|
||||||
|
home
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
"linux" => {
|
"linux" => {
|
||||||
roots.push(PathBuf::from(format!("{}/.mozilla/firefox", home)));
|
roots.push(PathBuf::from(format!("{}/.mozilla/firefox", home)));
|
||||||
@@ -1380,20 +1418,52 @@ fn firefox_profile_dirs() -> Vec<std::path::PathBuf> {
|
|||||||
"{}/snap/firefox/common/.mozilla/firefox",
|
"{}/snap/firefox/common/.mozilla/firefox",
|
||||||
home
|
home
|
||||||
)));
|
)));
|
||||||
|
// Legacy LibreWolf layout (still present on older installs).
|
||||||
|
roots.push(PathBuf::from(format!("{}/.librewolf", home)));
|
||||||
|
// Current XDG layout. Empty XDG_CONFIG_HOME is treated as
|
||||||
|
// unset per XDG Base Directory spec.
|
||||||
|
let xdg = xdg_config_home
|
||||||
|
.filter(|v| !v.is_empty())
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or_else(|| format!("{}/.config", home));
|
||||||
|
roots.push(PathBuf::from(format!("{}/librewolf/librewolf", xdg)));
|
||||||
|
// Flatpak sandbox: $HOME inside the container is
|
||||||
|
// ~/.var/app/<flatpak-id>/. Cover both legacy and XDG layouts
|
||||||
|
// since LibreWolf's migration mirrors the host inside the
|
||||||
|
// sandbox.
|
||||||
|
let flatpak_home = format!("{}/.var/app/io.gitlab.librewolf-community", home);
|
||||||
|
roots.push(PathBuf::from(format!("{}/.librewolf", flatpak_home)));
|
||||||
|
roots.push(PathBuf::from(format!(
|
||||||
|
"{}/.config/librewolf/librewolf",
|
||||||
|
flatpak_home
|
||||||
|
)));
|
||||||
|
// GNU IceCat: Firefox fork shipped by Trisquel / Parabola /
|
||||||
|
// Guix / Debian, primarily a GNU/Linux distribution target.
|
||||||
|
// Mirrors Firefox's `~/.mozilla/firefox` layout under
|
||||||
|
// `~/.mozilla/icecat`.
|
||||||
|
roots.push(PathBuf::from(format!("{}/.mozilla/icecat", home)));
|
||||||
}
|
}
|
||||||
"windows" => {
|
"windows" => {
|
||||||
if let Ok(appdata) = std::env::var("APPDATA") {
|
if let Some(appdata) = appdata {
|
||||||
roots.push(PathBuf::from(format!(
|
roots.push(PathBuf::from(format!(
|
||||||
"{}\\Mozilla\\Firefox\\Profiles",
|
"{}\\Mozilla\\Firefox\\Profiles",
|
||||||
appdata
|
appdata
|
||||||
)));
|
)));
|
||||||
|
roots.push(PathBuf::from(format!("{}\\LibreWolf\\Profiles", appdata)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
roots
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Walk each candidate root and return every immediate child that looks
|
||||||
|
/// like a Mozilla NSS profile (has cert9.db or cert8.db). Pure given the
|
||||||
|
/// roots — no env access — so tempdir tests can pin the filter without
|
||||||
|
/// stubbing HOME/APPDATA. Missing roots silently skip.
|
||||||
|
fn discover_profile_dirs(roots: &[PathBuf]) -> Vec<PathBuf> {
|
||||||
let mut out: Vec<PathBuf> = Vec::new();
|
let mut out: Vec<PathBuf> = Vec::new();
|
||||||
for root in &roots {
|
for root in roots {
|
||||||
let Ok(entries) = std::fs::read_dir(root) else {
|
let Ok(entries) = std::fs::read_dir(root) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -1402,7 +1472,7 @@ fn firefox_profile_dirs() -> Vec<std::path::PathBuf> {
|
|||||||
if !p.is_dir() {
|
if !p.is_dir() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// A profile has cert9.db or cert8.db.
|
// A profile has cert9.db (NSS sql:) or cert8.db (legacy dbm:).
|
||||||
if p.join("cert9.db").exists() || p.join("cert8.db").exists() {
|
if p.join("cert9.db").exists() || p.join("cert8.db").exists() {
|
||||||
out.push(p);
|
out.push(p);
|
||||||
}
|
}
|
||||||
@@ -1411,6 +1481,19 @@ fn firefox_profile_dirs() -> Vec<std::path::PathBuf> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mozilla_family_profile_dirs() -> Vec<std::path::PathBuf> {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_default();
|
||||||
|
let appdata = std::env::var("APPDATA").ok();
|
||||||
|
let xdg = std::env::var("XDG_CONFIG_HOME").ok();
|
||||||
|
let roots = mozilla_family_profile_roots(
|
||||||
|
std::env::consts::OS,
|
||||||
|
&home,
|
||||||
|
appdata.as_deref(),
|
||||||
|
xdg.as_deref(),
|
||||||
|
);
|
||||||
|
discover_profile_dirs(&roots)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1800,4 +1883,158 @@ ID_LIKE=debian
|
|||||||
// to classify as "not found is NOT proven", i.e. failure.
|
// to classify as "not found is NOT proven", i.e. failure.
|
||||||
assert!(!is_nss_not_found(""));
|
assert!(!is_nss_not_found(""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── mozilla_family_profile_roots ──
|
||||||
|
//
|
||||||
|
// Regression guard for issue #1145: LibreWolf users hit
|
||||||
|
// MOZILLA_PKIX_ERROR_MITM_DETECTED on HSTS sites (bing.com,
|
||||||
|
// youtube.com) because the installer only scanned Firefox profile
|
||||||
|
// roots, never reaching LibreWolf's NSS DB. LibreWolf on Linux
|
||||||
|
// additionally migrated from `~/.librewolf` to XDG
|
||||||
|
// (`~/.config/librewolf/librewolf`) mid-project, and Flatpak
|
||||||
|
// installs redirect HOME inside the sandbox — both classes of
|
||||||
|
// install were silently missed by a first-pass legacy-only fix.
|
||||||
|
// These tests pin every layout so regressions can't sneak back.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_linux_covers_firefox_librewolf_flatpak_and_icecat() {
|
||||||
|
let roots = mozilla_family_profile_roots("linux", "/home/u", None, None);
|
||||||
|
let s: Vec<String> = roots.iter().map(|p| p.display().to_string()).collect();
|
||||||
|
assert!(s.iter().any(|p| p == "/home/u/.mozilla/firefox"));
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == "/home/u/snap/firefox/common/.mozilla/firefox"));
|
||||||
|
// LibreWolf legacy.
|
||||||
|
assert!(s.iter().any(|p| p == "/home/u/.librewolf"));
|
||||||
|
// LibreWolf XDG default (XDG_CONFIG_HOME unset → ~/.config).
|
||||||
|
assert!(s.iter().any(|p| p == "/home/u/.config/librewolf/librewolf"));
|
||||||
|
// LibreWolf Flatpak — both legacy and XDG layouts inside the sandbox.
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == "/home/u/.var/app/io.gitlab.librewolf-community/.librewolf"));
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p
|
||||||
|
== "/home/u/.var/app/io.gitlab.librewolf-community/.config/librewolf/librewolf"));
|
||||||
|
// GNU IceCat (Trisquel / Parabola / Guix / Debian).
|
||||||
|
assert!(s.iter().any(|p| p == "/home/u/.mozilla/icecat"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_linux_honors_xdg_config_home_override() {
|
||||||
|
// When XDG_CONFIG_HOME is set we must use it verbatim, not
|
||||||
|
// ~/.config. Pinned because a refactor that always defaulted
|
||||||
|
// would silently miss profiles for users who relocate their
|
||||||
|
// XDG config dir.
|
||||||
|
let roots = mozilla_family_profile_roots("linux", "/home/u", None, Some("/srv/xdg"));
|
||||||
|
let s: Vec<String> = roots.iter().map(|p| p.display().to_string()).collect();
|
||||||
|
assert!(s.iter().any(|p| p == "/srv/xdg/librewolf/librewolf"));
|
||||||
|
// Default-derived path must NOT also be emitted when override
|
||||||
|
// is present — otherwise we double-scan a path that no longer
|
||||||
|
// exists for this user.
|
||||||
|
assert!(!s.iter().any(|p| p == "/home/u/.config/librewolf/librewolf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_linux_treats_empty_xdg_config_home_as_unset() {
|
||||||
|
// Per the XDG Base Directory spec, an empty value means
|
||||||
|
// "fall back to the default" — same as if the variable were
|
||||||
|
// unset entirely.
|
||||||
|
let roots = mozilla_family_profile_roots("linux", "/home/u", None, Some(""));
|
||||||
|
let s: Vec<String> = roots.iter().map(|p| p.display().to_string()).collect();
|
||||||
|
assert!(s.iter().any(|p| p == "/home/u/.config/librewolf/librewolf"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_macos_covers_firefox_and_librewolf() {
|
||||||
|
let roots = mozilla_family_profile_roots("macos", "/Users/u", None, None);
|
||||||
|
let s: Vec<String> = roots.iter().map(|p| p.display().to_string()).collect();
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == "/Users/u/Library/Application Support/Firefox/Profiles"));
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == "/Users/u/Library/Application Support/LibreWolf/Profiles"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_windows_covers_firefox_and_librewolf() {
|
||||||
|
let roots = mozilla_family_profile_roots(
|
||||||
|
"windows",
|
||||||
|
"ignored",
|
||||||
|
Some("C:\\Users\\u\\AppData\\Roaming"),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let s: Vec<String> = roots.iter().map(|p| p.display().to_string()).collect();
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == "C:\\Users\\u\\AppData\\Roaming\\Mozilla\\Firefox\\Profiles"));
|
||||||
|
assert!(s
|
||||||
|
.iter()
|
||||||
|
.any(|p| p == "C:\\Users\\u\\AppData\\Roaming\\LibreWolf\\Profiles"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_windows_without_appdata_yields_nothing() {
|
||||||
|
// %APPDATA% can be missing in stripped CI / service contexts.
|
||||||
|
// Existing behaviour was to no-op; LibreWolf addition must not
|
||||||
|
// panic or fabricate a path from an empty string either.
|
||||||
|
let roots = mozilla_family_profile_roots("windows", "ignored", None, None);
|
||||||
|
assert!(roots.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mozilla_roots_unknown_os_is_empty() {
|
||||||
|
let roots = mozilla_family_profile_roots("freebsd", "/home/u", None, None);
|
||||||
|
assert!(roots.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── discover_profile_dirs (cert-db filter) ──
|
||||||
|
|
||||||
|
fn touch(path: &Path) {
|
||||||
|
std::fs::write(path, b"").expect("write");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn discover_profile_dirs_picks_profiles_with_cert9_or_cert8() {
|
||||||
|
// Build a tempdir that mimics the real Mozilla profile layout
|
||||||
|
// and assert the filter accepts cert9.db (NSS sql:) and
|
||||||
|
// cert8.db (legacy dbm:) profiles, skips siblings that have
|
||||||
|
// neither, ignores plain files, and tolerates missing roots.
|
||||||
|
let tmp = std::env::temp_dir().join(format!(
|
||||||
|
"mhrv-discover-{}-{:x}",
|
||||||
|
std::process::id(),
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0)
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
std::fs::create_dir_all(&tmp).expect("mkdir tmp");
|
||||||
|
|
||||||
|
let with_cert9 = tmp.join("abc.default");
|
||||||
|
let with_cert8 = tmp.join("legacy.profile");
|
||||||
|
let without_db = tmp.join("not-a-profile");
|
||||||
|
let stray_file = tmp.join("profiles.ini");
|
||||||
|
std::fs::create_dir_all(&with_cert9).unwrap();
|
||||||
|
std::fs::create_dir_all(&with_cert8).unwrap();
|
||||||
|
std::fs::create_dir_all(&without_db).unwrap();
|
||||||
|
touch(&with_cert9.join("cert9.db"));
|
||||||
|
touch(&with_cert8.join("cert8.db"));
|
||||||
|
touch(&stray_file);
|
||||||
|
|
||||||
|
let missing_root = tmp.join("does-not-exist");
|
||||||
|
let got = discover_profile_dirs(&[tmp.clone(), missing_root]);
|
||||||
|
|
||||||
|
let names: std::collections::HashSet<_> = got
|
||||||
|
.iter()
|
||||||
|
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
|
||||||
|
.collect();
|
||||||
|
assert!(names.contains("abc.default"));
|
||||||
|
assert!(names.contains("legacy.profile"));
|
||||||
|
assert!(!names.contains("not-a-profile"));
|
||||||
|
assert!(!names.contains("profiles.ini"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&tmp);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user