node-ip-ts 1.0.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/src/index.ts ADDED
@@ -0,0 +1,454 @@
1
+ import { networkInterfaces } from 'os';
2
+
3
+ // ─── Types ────────────────────────────────────────────────────────────────────
4
+
5
+ export type IPFamily = 'ipv4' | 'ipv6';
6
+
7
+ export interface SubnetInfo {
8
+ networkAddress: string;
9
+ firstAddress: string;
10
+ lastAddress: string;
11
+ broadcastAddress: string;
12
+ subnetMask: string;
13
+ subnetMaskLength: number;
14
+ numHosts: number;
15
+ length: number;
16
+ contains(other: string): boolean;
17
+ }
18
+
19
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
20
+
21
+ function normalizeFamily(family?: number | string): IPFamily {
22
+ if (family === 4) return 'ipv4';
23
+ if (family === 6) return 'ipv6';
24
+ return family ? (family as string).toLowerCase() as IPFamily : 'ipv4';
25
+ }
26
+
27
+ // ─── Buffer ↔ String conversion ───────────────────────────────────────────────
28
+
29
+ /**
30
+ * Convert an IP address string to a Buffer.
31
+ * Optionally write into an existing buffer at a given offset.
32
+ */
33
+ export function toBuffer(ip: string, buff?: Buffer, offset?: number): Buffer {
34
+ const off = offset !== undefined ? ~~offset : 0;
35
+ let result: Buffer;
36
+
37
+ if (isV4Format(ip)) {
38
+ result = buff ?? Buffer.alloc(off + 4);
39
+ let pos = off;
40
+ for (const byte of ip.split('.')) {
41
+ result[pos++] = parseInt(byte, 10) & 0xff;
42
+ }
43
+ return result;
44
+ }
45
+
46
+ if (isV6Format(ip)) {
47
+ const sections = ip.split(':', 8);
48
+
49
+ for (let i = 0; i < sections.length; i++) {
50
+ if (isV4Format(sections[i])) {
51
+ const v4buf = toBuffer(sections[i]);
52
+ sections[i] = v4buf.subarray(0, 2).toString('hex');
53
+ if (++i < 8) {
54
+ sections.splice(i, 0, v4buf.subarray(2, 4).toString('hex'));
55
+ }
56
+ }
57
+ }
58
+
59
+ // Expand ::
60
+ if (sections[0] === '') {
61
+ while (sections.length < 8) sections.unshift('0');
62
+ } else if (sections[sections.length - 1] === '') {
63
+ while (sections.length < 8) sections.push('0');
64
+ } else if (sections.length < 8) {
65
+ let emptyIdx = 0;
66
+ for (; emptyIdx < sections.length && sections[emptyIdx] !== ''; emptyIdx++);
67
+ const toInsert = 9 - sections.length;
68
+ sections.splice(emptyIdx, 1, ...Array(toInsert).fill('0'));
69
+ }
70
+
71
+ result = buff ?? Buffer.alloc(off + 16);
72
+ let pos = off;
73
+ for (const section of sections) {
74
+ const word = parseInt(section, 16);
75
+ result[pos++] = (word >> 8) & 0xff;
76
+ result[pos++] = word & 0xff;
77
+ }
78
+ return result;
79
+ }
80
+
81
+ throw new Error(`Invalid ip address: ${ip}`);
82
+ }
83
+
84
+ /**
85
+ * Convert a Buffer back to an IP address string.
86
+ */
87
+ export function toString(buff: Buffer, offset = 0, length?: number): string {
88
+ const len = length ?? buff.length - offset;
89
+
90
+ if (len === 4) {
91
+ const parts: number[] = [];
92
+ for (let i = 0; i < 4; i++) parts.push(buff[offset + i]);
93
+ return parts.join('.');
94
+ }
95
+
96
+ if (len === 16) {
97
+ const parts: string[] = [];
98
+ for (let i = 0; i < 16; i += 2) {
99
+ parts.push(buff.readUInt16BE(offset + i).toString(16));
100
+ }
101
+ let result = parts.join(':');
102
+ result = result.replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3');
103
+ result = result.replace(/:{3,4}/, '::');
104
+ return result;
105
+ }
106
+
107
+ return '';
108
+ }
109
+
110
+ // ─── Format detection ─────────────────────────────────────────────────────────
111
+
112
+ const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
113
+ const IPV6_REGEX =
114
+ /^(::)?(((\d{1,3}\.){3}(\d{1,3}){1})?([0-9a-f]){0,4}:{0,2}){1,8}(::)?$/i;
115
+
116
+ export function isV4Format(ip: string): boolean {
117
+ return IPV4_REGEX.test(ip);
118
+ }
119
+
120
+ export function isV6Format(ip: string): boolean {
121
+ return IPV6_REGEX.test(ip);
122
+ }
123
+
124
+ // ─── Mask / CIDR utilities ───────────────────────────────────────────────────
125
+
126
+ /**
127
+ * Create a subnet mask from a prefix length.
128
+ */
129
+ export function fromPrefixLen(prefixlen: number, family?: number | string): string {
130
+ const fam = prefixlen > 32 ? 'ipv6' : normalizeFamily(family);
131
+ const len = fam === 'ipv6' ? 16 : 4;
132
+ const buff = Buffer.alloc(len);
133
+
134
+ let remaining = prefixlen;
135
+ for (let i = 0; i < len; i++) {
136
+ const bits = Math.min(remaining, 8);
137
+ remaining -= bits;
138
+ buff[i] = ~(0xff >> bits) & 0xff;
139
+ }
140
+
141
+ return toString(buff);
142
+ }
143
+
144
+ /**
145
+ * Apply a subnet mask to an address.
146
+ */
147
+ export function mask(addr: string, maskStr: string): string {
148
+ const addrBuf = toBuffer(addr);
149
+ const maskBuf = toBuffer(maskStr);
150
+ const result = Buffer.alloc(Math.max(addrBuf.length, maskBuf.length));
151
+
152
+ if (addrBuf.length === maskBuf.length) {
153
+ for (let i = 0; i < addrBuf.length; i++) {
154
+ result[i] = addrBuf[i] & maskBuf[i];
155
+ }
156
+ } else if (maskBuf.length === 4) {
157
+ // IPv6 address, IPv4 mask → mask low 4 bytes
158
+ for (let i = 0; i < maskBuf.length; i++) {
159
+ result[i] = addrBuf[addrBuf.length - 4 + i] & maskBuf[i];
160
+ }
161
+ } else {
162
+ // IPv6 mask, IPv4 address → embed as ::ffff:ipv4
163
+ for (let i = 0; i < result.length - 6; i++) result[i] = 0;
164
+ result[10] = 0xff;
165
+ result[11] = 0xff;
166
+ for (let i = 0; i < addrBuf.length; i++) {
167
+ result[i + 12] = addrBuf[i] & maskBuf[i + 12];
168
+ }
169
+ }
170
+
171
+ return toString(result);
172
+ }
173
+
174
+ /**
175
+ * Return the network address for a CIDR string (e.g. "192.168.1.1/24").
176
+ */
177
+ export function cidr(cidrString: string): string {
178
+ const [addr, prefixStr] = cidrString.split('/');
179
+ if (!prefixStr) throw new Error(`invalid CIDR subnet: ${cidrString}`);
180
+ return mask(addr, fromPrefixLen(parseInt(prefixStr, 10)));
181
+ }
182
+
183
+ /**
184
+ * Compute full subnet information from address + mask.
185
+ */
186
+ export function subnet(addr: string, subnetMask: string): SubnetInfo {
187
+ const networkAddress = toLong(mask(addr, subnetMask));
188
+ const maskBuf = toBuffer(subnetMask);
189
+
190
+ let maskLength = 0;
191
+ for (const byte of maskBuf) {
192
+ if (byte === 0xff) {
193
+ maskLength += 8;
194
+ } else {
195
+ let octet = byte & 0xff;
196
+ while (octet) {
197
+ octet = (octet << 1) & 0xff;
198
+ maskLength++;
199
+ }
200
+ }
201
+ }
202
+
203
+ const numberOfAddresses = 2 ** (32 - maskLength);
204
+
205
+ return {
206
+ networkAddress: fromLong(networkAddress),
207
+ firstAddress:
208
+ numberOfAddresses <= 2
209
+ ? fromLong(networkAddress)
210
+ : fromLong(networkAddress + 1),
211
+ lastAddress:
212
+ numberOfAddresses <= 2
213
+ ? fromLong(networkAddress + numberOfAddresses - 1)
214
+ : fromLong(networkAddress + numberOfAddresses - 2),
215
+ broadcastAddress: fromLong(networkAddress + numberOfAddresses - 1),
216
+ subnetMask,
217
+ subnetMaskLength: maskLength,
218
+ numHosts: numberOfAddresses <= 2 ? numberOfAddresses : numberOfAddresses - 2,
219
+ length: numberOfAddresses,
220
+ contains(other: string): boolean {
221
+ return networkAddress === toLong(mask(other, subnetMask));
222
+ },
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Compute full subnet information from a CIDR string.
228
+ */
229
+ export function cidrSubnet(cidrString: string): SubnetInfo {
230
+ const [addr, prefixStr] = cidrString.split('/');
231
+ if (!prefixStr) throw new Error(`invalid CIDR subnet: ${cidrString}`);
232
+ return subnet(addr, fromPrefixLen(parseInt(prefixStr, 10)));
233
+ }
234
+
235
+ // ─── Bitwise operations ───────────────────────────────────────────────────────
236
+
237
+ /** Bitwise NOT of an IP address. */
238
+ export function not(addr: string): string {
239
+ const buff = toBuffer(addr);
240
+ for (let i = 0; i < buff.length; i++) buff[i] ^= 0xff;
241
+ return toString(buff);
242
+ }
243
+
244
+ /** Bitwise OR of two IP addresses (supports mixed protocol). */
245
+ export function or(a: string, b: string): string {
246
+ let bufA = toBuffer(a);
247
+ let bufB = toBuffer(b);
248
+
249
+ if (bufA.length === bufB.length) {
250
+ for (let i = 0; i < bufA.length; i++) bufA[i] |= bufB[i];
251
+ return toString(bufA);
252
+ }
253
+
254
+ // Ensure bufLong is the longer buffer
255
+ let bufLong = bufA.length > bufB.length ? bufA : bufB;
256
+ const bufShort = bufA.length > bufB.length ? bufB : bufA;
257
+ bufLong = Buffer.from(bufLong); // avoid mutating original ref
258
+
259
+ const offset = bufLong.length - bufShort.length;
260
+ for (let i = offset; i < bufLong.length; i++) {
261
+ bufLong[i] |= bufShort[i - offset];
262
+ }
263
+
264
+ return toString(bufLong);
265
+ }
266
+
267
+ /** Deep equality check for two IP addresses (supports mixed IPv4/IPv6). */
268
+ export function isEqual(a: string, b: string): boolean {
269
+ let bufA = toBuffer(a);
270
+ let bufB = toBuffer(b);
271
+
272
+ if (bufA.length === bufB.length) {
273
+ for (let i = 0; i < bufA.length; i++) {
274
+ if (bufA[i] !== bufB[i]) return false;
275
+ }
276
+ return true;
277
+ }
278
+
279
+ // Ensure bufA is IPv4 (shorter)
280
+ if (bufB.length === 4) {
281
+ const tmp = bufB;
282
+ bufB = bufA;
283
+ bufA = tmp;
284
+ }
285
+
286
+ for (let i = 0; i < 10; i++) {
287
+ if (bufB[i] !== 0) return false;
288
+ }
289
+
290
+ const word = bufB.readUInt16BE(10);
291
+ if (word !== 0 && word !== 0xffff) return false;
292
+
293
+ for (let i = 0; i < 4; i++) {
294
+ if (bufA[i] !== bufB[i + 12]) return false;
295
+ }
296
+
297
+ return true;
298
+ }
299
+
300
+ // ─── Address classification ───────────────────────────────────────────────────
301
+
302
+ /** Returns true if the address is a loopback address. */
303
+ export function isLoopback(addr: string): boolean {
304
+ // Plain long integer (no dots or colons) → convert to dotted decimal
305
+ if (!/\./.test(addr) && !/:/.test(addr)) {
306
+ addr = fromLong(Number(addr));
307
+ }
308
+
309
+ return (
310
+ /^(::f{4}:)?127\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/.test(addr) ||
311
+ /^0177\./.test(addr) ||
312
+ /^0x7f\./i.test(addr) ||
313
+ /^fe80::1$/i.test(addr) ||
314
+ /^::1$/.test(addr) ||
315
+ /^::$/.test(addr)
316
+ );
317
+ }
318
+
319
+ /** Returns true if the address is a private (RFC 1918 / ULA) address. */
320
+ export function isPrivate(addr: string): boolean {
321
+ if (isLoopback(addr)) return true;
322
+
323
+ // For IPv4-only addresses, normalize first
324
+ if (!isV6Format(addr)) {
325
+ const ipl = normalizeToLong(addr);
326
+ if (ipl < 0) throw new Error('invalid ipv4 address');
327
+ addr = fromLong(ipl);
328
+ }
329
+
330
+ return (
331
+ /^(::f{4}:)?10\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
332
+ /^(::f{4}:)?192\.168\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
333
+ /^(::f{4}:)?172\.(1[6-9]|2\d|30|31)\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
334
+ /^(::f{4}:)?169\.254\.([0-9]{1,3})\.([0-9]{1,3})$/i.test(addr) ||
335
+ /^f[cd][0-9a-f]{2}:/i.test(addr) ||
336
+ /^fe80:/i.test(addr) ||
337
+ /^::1$/.test(addr) ||
338
+ /^::$/.test(addr)
339
+ );
340
+ }
341
+
342
+ /** Returns true if the address is a publicly routable address. */
343
+ export function isPublic(addr: string): boolean {
344
+ return !isPrivate(addr);
345
+ }
346
+
347
+ // ─── Loopback & address resolution ───────────────────────────────────────────
348
+
349
+ /** Return the loopback address for the given IP family. */
350
+ export function loopback(family?: number | string): string {
351
+ const fam = normalizeFamily(family);
352
+ if (fam !== 'ipv4' && fam !== 'ipv6') {
353
+ throw new Error('family must be ipv4 or ipv6');
354
+ }
355
+ return fam === 'ipv4' ? '127.0.0.1' : 'fe80::1';
356
+ }
357
+
358
+ /**
359
+ * Return a network interface address.
360
+ * - `name`: interface name, `'public'`, `'private'`, or `undefined` (any private)
361
+ * - `family`: `'ipv4'` (default) or `'ipv6'`
362
+ */
363
+ export function address(name?: string, family?: number | string): string {
364
+ const ifaces = networkInterfaces();
365
+ const fam = normalizeFamily(family);
366
+
367
+ if (name && name !== 'private' && name !== 'public') {
368
+ const iface = ifaces[name];
369
+ if (!iface) return loopback(fam);
370
+ const match = iface.filter((d) => normalizeFamily(d.family) === fam);
371
+ return match.length ? match[0].address : loopback(fam);
372
+ }
373
+
374
+ const all = Object.values(ifaces)
375
+ .flat()
376
+ .filter((d): d is NonNullable<typeof d> => {
377
+ if (!d) return false;
378
+ if (normalizeFamily(d.family) !== fam) return false;
379
+ if (isLoopback(d.address)) return false;
380
+ if (!name) return true;
381
+ return name === 'public' ? isPublic(d.address) : isPrivate(d.address);
382
+ })
383
+ .map((d) => d.address);
384
+
385
+ return all.length ? all[0] : loopback(fam);
386
+ }
387
+
388
+ // ─── Long integer conversion ──────────────────────────────────────────────────
389
+
390
+ /** Convert a dotted-decimal IPv4 address to a 32-bit unsigned integer. */
391
+ export function toLong(ip: string): number {
392
+ let ipl = 0;
393
+ for (const octet of ip.split('.')) {
394
+ ipl = (ipl << 8) + parseInt(octet, 10);
395
+ }
396
+ return ipl >>> 0;
397
+ }
398
+
399
+ /** Convert a 32-bit unsigned integer to a dotted-decimal IPv4 address. */
400
+ export function fromLong(ipl: number): string {
401
+ return [
402
+ (ipl >>> 24) & 255,
403
+ (ipl >>> 16) & 255,
404
+ (ipl >>> 8) & 255,
405
+ ipl & 255,
406
+ ].join('.');
407
+ }
408
+
409
+ /**
410
+ * Normalize an IPv4 address (supports decimal, octal, hex, and compact notations)
411
+ * and return it as a 32-bit unsigned long. Returns -1 on error.
412
+ */
413
+ export function normalizeToLong(addr: string): number {
414
+ const parts = addr.split('.').map((part) => {
415
+ if (part.startsWith('0x') || part.startsWith('0X')) {
416
+ const v = parseInt(part, 16);
417
+ return Number.isNaN(v) ? NaN : v;
418
+ }
419
+ if (part.startsWith('0') && part !== '0' && /^[0-7]+$/.test(part)) {
420
+ return parseInt(part, 8);
421
+ }
422
+ if (/^[1-9]\d*$/.test(part) || part === '0') {
423
+ return parseInt(part, 10);
424
+ }
425
+ return NaN;
426
+ });
427
+
428
+ if (parts.some(Number.isNaN)) return -1;
429
+
430
+ const n = parts.length;
431
+ let val = 0;
432
+
433
+ switch (n) {
434
+ case 1:
435
+ val = parts[0];
436
+ break;
437
+ case 2:
438
+ if (parts[0] > 0xff || parts[1] > 0xffffff) return -1;
439
+ val = (parts[0] << 24) | (parts[1] & 0xffffff);
440
+ break;
441
+ case 3:
442
+ if (parts[0] > 0xff || parts[1] > 0xff || parts[2] > 0xffff) return -1;
443
+ val = (parts[0] << 24) | (parts[1] << 16) | (parts[2] & 0xffff);
444
+ break;
445
+ case 4:
446
+ if (parts.some((p) => p > 0xff)) return -1;
447
+ val = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
448
+ break;
449
+ default:
450
+ return -1;
451
+ }
452
+
453
+ return val >>> 0;
454
+ }