Skip to content

Instantly share code, notes, and snippets.

@nobiit
Created October 30, 2019 03:26
Show Gist options
  • Save nobiit/621e399a405018ebd84c1d8482f30daa to your computer and use it in GitHub Desktop.
Save nobiit/621e399a405018ebd84c1d8482f30daa to your computer and use it in GitHub Desktop.
// 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