net: update net.blocklist to allow file save and file management · nodejs/node@f753645
11'use strict';
2233const {
4+ ArrayIsArray,
45 Boolean,
6+ JSONParse,
7+ NumberParseInt,
58 ObjectSetPrototypeOf,
69 Symbol,
710} = primordials;
@@ -32,6 +35,7 @@ const { owner_symbol } = internalBinding('symbols');
32353336const {
3437ERR_INVALID_ARG_VALUE,
38+ERR_INVALID_ARG_TYPE,
3539} = require('internal/errors').codes;
36403741const { validateInt32, validateString } = require('internal/validators');
@@ -139,10 +143,130 @@ class BlockList {
139143return Boolean(this[kHandle].check(address[kSocketAddressHandle]));
140144}
141145146+/*
147+ * @param {string[]} data
148+ * @example
149+ * const data = [
150+ * // IPv4 examples
151+ * 'Subnet: IPv4 192.168.1.0/24',
152+ * 'Address: IPv4 10.0.0.5',
153+ * 'Range: IPv4 192.168.2.1-192.168.2.10',
154+ * 'Range: IPv4 10.0.0.1-10.0.0.10',
155+ *
156+ * // IPv6 examples
157+ * 'Subnet: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64',
158+ * 'Address: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334',
159+ * 'Range: IPv6 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335',
160+ * 'Subnet: IPv6 2001:db8:1234::/48',
161+ * 'Address: IPv6 2001:db8:1234::1',
162+ * 'Range: IPv6 2001:db8:1234::1-2001:db8:1234::10'
163+ * ];
164+ */
165+ #parseIPInfo(data) {
166+for (const item of data) {
167+if (item.includes('IPv4')) {
168+const subnetMatch = item.match(
169+/Subnet: IPv4 (\d{1,3}(?:\.\d{1,3}){3})\/(\d{1,2})/,
170+);
171+if (subnetMatch) {
172+const { 1: network, 2: prefix } = subnetMatch;
173+this.addSubnet(network, NumberParseInt(prefix));
174+continue;
175+}
176+const addressMatch = item.match(/Address: IPv4 (\d{1,3}(?:\.\d{1,3}){3})/);
177+if (addressMatch) {
178+const { 1: address } = addressMatch;
179+this.addAddress(address);
180+continue;
181+}
182+183+const rangeMatch = item.match(
184+/Range: IPv4 (\d{1,3}(?:\.\d{1,3}){3})-(\d{1,3}(?:\.\d{1,3}){3})/,
185+);
186+if (rangeMatch) {
187+const { 1: start, 2: end } = rangeMatch;
188+this.addRange(start, end);
189+continue;
190+}
191+}
192+// IPv6 parsing with support for compressed addresses
193+if (item.includes('IPv6')) {
194+// IPv6 subnet pattern: supports both full and compressed formats
195+// Examples:
196+// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334/64 (full)
197+// - 2001:db8:85a3::8a2e:370:7334/64 (compressed)
198+// - 2001:db8:85a3::192.0.2.128/64 (mixed)
199+const ipv6SubnetMatch = item.match(
200+/Subnet: IPv6 ([0-9a-fA-F:]{1,39})\/([0-9]{1,3})/i,
201+);
202+if (ipv6SubnetMatch) {
203+const { 1: network, 2: prefix } = ipv6SubnetMatch;
204+this.addSubnet(network, NumberParseInt(prefix), 'ipv6');
205+continue;
206+}
207+208+// IPv6 address pattern: supports both full and compressed formats
209+// Examples:
210+// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334 (full)
211+// - 2001:db8:85a3::8a2e:370:7334 (compressed)
212+// - 2001:db8:85a3::192.0.2.128 (mixed)
213+const ipv6AddressMatch = item.match(/Address: IPv6 ([0-9a-fA-F:]{1,39})/i);
214+if (ipv6AddressMatch) {
215+const { 1: address } = ipv6AddressMatch;
216+this.addAddress(address, 'ipv6');
217+continue;
218+}
219+220+// IPv6 range pattern: supports both full and compressed formats
221+// Examples:
222+// - 2001:0db8:85a3:0000:0000:8a2e:0370:7334-2001:0db8:85a3:0000:0000:8a2e:0370:7335 (full)
223+// - 2001:db8:85a3::8a2e:370:7334-2001:db8:85a3::8a2e:370:7335 (compressed)
224+// - 2001:db8:85a3::192.0.2.128-2001:db8:85a3::192.0.2.129 (mixed)
225+const ipv6RangeMatch = item.match(/Range: IPv6 ([0-9a-fA-F:]{1,39})-([0-9a-fA-F:]{1,39})/i);
226+if (ipv6RangeMatch) {
227+const { 1: start, 2: end } = ipv6RangeMatch;
228+this.addRange(start, end, 'ipv6');
229+continue;
230+}
231+}
232+}
233+}
234+235+236+toJSON() {
237+return this.rules;
238+}
239+240+fromJSON(data) {
241+// The data argument must be a string, or an array of strings that
242+// is JSON parseable.
243+if (ArrayIsArray(data)) {
244+for (const n of data) {
245+if (typeof n !== 'string') {
246+throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
247+}
248+}
249+} else if (typeof data !== 'string') {
250+throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
251+} else {
252+data = JSONParse(data);
253+if (!ArrayIsArray(data)) {
254+throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
255+}
256+for (const n of data) {
257+if (typeof n !== 'string') {
258+throw new ERR_INVALID_ARG_TYPE('data', ['string', 'string[]'], data);
259+}
260+}
261+}
262+263+this.#parseIPInfo(data);
264+}
265+266+142267get rules() {
143268return this[kHandle].getRules();
144269}
145-146270[kClone]() {
147271const handle = this[kHandle];
148272return {