Skip to content

Instantly share code, notes, and snippets.

@ArrayIterator
Created August 17, 2024 09:10
Show Gist options
  • Save ArrayIterator/e54e83513875e6eb2ffc1e1b3566d7eb to your computer and use it in GitHub Desktop.
Save ArrayIterator/e54e83513875e6eb2ffc1e1b3566d7eb to your computer and use it in GitHub Desktop.
IP Calculator (inet_pton/ntop) and CIR Ranges
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