Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1281b081c |
No files matched your search
@@ -1,7 +1,6 @@
|
||||
use crate::policies::registries::{Registry, BUILT_IN};
|
||||
use directories::ProjectDirs;
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::PathBuf;
|
||||
@@ -69,31 +68,18 @@ impl Default for DenylistSource {
|
||||
}
|
||||
|
||||
impl FirewallConfig {
|
||||
/// A missing file means defaults; a bad one is an error, so the policy is never quietly relaxed.
|
||||
pub fn load() -> std::io::Result<Self> {
|
||||
let mut paths: Vec<PathBuf> = path().into_iter().collect();
|
||||
paths.extend(crate::os::system_config());
|
||||
Self::load_from(&paths)
|
||||
}
|
||||
|
||||
/// Later files win key by key, but allow lists add up.
|
||||
/// A bad file is an error, so the policy is never quietly relaxed.
|
||||
pub fn load_from(paths: &[PathBuf]) -> std::io::Result<Self> {
|
||||
let mut merged = Map::new();
|
||||
for path in paths {
|
||||
let text = match std::fs::read_to_string(path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => continue,
|
||||
Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))),
|
||||
};
|
||||
let invalid = |e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display()));
|
||||
let layer: Value = serde_json::from_str(&text).map_err(invalid)?;
|
||||
FirewallConfig::deserialize(&layer).map_err(invalid)?;
|
||||
let Value::Object(layer) = layer else {
|
||||
unreachable!("a config parses only from an object");
|
||||
};
|
||||
merge(&mut merged, layer);
|
||||
}
|
||||
Ok(serde_json::from_value(Value::Object(merged)).expect("each layer parsed on its own"))
|
||||
let Some(path) = path() else {
|
||||
return Ok(Self::default());
|
||||
};
|
||||
let text = match std::fs::read_to_string(&path) {
|
||||
Ok(text) => text,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => return Ok(Self::default()),
|
||||
Err(e) => return Err(Error::new(e.kind(), format!("{}: {e}", path.display()))),
|
||||
};
|
||||
serde_json::from_str(&text)
|
||||
.map_err(|e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display())))
|
||||
}
|
||||
|
||||
pub fn local_url(&self, path: &str) -> String {
|
||||
@@ -117,15 +103,3 @@ pub fn path() -> Option<PathBuf> {
|
||||
let dirs = ProjectDirs::from("com", "usestrix", "firewall")?;
|
||||
Some(dirs.config_dir().join("config.json"))
|
||||
}
|
||||
|
||||
fn merge(base: &mut Map<String, Value>, top: Map<String, Value>) {
|
||||
for (key, value) in top {
|
||||
match (base.get_mut(&key), value) {
|
||||
(Some(Value::Object(base)), Value::Object(top)) => merge(base, top),
|
||||
(Some(Value::Array(base)), Value::Array(top)) => base.extend(top),
|
||||
(_, value) => {
|
||||
base.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,6 @@ use rama::rt::Executor;
|
||||
use rama::telemetry::tracing::apple::oslog::{OsLogLayer, Privacy};
|
||||
use rama::{Layer, Service};
|
||||
use std::convert::Infallible;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
@@ -33,7 +32,6 @@ const KEYCHAIN_SERVICE: &str = BUNDLE_ID;
|
||||
const KEYCHAIN_ACCOUNT: &str = "ca";
|
||||
/// Must match `macos/LaunchAgents/com.usestrix.firewall.env.plist`, which points npm at it.
|
||||
const CA_FILE: &str = "/Library/Application Support/Firewall/ca.pem";
|
||||
const CONFIG_FILE: &str = "/Library/Application Support/Firewall/config.json";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Handler {
|
||||
@@ -99,14 +97,10 @@ impl TransparentProxyHandlerFactory for Factory {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn system_config() -> Option<PathBuf> {
|
||||
Some(CONFIG_FILE.into())
|
||||
}
|
||||
|
||||
fn handler(exec: Executor) -> Result<Handler, BoxError> {
|
||||
telemetry::set_mode("extension");
|
||||
telemetry::heartbeat();
|
||||
let config = FirewallConfig::load_from(&[CONFIG_FILE.into()]).inspect_err(|_| telemetry::error("config"))?;
|
||||
let config = FirewallConfig::load().inspect_err(|_| telemetry::error("config"))?;
|
||||
let firewall = Arc::new(Firewall::new(config));
|
||||
let hosts = firewall.hosts();
|
||||
let key = enclave::EnclaveKey::load_or_create().inspect_err(|_| telemetry::error("enclave"))?;
|
||||
|
||||
@@ -2,15 +2,10 @@
|
||||
pub mod macos;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::{system_config, uninstall};
|
||||
pub use macos::uninstall;
|
||||
|
||||
/// Elsewhere nothing is installed system-wide.
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn uninstall() -> Result<(), rama::error::BoxError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub fn system_config() -> Option<std::path::PathBuf> {
|
||||
None
|
||||
}
|
||||
@@ -21,23 +21,6 @@ fn the_config_lives_under_the_config_directory() {
|
||||
assert!(path.ends_with("config.json"), "{}", path.display());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_later_file_wins_and_allow_lists_add_up() {
|
||||
let dir = std::env::temp_dir().join(format!("firewall-layers-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).expect("temp dir");
|
||||
let user = dir.join("user.json");
|
||||
let system = dir.join("system.json");
|
||||
std::fs::write(&user, r#"{"min_age_hours": 1, "fail_open": false, "allow": {"npm": [{"name": "a"}]}}"#).expect("write");
|
||||
std::fs::write(&system, r#"{"min_age_hours": 24, "allow": {"npm": [{"name": "b"}]}}"#).expect("write");
|
||||
let missing = dir.join("missing.json");
|
||||
let config = FirewallConfig::load_from(&[user, system, missing]).expect("loads");
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
assert_eq!(config.min_age_hours, 24);
|
||||
assert!(!config.fail_open);
|
||||
let names: Vec<&str> = config.allow["npm"].iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["a", "b"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_allowed_version_skips_the_filters() {
|
||||
// An unreachable denylist would otherwise leave the package unchecked.
|
||||
|
||||
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"fill" : {
|
||||
"solid" : "srgb:0.07000,0.07000,0.07000,1.00000"
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"layers" : [
|
||||
{
|
||||
"image-name" : "logo.png",
|
||||
"name" : "logo",
|
||||
"position" : {
|
||||
"scale" : 1.8,
|
||||
"translation-in-points" : [0, 0]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"squares" : [
|
||||
"macOS"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"images": [
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "16x16",
|
||||
"scale": "1x",
|
||||
"filename": "icon_16.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "16x16",
|
||||
"scale": "2x",
|
||||
"filename": "icon_32.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "32x32",
|
||||
"scale": "1x",
|
||||
"filename": "icon_32.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "32x32",
|
||||
"scale": "2x",
|
||||
"filename": "icon_64.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "128x128",
|
||||
"scale": "1x",
|
||||
"filename": "icon_128.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "128x128",
|
||||
"scale": "2x",
|
||||
"filename": "icon_256.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "256x256",
|
||||
"scale": "1x",
|
||||
"filename": "icon_256.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "256x256",
|
||||
"scale": "2x",
|
||||
"filename": "icon_512.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "512x512",
|
||||
"scale": "1x",
|
||||
"filename": "icon_512.png"
|
||||
},
|
||||
{
|
||||
"idiom": "mac",
|
||||
"size": "512x512",
|
||||
"scale": "2x",
|
||||
"filename": "icon_1024.png"
|
||||
}
|
||||
],
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
@@ -114,22 +114,6 @@ final class FirewallModel: NSObject, ObservableObject {
|
||||
|
||||
completed = true
|
||||
log("Setup complete")
|
||||
offerRestart()
|
||||
}
|
||||
|
||||
/// Apps opened before setup, terminals included, only get the environment the env agent sets once reopened.
|
||||
private func offerRestart() {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Restart to finish setting up"
|
||||
alert.informativeText = "Apps that were already open, such as terminals, can't install packages until they're reopened. Restarting reopens them all."
|
||||
alert.addButton(withTitle: "Restart")
|
||||
alert.addButton(withTitle: "Later")
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
guard alert.runModal() == .alertFirstButtonReturn else { return }
|
||||
let osascript = Process()
|
||||
osascript.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
|
||||
osascript.arguments = ["-e", "tell application \"loginwindow\" to «event aevtrest»"]
|
||||
try? osascript.run()
|
||||
}
|
||||
|
||||
/// After the admin gate, every stage runs even if an earlier one fails, so a partial uninstall never strands the rest.
|
||||
|
||||