diff options
author | Satya Rohith <me@satyarohith.com> | 2024-06-18 16:16:13 +0530 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-18 10:46:13 +0000 |
commit | 8c4b33db0d05181a0e5538bddaf063144724c938 (patch) | |
tree | 368ced63a6ac484db822212d9d332d92bd3466ed /ext/node | |
parent | 4b83ce8acabaf868d47bf764fce18ce5450fd314 (diff) |
feat(ext/node): add BlockList & SocketAddress classes (#24229)
Closes https://github.com/denoland/deno/issues/24059
Diffstat (limited to 'ext/node')
-rw-r--r-- | ext/node/Cargo.toml | 1 | ||||
-rw-r--r-- | ext/node/lib.rs | 10 | ||||
-rw-r--r-- | ext/node/ops/blocklist.rs | 290 | ||||
-rw-r--r-- | ext/node/ops/mod.rs | 1 | ||||
-rw-r--r-- | ext/node/polyfills/internal/blocklist.mjs | 227 | ||||
-rw-r--r-- | ext/node/polyfills/internal/errors.ts | 14 | ||||
-rw-r--r-- | ext/node/polyfills/net.ts | 6 |
7 files changed, 536 insertions, 13 deletions
diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 83ce49060..ecb618e48 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -41,6 +41,7 @@ home = "0.5.9" http.workspace = true idna = "0.3.0" indexmap.workspace = true +ipnetwork = "0.20.0" k256 = "0.13.1" lazy-regex.workspace = true libc.workspace = true diff --git a/ext/node/lib.rs b/ext/node/lib.rs index d05434b88..7654607d7 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -230,6 +230,15 @@ deno_core::extension!(deno_node, deps = [ deno_io, deno_fs ], parameters = [P: NodePermissions], ops = [ + ops::blocklist::op_socket_address_parse, + ops::blocklist::op_socket_address_get_serialization, + + ops::blocklist::op_blocklist_new, + ops::blocklist::op_blocklist_add_address, + ops::blocklist::op_blocklist_add_range, + ops::blocklist::op_blocklist_add_subnet, + ops::blocklist::op_blocklist_check, + ops::buffer::op_is_ascii, ops::buffer::op_is_utf8, ops::crypto::op_node_create_decipheriv, @@ -489,6 +498,7 @@ deno_core::extension!(deno_node, "internal_binding/uv.ts", "internal/assert.mjs", "internal/async_hooks.ts", + "internal/blocklist.mjs", "internal/buffer.mjs", "internal/child_process.ts", "internal/cli_table.ts", diff --git a/ext/node/ops/blocklist.rs b/ext/node/ops/blocklist.rs new file mode 100644 index 000000000..ce32c14ba --- /dev/null +++ b/ext/node/ops/blocklist.rs @@ -0,0 +1,290 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use std::cell::RefCell; +use std::collections::HashSet; +use std::net::IpAddr; +use std::net::Ipv4Addr; +use std::net::Ipv6Addr; +use std::net::SocketAddr; + +use deno_core::anyhow::anyhow; +use deno_core::anyhow::bail; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::OpState; + +use ipnetwork::IpNetwork; +use ipnetwork::Ipv4Network; +use ipnetwork::Ipv6Network; +use serde::Serialize; + +pub struct BlockListResource { + blocklist: RefCell<BlockList>, +} + +#[derive(Serialize)] +struct SocketAddressSerialization(String, String); + +#[op2(fast)] +pub fn op_socket_address_parse( + state: &mut OpState, + #[string] addr: &str, + #[smi] port: u16, + #[string] family: &str, +) -> Result<bool, AnyError> { + let ip = addr.parse::<IpAddr>()?; + let parsed: SocketAddr = SocketAddr::new(ip, port); + let parsed_ip_str = parsed.ip().to_string(); + let family_correct = family.eq_ignore_ascii_case("ipv4") && parsed.is_ipv4() + || family.eq_ignore_ascii_case("ipv6") && parsed.is_ipv6(); + + if family_correct { + let family_is_lowercase = family[..3].chars().all(char::is_lowercase); + if family_is_lowercase && parsed_ip_str == addr { + Ok(true) + } else { + state.put::<SocketAddressSerialization>(SocketAddressSerialization( + parsed_ip_str, + family.to_lowercase(), + )); + Ok(false) + } + } else { + Err(anyhow!("Invalid address")) + } +} + +#[op2] +#[serde] +pub fn op_socket_address_get_serialization( + state: &mut OpState, +) -> Result<SocketAddressSerialization, AnyError> { + Ok(state.take::<SocketAddressSerialization>()) +} + +#[op2] +#[cppgc] +pub fn op_blocklist_new() -> BlockListResource { + let blocklist = BlockList::new(); + BlockListResource { + blocklist: RefCell::new(blocklist), + } +} + +#[op2(fast)] +pub fn op_blocklist_add_address( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, +) -> Result<(), AnyError> { + wrap.blocklist.borrow_mut().add_address(addr) +} + +#[op2(fast)] +pub fn op_blocklist_add_range( + #[cppgc] wrap: &BlockListResource, + #[string] start: &str, + #[string] end: &str, +) -> Result<bool, AnyError> { + wrap.blocklist.borrow_mut().add_range(start, end) +} + +#[op2(fast)] +pub fn op_blocklist_add_subnet( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, + #[smi] prefix: u8, +) -> Result<(), AnyError> { + wrap.blocklist.borrow_mut().add_subnet(addr, prefix) +} + +#[op2(fast)] +pub fn op_blocklist_check( + #[cppgc] wrap: &BlockListResource, + #[string] addr: &str, + #[string] r#type: &str, +) -> Result<bool, AnyError> { + wrap.blocklist.borrow().check(addr, r#type) +} + +struct BlockList { + rules: HashSet<IpNetwork>, +} + +impl BlockList { + pub fn new() -> Self { + BlockList { + rules: HashSet::new(), + } + } + + fn map_addr_add_network(&mut self, addr: IpAddr, prefix: Option<u8>) { + match addr { + IpAddr::V4(addr) => { + self.rules.insert(IpNetwork::V4( + Ipv4Network::new(addr, prefix.unwrap_or(32)).unwrap(), + )); + self.rules.insert(IpNetwork::V6( + Ipv6Network::new(addr.to_ipv6_mapped(), prefix.unwrap_or(128)) + .unwrap(), + )); + } + IpAddr::V6(addr) => { + if let Some(ipv4_mapped) = addr.to_ipv4_mapped() { + self.rules.insert(IpNetwork::V4( + Ipv4Network::new(ipv4_mapped, prefix.unwrap_or(32)).unwrap(), + )); + } + self.rules.insert(IpNetwork::V6( + Ipv6Network::new(addr, prefix.unwrap_or(128)).unwrap(), + )); + } + }; + } + + pub fn add_address(&mut self, address: &str) -> Result<(), AnyError> { + let ip: IpAddr = address.parse()?; + self.map_addr_add_network(ip, None); + Ok(()) + } + + pub fn add_range( + &mut self, + start: &str, + end: &str, + ) -> Result<bool, AnyError> { + let start_ip: IpAddr = start.parse()?; + let end_ip: IpAddr = end.parse()?; + + match (start_ip, end_ip) { + (IpAddr::V4(start), IpAddr::V4(end)) => { + let start_u32: u32 = start.into(); + let end_u32: u32 = end.into(); + if end_u32 < start_u32 { + // Indicates invalid range. + return Ok(false); + } + for ip in start_u32..=end_u32 { + let addr: Ipv4Addr = ip.into(); + self.map_addr_add_network(IpAddr::V4(addr), None); + } + } + (IpAddr::V6(start), IpAddr::V6(end)) => { + let start_u128: u128 = start.into(); + let end_u128: u128 = end.into(); + if end_u128 < start_u128 { + // Indicates invalid range. + return Ok(false); + } + for ip in start_u128..=end_u128 { + let addr: Ipv6Addr = ip.into(); + self.map_addr_add_network(IpAddr::V6(addr), None); + } + } + _ => bail!("IP version mismatch between start and end addresses"), + } + Ok(true) + } + + pub fn add_subnet(&mut self, addr: &str, prefix: u8) -> Result<(), AnyError> { + let ip: IpAddr = addr.parse()?; + self.map_addr_add_network(ip, Some(prefix)); + Ok(()) + } + + pub fn check(&self, addr: &str, r#type: &str) -> Result<bool, AnyError> { + let addr: IpAddr = addr.parse()?; + let family = r#type.to_lowercase(); + if family == "ipv4" && addr.is_ipv4() || family == "ipv6" && addr.is_ipv6() + { + Ok(self.rules.iter().any(|net| net.contains(addr))) + } else { + Err(anyhow!("Invalid address")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add_address() { + // Single IPv4 address + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:1", "ipv6").unwrap()); + + // Single IPv6 address + let mut block_list = BlockList::new(); + block_list.add_address("2001:db8::1").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_add_range() { + // IPv4 range + let mut block_list = BlockList::new(); + block_list.add_range("192.168.0.1", "192.168.0.3").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.2", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.3", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:1", "ipv6").unwrap()); + + // IPv6 range + let mut block_list = BlockList::new(); + block_list.add_range("2001:db8::1", "2001:db8::3").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::2", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::3", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_add_subnet() { + // IPv4 subnet + let mut block_list = BlockList::new(); + block_list.add_subnet("192.168.0.0", 24).unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + assert!(block_list.check("192.168.0.255", "ipv4").unwrap()); + assert!(block_list.check("::ffff:c0a8:0", "ipv6").unwrap()); + + // IPv6 subnet + let mut block_list = BlockList::new(); + block_list.add_subnet("2001:db8::", 64).unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + assert!(block_list.check("2001:db8::ffff", "ipv6").unwrap()); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + } + + #[test] + fn test_check() { + // Check IPv4 presence + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check IPv6 presence + let mut block_list = BlockList::new(); + block_list.add_address("2001:db8::1").unwrap(); + assert!(block_list.check("2001:db8::1", "ipv6").unwrap()); + + // Check IPv4 not present + let block_list = BlockList::new(); + assert!(!block_list.check("192.168.0.1", "ipv4").unwrap()); + + // Check IPv6 not present + let block_list = BlockList::new(); + assert!(!block_list.check("2001:db8::1", "ipv6").unwrap()); + + // Check invalid IP version + let block_list = BlockList::new(); + assert!(block_list.check("192.168.0.1", "ipv6").is_err()); + + // Check invalid type + let mut block_list = BlockList::new(); + block_list.add_address("192.168.0.1").unwrap(); + assert!(block_list.check("192.168.0.1", "invalid_type").is_err()); + } +} diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index ae703e3f3..b51e23ac8 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -1,5 +1,6 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +pub mod blocklist; pub mod buffer; pub mod crypto; pub mod fs; diff --git a/ext/node/polyfills/internal/blocklist.mjs b/ext/node/polyfills/internal/blocklist.mjs new file mode 100644 index 000000000..a9aba03b6 --- /dev/null +++ b/ext/node/polyfills/internal/blocklist.mjs @@ -0,0 +1,227 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent and Node contributors. All rights reserved. MIT license. + +import { primordials } from "ext:core/mod.js"; +import { + op_blocklist_add_address, + op_blocklist_add_range, + op_blocklist_add_subnet, + op_blocklist_check, + op_blocklist_new, + op_socket_address_get_serialization, + op_socket_address_parse, +} from "ext:core/ops"; + +import { + validateInt32, + validateObject, + validatePort, + validateString, + validateUint32, +} from "ext:deno_node/internal/validators.mjs"; +import { ERR_INVALID_ARG_VALUE } from "ext:deno_node/internal/errors.ts"; +import { customInspectSymbol } from "ext:deno_node/internal/util.mjs"; +import { inspect } from "ext:deno_node/internal/util/inspect.mjs"; + +const { Symbol } = primordials; + +const internalBlockList = Symbol("blocklist"); + +class BlockList { + constructor() { + this[internalBlockList] = op_blocklist_new(); + } + + [customInspectSymbol](depth, options) { + if (depth < 0) { + return this; + } + + const opts = { + ...options, + depth: options.depth == null ? null : options.depth - 1, + }; + + return `BlockList ${ + inspect({ + rules: [], // TODO(satyarohith): provide the actual rules + }, opts) + }`; + } + + addAddress(address, family = "ipv4") { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, "address"); + validateString(family, "family"); + new SocketAddress({ + address, + family, + }); + } else { + address = address.address; + } + op_blocklist_add_address(this[internalBlockList], address); + } + + addRange(start, end, family = "ipv4") { + if (!SocketAddress.isSocketAddress(start)) { + validateString(start, "start"); + validateString(family, "family"); + new SocketAddress({ + address: start, + family, + }); + } else { + start = start.address; + } + if (!SocketAddress.isSocketAddress(end)) { + validateString(end, "end"); + validateString(family, "family"); + new SocketAddress({ + address: end, + family, + }); + } else { + end = end.address; + } + const ret = op_blocklist_add_range(this[internalBlockList], start, end); + if (ret === false) { + throw new ERR_INVALID_ARG_VALUE("start", start, "must come before end"); + } + } + + addSubnet(network, prefix, family = "ipv4") { + if (!SocketAddress.isSocketAddress(network)) { + validateString(network, "network"); + validateString(family, "family"); + new SocketAddress({ + address: network, + family, + }); + } else { + network = network.address; + family = network.family; + } + switch (family) { + case "ipv4": + validateInt32(prefix, "prefix", 0, 32); + break; + case "ipv6": + validateInt32(prefix, "prefix", 0, 128); + break; + } + op_blocklist_add_subnet(this[internalBlockList], network, prefix); + } + + check(address, family = "ipv4") { + if (!SocketAddress.isSocketAddress(address)) { + validateString(address, "address"); + validateString(family, "family"); + try { + new SocketAddress({ + address, + family, + }); + } catch { + // Ignore the error. If it's not a valid address, return false. + return false; + } + } else { + family = address.family; + address = address.address; + } + try { + return op_blocklist_check(this[internalBlockList], address, family); + } catch (_) { + // Node API expects false as return value if the address is invalid. + // Example: `blocklist.check("1.1.1.1", "ipv6")` should return false. + return false; + } + } + + get rules() { + // TODO(satyarohith): return the actual rules + return []; + } +} + +const kDetail = Symbol("kDetail"); + +class SocketAddress { + static isSocketAddress(value) { + return value?.[kDetail] !== undefined; + } + + constructor(options = kEmptyObject) { + validateObject(options, "options"); + let { family = "ipv4" } = options; + const { + address = (family === "ipv4" ? "127.0.0.1" : "::"), + port = 0, + flowlabel = 0, + } = options; + + if (typeof family?.toLowerCase === "function") { + // deno-lint-ignore prefer-primordials + family = family.toLowerCase(); + } + switch (family) { + case "ipv4": + break; + case "ipv6": + break; + default: + throw new ERR_INVALID_ARG_VALUE("options.family", options.family); + } + + validateString(address, "options.address"); + validatePort(port, "options.port"); + validateUint32(flowlabel, "options.flowlabel", false); + + this[kDetail] = { + address, + port, + family, + flowlabel, + }; + const useInput = op_socket_address_parse( + address, + port, + family, + ); + if (!useInput) { + const { 0: address_, 1: family_ } = op_socket_address_get_serialization(); + this[kDetail].address = address_; + this[kDetail].family = family_; + } + } + + get address() { + return this[kDetail].address; + } + + get port() { + return this[kDetail].port; + } + + get family() { + return this[kDetail].family; + } + + get flowlabel() { + // TODO(satyarohith): Implement this in Rust. + // The flow label can be changed internally. + return this[kDetail].flowlabel; + } + + toJSON() { + return { + address: this.address, + port: this.port, + family: this.family, + flowlabel: this.flowlabel, + }; + } +} + +export { BlockList, SocketAddress }; diff --git a/ext/node/polyfills/internal/errors.ts b/ext/node/polyfills/internal/errors.ts index cb4119411..6529e9894 100644 --- a/ext/node/polyfills/internal/errors.ts +++ b/ext/node/polyfills/internal/errors.ts @@ -667,9 +667,7 @@ function invalidArgTypeHelper(input: any) { return ` Received type ${typeof input} (${inspected})`; } -export class ERR_OUT_OF_RANGE extends RangeError { - code = "ERR_OUT_OF_RANGE"; - +export class ERR_OUT_OF_RANGE extends NodeRangeError { constructor( str: string, range: string, @@ -694,15 +692,7 @@ export class ERR_OUT_OF_RANGE extends RangeError { } msg += ` It must be ${range}. Received ${received}`; - super(msg); - - const { name } = this; - // Add the error code to the name to include it in the stack trace. - this.name = `${name} [${this.code}]`; - // Access the stack to generate the error message including the error code from the name. - this.stack; - // Reset the name to the actual name. - this.name = name; + super("ERR_OUT_OF_RANGE", msg); } } diff --git a/ext/node/polyfills/net.ts b/ext/node/polyfills/net.ts index 66b7735d9..6625ce7b5 100644 --- a/ext/node/polyfills/net.ts +++ b/ext/node/polyfills/net.ts @@ -24,6 +24,8 @@ // deno-lint-ignore-file prefer-primordials import { notImplemented } from "ext:deno_node/_utils.ts"; +import { BlockList, SocketAddress } from "ext:deno_node/internal/blocklist.mjs"; + import { EventEmitter } from "node:events"; import { isIP, @@ -2472,7 +2474,7 @@ export function createServer( return new Server(options, connectionListener); } -export { isIP, isIPv4, isIPv6 }; +export { BlockList, isIP, isIPv4, isIPv6, SocketAddress }; export default { _createServerHandle, @@ -2480,6 +2482,8 @@ export default { isIP, isIPv4, isIPv6, + BlockList, + SocketAddress, connect, createConnection, createServer, |