mirror of
https://github.com/denoland/deno.git
synced 2024-11-24 15:19:26 -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 deno_core::ModuleSpecifier;
|
||||||
use log::debug;
|
use log::debug;
|
||||||
use log::error;
|
use log::error;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum AuthTokenData {
|
pub enum AuthTokenData {
|
||||||
|
@ -15,7 +21,7 @@ pub enum AuthTokenData {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct AuthToken {
|
pub struct AuthToken {
|
||||||
host: String,
|
host: AuthDomain,
|
||||||
token: AuthTokenData,
|
token: AuthTokenData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +43,78 @@ impl fmt::Display for AuthToken {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct AuthTokens(Vec<AuthToken>);
|
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 {
|
impl AuthTokens {
|
||||||
/// Create a new set of tokens based on the provided string. It is intended
|
/// 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
|
/// that the string be the value of an environment variable and the string is
|
||||||
|
@ -49,7 +127,7 @@ impl AuthTokens {
|
||||||
if token_str.contains('@') {
|
if token_str.contains('@') {
|
||||||
let pair: Vec<&str> = token_str.rsplitn(2, '@').collect();
|
let pair: Vec<&str> = token_str.rsplitn(2, '@').collect();
|
||||||
let token = pair[1];
|
let token = pair[1];
|
||||||
let host = pair[0].to_lowercase();
|
let host = AuthDomain::from(pair[0]);
|
||||||
if token.contains(':') {
|
if token.contains(':') {
|
||||||
let pair: Vec<&str> = token.rsplitn(2, ':').collect();
|
let pair: Vec<&str> = token.rsplitn(2, ':').collect();
|
||||||
let username = pair[1].to_string();
|
let username = pair[1].to_string();
|
||||||
|
@ -81,12 +159,7 @@ impl AuthTokens {
|
||||||
/// matching is case insensitive.
|
/// matching is case insensitive.
|
||||||
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<AuthToken> {
|
pub fn get(&self, specifier: &ModuleSpecifier) -> Option<AuthToken> {
|
||||||
self.0.iter().find_map(|t| {
|
self.0.iter().find_map(|t| {
|
||||||
let hostname = if let Some(port) = specifier.port() {
|
if t.host.matches(specifier) {
|
||||||
format!("{}:{}", specifier.host_str()?, port)
|
|
||||||
} else {
|
|
||||||
specifier.host_str()?.to_string()
|
|
||||||
};
|
|
||||||
if hostname.to_lowercase().ends_with(&t.host) {
|
|
||||||
Some(t.clone())
|
Some(t.clone())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -182,4 +255,81 @@ mod tests {
|
||||||
let fixture = resolve_url("https://deno.land:8080/x/mod.ts").unwrap();
|
let fixture = resolve_url("https://deno.land:8080/x/mod.ts").unwrap();
|
||||||
assert_eq!(auth_tokens.get(&fixture), None);
|
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