From 68fcd7db69d42e0c678274e40f629bfd0fcf30d0 Mon Sep 17 00:00:00 2001 From: Eric Swanson <64809312+ericswanson-dfinity@users.noreply.github.com> Date: Mon, 1 Nov 2021 17:40:49 -0700 Subject: [PATCH] chore: remove icx-proxy (#279) Moved to https://github.com/dfinity/icx-proxy --- .github/workflows/release.yml | 4 +- Cargo.lock | 88 --- Cargo.toml | 1 - icx-proxy/Cargo.toml | 35 -- icx-proxy/README.md | 15 - icx-proxy/src/config/dns_canister_config.rs | 417 --------------- icx-proxy/src/config/dns_canister_rule.rs | 143 ----- icx-proxy/src/config/mod.rs | 2 - icx-proxy/src/logging.rs | 79 --- icx-proxy/src/main.rs | 562 -------------------- 10 files changed, 2 insertions(+), 1344 deletions(-) delete mode 100644 icx-proxy/Cargo.toml delete mode 100644 icx-proxy/README.md delete mode 100644 icx-proxy/src/config/dns_canister_config.rs delete mode 100644 icx-proxy/src/config/dns_canister_rule.rs delete mode 100644 icx-proxy/src/config/mod.rs delete mode 100644 icx-proxy/src/logging.rs delete mode 100644 icx-proxy/src/main.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 04e6e4fa..c9d40672 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,12 +15,12 @@ jobs: - os: ubuntu-latest name: linux binary_path: target/x86_64-unknown-linux-musl/release - binary_files: icx icx-proxy + binary_files: icx rust: 1.50.0 # gmiam/rust-musl-action@master is only at 1.50.0 now - os: macos-latest name: macos binary_path: target/release - binary_files: icx icx-proxy + binary_files: icx rust: 1.52.1 steps: - uses: actions/checkout@master diff --git a/Cargo.lock b/Cargo.lock index f1bbedfe..013c404b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,26 +382,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-channel" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ed27e177f16d65f0f0c22a213e17c696ace5dd64b14258b52f9417ccb52db4" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db" -dependencies = [ - "cfg-if", - "lazy_static", -] - [[package]] name = "crunchy" version = "0.2.2" @@ -1022,28 +1002,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "icx-proxy" -version = "0.7.0" -dependencies = [ - "anyhow", - "clap", - "clap_derive", - "garcon", - "hex", - "hyper", - "hyper-tls", - "ic-agent", - "ic-utils", - "serde", - "serde_json", - "slog", - "slog-async", - "slog-term", - "tokio", - "url", -] - [[package]] name = "idna" version = "0.2.3" @@ -2159,37 +2117,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" -[[package]] -name = "slog" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" - -[[package]] -name = "slog-async" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" -dependencies = [ - "crossbeam-channel", - "slog", - "take_mut", - "thread_local", -] - -[[package]] -name = "slog-term" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95c1e7e5aab61ced6006149ea772770b84a0d16ce0f7885def313e4829946d76" -dependencies = [ - "atty", - "chrono", - "slog", - "term", - "thread_local", -] - [[package]] name = "smallvec" version = "1.6.1" @@ -2271,12 +2198,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "take_mut" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" - [[package]] name = "tempfile" version = "3.2.0" @@ -2346,15 +2267,6 @@ dependencies = [ "syn", ] -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - [[package]] name = "time" version = "0.1.44" diff --git a/Cargo.toml b/Cargo.toml index c6e6d481..0a7c6064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,6 @@ members = [ "ic-utils", "icx", "icx-asset", - "icx-proxy", "ref-tests", "zzz-release", ] diff --git a/icx-proxy/Cargo.toml b/icx-proxy/Cargo.toml deleted file mode 100644 index 78151273..00000000 --- a/icx-proxy/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "icx-proxy" -version = "0.7.0" -authors = ["DFINITY Stiftung "] -edition = "2018" -description = "CLI tool to create an HTTP proxy to the Internet Computer." -homepage = "https://docs.rs/icx-proxy" -documentation = "https://docs.rs/icx-proxy" -license = "Apache-2.0" -readme = "README.md" -categories = ["command-line-interface", "web-programming::http-server"] -keywords = ["internet-computer", "agent", "icp", "dfinity", "proxy"] -include = ["src", "Cargo.toml", "../LICENSE", "README.md"] - -[[bin]] -name = "icx-proxy" -path = "src/main.rs" - -[dependencies] -anyhow = "1.0.34" -clap = "=3.0.0-beta.2" -clap_derive = "=3.0.0-beta.2" -garcon = { version = "0.2.3", features = ["async"] } -hex = "0.4.3" -hyper = { version = "0.14.13", features = ["full"] } -hyper-tls = "0.5.0" -ic-agent = { path = "../ic-agent", version = "0.9" } -ic-utils = { path = "../ic-utils", version = "0.7" } -tokio = { version = "1.8.1", features = ["full"] } -serde = "1.0.115" -serde_json = "1.0.57" -slog = { version = "2.7.0", features = ["max_level_trace"] } -slog-async = "2.7.0" -slog-term = "2.8.0" -url = "2.2.1" diff --git a/icx-proxy/README.md b/icx-proxy/README.md deleted file mode 100644 index d3505103..00000000 --- a/icx-proxy/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# `icx-proxy` -A command line tool to serve as a gateway for a Internet Computer replica. - -## Installing `icx-proxy` -One can install `icx-proxy` by running cargo; - -```bash -cargo install icx-proxy -``` - -## Usage -Once installed, using `icx-proxy --help` will show the usage message and all the flags. - -## Ecosystem -This is similar in principle to `dfx bootstrap`, but is simpler and more configurable. This also can replace a Replica when using the `--network` flag in `dfx`. diff --git a/icx-proxy/src/config/dns_canister_config.rs b/icx-proxy/src/config/dns_canister_config.rs deleted file mode 100644 index 60668260..00000000 --- a/icx-proxy/src/config/dns_canister_config.rs +++ /dev/null @@ -1,417 +0,0 @@ -use crate::config::dns_canister_rule::DnsCanisterRule; -use ic_agent::ic_types::Principal; -use std::cmp::Reverse; - -/// Configuration for determination of Domain Name to Principal -#[derive(Clone, Debug)] -pub struct DnsCanisterConfig { - rules: Vec, -} - -impl DnsCanisterConfig { - /// Create a DnsCanisterConfig instance from command-line configuration. - /// dns_aliases: 0 or more entries of the form of dns.alias:canister-id - /// dns_suffixes: 0 or more domain names which will match as a suffix - pub fn new( - dns_aliases: &[String], - dns_suffixes: &[String], - ) -> anyhow::Result { - let mut rules = vec![]; - for suffix in dns_suffixes { - rules.push(DnsCanisterRule::new_suffix(suffix)); - } - for alias in dns_aliases { - rules.push(DnsCanisterRule::new_alias(alias)?); - } - // 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())); - Ok(DnsCanisterConfig { rules }) - } - - /// Return the Principal of the canister that matches the host name. - /// - /// 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(); - self.rules - .iter() - .find_map(|rule| rule.lookup(&split_hostname_lowercase)) - } -} - -#[cfg(test)] -mod tests { - use crate::config::dns_canister_config::DnsCanisterConfig; - use ic_agent::ic_types::Principal; - - #[test] - fn matches_whole_hostname() { - let dns_aliases = - parse_dns_aliases(vec!["happy.little.domain.name:r7inp-6aaaa-aaaaa-aaabq-cai"]) - .unwrap(); - - assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["happy", "little", "domain", "name"]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ) - } - - #[test] - fn matches_partial_hostname() { - let dns_aliases = - 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"]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ) - } - - #[test] - fn extraneous_does_not_match() { - let dns_aliases = parse_dns_aliases(vec![ - "very.happy.little.domain.name:r7inp-6aaaa-aaaaa-aaabq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["happy", "little", "domain", "name"]), - None - ) - } - - #[test] - fn case_insensitive_match() { - let dns_aliases = - 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"]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ) - } - - #[test] - fn chooses_among_many() { - let dns_aliases = parse_dns_aliases(vec![ - "happy.little.domain.name:r7inp-6aaaa-aaaaa-aaabq-cai", - "ecstatic.domain.name:rrkah-fqaaa-aaaaa-aaaaq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["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"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - - assert_eq!( - dns_aliases - .resolve_canister_id_from_split_hostname(&["super", "ecstatic", "domain", "name"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ) - } - - #[test] - fn chooses_first_match() { - let dns_aliases = parse_dns_aliases(vec![ - "specific.of.many:r7inp-6aaaa-aaaaa-aaabq-cai", - "of.many:rrkah-fqaaa-aaaaa-aaaaq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["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"]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ); - - assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["another", "of", "many"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ) - } - - #[test] - fn searches_longest_to_shortest() { - // If we checked these in the order passed, a.b.c would erroneously resolve - // to the canister id associated with b.c - let dns_aliases = parse_dns_aliases(vec![ - "b.c:rrkah-fqaaa-aaaaa-aaaaq-cai", - "a.b.c:r7inp-6aaaa-aaaaa-aaabq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["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"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn searches_longest_to_shortest_even_if_already_ordered() { - // Similar to searches_longest_to_shortest, just to ensure that - // we do the right thing no matter which order they are passed. - let dns_aliases = parse_dns_aliases(vec![ - "a.b.c:r7inp-6aaaa-aaaaa-aaabq-cai", - "b.c:rrkah-fqaaa-aaaaa-aaaaq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["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"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn searches_longest_to_shortest_not_alpha() { - // Similar to searches_longest_to_shortest, but make sure we - // don't happen to get there by sorting alphabetically - let dns_aliases = parse_dns_aliases(vec![ - "x.c:rrkah-fqaaa-aaaaa-aaaaq-cai", - "a.x.c:r7inp-6aaaa-aaaaa-aaabq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["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"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn searches_longest_to_shortest_not_alpha_reversed() { - // Similar to searches_longest_to_shortest, but make sure we - // don't happen to get there by sorting alphabetically/reversed - let dns_aliases = parse_dns_aliases(vec![ - "a.c:rrkah-fqaaa-aaaaa-aaaaq-cai", - "x.a.c:r7inp-6aaaa-aaaaa-aaabq-cai", - ]) - .unwrap(); - - assert_eq!( - dns_aliases.resolve_canister_id_from_split_hostname(&["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"]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn dns_suffix_localhost_canister_found() { - let config = parse_config(vec![], vec!["localhost"]).unwrap(); - - assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "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" - ]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ) - } - - #[test] - fn dns_suffix_localhost_more_domain_names_ok() { - let config = parse_config(vec![], vec!["localhost"]).unwrap(); - - assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "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" - ]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ) - } - - #[test] - fn dns_suffix_must_immediately_precede_suffix() { - let config = parse_config(vec![], vec!["localhost"]).unwrap(); - - assert_eq!( - config.resolve_canister_id_from_split_hostname(&[ - "rrkah-fqaaa-aaaaa-aaaaq-cai", - "nope", - "localhost" - ]), - None - ); - } - - #[test] - fn dns_suffix_longer_suffix_ok() { - 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" - ]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn dns_suffix_longer_suffix_still_requires_exact_positionok() { - 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" - ]), - None - ); - } - - #[test] - fn dns_suffix_longer_suffix_can_be_preceded_by_more() { - 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" - ]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn dns_suffix_ignores_earlier_canister_ids() { - 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" - ]), - Some(Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap()) - ); - } - - #[test] - fn aliases_and_suffixes() { - let config = parse_config( - vec![ - "a.b.c:r7inp-6aaaa-aaaaa-aaabq-cai", - "d.e:rrkah-fqaaa-aaaaa-aaaaq-cai", - ], - vec!["g.h.i"], - ) - .unwrap(); - - assert_eq!( - config.resolve_canister_id_from_split_hostname(&["a", "b", "c"]), - Some(Principal::from_text("r7inp-6aaaa-aaaaa-aaabq-cai").unwrap()) - ); - assert_eq!( - config.resolve_canister_id_from_split_hostname(&["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", - ]), - Some(Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap()) - ); - } - - #[test] - fn same_alias_and_suffix_prefers_alias() { - // because the suffix will only match if preceded by a canister id - let config = - 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"]), - 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" - ]), - 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, &[]) - } - - fn parse_config(aliases: Vec<&str>, suffixes: Vec<&str>) -> anyhow::Result { - 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) - } -} diff --git a/icx-proxy/src/config/dns_canister_rule.rs b/icx-proxy/src/config/dns_canister_rule.rs deleted file mode 100644 index 0fe0bea2..00000000 --- a/icx-proxy/src/config/dns_canister_rule.rs +++ /dev/null @@ -1,143 +0,0 @@ -use ic_agent::ic_types::Principal; - -use anyhow::anyhow; - -const DNS_ALIAS_FORMAT_HELP: &str = "Format is dns.alias:principal-id"; - -#[derive(Clone, Debug)] -enum PrincipalDeterminationStrategy { - // A domain name which matches the suffix is an alias for this specific Principal. - Alias(Principal), - - // The subdomain to the immediate left of the suffix is the Principal, - // if it parses as a valid Principal. - PrecedingDomainName, -} - -/// A mapping from a domain name to a Principal. The domain name must -/// 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, - - strategy: PrincipalDeterminationStrategy, -} - -impl DnsCanisterRule { - /// Create a rule for a domain name alias with form dns.alias:canister-id - pub fn new_alias(dns_alias: &str) -> anyhow::Result { - 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), - }) - } - - /// Create a rule which for domain names that match the specified suffix, - /// if the preceding subdomain parses as a principal, return that principal. - 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 - } - } - } - } else { - None - } - } -} - -fn split_hostname_lowercase(hostname: &str) -> Vec { - hostname - .split('.') - .map(|s| s.to_ascii_lowercase()) - .collect() -} - -fn split_dns_alias(alias: &str) -> Result<(String, Principal), anyhow::Error> { - match alias.find(':') { - Some(0) => Err(anyhow!( - r#"No domain specifed in DNS alias "{}". {}"#, - alias.to_string(), - DNS_ALIAS_FORMAT_HELP - )), - Some(index) if index == alias.len() - 1 => Err(anyhow!( - r#"No canister ID specifed in DNS alias "{}". {}"#, - alias.to_string(), - DNS_ALIAS_FORMAT_HELP - )), - Some(index) => { - let (domain_name, principal) = alias.split_at(index); - let principal = &principal[1..]; - let principal = Principal::from_text(principal)?; - Ok((domain_name.to_string(), principal)) - } - None => Err(anyhow!( - r#"Unrecognized DNS alias "{}". {}"#, - alias.to_string(), - DNS_ALIAS_FORMAT_HELP, - )), - } -} - -#[cfg(test)] -mod tests { - use crate::config::dns_canister_rule::DnsCanisterRule; - - #[test] - fn parse_error_no_colon() { - let e = parse_dns_alias("happy.little.domain.name!r7inp-6aaaa-aaaaa-aaabq-cai") - .expect_err("expected failure due to missing colon"); - assert_eq!( - e.to_string(), - r#"Unrecognized DNS alias "happy.little.domain.name!r7inp-6aaaa-aaaaa-aaabq-cai". Format is dns.alias:principal-id"# - ) - } - - #[test] - fn parse_error_nothing_after_colon() { - let e = parse_dns_alias("happy.little.domain.name:") - .expect_err("expected failure due to nothing after colon"); - assert_eq!( - e.to_string(), - r#"No canister ID specifed in DNS alias "happy.little.domain.name:". Format is dns.alias:principal-id"# - ) - } - - #[test] - fn parse_error_nothing_before_colon() { - let e = parse_dns_alias(":r7inp-6aaaa-aaaaa-aaabq-cai") - .expect_err("expected failure due to nothing after colon"); - assert_eq!( - e.to_string(), - r#"No domain specifed in DNS alias ":r7inp-6aaaa-aaaaa-aaabq-cai". Format is dns.alias:principal-id"# - ) - } - - fn parse_dns_alias(alias: &str) -> anyhow::Result { - DnsCanisterRule::new_alias(alias) - } -} diff --git a/icx-proxy/src/config/mod.rs b/icx-proxy/src/config/mod.rs deleted file mode 100644 index 8d354814..00000000 --- a/icx-proxy/src/config/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod dns_canister_config; -mod dns_canister_rule; diff --git a/icx-proxy/src/logging.rs b/icx-proxy/src/logging.rs deleted file mode 100644 index d04e1b20..00000000 --- a/icx-proxy/src/logging.rs +++ /dev/null @@ -1,79 +0,0 @@ -use crate::Opts; -use slog::{Drain, Level, LevelFilter, Logger}; -use std::{fs::File, path::PathBuf}; - -/// The logging mode to use. -enum LoggingMode { - /// The default mode for logging; output without any decoration, to STDERR. - Stderr, - - /// Tee logging to a file (in addition to STDERR). This mimics the verbose flag. - /// So it would be similar to `dfx ... |& tee /some/file.txt - Tee(PathBuf), - - /// Output Debug logs and up to a file, regardless of verbosity, keep the STDERR output - /// the same (with verbosity). - File(PathBuf), -} - -fn create_drain(mode: LoggingMode) -> Logger { - match mode { - LoggingMode::File(out) => { - let file = File::create(out).expect("Couldn't open log file"); - let decorator = slog_term::PlainDecorator::new(file); - let drain = slog_term::FullFormat::new(decorator).build().fuse(); - Logger::root(slog_async::Async::new(drain).build().fuse(), slog::o!()) - } - // A Tee mode is basically 2 drains duplicated. - LoggingMode::Tee(out) => Logger::root( - slog::Duplicate::new( - create_drain(LoggingMode::Stderr), - create_drain(LoggingMode::File(out)), - ) - .fuse(), - slog::o!(), - ), - LoggingMode::Stderr => { - let decorator = slog_term::PlainDecorator::new(std::io::stderr()); - let drain = slog_term::CompactFormat::new(decorator).build().fuse(); - Logger::root(slog_async::Async::new(drain).build().fuse(), slog::o!()) - } - } -} - -pub(crate) fn setup_logging(opts: &Opts) -> Logger { - // Create a logger with our argument matches. - let verbose_level = opts.verbose as i64 - opts.quiet as i64; - let logfile = opts.logfile.clone().unwrap_or_else(|| "log.txt".into()); - - let mode = match opts.logmode.as_str() { - "tee" => LoggingMode::Tee(logfile), - "file" => LoggingMode::File(logfile), - "stderr" => LoggingMode::Stderr, - _ => unreachable!("unhandled logmode"), - }; - - let log_level = match verbose_level { - -3 => Level::Critical, - -2 => Level::Error, - -1 => Level::Warning, - 0 => Level::Info, - 1 => Level::Debug, - 2 => Level::Trace, - x => { - if x > 0 { - Level::Trace - } else { - // Silent. - return Logger::root(slog::Discard, slog::o!()); - } - } - }; - - let drain = LevelFilter::new(create_drain(mode), log_level).fuse(); - let drain = slog_async::Async::new(drain).build().fuse(); - - let root = Logger::root(drain, slog::o!("version" => clap::crate_version!())); - slog::info!(root, "Log Level: {}", log_level); - root -} diff --git a/icx-proxy/src/main.rs b/icx-proxy/src/main.rs deleted file mode 100644 index a365abf8..00000000 --- a/icx-proxy/src/main.rs +++ /dev/null @@ -1,562 +0,0 @@ -use crate::config::dns_canister_config::DnsCanisterConfig; -use clap::{crate_authors, crate_version, AppSettings, Clap}; -use hyper::{ - body, - body::Bytes, - http::uri::Parts, - service::{make_service_fn, service_fn}, - Body, Client, Request, Response, Server, StatusCode, Uri, -}; -use ic_agent::{ - agent::http_transport::ReqwestHttpReplicaV2Transport, export::Principal, Agent, AgentError, -}; -use ic_utils::{ - call::SyncCall, - interfaces::http_request::{ - HeaderField, HttpRequestCanister, StreamingCallbackHttpResponse, StreamingStrategy, - }, -}; -use slog::Drain; -use std::{ - convert::Infallible, - error::Error, - net::{IpAddr, SocketAddr}, - path::PathBuf, - str::FromStr, - sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, Mutex, - }, -}; - -mod config; -mod logging; - -// Limit the total number of calls to an HTTP Request loop to 1000 for now. -static MAX_HTTP_REQUEST_STREAM_CALLBACK_CALL_COUNT: i32 = 1000; - -// The maximum length of a body we should log as tracing. -static MAX_LOG_BODY_SIZE: usize = 100; - -#[derive(Clap)] -#[clap( - version = crate_version!(), - author = crate_authors!(), - global_setting = AppSettings::GlobalVersion, - global_setting = AppSettings::ColoredHelp -)] -pub(crate) struct Opts { - /// Verbose level. By default, INFO will be used. Add a single `-v` to upgrade to - /// DEBUG, and another `-v` to upgrade to TRACE. - #[clap(long, short('v'), parse(from_occurrences))] - verbose: u64, - - /// Quiet level. The opposite of verbose. A single `-q` will drop the logging to - /// WARN only, then another one to ERR, and finally another one for FATAL. Another - /// `-q` will silence ALL logs. - #[clap(long, short('q'), parse(from_occurrences))] - quiet: u64, - - /// Mode to use the logging. "stderr" will output logs in STDERR, "file" will output - /// logs in a file, and "tee" will do both. - #[clap(long("log"), default_value("stderr"), possible_values(&["stderr", "tee", "file"]))] - logmode: String, - - /// File to output the log to, when using logmode=tee or logmode=file. - #[clap(long)] - logfile: Option, - - /// The address to bind to. - #[clap(long, default_value = "127.0.0.1:3000")] - address: SocketAddr, - - /// A replica to use as backend. Locally, this should be a local instance or the - /// boundary node. Multiple replicas can be passed and they'll be used round-robin. - #[clap(long, default_value = "http://localhost:8000/")] - replica: Vec, - - /// An address to forward any requests from /_/ - #[clap(long)] - proxy: Option, - - /// Whether or not this is run in a debug context (e.g. errors returned in responses - /// should show full stack and error details). - #[clap(long)] - debug: bool, - - /// Whether or not to fetch the root key from the replica back end. Do not use this when - /// talking to the Internet Computer blockchain mainnet as it is unsecure. - #[clap(long)] - fetch_root_key: bool, - - /// A map of domain names to canister IDs. - /// Format: domain.name:canister-id - #[clap(long)] - dns_alias: Vec, - - /// A list of domain name suffixes. If found, the next (to the left) subdomain - /// is used as the Principal, if it parses as a Principal. - #[clap(long, default_value = "localhost")] - dns_suffix: Vec, -} - -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 -} - -async fn forward_request( - request: Request, - agent: Arc, - dns_canister_config: &DnsCanisterConfig, - logger: slog::Logger, -) -> Result, Box> { - let canister_id = match resolve_canister_id(&request, dns_canister_config) { - None => { - return Ok(Response::builder() - .status(StatusCode::BAD_REQUEST) - .body("Could not find a canister id to forward to.".into()) - .unwrap()) - } - Some(x) => x, - }; - - slog::trace!( - logger, - "<< {} {} {:?}", - request.method(), - request.uri(), - &request.version() - ); - - let method = request.method().to_string(); - let uri = request.uri().clone(); - let headers = request - .headers() - .into_iter() - .filter_map(|(name, value)| { - Some(HeaderField( - name.to_string(), - value.to_str().ok()?.to_string(), - )) - }) - .inspect(|HeaderField(name, value)| { - slog::trace!(logger, "<< {}: {}", name, value); - }) - .collect(); - - let entire_body = body::to_bytes(request.into_body()).await?.to_vec(); - - slog::trace!(logger, "<<"); - if logger.is_trace_enabled() { - let body = String::from_utf8_lossy(&entire_body); - slog::trace!( - logger, - "<< {}{}", - &body[0..usize::min(body.len(), MAX_LOG_BODY_SIZE)], - if body.len() > MAX_LOG_BODY_SIZE { - format!("... {} bytes total", body.len()) - } else { - String::new() - } - ); - } - - let canister = HttpRequestCanister::create(agent.as_ref(), canister_id); - let result = canister - .http_request(method, uri.to_string(), headers, &entire_body) - .call() - .await; - - // If the result is a Replica error, returns the 500 code and message. There is no information - // leak here because a user could use `dfx` to get the same reply. - let (http_response,) = match result { - Ok(response) => response, - Err(AgentError::ReplicaError { - reject_code, - reject_message, - }) => { - return Ok(Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(format!(r#"Replica Error ({}): "{}""#, reject_code, reject_message).into()) - .unwrap()); - } - Err(e) => return Err(e.into()), - }; - - let mut builder = Response::builder().status(StatusCode::from_u16(http_response.status_code)?); - for HeaderField(name, value) in http_response.headers { - builder = builder.header(&name, value); - } - - let body = if logger.is_trace_enabled() { - Some(http_response.body.clone()) - } else { - None - }; - let is_streaming = http_response.streaming_strategy.is_some(); - let response = if let Some(streaming_strategy) = http_response.streaming_strategy { - let (mut sender, body) = body::Body::channel(); - let agent = agent.as_ref().clone(); - sender.send_data(Bytes::from(http_response.body)).await?; - - match streaming_strategy { - StreamingStrategy::Callback(callback) => { - let streaming_canister_id_id = callback.callback.principal; - let method_name = callback.callback.method; - let mut callback_token = callback.token; - let logger = logger.clone(); - tokio::spawn(async move { - let canister = HttpRequestCanister::create(&agent, streaming_canister_id_id); - // We have not yet called http_request_stream_callback. - let mut count = 0; - loop { - count += 1; - if count > MAX_HTTP_REQUEST_STREAM_CALLBACK_CALL_COUNT { - sender.abort(); - break; - } - - match canister - .http_request_stream_callback(&method_name, callback_token) - .call() - .await - { - Ok((StreamingCallbackHttpResponse { body, token },)) => { - if sender.send_data(Bytes::from(body)).await.is_err() { - sender.abort(); - break; - } - if let Some(next_token) = token { - callback_token = next_token; - } else { - break; - } - } - Err(e) => { - slog::debug!(logger, "Error happened during streaming: {}", e); - sender.abort(); - break; - } - } - } - }); - } - } - - builder.body(body)? - } else { - builder.body(http_response.body.into())? - }; - - if logger.is_trace_enabled() { - slog::trace!( - logger, - ">> {:?} {} {}", - &response.version(), - response.status().as_u16(), - response.status().to_string() - ); - - for (name, value) in response.headers() { - let value = String::from_utf8_lossy(value.as_bytes()); - slog::trace!(logger, ">> {}: {}", name, value); - } - - let body = body.unwrap_or_else(|| b"... streaming ...".to_vec()); - - slog::trace!(logger, ">>"); - slog::trace!( - logger, - ">> {}{}", - match std::str::from_utf8(&body) { - Ok(s) => format!( - r#""{}""#, - s[..usize::min(MAX_LOG_BODY_SIZE, s.len())].escape_default() - ), - Err(_) => hex::encode(&body[..usize::min(MAX_LOG_BODY_SIZE, body.len())]), - }, - if is_streaming { - "... streaming".to_string() - } else if body.len() > MAX_LOG_BODY_SIZE { - format!("... {} bytes total", body.len()) - } else { - String::new() - } - ); - } - - Ok(response) -} - -fn is_hop_header(name: &str) -> bool { - name.to_ascii_lowercase() == "connection" - || name.to_ascii_lowercase() == "keep-alive" - || name.to_ascii_lowercase() == "proxy-authenticate" - || name.to_ascii_lowercase() == "proxy-authorization" - || name.to_ascii_lowercase() == "te" - || name.to_ascii_lowercase() == "trailers" - || name.to_ascii_lowercase() == "transfer-encoding" - || name.to_ascii_lowercase() == "upgrade" -} - -/// Returns a clone of the headers without the [hop-by-hop headers]. -/// -/// [hop-by-hop headers]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html -fn remove_hop_headers( - headers: &hyper::header::HeaderMap, -) -> hyper::header::HeaderMap { - let mut result = hyper::HeaderMap::new(); - for (k, v) in headers.iter() { - if !is_hop_header(k.as_str()) { - result.insert(k.clone(), v.clone()); - } - } - result -} - -fn forward_uri(forward_url: &str, req: &Request) -> Result> { - let uri = Uri::from_str(forward_url)?; - let mut parts = Parts::from(uri); - parts.path_and_query = req.uri().path_and_query().cloned(); - - Ok(Uri::from_parts(parts)?) -} - -fn create_proxied_request( - client_ip: &IpAddr, - forward_url: &str, - mut request: Request, -) -> Result, Box> { - *request.headers_mut() = remove_hop_headers(request.headers()); - *request.uri_mut() = forward_uri(forward_url, &request)?; - - let x_forwarded_for_header_name = "x-forwarded-for"; - - // Add forwarding information in the headers - match request.headers_mut().entry(x_forwarded_for_header_name) { - hyper::header::Entry::Vacant(entry) => { - entry.insert(client_ip.to_string().parse()?); - } - - hyper::header::Entry::Occupied(mut entry) => { - let addr = format!("{}, {}", entry.get().to_str()?, client_ip); - entry.insert(addr.parse()?); - } - } - - Ok(request) -} - -async fn forward_api( - ip_addr: &IpAddr, - request: Request, - replica_url: &str, -) -> Result, Box> { - let proxied_request = create_proxied_request(ip_addr, &replica_url, request)?; - - let client = Client::builder().build(hyper_tls::HttpsConnector::new()); - let response = client.request(proxied_request).await?; - Ok(response) -} - -fn not_found() -> Result, Box> { - Ok(Response::builder() - .status(StatusCode::NOT_FOUND) - .body("Not found".into())?) -} - -fn unable_to_fetch_root_key() -> Result, Box> { - Ok(Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body("Unable to fetch root key".into())?) -} - -#[allow(clippy::too_many_arguments)] -async fn handle_request( - ip_addr: IpAddr, - request: Request, - replica_url: String, - proxy_url: Option, - dns_canister_config: Arc, - logger: slog::Logger, - fetch_root_key: bool, - debug: bool, -) -> Result, Infallible> { - let request_uri_path = request.uri().path(); - match if request_uri_path.starts_with("/api/") { - slog::debug!( - logger, - "URI Request to path '{}' being forwarded to Replica", - &request.uri().path() - ); - forward_api(&ip_addr, request, &replica_url).await - } else if request_uri_path.starts_with("/_/") { - if let Some(proxy_url) = proxy_url { - slog::debug!( - logger, - "URI Request to path '{}' being forwarded to proxy", - &request.uri().path(), - ); - forward_api(&ip_addr, request, &proxy_url).await - } else { - slog::warn!( - logger, - "Unable to proxy {} because no --proxy is configured", - &request.uri().path() - ); - not_found() - } - } else { - let agent = Arc::new( - ic_agent::Agent::builder() - .with_transport(ReqwestHttpReplicaV2Transport::create(replica_url).unwrap()) - .build() - .expect("Could not create agent..."), - ); - 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 - } - } { - Err(err) => { - slog::warn!(logger, "Internal Error during request:\n{:#?}", err); - - Ok(Response::builder() - .status(StatusCode::INTERNAL_SERVER_ERROR) - .body(if debug { - format!("Internal Error: {:?}", err).into() - } else { - "Internal Server Error".into() - }) - .unwrap()) - } - Ok(x) => Ok::<_, Infallible>(x), - } -} - -fn main() -> Result<(), Box> { - let opts: Opts = Opts::parse(); - - let logger = logging::setup_logging(&opts); - - // 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 counter = AtomicUsize::new(0); - let debug = opts.debug; - let proxy_url = opts.proxy.clone(); - let fetch_root_key = opts.fetch_root_key; - - 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 logger = logger.clone(); - - // Select an agent. - let replica_url_array = replicas.lock().unwrap(); - let count = counter.fetch_add(1, Ordering::SeqCst); - let replica_url = replica_url_array - .get(count % replica_url_array.len()) - .unwrap_or_else(|| unreachable!()); - let replica_url = replica_url.clone(); - slog::debug!(logger, "Replica URL: {}", replica_url); - - let proxy_url = proxy_url.clone(); - - async move { - Ok::<_, Infallible>(service_fn(move |req| { - let logger = logger.clone(); - let dns_canister_config = dns_canister_config.clone(); - handle_request( - ip_addr, - req, - replica_url.clone(), - proxy_url.clone(), - dns_canister_config, - logger, - fetch_root_key, - debug, - ) - })) - } - }); - - slog::info!( - logger, - "Starting server. Listening on http://{}/", - opts.address - ); - - let runtime = tokio::runtime::Builder::new_multi_thread() - .worker_threads(10) - .enable_all() - .build()?; - runtime.block_on(async { - let server = Server::bind(&opts.address).serve(service); - server.await?; - Ok(()) - }) -}