@simplyappdevs/cidr-calculator
Version:
Classless Inter-Domain Routing (CIDR) Notation Calculator
798 lines (672 loc) • 22 kB
text/typescript
/**
* Node imports
*/
/**
* SimplyAppDevs imports
*/
/**
* 3rd party imports
*/
import {default as BitSet} from 'bitset';
// type alias for all
export type IPTUPPLE = [number, number, number, number];
export type IPFORMATS = string | number | IPTUPPLE | OCTECTS;
/**
* Octects definition
*/
export interface OCTECTS {
readonly first: number;
readonly second: number;
readonly third: number;
readonly fourth: number;
add: (num: number) => OCTECTS;
substract: (num: number) => OCTECTS;
toArray: () => IPTUPPLE;
toString: () => string;
toBinary: () => string;
}
/**
* Return defalt or cloned Octects instance
* @param clone Octect instance to clonef from
* @returns Return default or cloned instance
*/
function defaultOctects(clone?: OCTECTS): OCTECTS {
if (clone) {
return {
first: clone.first,
second: clone.second,
third: clone.third,
fourth: clone.fourth,
add: (num: number) => {return defaultOctects(OctectsImpl.add(clone, num));},
substract: (num: number) => {return defaultOctects(OctectsImpl.substract(clone, num));},
toArray: (): IPTUPPLE => {return clone.toArray();},
toBinary: (): string => {return clone.toBinary();},
toString: (): string => {return clone.toString();}
} as OCTECTS;
} else {
return {
first: 0,
second: 0,
third: 0,
fourth: 0,
add: (num: number) => {return defaultOctects(OctectsImpl.add(OCTECTSMIN, num));},
substract: (num: number) => {return defaultOctects(OctectsImpl.substract(OCTECTSMIN, num));},
toArray: (): IPTUPPLE => {return [0, 0, 0, 0];},
toBinary: (): string => {return '00000000000000000000000000000000';},
toString: (): string => {return '0.0.0.0'}
} as OCTECTS;
}
}
/**
* Octects implementation
*/
class OctectsImpl implements OCTECTS {
private static MAXOCTECTSNUM: number = 0xFFFFFFFF >>> 0;
private static IPREGEX: RegExp = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
private static SHIFTVALS: IPTUPPLE = [24, 16, 8, 0];
private static LENVALS: IPTUPPLE = [8, 16, 24, 32];
private static ANDVALS: IPTUPPLE = [0xFF000000, 0xFF0000, 0xFF00, 0xFF];
// private members
private _first: number = 0;
private _second: number = 0;
private _third: number = 0;
private _fourth: number = 0;
/**
* Default constructure
*/
constructor();
/**
* Parse octects from IP string
* @param ip String IP address format [^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$]
*/
constructor(ip: string);
/**
* Parse octects from number
* @param ip IP number value
*/
constructor(ip: number);
/**
* Clones from existing octects
* @param ip IP from array of numbers
*/
constructor(ip: IPTUPPLE);
/**
* Clones from existing octects
* @param ip Octects
*/
constructor(ip: OCTECTS);
/**
* Creates new OCTECTS instance
* @param ip IP in either IP string, OCTECTS, or array of number
*/
constructor(ip?: IPFORMATS) {
if (ip) {
if (typeof ip === 'string') {
const tmpOct = OctectsImpl.octectsFromString(ip);
this.setOctects(tmpOct.first, tmpOct.second, tmpOct.third, tmpOct.fourth);
} else if (typeof ip === 'number') {
const tmpOct = OctectsImpl.numberToOctects(ip);
this.setOctects(tmpOct.first, tmpOct.second, tmpOct.third, tmpOct.fourth);
} else if (Array.isArray(ip)) {
this.setOctects(ip[0], ip[1], ip[2], ip[3]);
} else {
// OCTECTS
this.setOctects(ip.first, ip.second, ip.third, ip.fourth);
}
}
}
/**
* Sets octects
* @param f First octect
* @param s Second octect
* @param t Third octect
* @param h Forth octect
*/
private setOctects(f: number, s: number, t: number, h: number) {
// validate before we set
if ([f, s, t, h].some((octect: number) => {
return ((octect < 0) || (octect > 255));
})) {
throw new Error('One or more octect values are invalid');
}
// set
this._first = f;
this._second = s;
this._third = t;
this._fourth = h;
}
/**
* Gets first octect
*/
public get first(): number {
return this._first;
}
/**
* Gets second octect
*/
public get second(): number {
return this._second;
}
/**
* Gets third octect
*/
public get third(): number {
return this._third;
}
/**
* Gets fourth octect
*/
public get fourth(): number {
return this._fourth;
}
/**
* Adds number of IPs to current IP
* @param num Number of IP to add
* @returns New resulting OCTECTS
*/
public add(num: number): OCTECTS {
return OctectsImpl.add(this, num);
}
/**
* Subtract number of IPs to currrent IP
* @param num Number of IP to substract
* @returns New resulting OCTECTS
*/
public substract(num: number): OCTECTS {
return OctectsImpl.substract(this, num);
}
/**
* Returns octects as array of number
* @returns Octects as [first, second, third, forth]
*/
public toArray(): IPTUPPLE {
return [this._first, this._second, this._third, this._fourth];
}
/**
* Returns octects as IP string
* @returns Octects as IP string
*/
public toString(): string {
return `${this._first}.${this._second}.${this._third}.${this._fourth}`;
}
/**
* Returns octects as binary string
* @returns Octects as binary string
*/
public toBinary(): string {
// return `${this.formatBinary(this._first)}${this.formatBinary(this._second)}${this.formatBinary(this._third)}${this.formatBinary(this._fourth)}`;
return `${OctectsImpl.formatBinary(OctectsImpl.octectsToNumber(this))}`;
}
/**
* Converts from IP string to OCTECTS
* @param ip IP in string format
* @returns IP as OCTECTS
*/
static octectsFromString(ip: string): OCTECTS {
// validate
ip = ip.trim();
if (!OctectsImpl.IPREGEX.test(ip)) {
throw new Error('Invalid IP format');
}
// extract
let octString: string[] = OctectsImpl.IPREGEX.exec(ip)!;
// remove first item
octString.splice(0, 1);
let octs: number[] = octString.map((oct: string): number => {
return parseInt(oct);
});
return new OctectsImpl([octs[0], octs[1], octs[2], octs[3]]);
}
/**
* Returns number for the octects
* @param octects Octects to convert to decimal
* @returns Decimal value for octects
*/
static octectsToNumber(octects: OCTECTS): number {
// get array
const octs = octects.toArray();
return octs.reduce((acc: number, oct: number, ind: number) => {
return acc | ((oct << OctectsImpl.SHIFTVALS[ind]) >>> 0);
}, 0) >>> 0;
}
/**
* Returns OCTECTS representation of a number
* @param num Number to convert to OCTECTS
* @returns OCTECTS for the number
*/
static numberToOctects(num: number): OCTECTS {
// validate
if (num > OctectsImpl.MAXOCTECTSNUM) {
throw new Error(`Number ${num} is greater than maximum octects value ${OctectsImpl.MAXOCTECTSNUM}`);
}
// AND the number by masks and right shift by the octect bits
const octs = OctectsImpl.ANDVALS.map((val: number, ind: number) => {
return (num & val) >>> OctectsImpl.SHIFTVALS[ind];
});
// return
return new OctectsImpl([octs[0], octs[1], octs[2], octs[3]]);
}
/**
* Formats octect in binary string
* @param octect Octect to format in binary
* @param separator Optional seperator to use
* @returns Returns octect in binary string (8 bits)
*/
static formatBinary(octect: number, separator?: string): string {
// convert to binary string
const binString = octect.toString(2);
const binLen = binString.length;
// determine the max string length
let padCount = 0;
OctectsImpl.LENVALS.forEach((val: number) => {
if ((binLen <= val) && (padCount === 0)) {
padCount = val;
}
});
// return
const fullBinString = binString.padStart(padCount, '0');
const fillBinStrLen = fullBinString.length;
separator = separator || '';
const seps = [separator, separator, separator, ''];
let startInd = 0;
let endInd = 0;
return OctectsImpl.LENVALS.reduce((acc: string, val: number, ind: number) => {
endInd += 8;
acc = `${acc}${fullBinString.substring(startInd, endInd)}${seps[ind]}`;
startInd = endInd;
return acc;
}, '');
}
/**
* Adds number of IPs to an IP
* @param octects Octects to add to
* @param num Number of IP to add
* @returns New resulting OCTECTS
*/
static add(octects: OCTECTS, num: number): OCTECTS {
// validation
const curVal = octects.toString();
if (curVal === '255.255.255.255') {
throw new Error(`Cannot add to \'${curVal}\'`);
}
if (num < 0) {
throw new Error(`Invalid number \'${num}\' to add`);
}
if (num === 0) {
return new OctectsImpl(octects);
}
// convert octects to number
let octNum = OctectsImpl.octectsToNumber(octects);
// add it
octNum += num;
// validate
if (octNum > OctectsImpl.MAXOCTECTSNUM) {
throw new Error(`Resulting value \'${octNum}\' is greater than maximum octects value \'${OctectsImpl.MAXOCTECTSNUM}\'`);
}
return new OctectsImpl(OctectsImpl.numberToOctects(octNum));
}
/**
* Subtract number of IPs to an IP
* @param octects Octects to substract from
* @param num Number of IP to substract
* @returns New resulting OCTECTS
*/
static substract(octects: OCTECTS, num: number): OCTECTS {
// validation
const curVal = octects.toString();
if (curVal === '0.0.0.0') {
throw new Error(`Cannot substract from \'${curVal}\'`);
}
if (num < 0) {
throw new Error(`Invalid number \'${num}\' to substract`);
}
if (num === 0) {
return new OctectsImpl(octects);
}
// covert octects to number
let octNum = OctectsImpl.octectsToNumber(octects);
octNum -= num;
return new OctectsImpl(OctectsImpl.numberToOctects(octNum));
}
}
// MAX & MIN octects
export const OCTECTSMIN = defaultOctects();
export const OCTECTSMAX = defaultOctects(new OctectsImpl('255.255.255.255'));
/**
* CIDR information
*/
export interface CIDRInformation {
readonly networkPrefix: string;
readonly cidrBlock: number;
readonly minIP: string;
readonly maxIP: string;
readonly subnetMask: string;
readonly wilcardMask: string;
readonly broadcastIP: string;
readonly totalIPs: number;
}
/**
* Returns default or clone CIDRInformation instance
* @returns Default CIDRInformation instance
*/
function defaultCIDRInformation(): CIDRInformation {
return {
networkPrefix: '0.0.0.0',
cidrBlock: 0,
subnetMask: '0.0.0.0',
wilcardMask: '0.0.0.0',
broadcastIP: '0.0.0.0',
minIP: '0.0.0.0',
maxIP: '0.0.0.0',
totalIPs: 0
};
}
/**
* CIDR definition
*/
export interface CIDR {
readonly inputIP: string;
readonly inputCIDR: number;
readonly ipOctects: OCTECTS;
readonly cidrInformation: CIDRInformation;
readonly maxSubnetCount: number;
calculateCIDR: (cidr: number) => CIDRInformation;
splitCIDR: (subnetCount: number) => CIDRInformation[];
toString: () => string;
}
/**
* Returns default CIDR instance
* @param clone CIDR instance to clone from
* @returns Default CIDR instance
*/
function defaultCIDR(clone: CIDR): CIDR {
return {
inputIP: clone.inputIP,
inputCIDR: clone.inputCIDR,
ipOctects: defaultOctects(clone.ipOctects),
cidrInformation: clone.cidrInformation,
maxSubnetCount: clone.maxSubnetCount,
calculateCIDR: (cidr: number): CIDRInformation => {return clone.calculateCIDR(cidr);},
splitCIDR: (subnetCount: number): CIDRInformation[] => {return clone.splitCIDR(subnetCount);},
toString: (): string => {return clone.toString();}
};
}
/**
* CIDR implementation
*/
class CIDRImpl implements CIDR {
// private
private _octects: OCTECTS = new OctectsImpl();
private _inputIP: string = this._octects.toString();
private _inputCIDR: number = 0;
private _cidrInfo: CIDRInformation = defaultCIDRInformation();
private _maxSubnetCount: number = 0;
private _networkBitCount: number = 0;
private _hostBitCount: number = 0;
/**
* Default constructor
*/
constructor();
/**
* Parse octects from IP string
* @param ip String IP address format [^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$]
* @param cidr CIDR block
*/
constructor(ip: string, cidr?: string | number);
/**
* Clones from existing octects
* @param ip Octects
* @param cidr CIDR block
*/
constructor(ip: OCTECTS, cidr?: string | number);
/**
* Clones from existing octects
* @param ip IP from array of numbers
* @param cidr CIDR block
*/
constructor(ip: number[], cidr?: string | number);
/**
* Creates new CIDR instance
* @param ip IP in either IP string, OCTECTS, or array of number
* @param cidr CIDR block
*/
constructor(ip?: string | OCTECTS | number[], cidr?: string | number) {
if (ip) {
this._octects = new OctectsImpl(ip as any); // validation of IP happens in here
this._inputIP = this._octects.toString();
}
if (cidr) {
if (typeof cidr === 'string') {
const cidrNum = parseInt(cidr);
if (isNaN(cidrNum)) {
throw new Error(`\'${cidr}\' is an invalid CIDR block value`);
}
this._inputCIDR = cidrNum;
} else {
this._inputCIDR = cidr;
}
}
// if we have IP, we need CIDR
if ((this._inputIP !== '0.0.0.0') && (this._inputCIDR <= 0)) {
throw new Error(`Must specify CIDR block value for IP \'${this._inputIP}\'`);
}
// calculate CIDR
if ((this._octects.toString() !== '0.0.0.0') && (this._inputCIDR > 0)) {
this.calculateCIDR(this._inputCIDR);
}
}
/**
* Converts BitSet to Octects
* @param bs Bitset to convert
* @returns Returns Octects representation of the BitSet
*/
private bitsetToIP(bs: BitSet): OCTECTS {
// grab every 8bits
const frh: number = +bs.slice(0, 7).toString(10);
const thd: number = +bs.slice(8, 15).toString(10);
const sec: number = +bs.slice(16, 23).toString(10);
const fst: number = +bs.slice(24, 31).toString(10);
return new OctectsImpl([fst, sec, thd, frh]);
}
/**
* Gets IP that was used to initialize this instance
*/
get inputIP(): string {
return this._inputIP;
};
/**
* Gets IP that was used to initialize this instance
*/
get inputCIDR(): number {
return this._inputCIDR;
};
/**
* Gets octects
*/
get ipOctects(): OCTECTS {
return this._octects;
}
/**
* Gets CIDRInformation (last successful calculation)
*/
get cidrInformation(): CIDRInformation {
return this._cidrInfo;
}
/**
* Gets the maximum number of subnet that this CIDR can be split into
*/
get maxSubnetCount(): number {
return this._maxSubnetCount;
}
/**
* Calculate CIDR information based on CIDR block
* @param cidr CIDR block
*/
public calculateCIDR(cidr: number): CIDRInformation {
// CIDR is between 1-32
if ((cidr < 1) || (cidr > 32)) {
throw new Error(`CIDR block value must be between 1 and 32`);
}
if (this._octects.first === 0) {
throw new Error(`Cannot calculate CIDR for \'${this.inputIP}\'`);
}
// IP consists of 4x 8bits octects
// each octect range is 0 - 255
// CIDR block value indicates which top N bits to "lock" (unchangble) based on the input IP
// for example:
// IP := 172.90.0.2 and CIDR := 8 - that means 172 is locked while the last 3 octects (24bits) can change
// bitset for CIDR block
const cidrBS = new BitSet('0xffffffff');
// calculate the lower bits that can change
let varBits = 32 - cidr;
this._networkBitCount = cidr;
this._hostBitCount = varBits;
if (varBits > 0) {
varBits -= 1; // 0 based array index
cidrBS.setRange(0, varBits, 0);
}
// bitset for input IP
const inputIPBS = new BitSet(`${this.ipOctects.toBinary()}`);
// netmask - AND inputIP and CIDR block
const lockedCIDR = inputIPBS.and(cidrBS);
// minimum IP is just the locked CIDR block (this is also the network prefix)
const minIP: string = this.bitsetToIP(lockedCIDR).toString();
// maximum IP is lockedCIDR block but with the lower 32-cidr bits flipped
const maxIPBS = lockedCIDR.clone();
maxIPBS.flip(0, 31 - cidr);
const maxIP = this.bitsetToIP(maxIPBS).toString();
// maximum # of IP 2^N where N is the # of lower bits that can change
const maxIPCount = Math.pow(2, 32 - cidr);
// wildcard address is NOT of netmask
const wilcardMask = cidrBS.clone().not();
// save result
this._inputCIDR = cidr;
this._maxSubnetCount = Math.pow(2, this._hostBitCount) - 2;
this._cidrInfo = {
networkPrefix: minIP,
cidrBlock: this.inputCIDR,
subnetMask: `${this.bitsetToIP(cidrBS).toString()}`,
wilcardMask: `${this.bitsetToIP(wilcardMask).toString()}`,
broadcastIP: maxIP,
minIP: minIP,
maxIP: maxIP,
totalIPs: maxIPCount
};
return this._cidrInfo;
}
/**
* Returns subnet segments for the current CIDR
* @param subnetCount Number of subnets to split from the current CIDR
*/
public splitCIDR(subnetCount: number): CIDRInformation[] {
// validate
if ((this._inputIP === '0.0.0.0') || (this._inputCIDR === 0)) {
throw new Error('Cannot split CIDR because it has not been defined.');
}
if (subnetCount <= 1) {
return [this._cidrInfo];
}
if (this._hostBitCount === 0) {
return [];
}
if (subnetCount > this.maxSubnetCount) {
throw new Error(`CIDR can only be split into maximum of \'${this.maxSubnetCount}\' subnets.`);
}
// subnetting takes the available host bits and lock N high order bits
// to use as network bits (in addtional to the CIDR network bits)
// so we need to find the max bits we need to reserve for the subnetting
// formula: 2^N >= subnetCount - find N that result to subnetCount or the next
// one greater than subnetCount
let realSubnetCount = 0;
let n = 0;
while (realSubnetCount === 0) {
const subCount = Math.pow(2, n);
if (subCount >= subnetCount) {
realSubnetCount = subCount;
} else {
++n;
}
}
// new CIDR
let newCIDR = this._cidrInfo.cidrBlock + n;
// loop to calculate subnets
let subnets: CIDRInformation[] = [];
let startIP: string = this._cidrInfo.minIP;
let startIPOctects: OCTECTS = new OctectsImpl(startIP);
for (let i = 0; i < realSubnetCount; i++) {
// subnet using startIP but new CIDR
subnets[i] = new CIDRImpl(startIP, newCIDR).cidrInformation;
// next start IP is current subnet maxIP + 1
startIPOctects = new OctectsImpl(subnets[i].maxIP);
startIPOctects = startIPOctects.add(1);
startIP = startIPOctects.toString();
}
return subnets;
}
/**
* Return string representation of this instance
* @returns String representation of this CIDR instance
*/
public toString(): string {
return `${this._inputIP}/${this.inputCIDR}`;
}
/**
* Returns CIDR block based on subnet mask
* @param mask Subnet mask
*/
static cidrBlockFromSubnetMask(mask: string): number {
// convert to octects
const maskOctects = new OctectsImpl(mask);
// bitset
const maskBS = new BitSet(maskOctects.toBinary());
if (maskBS.toString(10) === '0') {
throw new Error(`\'${mask}\' is not a valid subnet mask value`);
}
// subnet mask cannot have 0 in between 1s
const bits = maskBS.toString(2);
if (/01/.test(bits)) {
throw new Error(`\'${mask}\' is not a valid subnet mask format`);
}
// get the last 0s
const zeroBits = /(0+)/.exec(bits);
if (!zeroBits) {
// all 1s
return 32;
} else {
// regex match - first item is the full matched text and subsequent items are the groups
// in this case it has to be a match and the first item is all the 0s
return 32 - zeroBits[0].length;
}
}
}
/**
* CIDR module
*/
export const CIDRModule = {
parseCIDR: (ip?: IPFORMATS, cidr?: string | number): CIDR => {
return defaultCIDR(new CIDRImpl(ip as any, cidr));
},
parseOctects: (ip: IPFORMATS): OCTECTS => {
return defaultOctects(new OctectsImpl(ip as any));
},
cidrFromSubnetMask: (mask: string): number => {
return CIDRImpl.cidrBlockFromSubnetMask(mask);
},
octectsFromIPString: (ip: string): OCTECTS => {
return defaultOctects(OctectsImpl.octectsFromString(ip));
},
octectsToNumber: (octs: OCTECTS): number => {
return OctectsImpl.octectsToNumber(octs);
},
numberToBinary: (num: number, sep?: string): string => {
return OctectsImpl.formatBinary(num, sep);
},
addToIP: (ip: IPFORMATS, num: number): OCTECTS => {
const octs = new OctectsImpl(ip as any);
return defaultOctects(octs.add(num));
},
substractFromIP: (ip: IPFORMATS, num: number): OCTECTS => {
const octs = new OctectsImpl(ip as any);
return defaultOctects(octs.substract(num));
}
};