mirror of
https://github.com/denoland/deno.git
synced 2024-11-21 15:04:11 -05:00
fix(cli): Add IP address support to DENO_AUTH_TOKEN (#22297)
Auth tokens may be specified for one of the following: - `abc123@deno.land`: `deno.land`, `www.deno.land`, etc - `abc123@deno.land:8080`: `deno.land:8080`, `www.deno.land:8080`, etc - `abc123@1.1.1.1`: IP `1.1.1.1` only - `abc123@1.1.1.1:8080`: IP `1.1.1.1`, port 8080 only - `abc123@[ipv6]`: IPv6 `[ipv6]` only - `abc123@[ipv6]:8080`: IPv6 `[ipv6]`, port 8080 only Leading dots are ignored, so `.deno.dev` is equivalent to `deno.dev`.
This commit is contained in:
parent
327b5b280b
commit
a6b2a4474e
1 changed files with 158 additions and 8 deletions
|
@ -5,7 +5,13 @@ use base64::Engine;
|
|||
use deno_core::ModuleSpecifier;
|
||||
use log::debug;
|
||||
use log::error;
|
||||
use std::borrow::Cow;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::Ipv6Addr;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AuthTokenData {
|
||||
|
@ -15,7 +21,7 @@ pub enum AuthTokenData {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct AuthToken {
|
||||
host: String,
|
||||
host: AuthDomain,
|
||||
token: AuthTokenData,
|
||||
}
|
||||
|
||||
|
@ -37,6 +43,78 @@ impl fmt::Display for AuthToken {
|
|||
#[derive(Debug, Clone)]
|
||||
pub struct AuthTokens(Vec<AuthToken>);
|
||||
|
||||
/// An authorization domain, either an exact or suffix match.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AuthDomain {
|
||||
Ip(IpAddr),
|
||||
IpPort(SocketAddr),
|
||||
/// Suffix match, no dot. May include a port.
|
||||
Suffix(Cow<'static, str>),
|
||||
}
|
||||
|
||||
impl<T: ToString> From<T> for AuthDomain {
|
||||
fn from(value: T) -> Self {
|
||||
let s = value.to_string().to_lowercase();
|
||||
if let Ok(ip) = SocketAddr::from_str(&s) {
|
||||
return AuthDomain::IpPort(ip);
|
||||
};
|
||||
if s.starts_with('[') && s.ends_with(']') {
|
||||
if let Ok(ip) = Ipv6Addr::from_str(&s[1..s.len() - 1]) {
|
||||
return AuthDomain::Ip(ip.into());
|
||||
}
|
||||
} else if let Ok(ip) = Ipv4Addr::from_str(&s) {
|
||||
return AuthDomain::Ip(ip.into());
|
||||
}
|
||||
if let Some(s) = s.strip_prefix('.') {
|
||||
AuthDomain::Suffix(Cow::Owned(s.to_owned()))
|
||||
} else {
|
||||
AuthDomain::Suffix(Cow::Owned(s))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthDomain {
|
||||
pub fn matches(&self, specifier: &ModuleSpecifier) -> bool {
|
||||
let Some(host) = specifier.host_str() else {
|
||||
return false;
|
||||
};
|
||||
match *self {
|
||||
Self::Ip(ip) => {
|
||||
let AuthDomain::Ip(parsed) = AuthDomain::from(host) else {
|
||||
return false;
|
||||
};
|
||||
ip == parsed && specifier.port().is_none()
|
||||
}
|
||||
Self::IpPort(ip) => {
|
||||
let AuthDomain::Ip(parsed) = AuthDomain::from(host) else {
|
||||
return false;
|
||||
};
|
||||
ip.ip() == parsed && specifier.port() == Some(ip.port())
|
||||
}
|
||||
Self::Suffix(ref suffix) => {
|
||||
let hostname = if let Some(port) = specifier.port() {
|
||||
Cow::Owned(format!("{}:{}", host, port))
|
||||
} else {
|
||||
Cow::Borrowed(host)
|
||||
};
|
||||
|
||||
if suffix.len() == hostname.len() {
|
||||
return suffix == &hostname;
|
||||
}
|
||||
|
||||
// If it's a suffix match, ensure a dot
|
||||
if hostname.ends_with(suffix.as_ref())
|
||||
&& hostname.ends_with(&format!(".{suffix}"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthTokens {
|
||||
/// Create a new set of tokens based on the provided string. It is intended
|
||||
/// that the string be the value of an environment variable and the string is
|
||||
|
@ -49,7 +127,7 @@ impl AuthTokens {
|
|||
if token_str.contains('@') {
|
||||
let pair: Vec<&str> = token_str.rsplitn(2, '@').collect();
|
||||
let token = pair[1];
|
||||
let host = pair[0].to_lowercase();
|
||||
let host = AuthDomain::from(pair[0]);
|
||||
if token.contains(':') {
|
||||
let pair: Vec<&str> = token.rsplitn(2, ':').collect();
|
||||
let username = pair[1].to_string();
|
||||
|
@ -81,12 +159,7 @@ impl AuthTokens {
|
|||
/// matching is case insensitive.
|
||||
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<AuthToken> {
|
||||
self.0.iter().find_map(|t| {
|
||||
let hostname = if let Some(port) = specifier.port() {
|
||||
format!("{}:{}", specifier.host_str()?, port)
|
||||
} else {
|
||||
specifier.host_str()?.to_string()
|
||||
};
|
||||
if hostname.to_lowercase().ends_with(&t.host) {
|
||||
if t.host.matches(specifier) {
|
||||
Some(t.clone())
|
||||
} else {
|
||||
None
|
||||
|
@ -182,4 +255,81 @@ mod tests {
|
|||
let fixture = resolve_url("https://deno.land:8080/x/mod.ts").unwrap();
|
||||
assert_eq!(auth_tokens.get(&fixture), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ip() {
|
||||
let ip = AuthDomain::from("[2001:db8:a::123]");
|
||||
assert_eq!("Ip(2001:db8:a::123)", format!("{ip:?}"));
|
||||
let ip = AuthDomain::from("[2001:db8:a::123]:8080");
|
||||
assert_eq!("IpPort([2001:db8:a::123]:8080)", format!("{ip:?}"));
|
||||
let ip = AuthDomain::from("1.1.1.1");
|
||||
assert_eq!("Ip(1.1.1.1)", format!("{ip:?}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_insensitive() {
|
||||
let domain = AuthDomain::from("EXAMPLE.com");
|
||||
assert!(
|
||||
domain.matches(&ModuleSpecifier::parse("http://example.com").unwrap())
|
||||
);
|
||||
assert!(
|
||||
domain.matches(&ModuleSpecifier::parse("http://example.COM").unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matches() {
|
||||
let candidates = [
|
||||
"example.com",
|
||||
"www.example.com",
|
||||
"1.1.1.1",
|
||||
"[2001:db8:a::123]",
|
||||
// These will never match
|
||||
"example.com.evil.com",
|
||||
"1.1.1.1.evil.com",
|
||||
"notexample.com",
|
||||
"www.notexample.com",
|
||||
];
|
||||
let domains = [
|
||||
("example.com", vec!["example.com", "www.example.com"]),
|
||||
(".example.com", vec!["example.com", "www.example.com"]),
|
||||
("www.example.com", vec!["www.example.com"]),
|
||||
("1.1.1.1", vec!["1.1.1.1"]),
|
||||
("[2001:db8:a::123]", vec!["[2001:db8:a::123]"]),
|
||||
];
|
||||
let url = |c: &str| ModuleSpecifier::parse(&format!("http://{c}")).unwrap();
|
||||
let url_port =
|
||||
|c: &str| ModuleSpecifier::parse(&format!("http://{c}:8080")).unwrap();
|
||||
|
||||
// Generate each candidate with and without a port
|
||||
let candidates = candidates
|
||||
.into_iter()
|
||||
.flat_map(|c| [url(c), url_port(c)])
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (domain, expected_domain) in domains {
|
||||
// Test without a port -- all candidates return without a port
|
||||
let auth_domain = AuthDomain::from(domain);
|
||||
let actual = candidates
|
||||
.iter()
|
||||
.filter(|c| auth_domain.matches(c))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let expected = expected_domain.iter().map(|u| url(u)).collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
// Test with a port, all candidates return with a port
|
||||
let auth_domain = AuthDomain::from(&format!("{domain}:8080"));
|
||||
let actual = candidates
|
||||
.iter()
|
||||
.filter(|c| auth_domain.matches(c))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let expected = expected_domain
|
||||
.iter()
|
||||
.map(|u| url_port(u))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue