Created
August 17, 2024 09:10
-
-
Save ArrayIterator/e54e83513875e6eb2ffc1e1b3566d7eb to your computer and use it in GitHub Desktop.
IP Calculator (inet_pton/ntop) and CIR Ranges
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export default class Ip { | |
/** | |
* IPv4 regex | |
*/ | |
public static readonly IPv4_REGEX = /^(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])\.(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])\.(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])\.(?:[0-1]?[0-9]{1,2}|2[0-4][0-9]|25[0-5])$/; | |
/** | |
* IPv6 regex | |
* ::1 is also valid IPv6 address | |
*/ | |
public static readonly IPv6_REGEX = /^([0-9a-fA-F]{0,4}:){1,7}([0-9a-fA-F]{1,4})$/; | |
/** | |
* IPv4 broadcast range | |
*/ | |
public static readonly IPv4_BROADCAST_RANGE = '255.255.255.255/32'; | |
/** | |
* 0.0.0.0/8: Current network (only valid as source address) | |
* 10.0.0.0/8: Private network | |
* 100.64.0.0/10: Shared Address Space | |
* 127.0.0.0/8: LoopBack | |
* 169.254.0.0/16: Link-local | |
* 172.16.0.0/12: Private network | |
* 192.0.0.0/24: IETF Protocol Assignments | |
* 192.0.2.0/24: TEST-NET-1, documentation and examples | |
* 192.88.99.0/24: IPv6 to IPv4 relay | |
* 192.168.0.0/16: Private network | |
* 198.18.0.0/15: Network Interconnect Device Benchmark Testing | |
* 198.51.100.0/24: TEST-NET-2, documentation and examples | |
* 203.0.113.0/24: TEST-NET-3, documentation and examples | |
* 224.0.0.0/4: IP Multicast (former Class D network) | |
* 240.0.0.0/4: Reserved (former Class E network) | |
* 255.255.255.255/32: Broadcast | |
* @type {string[]} | |
*/ | |
public static readonly IPv4_BOGON_RANGES: string[] = [ | |
'0.0.0.0/8', | |
'10.0.0.0/8', | |
'100.64.0.0/10', | |
'127.0.0.0/8', | |
'169.254.0.0/16', | |
'172.16.0.0/12', | |
'192.0.0.0/24', | |
'192.0.2.0/24', | |
'192.88.99.0/24', | |
'192.168.0.0/16', | |
'198.18.0.0/15', | |
'198.51.100.0/24', | |
'203.0.113.0/24', | |
'224.0.0.0/4', | |
'240.0.0.0/4', | |
Ip.IPv4_BROADCAST_RANGE, | |
]; | |
/** | |
* ::/128: Unspecified address | |
* ::1/128: LoopBack address | |
* 2001::/32: Teredo tunneling | |
* 2002::/16: 6to4 | |
* fc00::/7: Unique local address | |
* fe80::/10: Link-local address | |
* ff00::/8: Multicast | |
* | |
* @type {string[]} | |
*/ | |
public static readonly IPv6_RESERVED_RANGES: string[] = [ | |
'::/128', // Unspecified address | |
'::1/128', // LoopBack address | |
'2001::/32', // Teredo tunneling | |
'2002::/16', // 6to4 | |
'fc00::/7', // Unique local address | |
'fe80::/10', // Link-local address | |
'ff00::/8', // Multicast | |
]; | |
public static version = (ip: any): null | 4 | 6 => { | |
if (typeof ip !== 'string') { | |
return null; | |
} | |
if (Ip.filter_ipv4(ip) !== false) { | |
return 4; | |
} | |
if (Ip.filter_ipv6(ip) !== false) { | |
return 6; | |
} | |
return null; | |
} | |
/** | |
* Convert IP to long | |
* | |
* @param {string} ip | |
* @return {bigint|null} | |
*/ | |
public static ip2long = (ip: any): bigint | null => { | |
const version = Ip.version(ip); | |
if (version === null) { | |
return null; | |
} | |
ip = version === 4 ? Ip.filter_ipv4(ip) : Ip.filter_ipv6(ip); | |
if (ip === false) { | |
return null; | |
} | |
if (version === 4) { | |
return BigInt(ip.split('.').reduce((acc: any, octet: string, i: number) => parseInt(acc) + (parseInt(octet) << (24 - 8 * i)), 0) >>> 0); | |
} | |
let bigInt: bigint = BigInt(0); | |
ip.split(':').forEach((hexOctet: string, i: number) => { | |
bigInt += BigInt(parseInt(hexOctet, 16) << (112 - 16 * i)); | |
}); | |
return bigInt; | |
} | |
/** | |
* Convert long to IP | |
* | |
* @param {number|bigint|string} long | |
* @return {string|false} | |
*/ | |
public static long2ip = (long: any): null | string => { | |
if (typeof long !== 'bigint' && typeof long !== 'number' && typeof long !== 'string') { | |
return null; | |
} | |
if (typeof long === 'string' && !/^\d+$/.test(long)) { | |
return null; | |
} | |
// 2^128 = 340282366920938463463374607431768211455 | |
const LargestInteger = BigInt('340282366920938463463374607431768211455'); | |
const CurrentInteger = BigInt(long); | |
if (CurrentInteger < BigInt(0) || CurrentInteger > LargestInteger) { | |
return null; | |
} | |
// if ipv4 long | |
if (CurrentInteger <= CurrentInteger) { | |
const Shifter = BigInt(0xFF); | |
return [ | |
CurrentInteger >> BigInt(24) & Shifter, | |
CurrentInteger >> BigInt(16) & Shifter, | |
CurrentInteger >> BigInt(8) & Shifter, | |
CurrentInteger & Shifter, | |
].join('.'); | |
} | |
if (CurrentInteger <= LargestInteger) { | |
const Shifter = BigInt(0xFFFF); | |
return [ | |
(CurrentInteger >> BigInt(112)) & Shifter, | |
(CurrentInteger >> BigInt(96)) & Shifter, | |
(CurrentInteger >> BigInt(80)) & Shifter, | |
(CurrentInteger >> BigInt(64)) & Shifter, | |
(CurrentInteger >> BigInt(48)) & Shifter, | |
(CurrentInteger >> BigInt(32)) & Shifter, | |
(CurrentInteger >> BigInt(16)) & Shifter, | |
CurrentInteger & Shifter, | |
].map(x => x.toString(16)).join(':'); | |
} | |
return null; | |
} | |
/** | |
* Inet Pton | |
* | |
* @param {string} ip | |
* @return {string|false} IP address | |
*/ | |
public static inet_pton = (ip: any): string | false => { | |
if (typeof ip !== 'string') { | |
return false; | |
} | |
let m: any, i: any, j: any; | |
const f = String.fromCharCode | |
// IPv4 | |
m = ip.match(/^(?:\d{1,3}(?:\.|$)){4}/) | |
if (m) { | |
m = m[0].split('.') | |
m = f(m[0], m[1], m[2], m[3]) | |
// Return if 4 bytes, otherwise false. | |
return m.length === 4 ? m : false | |
} | |
// IPv6 | |
if (ip.length > 39) { | |
return false | |
} | |
m = ip.split('::'); | |
if (m.length > 2) { | |
return false | |
} // :: can't be used more than once in IPv6. | |
const reHexDigits = /^[\da-f]{1,4}$/i; | |
for (j = 0; j < m.length; j++) { | |
if (m[j].length === 0) { | |
// Skip if empty. | |
continue | |
} | |
m[j] = m[j].split(':'); | |
for (i = 0; i < m[j].length; i++) { | |
let hextet = m[j][i]; | |
// check if valid hex string up to 4 chars | |
if (!reHexDigits.test(hextet)) { | |
return false; | |
} | |
hextet = parseInt(hextet, 16); | |
// Would be NaN if it was blank, return false. | |
if (isNaN(hextet)) { | |
// Invalid IP. | |
return false; | |
} | |
m[j][i] = f(hextet >> 8, hextet & 0xff); | |
} | |
m[j] = m[j].join(''); | |
} | |
return m.join('\x00'.repeat(16 - m.reduce((tl: null, m: any) => tl + m.length, 0))); | |
} | |
/** | |
* Inverse of inet_pton | |
* | |
* @param {string} ip | |
* @return {string|false} IP address | |
*/ | |
public static inet_ntop = (ip: any): string | false => { | |
if (typeof ip !== 'string') { | |
return false; | |
} | |
let i = 0 | |
let m = '' | |
const c = [] | |
ip += '' | |
if (ip.length === 4) { | |
// IPv4 | |
return [ip.charCodeAt(0), ip.charCodeAt(1), ip.charCodeAt(2), ip.charCodeAt(3)].join('.') | |
} else if (ip.length === 16) { | |
// IPv6 | |
for (i = 0; i < 16; i++) { | |
c.push(((ip.charCodeAt(i++) << 8) + ip.charCodeAt(i)).toString(16)) | |
} | |
return c | |
.join(':') | |
.replace(/((^|:)0(?=:|$))+:?/g, function (t) { | |
m = t.length > m.length ? t : m | |
return t | |
}) | |
.replace(m || ' ', '::') | |
} else { | |
// Invalid length | |
return false | |
} | |
} | |
/** | |
* Check if an IP is in a range | |
* | |
* @param {string} ip | |
* @param {string|Array<string>} ip_ranges | |
* @return {boolean} | |
*/ | |
public static in_range = (ip: any, ip_ranges: any): boolean => { | |
const version = Ip.version(ip); | |
if (version === null) { | |
return false; | |
} | |
if (typeof ip_ranges === 'string') { | |
ip_ranges = [ip_ranges]; | |
} | |
if (!Array.isArray(ip_ranges)) { | |
return false; | |
} | |
ip = version === 4 ? Ip.filter_ipv4(ip) : Ip.filter_ipv6(ip); | |
if (ip === false) { | |
return false; | |
} | |
let ipNum: any; | |
ipNum = Ip.ip2long(ip); | |
if (ipNum === false) { | |
return false; | |
} | |
for (let _ip of ip_ranges) { | |
if (typeof _ip !== 'string') { | |
continue; | |
} | |
let _version: any; | |
let range: any; | |
if (!_ip.includes('/')) { | |
_ip = Ip.filter_ipv4(_ip); | |
if (_ip === ip) { | |
return true; | |
} | |
continue; | |
} | |
let split = _ip.split('/'); | |
if (split.length !== 2) { | |
continue; | |
} | |
_version = Ip.version(split[0]); | |
if (_version === null || !split[1] || !/^\d+$/.test(split[1])) { | |
continue; | |
} | |
range = parseInt(split[1]); | |
_ip = split[0]; | |
if (_version === 4 && (range < 0 || range > 32)) { | |
continue; | |
} | |
if (_version === 6 && (range < 0 || range > 128)) { | |
continue; | |
} | |
_ip = _version === 4 ? Ip.filter_ipv4(_ip) : Ip.filter_ipv6(_ip); | |
if (_ip === false) { | |
continue; | |
} | |
if (_ip === ip) { | |
return true; | |
} | |
let _ipNum = Ip.ip2long(_ip); | |
if (typeof _ipNum !== 'bigint') { | |
continue; | |
} | |
let mask: bigint = BigInt(_version === 4 ? (1 << (32 - range)) - 1 : (1 << (128 - range)) - 1); | |
if ((_ipNum & ~mask) === (ipNum & ~mask)) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Check if an IP is a bogon IP | |
* | |
* @param {string} ip | |
* @return {boolean} | |
*/ | |
public static is_bogon = (ip: any): boolean => { | |
return Ip.in_range(ip, [ | |
...Ip.IPv4_BOGON_RANGES, | |
...Ip.IPv6_RESERVED_RANGES, | |
]); | |
} | |
/** | |
* Check if an IP is a broadcast IP | |
* | |
* @param {string} ip | |
* @return {boolean} | |
*/ | |
public static is_broadcast = (ip: any): boolean => { | |
return Ip.in_range(ip, Ip.IPv4_BROADCAST_RANGE); | |
} | |
/** | |
* Filter IPv4 address | |
* | |
* @param {string} ip | |
* @return {string|false} | |
*/ | |
public static filter_ipv4 = (ip: any): string | false => { | |
if (typeof ip !== 'string' || !ip.match(Ip.IPv4_REGEX)) { | |
return false; | |
} | |
const split = ip.split('.'); | |
if (split.length !== 4) { | |
return false; | |
} | |
const number = '0123456789'; | |
for (let i = 0; i < split.length; i++) { | |
let num: any = split[i]; | |
if (num.length > 3) { | |
return false; | |
} | |
for (let j = 0; j < num.length; j++) { | |
if (!number.includes(num[j])) { | |
return false; | |
} | |
} | |
num = parseInt(num); | |
if (num > 255) { | |
return false; | |
} | |
split[i] = num; | |
} | |
return split.join('.'); | |
} | |
/** | |
* Filter IPv6 address | |
* | |
* @param {string} ip | |
* @return {string|false} | |
*/ | |
public static filter_ipv6 = (ip: any): string|boolean => { | |
if (typeof ip !== 'string' || !ip.includes(':') || ip.includes('.')) { | |
return false; | |
} | |
ip = ip.toLowerCase(); | |
const split = ip.split(':'); | |
if (split.length > 8 || split.length < 3) { | |
return false; | |
} | |
const hex_chars = '0123456789abcdef'; | |
for (let i = 0; i < split.length; i++) { | |
let hex = split[i]; | |
if (hex.length > 4) { | |
return false; | |
} | |
for (let j = 0; j < split[i].length; j++) { | |
if (!hex_chars.includes(split[i][j])) { | |
return false; | |
} | |
} | |
} | |
return split | |
.join(':') | |
.replace(/(^:|)0+/, '$1') | |
.replace(/^::+/, '::'); | |
} | |
/** | |
* Convert binary to hexadecimal | |
* @param {string} param | |
*/ | |
public static bin2hex = (param: string): string => { | |
if (typeof param !== 'string') { | |
return ''; | |
} | |
const bytes = (new TextEncoder()).encode(param); | |
const hex = []; | |
bytes.forEach((byte) => { | |
hex.push(byte.toString(16).padStart(2, '0')); | |
}); | |
return hex.join(''); | |
} | |
/** | |
* Convert the parameter to a number | |
* | |
* @param {any} hexString | |
* @return {number} | |
*/ | |
public static hexdec = (hexString: string): number => { | |
if (typeof hexString !== 'string') { | |
return 0; | |
} | |
if (hexString === '') { | |
return 0; | |
} | |
hexString = hexString | |
.replace(/^0x/i, '') | |
.replace(/\s*/g, ''); | |
return parseInt(hexString, 16); | |
} | |
/** | |
* Convert the parameter to a binary string | |
* | |
* @param {any} param | |
* @return {string} | |
*/ | |
public static hex2bin = (param: string): string => { | |
if (typeof param !== 'string') { | |
return ''; | |
} | |
const ret = []; | |
let i = 0; | |
let l: number; | |
param += ''; | |
for (l = param.length; i < l; i += 2) { | |
const c = parseInt(param.substring(i, 1), 16) | |
const k = parseInt(param.substring(i + 1, 1), 16) | |
if (isNaN(c) || isNaN(k)) { | |
return ''; | |
} | |
ret.push((c << 4) | k); | |
} | |
return String.fromCharCode.apply(String, ret); | |
} | |
/** | |
* Convert CIDR to IP range start & end | |
* | |
* @param {string} cidr | |
* @return {Array<string>|null} start ip & end ip | |
*/ | |
public static ipv6_cidr_ranges = (cidr: any): null | [string, string] => { | |
if (typeof cidr !== 'string') { | |
return null; | |
} | |
cidr = cidr.trim().split('/'); | |
if (cidr.length !== 2) { | |
return null; | |
} | |
let $ip: any = cidr[0]; | |
let $range: any = cidr[1].trim(); | |
if ($ip === '' || $range === '' || $range.includes('.') || !/^\d+$/.test($range)) { | |
return null; | |
} | |
$range = parseInt($range); | |
if ($range < 0 | |
|| $range > 128 | |
|| !Ip.filter_ipv6($ip) | |
) { | |
return null; | |
} | |
let version = Ip.version(cidr); | |
if (version !== 6) { | |
return null; | |
} | |
let $firstAddrBin = Ip.inet_pton(cidr); | |
let $firstAddr: any; | |
// fail return null | |
if ($firstAddrBin === false | |
|| !($firstAddr = Ip.inet_ntop($firstAddrBin)) | |
) { | |
return null; | |
} | |
let $flexBits = 128 - $range; | |
// Build the hexadecimal string of the last address | |
let $lastAddrHex = Ip.bin2hex($firstAddrBin); | |
// start at the end of the string (which is always 32 characters long) | |
let $pos = 31; | |
let $orig : any, $originalVal : any,$newVal : any,$new : any; | |
while ($flexBits > 0) { | |
// Get the character at this position | |
$orig = $lastAddrHex.substring($pos, $pos + 1); | |
// Convert it to an integer | |
$originalVal = Ip.hexdec($orig); | |
// OR it with (2^flexBits)-1, with flexBits limited to 4 at a time | |
$newVal = $originalVal | (Math.pow(2, Math.min(4, $flexBits)) - 1); | |
// Convert it back to a hexadecimal character | |
$new = Number($newVal).toString(16); | |
// And put that character back in the string | |
$lastAddrHex = $lastAddrHex.substring(0, $pos) + $new + $lastAddrHex.substring($pos + 1); | |
// process one nibble, move to previous position | |
$flexBits -= 4; | |
$pos -= 1; | |
} | |
let $lastAddrBin = Ip.hex2bin($lastAddrHex); | |
let $lastAddr = Ip.inet_ntop($lastAddrBin); | |
if (!$lastAddr) { | |
return null; | |
} | |
return [$firstAddr, $lastAddr]; | |
} | |
/** | |
* Convert CIDR to IP range start & end | |
* | |
* @param {string} cidr | |
* @return {Array<string>|null} start ip & end ip | |
*/ | |
public static ipv4_cidr_ranges = (cidr: any): null | [string, string] => { | |
if (typeof cidr !== 'string') { | |
return null; | |
} | |
cidr = cidr.trim().split('/'); | |
if (cidr.length !== 2) { | |
return null; | |
} | |
let $ip: any = cidr[0]; | |
let $range: any = cidr[1].trim(); | |
if ($ip === '' || $range === '' || $range.includes('.') || !/^\d+$/.test($range)) { | |
return null; | |
} | |
if ($range > 32 | |
|| $range < 0 | |
|| !Ip.filter_ipv4($ip) | |
) { | |
return null; | |
} | |
let $ips = $ip.split('.'); | |
if ($ips.length !== 4) { | |
return null; | |
} | |
for (let $ip_address of $ips) { | |
if ($ip_address === '') { | |
return null; | |
} | |
if ($ip_address.includes('.') | |
|| !/^\d+$/.test($ip_address) | |
|| parseInt($ip_address) > 255 | |
|| parseInt($ip_address) < 0 | |
) { | |
return null; | |
} | |
} | |
const start = Ip.long2ip((Ip.ip2long($ip)) & BigInt((-1 << (32 - $range)))); | |
const end = Ip.long2ip((Ip.ip2long($ip)) + BigInt(Math.pow(2, (32 - $range)) - 1)); | |
if (start === null || end === null) { | |
return null; | |
} | |
return [start, end]; | |
} | |
/** | |
* Get total number of IPs in a CIDR | |
* | |
* @param {string} cidr | |
* @return {number} total number of IPs | |
*/ | |
public static total_cidr_ips = (cidr: any): number => { | |
if (typeof cidr !== 'string') { | |
return 0; | |
} | |
let version = Ip.version(cidr); | |
if (version === null) { | |
return 0; | |
} | |
let range: any; | |
if (version === 4) { | |
range = Ip.ipv4_cidr_ranges(cidr); | |
} else { | |
range = Ip.ipv6_cidr_ranges(cidr); | |
} | |
if (range === null) { | |
return 0; | |
} | |
let start = Ip.ip2long(range[0]); | |
let end = Ip.ip2long(range[1]); | |
if (start === null || end === null) { | |
return 0; | |
} | |
return parseInt((end - start).toString()) + 1; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment