From 11bf43f777666fc3a7c941e844e0130904f63b6a Mon Sep 17 00:00:00 2001 From: Daniel Bloom <82895745+Daniel-Bloom-dfinity@users.noreply.github.com> Date: Mon, 13 Jun 2022 10:59:29 -0700 Subject: [PATCH] refactor: canister ids and aliases (#37) * refactor: canister ids and aliases Add a new flag `ignore_url_canister_param` for prod. Move logic to `canister_id.rs`. Add traits to make composition easier. Add basic tests. Redid `lookup` and `resolve_canister_id` to remove allocation. Bump version and dependencies. --- Cargo.lock | 95 ++++++------ Cargo.toml | 6 +- src/canister_id.rs | 246 ++++++++++++++++++++++++++++++ src/config/dns_canister_config.rs | 146 +++++------------- src/config/dns_canister_rule.rs | 70 ++++++--- src/main.rs | 122 +++++---------- 6 files changed, 425 insertions(+), 260 deletions(-) create mode 100644 src/canister_id.rs diff --git a/Cargo.lock b/Cargo.lock index 61c7bda..56d9a66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,9 +314,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +checksum = "722e23542a15cea1f65d4a1419c4cfd7a26706c70871a13a04238ca3f40f1661" [[package]] name = "core-foundation" @@ -380,9 +380,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.3.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +checksum = "40997c4145fd5570180f579db9fcea452c91a2b72411da899efb1fb041136eae" dependencies = [ "generic-array", "rand_core", @@ -400,16 +400,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "crypto-mac" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "dashmap" version = "4.0.2" @@ -422,12 +412,13 @@ dependencies = [ [[package]] name = "der" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +checksum = "13dd2ae565c0a381dde7fade45fce95984c568bdcb4700a4fdbe3175e0380b2f" dependencies = [ "const-oid", "pem-rfc7468", + "zeroize", ] [[package]] @@ -453,6 +444,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer 0.10.2", "crypto-common", + "subtle", ] [[package]] @@ -478,9 +470,9 @@ dependencies = [ [[package]] name = "ecdsa" -version = "0.13.4" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0d69ae62e0ce582d56380743515fefaf1a8c70cec685d9677636d7e30ae9dc9" +checksum = "e1e737f9eebb44576f3ee654141a789464071eb369d02c4397b32b6a79790112" dependencies = [ "der", "elliptic-curve", @@ -496,16 +488,19 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "elliptic-curve" -version = "0.11.12" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b477563c2bfed38a3b7a60964c49e058b2510ad3f12ba3483fd8f62c2306d6" +checksum = "bdd8c93ccd534d6a9790f4455cd71e7adb53a12e9af7dd54d1e258473f100cea" dependencies = [ "base16ct", "crypto-bigint", "der", + "digest 0.10.3", "ff", "generic-array", "group", + "pem-rfc7468", + "pkcs8", "rand_core", "sec1", "subtle", @@ -541,9 +536,9 @@ dependencies = [ [[package]] name = "ff" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2958d04124b9f27f175eaeb9a9f383d026098aa837eadd8ba22c11f13a05b9e" +checksum = "df689201f395c6b90dfe87127685f8dbfc083a5e779e613575d8bd7314300c3e" dependencies = [ "rand_core", "subtle", @@ -719,9 +714,9 @@ dependencies = [ [[package]] name = "group" -version = "0.11.0" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" +checksum = "7391856def869c1c81063a03457c676fbcd419709c3dfb33d8d319de484b154d" dependencies = [ "ff", "rand_core", @@ -782,12 +777,11 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hmac" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "crypto-mac", - "digest 0.9.0", + "digest 0.10.3", ] [[package]] @@ -885,9 +879,9 @@ dependencies = [ [[package]] name = "ic-agent" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d88689893b1d4ccb605e9bda11fdd6ccf3fccb8291fd5b946c92415fc787a5e" +checksum = "6a0cabf758d04a2389ffba0700bd7099de9b5cd47a04255063de1b0f9aac1f6e" dependencies = [ "async-trait", "base32", @@ -934,9 +928,9 @@ dependencies = [ [[package]] name = "ic-utils" -version = "0.16.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73601e532dd9711856c18cde0a4b672c669c69bf18b249d18c0f47ce62d85883" +checksum = "2f84ea6aad8345896eb336fe2757cbe14a8c476a0e586403b84a61a2106053cd" dependencies = [ "async-trait", "candid", @@ -956,7 +950,7 @@ dependencies = [ [[package]] name = "icx-proxy" -version = "0.8.1" +version = "0.9.0" dependencies = [ "anyhow", "axum", @@ -1053,15 +1047,14 @@ dependencies = [ [[package]] name = "k256" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19c3a5e0a0b8450278feda242592512e09f61c72e018b8cd5c859482802daf2d" +checksum = "b953594f084668b4138b8b2fa63ed9776b476c58aa507d575c5206e8bfe5dc4a" dependencies = [ "cfg-if", "ecdsa", "elliptic-curve", - "sec1", - "sha2 0.9.9", + "sha2 0.10.2", ] [[package]] @@ -1490,9 +1483,9 @@ dependencies = [ [[package]] name = "pem-rfc7468" -version = "0.3.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" dependencies = [ "base64ct", ] @@ -1562,13 +1555,12 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ "der", "spki", - "zeroize", ] [[package]] @@ -1799,9 +1791,9 @@ dependencies = [ [[package]] name = "rfc6979" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ef608575f6392792f9ecf7890c00086591d29a83910939d430753f7c050525" +checksum = "6c0788437d5ee113c49af91d3594ebc4fcdcc962f8b6df5aa1c3eeafd8ad95de" dependencies = [ "crypto-bigint", "hmac", @@ -1914,10 +1906,11 @@ dependencies = [ [[package]] name = "sec1" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" dependencies = [ + "base16ct", "der", "generic-array", "pkcs8", @@ -2051,11 +2044,11 @@ dependencies = [ [[package]] name = "signature" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" +checksum = "f054c6c1a6e95179d6f23ed974060dcefb2d9388bb7256900badad682c499de4" dependencies = [ - "digest 0.9.0", + "digest 0.10.3", "rand_core", ] @@ -2138,9 +2131,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.5.4" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", "der", diff --git a/Cargo.toml b/Cargo.toml index d154136..4d6a183 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "icx-proxy" -version = "0.8.1" +version = "0.9.0" authors = ["DFINITY Stiftung "] edition = "2018" description = "CLI tool to create an HTTP proxy to the Internet Computer." @@ -29,8 +29,8 @@ hex = "0.4" hyper = { version = "0.14", features = ["full"] } hyper-rustls = { version = "0.23", features = [ "webpki-roots" ] } hyper-tls = "0.5" -ic-agent = { version = "0.16" } -ic-utils = { version = "0.16", features = ["raw"] } +ic-agent = { version = "0.17" } +ic-utils = { version = "0.17", features = ["raw"] } lazy-regex = "2" opentelemetry = "0.17.0" opentelemetry-prometheus = "0.10.0" diff --git a/src/canister_id.rs b/src/canister_id.rs new file mode 100644 index 0000000..9a86c7d --- /dev/null +++ b/src/canister_id.rs @@ -0,0 +1,246 @@ +use hyper::{header::HOST, Request, Uri}; +use ic_agent::export::Principal; + +use crate::config::dns_canister_config::DnsCanisterConfig; + +/// A resolver for `Principal`s from a `Uri`. +trait UriResolver: Sync + Send { + fn resolve(&self, uri: &Uri) -> Option; +} + +impl UriResolver for &T { + fn resolve(&self, uri: &Uri) -> Option { + T::resolve(self, uri) + } +} + +struct UriParameterResolver; + +impl UriResolver for UriParameterResolver { + fn resolve(&self, uri: &Uri) -> Option { + url::form_urlencoded::parse(uri.query()?.as_bytes()) + .find(|(name, _)| name == "canisterId") + .and_then(|(_, canister_id)| Principal::from_text(canister_id.as_ref()).ok()) + } +} + +impl UriResolver for DnsCanisterConfig { + fn resolve(&self, uri: &Uri) -> Option { + self.resolve_canister_id(uri.host()?) + } +} + +/// A resolver for `Principal`s from a `Request`. +pub trait Resolver: Sync + Send { + fn resolve(&self, request: &Request) -> Option; +} + +impl> Resolver for &T { + fn resolve(&self, request: &Request) -> Option { + T::resolve(self, request) + } +} + +struct RequestUriResolver(pub T); + +impl Resolver for RequestUriResolver { + fn resolve(&self, request: &Request) -> Option { + self.0.resolve(request.uri()) + } +} + +struct RequestHostResolver(pub T); + +impl Resolver for RequestHostResolver { + fn resolve(&self, request: &Request) -> Option { + self.0.resolve( + &Uri::builder() + .authority(request.headers().get(HOST)?.as_bytes()) + .build() + .ok()?, + ) + } +} + +/// The default canister id resolver +pub struct DefaultResolver { + pub dns: DnsCanisterConfig, + pub check_params: bool, +} + +impl Resolver for DefaultResolver { + fn resolve(&self, request: &Request) -> Option { + if let Some(v) = RequestHostResolver(&self.dns).resolve(request) { + return Some(v); + } + if let Some(v) = RequestUriResolver(&self.dns).resolve(request) { + return Some(v); + } + if self.check_params { + if let Some(v) = RequestUriResolver(UriParameterResolver).resolve(request) { + return Some(v); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use hyper::{header::HOST, Request}; + use ic_agent::export::Principal; + + use super::{DefaultResolver, Resolver}; + use crate::config::dns_canister_config::DnsCanisterConfig; + + #[test] + fn simple_resolve() { + let dns = parse_config( + vec!["happy.little.domain.name:r7inp-6aaaa-aaaaa-aaabq-cai"], + vec!["little.domain.name"], + ); + + let resolver = DefaultResolver { + dns, + check_params: false, + }; + + let req = build_req( + Some("happy.little.domain.name"), + "https://happy.little.domain.name/rrkah-fqaaa-aaaaa-aaaaq-cai", + ); + + assert_eq!( + resolver.resolve(&req), + Some(principal("r7inp-6aaaa-aaaaa-aaabq-cai")) + ); + + let req = build_req( + Some("rrkah-fqaaa-aaaaa-aaaaq-cai.little.domain.name"), + "/r7inp-6aaaa-aaaaa-aaabq-cai", + ); + + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + } + + #[test] + fn prod() { + let dns = parse_config( + vec![ + "personhood.ic0.app:g3wsl-eqaaa-aaaan-aaaaa-cai", + "personhood.raw.ic0.app:g3wsl-eqaaa-aaaan-aaaaa-cai", + "identity.ic0.app:rdmx6-jaaaa-aaaaa-aaadq-cai", + "identity.raw.ic0.app:rdmx6-jaaaa-aaaaa-aaadq-cai", + "nns.ic0.app:qoctq-giaaa-aaaaa-aaaea-cai", + "nns.raw.ic0.app:qoctq-giaaa-aaaaa-aaaea-cai", + "dscvr.ic0.app:h5aet-waaaa-aaaab-qaamq-cai", + "dscvr.raw.ic0.app:h5aet-waaaa-aaaab-qaamq-cai", + ], + vec!["raw.ic0.app", "ic0.app"], + ); + + let resolver = DefaultResolver { + dns, + check_params: false, + }; + + let req = build_req(Some("nns.ic0.app"), "/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("qoctq-giaaa-aaaaa-aaaea-cai")) + ); + + let req = build_req(Some("nns.ic0.app"), "https://nns.ic0.app/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("qoctq-giaaa-aaaaa-aaaea-cai")) + ); + + let req = build_req(None, "https://nns.ic0.app/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("qoctq-giaaa-aaaaa-aaaea-cai")) + ); + + let req = build_req(None, "https://rrkah-fqaaa-aaaaa-aaaaq-cai.ic0.app/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + + let req = build_req( + Some("rrkah-fqaaa-aaaaa-aaaaq-cai.ic0.app"), + "https://rrkah-fqaaa-aaaaa-aaaaq-cai.ic0.app/about", + ); + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + + let req = build_req(Some("rrkah-fqaaa-aaaaa-aaaaq-cai.ic0.app"), "/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + + let req = build_req(Some("rrkah-fqaaa-aaaaa-aaaaq-cai.raw.ic0.app"), "/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + + let req = build_req( + Some("rrkah-fqaaa-aaaaa-aaaaq-cai.foo.raw.ic0.app"), + "/about", + ); + assert_eq!(resolver.resolve(&req), None); + } + + #[test] + fn dfx() { + let dns = parse_config(vec![], vec!["localhost"]); + + let resolver = DefaultResolver { + dns, + check_params: true, + }; + + let req = build_req(Some("rrkah-fqaaa-aaaaa-aaaaq-cai.localhost"), "/about"); + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + let req = build_req( + Some("localhost"), + "/about?canisterId=rrkah-fqaaa-aaaaa-aaaaq-cai", + ); + assert_eq!( + resolver.resolve(&req), + Some(principal("rrkah-fqaaa-aaaaa-aaaaq-cai")) + ); + } + + fn parse_config(aliases: Vec<&str>, suffixes: Vec<&str>) -> DnsCanisterConfig { + let aliases: Vec = aliases.iter().map(|&s| String::from(s)).collect(); + let suffixes: Vec = suffixes.iter().map(|&s| String::from(s)).collect(); + DnsCanisterConfig::new(&aliases, &suffixes).unwrap() + } + + fn build_req(host: Option<&str>, uri: &str) -> Request<()> { + let req = Request::builder().uri(uri); + if let Some(host) = host { + req.header(HOST, host) + } else { + req + } + .body(()) + .unwrap() + } + + fn principal(v: &str) -> Principal { + Principal::from_text(v).unwrap() + } +} diff --git a/src/config/dns_canister_config.rs b/src/config/dns_canister_config.rs index 6066826..cb5fce5 100644 --- a/src/config/dns_canister_config.rs +++ b/src/config/dns_canister_config.rs @@ -25,25 +25,17 @@ impl DnsCanisterConfig { } // Check suffixes first (via stable sort), because they will only match // if actually preceded by a canister id. - rules.sort_by_key(|x| Reverse(x.dns_suffix.len())); + rules.sort_by_key(|x| Reverse(x.dns_suffix().len())); Ok(DnsCanisterConfig { rules }) } - /// Return the Principal of the canister that matches the host name. + /// Return the Principal of the canister that matches the hostname. /// - /// split_hostname is expected to be the hostname split by '.', - /// but may contain upper- or lower-case characters. - pub fn resolve_canister_id_from_split_hostname( - &self, - split_hostname: &[&str], - ) -> Option { - let split_hostname_lowercase: Vec = split_hostname - .iter() - .map(|s| s.to_ascii_lowercase()) - .collect(); + /// `hostname` may contain uppercase or lowercase characters. + pub fn resolve_canister_id(&self, hostname: &str) -> Option { self.rules .iter() - .find_map(|rule| rule.lookup(&split_hostname_lowercase)) + .find_map(|rule| rule.lookup(hostname.split('.'))) } } @@ -59,8 +51,7 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["happy", "little", "domain", "name"]), + dns_aliases.resolve_canister_id("happy.little.domain.name"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ) } @@ -71,8 +62,7 @@ mod tests { parse_dns_aliases(vec!["little.domain.name:r7inp-6aaaa-aaaaa-aaabq-cai"]).unwrap(); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["happy", "little", "domain", "name"]), + dns_aliases.resolve_canister_id("happy.little.domain.name"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ) } @@ -85,8 +75,7 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["happy", "little", "domain", "name"]), + dns_aliases.resolve_canister_id("happy.little.domain.name"), None ) } @@ -97,8 +86,7 @@ mod tests { parse_dns_aliases(vec!["lItTlE.doMain.nAMe:r7inp-6aaaa-aaaaa-aaabq-cai"]).unwrap(); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["hAPpy", "littLE", "doMAin", "NAme"]), + dns_aliases.resolve_canister_id("happy.little.domain.name"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ) } @@ -112,19 +100,17 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["happy", "little", "domain", "name"]), + dns_aliases.resolve_canister_id("happy.little.domain.name"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["ecstatic", "domain", "name"]), + dns_aliases.resolve_canister_id("ecstatic.domain.name"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["super", "ecstatic", "domain", "name"]), + dns_aliases.resolve_canister_id("super.ecstatic.domain.name"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ) } @@ -138,17 +124,16 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["specific", "of", "many"]), + dns_aliases.resolve_canister_id("specific.of.many"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["more", "specific", "of", "many"]), + dns_aliases.resolve_canister_id("more.specific.of.many"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["another", "of", "many"]), + dns_aliases.resolve_canister_id("another.of.many"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ) } @@ -164,11 +149,11 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["a", "b", "c"]), + dns_aliases.resolve_canister_id("a.b.c"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["d", "b", "c"]), + dns_aliases.resolve_canister_id("d.b.c"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -184,11 +169,11 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["a", "b", "c"]), + dns_aliases.resolve_canister_id("a.b.c"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["d", "b", "c"]), + dns_aliases.resolve_canister_id("d.b.c"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -204,11 +189,11 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["a", "x", "c"]), + dns_aliases.resolve_canister_id("a.x.c"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["d", "x", "c"]), + dns_aliases.resolve_canister_id("d.x.c"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -224,11 +209,11 @@ mod tests { .unwrap(); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["x", "a", "c"]), + dns_aliases.resolve_canister_id("x.a.c"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["d", "a", "c"]), + dns_aliases.resolve_canister_id("d.a.c"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -238,17 +223,11 @@ mod tests { let config = parse_config(vec![], vec!["localhost"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "localhost" - ]), + config.resolve_canister_id("rrkah-fqaaa-aaaaa-aaaaq-cai.localhost"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "r7inp-6aaaa-aaaaa-aaabq-cai", - "localhost" - ]), + config.resolve_canister_id("r7inp-6aaaa-aaaaa-aaabq-cai.localhost"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ) } @@ -258,20 +237,11 @@ mod tests { let config = parse_config(vec![], vec!["localhost"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "more", - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "localhost" - ]), + config.resolve_canister_id("more.rrkah-fqaaa-aaaaa-aaaaq-cai.localhost"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "even", - "more", - "r7inp-6aaaa-aaaaa-aaabq-cai", - "localhost" - ]), + config.resolve_canister_id("even.more.r7inp-6aaaa-aaaaa-aaabq-cai.localhost"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ) } @@ -281,11 +251,7 @@ mod tests { let config = parse_config(vec![], vec!["localhost"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "nope", - "localhost" - ]), + config.resolve_canister_id("rrkah-fqaaa-aaaaa-aaaaq-cai.nope.localhost"), None ); } @@ -295,12 +261,7 @@ mod tests { let config = parse_config(vec![], vec!["a.b.c"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "a", - "b", - "c" - ]), + config.resolve_canister_id("rrkah-fqaaa-aaaaa-aaaaq-cai.a.b.c"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -310,13 +271,7 @@ mod tests { let config = parse_config(vec![], vec!["a.b.c"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "no", - "a", - "b", - "c" - ]), + config.resolve_canister_id("rrkah-fqaaa-aaaaa-aaaaq-cai.no.a.b.c"), None ); } @@ -326,13 +281,7 @@ mod tests { let config = parse_config(vec![], vec!["a.b.c"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "yes", - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "a", - "b", - "c" - ]), + config.resolve_canister_id("yes.rrkah-fqaaa-aaaaa-aaaaq-cai.a.b.c"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -342,13 +291,9 @@ mod tests { let config = parse_config(vec![], vec!["a.b.c"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "r7inp-6aaaa-aaaaa-aaabq-cai", // not seen/returned - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "a", - "b", - "c" - ]), + config.resolve_canister_id( + "r7inp-6aaaa-aaaaa-aaabq-cai.rrkah-fqaaa-aaaaa-aaaaq-cai.a.b.c" + ), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); } @@ -365,20 +310,15 @@ mod tests { .unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&["a", "b", "c"]), + config.resolve_canister_id("a.b.c"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - config.resolve_canister_id_from_split_hostname(&["d", "e",]), + config.resolve_canister_id("d.e"), Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) ); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "ryjl3-tyaaa-aaaaa-aaaba-cai", - "g", - "h", - "i", - ]), + config.resolve_canister_id("ryjl3-tyaaa-aaaaa-aaaba-cai.g.h.i"), Some(Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap()) ); } @@ -390,23 +330,17 @@ mod tests { parse_config(vec!["a.b.c:r7inp-6aaaa-aaaaa-aaabq-cai"], vec!["a.b.c"]).unwrap(); assert_eq!( - config.resolve_canister_id_from_split_hostname(&["a", "b", "c"]), + config.resolve_canister_id("a.b.c"), Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) ); assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "ryjl3-tyaaa-aaaaa-aaaba-cai", - "a", - "b", - "c" - ]), + config.resolve_canister_id("ryjl3-tyaaa-aaaaa-aaaba-cai.a.b.c"), Some(Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap()) ); } fn parse_dns_aliases(aliases: Vec<&str>) -> anyhow::Result { - let aliases: Vec = aliases.iter().map(|&s| String::from(s)).collect(); - DnsCanisterConfig::new(&aliases, &[]) + parse_config(aliases, vec![]) } fn parse_config(aliases: Vec<&str>, suffixes: Vec<&str>) -> anyhow::Result { diff --git a/src/config/dns_canister_rule.rs b/src/config/dns_canister_rule.rs index 0fe0bea..e34fff2 100644 --- a/src/config/dns_canister_rule.rs +++ b/src/config/dns_canister_rule.rs @@ -18,10 +18,8 @@ enum PrincipalDeterminationStrategy { /// match the last portion, as split by '.', of the host specified in the request. #[derive(Clone, Debug)] pub struct DnsCanisterRule { - domain_name: String, - /// The hostname parts that must match the right-hand side of the domain name. Lower case. - pub dns_suffix: Vec, + dns_suffix: Vec, strategy: PrincipalDeterminationStrategy, } @@ -32,7 +30,6 @@ impl DnsCanisterRule { let (domain_name, principal) = split_dns_alias(dns_alias)?; let dns_suffix = split_hostname_lowercase(&domain_name); Ok(DnsCanisterRule { - domain_name, dns_suffix, strategy: PrincipalDeterminationStrategy::Alias(principal), }) @@ -43,31 +40,66 @@ impl DnsCanisterRule { pub fn new_suffix(suffix: &str) -> DnsCanisterRule { let dns_suffix: Vec = split_hostname_lowercase(suffix); DnsCanisterRule { - domain_name: suffix.to_string(), dns_suffix, strategy: PrincipalDeterminationStrategy::PrecedingDomainName, } } /// Return the associated principal if this rule applies to the domain name. - pub fn lookup(&self, split_hostname_lowercase: &[String]) -> Option { - if split_hostname_lowercase.ends_with(&self.dns_suffix) { - match &self.strategy { - PrincipalDeterminationStrategy::Alias(principal) => Some(*principal), - PrincipalDeterminationStrategy::PrecedingDomainName => { - if split_hostname_lowercase.len() > self.dns_suffix.len() { - let subdomain = &split_hostname_lowercase - [split_hostname_lowercase.len() - self.dns_suffix.len() - 1]; - Principal::from_text(subdomain).ok() - } else { - None - } + pub fn lookup(&self, split_hostname: I) -> Option + where + T: AsRef, + I: IntoIterator, + I::IntoIter: DoubleEndedIterator, + { + fn extend_with_none(i: impl Iterator) -> impl Iterator> { + i.map(Some).chain(std::iter::once(None)) + } + fn eq(a: impl AsRef, b: &str) -> bool { + a.as_ref().eq_ignore_ascii_case(b) + } + + use PrincipalDeterminationStrategy::{Alias, PrecedingDomainName}; + + let split_hostname = split_hostname.into_iter().rev(); + let dns_suffix = self.dns_suffix().iter().rev(); + match (&self.strategy, split_hostname.size_hint()) { + (Alias(_), (_, Some(len))) if len < self.dns_suffix().len() => None, + (Alias(principal), _) => { + // Extend `split_hostname` with `None` + if extend_with_none(split_hostname) + .zip(dns_suffix) + // Loop through `split_hostname` and `dns_suffix`. + // + // If we reach the end of `split_hostname` (aka the `None` we extended) before + // we reach the end of `dns_suffix`, then short circuit with `false`. + .all(|(host, dns)| host.map(|host| eq(host, dns)).unwrap_or(false)) + { + Some(*principal) + } else { + None } } - } else { - None + (PrecedingDomainName, (_, Some(len))) if len <= self.dns_suffix().len() => None, + (PrecedingDomainName, _) => split_hostname + // Extend `dns_suffix` with `None` + .zip(extend_with_none(dns_suffix)) + // Loop through `split_hostname` and `dns_suffix`. + // + // Once we reach the end of `dns_suffix` (aka the `None` we extended) we know + // we're at the subdomain of `split_hostname`, so extract that. + .map_while(|(host, dns)| match dns { + Some(dns) if eq(&host, dns) => Some(None), + Some(_) => None, + None => Principal::from_text(host.as_ref()).ok().map(Some), + }) + .find_map(|x| x), } } + + pub fn dns_suffix(&self) -> &Vec { + &self.dns_suffix + } } fn split_hostname_lowercase(hostname: &str) -> Vec { diff --git a/src/main.rs b/src/main.rs index 76e922d..090f2a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,3 @@ -use crate::config::dns_canister_config::DnsCanisterConfig; use axum::{handler::Handler, routing::get, Extension, Router}; use clap::{crate_authors, crate_version, Parser}; use flate2::read::{DeflateDecoder, GzDecoder}; @@ -45,9 +44,12 @@ use std::{ }, }; +mod canister_id; mod config; mod logging; +use crate::config::dns_canister_config::DnsCanisterConfig; + type HttpResponseAny = HttpResponse; // Limit the total number of calls to an HTTP Request loop to 1000 for now. @@ -159,75 +161,16 @@ pub(crate) struct Opts { #[clap(long, default_value = "localhost")] dns_suffix: Vec, + /// Whether or not to ignore `canisterId=` when locating the canister. + #[clap(long)] + ignore_url_canister_param: bool, + /// Address to expose Prometheus metrics on /// Examples: 127.0.0.1:9090, [::1]:9090 #[clap(long)] metrics_addr: Option, } -fn resolve_canister_id_from_hostname( - hostname: &str, - dns_canister_config: &DnsCanisterConfig, -) -> Option { - let url = Uri::from_str(hostname).ok()?; - - let split_hostname = url.host()?.split('.').collect::>(); - let split_hostname = split_hostname.as_slice(); - - if let Some(principal) = - dns_canister_config.resolve_canister_id_from_split_hostname(split_hostname) - { - return Some(principal); - } - // Check if it's localhost or ic0. - match split_hostname { - [.., maybe_canister_id, "localhost"] => Principal::from_text(maybe_canister_id).ok(), - [maybe_canister_id, ..] => Principal::from_text(maybe_canister_id).ok(), - _ => None, - } -} - -fn resolve_canister_id_from_uri(url: &hyper::Uri) -> Option { - let (_, canister_id) = url::form_urlencoded::parse(url.query()?.as_bytes()) - .find(|(name, _)| name == "canisterId")?; - Principal::from_text(canister_id.as_ref()).ok() -} - -/// Try to resolve a canister ID from an HTTP Request. If it cannot be resolved, -/// [None] will be returned. -fn resolve_canister_id( - request: &Request, - dns_canister_config: &DnsCanisterConfig, -) -> Option { - // Look for subdomains if there's a host header. - if let Some(host_header) = request.headers().get("Host") { - if let Ok(host) = host_header.to_str() { - if let Some(canister_id) = resolve_canister_id_from_hostname(host, dns_canister_config) - { - return Some(canister_id); - } - } - } - - // Look into the URI. - if let Some(canister_id) = resolve_canister_id_from_uri(request.uri()) { - return Some(canister_id); - } - - // Look into the request by header. - if let Some(referer_header) = request.headers().get("referer") { - if let Ok(referer) = referer_header.to_str() { - if let Ok(referer_uri) = hyper::Uri::from_str(referer) { - if let Some(canister_id) = resolve_canister_id_from_uri(&referer_uri) { - return Some(canister_id); - } - } - } - } - - None -} - fn decode_hash_tree( name: &str, value: Option, @@ -320,10 +263,10 @@ fn extract_headers_data(headers: &[HeaderField], logger: &slog::Logger) -> Heade async fn forward_request( request: Request, agent: Arc, - dns_canister_config: &DnsCanisterConfig, + resolver: &dyn canister_id::Resolver, logger: slog::Logger, ) -> Result, Box> { - let canister_id = match resolve_canister_id(&request, dns_canister_config) { + let canister_id = match resolver.resolve(&request) { None => { return Ok(Response::builder() .status(StatusCode::BAD_REQUEST) @@ -795,17 +738,30 @@ fn unable_to_fetch_root_key() -> Result, Box> { .body("Unable to fetch root key".into())?) } -#[allow(clippy::too_many_arguments)] -async fn handle_request( +struct HandleRequest { ip_addr: IpAddr, request: Request, replica_url: String, client: reqwest::Client, proxy_url: Option, - dns_canister_config: Arc, + resolver: Arc>, logger: slog::Logger, fetch_root_key: bool, debug: bool, +} + +async fn handle_request( + HandleRequest { + ip_addr, + request, + replica_url, + client, + proxy_url, + resolver, + logger, + fetch_root_key, + debug, + }: HandleRequest, ) -> Result, Infallible> { let request_uri_path = request.uri().path(); let result = if request_uri_path.starts_with("/api/") { @@ -843,7 +799,7 @@ async fn handle_request( if fetch_root_key && agent.fetch_root_key().await.is_err() { unable_to_fetch_root_key() } else { - forward_request(request, agent, dns_canister_config.as_ref(), logger.clone()).await + forward_request(request, agent, resolver.as_ref(), logger.clone()).await } }; @@ -1071,7 +1027,11 @@ fn main() -> Result<(), Box> { // Prepare a list of agents for each backend replicas. let replicas = Mutex::new(opts.replica.clone()); - let dns_canister_config = Arc::new(DnsCanisterConfig::new(&opts.dns_alias, &opts.dns_suffix)?); + let dns = DnsCanisterConfig::new(&opts.dns_alias, &opts.dns_suffix)?; + let resolver = Arc::new(canister_id::DefaultResolver { + dns, + check_params: !opts.ignore_url_canister_param, + }); let counter = AtomicUsize::new(0); let debug = opts.debug; @@ -1081,7 +1041,7 @@ fn main() -> Result<(), Box> { let service = make_service_fn(|socket: &hyper::server::conn::AddrStream| { let ip_addr = socket.remote_addr(); let ip_addr = ip_addr.ip(); - let dns_canister_config = dns_canister_config.clone(); + let resolver = resolver.clone(); let logger = logger.clone(); // Select an agent. @@ -1097,20 +1057,20 @@ fn main() -> Result<(), Box> { let client = client.clone(); async move { - Ok::<_, Infallible>(service_fn(move |req| { + Ok::<_, Infallible>(service_fn(move |request| { let logger = logger.clone(); - let dns_canister_config = dns_canister_config.clone(); - handle_request( + let resolver = resolver.clone(); + handle_request(HandleRequest { ip_addr, - req, - replica_url.clone(), - client.clone(), - proxy_url.clone(), - dns_canister_config, + request, + replica_url: replica_url.clone(), + client: client.clone(), + proxy_url: proxy_url.clone(), + resolver, logger, fetch_root_key, debug, - ) + }) })) } });