summaryrefslogtreecommitdiff
path: root/cli/auth_tokens.rs
diff options
context:
space:
mode:
authorMatt Mastracci <matthew@mastracci.com>2024-02-06 11:45:40 -0700
committerGitHub <noreply@github.com>2024-02-06 19:45:40 +0100
commita6b2a4474e50952f28cb933ada0d698fc1055578 (patch)
tree394b3d1327905a4b410c67a08bf0ce4e263b02de /cli/auth_tokens.rs
parent327b5b280b3914fffb5dc89019e4adfefa2b9eb5 (diff)
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`.
Diffstat (limited to 'cli/auth_tokens.rs')
-rw-r--r--cli/auth_tokens.rs166
1 files changed, 158 insertions, 8 deletions
diff --git a/cli/auth_tokens.rs b/cli/auth_tokens.rs
index 5143ea604..42009ef27 100644
--- a/cli/auth_tokens.rs
+++ b/cli/auth_tokens.rs
@@ -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);
+ }
+ }
}