Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(cli): Add IP address support to DENO_AUTH_TOKEN #22297

Merged
merged 7 commits into from
Feb 6, 2024
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 151 additions & 8 deletions cli/auth_tokens.rs
Original file line number Diff line number Diff line change
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,83 @@ 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),
mmastrac marked this conversation as resolved.
Show resolved Hide resolved
/// 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();
match SocketAddr::from_str(&s) {
Ok(ip) => return AuthDomain::IPPort(ip),
Err(_) => {}
};
if s.starts_with('[') && s.ends_with(']') {
match Ipv6Addr::from_str(&s[1..s.len() - 1]) {
Ok(ip) => return AuthDomain::IP(ip.into()),
Err(_) => {}
};
} else {
match Ipv4Addr::from_str(&s) {
Ok(ip) => return AuthDomain::IP(ip.into()),
Err(_) => {}
};
}
if s.starts_with('.') {
AuthDomain::Suffix(Cow::Owned(s[1..].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;
}

return 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 +132,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 +164,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 +260,69 @@ 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_matches() {
let candidates = [
"example.com",
"www.example.com",
"notexample.com",
"www.notexample.com",
"1.1.1.1",
"[2001:db8:a::123]",
];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some tests with ports here?

Copy link
Contributor Author

@mmastrac mmastrac Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are tested with ports -- we add a port to each candidate below.

    // Generate each candidate with and without a port
    let candidates = candidates
      .into_iter()
      .flat_map(|c| [url(c), url_port(c)])
      .collect::<Vec<_>>();

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()
.map(|c| [url(c), url_port(c)])
.flatten()
.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