Viewing File: /usr/local/cpanel/share/libraries/cjt2/src/util/inet6.js

/*
# cpanel - share/libraries/cjt2/src/util/inet6.js    Copyright 2022 cPanel, L.L.C.
#                                                           All rights reserved.
# copyright@cpanel.net                                         http://cpanel.net
# This code is subject to the cPanel license. Unauthorized copying is prohibited
*/

/* global define: false, module: false */

/**
 *
 * @module cjt/util/inet6
 * @example var output = inet6.parse(input).toString();
 * @exports cjt/util/inet6
 */
(function(root, factory) {
    if (typeof define === "function" && define.amd) {

        /*
         * AMD; Register as an anonymous module because
         * the filename (in this case cjt/util/inet6) will
         * become the name of the module.
         */
        define([], factory);
    } else if (typeof exports === "object") {

        /*
         * Node. Does not work with strict CommonJS, but * only CommonJS-like
         * enviroments that support module.exports, like Node.
         */
        module.exports = factory();
    } else {

        /*
         * Export to cPanel browser global namespace
         */
        if (root.CPANEL) {
            root.CPANEL.inet6 = factory();
        } else {
            root.inet6 = factory();
        }
    }
}(this, function() {
    "use strict";

    var inet = {};

    // TODO: replace with $Cpanel::Regex::regex{'ipv4'}
    var ipv4Regex = /^\d{1,3}(?:\.\d{1,3}){3}$/;
    var ipv6PartRegex = /^[0-9a-f]{1,4}$/i;

    /**
     * @constructor
     * @param {string} address - String we want to represent an IPv4 address
     *                           portion of a IPv4 compatible address
     */
    inet.Address = function(address) {
        if (address === void 0 || Object.prototype.toString.call(address) !== "[object String]") {
            throw "Invalid input: Not a String";
        }

        var parts = address.split(".");

        if (parts.length > 4) {
            throw "Invalid IPv4 address: Too many components";
        }

        if (parts.length < 4) {
            throw "Invalid IPv4 address: Too few components";
        }

        for (var i = 0, len = parts.length; i < len; i++) {
            var part = parts[i];

            if (part > 255 || part < 0) {
                throw "Invalid IPv4 address: Invalid component";
            }

            this.push(part);
        }
    };

    inet.Address.prototype = [];

    inet.Address.prototype.toString = function() {
        return this.join(".");
    };

    var inet6 = {};

    /**
     * @constructor
     * @param {string} address - the string we want to convert into an IPv6 object
     */
    inet6.Address = function(address) {
        var self = this;

        /*
         * A quick convenience for adding zero padding groups to the current
         * object.
         */
        function pad(count) {
            for (var i = 0; i < count; i++) {
                self.push(0x0000);
            }
        }

        if (address === void 0 || Object.prototype.toString.call(address) !== "[object String]") {
            throw "Invalid input: Not a String";
        }

        /*
         * First, take a look through all the address components passed to the
         * constructor.
         */
        var parts    = address.split(":");
        var expected = 8;
        var minimum  = 3;
        var count    = parts.length; /* Number of logical parts in address */
        var length   = parts.length; /* Number of string parts in address */
        var padded   = false;

        var i, part, value, first, last;

        /*
         * This value will change to true if there is a trailing IPv4 address
         * embedded in the address string.
         */
        var hasv4Address = false;

        /*
         * If the address does not contain at least "::", then bail, of course.
         */
        if (length < minimum) {
            throw "Invalid IPv6 address: Too few components";
        }

        if (length > 3 && parts[0] === "" && parts[1] === "" && parts[length - 1] === "" && parts[length - 2] === "") {
            throw "Invalid IPv6 address: Too many colons";
        }

        if (parts[0] === "" && parts[1] !== "") {
            throw "Invalid IPv6 address: Missing beginning component";
        }

        if (parts[length - 1] === "" && parts[length - 2] !== "") {
            throw "Invalid IPv6 address: Missing end component";
        }

        /*
         * Get rid of the leading and trailing double-colon effects
         */
        if (parts[0] === "" && parts[1] === "") {
            parts.shift();
            length = parts.length;
            count  = parts.length;
        }
        if (parts[parts.length - 1] === "" && parts[parts.length - 2] === "") {
            parts.pop();
            length = parts.length;
            count  = parts.length;
        }

        /*
         * If we're left with one empty item, our original address was
         * ::, so just pad the whole thing out and be done.
         */
        if (length === 1 && parts[0] === "") {
            pad(8);
            return;
        }

        /*
         * This counter is used to keep track of the number of empty components
         * in the middle of a tokenized IPv6 address string.  For example:
         *
         * fe80::1::2
         *
         * Any more than one empty component in the middle of an address leads
         * to an ambiguity in determining how much zero padding to use in an
         * address.
         */
        var emptyMiddle = 0;

        /*
         * Increase the parts count by one for each IPv4 address component
         * found.
         */
        for (i = 0; i < length; i++) {
            part = parts[i].trim();

            if (ipv4Regex.test(part)) {
                count++;
            }
        }

        for (i = 0; i < length; i++) {
            part  = parts[i].trim();
            value = null;
            first = (i ===           0) ? true : false;
            last  = (i === (length - 1)) ? true : false;

            if (ipv4Regex.test(part)) {

                /*
                 * Check for an embedded IPv4 address
                 */
                if (i !== length - 1) {
                    throw "Invalid IPv6 address: Embedded IPv4 address not at end";
                }

                for (var n = 4; n < expected - count; n++) {
                    this.shift();
                }

                var inet4address = new inet.Address(part);

                this.push((inet4address[0] << 8) | inet4address[1]);

                value        = (inet4address[2] << 8) | inet4address[3];
                hasv4Address = true;
            } else if (ipv6PartRegex.test(part)) {

                /*
                 * Check for a valid IPv6 part
                 */
                value = parseInt(part, 16);
            } else if (part === "") {
                emptyMiddle++;

                /*
                 * If we have reached an empty component, and no padding has
                 * been applied yet, then introduce the requisite amount of
                 * zero padding.
                 */
                if (!padded) {
                    pad(expected - count);
                    padded = true;
                }

                value = 0x0000;
            } else {
                throw "Invalid IPv6 address: Invalid component " + part;
            }

            this.push(value);
        }

        if (emptyMiddle > 1) {
            throw "Invalid IPv6 address: Too many colons";
        }

        if (this.length < expected) {
            throw "Invalid IPv6 address: Too few components";
        }

        if (this.length > expected) {
            throw "Invalid IPv6 address: Too many components";
        }

        if (hasv4Address) {
            for (i = 0; i < 5; i++) {
                if (this[i] !== 0x0000) {
                    throw "Invalid IPv4 compatible address";
                }
            }

            if (this[5] !== 0xffff) {
                throw "Invalid IPv6 compatible address";
            }
        }
    };

    inet6.Address.prototype = [];

    /**
     * Stringify an IPv6 address with an embedded IPv4 address
     * @return {string}
     */
    inet6.Address.prototype.toString_v4Compat = function() {
        var parts = [];

        parts.push((this[6] & 0xff00) >> 8);
        parts.push( this[6] & 0x00ff);
        parts.push((this[7] & 0xff00) >> 8);
        parts.push( this[7] & 0x00ff);

        return "::ffff:" + parts.join(".");
    };

    /**
     * Returns true if the current address object is an IPv4 compatibility
     * address; in other words, an address in the ::ffff:0:0/96 space.
     *
     * @return {boolean}
     */
    inet6.Address.prototype.isv4Compat = function() {

        /*
         * Ensure the first five uint16s of the address are 0x0000 values.
         */
        for (var i = 0; i < 5; i++) {
            if (this[i] !== 0x0000) {
                return 0;
            }
        }

        /*
         * At this point, the sixth uint16 determines if we do indeed have an
         * IPv4 compatibility address.
         */
        return (this[5] === 0xffff) ? true : false;
    };

    /**
     * Stringify an IPv6 address
     * @return {string}
     */
    inet6.Address.prototype.toString = function() {
        var ranges  = [];
        var count   = this.length;
        var last    = null;
        var longest = null;
        var range   = null;

        /*
         * If this is an IPv4 compatible address, stringify using a method that
         * will encode it in the proper quad octet notation.
         */
        if (this.isv4Compat()) {
            return this.toString_v4Compat();
        }

        /*
         * First, collate contiguous groups of zeroes into an array of
         * ranges, indicating the index within the current address object
         * of their first and their last occurences.  Along the way,
         * determine which range of contiguous zeroes is the longest,
         * preferring the rightmost one if there are multiple groups of
         * zeroes in the address.
         */
        for (var i = 0; i < count; i++) {
            var value = this[i];

            if (value !== 0x0000 || (value === 0x0000 && last !== 0x0000)) {
                ranges.push({
                    "value": value,
                    "first": i,
                    "last": i,
                    "longest": false
                });
            }

            range = ranges[ranges.length - 1];

            range.last = i;

            if (longest === null) {
                longest = range;
            }

            var currentSize =   range.last -   range.first;
            var longestSize = longest.last - longest.first;

            if (value === 0x0000 && currentSize > longestSize) {
                longest = range;
            }

            last = value;
        }

        /*
         * Next, format the number ranges into an array of string tokens,
         * adding empty tokens along the way where necessary to express
         * contiguous ranges of zeroes as accurately as possible.
         */
        var ret = [];
        var len = ranges.length;

        for (i = 0; i < len; i++) {
            range = ranges[i];

            if (range.value === 0x0000 && range === longest) {

                /*
                 * If this is the first range of contiguous zeroes in the
                 * address, then add an empty token to the left of the
                 * address to be returned.
                 */
                if (i === 0) {
                    ret.push("");
                }

                /*
                 * Regardless of the position of the longest range of
                 * contiguous zeroes, add an empty token to the output.
                 */
                ret.push("");

                /*
                 * If this is the last range of contiguous zeroes in the
                 * address, then add another empty token to the output.
                 */
                if (i === len - 1) {
                    ret.push("");
                }
            } else {
                for (var n = range.first; n <= range.last; n++) {
                    ret.push(range.value.toString(16));
                }
            }
        }

        return ret.join(":");
    };

    /**
     * Exported method to validate an IPv6 address
     * @param {string} address - IPv6 address string
     * @return {boolean}
     */
    inet6.isValid = function(address) {
        try {
            this.parse(address);
            return true;
        } catch (e) {
            return false;
        }
    };

    /**
     * Exported method for parsing IPv6 addresses to inet6.Address objects
     * @param  {string} address - IPv6 address string
     * @return {inet6.Address}
     */
    inet6.parse = function(address) {
        if (address === void 0 || Object.prototype.toString.call(address) !== "[object String]") {
            throw "Invalid input: Not a String";
        }

        return new this.Address(address);
    };

    /**
     * Reformat an IPv6 address into its canonical compact representation for
     * display; if the input is an invalid IPv6 address, it is returned to the
     * caller unmodified, otherwise the newly-reformatted address is returned
     * upon success
     *
     * @param {string} address - IPv6 address string
     * @return {string}
     */
    inet6.formatForDisplay = function(address) {
        var ret;

        try {
            var inet6 = new this.Address(address);

            ret = inet6.toString();
        } catch (e) {
            ret = address;
        }

        return ret;
    };

    return inet6;
}));
Back to Directory File Manager