Created
October 30, 2019 03:26
-
-
Save nobiit/621e399a405018ebd84c1d8482f30daa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// A list of regular expressions that match arbitrary IPv4 addresses, | |
// for which a number of weird notations exist. | |
// Note that an address like 0010.0xa5.1.1 is considered legal. | |
const ipv4Part = '(0?\\d+|0x[a-f0-9]+)'; | |
const ipv4Regexes = { | |
fourOctet: new RegExp(`^${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}$`, 'i'), | |
longValue: new RegExp(`^${ipv4Part}$`, 'i') | |
}; | |
const zoneIndex = '%[0-9a-z]{1,}'; | |
// IPv6-matching regular expressions. | |
// For IPv6, the task is simpler: it is enough to match the colon-delimited | |
// hexadecimal IPv6 and a transitional variant with dotted-decimal IPv4 at | |
// the end. | |
const ipv6Part = '(?:[0-9a-f]+::?)+'; | |
const ipv6Regexes = { | |
zoneIndex: new RegExp(zoneIndex, 'i'), | |
'native': new RegExp(`^(::)?(${ipv6Part})?([0-9a-f]+)?(::)?(${zoneIndex})?$`, 'i'), | |
transitional: new RegExp(`^((?:${ipv6Part})|(?:::)(?:${ipv6Part})?)${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}\\.${ipv4Part}(${zoneIndex})?$`, 'i') | |
}; | |
interface IpParts { | |
parts: number[]; | |
zoneId: string; | |
} | |
// Expand :: in an IPv6 address or address part consisting of `parts` groups. | |
const expandIPv6 = (str: string, partSize: number): IpParts | null => { | |
// More than one '::' means invalid adddress | |
if (str.indexOf('::') !== str.lastIndexOf('::')) { | |
return null; | |
} | |
let colonCount = 0; | |
let lastColon = -1; | |
let zoneId = (str.match(ipv6Regexes.zoneIndex) || [])[0]; | |
// Remove zone index and save it for later | |
if (zoneId) { | |
zoneId = zoneId.substr(1); | |
str = str.replace(/%.+$/, ''); | |
} | |
// How many parts do we already have? | |
while ((lastColon = str.indexOf(':', lastColon + 1)) >= 0) { | |
colonCount++; | |
} | |
// 0::0 is two parts more than :: | |
if (str.substr(0, 2) === '::') { | |
colonCount--; | |
} | |
if (str.substr(-2, 2) === '::') { | |
colonCount--; | |
} | |
// The following loop would hang if colonCount > parts | |
if (colonCount > partSize) { | |
return null; | |
} | |
// replacement = ':' + '0:' * (parts - colonCount) | |
let replacementCount = partSize - colonCount; | |
let replacement = ':'; | |
while (replacementCount--) { | |
replacement += '0:'; | |
} | |
// Insert the missing zeroes | |
str = str.replace('::', replacement); | |
// Trim any garbage which may be hanging around if :: was at the edge in | |
// the source strin | |
if (str[0] === ':') { | |
str = str.slice(1); | |
} | |
if (str[str.length - 1] === ':') { | |
str = str.slice(0, -1); | |
} | |
let parts = (() => { | |
const ref = str.split(':'); | |
const results = []; | |
for (let i = 0; i < ref.length; i++) { | |
results.push(parseInt(ref[i], 16)); | |
} | |
return results; | |
})(); | |
return { | |
parts: parts, | |
zoneId: zoneId | |
}; | |
}; | |
// A generic CIDR (Classless Inter-Domain Routing) RFC1518 range matcher. | |
const matchCIDR = (first: number[], second: number[], partSize: number, cidrBits: number): boolean => { | |
if (first.length !== second.length) { | |
throw new Error('IpAddr: cannot match CIDR for objects with different lengths'); | |
} | |
let part = 0; | |
while (cidrBits > 0) { | |
let shift = partSize - cidrBits; | |
if (shift < 0) { | |
shift = 0; | |
} | |
if (first[part] >> shift !== second[part] >> shift) { | |
return false; | |
} | |
cidrBits -= partSize; | |
part += 1; | |
} | |
return true; | |
}; | |
const parseIntAuto = (str: string): number => { | |
if (str[0] === '0' && str[1] !== 'x') { | |
return parseInt(str, 8); | |
} else { | |
return parseInt(str); | |
} | |
}; | |
const padPart = (part: string, length: number): string => { | |
while (part.length < length) { | |
part = `0${part}`; | |
} | |
return part; | |
}; | |
interface Ip { | |
kind(): string; | |
toString(): string; | |
toNormalizedString(): string; | |
toByteArray(): number[]; | |
match(other: SpecialRange<Ip>): boolean; | |
range(): string; | |
} | |
class SpecialRange<T> { | |
constructor( | |
public readonly block: T, | |
public readonly cidr: number, | |
) { | |
} | |
toString(): string { | |
return [this.block, this.cidr].join('/'); | |
} | |
} | |
interface SpecialRanges<T> { | |
[range: string]: SpecialRange<T>[] | undefined; | |
unspecified: SpecialRange<T>[]; | |
broadcast?: SpecialRange<T>[]; | |
linkLocal: SpecialRange<T>[]; | |
multicast: SpecialRange<T>[]; | |
loopback: SpecialRange<T>[]; | |
carrierGradeNat?: SpecialRange<T>[]; | |
private?: SpecialRange<T>[]; | |
uniqueLocal?: SpecialRange<T>[]; | |
ipv4Mapped?: SpecialRange<T>[]; | |
rfc6145?: SpecialRange<T>[]; | |
rfc6052?: SpecialRange<T>[]; | |
'6to4'?: SpecialRange<T>[]; | |
teredo?: SpecialRange<T>[]; | |
reserved: SpecialRange<T>[]; | |
} | |
// An IPv4 address (RFC791). | |
class IPv4 implements Ip { | |
public readonly octets: number[]; | |
// Constructs a new IPv4 address from an array of four octets | |
// in network order (MSB first) | |
// Verifies the input. | |
constructor(octets: number[]) { | |
if (octets.length !== 4) { | |
throw new Error('IpAddr: ipv4 octet count should be 4'); | |
} | |
for (let i = 0; i < octets.length; i++) { | |
let octet = octets[i]; | |
if (!((0 <= octet && octet <= 255))) { | |
throw new Error('IpAddr: ipv4 octet should fit in 8 bits'); | |
} | |
} | |
this.octets = octets; | |
} | |
// The 'kind' method exists on both IPv4 and IPv6 classes. | |
kind(): string { | |
return 'ipv4'; | |
}; | |
// Returns the address in convenient, decimal-dotted format. | |
toString(): string { | |
return this.octets.join('.'); | |
}; | |
// Symmetrical method strictly for aligning with the IPv6 methods. | |
toNormalizedString(): string { | |
return this.toString(); | |
}; | |
// Returns an array of byte-sized values in network order (MSB first) | |
toByteArray(): number[] { | |
return this.octets.slice(0); | |
}; | |
// Checks if this address matches other one within given CIDR range. | |
match(other: SpecialRange<IPv4>): boolean { | |
return matchCIDR(this.octets, other.block.octets, 8, other.cidr); | |
} | |
// Special IPv4 address ranges. | |
// See also https://en.wikipedia.org/wiki/Reserved_IP_addresses | |
_SpecialRanges: SpecialRanges<IPv4> = { | |
unspecified: [new SpecialRange(new IPv4([0, 0, 0, 0]), 8)], | |
broadcast: [new SpecialRange(new IPv4([255, 255, 255, 255]), 32)], | |
// RFC3171 | |
multicast: [new SpecialRange(new IPv4([224, 0, 0, 0]), 4)], | |
// RFC3927 | |
linkLocal: [new SpecialRange(new IPv4([169, 254, 0, 0]), 16)], | |
// RFC5735 | |
loopback: [new SpecialRange(new IPv4([127, 0, 0, 0]), 8)], | |
// RFC6598 | |
carrierGradeNat: [new SpecialRange(new IPv4([100, 64, 0, 0]), 10)], | |
// RFC1918 | |
private: [ | |
new SpecialRange(new IPv4([10, 0, 0, 0]), 8), | |
new SpecialRange(new IPv4([172, 16, 0, 0]), 12), | |
new SpecialRange(new IPv4([192, 168, 0, 0]), 16), | |
], | |
// Reserved and testing-only ranges; RFCs 5735, 5737, 2544, 1700 | |
reserved: [ | |
new SpecialRange(new IPv4([192, 0, 0, 0]), 24), | |
new SpecialRange(new IPv4([192, 0, 2, 0]), 24), | |
new SpecialRange(new IPv4([192, 88, 99, 0]), 24), | |
new SpecialRange(new IPv4([198, 51, 100, 0]), 24), | |
new SpecialRange(new IPv4([203, 0, 113, 0]), 24), | |
new SpecialRange(new IPv4([240, 0, 0, 0]), 4), | |
] | |
}; | |
// Checks if the address corresponds to one of the special ranges. | |
range(): string { | |
return IpAddr.subnetMatch(this, this._SpecialRanges); | |
}; | |
// Converts this IPv4 address to an IPv4-mapped IPv6 address. | |
// noinspection JSUnusedGlobalSymbols | |
toIPv4MappedAddress(): IPv6 { | |
return IPv6.parse(`::ffff:${this.toString()}`); | |
}; | |
// returns a number of leading ones in IPv4 address, making sure that | |
// the rest is a solid sequence of 0's (valid netmask) | |
// returns either the CIDR length or null if mask is not valid | |
// noinspection JSUnusedGlobalSymbols | |
prefixLengthFromSubnetMask(): number | null { | |
let cidr = 0; | |
// non-zero encountered stop scanning for zeroes | |
let stop = false; | |
// number of zeroes in octet | |
const zerotable = [255, 254, 252, 248, 240, 224, 192, 128, 0]; | |
for (let i = 3; i >= 0; i -= 1) { | |
let octet = this.octets[i]; | |
let zeros = zerotable.indexOf(octet); | |
if (~zeros) { | |
if (stop && zeros !== 0) { | |
return null; | |
} | |
if (zeros !== 8) { | |
stop = true; | |
} | |
cidr += zeros; | |
} else { | |
return null; | |
} | |
} | |
return 32 - cidr; | |
}; | |
// Classful variants (like a.b, where a is an octet, and b is a 24-bit | |
// value representing last three octets; this corresponds to a class C | |
// address) are omitted due to classless nature of modern Internet. | |
static parser(str: string): number[] | null { | |
// parseInt recognizes all that octal & hexadecimal weirdness for us | |
let match = str.match(ipv4Regexes.fourOctet); | |
if (match) { | |
return (function () { | |
const ref = match.slice(1, 6); | |
const results = []; | |
for (let i = 0; i < ref.length; i++) { | |
let part = ref[i]; | |
results.push(parseIntAuto(part)); | |
} | |
return results; | |
})(); | |
} else { | |
match = str.match(ipv4Regexes.longValue); | |
if (match) { | |
let value = parseIntAuto(match[1]); | |
if (value > 0xffffffff || value < 0) { | |
throw new Error('IpAddr: address outside defined range'); | |
} | |
return ((function () { | |
const results = []; | |
for (let shift = 0; shift <= 24; shift += 8) { | |
results.push((value >> shift) & 0xff); | |
} | |
return results; | |
})()).reverse(); | |
} else { | |
return null; | |
} | |
} | |
} | |
// Checks if a given string is formatted like IPv4 address. | |
// noinspection JSUnusedGlobalSymbols | |
static isIPv4(str: string): boolean { | |
return this.parser(str) !== null; | |
} | |
// A utility function to return network address given the IPv4 interface and prefix length in CIDR notation | |
// noinspection JSUnusedGlobalSymbols | |
static networkAddressFromCIDR(str: string): IPv4 { | |
try { | |
let specialRange = this.parseCIDR(str); | |
let ipInterfaceOctets = specialRange.block.toByteArray(); | |
let subnetMaskOctets = this.subnetMaskFromPrefixLength(specialRange.cidr).toByteArray(); | |
let octets = []; | |
for (let i = 0; i < 4; i++) { | |
// Network address is bitwise AND between ip interface and mask | |
octets.push(~~ipInterfaceOctets[i] & ~~subnetMaskOctets[i]); | |
} | |
return new IPv4(octets); | |
} catch (e) { | |
throw new Error('IpAddr: the address does not have IPv4 CIDR format'); | |
} | |
} | |
// A utility function to return broadcast address given the IPv4 interface and prefix length in CIDR notation | |
// noinspection JSUnusedGlobalSymbols | |
static broadcastAddressFromCIDR(str: string): IPv4 { | |
try { | |
const specialRange = this.parseCIDR(str); | |
const ipInterfaceOctets = specialRange.block.toByteArray(); | |
const subnetMaskOctets = this.subnetMaskFromPrefixLength(specialRange.cidr).toByteArray(); | |
const octets = []; | |
let i = 0; | |
while (i < 4) { | |
// Broadcast address is bitwise OR between ip interface and inverted mask | |
octets.push(~~ipInterfaceOctets[i] | ~~subnetMaskOctets[i] ^ 255); | |
i++; | |
} | |
return new IPv4(octets); | |
} catch (e) { | |
throw new Error('IpAddr: the address does not have IPv4 CIDR format'); | |
} | |
} | |
// A utility function to return subnet mask in IPv4 format given the prefix length | |
static subnetMaskFromPrefixLength(prefix: number): IPv4 { | |
prefix = ~~prefix; | |
if (prefix < 0 || prefix > 32) { | |
throw new Error('IpAddr: invalid IPv4 prefix length'); | |
} | |
const octets = [0, 0, 0, 0]; | |
let j = 0; | |
const filledOctetCount = Math.floor(prefix / 8); | |
while (j < filledOctetCount) { | |
octets[j] = 255; | |
j++; | |
} | |
if (filledOctetCount < 4) { | |
octets[filledOctetCount] = Math.pow(2, prefix % 8) - 1 << 8 - (prefix % 8); | |
} | |
return new IPv4(octets); | |
} | |
static parseCIDR(str: string): SpecialRange<IPv4> { | |
let match = str.match(/^(.+)\/(\d+)$/); | |
if (match) { | |
const maskLength = parseInt(match[2]); | |
if (maskLength >= 0 && maskLength <= 32) { | |
return new SpecialRange(this.parse(match[1]), maskLength); | |
} | |
} | |
throw new Error('IpAddr: string is not formatted like an IPv4 CIDR range'); | |
} | |
// Checks if a given string is a valid IPv4/IPv6 address. | |
static isValid(str: string): boolean { | |
try { | |
this.parse(str); | |
return true; | |
} catch (e) { | |
return false; | |
} | |
} | |
// noinspection JSUnusedGlobalSymbols | |
static isValidFourPartDecimal(str: string): boolean { | |
return !!(IPv4.isValid(str) && str.match(/^(0|[1-9]\d*)(\.(0|[1-9]\d*)){3}$/)); | |
} | |
// Tries to parse and validate a string with IPv4 address. | |
// Throws an error if it fails. | |
static parse(str: string): IPv4 { | |
const parts = this.parser(str); | |
if (parts === null) { | |
throw new Error('IpAddr: string is not formatted like ip address'); | |
} | |
return new IPv4(parts); | |
} | |
} | |
// An IPv6 address (RFC2460) | |
class IPv6 implements Ip { | |
private readonly parts: number[]; | |
private readonly zoneId?: string; | |
// Constructs an IPv6 address from an array of eight 16 - bit parts | |
// or sixteen 8 - bit parts in network order(MSB first). | |
// Throws an error if the input is invalid. | |
constructor(parts: number[], zoneId?: string) { | |
if (parts.length === 16) { | |
this.parts = []; | |
for (let i = 0; i <= 14; i += 2) { | |
this.parts.push((parts[i] << 8) | parts[i + 1]); | |
} | |
} else if (parts.length === 8) { | |
this.parts = parts; | |
} else { | |
throw new Error('IpAddr: ipv6 part count should be 8 or 16'); | |
} | |
for (let i = 0; i < this.parts.length; i++) { | |
let part = this.parts[i]; | |
if (!((0 <= part && part <= 0xffff))) { | |
throw new Error('IpAddr: ipv6 part should fit in 16 bits'); | |
} | |
} | |
if (zoneId) { | |
this.zoneId = zoneId; | |
} | |
} | |
// The 'kind' method exists on both IPv4 and IPv6 classes. | |
kind(): string { | |
return 'ipv6'; | |
}; | |
// Returns the address in compact, human-readable format like | |
// 2001:db8:8:66::1 | |
// | |
// Deprecated: use toRFC5952String() instead. | |
toString(): string { | |
// Replace the first sequence of 1 or more '0' parts with '::' | |
return this.toNormalizedString().replace(/((^|:)(0(:|$))+)/, '::'); | |
}; | |
// Returns the address in compact, human-readable format like | |
// 2001:db8:8:66::1 | |
// in line with RFC 5952 (see https://tools.ietf.org/html/rfc5952#section-4) | |
// noinspection JSUnusedGlobalSymbols | |
toRFC5952String() { | |
const regex = /((^|:)(0(:|$)){2,})/g; | |
const str = this.toNormalizedString(); | |
let bestMatchIndex = 0; | |
let bestMatchLength = -1; | |
while (str) { | |
let match = regex.exec(str); | |
if (!match) break; | |
if (match[0].length > bestMatchLength) { | |
bestMatchIndex = match.index; | |
bestMatchLength = match[0].length; | |
} | |
} | |
if (bestMatchLength < 0) { | |
return str; | |
} | |
return `${str.substr(0, bestMatchIndex)}::${str.substr(bestMatchIndex + bestMatchLength)}`; | |
}; | |
// Returns an array of byte-sized values in network order (MSB first) | |
toByteArray(): number[] { | |
const bytes = []; | |
const ref = this.parts; | |
for (let i = 0; i < ref.length; i++) { | |
let part = ref[i]; | |
bytes.push(part >> 8); | |
bytes.push(part & 0xff); | |
} | |
return bytes; | |
}; | |
// Returns the address in expanded format with all zeroes included, like | |
// 2001:db8:8:66:0:0:0:1 | |
// | |
// Deprecated: use toFixedLengthString() instead. | |
toNormalizedString(): string { | |
const addr = (() => { | |
const results = []; | |
for (let i = 0; i < this.parts.length; i++) { | |
results.push(this.parts[i].toString(16)); | |
} | |
return results; | |
})().join(':'); | |
let suffix = ''; | |
if (this.zoneId) { | |
suffix = `%${this.zoneId}`; | |
} | |
return addr + suffix; | |
}; | |
// Returns the address in expanded format with all zeroes included, like | |
// 2001:0db8:0008:0066:0000:0000:0000:0001 | |
// noinspection JSUnusedGlobalSymbols | |
toFixedLengthString() { | |
const addr = ((() => { | |
const results = []; | |
for (let i = 0; i < this.parts.length; i++) { | |
results.push(padPart(this.parts[i].toString(16), 4)); | |
} | |
return results; | |
})()).join(':'); | |
let suffix = ''; | |
if (this.zoneId) { | |
suffix = `%${this.zoneId}`; | |
} | |
return addr + suffix; | |
}; | |
// Checks if this address matches other one within given CIDR range. | |
match(other: SpecialRange<IPv6>): boolean { | |
return matchCIDR(this.parts, other.block.parts, 16, other.cidr); | |
}; | |
// Special IPv6 ranges | |
_SpecialRanges: SpecialRanges<IPv6> = { | |
// RFC4291, here and after | |
unspecified: [new SpecialRange(new IPv6([0, 0, 0, 0, 0, 0, 0, 0]), 128)], | |
linkLocal: [new SpecialRange(new IPv6([0xfe80, 0, 0, 0, 0, 0, 0, 0]), 10)], | |
multicast: [new SpecialRange(new IPv6([0xff00, 0, 0, 0, 0, 0, 0, 0]), 8)], | |
loopback: [new SpecialRange(new IPv6([0, 0, 0, 0, 0, 0, 0, 1]), 128)], | |
uniqueLocal: [new SpecialRange(new IPv6([0xfc00, 0, 0, 0, 0, 0, 0, 0]), 7)], | |
ipv4Mapped: [new SpecialRange(new IPv6([0, 0, 0, 0, 0, 0xffff, 0, 0]), 96)], | |
// RFC6145 | |
rfc6145: [new SpecialRange(new IPv6([0, 0, 0, 0, 0xffff, 0, 0, 0]), 96)], | |
// RFC6052 | |
rfc6052: [new SpecialRange(new IPv6([0x64, 0xff9b, 0, 0, 0, 0, 0, 0]), 96)], | |
// RFC3056 | |
'6to4': [new SpecialRange(new IPv6([0x2002, 0, 0, 0, 0, 0, 0, 0]), 16)], | |
// RFC6052, RFC6146 | |
teredo: [new SpecialRange(new IPv6([0x2001, 0, 0, 0, 0, 0, 0, 0]), 32)], | |
// RFC4291 | |
reserved: [new SpecialRange(new IPv6([0x2001, 0xdb8, 0, 0, 0, 0, 0, 0]), 32)] | |
}; | |
// Checks if the address corresponds to one of the special ranges. | |
range(): string { | |
return IpAddr.subnetMatch(this, this._SpecialRanges); | |
}; | |
// Checks if this address is an IPv4-mapped IPv6 address. | |
isIPv4MappedAddress() { | |
return this.range() === 'ipv4Mapped'; | |
}; | |
// Converts this address to IPv4 address if it is an IPv4-mapped IPv6 address. | |
// Throws an error otherwise. | |
// noinspection JSUnusedGlobalSymbols | |
toIPv4Address() { | |
if (!this.isIPv4MappedAddress()) { | |
throw new Error('IpAddr: trying to convert a generic ipv6 address to ipv4'); | |
} | |
const ref = this.parts.slice(-2); | |
const high = ref[0]; | |
const low = ref[1]; | |
return new IPv4([high >> 8, high & 0xff, low >> 8, low & 0xff]); | |
} | |
// returns a number of leading ones in IPv6 address, making sure that | |
// the rest is a solid sequence of 0's (valid netmask) | |
// returns either the CIDR length or null if mask is not valid | |
// noinspection JSUnusedGlobalSymbols | |
prefixLengthFromSubnetMask() { | |
let cidr = 0; | |
// non-zero encountered stop scanning for zeroes | |
let stop = false; | |
// number of zeroes in octet | |
const zerotable = [65535, 65534, 65532, 65528, 65520, 65504, 65472, 65408, 65280, 65024, 64512, 63488, 61440, 57344, 49152, 32768, 0]; | |
for (let i = 7; i >= 0; i -= 1) { | |
let part = this.parts[i]; | |
let zeros = zerotable.indexOf(part); | |
if (~zeros) { | |
if (stop && zeros !== 0) { | |
return null; | |
} | |
if (zeros !== 16) { | |
stop = true; | |
} | |
cidr += zeros; | |
} else { | |
return null; | |
} | |
} | |
return 128 - cidr; | |
}; | |
// Parse an IPv6 address. | |
static parser(str: string) { | |
if (ipv6Regexes.native.test(str)) { | |
return expandIPv6(str, 8); | |
} else { | |
let match = str.match(ipv6Regexes.transitional); | |
if (match) { | |
let zoneId = match[6] || ''; | |
let addr = expandIPv6(match[1].slice(0, -1) + zoneId, 6); | |
if (addr && addr.parts) { | |
let octets = [ | |
parseInt(match[2]), | |
parseInt(match[3]), | |
parseInt(match[4]), | |
parseInt(match[5]) | |
]; | |
for (let i = 0; i < octets.length; i++) { | |
let octet = octets[i]; | |
if (!((0 <= octet && octet <= 255))) { | |
return null; | |
} | |
} | |
addr.parts.push(octets[0] << 8 | octets[1]); | |
addr.parts.push(octets[2] << 8 | octets[3]); | |
return { | |
parts: addr.parts, | |
zoneId: addr.zoneId | |
}; | |
} | |
} | |
} | |
return null; | |
} | |
// Checks if a given string is formatted like IPv6 address. | |
static isIPv6(str: string) { | |
return this.parser(str) !== null; | |
} | |
static isValid(str: string) { | |
// Since IPv6.isValid is always called first, this shortcut | |
// provides a substantial performance gain. | |
if (~str.indexOf(':')) { | |
return false; | |
} | |
try { | |
this.parse(str); | |
return true; | |
} catch (e) { | |
return false; | |
} | |
} | |
// Tries to parse and validate a string with IPv6 address. | |
// Throws an error if it fails. | |
static parse(str: string) { | |
const addr = this.parser(str); | |
if (!addr || addr.parts === null) { | |
throw new Error('IpAddr: string is not formatted like ip address'); | |
} | |
return new IPv6(addr.parts, addr.zoneId); | |
} | |
static parseCIDR(str: string) { | |
let maskLength, match, parsed; | |
if ((match = str.match(/^(.+)\/(\d+)$/))) { | |
maskLength = parseInt(match[2]); | |
if (maskLength >= 0 && maskLength <= 128) { | |
parsed = [this.parse(match[1]), maskLength]; | |
Object.defineProperty(parsed, 'toString', { | |
value: function () { | |
return this.join('/'); | |
} | |
}); | |
return parsed; | |
} | |
} | |
throw new Error('IpAddr: string is not formatted like an IPv6 CIDR range'); | |
} | |
} | |
class IpAddr { | |
// An utility function to ease named range matching. See examples below. | |
// rangeList can contain both IPv4 and IPv6 subnet entries and will not throw errors | |
// on matching IPv4 addresses to IPv6 ranges or vice versa. | |
static subnetMatch(address: Ip, rangeList: SpecialRanges<Ip>, defaultName?: string): string { | |
if (defaultName === undefined || defaultName === null) { | |
defaultName = 'unicast'; | |
} | |
for (let rangeName in rangeList) { | |
if (rangeList.hasOwnProperty(rangeName)) { | |
let rangeSubnets = rangeList[rangeName]; | |
if (!rangeSubnets) continue; | |
for (let i = 0; i < rangeSubnets.length; i++) { | |
let subnet = rangeSubnets[i]; | |
if (subnet && address.kind() === subnet.block.kind() && address.match(subnet)) { | |
return rangeName; | |
} | |
} | |
} | |
} | |
return defaultName; | |
}; | |
// Checks if the address is valid IP address | |
// noinspection JSUnusedGlobalSymbols | |
static isValid(str: string) { | |
return IPv6.isValid(str) || IPv4.isValid(str); | |
}; | |
// Try to parse an address and throw an error if it is impossible | |
static parse(str: string) { | |
if (IPv6.isValid(str)) { | |
return IPv6.parse(str); | |
} else if (IPv4.isValid(str)) { | |
return IPv4.parse(str); | |
} else { | |
throw new Error('IpAddr: the address has neither IPv6 nor IPv4 format'); | |
} | |
}; | |
// noinspection JSUnusedGlobalSymbols | |
static parseCIDR(str: string) { | |
try { | |
return IPv6.parseCIDR(str); | |
} catch (e) { | |
try { | |
return IPv4.parseCIDR(str); | |
} catch (e2) { | |
throw new Error('IpAddr: the address has neither IPv6 nor IPv4 CIDR format'); | |
} | |
} | |
}; | |
// Try to parse an array in network order (MSB first) for IPv4 and IPv6 | |
// noinspection JSUnusedGlobalSymbols | |
static fromByteArray(bytes: number[]): Ip { | |
const length = bytes.length; | |
if (length === 4) { | |
return new IPv4(bytes); | |
} else if (length === 16) { | |
return new IPv6(bytes); | |
} else { | |
throw new Error('IpAddr: the binary input is neither an IPv6 nor IPv4 address'); | |
} | |
}; | |
// Parse an address and return plain IPv4 address if it is an IPv4-mapped address | |
// noinspection JSUnusedGlobalSymbols | |
static process(str: string) { | |
const addr = this.parse(str); | |
if (addr instanceof IPv6 && addr.isIPv4MappedAddress()) { | |
return addr.toIPv4Address(); | |
} else { | |
return addr; | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment