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.

Revisions

  1. ArrayIterator created this gist Aug 17, 2024.
    638 changes: 638 additions & 0 deletions Ip.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,638 @@
    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;
    }
    }