UNPKG

ip-sub

Version:

A set of utilities for IP/prefix validation and subnet matching.

598 lines (483 loc) 19.4 kB
const {IpAddress, IpRange} = require("cidr-calc"); const cache = { v4: {}, v6: {} }; const spaceConfig = { v4: { blockSize: 8, ipSizeCheck: 32, splitChar: ".", blockMax: 255, bits: 32, regex: new RegExp("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") }, v6: { blockSize: 16, ipSizeCheck: 64, splitChar: ":", blockMax: "ffff", bits: 128, regex: new RegExp("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$") } }; const ip = { getIpAndCidr: function (prefix) { let bits, ip; for (let n = prefix.length - 1; n >= 0; n--) { if (prefix[n] == "/") { ip = prefix.slice(0, n); bits = prefix.slice(n + 1); if (bits !== "" && !isNaN(bits)) { return [ip, parseInt(bits)]; } } } throw new Error("Not valid prefix"); }, isValidPrefix: function (prefix) { try { return this._isValidPrefix(prefix, this.getAddressFamily(prefix)); } catch (error) { return false; } }, _isValidPrefix: function (prefix, af) { let bits, ip; if (prefix !== prefix.trim()) { return false; } try { const components = this.getIpAndCidr(prefix); ip = components[0]; bits = components[1]; if (af === 4) { return this._isValidIP(ip, af) && (bits >= 0 && bits <= spaceConfig.v4.bits); } else { return this._isValidIP(ip, af) && (bits >= 0 && bits <= spaceConfig.v6.bits); } } catch (e) { return false; } }, isValidIP: function (ip) { try { return this._isValidIP(ip, this.getAddressFamily(ip)); } catch (error) { return false; } }, _isValidCIDR: function (cidr) { const [ip, bits] = this.getIpAndCidr(cidr); const ntBin = this.toBinary(ip).slice(bits); return /^[0]*$/.test(ntBin); }, isValidCIDR: function (cidr) { try { return this.isValidPrefix(cidr) && this._isValidCIDR(cidr); } catch (error) { return false; } }, _isValidIP: function (ip, af) { try { if (!ip.includes("/")) { if (af === 4) { return spaceConfig.v4.regex.test(ip); } else { return spaceConfig.v6.regex.test(ip); } } return false; } catch (e) { return false; } }, sortByPrefixLength: function (a, b) { const netA = this.getIpAndCidr(a)[1]; const netB = this.getIpAndCidr(b)[1]; return parseInt(netA) - parseInt(netB); }, sortByPrefix: function (a, b) { const prefixAndNetA = this.getIpAndCidr(a); const prefixAndNetB = this.getIpAndCidr(b); const prefixA = prefixAndNetA[0]; const prefixB = prefixAndNetB[0]; const netA = prefixAndNetA[1]; const netB = prefixAndNetB[1]; const sortIp = this.sortByIp(prefixA, prefixB); if (sortIp === 0) { return parseInt(netA) - parseInt(netB); } else { return sortIp; } }, getPieces: function (ip) { const af = this.getAddressFamily(ip); if (af === 4) { return ip.split(spaceConfig.v4.splitChar).map(i => parseInt(i)); } else { return ip.split(spaceConfig.v6.splitChar).map(i => parseInt(i, 16)); } }, sortByIp: function (ipA, ipB) { const piecesA = this.getPieces(ipA); const piecesB = this.getPieces(ipB); for (let n = 0; n < piecesA.length; n++) { if (piecesA[n] < piecesB[n]) { return -1; } else if (piecesA[n] > piecesB[n]) { return 1; } } return 0; }, _v6Pad: function (ip) { return parseInt(ip, 16).toString(2).padStart(spaceConfig.v6.blockSize, "0"); }, _v4Pad: function (ip) { return parseInt(ip).toString(2).padStart(spaceConfig.v4.blockSize, "0"); }, _expandIP: function (ip, af, shortenedIpv6) { af = af || this.getAddressFamily(ip); return (af === 4) ? this._expandIPv4(ip) : this._expandIPv6(ip, shortenedIpv6); }, expandIP: function (ip, shortenedIpv6) { const af = this.getAddressFamily(ip); if (this._isValidIP(ip, af)) { return this._expandIP(ip, af, shortenedIpv6); } else { throw new Error("Not valid IP"); } }, expandPrefix: function (prefix, shortenedIpv6) { const [ip, bits] = this.getIpAndCidr(prefix); return [this.expandIP(ip, shortenedIpv6), bits].join("/"); }, _expandIPv4: function (ip) { let count = 0; for (let i = 0; i < ip.length; i++) { if (ip[i] === ".") { count++; } } for (let n = count; n < 3; n++) { ip += ".0"; } return ip; }, _expandIPv6: function (ip, shortenedIpv6) { ip = ip.toLowerCase(); const segments = ip.split(spaceConfig.v6.splitChar); const count = segments.length - 1; ip = segments .map(segment => { if (segment.length && segment == 0) { // Weak equality return "0"; } else { return segment || ""; } }) .join(spaceConfig.v6.splitChar); if (count !== 7) { const extra = ":" + (new Array(8 - count).fill(0)).join(":") + ":"; ip = ip.replace("::", extra); if (ip[0] === ":") { ip = "0" + ip; } if (ip[ip.length - 1] === ":") { ip += "0"; } } return shortenedIpv6 ? ip : ip.split(":").map(i => ("0000" + i).slice(-4)).join(":"); }, isEqualIP: function (ip1, ip2) { const af1 = this.getAddressFamily(ip1); const af2 = this.getAddressFamily(ip2); if (this._isValidIP(ip1, af1) && this._isValidIP(ip2, af2)) { return af1 === af2 && this._isEqualIP(ip1, ip2, af1); } else { throw new Error("Not valid IP"); } }, isEqualPrefix: function (prefix1, prefix2) { const af1 = this.getAddressFamily(prefix1); const af2 = this.getAddressFamily(prefix2); if (this._isValidPrefix(prefix1, af1) && this._isValidPrefix(prefix2, af2)) { return af1 === af2 && this._isEqualPrefix(prefix1, prefix2, af1); } else { throw new Error("Not valid prefix"); } }, _isEqualIP: function (ip1, ip2, af) { return this._expandIP(ip1, af) === this._expandIP(ip2, af); }, _isEqualPrefix: function (prefix1, prefix2, af) { const components1 = this.getIpAndCidr(prefix1); const components2 = this.getIpAndCidr(prefix2); const ip1 = this._applyNetmask(this._expandIP(components1[0], af), components1[1], af); const ip2 = this._applyNetmask(this._expandIP(components2[0], af), components2[1], af); return components1[1] === components2[1] && ip1 === ip2; }, getAddressFamily: function (ip) { try { return (ip.indexOf(spaceConfig.v6.splitChar) === -1) ? 4 : 6; } catch (error) { throw new Error("Not valid IP or Prefix"); } }, toBinary: function (ip) { return this._toBinary(ip, this.getAddressFamily(ip)); }, _toBinary: function (ip, af) { let bytes = ""; let pad, splitter; let point = 0; let internalCache; if (af === 4) { pad = this._v4Pad; splitter = spaceConfig.v4.splitChar; ip = this._expandIPv4(ip); internalCache = cache.v4; } else { pad = this._v6Pad; splitter = spaceConfig.v6.splitChar; ip = this._expandIPv6(ip); internalCache = cache.v6; } for (let n = 1; n < ip.length; n++) { if (ip[n] == splitter) { const segment = ip.slice(point, n); if (internalCache[segment] == undefined) { internalCache[segment] = pad(segment); } bytes += internalCache[segment]; point = n + 1; } } const segment = ip.slice(point); if (internalCache[segment] == undefined) { internalCache[segment] = pad(segment); } bytes += internalCache[segment]; return bytes; }, applyNetmask: function (prefix, af) { const components = this.getIpAndCidr(prefix); const ip = components[0]; const bits = components[1]; af = af || this.getAddressFamily(ip); return this._applyNetmask(ip, bits, af); }, _applyNetmask: function (ip, bits, af) { if (af === 4) { return this._toBinary(ip, af).padEnd(spaceConfig.v4.bits, "0").slice(0, bits); } else { return this._toBinary(ip, af).padEnd(spaceConfig.v6.bits, "0").slice(0, bits); } }, _getPaddedNetmask: function (binary, af) { if (af === 4) { return binary.padEnd(spaceConfig.v4.bits, "0"); } else { return binary.padEnd(spaceConfig.v6.bits, "0"); } }, isSubnetBinary: (prefixContainer, prefixContained) => { return prefixContained !== prefixContainer && prefixContained.startsWith(prefixContainer); }, isSubnet: function (prefixContainer, prefixContained) { const p1af = this.getAddressFamily(prefixContainer); const p2af = this.getAddressFamily(prefixContained); return p1af === p2af && this._isSubnet(prefixContainer, prefixContained, p1af, p2af); }, _isSubnet: function (prefixContainer, prefixContained, p1af, p2af) { const components1 = this.getIpAndCidr(prefixContainer); const components2 = this.getIpAndCidr(prefixContained); return this.isSubnetBinary(this._applyNetmask(components1[0], components1[1], p1af), this._applyNetmask(components2[0], components2[1], p2af)); }, cidrToRange: function (cidr) { // Not optimized! const af = this.getAddressFamily(cidr); return this._cidrToRange(cidr, af); }, _cidrToRange: function (prefix, af) { const components = this.getIpAndCidr(prefix); const ip = components[0]; const bits = components[1]; let first, last; if (af === 4) { first = this._toBinary(ip, af).slice(0, bits).padEnd(spaceConfig.v4.bits, "0"); last = this._toBinary(ip, af).slice(0, bits).padEnd(spaceConfig.v4.bits, "1"); } else { first = this._toBinary(ip, af).slice(0, bits).padEnd(spaceConfig.v6.bits, "0"); last = this._toBinary(ip, af).slice(0, bits).padEnd(spaceConfig.v6.bits, "1"); } first = this.fromBinary(first, af); last = this.fromBinary(last, af); return [first, last]; }, getComponents: function (ip) { const af = this.getAddressFamily(ip); ip = this._expandIP(ip, af); return this._getComponents(ip, af); }, _getComponents: function (ip, af) { return ip.split((af === 4) ? spaceConfig.v4.splitChar : spaceConfig.v6.splitChar); }, fromBinary: function (ip, af) { let splitter, blockSize, mapper; if (af === 4) { blockSize = spaceConfig.v4.blockSize; splitter = spaceConfig.v4.splitChar; mapper = function (bin) { return parseInt(bin, 2); }; } else { blockSize = spaceConfig.v6.blockSize; splitter = spaceConfig.v6.splitChar; mapper = function (bin) { return parseInt(bin, 2).toString(16); }; } const components = ip.match(new RegExp(".{1," + blockSize + "}", "g")).map(mapper); return components.join(splitter); }, getSubPrefixes: function (prefix, recursive, netMaskIndex, af) { const [ip, bits] = this.getIpAndCidr(prefix); let out = []; netMaskIndex = netMaskIndex || {}; const newbits = bits + 1; const position = Math.floor(bits / spaceConfig[`v${af}`].blockSize); if (af === 4) { let components = this._getComponents(ip, af); if (bits >= spaceConfig.v4.ipSizeCheck) { return []; } while (components[position] < spaceConfig.v4.blockMax) { const ipTmp = components.join(spaceConfig.v4.splitChar); const msk = this._applyNetmask(ipTmp, newbits, af); if (!netMaskIndex[msk]) { out.push(ipTmp + "/" + newbits); netMaskIndex[msk] = true; } components[position]++; } } else { let components = this._getComponents(this._expandIP(ip, af), af); if (bits >= spaceConfig.v6.ipSizeCheck) { return []; } let item = parseInt(components[position], spaceConfig.v6.blockSize); const max = parseInt(spaceConfig.v6.blockMax, spaceConfig.v6.blockSize); while (item < max) { const ipTmp = components.join(spaceConfig.v6.splitChar); const prefixTmp = `${ipTmp}/${newbits}`; const msk = this._applyNetmask(ipTmp, newbits, af); if (!netMaskIndex[msk]) { out.push(prefixTmp); netMaskIndex[msk] = true; } item++; components[position] = item.toString(16); } } if (recursive) { return [].concat.apply([], out.concat(out.map(prefix => this.getSubPrefixes(prefix, true, netMaskIndex, af)))); } else { return out; } }, getAllSiblings: function (prefix) { const [ip, bits] = this.getIpAndCidr(prefix); const af = this.getAddressFamily(ip); this._getAllSiblings(ip, bits, af, bits); }, _getAllSiblings: function (ip, bits, af, parentBits) { let next = this._getNextSiblingPrefix(ip, bits, af, parentBits); const out = []; while (next) { out.push(next); next = this._getNextSiblingPrefix(...next, parentBits); } return out.map(i => i[0] + "/" + i[1]); }, _getNextSiblingPrefix: function (ip, bits, af, parentBits) { parentBits = parentBits || bits - 1; ip = this._expandIP(ip, af); let msk = this._applyNetmask(ip, bits, af); if (af === 4) { let value = parseInt(msk, 2) + 1; const result = value.toString(2).padStart(msk.length, "0"); if (msk.slice(0, parentBits) === result.slice(0, parentBits)) { return [this.fromBinary(this._getPaddedNetmask(result, 4), 4), bits, af]; } else { return null; } } else { // TODO // let value = parseInt(msk, 16) + "1".toString(16); // return [this.fromBinary(this._getPaddedNetmask(value.toString(2), 6), 6), bits, af]; } }, ipRangeToCidr: function (ip1, ip2) { return new IpRange(IpAddress.of(ip1), IpAddress.of(ip2)).toCidrs().map(i => i.toString()); }, _compositeRange: function (ip1, ip2, bits, af, ipSizeCheck) { let mPrefixes = []; for (let b = bits; b <= ipSizeCheck; b++) { const tested = `${ip1}/${b}`; mPrefixes.push(tested); mPrefixes = mPrefixes.concat(this._getAllSiblings(ip1, b, af, bits)); const range = this._cidrToRange(tested, af); if (range[0] === ip1 && range[1] === ip2) { return [tested]; } } const out = []; const start = this._toBinary(ip1, af); const stop = this._toBinary(ip2, af); const sorted = mPrefixes .filter(prefix => { const range = this._cidrToRange(prefix, af).map(ip => this._toBinary(ip, af)); return range[0] >= start && range[1] <= stop; }) .sort((a, b) => this.sortByPrefixLength(a, b)); let pTmp = sorted.pop(); while (pTmp) { if (!sorted.some(prefix => this._isSubnet(prefix, pTmp, af, af))) { out.push(pTmp); } pTmp = sorted.pop(); } return out; }, toPrefix: function (ipOrPrefix) { if (this.isValidPrefix(ipOrPrefix)) { return this.shortenPrefix(ipOrPrefix); } else if (this.isValidIP(ipOrPrefix)) { if (this.getAddressFamily(ipOrPrefix) === 4) { return this.toPrefix(`${ipOrPrefix}/${spaceConfig.v4.bits}`); } else { return this.toPrefix(`${ipOrPrefix}/${spaceConfig.v6.bits}`); } } }, shortenIP: function (ip) { return IpAddress.of(ip).shortNotation().toLowerCase(); }, shortenPrefix: function (prefix) { const [ip, cidr] = this.getIpAndCidr(prefix); const af = this.getAddressFamily(ip); const binaryIp = this._getPaddedNetmask(this.applyNetmask(prefix, af), af); return `${this.shortenIP(this.fromBinary(binaryIp, af))}/${cidr}`; }, getAllLessSpecificBinaries: function (prefix) { const binary = this.applyNetmask(prefix); const out = []; for (let i = 1; i <= binary.length; i++) { out.push(binary.slice(0, i)); } return out; } }; module.exports = ip;