feat(unstable): --allow-net subdomain wildcards (#29327)

This commit is contained in:
Nayeem Rahman 2025-05-29 04:05:37 +01:00 committed by GitHub
parent cb23193f74
commit ab9673dcc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 377 additions and 54 deletions

View file

@ -6121,6 +6121,8 @@ fn unstable_args_parse(
} }
// TODO(bartlomieju): this should be factored out since these are configured via UNSTABLE_FEATURES // TODO(bartlomieju): this should be factored out since these are configured via UNSTABLE_FEATURES
flags.unstable_config.subdomain_wildcards =
matches.get_flag("unstable-subdomain-wildcards");
flags.unstable_config.bare_node_builtins = flags.unstable_config.bare_node_builtins =
matches.get_flag("unstable-bare-node-builtins"); matches.get_flag("unstable-bare-node-builtins");
flags.unstable_config.detect_cjs = matches.get_flag("unstable-detect-cjs"); flags.unstable_config.detect_cjs = matches.get_flag("unstable-detect-cjs");

View file

@ -5,6 +5,7 @@ use std::str::FromStr;
use deno_core::url::Url; use deno_core::url::Url;
use deno_runtime::deno_permissions::NetDescriptor; use deno_runtime::deno_permissions::NetDescriptor;
use deno_runtime::deno_permissions::UnstableSubdomainWildcards;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct ParsePortError(String); pub struct ParsePortError(String);
@ -32,7 +33,11 @@ pub fn validator(host_and_port: &str) -> Result<String, String> {
if Url::parse(&format!("internal://{host_and_port}")).is_ok() if Url::parse(&format!("internal://{host_and_port}")).is_ok()
|| host_and_port.parse::<IpAddr>().is_ok() || host_and_port.parse::<IpAddr>().is_ok()
|| host_and_port.parse::<BarePort>().is_ok() || host_and_port.parse::<BarePort>().is_ok()
|| NetDescriptor::parse(host_and_port).is_ok() || NetDescriptor::parse_for_list(
host_and_port,
UnstableSubdomainWildcards::Enabled,
)
.is_ok()
{ {
Ok(host_and_port.to_string()) Ok(host_and_port.to_string())
} else { } else {
@ -52,7 +57,11 @@ pub fn parse(paths: Vec<String>) -> clap::error::Result<Vec<String>> {
out.push(format!("{}:{}", host, port.0)); out.push(format!("{}:{}", host, port.0));
} }
} else { } else {
NetDescriptor::parse(&host_and_port).map_err(|e| { NetDescriptor::parse_for_list(
&host_and_port,
UnstableSubdomainWildcards::Enabled,
)
.map_err(|e| {
clap::Error::raw(clap::error::ErrorKind::InvalidValue, e.to_string()) clap::Error::raw(clap::error::ErrorKind::InvalidValue, e.to_string())
})?; })?;
out.push(host_and_port) out.push(host_and_port)
@ -120,6 +129,7 @@ mod tests {
let entries = svec![ let entries = svec![
"deno.land", "deno.land",
"deno.land:80", "deno.land:80",
"*.deno.land",
"[::]", "[::]",
"[::1]", "[::1]",
"127.0.0.1", "127.0.0.1",
@ -141,6 +151,7 @@ mod tests {
let expected = svec![ let expected = svec![
"deno.land", "deno.land",
"deno.land:80", "deno.land:80",
"*.deno.land",
"[::]", "[::]",
"[::1]", "[::1]",
"127.0.0.1", "127.0.0.1",

View file

@ -993,6 +993,11 @@ impl CliOptions {
&self.flags.unsafely_ignore_certificate_errors &self.flags.unsafely_ignore_certificate_errors
} }
pub fn unstable_subdomain_wildcards(&self) -> bool {
self.flags.unstable_config.subdomain_wildcards
|| self.workspace().has_unstable("subdomain-wildcards")
}
pub fn unstable_bare_node_builtins(&self) -> bool { pub fn unstable_bare_node_builtins(&self) -> bool {
self.flags.unstable_config.bare_node_builtins self.flags.unstable_config.bare_node_builtins
|| self.workspace().has_unstable("bare-node-builtins") || self.workspace().has_unstable("bare-node-builtins")

View file

@ -45,6 +45,7 @@ use deno_runtime::deno_fs;
use deno_runtime::deno_fs::RealFs; use deno_runtime::deno_fs::RealFs;
use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::Permissions;
use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::deno_permissions::UnstableSubdomainWildcards;
use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::rustls::RootCertStore;
use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_tls::RootCertStoreProvider;
use deno_runtime::deno_web::BlobStore; use deno_runtime::deno_web::BlobStore;
@ -930,7 +931,14 @@ impl CliFactory {
&self, &self,
) -> Result<&Arc<RuntimePermissionDescriptorParser<CliSys>>, AnyError> { ) -> Result<&Arc<RuntimePermissionDescriptorParser<CliSys>>, AnyError> {
self.services.permission_desc_parser.get_or_try_init(|| { self.services.permission_desc_parser.get_or_try_init(|| {
Ok(Arc::new(RuntimePermissionDescriptorParser::new(self.sys()))) Ok(Arc::new(RuntimePermissionDescriptorParser::new(
self.sys(),
if self.cli_options()?.unstable_subdomain_wildcards() {
UnstableSubdomainWildcards::Enabled
} else {
UnstableSubdomainWildcards::Disabled
},
)))
}) })
} }

View file

@ -190,6 +190,7 @@ pub fn resolve_npm_resolution_snapshot(
pub struct UnstableConfig { pub struct UnstableConfig {
// TODO(bartlomieju): remove in Deno 2.5 // TODO(bartlomieju): remove in Deno 2.5
pub legacy_flag_enabled: bool, // --unstable pub legacy_flag_enabled: bool, // --unstable
pub subdomain_wildcards: bool,
pub bare_node_builtins: bool, pub bare_node_builtins: bool,
pub detect_cjs: bool, pub detect_cjs: bool,
pub lazy_dynamic_imports: bool, pub lazy_dynamic_imports: bool,
@ -206,6 +207,10 @@ impl UnstableConfig {
} }
} }
maybe_set(
&mut self.subdomain_wildcards,
UNSTABLE_ENV_VAR_NAMES.subdomain_wildcards,
);
maybe_set( maybe_set(
&mut self.bare_node_builtins, &mut self.bare_node_builtins,
UNSTABLE_ENV_VAR_NAMES.bare_node_builtins, UNSTABLE_ENV_VAR_NAMES.bare_node_builtins,

View file

@ -66,6 +66,7 @@ use deno_runtime::deno_node::create_host_defined_options;
use deno_runtime::deno_node::NodeRequireLoader; use deno_runtime::deno_node::NodeRequireLoader;
use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::Permissions;
use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_permissions::PermissionsContainer;
use deno_runtime::deno_permissions::UnstableSubdomainWildcards;
use deno_runtime::deno_tls::rustls::RootCertStore; use deno_runtime::deno_tls::rustls::RootCertStore;
use deno_runtime::deno_tls::RootCertStoreProvider; use deno_runtime::deno_tls::RootCertStoreProvider;
use deno_runtime::deno_web::BlobStore; use deno_runtime::deno_web::BlobStore;
@ -924,8 +925,14 @@ pub async fn run(
} }
} }
let desc_parser = let desc_parser = Arc::new(RuntimePermissionDescriptorParser::new(
Arc::new(RuntimePermissionDescriptorParser::new(sys.clone())); sys.clone(),
if metadata.unstable_config.subdomain_wildcards {
UnstableSubdomainWildcards::Enabled
} else {
UnstableSubdomainWildcards::Disabled
},
));
let permissions = let permissions =
Permissions::from_options(desc_parser.as_ref(), &permissions)?; Permissions::from_options(desc_parser.as_ref(), &permissions)?;
PermissionsContainer::new(desc_parser, permissions) PermissionsContainer::new(desc_parser, permissions)

View file

@ -709,6 +709,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
node_modules, node_modules,
unstable_config: UnstableConfig { unstable_config: UnstableConfig {
legacy_flag_enabled: false, legacy_flag_enabled: false,
subdomain_wildcards: self.cli_options.unstable_subdomain_wildcards(),
bare_node_builtins: self.cli_options.unstable_bare_node_builtins(), bare_node_builtins: self.cli_options.unstable_bare_node_builtins(),
detect_cjs: self.cli_options.unstable_detect_cjs(), detect_cjs: self.cli_options.unstable_detect_cjs(),
features: self.cli_options.unstable_features(), features: self.cli_options.unstable_features(),

View file

@ -492,6 +492,7 @@ mod tests {
use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::npm::DenoInNpmPackageChecker;
use deno_runtime::deno_fs::RealFs; use deno_runtime::deno_fs::RealFs;
use deno_runtime::deno_permissions::Permissions; use deno_runtime::deno_permissions::Permissions;
use deno_runtime::deno_permissions::UnstableSubdomainWildcards;
use deno_runtime::permissions::RuntimePermissionDescriptorParser; use deno_runtime::permissions::RuntimePermissionDescriptorParser;
use deno_runtime::worker::WorkerOptions; use deno_runtime::worker::WorkerOptions;
use deno_runtime::worker::WorkerServiceOptions; use deno_runtime::worker::WorkerServiceOptions;
@ -502,9 +503,11 @@ mod tests {
let main_module = let main_module =
resolve_path("./hello.js", &std::env::current_dir().unwrap()).unwrap(); resolve_path("./hello.js", &std::env::current_dir().unwrap()).unwrap();
let fs = Arc::new(RealFs); let fs = Arc::new(RealFs);
let permission_desc_parser = Arc::new( let permission_desc_parser =
RuntimePermissionDescriptorParser::new(crate::sys::CliSys::default()), Arc::new(RuntimePermissionDescriptorParser::new(
); crate::sys::CliSys::default(),
UnstableSubdomainWildcards::Enabled,
));
let options = WorkerOptions { let options = WorkerOptions {
startup_snapshot: deno_snapshots::CLI_SNAPSHOT, startup_snapshot: deno_snapshots::CLI_SNAPSHOT,
..Default::default() ..Default::default()

View file

@ -12,6 +12,7 @@ use deno_core::op2;
use deno_core::FsModuleLoader; use deno_core::FsModuleLoader;
use deno_core::ModuleSpecifier; use deno_core::ModuleSpecifier;
use deno_fs::RealFs; use deno_fs::RealFs;
use deno_permissions::UnstableSubdomainWildcards;
use deno_resolver::npm::DenoInNpmPackageChecker; use deno_resolver::npm::DenoInNpmPackageChecker;
use deno_resolver::npm::NpmResolver; use deno_resolver::npm::NpmResolver;
use deno_runtime::deno_permissions::PermissionsContainer; use deno_runtime::deno_permissions::PermissionsContainer;
@ -39,9 +40,11 @@ async fn main() -> Result<(), AnyError> {
let main_module = ModuleSpecifier::from_file_path(js_path).unwrap(); let main_module = ModuleSpecifier::from_file_path(js_path).unwrap();
eprintln!("Running {main_module}..."); eprintln!("Running {main_module}...");
let fs = Arc::new(RealFs); let fs = Arc::new(RealFs);
let permission_desc_parser = Arc::new( let permission_desc_parser =
RuntimePermissionDescriptorParser::new(sys_traits::impls::RealSys), Arc::new(RuntimePermissionDescriptorParser::new(
); sys_traits::impls::RealSys,
UnstableSubdomainWildcards::Enabled,
));
let mut worker = MainWorker::bootstrap_from_options( let mut worker = MainWorker::bootstrap_from_options(
&main_module, &main_module,
WorkerServiceOptions::< WorkerServiceOptions::<

View file

@ -168,6 +168,14 @@ pub static FEATURE_DESCRIPTIONS: &[UnstableFeatureDescription] = &[
config_option: ConfigFileOption::SameAsFlagName, config_option: ConfigFileOption::SameAsFlagName,
env_var: Some("DENO_UNSTABLE_SLOPPY_IMPORTS"), env_var: Some("DENO_UNSTABLE_SLOPPY_IMPORTS"),
}, },
UnstableFeatureDescription {
name: "subdomain-wildcards",
help_text: "Enable subdomain wildcards support for the `--allow-net` flag",
show_in_help: true,
kind: UnstableFeatureKind::Cli,
config_option: ConfigFileOption::SameAsFlagName,
env_var: Some("DENO_UNSTABLE_SUBDOMAIN_WILDCARDS"),
},
UnstableFeatureDescription { UnstableFeatureDescription {
name: "temporal", name: "temporal",
help_text: "Enable unstable Temporal API", help_text: "Enable unstable Temporal API",

View file

@ -18,9 +18,9 @@ export const unstableIds = {
nodeGlobals: 13, nodeGlobals: 13,
otel: 15, otel: 15,
process: 16, process: 16,
temporal: 18, temporal: 19,
unsafeProto: 19, unsafeProto: 20,
vsock: 20, vsock: 21,
webgpu: 21, webgpu: 22,
workerOptions: 22, workerOptions: 23,
}; };

View file

@ -168,12 +168,21 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ UnstableFeature
kind: UnstableFeatureKind::Cli, kind: UnstableFeatureKind::Cli,
config_file_option: "sloppy-imports", config_file_option: "sloppy-imports",
}, },
UnstableFeatureDefinition {
name: "subdomain-wildcards",
flag_name: "unstable-subdomain-wildcards",
help_text: "Enable subdomain wildcards support for the `--allow-net` flag",
show_in_help: true,
id: 18,
kind: UnstableFeatureKind::Cli,
config_file_option: "subdomain-wildcards",
},
UnstableFeatureDefinition { UnstableFeatureDefinition {
name: "temporal", name: "temporal",
flag_name: "unstable-temporal", flag_name: "unstable-temporal",
help_text: "Enable unstable Temporal API", help_text: "Enable unstable Temporal API",
show_in_help: true, show_in_help: true,
id: 18, id: 19,
kind: UnstableFeatureKind::Runtime, kind: UnstableFeatureKind::Runtime,
config_file_option: "temporal", config_file_option: "temporal",
}, },
@ -182,7 +191,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ UnstableFeature
flag_name: "unstable-unsafe-proto", flag_name: "unstable-unsafe-proto",
help_text: "Enable unsafe __proto__ support. This is a security risk.", help_text: "Enable unsafe __proto__ support. This is a security risk.",
show_in_help: true, show_in_help: true,
id: 19, id: 20,
kind: UnstableFeatureKind::Runtime, kind: UnstableFeatureKind::Runtime,
config_file_option: "unsafe-proto", config_file_option: "unsafe-proto",
}, },
@ -191,7 +200,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ UnstableFeature
flag_name: "unstable-vsock", flag_name: "unstable-vsock",
help_text: "Enable unstable VSOCK APIs", help_text: "Enable unstable VSOCK APIs",
show_in_help: false, show_in_help: false,
id: 20, id: 21,
kind: UnstableFeatureKind::Runtime, kind: UnstableFeatureKind::Runtime,
config_file_option: "vsock", config_file_option: "vsock",
}, },
@ -200,7 +209,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ UnstableFeature
flag_name: "unstable-webgpu", flag_name: "unstable-webgpu",
help_text: "Enable unstable WebGPU APIs", help_text: "Enable unstable WebGPU APIs",
show_in_help: true, show_in_help: true,
id: 21, id: 22,
kind: UnstableFeatureKind::Runtime, kind: UnstableFeatureKind::Runtime,
config_file_option: "webgpu", config_file_option: "webgpu",
}, },
@ -209,7 +218,7 @@ pub static UNSTABLE_FEATURES: &[UnstableFeatureDefinition] = &[ UnstableFeature
flag_name: "unstable-worker-options", flag_name: "unstable-worker-options",
help_text: "Enable unstable Web Worker APIs", help_text: "Enable unstable Web Worker APIs",
show_in_help: true, show_in_help: true,
id: 22, id: 23,
kind: UnstableFeatureKind::Runtime, kind: UnstableFeatureKind::Runtime,
config_file_option: "worker-options", config_file_option: "worker-options",
}, },
@ -220,6 +229,7 @@ pub struct UnstableEnvVarNames {
pub lockfile_v5: &'static str, pub lockfile_v5: &'static str,
pub npm_lazy_caching: &'static str, pub npm_lazy_caching: &'static str,
pub sloppy_imports: &'static str, pub sloppy_imports: &'static str,
pub subdomain_wildcards: &'static str,
} }
pub static UNSTABLE_ENV_VAR_NAMES: UnstableEnvVarNames = UnstableEnvVarNames { pub static UNSTABLE_ENV_VAR_NAMES: UnstableEnvVarNames = UnstableEnvVarNames {
bare_node_builtins: "DENO_UNSTABLE_BARE_NODE_BUILTINS", bare_node_builtins: "DENO_UNSTABLE_BARE_NODE_BUILTINS",
@ -227,4 +237,5 @@ pub static UNSTABLE_ENV_VAR_NAMES: UnstableEnvVarNames = UnstableEnvVarNames {
lockfile_v5: "DENO_UNSTABLE_LOCKFILE_V5", lockfile_v5: "DENO_UNSTABLE_LOCKFILE_V5",
npm_lazy_caching: "DENO_UNSTABLE_NPM_LAZY_CACHING", npm_lazy_caching: "DENO_UNSTABLE_NPM_LAZY_CACHING",
sloppy_imports: "DENO_UNSTABLE_SLOPPY_IMPORTS", sloppy_imports: "DENO_UNSTABLE_SLOPPY_IMPORTS",
subdomain_wildcards: "DENO_UNSTABLE_SUBDOMAIN_WILDCARDS",
}; };

View file

@ -18,6 +18,7 @@ use deno_permissions::RunDescriptorParseError;
use deno_permissions::RunQueryDescriptor; use deno_permissions::RunQueryDescriptor;
use deno_permissions::SysDescriptor; use deno_permissions::SysDescriptor;
use deno_permissions::SysDescriptorParseError; use deno_permissions::SysDescriptorParseError;
use deno_permissions::UnstableSubdomainWildcards;
use deno_permissions::WriteDescriptor; use deno_permissions::WriteDescriptor;
#[derive(Debug)] #[derive(Debug)]
@ -25,13 +26,20 @@ pub struct RuntimePermissionDescriptorParser<
TSys: deno_permissions::which::WhichSys + Send + Sync, TSys: deno_permissions::which::WhichSys + Send + Sync,
> { > {
sys: TSys, sys: TSys,
unstable_subdomain_wildcards: UnstableSubdomainWildcards,
} }
impl<TSys: deno_permissions::which::WhichSys + Send + Sync> impl<TSys: deno_permissions::which::WhichSys + Send + Sync>
RuntimePermissionDescriptorParser<TSys> RuntimePermissionDescriptorParser<TSys>
{ {
pub fn new(sys: TSys) -> Self { pub fn new(
Self { sys } sys: TSys,
unstable_subdomain_wildcards: UnstableSubdomainWildcards,
) -> Self {
Self {
sys,
unstable_subdomain_wildcards,
}
} }
fn resolve_from_cwd(&self, path: &str) -> Result<PathBuf, PathResolveError> { fn resolve_from_cwd(&self, path: &str) -> Result<PathBuf, PathResolveError> {
@ -77,14 +85,14 @@ impl<TSys: deno_permissions::which::WhichSys + Send + Sync + std::fmt::Debug>
&self, &self,
text: &str, text: &str,
) -> Result<NetDescriptor, deno_permissions::NetDescriptorParseError> { ) -> Result<NetDescriptor, deno_permissions::NetDescriptorParseError> {
NetDescriptor::parse(text) NetDescriptor::parse_for_list(text, self.unstable_subdomain_wildcards)
} }
fn parse_import_descriptor( fn parse_import_descriptor(
&self, &self,
text: &str, text: &str,
) -> Result<ImportDescriptor, deno_permissions::NetDescriptorParseError> { ) -> Result<ImportDescriptor, deno_permissions::NetDescriptorParseError> {
ImportDescriptor::parse(text) ImportDescriptor::parse_for_list(text, self.unstable_subdomain_wildcards)
} }
fn parse_env_descriptor( fn parse_env_descriptor(
@ -146,6 +154,13 @@ impl<TSys: deno_permissions::which::WhichSys + Send + Sync + std::fmt::Debug>
}) })
} }
fn parse_net_query(
&self,
text: &str,
) -> Result<NetDescriptor, deno_permissions::NetDescriptorParseError> {
NetDescriptor::parse_for_query(text)
}
fn parse_run_query( fn parse_run_query(
&self, &self,
requested: &str, requested: &str,
@ -166,14 +181,17 @@ mod test {
#[test] #[test]
fn test_handle_empty_value() { fn test_handle_empty_value() {
let parser = let parser = RuntimePermissionDescriptorParser::new(
RuntimePermissionDescriptorParser::new(sys_traits::impls::RealSys); sys_traits::impls::RealSys,
UnstableSubdomainWildcards::Enabled,
);
assert!(parser.parse_read_descriptor("").is_err()); assert!(parser.parse_read_descriptor("").is_err());
assert!(parser.parse_write_descriptor("").is_err()); assert!(parser.parse_write_descriptor("").is_err());
assert!(parser.parse_env_descriptor("").is_err()); assert!(parser.parse_env_descriptor("").is_err());
assert!(parser.parse_net_descriptor("").is_err()); assert!(parser.parse_net_descriptor("").is_err());
assert!(parser.parse_ffi_descriptor("").is_err()); assert!(parser.parse_ffi_descriptor("").is_err());
assert!(parser.parse_path_query("").is_err()); assert!(parser.parse_path_query("").is_err());
assert!(parser.parse_net_query("").is_err());
assert!(parser.parse_run_query("").is_err()); assert!(parser.parse_run_query("").is_err());
} }
} }

View file

@ -818,9 +818,19 @@ impl QueryDescriptor for WriteQueryDescriptor {
#[derive(Clone, Eq, PartialEq, Hash, Debug)] #[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub struct WriteDescriptor(pub PathBuf); pub struct WriteDescriptor(pub PathBuf);
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
pub enum SubdomainWildcards {
Enabled,
#[default]
Disabled,
}
pub type UnstableSubdomainWildcards = SubdomainWildcards;
#[derive(Clone, Eq, PartialEq, Hash, Debug)] #[derive(Clone, Eq, PartialEq, Hash, Debug)]
pub enum Host { pub enum Host {
Fqdn(FQDN), Fqdn(FQDN),
FqdnWithSubdomainWildcard(FQDN),
Ip(IpAddr), Ip(IpAddr),
Vsock(u32), Vsock(u32),
} }
@ -843,7 +853,19 @@ pub enum HostParseError {
} }
impl Host { impl Host {
fn parse(s: &str) -> Result<Self, HostParseError> { fn parse_for_query(s: &str) -> Result<Self, HostParseError> {
Self::parse_inner(s, SubdomainWildcards::Disabled)
}
#[cfg(test)]
fn parse_for_list(s: &str) -> Result<Self, HostParseError> {
Self::parse_inner(s, SubdomainWildcards::Enabled)
}
fn parse_inner(
s: &str,
subdomain_wildcards: SubdomainWildcards,
) -> Result<Self, HostParseError> {
if s.starts_with('[') && s.ends_with(']') { if s.starts_with('[') && s.ends_with(']') {
let ip = s[1..s.len() - 1] let ip = s[1..s.len() - 1]
.parse::<Ipv6Addr>() .parse::<Ipv6Addr>()
@ -865,9 +887,17 @@ impl Host {
} else { } else {
Cow::Owned(s.to_ascii_lowercase()) Cow::Owned(s.to_ascii_lowercase())
}; };
let mut host_or_suffix = lower.as_ref();
let mut has_subdomain_wildcard = false;
if matches!(subdomain_wildcards, SubdomainWildcards::Enabled) {
if let Some(suffix) = lower.strip_prefix("*.") {
host_or_suffix = suffix;
has_subdomain_wildcard = true;
}
}
let fqdn = { let fqdn = {
use std::str::FromStr; use std::str::FromStr;
FQDN::from_str(&lower).map_err(|e| HostParseError::Fqdn { FQDN::from_str(host_or_suffix).map_err(|e| HostParseError::Fqdn {
error: e, error: e,
host: s.to_string(), host: s.to_string(),
})? })?
@ -875,14 +905,18 @@ impl Host {
if fqdn.is_root() { if fqdn.is_root() {
return Err(HostParseError::InvalidEmptyHost(s.to_string())); return Err(HostParseError::InvalidEmptyHost(s.to_string()));
} }
Ok(Host::Fqdn(fqdn)) if has_subdomain_wildcard {
Ok(Host::FqdnWithSubdomainWildcard(fqdn))
} else {
Ok(Host::Fqdn(fqdn))
}
} }
} }
#[cfg(test)] #[cfg(test)]
#[track_caller] #[track_caller]
fn must_parse(s: &str) -> Self { fn must_parse(s: &str) -> Self {
Self::parse(s).unwrap() Self::parse_for_list(s).unwrap()
} }
} }
@ -923,11 +957,26 @@ impl QueryDescriptor for NetDescriptor {
} }
fn matches_allow(&self, other: &Self::AllowDesc) -> bool { fn matches_allow(&self, other: &Self::AllowDesc) -> bool {
self.0 == other.0 && (other.1.is_none() || self.1 == other.1) if other.1.is_some() && self.1 != other.1 {
return false;
}
match (&other.0, &self.0) {
(Host::Fqdn(a), Host::Fqdn(b)) => a == b,
(Host::FqdnWithSubdomainWildcard(a), Host::Fqdn(b)) => {
b.is_subdomain_of(a)
}
(
Host::FqdnWithSubdomainWildcard(a),
Host::FqdnWithSubdomainWildcard(b),
) => a == b,
(Host::Ip(a), Host::Ip(b)) => a == b,
(Host::Vsock(a), Host::Vsock(b)) => a == b,
_ => false,
}
} }
fn matches_deny(&self, other: &Self::DenyDesc) -> bool { fn matches_deny(&self, other: &Self::DenyDesc) -> bool {
self.0 == other.0 && (other.1.is_none() || self.1 == other.1) self.matches_allow(other)
} }
fn revokes(&self, other: &Self::AllowDesc) -> bool { fn revokes(&self, other: &Self::AllowDesc) -> bool {
@ -974,7 +1023,23 @@ pub enum NetDescriptorFromUrlParseError {
} }
impl NetDescriptor { impl NetDescriptor {
pub fn parse(hostname: &str) -> Result<Self, NetDescriptorParseError> { pub fn parse_for_query(
hostname: &str,
) -> Result<Self, NetDescriptorParseError> {
Self::parse_inner(hostname, SubdomainWildcards::Disabled)
}
pub fn parse_for_list(
hostname: &str,
unstable_subdomain_wildcards: UnstableSubdomainWildcards,
) -> Result<Self, NetDescriptorParseError> {
Self::parse_inner(hostname, unstable_subdomain_wildcards)
}
fn parse_inner(
hostname: &str,
subdomain_wildcards: SubdomainWildcards,
) -> Result<Self, NetDescriptorParseError> {
#[cfg(unix)] #[cfg(unix)]
if let Some(vsock) = hostname.strip_prefix("vsock:") { if let Some(vsock) = hostname.strip_prefix("vsock:") {
let mut split = vsock.split(':'); let mut split = vsock.split(':');
@ -1038,7 +1103,7 @@ impl NetDescriptor {
Some((host, port)) => (host, port), Some((host, port)) => (host, port),
None => (hostname, ""), None => (hostname, ""),
}; };
let host = Host::parse(host)?; let host = Host::parse_inner(host, subdomain_wildcards)?;
let port = if port.is_empty() { let port = if port.is_empty() {
None None
@ -1068,7 +1133,7 @@ impl NetDescriptor {
let host = url.host_str().ok_or_else(|| { let host = url.host_str().ok_or_else(|| {
NetDescriptorFromUrlParseError::MissingHost(url.clone()) NetDescriptorFromUrlParseError::MissingHost(url.clone())
})?; })?;
let host = Host::parse(host)?; let host = Host::parse_for_query(host)?;
let port = url.port_or_known_default(); let port = url.port_or_known_default();
Ok(NetDescriptor(host, port.map(Into::into))) Ok(NetDescriptor(host, port.map(Into::into)))
} }
@ -1085,6 +1150,7 @@ impl fmt::Display for NetDescriptor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 { match &self.0 {
Host::Fqdn(fqdn) => write!(f, "{fqdn}"), Host::Fqdn(fqdn) => write!(f, "{fqdn}"),
Host::FqdnWithSubdomainWildcard(fqdn) => write!(f, "*.{fqdn}"),
Host::Ip(IpAddr::V4(ip)) => write!(f, "{ip}"), Host::Ip(IpAddr::V4(ip)) => write!(f, "{ip}"),
Host::Ip(IpAddr::V6(ip)) => write!(f, "[{ip}]"), Host::Ip(IpAddr::V6(ip)) => write!(f, "[{ip}]"),
Host::Vsock(cid) => write!(f, "vsock:{cid}"), Host::Vsock(cid) => write!(f, "vsock:{cid}"),
@ -1154,8 +1220,14 @@ impl QueryDescriptor for ImportDescriptor {
} }
impl ImportDescriptor { impl ImportDescriptor {
pub fn parse(specifier: &str) -> Result<Self, NetDescriptorParseError> { pub fn parse_for_list(
Ok(ImportDescriptor(NetDescriptor::parse(specifier)?)) specifier: &str,
unstable_subdomain_wildcards: UnstableSubdomainWildcards,
) -> Result<Self, NetDescriptorParseError> {
Ok(ImportDescriptor(NetDescriptor::parse_for_list(
specifier,
unstable_subdomain_wildcards,
)?))
} }
pub fn from_url(url: &Url) -> Result<Self, NetDescriptorFromUrlParseError> { pub fn from_url(url: &Url) -> Result<Self, NetDescriptorFromUrlParseError> {
@ -2981,7 +3053,7 @@ impl PermissionsContainer {
let mut inner = self.inner.lock(); let mut inner = self.inner.lock();
let inner = &mut inner.net; let inner = &mut inner.net;
skip_check_if_is_permission_fully_granted!(inner); skip_check_if_is_permission_fully_granted!(inner);
let hostname = Host::parse(host.0.as_ref())?; let hostname = Host::parse_for_query(host.0.as_ref())?;
let descriptor = NetDescriptor(hostname, host.1.map(Into::into)); let descriptor = NetDescriptor(hostname, host.1.map(Into::into));
inner.check(&descriptor, Some(api_name))?; inner.check(&descriptor, Some(api_name))?;
Ok(()) Ok(())
@ -3113,7 +3185,7 @@ impl PermissionsContainer {
permission.query( permission.query(
match host { match host {
None => None, None => None,
Some(h) => Some(self.descriptor_parser.parse_net_descriptor(h)?), Some(h) => Some(self.descriptor_parser.parse_net_query(h)?),
} }
.as_ref(), .as_ref(),
), ),
@ -3243,7 +3315,7 @@ impl PermissionsContainer {
self.inner.lock().net.revoke( self.inner.lock().net.revoke(
match host { match host {
None => None, None => None,
Some(h) => Some(self.descriptor_parser.parse_net_descriptor(h)?), Some(h) => Some(self.descriptor_parser.parse_net_query(h)?),
} }
.as_ref(), .as_ref(),
), ),
@ -3353,7 +3425,7 @@ impl PermissionsContainer {
self.inner.lock().net.request( self.inner.lock().net.request(
match host { match host {
None => None, None => None,
Some(h) => Some(self.descriptor_parser.parse_net_descriptor(h)?), Some(h) => Some(self.descriptor_parser.parse_net_query(h)?),
} }
.as_ref(), .as_ref(),
), ),
@ -3767,6 +3839,11 @@ pub trait PermissionDescriptorParser: Debug + Send + Sync {
path: &str, path: &str,
) -> Result<PathQueryDescriptor, PathResolveError>; ) -> Result<PathQueryDescriptor, PathResolveError>;
fn parse_net_query(
&self,
text: &str,
) -> Result<NetDescriptor, NetDescriptorParseError>;
fn parse_run_query( fn parse_run_query(
&self, &self,
requested: &str, requested: &str,
@ -3830,14 +3907,17 @@ mod tests {
&self, &self,
text: &str, text: &str,
) -> Result<NetDescriptor, NetDescriptorParseError> { ) -> Result<NetDescriptor, NetDescriptorParseError> {
NetDescriptor::parse(text) NetDescriptor::parse_for_list(text, UnstableSubdomainWildcards::Enabled)
} }
fn parse_import_descriptor( fn parse_import_descriptor(
&self, &self,
text: &str, text: &str,
) -> Result<ImportDescriptor, NetDescriptorParseError> { ) -> Result<ImportDescriptor, NetDescriptorParseError> {
ImportDescriptor::parse(text) ImportDescriptor::parse_for_list(
text,
UnstableSubdomainWildcards::Enabled,
)
} }
fn parse_env_descriptor( fn parse_env_descriptor(
@ -3891,6 +3971,13 @@ mod tests {
}) })
} }
fn parse_net_query(
&self,
text: &str,
) -> Result<NetDescriptor, NetDescriptorParseError> {
NetDescriptor::parse_for_query(text)
}
fn parse_run_query( fn parse_run_query(
&self, &self,
requested: &str, requested: &str,
@ -3961,7 +4048,8 @@ mod tests {
"172.16.0.2:8000", "172.16.0.2:8000",
"www.github.com:443", "www.github.com:443",
"80.example.com:80", "80.example.com:80",
"443.example.com:443" "443.example.com:443",
"*.discord.gg"
]), ]),
..Default::default() ..Default::default()
}, },
@ -3989,13 +4077,15 @@ mod tests {
("443.example.com", 444, false), ("443.example.com", 444, false),
("80.example.com", 81, false), ("80.example.com", 81, false),
("80.example.com", 80, true), ("80.example.com", 80, true),
("discord.gg", 0, true),
("foo.discord.gg", 0, true),
// Just some random hosts that should err // Just some random hosts that should err
("somedomain", 0, false), ("somedomain", 0, false),
("192.168.0.1", 0, false), ("192.168.0.1", 0, false),
]; ];
for (host, port, is_ok) in domain_tests { for (host, port, is_ok) in domain_tests {
let host = Host::parse(host).unwrap(); let host = Host::parse_for_query(host).unwrap();
let descriptor = NetDescriptor(host, Some(port)); let descriptor = NetDescriptor(host, Some(port));
assert_eq!( assert_eq!(
is_ok, is_ok,
@ -4041,7 +4131,7 @@ mod tests {
]; ];
for (host_str, port) in domain_tests { for (host_str, port) in domain_tests {
let host = Host::parse(host_str).unwrap(); let host = Host::parse_for_query(host_str).unwrap();
let descriptor = NetDescriptor(host, Some(port)); let descriptor = NetDescriptor(host, Some(port));
assert!( assert!(
perms.net.check(&descriptor, None).is_ok(), perms.net.check(&descriptor, None).is_ok(),
@ -4086,7 +4176,7 @@ mod tests {
]; ];
for (host_str, port) in domain_tests { for (host_str, port) in domain_tests {
let host = Host::parse(host_str).unwrap(); let host = Host::parse_for_query(host_str).unwrap();
let descriptor = NetDescriptor(host, Some(port)); let descriptor = NetDescriptor(host, Some(port));
assert!( assert!(
perms.net.check(&descriptor, None).is_err(), perms.net.check(&descriptor, None).is_err(),
@ -5058,7 +5148,7 @@ mod tests {
&parser, &parser,
&PermissionsOptions { &PermissionsOptions {
allow_env: Some(vec![]), allow_env: Some(vec![]),
allow_net: Some(svec!["foo", "bar"]), allow_net: Some(svec!["*.foo", "bar"]),
..Default::default() ..Default::default()
}, },
) )
@ -5079,7 +5169,11 @@ mod tests {
Permissions { Permissions {
env: Permissions::new_unary(Some(HashSet::new()), None, false), env: Permissions::new_unary(Some(HashSet::new()), None, false),
net: Permissions::new_unary( net: Permissions::new_unary(
Some(HashSet::from([NetDescriptor::parse("foo").unwrap()])), Some(HashSet::from([NetDescriptor::parse_for_list(
"foo",
UnstableSubdomainWildcards::Enabled
)
.unwrap()])),
None, None,
false false
), ),
@ -5175,7 +5269,7 @@ mod tests {
} }
#[test] #[test]
fn test_host_parse() { fn test_host_parse_for_query() {
let hosts = &[ let hosts = &[
("deno.land", Some(Host::Fqdn(fqdn!("deno.land")))), ("deno.land", Some(Host::Fqdn(fqdn!("deno.land")))),
("DENO.land", Some(Host::Fqdn(fqdn!("deno.land")))), ("DENO.land", Some(Host::Fqdn(fqdn!("deno.land")))),
@ -5200,6 +5294,54 @@ mod tests {
("1::1.", None), ("1::1.", None),
("deno.land.", Some(Host::Fqdn(fqdn!("deno.land")))), ("deno.land.", Some(Host::Fqdn(fqdn!("deno.land")))),
(".deno.land", None), (".deno.land", None),
("*.deno.land", None),
(
"::ffff:1.1.1.1",
Some(Host::Ip(IpAddr::V6(Ipv6Addr::new(
0, 0, 0, 0, 0, 0xffff, 0x0101, 0x0101,
)))),
),
];
for (host_str, expected) in hosts {
assert_eq!(
Host::parse_for_query(host_str).ok(),
*expected,
"{host_str}"
);
}
}
#[test]
fn test_host_parse_for_list() {
let hosts = &[
("deno.land", Some(Host::Fqdn(fqdn!("deno.land")))),
(
"*.deno.land",
Some(Host::FqdnWithSubdomainWildcard(fqdn!("deno.land"))),
),
("DENO.land", Some(Host::Fqdn(fqdn!("deno.land")))),
("deno.land.", Some(Host::Fqdn(fqdn!("deno.land")))),
(
"1.1.1.1",
Some(Host::Ip(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)))),
),
(
"::1",
Some(Host::Ip(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))),
),
(
"[::1]",
Some(Host::Ip(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)))),
),
("[::1", None),
("::1]", None),
("deno. land", None),
("1. 1.1.1", None),
("1.1.1.1.", None),
("1::1.", None),
("deno.land.", Some(Host::Fqdn(fqdn!("deno.land")))),
(".deno.land", None),
( (
"::ffff:1.1.1.1", "::ffff:1.1.1.1",
Some(Host::Ip(IpAddr::V6(Ipv6Addr::new( Some(Host::Ip(IpAddr::V6(Ipv6Addr::new(
@ -5209,12 +5351,12 @@ mod tests {
]; ];
for (host_str, expected) in hosts { for (host_str, expected) in hosts {
assert_eq!(Host::parse(host_str).ok(), *expected, "{host_str}"); assert_eq!(Host::parse_for_list(host_str).ok(), *expected, "{host_str}");
} }
} }
#[test] #[test]
fn test_net_descriptor_parse() { fn test_net_descriptor_parse_for_query() {
let cases = &[ let cases = &[
( (
"deno.land", "deno.land",
@ -5228,6 +5370,84 @@ mod tests {
"deno.land:8000", "deno.land:8000",
Some(NetDescriptor(Host::Fqdn(fqdn!("deno.land")), Some(8000))), Some(NetDescriptor(Host::Fqdn(fqdn!("deno.land")), Some(8000))),
), ),
("*.deno.land", None),
("deno.land:", None),
("deno.land:a", None),
("deno. land:a", None),
("deno.land.: a", None),
(
"1.1.1.1",
Some(NetDescriptor(
Host::Ip(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1))),
None,
)),
),
("1.1.1.1.", None),
("1.1.1.1..", None),
(
"1.1.1.1:8000",
Some(NetDescriptor(
Host::Ip(IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1))),
Some(8000),
)),
),
("::", None),
(":::80", None),
("::80", None),
(
"[::]",
Some(NetDescriptor(
Host::Ip(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 0))),
None,
)),
),
("[::1", None),
("::1]", None),
("::1]", None),
("[::1]:", None),
("[::1]:a", None),
(
"[::1]:443",
Some(NetDescriptor(
Host::Ip(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))),
Some(443),
)),
),
("", None),
("deno.land..", None),
];
for (input, expected) in cases {
assert_eq!(
NetDescriptor::parse_for_query(input).ok(),
*expected,
"'{input}'"
);
}
}
#[test]
fn test_net_descriptor_parse_for_list() {
let cases = &[
(
"deno.land",
Some(NetDescriptor(Host::Fqdn(fqdn!("deno.land")), None)),
),
(
"DENO.land",
Some(NetDescriptor(Host::Fqdn(fqdn!("deno.land")), None)),
),
(
"deno.land:8000",
Some(NetDescriptor(Host::Fqdn(fqdn!("deno.land")), Some(8000))),
),
(
"*.deno.land",
Some(NetDescriptor(
Host::FqdnWithSubdomainWildcard(fqdn!("deno.land")),
None,
)),
),
("deno.land:", None), ("deno.land:", None),
("deno.land:a", None), ("deno.land:a", None),
("deno. land:a", None), ("deno. land:a", None),
@ -5275,7 +5495,15 @@ mod tests {
]; ];
for (input, expected) in cases { for (input, expected) in cases {
assert_eq!(NetDescriptor::parse(input).ok(), *expected, "'{input}'"); assert_eq!(
NetDescriptor::parse_for_list(
input,
UnstableSubdomainWildcards::Enabled
)
.ok(),
*expected,
"'{input}'"
);
} }
} }

View file

@ -1444,6 +1444,19 @@ mod permissions {
assert!(!err.contains(util::PERMISSION_DENIED_PATTERN)); assert!(!err.contains(util::PERMISSION_DENIED_PATTERN));
} }
#[test]
fn net_fetch_localhost_subdomain() {
let _http_guard = util::http_server();
let (_, err) = util::run_and_collect_output(
true,
"run --unstable-subdomain-wildcards --allow-net=*.localhost run/complex_permissions_test.ts netFetch http://localhost:4545/ http://localhost:4546/ http://localhost:4547/",
None,
None,
true,
);
assert!(!err.contains(util::PERMISSION_DENIED_PATTERN));
}
#[test] #[test]
fn net_connect_allow_localhost_ip_4555() { fn net_connect_allow_localhost_ip_4555() {
let _http_guard = util::http_server(); let _http_guard = util::http_server();