Skip to content

Commit 5268507

Browse files
committed
Add support to crash reporter and unifiy code in rust
1 parent cae1dbb commit 5268507

16 files changed

Lines changed: 582 additions & 171 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser/branding/enterprise/byteshift.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66

77
# Byte shift value applied to AutoConfig files. Must match the default of the
88
# general.config.obscure_value preference, which the browser subtracts when
9-
# reading the file.
9+
# reading the file, and DEFAULT_OBSCURE_VALUE in the enterprise-console crate
10+
# (toolkit/components/enterprise/rust), which decodes the file for the early
11+
# startup and crash reporter consumers. The generic console address
12+
# placeholder below is CONSOLE_ADDRESS_PLACEHOLDER in that crate.
1013
OBSCURE_VALUE = 13
1114

1215

toolkit/components/enterprise/modules/ConsoleClient.sys.mjs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ export const CONSOLE_ADDRESS_PREF = "enterprise.console.address";
3939
* the AutoConfig file does not bake in a real console address. The actual
4040
* address then comes from the MOZ_ENTERPRISE_CONSOLE_ADDRESS environment
4141
* variable or from felt.json, filled in by the pre-profile console setup
42-
* dialog. Keep in sync with ENTERPRISE_CONSOLE_PLACEHOLDER in nsXULAppAPI.h.
42+
* dialog. Keep in sync with CONSOLE_ADDRESS_PLACEHOLDER in the
43+
* enterprise-console crate (toolkit/components/enterprise/rust), which holds
44+
* this resolution logic for the native consumers; resolveConsoleAddress below
45+
* mirrors it in JS.
4346
*/
4447
export const CONSOLE_ADDRESS_PLACEHOLDER = "FIREFOX_ENTERPRISE_GENERIC";
4548

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This Source Code Form is subject to the terms of the Mozilla Public
2+
# License, v. 2.0. If a copy of the MPL was not distributed with this
3+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
5+
[package]
6+
name = "enterprise-console"
7+
version = "0.1.0"
8+
edition = "2021"
9+
license = "MPL-2.0"
10+
11+
[dependencies]
12+
serde_json = "1"
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
//! Shared logic for obtaining the enterprise console address.
6+
//!
7+
//! A repack bakes the console address into the AutoConfig file
8+
//! (`firefox.cfg`); on generic (non-repacked) builds the file holds
9+
//! [`CONSOLE_ADDRESS_PLACEHOLDER`] instead, and the address is resolved from
10+
//! the [`CONSOLE_ADDRESS_ENV`] environment variable or from the value the
11+
//! console setup dialog persisted in `felt.json`. This crate keeps that
12+
//! resolution identical for every native consumer: the browser (through the
13+
//! felt crate's FFI, used by CreateAppData.cpp) and the standalone crash
14+
//! reporter client. `resolveConsoleAddress` in ConsoleClient.sys.mjs mirrors
15+
//! it in JS.
16+
//!
17+
//! The crate is IO-free: callers hand in file contents and the environment
18+
//! value, so each consumer keeps its own file and environment access (the
19+
//! crash reporter mocks both in its tests).
20+
21+
/// The pref holding the enterprise console address, set by the AutoConfig
22+
/// file.
23+
pub const CONSOLE_ADDRESS_PREF: &str = "enterprise.console.address";
24+
25+
/// Placeholder address in the AutoConfig file of generic (non-repacked)
26+
/// builds. Keep in sync with CONSOLE_ADDRESS_PLACEHOLDER in
27+
/// ConsoleClient.sys.mjs and the placeholder in
28+
/// browser/branding/enterprise/byteshift.py.
29+
pub const CONSOLE_ADDRESS_PLACEHOLDER: &str = "FIREFOX_ENTERPRISE_GENERIC";
30+
31+
/// Environment variable providing the console address on generic builds
32+
/// (used by test harnesses).
33+
pub const CONSOLE_ADDRESS_ENV: &str = "MOZ_ENTERPRISE_CONSOLE_ADDRESS";
34+
35+
/// Storage file in the user application data directory where the console
36+
/// setup dialog persists the address on generic builds.
37+
pub const FELT_STORAGE_FILENAME: &str = "felt.json";
38+
39+
/// Key holding the console address in the felt storage file. Keep in sync
40+
/// with FeltStorage.sys.mjs.
41+
pub const FELT_CONSOLE_ADDRESS_KEY: &str = "consoleAddress";
42+
43+
/// Default byte shift applied to AutoConfig files
44+
/// (`general.config.obscure_value`). Keep in sync with OBSCURE_VALUE in
45+
/// browser/branding/enterprise/byteshift.py.
46+
pub const DEFAULT_OBSCURE_VALUE: u8 = 13;
47+
48+
/// Pref-setting functions an AutoConfig file may use to set a string pref.
49+
const PREF_FUNCTIONS: &[&str] = &["lockPref", "defaultPref", "pref"];
50+
51+
/// Extract the console address from the raw contents of an AutoConfig file
52+
/// without evaluating it.
53+
///
54+
/// AutoConfig files are byte shifted by `general.config.obscure_value` and
55+
/// have an intentionally unparseable first line; the shift is undone (trying
56+
/// the default value, then plaintext) and the file scanned for the pref call.
57+
/// On generic builds this yields [`CONSOLE_ADDRESS_PLACEHOLDER`]; pass the
58+
/// result through [`resolve_console_address`].
59+
pub fn console_address_from_autoconfig(contents: &[u8]) -> Option<String> {
60+
for shift in [DEFAULT_OBSCURE_VALUE, 0] {
61+
let decoded: Vec<u8> = contents.iter().map(|b| b.wrapping_sub(shift)).collect();
62+
let Ok(text) = String::from_utf8(decoded) else {
63+
continue;
64+
};
65+
for func in PREF_FUNCTIONS {
66+
if let Some(value) = find_string_pref_call(&text, func, CONSOLE_ADDRESS_PREF) {
67+
return Some(value.to_owned());
68+
}
69+
}
70+
}
71+
None
72+
}
73+
74+
/// Resolve a console address that may be [`CONSOLE_ADDRESS_PLACEHOLDER`].
75+
///
76+
/// A real address is returned unchanged. The placeholder resolves to
77+
/// `env_value` (the value of [`CONSOLE_ADDRESS_ENV`]) when non-empty, then to
78+
/// the address stored in felt.json, whose contents `read_felt_json` supplies
79+
/// (called only when needed). Returns None when the placeholder cannot be
80+
/// resolved, in which case the browser shows the console setup dialog.
81+
pub fn resolve_console_address(
82+
address: &str,
83+
env_value: Option<&str>,
84+
read_felt_json: impl FnOnce() -> Option<Vec<u8>>,
85+
) -> Option<String> {
86+
if address != CONSOLE_ADDRESS_PLACEHOLDER {
87+
return Some(address.to_owned());
88+
}
89+
if let Some(url) = env_value {
90+
if !url.is_empty() {
91+
return Some(url.to_owned());
92+
}
93+
}
94+
stored_console_address(&read_felt_json()?)
95+
}
96+
97+
/// Read the console address out of felt.json contents.
98+
pub fn stored_console_address(felt_json: &[u8]) -> Option<String> {
99+
let json: serde_json::Value = serde_json::from_slice(felt_json).ok()?;
100+
match json.get(FELT_CONSOLE_ADDRESS_KEY).and_then(|v| v.as_str()) {
101+
Some(url) if !url.is_empty() => Some(url.to_owned()),
102+
_ => None,
103+
}
104+
}
105+
106+
/// Outcome of [`remove_stored_console_address`].
107+
pub enum RemoveStoredAddress {
108+
/// No address was stored (including unparseable contents); nothing to
109+
/// write back.
110+
AlreadyAbsent,
111+
/// The address was removed; the new felt.json contents to write back.
112+
Removed(String),
113+
/// The contents are not a JSON object; nothing can be removed.
114+
Invalid,
115+
}
116+
117+
/// Remove the console address from felt.json contents, keeping other keys.
118+
pub fn remove_stored_console_address(felt_json: &[u8]) -> RemoveStoredAddress {
119+
let Ok(mut json) = serde_json::from_slice::<serde_json::Value>(felt_json) else {
120+
return RemoveStoredAddress::AlreadyAbsent;
121+
};
122+
let Some(obj) = json.as_object_mut() else {
123+
return RemoveStoredAddress::Invalid;
124+
};
125+
if obj.remove(FELT_CONSOLE_ADDRESS_KEY).is_none() {
126+
return RemoveStoredAddress::AlreadyAbsent;
127+
}
128+
RemoveStoredAddress::Removed(json.to_string())
129+
}
130+
131+
/// Find the string value of a `func("pref", "value");` call, ignoring calls
132+
/// setting other prefs and occurrences of the pref name in other positions.
133+
fn find_string_pref_call<'a>(contents: &'a str, func: &str, pref: &str) -> Option<&'a str> {
134+
let opener = format!("{func}(");
135+
let mut search_content = contents;
136+
loop {
137+
let (before, s) = search_content.split_once(&format!("\"{pref}\""))?;
138+
if !before.trim().ends_with(&opener) {
139+
search_content = s;
140+
continue;
141+
}
142+
let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ',');
143+
let (content, _) = s.split_once(");")?;
144+
return content.trim().strip_prefix('"')?.strip_suffix('"');
145+
}
146+
}
147+
148+
#[cfg(test)]
149+
mod test {
150+
use super::*;
151+
152+
fn encode(plaintext: &str) -> Vec<u8> {
153+
plaintext
154+
.bytes()
155+
.map(|b| b.wrapping_add(DEFAULT_OBSCURE_VALUE))
156+
.collect()
157+
}
158+
159+
const CFG: &str = "// first line is ignored\n\
160+
lockPref(\"enterprise.console.address\", \"https://console.example.com/foo/\");";
161+
162+
#[test]
163+
fn autoconfig_encoded() {
164+
assert_eq!(
165+
console_address_from_autoconfig(&encode(CFG)).as_deref(),
166+
Some("https://console.example.com/foo/")
167+
);
168+
}
169+
170+
#[test]
171+
fn autoconfig_plaintext() {
172+
assert_eq!(
173+
console_address_from_autoconfig(CFG.as_bytes()).as_deref(),
174+
Some("https://console.example.com/foo/")
175+
);
176+
}
177+
178+
#[test]
179+
fn autoconfig_default_pref_function() {
180+
let cfg = r#"defaultPref("enterprise.console.address", "https://d.example.com/");"#;
181+
assert_eq!(
182+
console_address_from_autoconfig(cfg.as_bytes()).as_deref(),
183+
Some("https://d.example.com/")
184+
);
185+
}
186+
187+
#[test]
188+
fn autoconfig_ignores_other_prefs_and_positions() {
189+
let cfg = "lockPref(\"other.pref\", \"enterprise.console.address\");\n\
190+
lockPref(\"enterprise.console.address\", \"https://real.example.com/\");";
191+
assert_eq!(
192+
console_address_from_autoconfig(cfg.as_bytes()).as_deref(),
193+
Some("https://real.example.com/")
194+
);
195+
}
196+
197+
#[test]
198+
fn autoconfig_missing_pref() {
199+
assert_eq!(console_address_from_autoconfig(b"// nothing here"), None);
200+
}
201+
202+
#[test]
203+
fn resolve_real_address_passes_through() {
204+
assert_eq!(
205+
resolve_console_address("https://console.example.com/", None, || panic!(
206+
"must not read felt.json"
207+
))
208+
.as_deref(),
209+
Some("https://console.example.com/")
210+
);
211+
}
212+
213+
#[test]
214+
fn resolve_placeholder_from_environment() {
215+
assert_eq!(
216+
resolve_console_address(
217+
CONSOLE_ADDRESS_PLACEHOLDER,
218+
Some("https://env.example.com/"),
219+
|| panic!("must not read felt.json")
220+
)
221+
.as_deref(),
222+
Some("https://env.example.com/")
223+
);
224+
}
225+
226+
#[test]
227+
fn resolve_placeholder_from_felt_storage() {
228+
assert_eq!(
229+
resolve_console_address(CONSOLE_ADDRESS_PLACEHOLDER, Some(""), || Some(
230+
br#"{"consoleAddress": "https://stored.example.com/"}"#.to_vec()
231+
))
232+
.as_deref(),
233+
Some("https://stored.example.com/")
234+
);
235+
}
236+
237+
#[test]
238+
fn resolve_placeholder_unresolvable() {
239+
assert_eq!(
240+
resolve_console_address(CONSOLE_ADDRESS_PLACEHOLDER, None, || Some(
241+
br#"{"deviceId": "abc"}"#.to_vec()
242+
)),
243+
None
244+
);
245+
assert_eq!(
246+
resolve_console_address(CONSOLE_ADDRESS_PLACEHOLDER, None, || None),
247+
None
248+
);
249+
}
250+
251+
#[test]
252+
fn remove_stored_address_keeps_other_keys() {
253+
let json = br#"{"consoleAddress": "https://x.example.com/", "deviceId": "abc"}"#;
254+
match remove_stored_console_address(json) {
255+
RemoveStoredAddress::Removed(new_json) => {
256+
assert_eq!(new_json, r#"{"deviceId":"abc"}"#);
257+
}
258+
_ => panic!("expected Removed"),
259+
}
260+
}
261+
262+
#[test]
263+
fn remove_stored_address_absent() {
264+
assert!(matches!(
265+
remove_stored_console_address(br#"{"deviceId": "abc"}"#),
266+
RemoveStoredAddress::AlreadyAbsent
267+
));
268+
assert!(matches!(
269+
remove_stored_console_address(b"not json"),
270+
RemoveStoredAddress::AlreadyAbsent
271+
));
272+
}
273+
274+
#[test]
275+
fn remove_stored_address_invalid() {
276+
assert!(matches!(
277+
remove_stored_console_address(b"[1, 2]"),
278+
RemoveStoredAddress::Invalid
279+
));
280+
}
281+
}

toolkit/components/felt/rust/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ thin-vec = "0.2.12"
1616
time = "0.3.36"
1717
serde = { version = "1", features = ["derive"] }
1818
serde_json = "1"
19+
enterprise-console = { path = "../../enterprise/rust/enterprise-console" }
1920

2021
[target.'cfg(unix)'.dependencies]
2122
libc = "0.2"

toolkit/components/felt/rust/felt.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ bool firefox_felt_read_stored_console_url(const nsACString* aFeltJsonPath,
3737

3838
bool firefox_felt_clear_stored_console_url(const nsACString* aFeltJsonPath);
3939

40+
bool firefox_felt_console_address_from_autoconfig(const nsACString* aContents,
41+
nsACString* aOutAddress);
42+
43+
bool firefox_felt_resolve_console_address(const nsACString* aAddress,
44+
const nsACString* aFeltJsonPath,
45+
nsACString* aOutUrl);
46+
4047
nsresult felt_constructor(REFNSIID iid, void** result);
4148

4249
nsresult felt_restartforced_constructor(REFNSIID iid, void** result);

0 commit comments

Comments
 (0)