recoder-code
Version:
🚀 AI-powered development platform - Chat with 32+ models, build projects, automate workflows. Free models included!
732 lines (607 loc) • 18.4 kB
text/typescript
/* eslint-disable prefer-destructuring */
/* eslint-disable no-param-reassign */
import * as common from './common';
import * as constants4 from './v4/constants';
import * as constants6 from './v6/constants';
import * as helpers from './v6/helpers';
import { Address4 } from './ipv4';
import {
ADDRESS_BOUNDARY,
possibleElisions,
simpleRegularExpression,
} from './v6/regular-expressions';
import { AddressError } from './address-error';
import { testBit } from './common';
function assert(condition: any): asserts condition {
if (!condition) {
throw new Error('Assertion failed.');
}
}
function addCommas(number: string): string {
const r = /(\d+)(\d{3})/;
while (r.test(number)) {
number = number.replace(r, '$1,$2');
}
return number;
}
function spanLeadingZeroes4(n: string): string {
n = n.replace(/^(0{1,})([1-9]+)$/, '<span class="parse-error">$1</span>$2');
n = n.replace(/^(0{1,})(0)$/, '<span class="parse-error">$1</span>$2');
return n;
}
/*
* A helper function to compact an array
*/
function compact(address: string[], slice: number[]) {
const s1 = [];
const s2 = [];
let i;
for (i = 0; i < address.length; i++) {
if (i < slice[0]) {
s1.push(address[i]);
} else if (i > slice[1]) {
s2.push(address[i]);
}
}
return s1.concat(['compact']).concat(s2);
}
function paddedHex(octet: string): string {
return parseInt(octet, 16).toString(16).padStart(4, '0');
}
function unsignByte(b: number) {
// eslint-disable-next-line no-bitwise
return b & 0xff;
}
interface SixToFourProperties {
prefix: string;
gateway: string;
}
interface TeredoProperties {
prefix: string;
server4: string;
client4: string;
flags: string;
coneNat: boolean;
microsoft: {
reserved: boolean;
universalLocal: boolean;
groupIndividual: boolean;
nonce: string;
};
udpPort: string;
}
/**
* Represents an IPv6 address
* @class Address6
* @param {string} address - An IPv6 address string
* @param {number} [groups=8] - How many octets to parse
* @example
* var address = new Address6('2001::/32');
*/
export class Address6 {
address4?: Address4;
address: string;
addressMinusSuffix: string = '';
elidedGroups?: number;
elisionBegin?: number;
elisionEnd?: number;
groups: number;
parsedAddress4?: string;
parsedAddress: string[];
parsedSubnet: string = '';
subnet: string = '/128';
subnetMask: number = 128;
v4: boolean = false;
zone: string = '';
constructor(address: string, optionalGroups?: number) {
if (optionalGroups === undefined) {
this.groups = constants6.GROUPS;
} else {
this.groups = optionalGroups;
}
this.address = address;
const subnet = constants6.RE_SUBNET_STRING.exec(address);
if (subnet) {
this.parsedSubnet = subnet[0].replace('/', '');
this.subnetMask = parseInt(this.parsedSubnet, 10);
this.subnet = `/${this.subnetMask}`;
if (
Number.isNaN(this.subnetMask) ||
this.subnetMask < 0 ||
this.subnetMask > constants6.BITS
) {
throw new AddressError('Invalid subnet mask.');
}
address = address.replace(constants6.RE_SUBNET_STRING, '');
} else if (/\//.test(address)) {
throw new AddressError('Invalid subnet mask.');
}
const zone = constants6.RE_ZONE_STRING.exec(address);
if (zone) {
this.zone = zone[0];
address = address.replace(constants6.RE_ZONE_STRING, '');
}
this.addressMinusSuffix = address;
this.parsedAddress = this.parse(this.addressMinusSuffix);
}
static isValid(address: string): boolean {
try {
// eslint-disable-next-line no-new
new Address6(address);
return true;
} catch (e) {
return false;
}
}
/**
* Convert a BigInt to a v6 address object
* @memberof Address6
* @static
* @param {bigint} bigInt - a BigInt to convert
* @returns {Address6}
* @example
* var bigInt = BigInt('1000000000000');
* var address = Address6.fromBigInt(bigInt);
* address.correctForm(); // '::e8:d4a5:1000'
*/
static fromBigInt(bigInt: bigint): Address6 {
const hex = bigInt.toString(16).padStart(32, '0');
const groups = [];
let i;
for (i = 0; i < constants6.GROUPS; i++) {
groups.push(hex.slice(i * 4, (i + 1) * 4));
}
return new Address6(groups.join(':'));
}
/**
* Convert a URL (with optional port number) to an address object
* @memberof Address6
* @static
* @param {string} url - a URL with optional port number
* @example
* var addressAndPort = Address6.fromURL('http://[ffff::]:8080/foo/');
* addressAndPort.address.correctForm(); // 'ffff::'
* addressAndPort.port; // 8080
*/
static fromURL(url: string) {
let host: string;
let port: string | number | null = null;
let result: string[] | null;
// If we have brackets parse them and find a port
if (url.indexOf('[') !== -1 && url.indexOf(']:') !== -1) {
result = constants6.RE_URL_WITH_PORT.exec(url);
if (result === null) {
return {
error: 'failed to parse address with port',
address: null,
port: null,
};
}
host = result[1];
port = result[2];
// If there's a URL extract the address
} else if (url.indexOf('/') !== -1) {
// Remove the protocol prefix
url = url.replace(/^[a-z0-9]+:\/\//, '');
// Parse the address
result = constants6.RE_URL.exec(url);
if (result === null) {
return {
error: 'failed to parse address from URL',
address: null,
port: null,
};
}
host = result[1];
// Otherwise just assign the URL to the host and let the library parse it
} else {
host = url;
}
// If there's a port convert it to an integer
if (port) {
port = parseInt(port, 10);
// squelch out of range ports
if (port < 0 || port > 65536) {
port = null;
}
} else {
// Standardize `undefined` to `null`
port = null;
}
return {
address: new Address6(host),
port,
};
}
/**
* Create an IPv6-mapped address given an IPv4 address
* @memberof Address6
* @static
* @param {string} address - An IPv4 address string
* @returns {Address6}
* @example
* var address = Address6.fromAddress4('192.168.0.1');
* address.correctForm(); // '::ffff:c0a8:1'
* address.to4in6(); // '::ffff:192.168.0.1'
*/
static fromAddress4(address: string): Address6 {
const address4 = new Address4(address);
const mask6 = constants6.BITS - (constants4.BITS - address4.subnetMask);
return new Address6(`::ffff:${address4.correctForm()}/${mask6}`);
}
/**
* Return an address from ip6.arpa form
* @memberof Address6
* @static
* @param {string} arpaFormAddress - an 'ip6.arpa' form address
* @returns {Adress6}
* @example
* var address = Address6.fromArpa(e.f.f.f.3.c.2.6.f.f.f.e.6.6.8.e.1.0.6.7.9.4.e.c.0.0.0.0.1.0.0.2.ip6.arpa.)
* address.correctForm(); // '2001:0:ce49:7601:e866:efff:62c3:fffe'
*/
static fromArpa(arpaFormAddress: string): Address6 {
// remove ending ".ip6.arpa." or just "."
let address = arpaFormAddress.replace(/(\.ip6\.arpa)?\.$/, '');
const semicolonAmount = 7;
// correct ip6.arpa form with ending removed will be 63 characters
if (address.length !== 63) {
throw new AddressError("Invalid 'ip6.arpa' form.");
}
const parts = address.split('.').reverse();
for (let i = semicolonAmount; i > 0; i--) {
const insertIndex = i * 4;
parts.splice(insertIndex, 0, ':');
}
address = parts.join('');
return new Address6(address);
}
/**
* Return the Microsoft UNC transcription of the address
* @memberof Address6
* @instance
* @returns {String} the Microsoft UNC transcription of the address
*/
microsoftTranscription(): string {
return `${this.correctForm().replace(/:/g, '-')}.ipv6-literal.net`;
}
/**
* Return the first n bits of the address, defaulting to the subnet mask
* @memberof Address6
* @instance
* @param {number} [mask=subnet] - the number of bits to mask
* @returns {String} the first n bits of the address as a string
*/
mask(mask: number = this.subnetMask): string {
return this.getBitsBase2(0, mask);
}
/**
* Return the number of possible subnets of a given size in the address
* @memberof Address6
* @instance
* @param {number} [subnetSize=128] - the subnet size
* @returns {String}
*/
// TODO: probably useful to have a numeric version of this too
possibleSubnets(subnetSize: number = 128): string {
const availableBits = constants6.BITS - this.subnetMask;
const subnetBits = Math.abs(subnetSize - constants6.BITS);
const subnetPowers = availableBits - subnetBits;
if (subnetPowers < 0) {
return '0';
}
return addCommas((BigInt('2') ** BigInt(subnetPowers)).toString(10));
}
/**
* Helper function getting start address.
* @memberof Address6
* @instance
* @returns {bigint}
*/
_startAddress(): bigint {
return BigInt(`0b${this.mask() + '0'.repeat(constants6.BITS - this.subnetMask)}`);
}
/**
* The first address in the range given by this address' subnet
* Often referred to as the Network Address.
* @memberof Address6
* @instance
* @returns {Address6}
*/
startAddress(): Address6 {
return Address6.fromBigInt(this._startAddress());
}
/**
* The first host address in the range given by this address's subnet ie
* the first address after the Network Address
* @memberof Address6
* @instance
* @returns {Address6}
*/
startAddressExclusive(): Address6 {
const adjust = BigInt('1');
return Address6.fromBigInt(this._startAddress() + adjust);
}
/**
* Helper function getting end address.
* @memberof Address6
* @instance
* @returns {bigint}
*/
_endAddress(): bigint {
return BigInt(`0b${this.mask() + '1'.repeat(constants6.BITS - this.subnetMask)}`);
}
/**
* The last address in the range given by this address' subnet
* Often referred to as the Broadcast
* @memberof Address6
* @instance
* @returns {Address6}
*/
endAddress(): Address6 {
return Address6.fromBigInt(this._endAddress());
}
/**
* The last host address in the range given by this address's subnet ie
* the last address prior to the Broadcast Address
* @memberof Address6
* @instance
* @returns {Address6}
*/
endAddressExclusive(): Address6 {
const adjust = BigInt('1');
return Address6.fromBigInt(this._endAddress() - adjust);
}
/**
* Return the scope of the address
* @memberof Address6
* @instance
* @returns {String}
*/
getScope(): string {
let scope = constants6.SCOPES[parseInt(this.getBits(12, 16).toString(10), 10)];
if (this.getType() === 'Global unicast' && scope !== 'Link local') {
scope = 'Global';
}
return scope || 'Unknown';
}
/**
* Return the type of the address
* @memberof Address6
* @instance
* @returns {String}
*/
getType(): string {
for (const subnet of Object.keys(constants6.TYPES)) {
if (this.isInSubnet(new Address6(subnet))) {
return constants6.TYPES[subnet] as string;
}
}
return 'Global unicast';
}
/**
* Return the bits in the given range as a BigInt
* @memberof Address6
* @instance
* @returns {bigint}
*/
getBits(start: number, end: number): bigint {
return BigInt(`0b${this.getBitsBase2(start, end)}`);
}
/**
* Return the bits in the given range as a base-2 string
* @memberof Address6
* @instance
* @returns {String}
*/
getBitsBase2(start: number, end: number): string {
return this.binaryZeroPad().slice(start, end);
}
/**
* Return the bits in the given range as a base-16 string
* @memberof Address6
* @instance
* @returns {String}
*/
getBitsBase16(start: number, end: number): string {
const length = end - start;
if (length % 4 !== 0) {
throw new Error('Length of bits to retrieve must be divisible by four');
}
return this.getBits(start, end)
.toString(16)
.padStart(length / 4, '0');
}
/**
* Return the bits that are set past the subnet mask length
* @memberof Address6
* @instance
* @returns {String}
*/
getBitsPastSubnet(): string {
return this.getBitsBase2(this.subnetMask, constants6.BITS);
}
/**
* Return the reversed ip6.arpa form of the address
* @memberof Address6
* @param {Object} options
* @param {boolean} options.omitSuffix - omit the "ip6.arpa" suffix
* @instance
* @returns {String}
*/
reverseForm(options?: common.ReverseFormOptions): string {
if (!options) {
options = {};
}
const characters = Math.floor(this.subnetMask / 4);
const reversed = this.canonicalForm()
.replace(/:/g, '')
.split('')
.slice(0, characters)
.reverse()
.join('.');
if (characters > 0) {
if (options.omitSuffix) {
return reversed;
}
return `${reversed}.ip6.arpa.`;
}
if (options.omitSuffix) {
return '';
}
return 'ip6.arpa.';
}
/**
* Return the correct form of the address
* @memberof Address6
* @instance
* @returns {String}
*/
correctForm(): string {
let i;
let groups = [];
let zeroCounter = 0;
const zeroes = [];
for (i = 0; i < this.parsedAddress.length; i++) {
const value = parseInt(this.parsedAddress[i], 16);
if (value === 0) {
zeroCounter++;
}
if (value !== 0 && zeroCounter > 0) {
if (zeroCounter > 1) {
zeroes.push([i - zeroCounter, i - 1]);
}
zeroCounter = 0;
}
}
// Do we end with a string of zeroes?
if (zeroCounter > 1) {
zeroes.push([this.parsedAddress.length - zeroCounter, this.parsedAddress.length - 1]);
}
const zeroLengths = zeroes.map((n) => n[1] - n[0] + 1);
if (zeroes.length > 0) {
const index = zeroLengths.indexOf(Math.max(...zeroLengths) as number);
groups = compact(this.parsedAddress, zeroes[index]);
} else {
groups = this.parsedAddress;
}
for (i = 0; i < groups.length; i++) {
if (groups[i] !== 'compact') {
groups[i] = parseInt(groups[i], 16).toString(16);
}
}
let correct = groups.join(':');
correct = correct.replace(/^compact$/, '::');
correct = correct.replace(/(^compact)|(compact$)/, ':');
correct = correct.replace(/compact/, '');
return correct;
}
/**
* Return a zero-padded base-2 string representation of the address
* @memberof Address6
* @instance
* @returns {String}
* @example
* var address = new Address6('2001:4860:4001:803::1011');
* address.binaryZeroPad();
* // '0010000000000001010010000110000001000000000000010000100000000011
* // 0000000000000000000000000000000000000000000000000001000000010001'
*/
binaryZeroPad(): string {
return this.bigInt().toString(2).padStart(constants6.BITS, '0');
}
// TODO: Improve the semantics of this helper function
parse4in6(address: string): string {
const groups = address.split(':');
const lastGroup = groups.slice(-1)[0];
const address4 = lastGroup.match(constants4.RE_ADDRESS);
if (address4) {
this.parsedAddress4 = address4[0];
this.address4 = new Address4(this.parsedAddress4);
for (let i = 0; i < this.address4.groups; i++) {
if (/^0[0-9]+/.test(this.address4.parsedAddress[i])) {
throw new AddressError(
"IPv4 addresses can't have leading zeroes.",
address.replace(
constants4.RE_ADDRESS,
this.address4.parsedAddress.map(spanLeadingZeroes4).join('.'),
),
);
}
}
this.v4 = true;
groups[groups.length - 1] = this.address4.toGroup6();
address = groups.join(':');
}
return address;
}
// TODO: Make private?
parse(address: string): string[] {
address = this.parse4in6(address);
const badCharacters = address.match(constants6.RE_BAD_CHARACTERS);
if (badCharacters) {
throw new AddressError(
`Bad character${
badCharacters.length > 1 ? 's' : ''
} detected in address: ${badCharacters.join('')}`,
address.replace(constants6.RE_BAD_CHARACTERS, '<span class="parse-error">$1</span>'),
);
}
const badAddress = address.match(constants6.RE_BAD_ADDRESS);
if (badAddress) {
throw new AddressError(
`Address failed regex: ${badAddress.join('')}`,
address.replace(constants6.RE_BAD_ADDRESS, '<span class="parse-error">$1</span>'),
);
}
let groups: string[] = [];
const halves = address.split('::');
if (halves.length === 2) {
let first = halves[0].split(':');
let last = halves[1].split(':');
if (first.length === 1 && first[0] === '') {
first = [];
}
if (last.length === 1 && last[0] === '') {
last = [];
}
const remaining = this.groups - (first.length + last.length);
if (!remaining) {
throw new AddressError('Error parsing groups');
}
this.elidedGroups = remaining;
this.elisionBegin = first.length;
this.elisionEnd = first.length + this.elidedGroups;
groups = groups.concat(first);
for (let i = 0; i < remaining; i++) {
groups.push('0');
}
groups = groups.concat(last);
} else if (halves.length === 1) {
groups = address.split(':');
this.elidedGroups = 0;
} else {
throw new AddressError('Too many :: groups found');
}
groups = groups.map((group: string) => parseInt(group, 16).toString(16));
if (groups.length !== this.groups) {
throw new AddressError('Incorrect number of groups found');
}
return groups;
}
/**
* Return the canonical form of the address
* @memberof Address6
* @instance
* @returns {String}
*/
canonicalForm(): string {
return this.parsedAddress.map(paddedHex).join(':');
}
/**
* Return the decimal form of the address
* @memberof Address6
* @instance
* @returns {String}
*/
decimal(): string {
retu