diff --git a/src/main/ts/ip.ts b/src/main/ts/ip.ts index ac7e64a..1f67349 100644 --- a/src/main/ts/ip.ts +++ b/src/main/ts/ip.ts @@ -1,5 +1,409 @@ -export const foo = 'bar' +import { Buffer } from 'buffer' +import os from 'os' -export const ip = {foo} +const PUBLIC = 'public' +const PRIVATE = 'private' +export const IPV4 = 'IPv4' +export const IPV6 = 'IPv6' + +// https://stackoverflow.com/questions/5284147/validating-ipv4-addresses-with-regexp +// https://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses +export const V4_RE = /^(\d{1,3}(\.|$)){4}$/ +export const V6_RE = /^(?=.+)(::)?(((\d{1,3}\.){3}\d{1,3})?|([0-9a-f]{0,4}:{0,2})){1,8}(::)?$/i +export const V4_S_RE = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/ +export const V6_S_RE = /(([\dA-Fa-f]{1,4}:){7}[\dA-Fa-f]{1,4}|([\dA-Fa-f]{1,4}:){1,7}:|([\dA-Fa-f]{1,4}:){1,6}:[\dA-Fa-f]{1,4}|([\dA-Fa-f]{1,4}:){1,5}(:[\dA-Fa-f]{1,4}){1,2}|([\dA-Fa-f]{1,4}:){1,4}(:[\dA-Fa-f]{1,4}){1,3}|([\dA-Fa-f]{1,4}:){1,3}(:[\dA-Fa-f]{1,4}){1,4}|([\dA-Fa-f]{1,4}:){1,2}(:[\dA-Fa-f]{1,4}){1,5}|[\dA-Fa-f]{1,4}:((:[\dA-Fa-f]{1,4}){1,6})|:((:[\dA-Fa-f]{1,4}){1,7}|:)|fe80:(:[\dA-Fa-f]{0,4}){0,4}%[\dA-Za-z]+|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)|([\dA-Fa-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d))$/ + +export const isV4Format = (ip: string): boolean => V4_RE.test(ip) // Legacy +export const isV6Format = (ip: string): boolean => V6_RE.test(ip) // Legacy +export const isV4 = (ip: string): boolean => V4_S_RE.test(ip) +export const isV6 = (ip: string): boolean => V6_S_RE.test(ip) + +// Corresponds Nodejs.NetworkInterfaceBase +export type Family = typeof IPV4 | typeof IPV6 +export function normalizeFamily(family?: string | number): Family { + const f = `${family}`.toLowerCase().trim() + return f === '6' || f === IPV6.toLowerCase() + ? IPV6 + : IPV4 +} + +export const normalizeAddress = (addr: string | number): string => { + const _a = (addr + '').toLowerCase() + + return _a.includes(':') + ? toString(toBuffer(_a)) + : fromLong(normalizeToLong(_a)) +} + +export const normalizeToLong = (addr: string): number => { + const parts = addr.split('.').map(part => { + if (/^0x[0-9a-f]+$/i.test(part)) + return parseInt(part, 16) // hex + + if (/^0[0-7]+$/.test(part)) + return parseInt(part, 8) // octal + + if (/^(0|[1-9]\d*)$/.test(part)) + return parseInt(part, 10) // decimal + + return NaN // invalid + }) + + if (parts.some(isNaN)) return -1 + + let val: number + switch (parts.length) { + case 1: + val = parts[0] + break + case 2: + if (parts[0] > 0xff || parts[1] > 0xffffff) return -1 + val = (parts[0] << 24) | (parts[1] & 0xffffff) + break + case 3: + if (parts[0] > 0xff || parts[1] > 0xff || parts[2] > 0xffff) return -1 + val = (parts[0] << 24) | (parts[1] << 16) | (parts[2] & 0xffff) + break + case 4: + if (parts.some(p => p > 0xff)) return -1 + val = (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3] + break + default: + return -1 + } + + return val >>> 0 // force unsigned +} + +// Loopbacks +const V4_LB = '127.0.0.1' +const V6_LB = 'fe80::1' + +export const isLoopback = (addr: string | number): boolean => { + const a = normalizeAddress(addr) + const s = a.slice(0, 5) + + return s === '::1' + || s === '::' + || s === '0177.' + || s === '0x7f.' + || a === V6_LB + || a === V4_LB + || a.startsWith('::ffff:7') + || /^(::f{4}:)?127\.(\d{1,3}(\.|$)){3}$/.test(a) +} + +export const loopback = (family?: string | number): typeof V4_LB | typeof V6_LB => { + family = normalizeFamily(family) + + if (family === IPV4) return V4_LB + if (family === IPV6) return V6_LB + + throw new Error('family must be ipv4 or ipv6') +} + +export const fromLong = (n: number): string => { + if (n < 0) throw new Error('invalid ipv4 long') + + return [ + (n >>> 24) & 0xff, + (n >>> 16) & 0xff, + (n >>> 8) & 0xff, + n & 0xff + ].join('.') +} + +export const toLong = (ip: string): number => + ip.split('.').reduce((acc, octet) => (acc << 8) + Number(octet), 0) >>> 0 + +export const toString = (buff: Buffer, offset = 0, length?: number): string => { + const o = ~~offset + const l = length || (buff.length - offset) + + // IPv4 + if (l === 4) + return [...buff.subarray(o, o + l)].join('.') + + // IPv6 + if (l === 16) + return Array + .from({ length: l / 2 }, (_, i) => + buff.readUInt16BE(o + i * 2).toString(16)) + .join(':') + .replace(/(^|:)0(:0)*:0(:|$)/, '$1::$3') + .replace(/:{3,4}/, '::') + + throw new Error('Invalid buffer length for IP address') +} + +export const toBuffer = (ip: string, buff?: Buffer, offset = 0): Buffer => { + offset = ~~offset + + if (isV4Format(ip)) { + const res = buff || Buffer.alloc(offset + 4) + for (const byte of ip.split('.')) + res[offset++] = +byte & 0xff + + return res + } + + if (isV6Format(ip)) { + let sections = ip.split(':', 8) + + // expand IPv4-in-IPv6 + for (let i = 0; i < sections.length; i++) { + if (isV4Format(sections[i])) { + const v4 = toBuffer(sections[i]) + sections[i] = v4.slice(0, 2).toString('hex') + if (++i < 8) sections.splice(i, 0, v4.slice(2, 4).toString('hex')) + } + } + + // expand :: + if (sections.includes('')) { + const emptyIndex = sections.indexOf('') + const pad = 8 - sections.length + 1 + sections.splice(emptyIndex, 1, ...Array(pad).fill('0')) + } else { + while (sections.length < 8) sections.push('0') + } + + // write result + const res = buff || Buffer.alloc(offset + 16) + for (const sec of sections) { + const word = parseInt(sec, 16) || 0 + res[offset++] = word >> 8 + res[offset++] = word & 0xff + } + return res + } + + throw Error(`Invalid ip address: ${ip}`) +} + +export const fromPrefixLen = (prefixlen: number, family?: string | number): string => { + family = prefixlen > 32 ? IPV6 : normalizeFamily(family) + const buff = Buffer.alloc(family === IPV6 ? 16 : 4) + + for (let i = 0; i < buff.length; i++) { + const bits = Math.min(prefixlen, 8) + prefixlen -= bits + buff[i] = ~(0xff >> bits) & 0xff + } + + return toString(buff) +} + +export const mask = (addr: string, maskStr: string): string => { + const a = toBuffer(addr) + const m = toBuffer(maskStr) + const out = Buffer.alloc(Math.max(a.length, m.length)) + + if (a.length === m.length) { + // Same protocol → direct AND + for (let i = 0; i < a.length; i++) out[i] = a[i] & m[i] + } else if (m.length === 4) { + // IPv6 addr with IPv4 mask → apply to low 32 bits + for (let i = 0; i < 4; i++) out[i] = a[a.length - 4 + i] & m[i] + } else { + // IPv4 addr with IPv6 mask → expand to ::ffff:ipv4 + out.fill(0, 0, 10) + out[10] = out[11] = 0xff + for (let i = 0; i < a.length; i++) out[i + 12] = a[i] & m[i + 12] + } + + return toString(out) +} + +type Subnet = { + networkAddress: string + firstAddress: string + lastAddress: string + broadcastAddress: string + subnetMask: string + subnetMaskLength: number + numHosts: number + length: number + contains(ip: string): boolean +} + +export const subnet = (addr: string, smask: string): Subnet => { + const networkAddress = toLong(mask(addr, smask)) + + // calculate prefix length + const maskBuf = toBuffer(smask) + let maskLen = 0 + for (const byte of maskBuf) { + if (byte === 0xff) { + maskLen += 8 + } else { + let b = byte + while (b) { + b = (b << 1) & 0xff + maskLen++ + } + } + } + + const numAddresses = 2 ** (32 - maskLen) + const numHosts = numAddresses <= 2 ? numAddresses : numAddresses - 2 + const firstAddress = numAddresses <= 2 ? networkAddress : networkAddress + 1 + const lastAddress = numAddresses <= 2 + ? networkAddress + numAddresses - 1 + : networkAddress + numAddresses - 2 + + return { + networkAddress: fromLong(networkAddress), + firstAddress: fromLong(firstAddress), + lastAddress: fromLong(lastAddress), + broadcastAddress: fromLong(networkAddress + numAddresses - 1), + subnetMask: smask, + subnetMaskLength: maskLen, + numHosts, + length: numAddresses, + contains(ip: string): boolean { + return networkAddress === toLong(mask(ip, smask)) + }, + } +} + +const parseCidr = (cidrString: string): [string, string] => { + const [addr, prefix] = cidrString.split('/') + if (!addr || prefix === undefined) + throw new Error(`invalid CIDR subnet: ${cidrString}`) + + const m = fromPrefixLen(parseInt(prefix, 10)) + return [addr, m] +} + +export const cidr = (cidrString: string): string => + mask(...parseCidr(cidrString)) + +export const cidrSubnet = (cidrString: string): Subnet => + subnet(...parseCidr(cidrString)) + +export const not = (addr: string): string => { + const buff = toBuffer(addr) + for (let i = 0; i < buff.length; i++) buff[i] ^= 0xff + return toString(buff) +} + +export const or = (a: string, b: string): string => { + let buffA = toBuffer(a) + let buffB = toBuffer(b) + + if (buffA.length === buffB.length) { + for (let i = 0; i < buffA.length; i++) buffA[i] |= buffB[i] + return toString(buffA) + } + + // mixed protocols: use longer buffer as base + if (buffB.length > buffA.length) [buffA, buffB] = [buffB, buffA] + + const offset = buffA.length - buffB.length + for (let i = 0; i < buffB.length; i++) buffA[offset + i] |= buffB[i] + + return toString(buffA) +} + +export const isEqual = (a: string, b: string): boolean => { + let ab = toBuffer(a) + let bb = toBuffer(b) + + // same protocol + if (ab.length === bb.length) { + for (let i = 0; i < ab.length; i++) { + if (ab[i] !== bb[i]) return false + } + return true + } + + // ensure ab is IPv4 and bb is IPv6 + if (bb.length === 4) [ab, bb] = [bb, ab] + + // first 10 bytes must be zero + for (let i = 0; i < 10; i++) if (bb[i] !== 0) return false + + // next 2 bytes must be either 0x0000 or 0xffff (::ffff:ipv4) + const prefix = bb.readUInt16BE(10) + if (prefix !== 0 && prefix !== 0xffff) return false + + // last 4 bytes must match IPv4 buffer + for (let i = 0; i < 4; i++) if (ab[i] !== bb[i + 12]) return false + + return true +} + +export const isPrivate = (addr: string): boolean => { + if (isLoopback(addr)) return true + + // private ranges + return ( + /^(::f{4}:)?10\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 10.0.0.0/8 + /^(::f{4}:)?192\.168\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 192.168.0.0/16 + /^(::f{4}:)?172\.(1[6-9]|2\d|3[01])\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 172.16.0.0 – 172.31.255.255 + /^(::f{4}:)?169\.254\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // link-local + /^f[cd][0-9a-f]{2}:/i.test(addr) || // unique local (fc00::/7) + /^fe80:/i.test(addr) || // link-local (fe80::/10) + addr === '::1' || // loopback (::1) + addr === '::' // unspecified (::) + ) +} + +export const isPublic = (addr: string): boolean => !isPrivate(addr) + +export const addresses = (name?: string, family?: string): string[] => { + const interfaces = os.networkInterfaces() + const fam = normalizeFamily(family) + const check = + name === PUBLIC ? isPublic + : name === PRIVATE ? isPrivate + : () => true + + // specific NIC requested + if (name && name !== PRIVATE && name !== PUBLIC) { + const nic = interfaces[name] + if (!nic) return [] + const match = nic.find(details => normalizeFamily(details.family) === fam) + return [match?.address!] + } + + // scan all NICs + const all = Object.values(interfaces).reduce((acc, nic) => { + for (const {family, address} of nic ?? []) { + if (normalizeFamily(family) !== fam) continue + if (isLoopback(address)) continue + if (check(address)) acc.push(address) + } + return acc + }, []) + + return all.length ? all : [loopback(fam)] +} + +export const address = (name?: string, family?: string): string | undefined => + addresses(name, family)[0] + +export const ip = { + address, + cidr, + cidrSubnet, + fromLong, + fromPrefixLen, + isEqual, + isLoopback, + isPrivate, + isPublic, + isV4Format, + isV6Format, + loopback, + mask, + not, + or, + subnet, + toBuffer, + toLong, + toString, +} export default ip diff --git a/src/test/smoke/ip.test.cjs b/src/test/smoke/ip.test.cjs index 2eedac0..c8a3ae4 100644 --- a/src/test/smoke/ip.test.cjs +++ b/src/test/smoke/ip.test.cjs @@ -3,7 +3,7 @@ const ip = require('@webpod/ip') // Smoke CJS test { - assert.equal(ip.foo, 'bar') + assert.ok(ip.isPrivate('127.0.0.1')) } console.log('smoke cjs: ok') diff --git a/src/test/smoke/ip.test.mjs b/src/test/smoke/ip.test.mjs index cdd13f1..c681cfa 100644 --- a/src/test/smoke/ip.test.mjs +++ b/src/test/smoke/ip.test.mjs @@ -3,7 +3,7 @@ import ip from '@webpod/ip' // Smoke ESM test { - assert.equal(ip.foo, 'bar') + assert.ok(ip.isPrivate('127.0.0.1')) } console.log('smoke ems: ok') diff --git a/src/test/ts/ip.test.ts b/src/test/ts/ip.test.ts index 14a4ab7..d97daba 100644 --- a/src/test/ts/ip.test.ts +++ b/src/test/ts/ip.test.ts @@ -1,9 +1,396 @@ -import * as assert from 'node:assert' -import {describe, test} from 'node:test' -import {foo} from '../../main/ts/ip.ts' +import os from 'node:os' +import net from 'node:net' +import {Buffer} from 'node:buffer' +import assert from 'node:assert' +import {test, describe} from 'node:test' + +import { + normalizeFamily, + IPV4, + IPV6, + V4_RE, + V6_RE, + V4_S_RE, + V6_S_RE, + isV4Format, + isV6Format, + isV4, + isV6, + isLoopback, + isEqual, + isPrivate, + isPublic, + fromLong, + fromPrefixLen, + toBuffer, + toString, + toLong, + mask, + subnet, + cidr, + cidrSubnet, + or, + not, + address, + addresses, +} from '../../main/ts/ip.ts' describe('ip', () => { - test('foo equals bar', () => { - assert.equal(foo, 'bar') + test('normalizeFamily() normalizes input to enum', () => { + const cases: [any, string][] = [ + [4, IPV4], + ['4', IPV4], + ['ipv4', IPV4], + ['iPV4', IPV4], + [6, IPV6], + ['6', IPV6], + ['ipv6', IPV6], + [undefined, IPV4], + [null, IPV4], + ['', IPV4], + ] + for (const [input, expected] of cases) { + const result = normalizeFamily(input) + assert.strictEqual(result, expected, `normalizeFamily(${input}) === ${expected}`) + } + }) + + // prettier-ignore + describe('ipv4/ipv6 checks', () => { + type Check = (ip: string) => boolean + const remap = new Map([ + [V4_RE, isV4Format], + [V6_RE, isV6Format], + [V4_S_RE, isV4], + [V6_S_RE, isV6] + ]) + + const cases: [string, ...Check[]][] = [ + [''], + ['10.0.0.1', isV4Format, isV6Format, isV4], + ['10.0.0.256', isV4Format, isV6Format], + ['10.00.0.255', isV4Format, isV6Format], + ['10.0.0.1111', isV6Format], + ['10.0.0'], + ['10.0.0.00', isV4Format, isV6Format], + ['10.0.0.0.0'], + ['::1', isV6Format, isV6], + ['2001:0db8:85a3:0000:0000:8a2e:0370:7334', isV6Format, isV6], + ] + + for (const [input, ...checks] of cases) { + const re = checks.map(c => [...remap.entries()].find(([,v]) => v === c)![0]) + const _re = [...remap.keys()].filter(r => !re.includes(r)) + const matches = checks.map(c => c.name).join(', ') || 'none' + + test(`${input} matches ${matches}`, () => { + for (const c of checks) assert.ok(c(input)) + for (const p of _re) assert.doesNotMatch(input, p) + for (const p of re) assert.match(input, p) + }) + } + }) + + test('isLoopback()', () => { + const cases: [string | number, boolean?][] = [ + ['127.0.0.1', true], + ['127.8.8.8', true], + ['fe80::1', true], + ['::1', true], + ['::', true], + ['128.0.0.1'], + ['8.8.8.8'], + [2130706434, true], + [4294967295], + ['0177.0.0.1', true], + ['0177.0.1', true], + ['0177.1', true], + ['0x7f.0.0.1', true], + ['0x7f.0.1', true], + ['0x7f.1', true], + ['2130706433', true], + ['192.168.1.1', false], + ] + + for (const [input, expected] of cases) { + assert.equal(isLoopback(input), !!expected, `isLoopback(${input}) === ${expected}`) + } + }) + + test('fromLong()', () => { + const cases: [number, string][] = [ + [2130706434, '127.0.0.2'], + [4294967295, '255.255.255.255'], + ] + + for (const [input, expected] of cases) { + assert.equal(fromLong(input), expected, `fromLong(${input}) === ${expected}`) + } + }) + + test('toLong()', () => { + const cases: [string, number][] = [ + ['127.0.0.1', 2130706433], + ['255.255.255.255', 4294967295] + ] + + for (const [input, expected] of cases) { + assert.equal(toLong(input), expected, `toLong(${input}) === ${expected}`) + } + }) + + test('toBuffer()/toString()', () => { + const u = undefined + const cases: [string, Buffer | undefined, number | undefined, number | undefined, string, string?][] = [ + ['127.0.0.1', u, u, u, '7f000001'], + ['::ffff:127.0.0.1', u, u, u, '00000000000000000000ffff7f000001', '::ffff:7f00:1'], + ['127.0.0.1', Buffer.alloc(128), 64, 4, '0'.repeat(128) + '7f000001' + '0'.repeat(120)], + ['::1', u, u, u, '00000000000000000000000000000001'], + ['1::', u, u, u, '00010000000000000000000000000000'], + ['abcd::dcba', u, u, u, 'abcd000000000000000000000000dcba'], + ['::1', Buffer.alloc(128), 64, 16, '0'.repeat(128 + 31) + '1' + '0'.repeat(128 - 32)], + ['abcd::dcba', Buffer.alloc(128), 64, 16, '0'.repeat(128) + 'abcd000000000000000000000000dcba' + '0'.repeat(128 - 32)], + ['::ffff:127.0.0.1', u, u, u, '00000000000000000000ffff7f000001', '::ffff:7f00:1'], + ['ffff::127.0.0.1', u, u, u, 'ffff000000000000000000007f000001', 'ffff::7f00:1'], + ['0:0:0:0:0:ffff:127.0.0.1', u, u, u, '00000000000000000000ffff7f000001', '::ffff:7f00:1'], + ] + for (const [input, b, o, l, h, s = input] of cases) { + const buf = toBuffer(input, b, o) + const str = toString(buf, o, l) + const hex = buf.toString('hex') + + assert.equal(hex, h, `toBuffer(${input}).toString('hex') === ${h}`) + assert.equal(str, s, `toString(toBuffer(${input})) === ${s}`) + } + }) + + test('fromPrefixLen()', () => { + const cases: [number, string, (string | number)?][] = [ + [24, '255.255.255.0'], + [64, 'ffff:ffff:ffff:ffff::'], + [24, 'ffff:ff00::', 'ipv6'], + ] + + for (const [input, expected, family] of cases) { + const res = fromPrefixLen(input, family) + assert.strictEqual(res, expected, `fromPrefixLen(${input}, ${family}) === ${expected}`) + } + }) + + test('mask()', () => { + const cases: [string, string, string][] = [ + ['192.168.1.134', '255.255.255.0', '192.168.1.0'], + ['192.168.1.134', '::ffff:ff00', '::ffff:c0a8:100'], + ['::1', '0.0.0.0', '::'] + ] + + for (const [a, m, expected] of cases) { + const res = mask(a, m) + assert.strictEqual(res, expected, `mask(${a}, ${m}) === ${expected}`) + } + }) + + test('subnet()', () => { + const cases: [string, string, Record, string[], string[]][] = [ + ['192.168.1.134', '255.255.255.192', { + networkAddress: '192.168.1.128', + firstAddress: '192.168.1.129', + lastAddress: '192.168.1.190', + broadcastAddress: '192.168.1.191', + subnetMask: '255.255.255.192', + subnetMaskLength: 26, + numHosts: 62, + length: 64, + }, ['192.168.1.180'], ['192.168.1.192']], + + ['192.168.1.134', '255.255.255.255', { + firstAddress: '192.168.1.134', + lastAddress: '192.168.1.134', + numHosts: 1 + }, ['192.168.1.134'], []], + ['192.168.1.134', '255.255.255.254', { + firstAddress: '192.168.1.134', + lastAddress: '192.168.1.135', + numHosts: 2 + }, [], []] + ] + for (const [addr, smask, expected, inside, out] of cases) { + const res = subnet(addr, smask) + for (const k of Object.keys(expected)) + assert.strictEqual(res[k as keyof typeof res], expected[k], `subnet(${addr}, ${smask}).${k} === ${expected[k]}`) + + assert.ok(inside.every(a => res.contains(a)), `subnet(${addr}, ${smask}) contains ${inside.join(', ')}`) + assert.ok(out.every(a => !res.contains(a)), `subnet(${addr}, ${smask}) does not contain ${out.join(', ')}`) + } + }) + + + test('cidr()', () => { + const cases: [string, string][] = [ + ['192.168.1.134/26', '192.168.1.128'], + ['2607:f0d0:1002:51::4/56', '2607:f0d0:1002::'] + ] + + for (const [input, expected] of cases) { + const res = cidr(input) + assert.strictEqual(res, expected, `cidr(${input}) === ${expected}`) + } + + assert.throws(() => cidr(''), /Error: invalid CIDR subnet/) + }) + + test('cidrSubnet()', () => { + const cases: [string, Record][] = [ + ['192.168.1.134/26', { + networkAddress: '192.168.1.128', + firstAddress: '192.168.1.129', + lastAddress: '192.168.1.190', + broadcastAddress: '192.168.1.191', + subnetMask: '255.255.255.192', + subnetMaskLength: 26, + numHosts: 62, + length: 64, + }], + ] + + for (const [input, expected] of cases) { + const res = cidrSubnet(input) + for (const k of Object.keys(expected)) + assert.strictEqual(res[k as keyof typeof res], expected[k], `cidrSubnet(${input}).${k} === ${expected[k]}`) + } + + assert.throws(() => cidrSubnet(''), /Error: invalid CIDR subnet/) + }) + + test('or()', () => { + const cases : [string, string, string][] = [ + ['0.0.0.255', '192.168.1.10', '192.168.1.255'], + ['::ff', '::1', '::ff'], + ['::ff', '::abcd:dcba:abcd:dcba', '::abcd:dcba:abcd:dcff'], + ['0.0.0.255', '::abcd:dcba:abcd:dcba', '::abcd:dcba:abcd:dcff'], + ] + + for (const [a, b, expected] of cases) + assert.strictEqual(or(a, b), expected, `or(${a}, ${b}) === ${expected}`) + }) + + test('not()', () => { + const cases: [string, string][] = [ + ['255.255.255.0', '0.0.0.255'], + ['255.0.0.0', '0.255.255.255'], + ['1.2.3.4', '254.253.252.251'], + ['::', 'ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff'], + ['::ffff:ffff', 'ffff:ffff:ffff:ffff:ffff:ffff::'], + ['::abcd:dcba:abcd:dcba', 'ffff:ffff:ffff:ffff:5432:2345:5432:2345'] + ] + + for (const [a, expected] of cases) + assert.strictEqual(not(a), expected, `not(${a}) === ${expected}`) + }) + + test('isEqual()', () => { + const cases: [string, string, boolean][] = [ + ['127.0.0.1', '::7f00:1', true], + ['127.0.0.1', '::7f00:2', false], + ['127.0.0.1', '::ffff:7f00:1', true], + ['127.0.0.1', '::ffaf:7f00:1', false], + ['::ffff:127.0.0.1', '::ffff:127.0.0.1', true], + ['::ffff:127.0.0.1', '127.0.0.1', true], + ] + + for (const [a, b, expected] of cases) + assert.equal(isEqual(a, b), expected, `isEqual(${a}, ${b}) === ${expected}`) + }) + + test('isPrivate()/isPublic()', () => { + const cases: [string, boolean?][] = [ + ['127.0.0.1', true], + ['127.0.0.2', true], + ['127.1.1.1', true], + + ['192.168.0.123', true], + ['192.168.122.123', true], + ['192.162.1.2'], + + ['172.16.0.5', true], + ['172.16.123.254', true], + ['171.16.0.5'], + ['172.25.232.15', true], + ['172.15.0.5'], + ['172.32.0.5'], + + ['169.254.2.3', true], + ['169.254.221.9', true], + ['168.254.2.3'], + + ['10.0.2.3', true], + ['10.1.23.45', true], + ['12.1.2.3'], + + ['fd12:3456:789a:1::1', true], + ['fe80::f2de:f1ff:fe3f:307e', true], + ['::ffff:10.100.1.42', true], + ['::FFFF:172.16.200.1', true], + ['::ffff:192.168.0.1', true], + + ['165.225.132.33'], + + ['::', true], + ['::1', true], + ['fe80::1', true], + + // CVE-2023-42282 + ['0x7f.1', true], + + // CVE-2024-29415 + ['127.1', true], + ['2130706433', true], + ['01200034567', false], + ['012.1.2.3', false], + ['000:0:0000::01', true], + ['::fFFf:127.0.0.1', true], + ['::fFFf:127.255.255.256', true] + ] + + for (const [input, expected] of cases) { + assert.equal(isPrivate(input), !!expected, `isPrivate(${input}) === ${!!expected}`) + assert.equal(isPublic(input), !expected, `isPublic(${input}) === ${!expected}`) + } + }) + + describe('address()', () => { + test('private', () => { + const cases = [undefined, 'ipv4', 'ipv6'] + + for (const family of cases) { + const addr = address('private', family)! + assert.ok(isPrivate(addr), `address('private', ${family}) === ${addr}`) + } + }) + + describe('net ifaces', () => { + const interfaces = os.networkInterfaces() + const cases: [string | undefined, (addr: string) => boolean][] = [ + [undefined, net.isIPv4], + ['ipv4', net.isIPv4], + ['ipv6', net.isIPv6], + ] + + Object.keys(interfaces).forEach((nic) => { + for (const [family, check] of cases) { + test(`${nic} ${family}`, () => { + const addr = address(nic, family) + assert.ok(!addr || check(addr), `address(${nic}, ${family}) === ${addr}`) + }) + } + }) + }) + + test('`addresses()` method returns all ipv4 by default', () => { + const all = addresses() + const v4 = addresses(undefined, 'ipv4') + + assert.deepEqual(all, v4) + }) }) }) diff --git a/target/cjs/cjslib.cjs b/target/cjs/cjslib.cjs index c625f7d..0f84557 100644 --- a/target/cjs/cjslib.cjs +++ b/target/cjs/cjslib.cjs @@ -23,6 +23,21 @@ var __copyProps = (to, from, except, desc) => { var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); +var __create = Object.create; + +var __getProtoOf = Object.getPrototypeOf; + +var __pow = Math.pow; + +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); + module.exports = { __defProp, __getOwnPropDesc, @@ -30,5 +45,9 @@ module.exports = { __hasOwnProp, __export, __copyProps, - __toCommonJS + __toCommonJS, + __create, + __getProtoOf, + __pow, + __toESM }; diff --git a/target/cjs/ip.cjs b/target/cjs/ip.cjs index 924cdac..8af5e44 100644 --- a/target/cjs/ip.cjs +++ b/target/cjs/ip.cjs @@ -1,6 +1,8 @@ "use strict"; const { + __pow, __export, + __toESM, __toCommonJS } = require('./cjslib.cjs'); @@ -8,16 +10,349 @@ const { // src/main/ts/ip.ts var ip_exports = {}; __export(ip_exports, { + IPV4: () => IPV4, + IPV6: () => IPV6, + V4_RE: () => V4_RE, + V4_S_RE: () => V4_S_RE, + V6_RE: () => V6_RE, + V6_S_RE: () => V6_S_RE, + address: () => address, + addresses: () => addresses, + cidr: () => cidr, + cidrSubnet: () => cidrSubnet, default: () => ip_default, - foo: () => foo, - ip: () => ip + fromLong: () => fromLong, + fromPrefixLen: () => fromPrefixLen, + ip: () => ip, + isEqual: () => isEqual, + isLoopback: () => isLoopback, + isPrivate: () => isPrivate, + isPublic: () => isPublic, + isV4: () => isV4, + isV4Format: () => isV4Format, + isV6: () => isV6, + isV6Format: () => isV6Format, + loopback: () => loopback, + mask: () => mask, + normalizeAddress: () => normalizeAddress, + normalizeFamily: () => normalizeFamily, + normalizeToLong: () => normalizeToLong, + not: () => not, + or: () => or, + subnet: () => subnet, + toBuffer: () => toBuffer, + toLong: () => toLong, + toString: () => toString }); module.exports = __toCommonJS(ip_exports); -var foo = "bar"; -var ip = { foo }; +var import_buffer = require("buffer"); +var import_os = __toESM(require("os"), 1); +var PUBLIC = "public"; +var PRIVATE = "private"; +var IPV4 = "IPv4"; +var IPV6 = "IPv6"; +var V4_RE = /^(\d{1,3}(\.|$)){4}$/; +var V6_RE = /^(?=.+)(::)?(((\d{1,3}\.){3}\d{1,3})?|([0-9a-f]{0,4}:{0,2})){1,8}(::)?$/i; +var V4_S_RE = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/; +var V6_S_RE = /(([\dA-Fa-f]{1,4}:){7}[\dA-Fa-f]{1,4}|([\dA-Fa-f]{1,4}:){1,7}:|([\dA-Fa-f]{1,4}:){1,6}:[\dA-Fa-f]{1,4}|([\dA-Fa-f]{1,4}:){1,5}(:[\dA-Fa-f]{1,4}){1,2}|([\dA-Fa-f]{1,4}:){1,4}(:[\dA-Fa-f]{1,4}){1,3}|([\dA-Fa-f]{1,4}:){1,3}(:[\dA-Fa-f]{1,4}){1,4}|([\dA-Fa-f]{1,4}:){1,2}(:[\dA-Fa-f]{1,4}){1,5}|[\dA-Fa-f]{1,4}:((:[\dA-Fa-f]{1,4}){1,6})|:((:[\dA-Fa-f]{1,4}){1,7}|:)|fe80:(:[\dA-Fa-f]{0,4}){0,4}%[\dA-Za-z]+|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)|([\dA-Fa-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d))$/; +var isV4Format = (ip2) => V4_RE.test(ip2); +var isV6Format = (ip2) => V6_RE.test(ip2); +var isV4 = (ip2) => V4_S_RE.test(ip2); +var isV6 = (ip2) => V6_S_RE.test(ip2); +function normalizeFamily(family) { + const f = `${family}`.toLowerCase().trim(); + return f === "6" || f === IPV6.toLowerCase() ? IPV6 : IPV4; +} +var normalizeAddress = (addr) => { + const _a = (addr + "").toLowerCase(); + return _a.includes(":") ? toString(toBuffer(_a)) : fromLong(normalizeToLong(_a)); +}; +var normalizeToLong = (addr) => { + const parts = addr.split(".").map((part) => { + if (/^0x[0-9a-f]+$/i.test(part)) + return parseInt(part, 16); + if (/^0[0-7]+$/.test(part)) + return parseInt(part, 8); + if (/^(0|[1-9]\d*)$/.test(part)) + return parseInt(part, 10); + return NaN; + }); + if (parts.some(isNaN)) return -1; + let val; + switch (parts.length) { + case 1: + val = parts[0]; + break; + case 2: + if (parts[0] > 255 || parts[1] > 16777215) return -1; + val = parts[0] << 24 | parts[1] & 16777215; + break; + case 3: + if (parts[0] > 255 || parts[1] > 255 || parts[2] > 65535) return -1; + val = parts[0] << 24 | parts[1] << 16 | parts[2] & 65535; + break; + case 4: + if (parts.some((p) => p > 255)) return -1; + val = parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; + break; + default: + return -1; + } + return val >>> 0; +}; +var V4_LB = "127.0.0.1"; +var V6_LB = "fe80::1"; +var isLoopback = (addr) => { + const a = normalizeAddress(addr); + const s = a.slice(0, 5); + return s === "::1" || s === "::" || s === "0177." || s === "0x7f." || a === V6_LB || a === V4_LB || a.startsWith("::ffff:7") || /^(::f{4}:)?127\.(\d{1,3}(\.|$)){3}$/.test(a); +}; +var loopback = (family) => { + family = normalizeFamily(family); + if (family === IPV4) return V4_LB; + if (family === IPV6) return V6_LB; + throw new Error("family must be ipv4 or ipv6"); +}; +var fromLong = (n) => { + if (n < 0) throw new Error("invalid ipv4 long"); + return [ + n >>> 24 & 255, + n >>> 16 & 255, + n >>> 8 & 255, + n & 255 + ].join("."); +}; +var toLong = (ip2) => ip2.split(".").reduce((acc, octet) => (acc << 8) + Number(octet), 0) >>> 0; +var toString = (buff, offset = 0, length) => { + const o = ~~offset; + const l = length || buff.length - offset; + if (l === 4) + return [...buff.subarray(o, o + l)].join("."); + if (l === 16) + return Array.from({ length: l / 2 }, (_, i) => buff.readUInt16BE(o + i * 2).toString(16)).join(":").replace(/(^|:)0(:0)*:0(:|$)/, "$1::$3").replace(/:{3,4}/, "::"); + throw new Error("Invalid buffer length for IP address"); +}; +var toBuffer = (ip2, buff, offset = 0) => { + offset = ~~offset; + if (isV4Format(ip2)) { + const res = buff || import_buffer.Buffer.alloc(offset + 4); + for (const byte of ip2.split(".")) + res[offset++] = +byte & 255; + return res; + } + if (isV6Format(ip2)) { + let sections = ip2.split(":", 8); + for (let i = 0; i < sections.length; i++) { + if (isV4Format(sections[i])) { + const v4 = toBuffer(sections[i]); + sections[i] = v4.slice(0, 2).toString("hex"); + if (++i < 8) sections.splice(i, 0, v4.slice(2, 4).toString("hex")); + } + } + if (sections.includes("")) { + const emptyIndex = sections.indexOf(""); + const pad = 8 - sections.length + 1; + sections.splice(emptyIndex, 1, ...Array(pad).fill("0")); + } else { + while (sections.length < 8) sections.push("0"); + } + const res = buff || import_buffer.Buffer.alloc(offset + 16); + for (const sec of sections) { + const word = parseInt(sec, 16) || 0; + res[offset++] = word >> 8; + res[offset++] = word & 255; + } + return res; + } + throw Error(`Invalid ip address: ${ip2}`); +}; +var fromPrefixLen = (prefixlen, family) => { + family = prefixlen > 32 ? IPV6 : normalizeFamily(family); + const buff = import_buffer.Buffer.alloc(family === IPV6 ? 16 : 4); + for (let i = 0; i < buff.length; i++) { + const bits = Math.min(prefixlen, 8); + prefixlen -= bits; + buff[i] = ~(255 >> bits) & 255; + } + return toString(buff); +}; +var mask = (addr, maskStr) => { + const a = toBuffer(addr); + const m = toBuffer(maskStr); + const out = import_buffer.Buffer.alloc(Math.max(a.length, m.length)); + if (a.length === m.length) { + for (let i = 0; i < a.length; i++) out[i] = a[i] & m[i]; + } else if (m.length === 4) { + for (let i = 0; i < 4; i++) out[i] = a[a.length - 4 + i] & m[i]; + } else { + out.fill(0, 0, 10); + out[10] = out[11] = 255; + for (let i = 0; i < a.length; i++) out[i + 12] = a[i] & m[i + 12]; + } + return toString(out); +}; +var subnet = (addr, smask) => { + const networkAddress = toLong(mask(addr, smask)); + const maskBuf = toBuffer(smask); + let maskLen = 0; + for (const byte of maskBuf) { + if (byte === 255) { + maskLen += 8; + } else { + let b = byte; + while (b) { + b = b << 1 & 255; + maskLen++; + } + } + } + const numAddresses = __pow(2, 32 - maskLen); + const numHosts = numAddresses <= 2 ? numAddresses : numAddresses - 2; + const firstAddress = numAddresses <= 2 ? networkAddress : networkAddress + 1; + const lastAddress = numAddresses <= 2 ? networkAddress + numAddresses - 1 : networkAddress + numAddresses - 2; + return { + networkAddress: fromLong(networkAddress), + firstAddress: fromLong(firstAddress), + lastAddress: fromLong(lastAddress), + broadcastAddress: fromLong(networkAddress + numAddresses - 1), + subnetMask: smask, + subnetMaskLength: maskLen, + numHosts, + length: numAddresses, + contains(ip2) { + return networkAddress === toLong(mask(ip2, smask)); + } + }; +}; +var parseCidr = (cidrString) => { + const [addr, prefix] = cidrString.split("/"); + if (!addr || prefix === void 0) + throw new Error(`invalid CIDR subnet: ${cidrString}`); + const m = fromPrefixLen(parseInt(prefix, 10)); + return [addr, m]; +}; +var cidr = (cidrString) => mask(...parseCidr(cidrString)); +var cidrSubnet = (cidrString) => subnet(...parseCidr(cidrString)); +var not = (addr) => { + const buff = toBuffer(addr); + for (let i = 0; i < buff.length; i++) buff[i] ^= 255; + return toString(buff); +}; +var or = (a, b) => { + let buffA = toBuffer(a); + let buffB = toBuffer(b); + if (buffA.length === buffB.length) { + for (let i = 0; i < buffA.length; i++) buffA[i] |= buffB[i]; + return toString(buffA); + } + if (buffB.length > buffA.length) [buffA, buffB] = [buffB, buffA]; + const offset = buffA.length - buffB.length; + for (let i = 0; i < buffB.length; i++) buffA[offset + i] |= buffB[i]; + return toString(buffA); +}; +var isEqual = (a, b) => { + let ab = toBuffer(a); + let bb = toBuffer(b); + if (ab.length === bb.length) { + for (let i = 0; i < ab.length; i++) { + if (ab[i] !== bb[i]) return false; + } + return true; + } + if (bb.length === 4) [ab, bb] = [bb, ab]; + for (let i = 0; i < 10; i++) if (bb[i] !== 0) return false; + const prefix = bb.readUInt16BE(10); + if (prefix !== 0 && prefix !== 65535) return false; + for (let i = 0; i < 4; i++) if (ab[i] !== bb[i + 12]) return false; + return true; +}; +var isPrivate = (addr) => { + if (isLoopback(addr)) return true; + return /^(::f{4}:)?10\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 10.0.0.0/8 + /^(::f{4}:)?192\.168\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 192.168.0.0/16 + /^(::f{4}:)?172\.(1[6-9]|2\d|3[01])\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 172.16.0.0 – 172.31.255.255 + /^(::f{4}:)?169\.254\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // link-local + /^f[cd][0-9a-f]{2}:/i.test(addr) || // unique local (fc00::/7) + /^fe80:/i.test(addr) || // link-local (fe80::/10) + addr === "::1" || // loopback (::1) + addr === "::"; +}; +var isPublic = (addr) => !isPrivate(addr); +var addresses = (name, family) => { + const interfaces = import_os.default.networkInterfaces(); + const fam = normalizeFamily(family); + const check = name === PUBLIC ? isPublic : name === PRIVATE ? isPrivate : () => true; + if (name && name !== PRIVATE && name !== PUBLIC) { + const nic = interfaces[name]; + if (!nic) return []; + const match = nic.find((details) => normalizeFamily(details.family) === fam); + return [match == null ? void 0 : match.address]; + } + const all = Object.values(interfaces).reduce((acc, nic) => { + for (const { family: family2, address: address2 } of nic != null ? nic : []) { + if (normalizeFamily(family2) !== fam) continue; + if (isLoopback(address2)) continue; + if (check(address2)) acc.push(address2); + } + return acc; + }, []); + return all.length ? all : [loopback(fam)]; +}; +var address = (name, family) => addresses(name, family)[0]; +var ip = { + address, + cidr, + cidrSubnet, + fromLong, + fromPrefixLen, + isEqual, + isLoopback, + isPrivate, + isPublic, + isV4Format, + isV6Format, + loopback, + mask, + not, + or, + subnet, + toBuffer, + toLong, + toString +}; var ip_default = ip; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { - foo, - ip + IPV4, + IPV6, + V4_RE, + V4_S_RE, + V6_RE, + V6_S_RE, + address, + addresses, + cidr, + cidrSubnet, + fromLong, + fromPrefixLen, + ip, + isEqual, + isLoopback, + isPrivate, + isPublic, + isV4, + isV4Format, + isV6, + isV6Format, + loopback, + mask, + normalizeAddress, + normalizeFamily, + normalizeToLong, + not, + or, + subnet, + toBuffer, + toLong, + toString }); \ No newline at end of file diff --git a/target/dts/ip.d.ts b/target/dts/ip.d.ts index 444eab9..9b87027 100644 --- a/target/dts/ip.d.ts +++ b/target/dts/ip.d.ts @@ -1,5 +1,68 @@ -export declare const foo = "bar"; +import { Buffer } from 'buffer'; +export declare const IPV4 = "IPv4"; +export declare const IPV6 = "IPv6"; +export declare const V4_RE: RegExp; +export declare const V6_RE: RegExp; +export declare const V4_S_RE: RegExp; +export declare const V6_S_RE: RegExp; +export declare const isV4Format: (ip: string) => boolean; +export declare const isV6Format: (ip: string) => boolean; +export declare const isV4: (ip: string) => boolean; +export declare const isV6: (ip: string) => boolean; +export type Family = typeof IPV4 | typeof IPV6; +export declare function normalizeFamily(family?: string | number): Family; +export declare const normalizeAddress: (addr: string | number) => string; +export declare const normalizeToLong: (addr: string) => number; +declare const V4_LB = "127.0.0.1"; +declare const V6_LB = "fe80::1"; +export declare const isLoopback: (addr: string | number) => boolean; +export declare const loopback: (family?: string | number) => typeof V4_LB | typeof V6_LB; +export declare const fromLong: (n: number) => string; +export declare const toLong: (ip: string) => number; +export declare const toString: (buff: Buffer, offset?: number, length?: number) => string; +export declare const toBuffer: (ip: string, buff?: Buffer, offset?: number) => Buffer; +export declare const fromPrefixLen: (prefixlen: number, family?: string | number) => string; +export declare const mask: (addr: string, maskStr: string) => string; +type Subnet = { + networkAddress: string; + firstAddress: string; + lastAddress: string; + broadcastAddress: string; + subnetMask: string; + subnetMaskLength: number; + numHosts: number; + length: number; + contains(ip: string): boolean; +}; +export declare const subnet: (addr: string, smask: string) => Subnet; +export declare const cidr: (cidrString: string) => string; +export declare const cidrSubnet: (cidrString: string) => Subnet; +export declare const not: (addr: string) => string; +export declare const or: (a: string, b: string) => string; +export declare const isEqual: (a: string, b: string) => boolean; +export declare const isPrivate: (addr: string) => boolean; +export declare const isPublic: (addr: string) => boolean; +export declare const addresses: (name?: string, family?: string) => string[]; +export declare const address: (name?: string, family?: string) => string | undefined; export declare const ip: { - foo: string; + address: (name?: string, family?: string) => string | undefined; + cidr: (cidrString: string) => string; + cidrSubnet: (cidrString: string) => Subnet; + fromLong: (n: number) => string; + fromPrefixLen: (prefixlen: number, family?: string | number) => string; + isEqual: (a: string, b: string) => boolean; + isLoopback: (addr: string | number) => boolean; + isPrivate: (addr: string) => boolean; + isPublic: (addr: string) => boolean; + isV4Format: (ip: string) => boolean; + isV6Format: (ip: string) => boolean; + loopback: (family?: string | number) => typeof V4_LB | typeof V6_LB; + mask: (addr: string, maskStr: string) => string; + not: (addr: string) => string; + or: (a: string, b: string) => string; + subnet: (addr: string, smask: string) => Subnet; + toBuffer: (ip: string, buff?: Buffer, offset?: number) => Buffer; + toLong: (ip: string) => number; + toString: (buff: Buffer, offset?: number, length?: number) => string; }; export default ip; diff --git a/target/esm/ip.mjs b/target/esm/ip.mjs index b04ba96..ed7bcbd 100644 --- a/target/esm/ip.mjs +++ b/target/esm/ip.mjs @@ -1,9 +1,312 @@ // src/main/ts/ip.ts -var foo = "bar"; -var ip = { foo }; +import { Buffer } from "buffer"; +import os from "os"; +var PUBLIC = "public"; +var PRIVATE = "private"; +var IPV4 = "IPv4"; +var IPV6 = "IPv6"; +var V4_RE = /^(\d{1,3}(\.|$)){4}$/; +var V6_RE = /^(?=.+)(::)?(((\d{1,3}\.){3}\d{1,3})?|([0-9a-f]{0,4}:{0,2})){1,8}(::)?$/i; +var V4_S_RE = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/; +var V6_S_RE = /(([\dA-Fa-f]{1,4}:){7}[\dA-Fa-f]{1,4}|([\dA-Fa-f]{1,4}:){1,7}:|([\dA-Fa-f]{1,4}:){1,6}:[\dA-Fa-f]{1,4}|([\dA-Fa-f]{1,4}:){1,5}(:[\dA-Fa-f]{1,4}){1,2}|([\dA-Fa-f]{1,4}:){1,4}(:[\dA-Fa-f]{1,4}){1,3}|([\dA-Fa-f]{1,4}:){1,3}(:[\dA-Fa-f]{1,4}){1,4}|([\dA-Fa-f]{1,4}:){1,2}(:[\dA-Fa-f]{1,4}){1,5}|[\dA-Fa-f]{1,4}:((:[\dA-Fa-f]{1,4}){1,6})|:((:[\dA-Fa-f]{1,4}){1,7}|:)|fe80:(:[\dA-Fa-f]{0,4}){0,4}%[\dA-Za-z]+|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)|([\dA-Fa-f]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d))$/; +var isV4Format = (ip2) => V4_RE.test(ip2); +var isV6Format = (ip2) => V6_RE.test(ip2); +var isV4 = (ip2) => V4_S_RE.test(ip2); +var isV6 = (ip2) => V6_S_RE.test(ip2); +function normalizeFamily(family) { + const f = `${family}`.toLowerCase().trim(); + return f === "6" || f === IPV6.toLowerCase() ? IPV6 : IPV4; +} +var normalizeAddress = (addr) => { + const _a = (addr + "").toLowerCase(); + return _a.includes(":") ? toString(toBuffer(_a)) : fromLong(normalizeToLong(_a)); +}; +var normalizeToLong = (addr) => { + const parts = addr.split(".").map((part) => { + if (/^0x[0-9a-f]+$/i.test(part)) + return parseInt(part, 16); + if (/^0[0-7]+$/.test(part)) + return parseInt(part, 8); + if (/^(0|[1-9]\d*)$/.test(part)) + return parseInt(part, 10); + return NaN; + }); + if (parts.some(isNaN)) return -1; + let val; + switch (parts.length) { + case 1: + val = parts[0]; + break; + case 2: + if (parts[0] > 255 || parts[1] > 16777215) return -1; + val = parts[0] << 24 | parts[1] & 16777215; + break; + case 3: + if (parts[0] > 255 || parts[1] > 255 || parts[2] > 65535) return -1; + val = parts[0] << 24 | parts[1] << 16 | parts[2] & 65535; + break; + case 4: + if (parts.some((p) => p > 255)) return -1; + val = parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]; + break; + default: + return -1; + } + return val >>> 0; +}; +var V4_LB = "127.0.0.1"; +var V6_LB = "fe80::1"; +var isLoopback = (addr) => { + const a = normalizeAddress(addr); + const s = a.slice(0, 5); + return s === "::1" || s === "::" || s === "0177." || s === "0x7f." || a === V6_LB || a === V4_LB || a.startsWith("::ffff:7") || /^(::f{4}:)?127\.(\d{1,3}(\.|$)){3}$/.test(a); +}; +var loopback = (family) => { + family = normalizeFamily(family); + if (family === IPV4) return V4_LB; + if (family === IPV6) return V6_LB; + throw new Error("family must be ipv4 or ipv6"); +}; +var fromLong = (n) => { + if (n < 0) throw new Error("invalid ipv4 long"); + return [ + n >>> 24 & 255, + n >>> 16 & 255, + n >>> 8 & 255, + n & 255 + ].join("."); +}; +var toLong = (ip2) => ip2.split(".").reduce((acc, octet) => (acc << 8) + Number(octet), 0) >>> 0; +var toString = (buff, offset = 0, length) => { + const o = ~~offset; + const l = length || buff.length - offset; + if (l === 4) + return [...buff.subarray(o, o + l)].join("."); + if (l === 16) + return Array.from({ length: l / 2 }, (_, i) => buff.readUInt16BE(o + i * 2).toString(16)).join(":").replace(/(^|:)0(:0)*:0(:|$)/, "$1::$3").replace(/:{3,4}/, "::"); + throw new Error("Invalid buffer length for IP address"); +}; +var toBuffer = (ip2, buff, offset = 0) => { + offset = ~~offset; + if (isV4Format(ip2)) { + const res = buff || Buffer.alloc(offset + 4); + for (const byte of ip2.split(".")) + res[offset++] = +byte & 255; + return res; + } + if (isV6Format(ip2)) { + let sections = ip2.split(":", 8); + for (let i = 0; i < sections.length; i++) { + if (isV4Format(sections[i])) { + const v4 = toBuffer(sections[i]); + sections[i] = v4.slice(0, 2).toString("hex"); + if (++i < 8) sections.splice(i, 0, v4.slice(2, 4).toString("hex")); + } + } + if (sections.includes("")) { + const emptyIndex = sections.indexOf(""); + const pad = 8 - sections.length + 1; + sections.splice(emptyIndex, 1, ...Array(pad).fill("0")); + } else { + while (sections.length < 8) sections.push("0"); + } + const res = buff || Buffer.alloc(offset + 16); + for (const sec of sections) { + const word = parseInt(sec, 16) || 0; + res[offset++] = word >> 8; + res[offset++] = word & 255; + } + return res; + } + throw Error(`Invalid ip address: ${ip2}`); +}; +var fromPrefixLen = (prefixlen, family) => { + family = prefixlen > 32 ? IPV6 : normalizeFamily(family); + const buff = Buffer.alloc(family === IPV6 ? 16 : 4); + for (let i = 0; i < buff.length; i++) { + const bits = Math.min(prefixlen, 8); + prefixlen -= bits; + buff[i] = ~(255 >> bits) & 255; + } + return toString(buff); +}; +var mask = (addr, maskStr) => { + const a = toBuffer(addr); + const m = toBuffer(maskStr); + const out = Buffer.alloc(Math.max(a.length, m.length)); + if (a.length === m.length) { + for (let i = 0; i < a.length; i++) out[i] = a[i] & m[i]; + } else if (m.length === 4) { + for (let i = 0; i < 4; i++) out[i] = a[a.length - 4 + i] & m[i]; + } else { + out.fill(0, 0, 10); + out[10] = out[11] = 255; + for (let i = 0; i < a.length; i++) out[i + 12] = a[i] & m[i + 12]; + } + return toString(out); +}; +var subnet = (addr, smask) => { + const networkAddress = toLong(mask(addr, smask)); + const maskBuf = toBuffer(smask); + let maskLen = 0; + for (const byte of maskBuf) { + if (byte === 255) { + maskLen += 8; + } else { + let b = byte; + while (b) { + b = b << 1 & 255; + maskLen++; + } + } + } + const numAddresses = 2 ** (32 - maskLen); + const numHosts = numAddresses <= 2 ? numAddresses : numAddresses - 2; + const firstAddress = numAddresses <= 2 ? networkAddress : networkAddress + 1; + const lastAddress = numAddresses <= 2 ? networkAddress + numAddresses - 1 : networkAddress + numAddresses - 2; + return { + networkAddress: fromLong(networkAddress), + firstAddress: fromLong(firstAddress), + lastAddress: fromLong(lastAddress), + broadcastAddress: fromLong(networkAddress + numAddresses - 1), + subnetMask: smask, + subnetMaskLength: maskLen, + numHosts, + length: numAddresses, + contains(ip2) { + return networkAddress === toLong(mask(ip2, smask)); + } + }; +}; +var parseCidr = (cidrString) => { + const [addr, prefix] = cidrString.split("/"); + if (!addr || prefix === void 0) + throw new Error(`invalid CIDR subnet: ${cidrString}`); + const m = fromPrefixLen(parseInt(prefix, 10)); + return [addr, m]; +}; +var cidr = (cidrString) => mask(...parseCidr(cidrString)); +var cidrSubnet = (cidrString) => subnet(...parseCidr(cidrString)); +var not = (addr) => { + const buff = toBuffer(addr); + for (let i = 0; i < buff.length; i++) buff[i] ^= 255; + return toString(buff); +}; +var or = (a, b) => { + let buffA = toBuffer(a); + let buffB = toBuffer(b); + if (buffA.length === buffB.length) { + for (let i = 0; i < buffA.length; i++) buffA[i] |= buffB[i]; + return toString(buffA); + } + if (buffB.length > buffA.length) [buffA, buffB] = [buffB, buffA]; + const offset = buffA.length - buffB.length; + for (let i = 0; i < buffB.length; i++) buffA[offset + i] |= buffB[i]; + return toString(buffA); +}; +var isEqual = (a, b) => { + let ab = toBuffer(a); + let bb = toBuffer(b); + if (ab.length === bb.length) { + for (let i = 0; i < ab.length; i++) { + if (ab[i] !== bb[i]) return false; + } + return true; + } + if (bb.length === 4) [ab, bb] = [bb, ab]; + for (let i = 0; i < 10; i++) if (bb[i] !== 0) return false; + const prefix = bb.readUInt16BE(10); + if (prefix !== 0 && prefix !== 65535) return false; + for (let i = 0; i < 4; i++) if (ab[i] !== bb[i + 12]) return false; + return true; +}; +var isPrivate = (addr) => { + if (isLoopback(addr)) return true; + return /^(::f{4}:)?10\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 10.0.0.0/8 + /^(::f{4}:)?192\.168\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 192.168.0.0/16 + /^(::f{4}:)?172\.(1[6-9]|2\d|3[01])\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // 172.16.0.0 – 172.31.255.255 + /^(::f{4}:)?169\.254\.(\d{1,3})\.(\d{1,3})$/i.test(addr) || // link-local + /^f[cd][0-9a-f]{2}:/i.test(addr) || // unique local (fc00::/7) + /^fe80:/i.test(addr) || // link-local (fe80::/10) + addr === "::1" || // loopback (::1) + addr === "::"; +}; +var isPublic = (addr) => !isPrivate(addr); +var addresses = (name, family) => { + const interfaces = os.networkInterfaces(); + const fam = normalizeFamily(family); + const check = name === PUBLIC ? isPublic : name === PRIVATE ? isPrivate : () => true; + if (name && name !== PRIVATE && name !== PUBLIC) { + const nic = interfaces[name]; + if (!nic) return []; + const match = nic.find((details) => normalizeFamily(details.family) === fam); + return [match == null ? void 0 : match.address]; + } + const all = Object.values(interfaces).reduce((acc, nic) => { + for (const { family: family2, address: address2 } of nic != null ? nic : []) { + if (normalizeFamily(family2) !== fam) continue; + if (isLoopback(address2)) continue; + if (check(address2)) acc.push(address2); + } + return acc; + }, []); + return all.length ? all : [loopback(fam)]; +}; +var address = (name, family) => addresses(name, family)[0]; +var ip = { + address, + cidr, + cidrSubnet, + fromLong, + fromPrefixLen, + isEqual, + isLoopback, + isPrivate, + isPublic, + isV4Format, + isV6Format, + loopback, + mask, + not, + or, + subnet, + toBuffer, + toLong, + toString +}; var ip_default = ip; export { + IPV4, + IPV6, + V4_RE, + V4_S_RE, + V6_RE, + V6_S_RE, + address, + addresses, + cidr, + cidrSubnet, ip_default as default, - foo, - ip + fromLong, + fromPrefixLen, + ip, + isEqual, + isLoopback, + isPrivate, + isPublic, + isV4, + isV4Format, + isV6, + isV6Format, + loopback, + mask, + normalizeAddress, + normalizeFamily, + normalizeToLong, + not, + or, + subnet, + toBuffer, + toLong, + toString };