Home Reference Source Test

src/main/generic/network/NetUtils.js

class NetUtils {
    /**
     * @param {string|Uint8Array} ip
     * @return {boolean}
     */
    static isPrivateIP(ip) {
        if (!(ip instanceof Uint8Array)) {
            ip = NetUtils.ipToBytes(ip);
        }

        if (NetUtils.isLocalIP(ip)) {
            return true;
        }

        if (NetUtils.isIPv4Address(ip)) {
            for (const subnet of NetUtils.IPv4_PRIVATE_NETWORK) {
                if (NetUtils.isIPv4inSubnet(ip, subnet)) {
                    return true;
                }
            }
            return false;
        }

        if (NetUtils.isIPv6Address(ip)) {
            // Private subnet is fc00::/7.
            // So, we only check the first 7 bits of the address to be equal fc00.
            if ((ip[0] & 0xfe) === 0xfc) {
                return true;
            }

            // Link-local addresses are fe80::/10.
            if (ip[0] === 0xfe && (ip[1] & 0xc0) === 0x80) {
                return true;
            }

            // Does not seem to be a private IP.
            return false;
        }

        throw new Error(`Malformed IP address ${ip}`);
    }

    /**
     * @param {string|Uint8Array} ip
     * @returns {boolean}
     */
    static isLocalIP(ip) {
        if (!(ip instanceof Uint8Array)) {
            ip = NetUtils.ipToBytes(ip);
        }

        if (ip.length === NetUtils.IPv4_LENGTH) {
            return ip[0] === 127 && ip[1] === 0 && ip[2] === 0 && ip[3] === 1;
        }
        if (ip.length === NetUtils.IPv6_LENGTH) {
            for (let i = 0; i < NetUtils.IPv6_LENGTH - 1; i++) {
                if (ip[i] !== 0) return false;
            }
            return ip[NetUtils.IPv6_LENGTH - 1] === 1;
        }

        return false;
    }

    /**
     * @param {string|Uint8Array} ip
     * @param {string} subnet
     * @return {boolean}
     */
    static isIPv4inSubnet(ip, subnet) {
        if (!(ip instanceof Uint8Array)) {
            ip = NetUtils.ipToBytes(ip);
        }

        let [subIp, mask] = subnet.split('/');
        mask = -1<<(32-parseInt(mask));
        return (NetUtils._IPv4toLong(ip) & mask) === NetUtils._IPv4toLong(subIp);
    }

    /**
     * @param {string|Uint8Array} ip
     * @return {boolean}
     */
    static isIPv4Address(ip) {
        if (ip instanceof Uint8Array) return ip.length === NetUtils.IPv4_LENGTH;
        const match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
        return !!match && parseInt(match[1]) <= 255 && parseInt(match[2]) <= 255
            && parseInt(match[3]) <= 255 && parseInt(match[4]) <= 255;
    }

    /**
     * @param {string|Uint8Array} ip
     * @return {boolean}
     */
    static isIPv6Address(ip) {
        if (ip instanceof Uint8Array) return ip.length === NetUtils.IPv6_LENGTH;

        const parts = ip.toLowerCase().split(':');
        // An IPv6 address consists of at most 8 parts and at least 3.
        if (parts.length > 8 || parts.length < 3) {
            return false;
        }

        const isEmbeddedIPv4 = NetUtils.isIPv4Address(parts[parts.length - 1]);

        let innerEmpty = false;
        for (let i = 0; i < parts.length; ++i) {
            // Check whether each part is valid.
            // Note: the last part may be a IPv4 address!
            // They can be embedded in the last part. Remember that they take 32bit.
            if (!(/^[a-f0-9]{0,4}$/.test(parts[i])
                    || (i === parts.length - 1
                        && isEmbeddedIPv4
                        && parts.length < 8))) {
                return false;
            }
            // Inside the parts, there has to be at most one empty part.
            if (parts[i].length === 0 && i > 0 && i < parts.length - 1) {
                if (innerEmpty) {
                    return false; // at least two empty parts
                }
                innerEmpty = true;
            }
        }

        // In the special case of embedded IPv4 addresses, everything but the last 48 bit must be 0.
        if (isEmbeddedIPv4) {
            // Exclude the last two parts.
            for (let i=0; i<parts.length-2; ++i) {
                if (!/^0{0,4}$/.test(parts[i])) {
                    return false;
                }
            }
        }

        // If the first part is empty, the second has to be empty as well (e.g., ::1).
        if (parts[0].length === 0) {
            return parts[1].length === 0;
        }

        // If the last part is empty, the second last has to be empty as well (e.g., 1::).
        if (parts[parts.length - 1].length === 0) {
            return parts[parts.length - 2].length === 0;
        }

        // If the length is less than 7 and an IPv4 address is embedded, there has to be an empty part.
        if (isEmbeddedIPv4 && parts.length < 7) {
            return innerEmpty;
        }

        // Otherwise if the length is less than 8, there has to be an empty part.
        if (parts.length < 8) {
            return innerEmpty;
        }

        return true;
    }

