Skip to content

Commit

Permalink
fix(cli): Add IP address support to DENO_AUTH_TOKEN (#22297)
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
mmastrac authored and littledivy committed Feb 8, 2024
1 parent 3d0b1ec commit de23e3b
Showing 1 changed file with 158 additions and 8 deletions.
166 changes: 158 additions & 8 deletions cli/auth_tokens.rs
Expand Up @@ -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 {
Expand All @@ -15,7 +21,7 @@ pub enum AuthTokenData {

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthToken {
host: String,
host: AuthDomain,
token: AuthTokenData,
}

Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
}

0 comments on commit de23e3b

Please sign in to comment.