Skip to content

Commit f753645

Browse files
alphaleadershiptargos
authored andcommitted
net: update net.blocklist to allow file save and file management
PR-URL: #58087 Fixes: #56252 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent d555db2 commit f753645

File tree

4 files changed

+229
-2
lines changed

4 files changed

+229
-2
lines changed

doc/api/net.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,38 @@ added:
181181
* `value` {any} Any JS value
182182
* Returns `true` if the `value` is a `net.BlockList`.
183183

184+
### `blockList.fromJSON(value)`
185+
186+
> Stability: 1 - Experimental
187+
188+
<!-- YAML
189+
added: REPLACEME
190+
-->
191+
192+
```js
193+
const blockList = new net.BlockList();
194+
const data = [
195+
'Subnet: IPv4 192.168.1.0/24',
196+
'Address: IPv4 10.0.0.5',
197+
'Range: IPv4 192.168.2.1-192.168.2.10',
198+
'Range: IPv4 10.0.0.1-10.0.0.10',
199+
];
200+
blockList.fromJSON(data);
201+
blockList.fromJSON(JSON.stringify(data));
202+
```
203+
204+
* `value` Blocklist.rules
205+
206+
### `blockList.toJSON()`
207+
208+
> Stability: 1 - Experimental
209+
210+
<!-- YAML
211+
added: REPLACEME
212+
-->
213+
214+
* Returns Blocklist.rules
215+
184216
## Class: `net.SocketAddress`
185217

186218
<!-- YAML

lib/internal/blocklist.js

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
Boolean,
6+
JSONParse,
7+
NumberParseInt,
58
ObjectSetPrototypeOf,
69
Symbol,
710
} = primordials;
@@ -32,6 +35,7 @@ const { owner_symbol } = internalBinding('symbols');
3235

3336
const {
3437
ERR_INVALID_ARG_VALUE,
38+
ERR_INVALID_ARG_TYPE,
3539
} = require('internal/errors').codes;
3640

3741
const { validateInt32, validateString } = require('internal/validators');
@@ -139,10 +143,130 @@ class BlockList {
139143
return Boolean(this[kHandle].check(address[kSocketAddressHandle]));
140144
}
141145

146+
/*
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+
142267
get rules() {
143268
return this[kHandle].getRules();
144269
}
145-
146270
[kClone]() {
147271
const handle = this[kHandle];
148272
return {

test/parallel/test-blocklist.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,3 +287,75 @@ const util = require('util');
287287
assert(BlockList.isBlockList(new BlockList()));
288288
assert(!BlockList.isBlockList({}));
289289
}
290+
291+
// Test exporting and importing the rule list to/from JSON
292+
{
293+
const ruleList = [
294+
'Address: IPv4 10.0.0.5',
295+
'Address: IPv6 ::',
296+
'Subnet: IPv4 192.168.1.0/24',
297+
'Subnet: IPv6 8592:757c:efae:4e45::/64',
298+
];
299+
300+
const test2 = new BlockList();
301+
const test3 = new BlockList();
302+
const test4 = new BlockList();
303+
const test5 = new BlockList();
304+
305+
const bl = new BlockList();
306+
bl.addAddress('10.0.0.5');
307+
bl.addAddress('::', 'ipv6');
308+
bl.addSubnet('192.168.1.0', 24);
309+
bl.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6');
310+
311+
// Test invalid inputs (input to fromJSON must be an array of
312+
// string rules or a serialized json string of an array of
313+
// string rules.
314+
[
315+
1, null, Symbol(), [1, 2, 3], '123', [Symbol()], new Map(),
316+
].forEach((i) => {
317+
assert.throws(() => test2.fromJSON(i), {
318+
code: 'ERR_INVALID_ARG_TYPE',
319+
});
320+
});
321+
322+
// Invalid rules are ignored.
323+
test2.fromJSON(['1', '2', '3']);
324+
assert.deepStrictEqual(test2.rules, []);
325+
326+
// Direct output from toJSON method works
327+
test2.fromJSON(bl.toJSON());
328+
assert.deepStrictEqual(test2.rules.sort(), ruleList);
329+
330+
// JSON stringified output works
331+
test3.fromJSON(JSON.stringify(bl));
332+
assert.deepStrictEqual(test3.rules.sort(), ruleList);
333+
334+
// A raw array works
335+
test4.fromJSON(ruleList);
336+
assert.deepStrictEqual(test4.rules.sort(), ruleList);
337+
338+
// Individual rules work
339+
ruleList.forEach((item) => {
340+
test5.fromJSON([item]);
341+
});
342+
assert.deepStrictEqual(test5.rules.sort(), ruleList);
343+
344+
// Each of the created blocklists should handle the checks identically.
345+
[
346+
['10.0.0.5', 'ipv4', true],
347+
['10.0.0.6', 'ipv4', false],
348+
['::', 'ipv6', true],
349+
['::1', 'ipv6', false],
350+
['192.168.1.0', 'ipv4', true],
351+
['193.168.1.0', 'ipv4', false],
352+
['8592:757c:efae:4e45::', 'ipv6', true],
353+
['1111:1111:1111:1111::', 'ipv6', false],
354+
].forEach((i) => {
355+
assert.strictEqual(bl.check(i[0], i[1]), i[2]);
356+
assert.strictEqual(test2.check(i[0], i[1]), i[2]);
357+
assert.strictEqual(test3.check(i[0], i[1]), i[2]);
358+
assert.strictEqual(test4.check(i[0], i[1]), i[2]);
359+
assert.strictEqual(test5.check(i[0], i[1]), i[2]);
360+
});
361+
}

test/parallel/test-net-blocklist.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
const common = require('../common');
44
const net = require('net');
55
const assert = require('assert');
6-
76
const blockList = new net.BlockList();
87
blockList.addAddress('127.0.0.1');
98
blockList.addAddress('127.0.0.2');

0 commit comments

Comments
 (0)