UNPKG

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
/* 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