mirror of
https://github.com/usestrix/firewall.git
synced 2026-11-12 19:14:50 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5df8e11459 | ||
|
|
bf0d8d5be8 | ||
|
|
6fb4db7492 |
No files matched your search
@@ -8,7 +8,7 @@ pub static BUN: Manager = Manager {
|
|||||||
("https_proxy", PROXY),
|
("https_proxy", PROXY),
|
||||||
("HTTP_PROXY", PROXY),
|
("HTTP_PROXY", PROXY),
|
||||||
("HTTPS_PROXY", PROXY),
|
("HTTPS_PROXY", PROXY),
|
||||||
("npm_config_registry", "http://registry.npmjs.org/"),
|
("npm_config_registry", "http://{npm}/"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
};
|
};
|
||||||
@@ -14,7 +14,7 @@ pub static CARGO: Manager = Manager {
|
|||||||
"--config",
|
"--config",
|
||||||
"source.crates-io.replace-with=\"firewall\"",
|
"source.crates-io.replace-with=\"firewall\"",
|
||||||
"--config",
|
"--config",
|
||||||
"source.firewall.registry=\"sparse+http://index.crates.io/\"",
|
"source.firewall.registry=\"sparse+http://{cargo/index}/\"",
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,6 @@ pub static GEM: Manager = Manager {
|
|||||||
// gem has no variable for the source list, and gem env and gem sources reject the flags.
|
// gem has no variable for the source list, and gem env and gem sources reject the flags.
|
||||||
args: &[Args {
|
args: &[Args {
|
||||||
when: &["install", "update", "fetch"],
|
when: &["install", "update", "fetch"],
|
||||||
flags: &["--clear-sources", "--source", "http://rubygems.org/"],
|
flags: &["--clear-sources", "--source", "http://{gem}/"],
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ pub static GO: Manager = Manager {
|
|||||||
env: &[
|
env: &[
|
||||||
("HTTP_PROXY", PROXY),
|
("HTTP_PROXY", PROXY),
|
||||||
("http_proxy", PROXY),
|
("http_proxy", PROXY),
|
||||||
("GOPROXY", "http://proxy.golang.org"),
|
("GOPROXY", "http://{go}"),
|
||||||
("GOSUMDB", "off"),
|
("GOSUMDB", "off"),
|
||||||
("GOFLAGS", "-mod=mod"),
|
("GOFLAGS", "-mod=mod"),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ mod pipenv;
|
|||||||
mod pnpm;
|
mod pnpm;
|
||||||
mod uv;
|
mod uv;
|
||||||
|
|
||||||
|
use crate::policies::registries::{host_of, BUILT_IN};
|
||||||
|
use crate::FirewallConfig;
|
||||||
|
|
||||||
pub use bun::BUN;
|
pub use bun::BUN;
|
||||||
pub use cargo::CARGO;
|
pub use cargo::CARGO;
|
||||||
pub use gem::GEM;
|
pub use gem::GEM;
|
||||||
@@ -20,7 +23,7 @@ pub use pipenv::PIPENV;
|
|||||||
pub use pnpm::PNPM;
|
pub use pnpm::PNPM;
|
||||||
pub use uv::UV;
|
pub use uv::UV;
|
||||||
|
|
||||||
pub const PYPI_SIMPLE: &str = "http://pypi.org/simple";
|
pub const PYPI_SIMPLE: &str = "http://{pypi}/simple";
|
||||||
|
|
||||||
pub const PROXY: &str = "{proxy}";
|
pub const PROXY: &str = "{proxy}";
|
||||||
|
|
||||||
@@ -31,7 +34,9 @@ pub struct Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// An https:// registry arrives as a CONNECT tunnel naming only the host, so each
|
/// An https:// registry arrives as a CONNECT tunnel naming only the host, so each
|
||||||
/// manager also overrides its registry URL to http://.
|
/// manager also overrides its registry URL to http://. `{npm}` and the like name a
|
||||||
|
/// built-in registry, and expand to its first mirror, if any, without the scheme;
|
||||||
|
/// `{host:npm}` to just its host.
|
||||||
pub struct Manager {
|
pub struct Manager {
|
||||||
pub command: &'static str,
|
pub command: &'static str,
|
||||||
pub env: &'static [(&'static str, &'static str)],
|
pub env: &'static [(&'static str, &'static str)],
|
||||||
@@ -42,23 +47,36 @@ pub static MANAGERS: &[&Manager] =
|
|||||||
&[&NPM, &PIP, &PIP3, &CARGO, &GO, &GEM, &UV, &PDM, &PIPENV, &PNPM, &BUN];
|
&[&NPM, &PIP, &PIP3, &CARGO, &GO, &GEM, &UV, &PDM, &PIPENV, &PNPM, &BUN];
|
||||||
|
|
||||||
/// Every manager's variables, so whichever the command starts is pointed at the firewall.
|
/// Every manager's variables, so whichever the command starts is pointed at the firewall.
|
||||||
pub fn variables(proxy: &str) -> Vec<(String, String)> {
|
pub fn variables(proxy: &str, config: &FirewallConfig) -> Vec<(String, String)> {
|
||||||
let env = MANAGERS.iter().flat_map(|manager| manager.env);
|
let env = MANAGERS.iter().flat_map(|manager| manager.env);
|
||||||
env.map(|(key, value)| (key.to_string(), expand(value, proxy))).collect()
|
env.map(|(key, value)| (key.to_string(), expand(value, proxy, config))).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arguments(proxy: &str, manager: &Manager, typed: &[String]) -> Vec<String> {
|
pub fn arguments(
|
||||||
|
proxy: &str,
|
||||||
|
config: &FirewallConfig,
|
||||||
|
manager: &Manager,
|
||||||
|
typed: &[String],
|
||||||
|
) -> Vec<String> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for args in manager.args {
|
for args in manager.args {
|
||||||
let wanted =
|
let wanted =
|
||||||
args.when.is_empty() || typed.iter().any(|arg| args.when.contains(&arg.as_str()));
|
args.when.is_empty() || typed.iter().any(|arg| args.when.contains(&arg.as_str()));
|
||||||
if wanted {
|
if wanted {
|
||||||
out.extend(args.flags.iter().map(|flag| expand(flag, proxy)));
|
out.extend(args.flags.iter().map(|flag| expand(flag, proxy, config)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expand(value: &str, proxy: &str) -> String {
|
fn expand(value: &str, proxy: &str, config: &FirewallConfig) -> String {
|
||||||
value.replace(PROXY, proxy)
|
let mut value = value.replace(PROXY, proxy);
|
||||||
|
for registry in BUILT_IN {
|
||||||
|
let mirror = config.registries.get(registry.name()).and_then(|mirrors| mirrors.first());
|
||||||
|
let upstream = mirror.map_or(registry.upstream, String::as_str);
|
||||||
|
let address = upstream.trim_start_matches("https://").trim_end_matches('/');
|
||||||
|
value = value.replace(&format!("{{{}}}", registry.name()), address);
|
||||||
|
value = value.replace(&format!("{{host:{}}}", registry.name()), host_of(upstream));
|
||||||
|
}
|
||||||
|
value
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ pub static NPM: Manager = Manager {
|
|||||||
env: &[
|
env: &[
|
||||||
("npm_config_proxy", PROXY),
|
("npm_config_proxy", PROXY),
|
||||||
("npm_config_https_proxy", PROXY),
|
("npm_config_https_proxy", PROXY),
|
||||||
("npm_config_registry", "http://registry.npmjs.org/"),
|
("npm_config_registry", "http://{npm}/"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
};
|
};
|
||||||
@@ -4,7 +4,7 @@ use super::{Manager, PROXY, PYPI_SIMPLE};
|
|||||||
const PIP_ENV: &[(&str, &str)] = &[
|
const PIP_ENV: &[(&str, &str)] = &[
|
||||||
("PIP_PROXY", PROXY),
|
("PIP_PROXY", PROXY),
|
||||||
("PIP_INDEX_URL", PYPI_SIMPLE),
|
("PIP_INDEX_URL", PYPI_SIMPLE),
|
||||||
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
|
("PIP_TRUSTED_HOST", "{host:pypi} {host:pypi-files}"),
|
||||||
("PIP_DISABLE_PIP_VERSION_CHECK", "1"),
|
("PIP_DISABLE_PIP_VERSION_CHECK", "1"),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ pub static PIPENV: Manager = Manager {
|
|||||||
("https_proxy", PROXY),
|
("https_proxy", PROXY),
|
||||||
("PIPENV_PYPI_MIRROR", PYPI_SIMPLE),
|
("PIPENV_PYPI_MIRROR", PYPI_SIMPLE),
|
||||||
// pipenv installs through pip, which skips a plaintext index on a host it doesn't trust.
|
// pipenv installs through pip, which skips a plaintext index on a host it doesn't trust.
|
||||||
("PIP_TRUSTED_HOST", "pypi.org files.pythonhosted.org"),
|
("PIP_TRUSTED_HOST", "{host:pypi} {host:pypi-files}"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
};
|
};
|
||||||
@@ -6,7 +6,7 @@ pub static PNPM: Manager = Manager {
|
|||||||
env: &[
|
env: &[
|
||||||
("pnpm_config_proxy", PROXY),
|
("pnpm_config_proxy", PROXY),
|
||||||
("pnpm_config_https_proxy", PROXY),
|
("pnpm_config_https_proxy", PROXY),
|
||||||
("pnpm_config_registry", "http://registry.npmjs.org/"),
|
("pnpm_config_registry", "http://{npm}/"),
|
||||||
],
|
],
|
||||||
args: &[],
|
args: &[],
|
||||||
};
|
};
|
||||||
@@ -6,6 +6,7 @@ mod windows;
|
|||||||
pub use forward::spawn;
|
pub use forward::spawn;
|
||||||
pub use managers::*;
|
pub use managers::*;
|
||||||
|
|
||||||
|
use crate::FirewallConfig;
|
||||||
use std::io::{Error, ErrorKind};
|
use std::io::{Error, ErrorKind};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -21,12 +22,12 @@ impl Invocation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// What the program is run with: the typed arguments plus any its manager needs.
|
/// What the program is run with: the typed arguments plus any its manager needs.
|
||||||
pub fn arguments(&self, proxy: &str) -> Vec<String> {
|
pub fn arguments(&self, proxy: &str, config: &FirewallConfig) -> Vec<String> {
|
||||||
let mut args = self.args.clone();
|
let mut args = self.args.clone();
|
||||||
let Some(manager) = MANAGERS.iter().find(|manager| manager.command == self.program) else {
|
let Some(manager) = MANAGERS.iter().find(|manager| manager.command == self.program) else {
|
||||||
return args;
|
return args;
|
||||||
};
|
};
|
||||||
let ours = managers::arguments(proxy, manager, &args);
|
let ours = managers::arguments(proxy, config, manager, &args);
|
||||||
// Anything after `--` belongs to another program, so ours go before it.
|
// Anything after `--` belongs to another program, so ours go before it.
|
||||||
let at = args.iter().position(|arg| arg == "--").unwrap_or(args.len());
|
let at = args.iter().position(|arg| arg == "--").unwrap_or(args.len());
|
||||||
args.splice(at..at, ours);
|
args.splice(at..at, ours);
|
||||||
@@ -34,15 +35,20 @@ impl Invocation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(port: u16, invocation: &Invocation, dir: Option<&Path>) -> std::io::Result<i32> {
|
pub fn run(
|
||||||
|
port: u16,
|
||||||
|
config: &FirewallConfig,
|
||||||
|
invocation: &Invocation,
|
||||||
|
dir: Option<&Path>,
|
||||||
|
) -> std::io::Result<i32> {
|
||||||
let proxy = crate::local_url(port, "");
|
let proxy = crate::local_url(port, "");
|
||||||
let program = resolve(&invocation.program)?;
|
let program = resolve(&invocation.program)?;
|
||||||
let mut command = Command::new(&program);
|
let mut command = Command::new(&program);
|
||||||
command.args(invocation.arguments(&proxy));
|
command.args(invocation.arguments(&proxy, config));
|
||||||
if let Some(dir) = dir {
|
if let Some(dir) = dir {
|
||||||
command.current_dir(dir);
|
command.current_dir(dir);
|
||||||
}
|
}
|
||||||
for (key, value) in managers::variables(&proxy) {
|
for (key, value) in managers::variables(&proxy, config) {
|
||||||
command.env(key, value);
|
command.env(key, value);
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
|
|||||||
+55
-25
@@ -1,17 +1,19 @@
|
|||||||
use crate::policies::registries::{Registry, BUILT_IN};
|
use crate::policies::registries::{host_of, BUILT_IN};
|
||||||
use directories::ProjectDirs;
|
use directories::ProjectDirs;
|
||||||
|
use rama::net::uri::Uri;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::{Error, ErrorKind};
|
use std::io::{Error, ErrorKind};
|
||||||
|
use std::num::{NonZeroU16, NonZeroU64};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub const DEFAULT_PORT: u16 = 8080;
|
pub const DEFAULT_PORT: u16 = 8080;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct FirewallConfig {
|
pub struct FirewallConfig {
|
||||||
pub port: u16,
|
pub port: NonZeroU16,
|
||||||
/// Allow a package when a filter could not check it.
|
/// Allow a package when a filter could not check it.
|
||||||
pub fail_open: bool,
|
pub fail_open: bool,
|
||||||
/// 0 turns the age check off.
|
/// 0 turns the age check off.
|
||||||
@@ -20,12 +22,11 @@ pub struct FirewallConfig {
|
|||||||
/// Packages exempt from the age check, by ecosystem: `{"npm": [{"name": "left-pad"}]}`.
|
/// Packages exempt from the age check, by ecosystem: `{"npm": [{"name": "left-pad"}]}`.
|
||||||
pub allow: HashMap<String, Vec<AllowedPackage>>,
|
pub allow: HashMap<String, Vec<AllowedPackage>>,
|
||||||
pub telemetry: bool,
|
pub telemetry: bool,
|
||||||
/// Not describable in the file yet, so this is always the built-in table.
|
/// Mirrors laid out like a built-in registry, by its name: `{"npm": ["https://npm.example.com/"]}`.
|
||||||
#[serde(skip)]
|
pub registries: HashMap<String, Vec<String>>,
|
||||||
pub registries: Vec<&'static Registry>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct AllowedPackage {
|
pub struct AllowedPackage {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -40,19 +41,19 @@ pub struct DenylistSource {
|
|||||||
pub base: String,
|
pub base: String,
|
||||||
/// Polled for a new commit, which re-fetches every list in use.
|
/// Polled for a new commit, which re-fetches every list in use.
|
||||||
pub revision: String,
|
pub revision: String,
|
||||||
pub poll_secs: u64,
|
pub poll_secs: NonZeroU64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FirewallConfig {
|
impl Default for FirewallConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
FirewallConfig {
|
FirewallConfig {
|
||||||
port: DEFAULT_PORT,
|
port: NonZeroU16::new(DEFAULT_PORT).unwrap(),
|
||||||
fail_open: true,
|
fail_open: true,
|
||||||
min_age_hours: 24,
|
min_age_hours: 24,
|
||||||
denylist: DenylistSource::default(),
|
denylist: DenylistSource::default(),
|
||||||
allow: HashMap::new(),
|
allow: HashMap::new(),
|
||||||
telemetry: true,
|
telemetry: true,
|
||||||
registries: BUILT_IN.to_vec(),
|
registries: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +64,7 @@ impl Default for DenylistSource {
|
|||||||
base: "https://raw.githubusercontent.com/usestrix/firewall-blocklist/main".into(),
|
base: "https://raw.githubusercontent.com/usestrix/firewall-blocklist/main".into(),
|
||||||
revision: "https://api.github.com/repos/usestrix/firewall-blocklist/commits/main"
|
revision: "https://api.github.com/repos/usestrix/firewall-blocklist/commits/main"
|
||||||
.into(),
|
.into(),
|
||||||
poll_secs: 10,
|
poll_secs: NonZeroU64::new(10).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,8 @@ impl FirewallConfig {
|
|||||||
};
|
};
|
||||||
let invalid = |e| Error::new(ErrorKind::InvalidData, 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)?;
|
let layer: Value = serde_json::from_str(&text).map_err(invalid)?;
|
||||||
FirewallConfig::deserialize(&layer).map_err(invalid)?;
|
let config = FirewallConfig::deserialize(&layer).map_err(invalid)?;
|
||||||
|
check(&config).map_err(|e| Error::new(ErrorKind::InvalidData, format!("{}: {e}", path.display())))?;
|
||||||
let Value::Object(layer) = layer else {
|
let Value::Object(layer) = layer else {
|
||||||
unreachable!("a config parses only from an object");
|
unreachable!("a config parses only from an object");
|
||||||
};
|
};
|
||||||
@@ -97,19 +99,7 @@ impl FirewallConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn local_url(&self, path: &str) -> String {
|
pub fn local_url(&self, path: &str) -> String {
|
||||||
crate::local_url(self.port, path)
|
crate::local_url(self.port.get(), path)
|
||||||
}
|
|
||||||
|
|
||||||
/// Without building a `Firewall`, which needs a runtime.
|
|
||||||
pub fn hosts(&self) -> Vec<&'static str> {
|
|
||||||
let mut hosts: Vec<&'static str> = Vec::new();
|
|
||||||
for registry in &self.registries {
|
|
||||||
let host = registry.host();
|
|
||||||
if !hosts.contains(&host) {
|
|
||||||
hosts.push(host);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hosts
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,3 +147,43 @@ pub fn allow(path: &Path, eco: &str, name: &str, version: Option<&str>) -> std::
|
|||||||
}
|
}
|
||||||
std::fs::write(path, serde_json::to_string_pretty(&config)?)
|
std::fs::write(path, serde_json::to_string_pretty(&config)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What serde's types cannot say.
|
||||||
|
fn check(config: &FirewallConfig) -> Result<(), String> {
|
||||||
|
// `Duration::hours` panics past this.
|
||||||
|
if config.min_age_hours > time::Duration::MAX.whole_hours() as u64 {
|
||||||
|
return Err(format!("{} hours is too long", config.min_age_hours));
|
||||||
|
}
|
||||||
|
for eco in config.allow.keys() {
|
||||||
|
if !BUILT_IN.iter().any(|r| r.eco == eco) {
|
||||||
|
return Err(format!("unknown ecosystem {eco:?} in allow"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for url in [&config.denylist.base, &config.denylist.revision] {
|
||||||
|
if parse_url(url, &["http", "https"]).is_none() {
|
||||||
|
return Err(format!("{url:?} is not an http(s) URL"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (name, mirrors) in &config.registries {
|
||||||
|
if !BUILT_IN.iter().any(|r| r.name() == name) {
|
||||||
|
return Err(format!("no built-in registry is named {name}"));
|
||||||
|
}
|
||||||
|
if let Some(url) = mirrors.iter().find(|url| !is_mirror(url)) {
|
||||||
|
return Err(format!("{url}: a mirror is an https:// URL ending in /"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// No port or userinfo, as `host_of` is what gets intercepted.
|
||||||
|
fn is_mirror(url: &str) -> bool {
|
||||||
|
let Some(uri) = parse_url(url, &["https"]) else { return false };
|
||||||
|
let bare_host = uri.host_str().is_some_and(|host| host == host_of(url));
|
||||||
|
bare_host && uri.query().is_none() && uri.fragment().is_none() && url.ends_with('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_url(url: &str, schemes: &[&str]) -> Option<Uri> {
|
||||||
|
let uri = Uri::parse(url).ok()?;
|
||||||
|
let has_host = uri.host_str().is_some_and(|host| !host.is_empty());
|
||||||
|
(has_host && schemes.contains(&uri.scheme_str()?)).then_some(uri)
|
||||||
|
}
|
||||||
+28
-7
@@ -9,6 +9,7 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
pub struct Firewall {
|
pub struct Firewall {
|
||||||
pub config: FirewallConfig,
|
pub config: FirewallConfig,
|
||||||
|
registries: Vec<&'static Registry>,
|
||||||
patterns: HashMap<&'static str, Vec<Regex>>,
|
patterns: HashMap<&'static str, Vec<Regex>>,
|
||||||
filters: Vec<Box<dyn Filter>>,
|
filters: Vec<Box<dyn Filter>>,
|
||||||
}
|
}
|
||||||
@@ -21,18 +22,19 @@ impl Default for Firewall {
|
|||||||
|
|
||||||
impl Firewall {
|
impl Firewall {
|
||||||
pub fn new(config: FirewallConfig) -> Self {
|
pub fn new(config: FirewallConfig) -> Self {
|
||||||
|
let registries = with_mirrors(&config.registries);
|
||||||
let mut patterns = HashMap::new();
|
let mut patterns = HashMap::new();
|
||||||
for registry in &config.registries {
|
for registry in ®istries {
|
||||||
let compiled = registry.patterns.iter().map(|p| Regex::new(p.re).expect(p.re));
|
let compiled = registry.patterns.iter().map(|p| Regex::new(p.re).expect(p.re));
|
||||||
patterns.insert(registry.prefix, compiled.collect());
|
patterns.insert(registry.prefix, compiled.collect());
|
||||||
}
|
}
|
||||||
let ecosystems = config.registries.iter().map(|r| r.osv_name);
|
let ecosystems = registries.iter().map(|r| r.osv_name);
|
||||||
let denylist = Denylist::new(config.denylist.clone(), ecosystems);
|
let denylist = Denylist::new(config.denylist.clone(), ecosystems);
|
||||||
let mut filters: Vec<Box<dyn Filter>> = vec![Box::new(denylist), Box::new(Osv)];
|
let mut filters: Vec<Box<dyn Filter>> = vec![Box::new(denylist), Box::new(Osv)];
|
||||||
if config.min_age_hours > 0 {
|
if config.min_age_hours > 0 {
|
||||||
filters.push(Box::new(Age::new(config.min_age_hours)));
|
filters.push(Box::new(Age::new(config.min_age_hours)));
|
||||||
}
|
}
|
||||||
Firewall { config, patterns, filters }
|
Firewall { config, registries, patterns, filters }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn match_on<'a>(
|
pub fn match_on<'a>(
|
||||||
@@ -40,20 +42,27 @@ impl Firewall {
|
|||||||
url: &'a str,
|
url: &'a str,
|
||||||
key: fn(&Registry) -> &'static str,
|
key: fn(&Registry) -> &'static str,
|
||||||
) -> Option<(&'static Registry, &'a str)> {
|
) -> Option<(&'static Registry, &'a str)> {
|
||||||
self.config.registries.iter().find_map(|r| url.strip_prefix(key(r)).map(|rest| (*r, rest)))
|
self.registries.iter().find_map(|r| url.strip_prefix(key(r)).map(|rest| (*r, rest)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hosts(&self) -> Vec<&'static str> {
|
pub fn hosts(&self) -> Vec<&'static str> {
|
||||||
self.config.hosts()
|
let mut hosts: Vec<&'static str> = Vec::new();
|
||||||
|
for registry in &self.registries {
|
||||||
|
let host = registry.host();
|
||||||
|
if !hosts.contains(&host) {
|
||||||
|
hosts.push(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hosts
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_https_url(&self, url: &str) -> bool {
|
pub fn is_https_url(&self, url: &str) -> bool {
|
||||||
let host = host_of(url);
|
let host = host_of(url);
|
||||||
self.config.registries.iter().any(|r| r.upstream.starts_with("https://") && r.host() == host)
|
self.registries.iter().any(|r| r.upstream.starts_with("https://") && r.host() == host)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_index_url(&self, url: &str) -> bool {
|
pub fn is_index_url(&self, url: &str) -> bool {
|
||||||
self.config.registries.iter().any(|r| r.index && r.host() == host_of(url))
|
self.registries.iter().any(|r| r.index && r.host() == host_of(url))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve(&self, registry: &'static Registry, rest: &str) -> Resolved {
|
pub fn resolve(&self, registry: &'static Registry, rest: &str) -> Resolved {
|
||||||
@@ -64,3 +73,15 @@ impl Firewall {
|
|||||||
&self.filters
|
&self.filters
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The built-ins, then a copy of one per mirror. Leaked, as a `Registry` is `'static` everywhere.
|
||||||
|
fn with_mirrors(mirrors: &HashMap<String, Vec<String>>) -> Vec<&'static Registry> {
|
||||||
|
let mut registries = BUILT_IN.to_vec();
|
||||||
|
for built_in in BUILT_IN {
|
||||||
|
for upstream in mirrors.get(built_in.name()).into_iter().flatten() {
|
||||||
|
let upstream = upstream.clone().leak();
|
||||||
|
registries.push(Box::leak(Box::new(Registry { upstream, ..(*built_in).clone() })));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registries
|
||||||
|
}
|
||||||
+3
-2
@@ -73,13 +73,14 @@ fn serve() -> ! {
|
|||||||
fn run(invocation: Invocation) -> ! {
|
fn run(invocation: Invocation) -> ! {
|
||||||
telemetry::set_mode("wrapper");
|
telemetry::set_mode("wrapper");
|
||||||
let command = &invocation.program;
|
let command = &invocation.program;
|
||||||
let port = match aliasing::spawn(0, load()) {
|
let config = load();
|
||||||
|
let port = match aliasing::spawn(0, config.clone()) {
|
||||||
Ok(port) => port,
|
Ok(port) => port,
|
||||||
Err(e) => fail("spawn", &format!("could not start the firewall: {e}")),
|
Err(e) => fail("spawn", &format!("could not start the firewall: {e}")),
|
||||||
};
|
};
|
||||||
info!(url = %firewall::local_url(port, ""), "firewall listening");
|
info!(url = %firewall::local_url(port, ""), "firewall listening");
|
||||||
|
|
||||||
let result = aliasing::run(port, &invocation, None);
|
let result = aliasing::run(port, &config, &invocation, None);
|
||||||
telemetry::flush();
|
telemetry::flush();
|
||||||
match result {
|
match result {
|
||||||
Ok(code) => exit(code),
|
Ok(code) => exit(code),
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ pub fn describe(advisories: &[&Advisory]) -> String {
|
|||||||
|
|
||||||
/// A failed fetch leaves the last good list in place; a list that never loaded is retried every tick.
|
/// A failed fetch leaves the last good list in place; a list that never loaded is retried every tick.
|
||||||
async fn watch(source: DenylistSource, lists: Arc<Lists>) {
|
async fn watch(source: DenylistSource, lists: Arc<Lists>) {
|
||||||
let period = Duration::from_secs(source.poll_secs);
|
let period = Duration::from_secs(source.poll_secs.get());
|
||||||
let mut seen = revision(&source, None).await;
|
let mut seen = revision(&source, None).await;
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(period).await;
|
tokio::time::sleep(period).await;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub struct Pattern {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Requests to the local server match on `prefix`; intercepted URLs match on `upstream`.
|
/// Requests to the local server match on `prefix`; intercepted URLs match on `upstream`.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Registry {
|
pub struct Registry {
|
||||||
pub prefix: &'static str,
|
pub prefix: &'static str,
|
||||||
pub eco: &'static str,
|
pub eco: &'static str,
|
||||||
@@ -52,7 +52,7 @@ pub struct Registry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A URL to fetch and a JSON pointer to the timestamp in its reply.
|
/// A URL to fetch and a JSON pointer to the timestamp in its reply.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Published {
|
pub struct Published {
|
||||||
pub url: &'static str,
|
pub url: &'static str,
|
||||||
pub pointer: &'static str,
|
pub pointer: &'static str,
|
||||||
@@ -63,6 +63,11 @@ impl Registry {
|
|||||||
pub fn host(&self) -> &'static str {
|
pub fn host(&self) -> &'static str {
|
||||||
host_of(self.upstream)
|
host_of(self.upstream)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the config file calls it.
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
self.prefix.trim_matches('/')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The host of an https:// URL, with the path dropped.
|
/// The host of an https:// URL, with the path dropped.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use std::sync::Arc;
|
|||||||
use tracing::{info, info_span, warn, Instrument};
|
use tracing::{info, info_span, warn, Instrument};
|
||||||
|
|
||||||
pub fn serve(config: FirewallConfig) -> std::io::Result<()> {
|
pub fn serve(config: FirewallConfig) -> std::io::Result<()> {
|
||||||
let bound = std::net::TcpListener::bind(("127.0.0.1", config.port))?;
|
let bound = std::net::TcpListener::bind(("127.0.0.1", config.port.get()))?;
|
||||||
bound.set_nonblocking(true)?;
|
bound.set_nonblocking(true)?;
|
||||||
crate::runtime()?.block_on(async move {
|
crate::runtime()?.block_on(async move {
|
||||||
let firewall = Arc::new(Firewall::new(config));
|
let firewall = Arc::new(Firewall::new(config));
|
||||||
|
|||||||
+38
-1
@@ -7,7 +7,7 @@ use firewall::{Firewall, FirewallConfig};
|
|||||||
fn a_file_only_has_to_set_what_it_changes() {
|
fn a_file_only_has_to_set_what_it_changes() {
|
||||||
let config: FirewallConfig = serde_json::from_str(r#"{"fail_open": false}"#).expect("parses");
|
let config: FirewallConfig = serde_json::from_str(r#"{"fail_open": false}"#).expect("parses");
|
||||||
assert!(!config.fail_open);
|
assert!(!config.fail_open);
|
||||||
assert_eq!(config.port, firewall::DEFAULT_PORT);
|
assert_eq!(config.port.get(), firewall::DEFAULT_PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -15,6 +15,28 @@ fn a_misspelled_setting_is_an_error() {
|
|||||||
assert!(serde_json::from_str::<FirewallConfig>(r#"{"fail-open": false}"#).is_err());
|
assert!(serde_json::from_str::<FirewallConfig>(r#"{"fail-open": false}"#).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonsense_values_are_errors() {
|
||||||
|
let file = std::env::temp_dir().join(format!("firewall-nonsense-{}.json", std::process::id()));
|
||||||
|
for bad in [
|
||||||
|
r#"{"allow": {"nmp": [{"name": "left-pad"}]}}"#,
|
||||||
|
r#"{"denylist": {"poll_secs": 0}}"#,
|
||||||
|
r#"{"denylist": {"base": "raw.githubusercontent.com/usestrix"}}"#,
|
||||||
|
r#"{"denylist": {"revision": "https://"}}"#,
|
||||||
|
r#"{"port": 0}"#,
|
||||||
|
r#"{"min_age_hours": 18446744073709551615}"#,
|
||||||
|
r#"{"registries": {"npm": ["http://npm.example.com/"]}}"#,
|
||||||
|
r#"{"registries": {"npm": ["https://npm.example.com"]}}"#,
|
||||||
|
r#"{"registries": {"npm": ["https://npm.example.com:8443/"]}}"#,
|
||||||
|
r#"{"registries": {"npm": ["https://[email protected]/"]}}"#,
|
||||||
|
r#"{"registries": {"npm": ["https://npm.example.com/?a=/"]}}"#,
|
||||||
|
] {
|
||||||
|
std::fs::write(&file, bad).expect("write");
|
||||||
|
assert!(FirewallConfig::load_from(std::slice::from_ref(&file)).is_err(), "{bad}");
|
||||||
|
}
|
||||||
|
std::fs::remove_file(&file).ok();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_config_lives_under_the_config_directory() {
|
fn the_config_lives_under_the_config_directory() {
|
||||||
let path = path().expect("a home directory");
|
let path = path().expect("a home directory");
|
||||||
@@ -38,6 +60,21 @@ fn a_later_file_wins_and_allow_lists_add_up() {
|
|||||||
assert_eq!(names, ["a", "b"]);
|
assert_eq!(names, ["a", "b"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_mirror_is_screened_like_the_registry_it_copies() {
|
||||||
|
let config = r#"{"registries": {"npm": ["https://npm.example.com/"]}}"#;
|
||||||
|
let firewall = Firewall::new(serde_json::from_str(config).expect("parses"));
|
||||||
|
assert!(firewall.hosts().contains(&"npm.example.com"));
|
||||||
|
let url = "https://npm.example.com/left-pad/-/left-pad-1.3.0.tgz";
|
||||||
|
let (registry, rest) = firewall.match_on(url, |r| r.upstream).expect("the mirror");
|
||||||
|
assert_eq!(firewall.resolve(registry, rest).pkg.map(|p| p.label()), Some("[email protected]".into()));
|
||||||
|
let unknown = std::env::temp_dir().join(format!("firewall-mirror-{}.json", std::process::id()));
|
||||||
|
std::fs::write(&unknown, r#"{"registries": {"nmp": ["https://npm.example.com/"]}}"#).expect("write");
|
||||||
|
let loaded = FirewallConfig::load_from(std::slice::from_ref(&unknown));
|
||||||
|
std::fs::remove_file(&unknown).ok();
|
||||||
|
assert!(loaded.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn an_allowed_version_is_still_checked_for_malware() {
|
async fn an_allowed_version_is_still_checked_for_malware() {
|
||||||
// The unreachable denylist shows that it still runs.
|
// The unreachable denylist shows that it still runs.
|
||||||
|
|||||||
+26
-13
@@ -1,11 +1,11 @@
|
|||||||
use firewall::aliasing::managers::*;
|
use firewall::aliasing::managers::*;
|
||||||
use firewall::aliasing::Invocation;
|
use firewall::aliasing::Invocation;
|
||||||
use firewall::Firewall;
|
use firewall::{Firewall, FirewallConfig};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arguments_after_a_double_dash_reach_the_program() {
|
fn arguments_after_a_double_dash_reach_the_program() {
|
||||||
let typed = ["run", "--", "--release"].map(String::from).to_vec();
|
let typed = ["run", "--", "--release"].map(String::from).to_vec();
|
||||||
let args = Invocation::new("cargo".into(), typed).arguments("http://127.0.0.1:9000");
|
let args = Invocation::new("cargo".into(), typed).arguments("http://127.0.0.1:9000", &FirewallConfig::default());
|
||||||
assert_eq!(args.first().map(String::as_str), Some("run"));
|
assert_eq!(args.first().map(String::as_str), Some("run"));
|
||||||
assert_eq!(args[args.len() - 2..], ["--", "--release"]);
|
assert_eq!(args[args.len() - 2..], ["--", "--release"]);
|
||||||
}
|
}
|
||||||
@@ -13,12 +13,23 @@ fn arguments_after_a_double_dash_reach_the_program() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn recipes_expand_into_the_environment() {
|
fn recipes_expand_into_the_environment() {
|
||||||
let var = |name: &str, value: &str| (name.to_string(), value.to_string());
|
let var = |name: &str, value: &str| (name.to_string(), value.to_string());
|
||||||
let npm = variables("http://127.0.0.1:9000");
|
let config = FirewallConfig::default();
|
||||||
|
let npm = variables("http://127.0.0.1:9000", &config);
|
||||||
assert!(npm.contains(&var("npm_config_proxy", "http://127.0.0.1:9000")));
|
assert!(npm.contains(&var("npm_config_proxy", "http://127.0.0.1:9000")));
|
||||||
assert!(npm.contains(&var("npm_config_registry", "http://registry.npmjs.org/")));
|
assert!(npm.contains(&var("npm_config_registry", "http://registry.npmjs.org/")));
|
||||||
let gem = arguments("http://127.0.0.1:9000", &GEM, &["install".to_string()]);
|
let gem = arguments("http://127.0.0.1:9000", &config, &GEM, &["install".to_string()]);
|
||||||
assert_eq!(gem, ["--clear-sources", "--source", "http://rubygems.org/"]);
|
assert_eq!(gem, ["--clear-sources", "--source", "http://rubygems.org/"]);
|
||||||
assert!(arguments("http://127.0.0.1:9000", &GEM, &["sources".to_string()]).is_empty());
|
assert!(arguments("http://127.0.0.1:9000", &config, &GEM, &["sources".to_string()]).is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_mirror_replaces_the_registry_it_copies() {
|
||||||
|
let var = |name: &str, value: &str| (name.to_string(), value.to_string());
|
||||||
|
let config = r#"{"registries": {"npm": ["https://npm.example.com/"], "pypi": ["https://art.example.com/pypi/"]}}"#;
|
||||||
|
let env = variables("http://127.0.0.1:9000", &serde_json::from_str(config).expect("parses"));
|
||||||
|
assert!(env.contains(&var("npm_config_registry", "http://npm.example.com/")));
|
||||||
|
assert!(env.contains(&var("PIP_INDEX_URL", "http://art.example.com/pypi/simple")));
|
||||||
|
assert!(env.contains(&var("PIP_TRUSTED_HOST", "art.example.com files.pythonhosted.org")));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -34,15 +45,17 @@ fn no_two_managers_set_a_variable_differently() {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn every_overridden_registry_is_known() {
|
async fn every_overridden_registry_is_known() {
|
||||||
|
let config = FirewallConfig::default();
|
||||||
let hosts = Firewall::default().hosts();
|
let hosts = Firewall::default().hosts();
|
||||||
for manager in MANAGERS {
|
let values = variables("", &config).into_iter().map(|(_, value)| value);
|
||||||
let values = manager.env.iter().map(|(_, value)| *value);
|
let flags = MANAGERS.iter().flat_map(|manager| {
|
||||||
let flags = manager.args.iter().flat_map(|args| args.flags.iter().copied());
|
let typed: Vec<String> = manager.args.iter().flat_map(|args| args.when).map(|arg| arg.to_string()).collect();
|
||||||
for value in values.chain(flags) {
|
arguments("", &config, manager, &typed)
|
||||||
for (_, after) in value.match_indices("http://").map(|(i, _)| value.split_at(i + 7)) {
|
});
|
||||||
let host = after.split(['/', '"']).next().unwrap_or(after);
|
for value in values.chain(flags) {
|
||||||
assert!(hosts.contains(&host), "{} points at {host}", manager.command);
|
for (_, after) in value.match_indices("http://").map(|(i, _)| value.split_at(i + 7)) {
|
||||||
}
|
let host = after.split(['/', '"']).next().unwrap_or(after);
|
||||||
|
assert!(hosts.contains(&host), "{value} points at {host}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,11 @@ use probe::Outcome;
|
|||||||
|
|
||||||
fn main() -> std::io::Result<()> {
|
fn main() -> std::io::Result<()> {
|
||||||
firewall::init_logging("info");
|
firewall::init_logging("info");
|
||||||
let port = aliasing::spawn(0, FirewallConfig::load()?)?;
|
let config = FirewallConfig::load()?;
|
||||||
|
let port = aliasing::spawn(0, config.clone())?;
|
||||||
println!("firewall proxy on {}\n", firewall::local_url(port, ""));
|
println!("firewall proxy on {}\n", firewall::local_url(port, ""));
|
||||||
|
|
||||||
let results = probe::all(port);
|
let results = probe::all(port, &config);
|
||||||
println!();
|
println!();
|
||||||
let mut failed = 0;
|
let mut failed = 0;
|
||||||
for (name, outcome) in &results {
|
for (name, outcome) in &results {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use firewall::aliasing::{self, Invocation, Manager, CARGO, GEM, GO, NPM, PIP, PIP3, UV};
|
use firewall::aliasing::{self, Invocation, Manager, CARGO, GEM, GO, NPM, PIP, PIP3, UV};
|
||||||
|
use firewall::FirewallConfig;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
struct Probe {
|
struct Probe {
|
||||||
@@ -29,14 +30,14 @@ static PROBES: &[Probe] = &[
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn all(port: u16) -> Vec<(&'static str, Outcome)> {
|
pub fn all(port: u16, config: &FirewallConfig) -> Vec<(&'static str, Outcome)> {
|
||||||
PROBES.iter().map(|probe| (probe.manager.command, run(probe, port))).collect()
|
PROBES.iter().map(|probe| (probe.manager.command, run(probe, port, config))).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(probe: &Probe, port: u16) -> Outcome {
|
fn run(probe: &Probe, port: u16, config: &FirewallConfig) -> Outcome {
|
||||||
let args = probe.args.iter().map(|arg| arg.to_string()).collect();
|
let args = probe.args.iter().map(|arg| arg.to_string()).collect();
|
||||||
let dir = probe.setup.and_then(|prepare| prepare());
|
let dir = probe.setup.and_then(|prepare| prepare());
|
||||||
match aliasing::run(port, &Invocation::new(probe.manager.command.into(), args), dir.as_deref()) {
|
match aliasing::run(port, config, &Invocation::new(probe.manager.command.into(), args), dir.as_deref()) {
|
||||||
Ok(0) => Outcome::Ok,
|
Ok(0) => Outcome::Ok,
|
||||||
Ok(_) => Outcome::Failed,
|
Ok(_) => Outcome::Failed,
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent,
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Outcome::Absent,
|
||||||
|
|||||||
Reference in new issue
Block a user