diff --git a/Cargo.lock b/Cargo.lock index f03ae67be9..0cb765cdec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,6 +397,12 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "data-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993a608597367c6377b258c25d7120740f00ed23a2252b729b1932dd7866f908" + [[package]] name = "deno" version = "1.6.3" @@ -449,6 +455,8 @@ dependencies = [ "tokio", "tokio-rustls", "tower-test", + "trust-dns-client", + "trust-dns-server", "uuid", "walkdir", "winapi 0.3.9", @@ -564,6 +572,8 @@ dependencies = [ "test_util", "tokio", "tokio-rustls", + "trust-dns-proto", + "trust-dns-resolver", "uuid", "webpki", "webpki-roots", @@ -698,6 +708,24 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +dependencies = [ + "heck", + "proc-macro2 1.0.24", + "quote 1.0.7", + "syn 1.0.56", +] + [[package]] name = "enum_kind" version = "0.2.0" @@ -1080,6 +1108,17 @@ dependencies = [ "libc", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi 0.3.9", +] + [[package]] name = "http" version = "0.2.3" @@ -1238,6 +1277,18 @@ dependencies = [ "libc", ] +[[package]] +name = "ipconfig" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7e2f18aece9709094573a9f24f483c4f65caa4298e2f7ae1b71cc65d853fad7" +dependencies = [ + "socket2", + "widestring", + "winapi 0.3.9", + "winreg 0.6.2", +] + [[package]] name = "ipnet" version = "2.3.0" @@ -1306,6 +1357,12 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929" +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + [[package]] name = "lock_api" version = "0.4.2" @@ -1325,6 +1382,15 @@ dependencies = [ "serde", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "lsp-types" version = "0.86.0" @@ -1373,6 +1439,12 @@ dependencies = [ "syn 1.0.56", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matches" version = "0.1.8" @@ -1490,6 +1562,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + [[package]] name = "nix" version = "0.19.1" @@ -1848,6 +1929,12 @@ dependencies = [ "libc", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "0.6.13" @@ -1878,6 +1965,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce082a9940a7ace2ad4a8b7d0b1eac6aa378895f18be598230c5f2284ac05426" +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + [[package]] name = "rand" version = "0.7.3" @@ -2042,7 +2139,17 @@ dependencies = [ "wasm-bindgen-futures", "web-sys", "webpki-roots", - "winreg", + "winreg 0.7.0", +] + +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", ] [[package]] @@ -3030,6 +3137,95 @@ dependencies = [ "tracing", ] +[[package]] +name = "trust-dns-client" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498e4de74132fb535c0608d0f221a3e64ddf6585717296afb2baf5925b38f31f" +dependencies = [ + "cfg-if 1.0.0", + "chrono", + "data-encoding", + "futures-channel", + "futures-util", + "lazy_static", + "log", + "radix_trie", + "rand 0.8.1", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "trust-dns-proto" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a0381b2864c2978db7f8e17c7b23cca5a3a5f99241076e13002261a8ecbabd" +dependencies = [ + "async-trait", + "cfg-if 1.0.0", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "lazy_static", + "log", + "rand 0.8.1", + "serde", + "smallvec", + "thiserror", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3072d18c10bd621cb00507d59cfab5517862285c353160366e37fbf4c74856e4" +dependencies = [ + "cfg-if 1.0.0", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot", + "resolv-conf", + "serde", + "smallvec", + "thiserror", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "trust-dns-server" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9da8b74feb06ae242b03f40f8da3a414e37827118200fcb3d03d8f825cbbff2c" +dependencies = [ + "async-trait", + "bytes", + "cfg-if 1.0.0", + "chrono", + "enum-as-inner", + "env_logger", + "futures-executor", + "futures-util", + "log", + "serde", + "thiserror", + "tokio", + "toml", + "trust-dns-client", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.3" @@ -3301,6 +3497,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "widestring" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" + [[package]] name = "winapi" version = "0.2.8" @@ -3344,6 +3546,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winreg" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "winreg" version = "0.7.0" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 38809706f9..04861e3027 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -91,6 +91,8 @@ chrono = "0.4.19" os_pipe = "0.9.2" test_util = { path = "../test_util" } tower-test = "0.4.0" +trust-dns-server = "0.20.0" +trust-dns-client = "0.20.0" [target.'cfg(unix)'.dev-dependencies] exec = "0.3.1" # Used in test_raw_tty diff --git a/cli/dts/lib.deno.unstable.d.ts b/cli/dts/lib.deno.unstable.d.ts index 1de7ed8cc9..c73703368a 100644 --- a/cli/dts/lib.deno.unstable.d.ts +++ b/cli/dts/lib.deno.unstable.d.ts @@ -835,6 +835,92 @@ declare namespace Deno { mtime: number | Date, ): Promise; + /** The type of the resource record. + * Only the listed types are supported currently. */ + export type RecordType = + | "A" + | "AAAA" + | "ANAME" + | "CNAME" + | "MX" + | "PTR" + | "SRV" + | "TXT"; + + export interface ResolveDnsOptions { + /** The name server to be used for lookups. + * If not specified, defaults to the system configuration e.g. `/etc/resolv.conf` on Unix. */ + nameServer?: { + /** The IP address of the name server */ + ipAddr: string; + /** The port number the query will be sent to. + * If not specified, defaults to 53. */ + port?: number; + }; + } + + /** If `resolveDns` is called with "MX" record type specified, it will return an array of this interface. */ + export interface MXRecord { + preference: number; + exchange: string; + } + + /** If `resolveDns` is called with "SRV" record type specified, it will return an array of this interface. */ + export interface SRVRecord { + priority: number; + weight: number; + port: number; + target: string; + } + + export function resolveDns( + query: string, + recordType: "A" | "AAAA" | "ANAME" | "CNAME" | "PTR", + options?: ResolveDnsOptions, + ): Promise; + + export function resolveDns( + query: string, + recordType: "MX", + options?: ResolveDnsOptions, + ): Promise; + + export function resolveDns( + query: string, + recordType: "SRV", + options?: ResolveDnsOptions, + ): Promise; + + export function resolveDns( + query: string, + recordType: "TXT", + options?: ResolveDnsOptions, + ): Promise; + + /** ** UNSTABLE**: new API, yet to be vetted. + * + * Performs DNS resolution against the given query, returning resolved records. + * Fails in the cases such as: + * - the query is in invalid format + * - the options have an invalid parameter, e.g. `nameServer.port` is beyond the range of 16-bit unsigned integer + * - timed out + * + * ```ts + * const a = await Deno.resolveDns("example.com", "A"); + * + * const aaaa = await Deno.resolveDns("example.com", "AAAA", { + * nameServer: { ipAddr: "8.8.8.8", port: 1234 }, + * }); + * ``` + * + * Requires `allow-net` permission. + */ + export function resolveDns( + query: string, + recordType: RecordType, + options?: ResolveDnsOptions, + ): Promise; + /** **UNSTABLE**: new API, yet to be vetted. * * A generic transport listener for message-oriented protocols. */ diff --git a/cli/tests/integration_tests.rs b/cli/tests/integration_tests.rs index 9cb5cd5f11..0af4709fba 100644 --- a/cli/tests/integration_tests.rs +++ b/cli/tests/integration_tests.rs @@ -5367,3 +5367,279 @@ fn web_platform_tests() { } } } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_resolve_dns() { + use std::collections::BTreeMap; + use std::net::Ipv4Addr; + use std::net::Ipv6Addr; + use std::net::SocketAddr; + use std::str::FromStr; + use std::sync::Arc; + use std::sync::RwLock; + use std::time::Duration; + use tokio::net::TcpListener; + use tokio::net::UdpSocket; + use tokio::sync::oneshot; + use trust_dns_client::rr::LowerName; + use trust_dns_client::rr::RecordType; + use trust_dns_client::rr::RrKey; + use trust_dns_server::authority::Catalog; + use trust_dns_server::authority::ZoneType; + use trust_dns_server::proto::rr::rdata::mx::MX; + use trust_dns_server::proto::rr::rdata::soa::SOA; + use trust_dns_server::proto::rr::rdata::srv::SRV; + use trust_dns_server::proto::rr::rdata::txt::TXT; + use trust_dns_server::proto::rr::record_data::RData; + use trust_dns_server::proto::rr::resource::Record; + use trust_dns_server::proto::rr::Name; + use trust_dns_server::proto::rr::RecordSet; + use trust_dns_server::store::in_memory::InMemoryAuthority; + use trust_dns_server::ServerFuture; + + const DNS_PORT: u16 = 4553; + + // Setup DNS server for testing + async fn run_dns_server(tx: oneshot::Sender<()>) { + let catalog = { + let records = { + let mut map = BTreeMap::new(); + let lookup_name = "www.example.com".parse::().unwrap(); + let lookup_name_lower = LowerName::new(&lookup_name); + + // Inserts SOA record + let soa = SOA::new( + Name::from_str("net").unwrap(), + Name::from_str("example").unwrap(), + 0, + i32::MAX, + i32::MAX, + i32::MAX, + 0, + ); + let rdata = RData::SOA(soa); + let record = Record::from_rdata(Name::new(), u32::MAX, rdata); + let record_set = RecordSet::from(record); + map + .insert(RrKey::new(Name::root().into(), RecordType::SOA), record_set); + + // Inserts A record + let rdata = RData::A(Ipv4Addr::new(1, 2, 3, 4)); + let record = Record::from_rdata(lookup_name.clone(), u32::MAX, rdata); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new(lookup_name_lower.clone(), RecordType::A), + record_set, + ); + + // Inserts AAAA record + let rdata = RData::AAAA(Ipv6Addr::new(1, 2, 3, 4, 5, 6, 7, 8)); + let record = Record::from_rdata(lookup_name.clone(), u32::MAX, rdata); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new(lookup_name_lower.clone(), RecordType::AAAA), + record_set, + ); + + // Inserts ANAME record + let rdata = RData::ANAME(Name::from_str("aname.com").unwrap()); + let record = Record::from_rdata(lookup_name.clone(), u32::MAX, rdata); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new(lookup_name_lower.clone(), RecordType::ANAME), + record_set, + ); + + // Inserts CNAME record + let rdata = RData::CNAME(Name::from_str("cname.com").unwrap()); + let record = + Record::from_rdata(Name::from_str("foo").unwrap(), u32::MAX, rdata); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new(lookup_name_lower.clone(), RecordType::CNAME), + record_set, + ); + + // Inserts MX record + let rdata = RData::MX(MX::new(0, Name::from_str("mx.com").unwrap())); + let record = Record::from_rdata(lookup_name.clone(), u32::MAX, rdata); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new(lookup_name_lower.clone(), RecordType::MX), + record_set, + ); + + // Inserts PTR record + let rdata = RData::PTR(Name::from_str("ptr.com").unwrap()); + let record = Record::from_rdata( + Name::from_str("5.6.7.8").unwrap(), + u32::MAX, + rdata, + ); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new("5.6.7.8".parse().unwrap(), RecordType::PTR), + record_set, + ); + + // Inserts SRV record + let rdata = RData::SRV(SRV::new( + 0, + 100, + 1234, + Name::from_str("srv.com").unwrap(), + )); + let record = Record::from_rdata( + Name::from_str("_Service._TCP.example.com").unwrap(), + u32::MAX, + rdata, + ); + let record_set = RecordSet::from(record); + map.insert( + RrKey::new(lookup_name_lower.clone(), RecordType::SRV), + record_set, + ); + + // Inserts TXT record + let rdata = + RData::TXT(TXT::new(vec!["foo".to_string(), "bar".to_string()])); + let record = Record::from_rdata(lookup_name, u32::MAX, rdata); + let record_set = RecordSet::from(record); + map.insert(RrKey::new(lookup_name_lower, RecordType::TXT), record_set); + + map + }; + + let authority = Box::new(Arc::new(RwLock::new( + InMemoryAuthority::new( + Name::from_str("com").unwrap(), + records, + ZoneType::Primary, + false, + ) + .unwrap(), + ))); + let mut c = Catalog::new(); + c.upsert(Name::root().into(), authority); + c + }; + + let mut server_fut = ServerFuture::new(catalog); + let socket_addr = SocketAddr::from(([127, 0, 0, 1], DNS_PORT)); + let tcp_listener = TcpListener::bind(socket_addr).await.unwrap(); + let udp_socket = UdpSocket::bind(socket_addr).await.unwrap(); + server_fut.register_socket(udp_socket); + server_fut.register_listener(tcp_listener, Duration::from_secs(2)); + + // Notifies that the DNS server is ready + tx.send(()).unwrap(); + + server_fut.block_until_done().await.unwrap(); + } + + let (ready_tx, ready_rx) = oneshot::channel(); + let dns_server_fut = run_dns_server(ready_tx); + let handle = tokio::spawn(dns_server_fut); + + // Waits for the DNS server to be ready + ready_rx.await.unwrap(); + + // Pass: `--allow-net` + { + let output = util::deno_cmd() + .current_dir(util::tests_path()) + .env("NO_COLOR", "1") + .arg("run") + .arg("--allow-net") + .arg("--unstable") + .arg("resolve_dns.ts") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + let err = String::from_utf8_lossy(&output.stderr); + let out = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success()); + assert!(err.starts_with("Check file")); + + let expected = + std::fs::read_to_string(util::tests_path().join("resolve_dns.ts.out")) + .unwrap(); + assert_eq!(expected, out); + } + + // Pass: `--allow-net=127.0.0.1:4553` + { + let output = util::deno_cmd() + .current_dir(util::tests_path()) + .env("NO_COLOR", "1") + .arg("run") + .arg("--allow-net=127.0.0.1:4553") + .arg("--unstable") + .arg("resolve_dns.ts") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + let err = String::from_utf8_lossy(&output.stderr); + let out = String::from_utf8_lossy(&output.stdout); + assert!(output.status.success()); + assert!(err.starts_with("Check file")); + + let expected = + std::fs::read_to_string(util::tests_path().join("resolve_dns.ts.out")) + .unwrap(); + assert_eq!(expected, out); + } + + // Permission error: `--allow-net=deno.land` + { + let output = util::deno_cmd() + .current_dir(util::tests_path()) + .env("NO_COLOR", "1") + .arg("run") + .arg("--allow-net=deno.land") + .arg("--unstable") + .arg("resolve_dns.ts") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + let err = String::from_utf8_lossy(&output.stderr); + let out = String::from_utf8_lossy(&output.stdout); + assert!(!output.status.success()); + assert!(err.starts_with("Check file")); + assert!(err.contains(r#"error: Uncaught (in promise) PermissionDenied: network access to "127.0.0.1:4553""#)); + assert!(out.is_empty()); + } + + // Permission error: no permission specified + { + let output = util::deno_cmd() + .current_dir(util::tests_path()) + .env("NO_COLOR", "1") + .arg("run") + .arg("--unstable") + .arg("resolve_dns.ts") + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .spawn() + .unwrap() + .wait_with_output() + .unwrap(); + let err = String::from_utf8_lossy(&output.stderr); + let out = String::from_utf8_lossy(&output.stdout); + assert!(!output.status.success()); + assert!(err.starts_with("Check file")); + assert!(err.contains(r#"error: Uncaught (in promise) PermissionDenied: network access to "127.0.0.1:4553""#)); + assert!(out.is_empty()); + } + + handle.abort(); +} diff --git a/cli/tests/resolve_dns.ts b/cli/tests/resolve_dns.ts new file mode 100644 index 0000000000..35abfc8039 --- /dev/null +++ b/cli/tests/resolve_dns.ts @@ -0,0 +1,36 @@ +const nameServer = { nameServer: { ipAddr: "127.0.0.1", port: 4553 } }; + +const [a, aaaa, aname, cname, mx, ptr, srv, txt] = await Promise.all([ + Deno.resolveDns("www.example.com", "A", nameServer), + Deno.resolveDns("www.example.com", "AAAA", nameServer), + Deno.resolveDns("www.example.com", "ANAME", nameServer), + Deno.resolveDns("foo", "CNAME", nameServer), + Deno.resolveDns("www.example.com", "MX", nameServer), + Deno.resolveDns("5.6.7.8", "PTR", nameServer), + Deno.resolveDns("_Service._TCP.example.com", "SRV", nameServer), + Deno.resolveDns("www.example.com", "TXT", nameServer), +]); + +console.log("A"); +console.log(JSON.stringify(a)); + +console.log("AAAA"); +console.log(JSON.stringify(aaaa)); + +console.log("ANAME"); +console.log(JSON.stringify(aname)); + +console.log("CNAME"); +console.log(JSON.stringify(cname)); + +console.log("MX"); +console.log(JSON.stringify(mx)); + +console.log("PTR"); +console.log(JSON.stringify(ptr)); + +console.log("SRV"); +console.log(JSON.stringify(srv)); + +console.log("TXT"); +console.log(JSON.stringify(txt)); diff --git a/cli/tests/resolve_dns.ts.out b/cli/tests/resolve_dns.ts.out new file mode 100644 index 0000000000..10bd78e8a2 --- /dev/null +++ b/cli/tests/resolve_dns.ts.out @@ -0,0 +1,16 @@ +A +["1.2.3.4"] +AAAA +["1:2:3:4:5:6:7:8"] +ANAME +["aname.com."] +CNAME +["cname.com."] +MX +[{"preference":0,"exchange":"mx.com."}] +PTR +["ptr.com."] +SRV +[{"priority":0,"weight":100,"port":1234,"target":"srv.com."}] +TXT +[["foo","bar"]] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b1718ee954..5383105b95 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -61,6 +61,8 @@ tokio-rustls = "0.22.0" uuid = { version = "0.8.2", features = ["v4"] } webpki = "0.21.4" webpki-roots = "0.21.0" +trust-dns-proto = "0.20.0" +trust-dns-resolver = { version = "0.20.0", features = ["tokio-runtime", "serde-config"] } [target.'cfg(windows)'.dependencies] fwdansi = "1.1.0" diff --git a/runtime/js/30_net.js b/runtime/js/30_net.js index db00f5a2bc..37cb50af36 100644 --- a/runtime/js/30_net.js +++ b/runtime/js/30_net.js @@ -33,6 +33,10 @@ return core.jsonOpAsync("op_datagram_send", args, zeroCopy); } + function resolveDns(query, recordType, options) { + return core.jsonOpAsync("op_dns_resolve", { query, recordType, options }); + } + class Conn { #rid = 0; #remoteAddr = null; @@ -210,5 +214,6 @@ Listener, shutdown, Datagram, + resolveDns, }; })(this); diff --git a/runtime/js/90_deno_ns.js b/runtime/js/90_deno_ns.js index c4205a1a91..bafa1a1b51 100644 --- a/runtime/js/90_deno_ns.js +++ b/runtime/js/90_deno_ns.js @@ -111,6 +111,7 @@ applySourceMap: __bootstrap.errorStack.opApplySourceMap, formatDiagnostics: __bootstrap.errorStack.opFormatDiagnostics, shutdown: __bootstrap.net.shutdown, + resolveDns: __bootstrap.net.resolveDns, listen: __bootstrap.netUnstable.listen, connect: __bootstrap.netUnstable.connect, listenDatagram: __bootstrap.netUnstable.listenDatagram, diff --git a/runtime/ops/net.rs b/runtime/ops/net.rs index caf1ef0d39..364f1c5768 100644 --- a/runtime/ops/net.rs +++ b/runtime/ops/net.rs @@ -22,6 +22,7 @@ use deno_core::RcRef; use deno_core::Resource; use deno_core::ZeroCopyBuf; use serde::Deserialize; +use serde::Serialize; use std::borrow::Cow; use std::cell::RefCell; use std::net::SocketAddr; @@ -30,6 +31,13 @@ use tokio::io::AsyncWriteExt; use tokio::net::TcpListener; use tokio::net::TcpStream; use tokio::net::UdpSocket; +use trust_dns_proto::rr::record_data::RData; +use trust_dns_proto::rr::record_type::RecordType; +use trust_dns_resolver::config::NameServerConfigGroup; +use trust_dns_resolver::config::ResolverConfig; +use trust_dns_resolver::config::ResolverOpts; +use trust_dns_resolver::system_conf; +use trust_dns_resolver::AsyncResolver; #[cfg(unix)] use super::net_unix; @@ -45,6 +53,7 @@ pub fn init(rt: &mut deno_core::JsRuntime) { super::reg_json_sync(rt, "op_listen", op_listen); super::reg_json_async(rt, "op_datagram_receive", op_datagram_receive); super::reg_json_async(rt, "op_datagram_send", op_datagram_send); + super::reg_json_async(rt, "op_dns_resolve", op_dns_resolve); } #[derive(Deserialize)] @@ -531,3 +540,249 @@ fn op_listen( _ => Err(type_error("Wrong argument format!")), } } + +#[derive(Serialize, PartialEq, Debug)] +#[serde(untagged)] +enum DnsReturnRecord { + A(String), + AAAA(String), + ANAME(String), + CNAME(String), + MX { + preference: u16, + exchange: String, + }, + PTR(String), + SRV { + priority: u16, + weight: u16, + port: u16, + target: String, + }, + TXT(Vec), +} + +async fn op_dns_resolve( + state: Rc>, + args: Value, + _zero_copy: BufVec, +) -> Result { + fn default_port() -> u16 { + 53 + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct ResolveAddrArgs { + query: String, + record_type: RecordType, + options: Option, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct ResolveDnsOption { + name_server: Option, + } + + #[derive(Deserialize)] + #[serde(rename_all = "camelCase")] + struct NameServer { + ip_addr: String, + #[serde(default = "default_port")] + port: u16, + } + + let ResolveAddrArgs { + query, + record_type, + options, + } = serde_json::from_value(args)?; + + let (config, opts) = if let Some(name_server) = + options.as_ref().and_then(|o| o.name_server.as_ref()) + { + let group = NameServerConfigGroup::from_ips_clear( + &[name_server.ip_addr.parse()?], + name_server.port, + true, + ); + ( + ResolverConfig::from_parts(None, vec![], group), + ResolverOpts::default(), + ) + } else { + system_conf::read_system_conf()? + }; + + { + let s = state.borrow(); + let perm = s.borrow::(); + + // Checks permission against the name servers which will be actually queried. + for ns in config.name_servers() { + let socker_addr = &ns.socket_addr; + let ip = socker_addr.ip().to_string(); + let port = socker_addr.port(); + perm.check_net(&(ip, Some(port)))?; + } + } + + let resolver = AsyncResolver::tokio(config, opts)?; + + let results: Vec = resolver + .lookup(query, record_type, Default::default()) + .await? + .iter() + .filter_map(rdata_to_return_record(record_type)) + .collect(); + + Ok(json!(results)) +} + +fn rdata_to_return_record( + ty: RecordType, +) -> impl Fn(&RData) -> Option { + use RecordType::*; + move |r: &RData| -> Option { + match ty { + A => r.as_a().map(ToString::to_string).map(DnsReturnRecord::A), + AAAA => r + .as_aaaa() + .map(ToString::to_string) + .map(DnsReturnRecord::AAAA), + ANAME => r + .as_aname() + .map(ToString::to_string) + .map(DnsReturnRecord::ANAME), + CNAME => r + .as_cname() + .map(ToString::to_string) + .map(DnsReturnRecord::CNAME), + MX => r.as_mx().map(|mx| DnsReturnRecord::MX { + preference: mx.preference(), + exchange: mx.exchange().to_string(), + }), + PTR => r + .as_ptr() + .map(ToString::to_string) + .map(DnsReturnRecord::PTR), + SRV => r.as_srv().map(|srv| DnsReturnRecord::SRV { + priority: srv.priority(), + weight: srv.weight(), + port: srv.port(), + target: srv.target().to_string(), + }), + TXT => r.as_txt().map(|txt| { + let texts: Vec = txt + .iter() + .map(|bytes| { + // Tries to parse these bytes as Latin-1 + bytes.iter().map(|&b| b as char).collect::() + }) + .collect(); + DnsReturnRecord::TXT(texts) + }), + // TODO(magurotuna): Other record types are not supported + _ => todo!(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::net::Ipv4Addr; + use std::net::Ipv6Addr; + use trust_dns_proto::rr::rdata::mx::MX; + use trust_dns_proto::rr::rdata::srv::SRV; + use trust_dns_proto::rr::rdata::txt::TXT; + use trust_dns_proto::rr::record_data::RData; + use trust_dns_proto::rr::Name; + + #[test] + fn rdata_to_return_record_a() { + let func = rdata_to_return_record(RecordType::A); + let rdata = RData::A(Ipv4Addr::new(127, 0, 0, 1)); + assert_eq!( + func(&rdata), + Some(DnsReturnRecord::A("127.0.0.1".to_string())) + ); + } + + #[test] + fn rdata_to_return_record_aaaa() { + let func = rdata_to_return_record(RecordType::AAAA); + let rdata = RData::AAAA(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)); + assert_eq!(func(&rdata), Some(DnsReturnRecord::AAAA("::1".to_string()))); + } + + #[test] + fn rdata_to_return_record_aname() { + let func = rdata_to_return_record(RecordType::ANAME); + let rdata = RData::ANAME(Name::new()); + assert_eq!(func(&rdata), Some(DnsReturnRecord::ANAME("".to_string()))); + } + + #[test] + fn rdata_to_return_record_cname() { + let func = rdata_to_return_record(RecordType::CNAME); + let rdata = RData::CNAME(Name::new()); + assert_eq!(func(&rdata), Some(DnsReturnRecord::CNAME("".to_string()))); + } + + #[test] + fn rdata_to_return_record_mx() { + let func = rdata_to_return_record(RecordType::MX); + let rdata = RData::MX(MX::new(10, Name::new())); + assert_eq!( + func(&rdata), + Some(DnsReturnRecord::MX { + preference: 10, + exchange: "".to_string() + }) + ); + } + + #[test] + fn rdata_to_return_record_ptr() { + let func = rdata_to_return_record(RecordType::PTR); + let rdata = RData::PTR(Name::new()); + assert_eq!(func(&rdata), Some(DnsReturnRecord::PTR("".to_string()))); + } + + #[test] + fn rdata_to_return_record_srv() { + let func = rdata_to_return_record(RecordType::SRV); + let rdata = RData::SRV(SRV::new(1, 2, 3, Name::new())); + assert_eq!( + func(&rdata), + Some(DnsReturnRecord::SRV { + priority: 1, + weight: 2, + port: 3, + target: "".to_string() + }) + ); + } + + #[test] + fn rdata_to_return_record_txt() { + let func = rdata_to_return_record(RecordType::TXT); + let rdata = RData::TXT(TXT::from_bytes(vec![ + "foo".as_bytes(), + "bar".as_bytes(), + &[0xa3], // "£" in Latin-1 + &[0xe3, 0x81, 0x82], // "あ" in UTF-8 + ])); + assert_eq!( + func(&rdata), + Some(DnsReturnRecord::TXT(vec![ + "foo".to_string(), + "bar".to_string(), + "£".to_string(), + "ã\u{81}\u{82}".to_string(), + ])) + ); + } +} diff --git a/runtime/permissions.rs b/runtime/permissions.rs index b88b862625..fdb3a425b9 100644 --- a/runtime/permissions.rs +++ b/runtime/permissions.rs @@ -818,7 +818,7 @@ mod tests { } #[test] - fn test_check_net() { + fn test_check_net_with_values() { let perms = Permissions::from_options(&PermissionsOptions { allow_net: Some(svec![ "localhost", @@ -854,6 +854,93 @@ mod tests { ("192.168.0.1", 0, false), ]; + for (host, port, is_ok) in domain_tests { + assert_eq!(is_ok, perms.check_net(&(host, Some(port))).is_ok()); + } + } + + #[test] + fn test_check_net_only_flag() { + let perms = Permissions::from_options(&PermissionsOptions { + allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign + ..Default::default() + }); + + let domain_tests = vec![ + ("localhost", 1234), + ("deno.land", 0), + ("deno.land", 3000), + ("deno.lands", 0), + ("deno.lands", 3000), + ("github.com", 3000), + ("github.com", 0), + ("github.com", 2000), + ("github.net", 3000), + ("127.0.0.1", 0), + ("127.0.0.1", 3000), + ("127.0.0.2", 0), + ("127.0.0.2", 3000), + ("172.16.0.2", 8000), + ("172.16.0.2", 0), + ("172.16.0.2", 6000), + ("172.16.0.1", 8000), + ("somedomain", 0), + ("192.168.0.1", 0), + ]; + + for (host, port) in domain_tests { + assert!(perms.check_net(&(host, Some(port))).is_ok()); + } + } + + #[test] + fn test_check_net_no_flag() { + let perms = Permissions::from_options(&PermissionsOptions { + allow_net: None, + ..Default::default() + }); + + let domain_tests = vec![ + ("localhost", 1234), + ("deno.land", 0), + ("deno.land", 3000), + ("deno.lands", 0), + ("deno.lands", 3000), + ("github.com", 3000), + ("github.com", 0), + ("github.com", 2000), + ("github.net", 3000), + ("127.0.0.1", 0), + ("127.0.0.1", 3000), + ("127.0.0.2", 0), + ("127.0.0.2", 3000), + ("172.16.0.2", 8000), + ("172.16.0.2", 0), + ("172.16.0.2", 6000), + ("172.16.0.1", 8000), + ("somedomain", 0), + ("192.168.0.1", 0), + ]; + + for (host, port) in domain_tests { + assert!(!perms.check_net(&(host, Some(port))).is_ok()); + } + } + + #[test] + fn test_check_net_url() { + let perms = Permissions::from_options(&PermissionsOptions { + allow_net: Some(svec![ + "localhost", + "deno.land", + "github.com:3000", + "127.0.0.1", + "172.16.0.2:8000", + "www.github.com:443" + ]), + ..Default::default() + }); + let url_tests = vec![ // Any protocol + port for localhost should be ok, since we don't specify ("http://localhost", true), @@ -893,13 +980,9 @@ mod tests { ("https://www.github.com:443/robots.txt", true), ]; - for (url_str, is_ok) in url_tests.iter() { + for (url_str, is_ok) in url_tests { let u = url::Url::parse(url_str).unwrap(); - assert_eq!(*is_ok, perms.check_net_url(&u).is_ok()); - } - - for (hostname, port, is_ok) in domain_tests.iter() { - assert_eq!(*is_ok, perms.check_net(&(hostname, Some(*port))).is_ok()); + assert_eq!(is_ok, perms.check_net_url(&u).is_ok()); } } diff --git a/test_util/src/lib.rs b/test_util/src/lib.rs index 53dc82c39f..14ec723f2a 100644 --- a/test_util/src/lib.rs +++ b/test_util/src/lib.rs @@ -5,7 +5,6 @@ #[macro_use] extern crate lazy_static; -use core::mem::replace; use futures::FutureExt; use futures::Stream; use futures::StreamExt; @@ -28,6 +27,7 @@ use std::env; use std::io; use std::io::Read; use std::io::Write; +use std::mem::replace; use std::net::SocketAddr; use std::path::PathBuf; use std::pin::Pin;