@verifyhash/ip-cidr 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/index.js +359 -0
- package/package.json +39 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <OWNER-NAME PLACEHOLDER>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# ip-cidr
|
|
2
|
+
|
|
3
|
+
A tiny, **zero-dependency** IPv4 (and basic IPv6) address/CIDR toolkit for
|
|
4
|
+
Node.js. It does the boring arithmetic you always end up rewriting: validate an
|
|
5
|
+
address, parse a CIDR block, ask whether a block contains an IP, and compute the
|
|
6
|
+
network/broadcast/first/last host and host count of a subnet — plus lossless
|
|
7
|
+
ip↔integer conversion.
|
|
8
|
+
|
|
9
|
+
No network access, no runtime dependencies, no servers. Just math on the bits.
|
|
10
|
+
|
|
11
|
+
> **Package name is a placeholder.** The manifest name is
|
|
12
|
+
> `PLACEHOLDER-ip-cidr`. TODO: the owner picks the final npm name/scope before
|
|
13
|
+
> any publish. This library is **not published** — graduation to npm is a
|
|
14
|
+
> human-gated step.
|
|
15
|
+
|
|
16
|
+
## Who it's for
|
|
17
|
+
|
|
18
|
+
- Anyone writing allow/deny lists, firewall-ish rules, or geo/ASN buckets who
|
|
19
|
+
needs a correct `contains(cidr, ip)` without pulling a dependency tree.
|
|
20
|
+
- Tools and CLIs that display subnet facts (network, broadcast, usable range,
|
|
21
|
+
host count) for an operator.
|
|
22
|
+
- Code that stores IPs as integers (databases, bitmaps) and needs a reliable
|
|
23
|
+
round-trip to/from dotted-quad.
|
|
24
|
+
|
|
25
|
+
## Install & usage
|
|
26
|
+
|
|
27
|
+
There is nothing to install beyond Node (uses only built-in `assert` for
|
|
28
|
+
tests). Once vendored/published under its final name you would:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const ip = require('ip-cidr'); // during development: require('./index.js')
|
|
32
|
+
|
|
33
|
+
// Containment
|
|
34
|
+
ip.contains('10.0.0.0/8', '10.1.2.3'); // => true
|
|
35
|
+
ip.contains('192.168.1.0/24', '192.168.2.9'); // => false
|
|
36
|
+
|
|
37
|
+
// Subnet facts
|
|
38
|
+
const net = ip.parseCidr('192.168.1.10/24');
|
|
39
|
+
net.network; // '192.168.1.0'
|
|
40
|
+
net.broadcast; // '192.168.1.255'
|
|
41
|
+
net.firstHost; // '192.168.1.1'
|
|
42
|
+
net.lastHost; // '192.168.1.254'
|
|
43
|
+
net.hostCount; // 254 (usable hosts)
|
|
44
|
+
net.size; // 256 (total addresses)
|
|
45
|
+
|
|
46
|
+
// Address <-> integer (lossless, unsigned 32-bit)
|
|
47
|
+
ip.ipv4ToInt('192.168.1.1'); // 3232235777
|
|
48
|
+
ip.intToIpv4(3232235777); // '192.168.1.1'
|
|
49
|
+
|
|
50
|
+
// Validation / dispatch
|
|
51
|
+
ip.isValidIPv4('1.2.3.256'); // false
|
|
52
|
+
ip.ipVersion('::1'); // 6
|
|
53
|
+
ip.parse('8.8.8.8'); // { version: 4, address: '8.8.8.8', int: 134744072 }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## API
|
|
57
|
+
|
|
58
|
+
### IPv4 core
|
|
59
|
+
- `isValidIPv4(str) → boolean` — strict dotted-quad. Rejects leading zeros
|
|
60
|
+
(`01.2.3.4`), out-of-range octets (`1.2.3.256`), and wrong segment counts.
|
|
61
|
+
- `ipv4ToInt(str) → number` — unsigned 32-bit integer (throws on invalid).
|
|
62
|
+
- `intToIpv4(int) → string` — inverse (throws if outside `0 … 4294967295`).
|
|
63
|
+
|
|
64
|
+
### IPv6 core
|
|
65
|
+
- `isValidIPv6(str) → boolean` — supports `::` compression and an embedded
|
|
66
|
+
IPv4 tail (`::ffff:192.168.0.1`). Zone ids (`%eth0`) are rejected.
|
|
67
|
+
- `ipv6ToBigInt(str) → bigint` / `bigIntToIpv6(bigint) → string` — lossless
|
|
68
|
+
round-trip via `BigInt`. `bigIntToIpv6` returns the **fully expanded**
|
|
69
|
+
(uncompressed) form, e.g. `0:0:0:0:0:0:0:1`, not `::1`.
|
|
70
|
+
|
|
71
|
+
### Unified
|
|
72
|
+
- `ipVersion(str) → 4 | 6 | null`
|
|
73
|
+
- `parse(str) → { version, address, int }` for v4 or
|
|
74
|
+
`{ version, address, big }` for v6 (throws on invalid).
|
|
75
|
+
- `parseCidr(str) → object` — see fields below (throws on invalid).
|
|
76
|
+
- `contains(cidr, ip) → boolean` — throws only if `cidr`/`ip` can't be parsed;
|
|
77
|
+
returns `false` (never throws) when the two are different address families.
|
|
78
|
+
|
|
79
|
+
### `parseCidr` result
|
|
80
|
+
|
|
81
|
+
IPv4 blocks return numeric and string forms:
|
|
82
|
+
|
|
83
|
+
| field | example (`192.168.1.10/24`) | notes |
|
|
84
|
+
|-------|------------------------------|-------|
|
|
85
|
+
| `network` / `networkInt` | `192.168.1.0` / `3232235776` | masked base address |
|
|
86
|
+
| `broadcast` / `broadcastInt` | `192.168.1.255` / … | all-ones host |
|
|
87
|
+
| `firstHost` / `lastHost` | `192.168.1.1` / `192.168.1.254` | usable range |
|
|
88
|
+
| `size` | `256` | total addresses in the block |
|
|
89
|
+
| `hostCount` | `254` | usable hosts (`size − 2`, except `/31`,`/32`) |
|
|
90
|
+
| `prefix` | `24` | |
|
|
91
|
+
|
|
92
|
+
IPv6 blocks return `network`, `firstHost`, `lastHost` (strings) plus
|
|
93
|
+
`networkBig`, `firstHostBig`, `lastHostBig`, `size`, and `hostCount` as
|
|
94
|
+
`BigInt`. There is **no** `broadcast` for IPv6 — broadcast is not an IPv6
|
|
95
|
+
concept.
|
|
96
|
+
|
|
97
|
+
### `/31` and `/32` edge cases
|
|
98
|
+
|
|
99
|
+
`/32` is treated as a single usable host (`hostCount: 1`). `/31` follows
|
|
100
|
+
RFC 3021: both addresses are usable point-to-point endpoints, so
|
|
101
|
+
`hostCount: 2` with no network/broadcast reservation. Wider IPv4 blocks use the
|
|
102
|
+
conventional `size − 2` usable-host count.
|
|
103
|
+
|
|
104
|
+
## IPv6 support scope (honest limits)
|
|
105
|
+
|
|
106
|
+
IPv6 is **partial by design**. Fully supported:
|
|
107
|
+
|
|
108
|
+
- **Validation & parsing** — `isValidIPv6`, `ipv6ToBigInt`, including `::`
|
|
109
|
+
compression and embedded-IPv4 tails.
|
|
110
|
+
- **Containment** — `contains(cidr, ip)` and `parseCidr` work for v6 using
|
|
111
|
+
`BigInt`, so there is no precision loss across the full 128-bit space.
|
|
112
|
+
|
|
113
|
+
Intentionally **not** provided for IPv6:
|
|
114
|
+
|
|
115
|
+
- No `broadcast`/first-usable/last-usable *reservation* semantics (v6 has no
|
|
116
|
+
broadcast). `firstHost`/`lastHost` are just the block's low/high addresses.
|
|
117
|
+
- `bigIntToIpv6` emits the expanded form only — it does **not** produce the
|
|
118
|
+
RFC 5952 shortest/canonical `::` form.
|
|
119
|
+
- No zone identifiers (`fe80::1%eth0`), no scoped/link-local special handling,
|
|
120
|
+
no CIDR aggregation/summarization, and no IPv4-mapped normalization beyond
|
|
121
|
+
parsing the embedded-IPv4 syntax.
|
|
122
|
+
|
|
123
|
+
If you need full IPv6 canonicalization or aggregation, use a dedicated library.
|
|
124
|
+
IPv4 is the first-class, fully-featured path here.
|
|
125
|
+
|
|
126
|
+
## Running the tests
|
|
127
|
+
|
|
128
|
+
One command, no framework (Node's built-in `assert`):
|
|
129
|
+
|
|
130
|
+
```sh
|
|
131
|
+
node test/index.test.js
|
|
132
|
+
# or
|
|
133
|
+
npm test
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The suite covers: containment true/false cases, `/31` and `/32` edges,
|
|
137
|
+
network/broadcast math for known blocks, ip↔int round-trips, invalid-input
|
|
138
|
+
rejection (bad octets, out-of-range masks, malformed strings), and the IPv6
|
|
139
|
+
validate/parse/contain paths.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT — see `LICENSE` (owner name is a placeholder until graduation).
|
package/index.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ip-cidr — zero-dependency IPv4 (+ basic IPv6) address/CIDR toolkit.
|
|
5
|
+
*
|
|
6
|
+
* Design notes:
|
|
7
|
+
* - IPv4 is fully supported: validate/parse, CIDR math (network, broadcast,
|
|
8
|
+
* first/last host, host count), ip<->integer conversion, and containment.
|
|
9
|
+
* - IPv6 support is intentionally limited to validate/parse and containment
|
|
10
|
+
* (see README "IPv6 support scope"). We use BigInt for IPv6 so no
|
|
11
|
+
* precision is lost, but we do not expose broadcast/first/last helpers for
|
|
12
|
+
* v6 because "broadcast" is not an IPv6 concept.
|
|
13
|
+
* - No network access, no dependencies, no I/O.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// IPv4 address <-> integer
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validate a dotted-quad IPv4 string. Requires exactly four octets, each an
|
|
22
|
+
* integer 0-255 with no leading zeros (so "01.2.3.4" and "1.2.3.256" are
|
|
23
|
+
* rejected). Returns true/false and never throws.
|
|
24
|
+
* @param {string} str
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
function isValidIPv4(str) {
|
|
28
|
+
if (typeof str !== 'string') return false;
|
|
29
|
+
const parts = str.split('.');
|
|
30
|
+
if (parts.length !== 4) return false;
|
|
31
|
+
for (const part of parts) {
|
|
32
|
+
// Must be 1-3 digits, no leading zero (except the single char "0").
|
|
33
|
+
if (!/^\d{1,3}$/.test(part)) return false;
|
|
34
|
+
if (part.length > 1 && part[0] === '0') return false;
|
|
35
|
+
const n = Number(part);
|
|
36
|
+
if (n < 0 || n > 255) return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Convert a valid IPv4 string to its unsigned 32-bit integer value.
|
|
43
|
+
* @param {string} str
|
|
44
|
+
* @returns {number} 0 .. 4294967295
|
|
45
|
+
* @throws {Error} on invalid input
|
|
46
|
+
*/
|
|
47
|
+
function ipv4ToInt(str) {
|
|
48
|
+
if (!isValidIPv4(str)) throw new Error('invalid IPv4 address: ' + str);
|
|
49
|
+
const [a, b, c, d] = str.split('.').map(Number);
|
|
50
|
+
// >>> 0 forces the result to an unsigned 32-bit integer.
|
|
51
|
+
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert an unsigned 32-bit integer to a dotted-quad IPv4 string.
|
|
56
|
+
* @param {number} int 0 .. 4294967295
|
|
57
|
+
* @returns {string}
|
|
58
|
+
* @throws {Error} on out-of-range / non-integer input
|
|
59
|
+
*/
|
|
60
|
+
function intToIpv4(int) {
|
|
61
|
+
if (!Number.isInteger(int) || int < 0 || int > 0xffffffff) {
|
|
62
|
+
throw new Error('integer out of IPv4 range: ' + int);
|
|
63
|
+
}
|
|
64
|
+
return [
|
|
65
|
+
(int >>> 24) & 0xff,
|
|
66
|
+
(int >>> 16) & 0xff,
|
|
67
|
+
(int >>> 8) & 0xff,
|
|
68
|
+
int & 0xff,
|
|
69
|
+
].join('.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// IPv6 address <-> BigInt (validate/parse + containment only)
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Validate an IPv6 string, supporting "::" compression and an optional
|
|
78
|
+
* embedded IPv4 tail (e.g. "::ffff:192.168.0.1"). Returns true/false, never
|
|
79
|
+
* throws. Zone identifiers ("%eth0") are not accepted.
|
|
80
|
+
* @param {string} str
|
|
81
|
+
* @returns {boolean}
|
|
82
|
+
*/
|
|
83
|
+
function isValidIPv6(str) {
|
|
84
|
+
try {
|
|
85
|
+
ipv6ToBigInt(str);
|
|
86
|
+
return true;
|
|
87
|
+
} catch (_e) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Convert an IPv6 string to a BigInt (0 .. 2**128-1). Supports "::"
|
|
94
|
+
* compression and an embedded IPv4 tail.
|
|
95
|
+
* @param {string} str
|
|
96
|
+
* @returns {bigint}
|
|
97
|
+
* @throws {Error} on invalid input
|
|
98
|
+
*/
|
|
99
|
+
function ipv6ToBigInt(str) {
|
|
100
|
+
if (typeof str !== 'string' || str.length === 0) {
|
|
101
|
+
throw new Error('invalid IPv6 address: not a string');
|
|
102
|
+
}
|
|
103
|
+
if (str.indexOf('%') !== -1) {
|
|
104
|
+
throw new Error('invalid IPv6 address: zone id not supported');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// At most one "::".
|
|
108
|
+
const doubleColonCount = str.split('::').length - 1;
|
|
109
|
+
if (doubleColonCount > 1) throw new Error('invalid IPv6 address: multiple "::"');
|
|
110
|
+
|
|
111
|
+
let head;
|
|
112
|
+
let tail;
|
|
113
|
+
if (doubleColonCount === 1) {
|
|
114
|
+
const [left, right] = str.split('::');
|
|
115
|
+
head = left.length ? left.split(':') : [];
|
|
116
|
+
tail = right.length ? right.split(':') : [];
|
|
117
|
+
} else {
|
|
118
|
+
head = str.split(':');
|
|
119
|
+
tail = [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Expand a trailing embedded IPv4 group (must be the last group) into two
|
|
123
|
+
// 16-bit hextets.
|
|
124
|
+
function expandGroups(groups) {
|
|
125
|
+
if (groups.length === 0) return [];
|
|
126
|
+
const out = [];
|
|
127
|
+
for (let i = 0; i < groups.length; i++) {
|
|
128
|
+
const g = groups[i];
|
|
129
|
+
if (g.indexOf('.') !== -1) {
|
|
130
|
+
// Only allowed as the very last group.
|
|
131
|
+
if (i !== groups.length - 1) {
|
|
132
|
+
throw new Error('invalid IPv6 address: embedded IPv4 not at end');
|
|
133
|
+
}
|
|
134
|
+
if (!isValidIPv4(g)) throw new Error('invalid IPv6 address: bad embedded IPv4');
|
|
135
|
+
const v = ipv4ToInt(g);
|
|
136
|
+
out.push(((v >>> 16) & 0xffff).toString(16));
|
|
137
|
+
out.push((v & 0xffff).toString(16));
|
|
138
|
+
} else {
|
|
139
|
+
if (!/^[0-9a-fA-F]{1,4}$/.test(g)) {
|
|
140
|
+
throw new Error('invalid IPv6 address: bad hextet "' + g + '"');
|
|
141
|
+
}
|
|
142
|
+
out.push(g);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const headExp = expandGroups(head);
|
|
149
|
+
const tailExp = expandGroups(tail);
|
|
150
|
+
|
|
151
|
+
let groups;
|
|
152
|
+
if (doubleColonCount === 1) {
|
|
153
|
+
const missing = 8 - (headExp.length + tailExp.length);
|
|
154
|
+
if (missing < 1) {
|
|
155
|
+
// "::" must stand for at least one zero group.
|
|
156
|
+
throw new Error('invalid IPv6 address: "::" compresses nothing');
|
|
157
|
+
}
|
|
158
|
+
groups = headExp.concat(new Array(missing).fill('0'), tailExp);
|
|
159
|
+
} else {
|
|
160
|
+
groups = headExp;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (groups.length !== 8) {
|
|
164
|
+
throw new Error('invalid IPv6 address: expected 8 groups, got ' + groups.length);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let result = 0n;
|
|
168
|
+
for (const g of groups) {
|
|
169
|
+
result = (result << 16n) | BigInt(parseInt(g, 16));
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Convert a BigInt (0 .. 2**128-1) to a fully-expanded (uncompressed) IPv6
|
|
176
|
+
* string. This is a canonical form, not the shortest form.
|
|
177
|
+
* @param {bigint} value
|
|
178
|
+
* @returns {string}
|
|
179
|
+
* @throws {Error} on out-of-range input
|
|
180
|
+
*/
|
|
181
|
+
function bigIntToIpv6(value) {
|
|
182
|
+
if (typeof value !== 'bigint' || value < 0n || value > (1n << 128n) - 1n) {
|
|
183
|
+
throw new Error('value out of IPv6 range');
|
|
184
|
+
}
|
|
185
|
+
const groups = [];
|
|
186
|
+
for (let i = 7; i >= 0; i--) {
|
|
187
|
+
const hextet = (value >> BigInt(i * 16)) & 0xffffn;
|
|
188
|
+
groups.push(hextet.toString(16));
|
|
189
|
+
}
|
|
190
|
+
return groups.join(':');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// Address family detection + unified parse
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Return 4 if str is a valid IPv4 address, 6 if a valid IPv6 address, else
|
|
199
|
+
* null. Never throws.
|
|
200
|
+
* @param {string} str
|
|
201
|
+
* @returns {4|6|null}
|
|
202
|
+
*/
|
|
203
|
+
function ipVersion(str) {
|
|
204
|
+
if (isValidIPv4(str)) return 4;
|
|
205
|
+
if (isValidIPv6(str)) return 6;
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Parse any IPv4 or IPv6 address into a descriptor object, or throw.
|
|
211
|
+
* @param {string} str
|
|
212
|
+
* @returns {{version:4, address:string, int:number}|{version:6, address:string, big:bigint}}
|
|
213
|
+
* @throws {Error} on invalid input
|
|
214
|
+
*/
|
|
215
|
+
function parse(str) {
|
|
216
|
+
const v = ipVersion(str);
|
|
217
|
+
if (v === 4) return { version: 4, address: str, int: ipv4ToInt(str) };
|
|
218
|
+
if (v === 6) return { version: 6, address: str, big: ipv6ToBigInt(str) };
|
|
219
|
+
throw new Error('invalid IP address: ' + str);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
// CIDR parsing
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Parse a CIDR string ("10.0.0.0/24" or "2001:db8::/32").
|
|
228
|
+
*
|
|
229
|
+
* For IPv4 returns a descriptor with numeric network/broadcast/first/last host
|
|
230
|
+
* and host count. For IPv6 returns network/first/last/count as BigInt strings
|
|
231
|
+
* plus a numeric prefix (no broadcast — not an IPv6 concept).
|
|
232
|
+
*
|
|
233
|
+
* @param {string} str
|
|
234
|
+
* @returns {object} see README for the full shape
|
|
235
|
+
* @throws {Error} on invalid input
|
|
236
|
+
*/
|
|
237
|
+
function parseCidr(str) {
|
|
238
|
+
if (typeof str !== 'string') throw new Error('invalid CIDR: not a string');
|
|
239
|
+
const slash = str.indexOf('/');
|
|
240
|
+
if (slash === -1) throw new Error('invalid CIDR: missing "/" in ' + str);
|
|
241
|
+
const addr = str.slice(0, slash);
|
|
242
|
+
const prefixStr = str.slice(slash + 1);
|
|
243
|
+
if (!/^\d{1,3}$/.test(prefixStr)) {
|
|
244
|
+
throw new Error('invalid CIDR: bad prefix in ' + str);
|
|
245
|
+
}
|
|
246
|
+
const prefix = Number(prefixStr);
|
|
247
|
+
const v = ipVersion(addr);
|
|
248
|
+
if (v === 4) return _parseCidr4(addr, prefix);
|
|
249
|
+
if (v === 6) return _parseCidr6(addr, prefix);
|
|
250
|
+
throw new Error('invalid CIDR: bad address in ' + str);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _parseCidr4(addr, prefix) {
|
|
254
|
+
if (prefix < 0 || prefix > 32) {
|
|
255
|
+
throw new Error('invalid CIDR: IPv4 prefix must be 0-32, got ' + prefix);
|
|
256
|
+
}
|
|
257
|
+
const ipInt = ipv4ToInt(addr);
|
|
258
|
+
// Mask as unsigned 32-bit. prefix 0 => 0 mask (avoid <<32 UB).
|
|
259
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
260
|
+
const network = (ipInt & mask) >>> 0;
|
|
261
|
+
const broadcast = (network | (~mask >>> 0)) >>> 0;
|
|
262
|
+
const size = prefix === 0 ? 0x100000000 : Math.pow(2, 32 - prefix);
|
|
263
|
+
|
|
264
|
+
let firstHost;
|
|
265
|
+
let lastHost;
|
|
266
|
+
let usableHosts;
|
|
267
|
+
if (prefix >= 31) {
|
|
268
|
+
// /31 (RFC 3021 point-to-point) and /32 have no "network/broadcast"
|
|
269
|
+
// reservation, so every address is usable.
|
|
270
|
+
firstHost = network;
|
|
271
|
+
lastHost = broadcast;
|
|
272
|
+
usableHosts = size; // /32 => 1, /31 => 2
|
|
273
|
+
} else {
|
|
274
|
+
firstHost = (network + 1) >>> 0;
|
|
275
|
+
lastHost = (broadcast - 1) >>> 0;
|
|
276
|
+
usableHosts = size - 2;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
version: 4,
|
|
281
|
+
cidr: intToIpv4(network) + '/' + prefix,
|
|
282
|
+
prefix: prefix,
|
|
283
|
+
network: intToIpv4(network),
|
|
284
|
+
networkInt: network,
|
|
285
|
+
broadcast: intToIpv4(broadcast),
|
|
286
|
+
broadcastInt: broadcast,
|
|
287
|
+
firstHost: intToIpv4(firstHost),
|
|
288
|
+
firstHostInt: firstHost,
|
|
289
|
+
lastHost: intToIpv4(lastHost),
|
|
290
|
+
lastHostInt: lastHost,
|
|
291
|
+
size: size, // total addresses in the block
|
|
292
|
+
hostCount: usableHosts, // usable host count
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function _parseCidr6(addr, prefix) {
|
|
297
|
+
if (prefix < 0 || prefix > 128) {
|
|
298
|
+
throw new Error('invalid CIDR: IPv6 prefix must be 0-128, got ' + prefix);
|
|
299
|
+
}
|
|
300
|
+
const ipBig = ipv6ToBigInt(addr);
|
|
301
|
+
const full = (1n << 128n) - 1n;
|
|
302
|
+
const mask = prefix === 0 ? 0n : (full << BigInt(128 - prefix)) & full;
|
|
303
|
+
const network = ipBig & mask;
|
|
304
|
+
const last = network | (~mask & full);
|
|
305
|
+
const size = 1n << BigInt(128 - prefix);
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
version: 6,
|
|
309
|
+
cidr: bigIntToIpv6(network) + '/' + prefix,
|
|
310
|
+
prefix: prefix,
|
|
311
|
+
network: bigIntToIpv6(network),
|
|
312
|
+
networkBig: network,
|
|
313
|
+
firstHost: bigIntToIpv6(network),
|
|
314
|
+
firstHostBig: network,
|
|
315
|
+
lastHost: bigIntToIpv6(last),
|
|
316
|
+
lastHostBig: last,
|
|
317
|
+
size: size, // BigInt total addresses
|
|
318
|
+
hostCount: size, // IPv6 has no broadcast reservation
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
// Containment
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Does the CIDR block contain the given IP? Both must be the same family.
|
|
328
|
+
* Returns a boolean; never throws for a well-formed cidr/ip of matching
|
|
329
|
+
* family. Throws only if either argument is not parseable.
|
|
330
|
+
* @param {string} cidr e.g. "10.0.0.0/24"
|
|
331
|
+
* @param {string} ip e.g. "10.0.0.5"
|
|
332
|
+
* @returns {boolean}
|
|
333
|
+
* @throws {Error} if cidr or ip cannot be parsed
|
|
334
|
+
*/
|
|
335
|
+
function contains(cidr, ip) {
|
|
336
|
+
const block = parseCidr(cidr);
|
|
337
|
+
const addr = parse(ip);
|
|
338
|
+
if (block.version !== addr.version) return false; // cross-family never matches
|
|
339
|
+
if (block.version === 4) {
|
|
340
|
+
return addr.int >= block.networkInt && addr.int <= block.broadcastInt;
|
|
341
|
+
}
|
|
342
|
+
return addr.big >= block.networkBig && addr.big <= block.lastHostBig;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
// IPv4 core
|
|
347
|
+
isValidIPv4,
|
|
348
|
+
ipv4ToInt,
|
|
349
|
+
intToIpv4,
|
|
350
|
+
// IPv6 core
|
|
351
|
+
isValidIPv6,
|
|
352
|
+
ipv6ToBigInt,
|
|
353
|
+
bigIntToIpv6,
|
|
354
|
+
// unified
|
|
355
|
+
ipVersion,
|
|
356
|
+
parse,
|
|
357
|
+
parseCidr,
|
|
358
|
+
contains,
|
|
359
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@verifyhash/ip-cidr",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-dependency IPv4 (and basic IPv6) address/CIDR toolkit for Node.js: validate/parse, CIDR math (network, broadcast, first/last host, host count), ip<->integer, and containment.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ip",
|
|
7
|
+
"ipv4",
|
|
8
|
+
"ipv6",
|
|
9
|
+
"cidr",
|
|
10
|
+
"subnet",
|
|
11
|
+
"netmask",
|
|
12
|
+
"network",
|
|
13
|
+
"broadcast",
|
|
14
|
+
"contains",
|
|
15
|
+
"ip-to-int"
|
|
16
|
+
],
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"test": "node test/index.test.js"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"files": [
|
|
23
|
+
"index.js",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/verifyhash/libs.git",
|
|
33
|
+
"directory": "ip-cidr"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/verifyhash/libs/tree/main/ip-cidr#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/verifyhash/libs/issues"
|
|
38
|
+
}
|
|
39
|
+
}
|