    /**
     * @param {string} host
     * @returns {boolean}
     */
    static hostGloballyReachable(host) {
        // IP addresses can't have a proper certificate
        if (NetUtils.isIPv4Address(host) || NetUtils.isIPv6Address(host)) {
            return false;
        }
        // "the use of dotless domains is prohibited [in new gTLDs]" [ https://www.icann.org/resources/board-material/resolutions-new-gtld-2013-08-13-en#1 ]. Old gTLDs rarely use them.
        if (!host.match(/.+\..+$/)) {
            return false;
        }
        return true;
    }

    /**
     * @param {string|Uint8Array} ip
     * @return {number}
     */
    static _IPv4toLong(ip) {
        if (!(ip instanceof Uint8Array)) {
            ip = NetUtils.ipToBytes(ip);
        }
        return (ip[0]<<24) + (ip[1]<<16) + (ip[2]<<8) + ip[3];
    }

    /**
     * @param {string} ip
     * @returns {string}
     * @private
     */
    static _IPv4toIPv6(ip) {
        let parts = ip.split('.');
        parts = parts.map(x => parseInt(x));
        const mask = [];
        for(let i = 0; i < 4; i++) {
            mask.push(('00' + parts[i].toString(16)).slice(-2));
        }
        return `${mask[0]}${mask[1]}:${mask[2]}${mask[3]}`;
    }

    /**
     * @param {string} ip
     * @returns {Uint8Array}
     */
    static ipToBytes(ip) {
        if (NetUtils.isIPv4Address(ip)) {
            const parts = ip.split('.');
            return new Uint8Array(parts.map(x => parseInt(x)));
        }

        if (NetUtils.isIPv6Address(ip)) {
            let parts = ip.toLowerCase().split(':');

            // Handle embedded IPv4 addresses.
            if (NetUtils.isIPv4Address(parts[parts.length - 1])) {
                return NetUtils.ipToBytes(parts[parts.length - 1]);
            }

            // IPv6
            parts = NetUtils._extendIPv6(parts);
            parts = parts.map(x => parseInt(x, 16));
            const bytes = [];
            for(let i = 0; i < 8; i++) {
                bytes.push(parts[i] >> 8);
                bytes.push(parts[i] & 0xff);
            }
            return new Uint8Array(bytes);
        }

        throw new Error(`Malformed IP address ${ip}`);
    }

    /**
     * @param {Uint8Array} ip
     * @returns {string}
     */
    static bytesToIp(ip) {
        if (NetUtils.isIPv4Address(ip)) {
            return ip.join('.');
        }

        if (NetUtils.isIPv6Address(ip)) {
            const hexIp = Array.from(ip, x => ('00' + x.toString(16)).slice(-2));
            const ipv6 = [];
            for (let i = 0; i < 8; i++) {
                ipv6.push(hexIp[i*2] + hexIp[i*2+1]);
            }
            return ipv6.join(':');
        }

        throw new Error(`Malformed IP address ${ip}`);
    }

    /**
     * @param {Array.<string>} parts
     * @returns {Array.<string>}
     * @private
     */
    static _extendIPv6(parts) {
        // Handle embedded IPv4 addresses.
        if (NetUtils.isIPv4Address(parts[parts.length - 1])) {
            const ipv4 = parts[parts.length - 1];
            const ipv6 = NetUtils._IPv4toIPv6(ipv4);
            ip = ip.replace(ipv4, ipv6);
            parts = ip.toLowerCase().split(':');
        }

        let emptyPart = parts.indexOf('');
        // If there is an empty part, fill it up.
        if (emptyPart >= 0) {
            parts[emptyPart] = '0';
            for (let i = parts.length; i < 8; i++) {
                parts.splice(emptyPart, 0, '0');
            }
        }
        // Fill remaining empty fields with 0 as well.
        emptyPart = parts.indexOf('');
        while (emptyPart >= 0) {
            parts[emptyPart] = '0';
            emptyPart = parts.indexOf('');
        }

        return parts;
    }

    /**
     * @param {string|Uint8Array} ip
     * @param {number} bitCount
     * @return {string|Uint8Array}
     */
    static ipToSubnet(ip, bitCount) {
        let stringResult = false;
        if (!(ip instanceof Uint8Array)) {
            ip = NetUtils.ipToBytes(ip);
            stringResult = true;
        }

        const mask = [];
        for(let i = 0; i < ip.byteLength; i++) {
            const n = Math.min(bitCount, 8);
            mask.push(ip[i] & (256 - Math.pow(2, 8 - n)));
            bitCount -= n;
        }
        const result = new Uint8Array(mask);
        return stringResult ? NetUtils.bytesToIp(result) : result;
    }
}
NetUtils.IPv4_LENGTH = 4;
NetUtils.IPv6_LENGTH = 16;
NetUtils.IPv4_PRIVATE_NETWORK = [
    '10.0.0.0/8',
    '172.16.0.0/12',
    '192.168.0.0/16',
    '100.64.0.0/10', // link-local

    // Actually, the following one is only an approximation,
    // the first and the last /24 subnets in the range should be excluded.
    '169.254.0.0/16'
];
Class.register(NetUtils);