Subnet command (#17)

* Started working on subnets

* Basic version of the subnet command

* Improved subnet command

* almost done with subnets

* improve positioning
This commit is contained in:
Borys Levytskyi
2021-01-16 11:33:45 +02:00
committed by GitHub
parent 0cd9c8049b
commit 478ecbfb60
24 changed files with 659 additions and 332 deletions

18
src/core/byte.test.ts Normal file
View File

@@ -0,0 +1,18 @@
import {flipBitsToZero, flipBitsToOne} from './byte';
describe('byte', () => {
it('can zero out bits', () => {
expect(flipBitsToZero(255, 1)).toBe(254);
expect(flipBitsToZero(212, 6)).toBe(192);
expect(flipBitsToZero(123, 8)).toBe(0);
expect(flipBitsToZero(23, 0)).toBe(23);
});
it('can flip bits to one', () => {
expect(flipBitsToOne(122,4)).toBe(127);
expect(flipBitsToOne(0,8)).toBe(255);
expect(flipBitsToOne(0,3)).toBe(7);
expect(flipBitsToOne(0,2)).toBe(3);
expect(flipBitsToOne(0,1)).toBe(1);
});
});

26
src/core/byte.ts Normal file
View File

@@ -0,0 +1,26 @@
function flipBitsToZero(byte: number, numberOfBits : number) : number {
if(numberOfBits == 0)
return byte;
const zerouOutMask = Math.pow(2, 8-numberOfBits)-1<<numberOfBits; // E.g. 11111000 for flipping first three bits
const result = byte & zerouOutMask;
return result;
}
// TODO: continue here to implement getting broadcast address
function flipBitsToOne(byte : number, numberOfBits : number) : number {
if(numberOfBits == 0) return byte;
const zerouOutMask = Math.pow(2, numberOfBits)-1; // E.g. 00000111 for flipping first three bits
const result = byte | zerouOutMask;
return result;
}
function createSubnetMaskByte(numberOfBits: number) {
return 255<<(8-numberOfBits)&255;;
}
export {flipBitsToZero, createSubnetMaskByte, flipBitsToOne};

View File

@@ -1,10 +1,10 @@
import React from 'react';
export type BinaryStringViewProps = {
allowFlipBits: boolean;
allowFlipBits?: boolean;
binaryString: string;
onFlipBit?: (input: FlipBitEventArg) => void;
emphasizeBytes: boolean;
emphasizeBytes?: boolean;
className?:string
};
@@ -51,7 +51,7 @@ export default class BinaryStringView extends React.Component<BinaryStringViewPr
const css = allowFlipBits ? ' flipable' : ''
return bitChars.map((c, i) => {
var className = c == '0' ? `zero${css}` : `one${css}`;
var className = c == '1' ? `one${css}` : `zero${css}`;
return <span className={className} key={i} onClick={e => this.onBitClick(i, e)}>{c}</span>
});
}

View File

@@ -14,6 +14,12 @@ export default {
}
return sb.join('');
},
bin(number: number) {
return this.formatString(number, 'bin');
},
emBin(number: number) {
return this.padLeft(this.bin(number), 8, '0');
}
};

9
src/core/utils.test.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { chunkifyString } from "./utils";
describe('utils', () => {
it('chunkifyString', () => {
expect(chunkifyString('aabbc', 2)).toMatchObject(["aa", "bb", "c"]);
expect(chunkifyString('aabbc', 3)).toMatchObject(["aab", "bc"]);
expect(chunkifyString('aabbc', 10)).toMatchObject(["aabbc"]);
})
})

12
src/core/utils.tsx Normal file
View File

@@ -0,0 +1,12 @@
function chunkifyString(input: string, chunkSize: number) : string[] {
const result : string[] = [];
for(var i=0;i<input.length;i+=chunkSize) {
const size = Math.min(chunkSize, input.length-i);
result.push(input.substr(i, size));
}
return result;
}
export {chunkifyString};

View File

@@ -41,6 +41,7 @@ code { font-size: 1.2em; font-weight: bold; }
.error { color: maroon; }
.soft { opacity: 0.7 }
.small-text { font-size: 0.8em;}
#view { padding: 10px}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import BinaryStringView from '../../core/components/BinaryString';
import formatter from '../../core/formatter';
import { IpAddress } from '../models';
function IpAddressBinaryString({ip}: {ip:IpAddress}) {
return <React.Fragment>
<BinaryStringView binaryString={formatter.emBin(ip.firstByte)} />
<span className="soft">.</span>
<BinaryStringView binaryString={formatter.emBin(ip.secondByte)} />
<span className="soft">.</span>
<BinaryStringView binaryString={formatter.emBin(ip.thirdByte)} />
<span className="soft">.</span>
<BinaryStringView binaryString={formatter.emBin(ip.fourthByte)} />
</React.Fragment>;
}
export default IpAddressBinaryString;

View File

@@ -1,8 +1,9 @@
import React from 'react';
import { IpAddress, OctetNumber, getNetworkClass } from '../ip';
import formatter from '../../core/formatter'
import BinaryStringView from '../../core/components/BinaryString';
import './IpAddressView.css';
import { IpAddress, OctetNumber } from '../models';
type IpAddressViewProps = {
ipAddresses: IpAddress[]
};
@@ -11,13 +12,6 @@ export class IpAddressView extends React.Component<IpAddressViewProps>
{
render() {
if(this.props.ipAddresses.length === 1)
return this.renderSingleIp(this.props.ipAddresses[0]);
return this.renderMultipleIps();
}
renderMultipleIps() {
return <table className="expression">
<tbody>
{this.props.ipAddresses.map((ip, i) => <tr key={i}>
@@ -30,33 +24,7 @@ export class IpAddressView extends React.Component<IpAddressViewProps>
</td>
</tr>)}
</tbody>
</table>
}
renderSingleIp(ip: IpAddress) {
return <table className="expression">
<thead>
<tr>
<th className='first-decimal'>{ip.firstByte}</th>
<th className='second-decimal'>{ip.secondByte}</th>
<th className='third-decimal'>{ip.thirdByte}</th>
<th className='fourth-decimal'>{ip.fourthByte}</th>
</tr>
</thead>
<tbody>
<tr>
<td className='first-bin'>{this.bin(ip.firstByte, 1, ip)}</td>
<td className='second-bin'>{this.bin(ip.secondByte, 2, ip)}</td>
<td className='third-bin'>{this.bin(ip.thirdByte, 3, ip)}</td>
<td className='fourth-bin'>{this.bin(ip.fourthByte, 4, ip)}</td>
</tr>
{/* <tr>
<td colSpan={2} className="ip-address-info">
<a href="https://www.wikiwand.com/en/Classful_network" target="_blank">Network Class: {getNetworkClass(ip).toUpperCase()}</a>
</td>
</tr> */}
</tbody>
</table>;
</table>;
}
bin(value: number, octetNumber: OctetNumber, ip: IpAddress) {

View File

@@ -0,0 +1,16 @@
.subnet-view .description {
vertical-align: middle;
text-align: right;
}
.subnet-view td {
padding-right: 15px;
}
.subnet-view {
margin-bottom: 20px;
}
.subnet-view .part {
border-bottom: solid 1px;
}

View File

@@ -0,0 +1,60 @@
import React from 'react';
import BinaryStringView from '../../core/components/BinaryString';
import './SubnetView.css';
import { getNetworkAddress, getBroadCastAddress, createSubnetMaskIp } from '../subnet-utils';
import { chunkifyString } from '../../core/utils';
import IpAddressBinaryString from './IpAddressBinaryString';
import { IpAddress, SubnetCommand } from '../models';
function SubnetView({subnet} : {subnet : SubnetCommand}) {
return <React.Fragment>
<table className="expression subnet-view">
<tbody>
<SubnetRow ip={subnet.input.ipAddress} descr="Address"/>
<SubnetRow ip={getNetworkAddress(subnet.input)} descr="Network"/>
<SubnetRow ip={createSubnetMaskIp(subnet.input)} descr="Net Mask"/>
<SubnetRow ip={getBroadCastAddress(subnet.input)} descr="Broadcast"/>
<tr>
<td className="description soft">
<span>Mask Length</span>
</td>
<td>
{subnet.input.maskBits}
</td>
</tr>
<tr>
<td className="description soft">
<span>Network Size</span>
</td>
<td>
{subnet.getAdressSpaceSize()}
</td>
</tr>
</tbody>
</table>
<div>
</div>
</React.Fragment>;
}
function SubnetRow(props: { ip: IpAddress, descr: string}) {
const {ip, descr} = props;
return <tr>
<td className="description soft">{descr}</td>
<td className="ip">
{ip.toString()}
</td>
<td className="class-part">
<IpAddressBinaryString ip={ip} />
</td>
</tr>;
function addDots(bin: string) {
return chunkifyString(bin, 8).map((s, i) => <BinaryStringView binaryString={s} key={i} />)
}
}
export default SubnetView;

View File

@@ -0,0 +1,65 @@
import ipAddressParser, { ParsedIpObject, ParsingError } from './ip-parser';
import { IpAddressWithSubnetMask, IpAddress, SubnetCommand } from "./models";
describe('parser tests', () => {
it('can parse correct ip address', () => {
const actual = ipAddressParser.parse('127.1.2.3');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const obj = (actual as ParsedIpObject[])[0];
const expected = new IpAddress(127, 1, 2, 3);
expect(obj).not.toBe(null);
expect(obj.toString()).toBe(expected.toString());
});
it('cannot parse incorrect ip address', () => {
expect(ipAddressParser.parse('abc')).toBe(null);
expect(ipAddressParser.parse('')).toBe(null);
});
it('should parse invalid ip address', () => {
expect(ipAddressParser.parse('256.0.0.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.256.0.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.256.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.256')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.255 asd')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.255/99')).toBeInstanceOf(ParsingError);
});
it('parses correct ip and subnet mask', () => {
const actual = ipAddressParser.parse('127.0.0.1/24');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const obj = (actual as ParsedIpObject[])[0];
expect(obj).toBeInstanceOf(IpAddressWithSubnetMask);
expect(obj!.toString()).toBe('127.0.0.1/24');
expect((obj as IpAddressWithSubnetMask).maskBits).toBe(24);
});
it('parses list of ip addresses', () => {
const actual = ipAddressParser.parse('127.0.0.1/24 255.255.1.1');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const first = (actual as ParsedIpObject[])[0];
expect(first).toBeInstanceOf(IpAddressWithSubnetMask);
expect(first!.toString()).toBe('127.0.0.1/24');
expect((first as IpAddressWithSubnetMask).maskBits).toBe(24);
const second = (actual as ParsedIpObject[])[1];
expect(second).toBeInstanceOf(IpAddress);
expect(second!.toString()).toBe('255.255.1.1');
});
it('parses subnet command', () => {
const actual = ipAddressParser.parse('subnet 192.168.1.1/23');
expect(actual).toBeInstanceOf(SubnetCommand);
const subnet = actual as SubnetCommand;
expect(subnet.toString()).toBe('192.168.1.1/23');
});
});

115
src/networking/ip-parser.ts Normal file
View File

@@ -0,0 +1,115 @@
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand } from './models';
export type ParsedIpObject = IpAddress | IpAddressWithSubnetMask;
const ipAddressParser = {
parse: function(input: string) : ParsedIpObject[] | SubnetCommand | ParsingError | null {
const result = this.parseCommand(input);
const matches = this.getMaches(result.nextInput);
const correctInputs = matches.filter(m => m.matches != null);
const incorrectInputs = matches.filter(m => m.matches == null);
if(correctInputs.length == 0)
return null;
if(incorrectInputs.length > 0) {
return new ParsingError(`Value(s) ${incorrectInputs.map(v => v.input).join(',')} was not recognized as valid ip address or ip address with a subnet mask`);
}
const parsedObjects = matches.map(m => this.parseSingle(m.matches!, m.input));
const parsingErrors = parsedObjects.filter(p => p instanceof ParsingError);
if(parsingErrors.length > 0) {
return parsingErrors[0] as ParsingError;
}
if(result.command != null) {
const result = this.createSubnetDefinition(parsedObjects as ParsedIpObject[]);
if(result instanceof ParsingError)
return result;
return result;
}
return parsedObjects as ParsedIpObject[];
},
parseCommand(input : string) : { command: null | string, nextInput: string } {
const command = 'subnet';
if(input.startsWith(command))
return { command, nextInput: input.substring(command.length)}
return { command: null, nextInput: input };
},
getMaches(input : string) : { matches: RegExpExecArray | null, input: string }[] {
return input.
replace(/[\t\s]+/g, ' ')
.split(' ')
.filter(s => s.length>0)
.map(s => {
const ipV4Regex = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(\/\d+)?$/;
const matches = ipV4Regex.exec(s);
if(matches == null || matches.length === 0)
return {matches: null, input: s};
return {matches, input: s};
});
},
parseSingle(matches : RegExpExecArray, input: string) : ParsedIpObject | ParsingError {
const invalid = (n: number) => n < 0 || n > 255;
const first = parseInt(matches[1]);
const second = parseInt(matches[2]);
const third = parseInt(matches[3]);
const fourth = parseInt(matches[4]);
if(invalid(first) || invalid(second) || invalid(third) || invalid(fourth))
return new ParsingError(`${input} value doesn't fall within the valid range of the IP address space`);
const ipAddress = new IpAddress(first, second, third, fourth);
if(matches[5]) {
const maskPart = matches[5].substr(1);
const maskBits = parseInt(maskPart);
if(maskBits > 32) {
return new ParsingError(`Subnet mask value in ${input} is out of range`);
}
return new IpAddressWithSubnetMask(ipAddress, maskBits);
}
return ipAddress;
},
createSubnetDefinition(items: ParsedIpObject[]) : SubnetCommand | ParsingError {
if(items.length != 1)
return new ParsingError("Incorrect network definition");
const first = items[0];
if(first instanceof IpAddressWithSubnetMask) {
return new SubnetCommand(first);
}
return new ParsingError("Network definition requires subnet mask");
}
}
export class ParsingError {
errorMessage: string;
constructor(message: string) {
this.errorMessage = message;
}
}
export default ipAddressParser;

View File

@@ -1,104 +0,0 @@
import {IpAddress, ipAddressParser, getNetworkClass, ParsingError, IpAddressWithSubnetMask, ParsedIpObject} from './ip';
describe('parser tests', () => {
it('can parse correct ip address', () => {
const actual = ipAddressParser.parse('127.1.2.3');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const obj = (actual as ParsedIpObject[])[0];
const expected = new IpAddress(127, 1, 2, 3);
expect(obj).not.toBe(null);
expect(obj.toString()).toBe(expected.toString());
});
it('cannot parse incorrect ip address', () => {
expect(ipAddressParser.parse('abc')).toBe(null);
expect(ipAddressParser.parse('')).toBe(null);
});
it('should parse invalid ip address', () => {
expect(ipAddressParser.parse('256.0.0.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.256.0.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.256.0')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.256')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.255 asd')).toBeInstanceOf(ParsingError);
expect(ipAddressParser.parse('0.0.0.255/99')).toBeInstanceOf(ParsingError);
});
it('parses correct ip and subnet mask', () => {
const actual = ipAddressParser.parse('127.0.0.1/24');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const obj = (actual as ParsedIpObject[])[0];
expect(obj).toBeInstanceOf(IpAddressWithSubnetMask);
expect(obj!.toString()).toBe('127.0.0.1/24');
expect((obj as IpAddressWithSubnetMask).maskBits).toBe(24);
});
it('parses list of ip addresses', () => {
const actual = ipAddressParser.parse('127.0.0.1/24 255.255.1.1');
expect(actual).not.toBe(null);
expect(actual).not.toBeInstanceOf(ParsingError);
const first = (actual as ParsedIpObject[])[0];
expect(first).toBeInstanceOf(IpAddressWithSubnetMask);
expect(first!.toString()).toBe('127.0.0.1/24');
expect((first as IpAddressWithSubnetMask).maskBits).toBe(24);
const second = (actual as ParsedIpObject[])[1];
expect(second).toBeInstanceOf(IpAddress);
expect(second!.toString()).toBe('255.255.1.1');
});
});
describe('getNetworkClass tests', () => {
it('detects class a', () => {
expect(getNetworkClass(new IpAddress(1, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(55, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(97, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(127, 0, 0, 0))).toBe('a');
});
it('detects class b', () => {
expect(getNetworkClass(new IpAddress(128, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(134, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(180, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(191, 0, 0, 0))).toBe('b');
});
it('detects class c', () => {
expect(getNetworkClass(new IpAddress(192, 0, 0, 0))).toBe('c');
expect(getNetworkClass(new IpAddress(218, 0, 0, 0))).toBe('c');
expect(getNetworkClass(new IpAddress(223, 0, 0, 0))).toBe('c');
});
it('detects class d', () => {
expect(getNetworkClass(new IpAddress(224, 0, 0, 0))).toBe('d');
expect(getNetworkClass(new IpAddress(234, 0, 0, 0))).toBe('d');
expect(getNetworkClass(new IpAddress(239, 0, 0, 0))).toBe('d');
});
it('detects class e', () => {
expect(getNetworkClass(new IpAddress(240, 0, 0, 0))).toBe('e');
expect(getNetworkClass(new IpAddress(241, 0, 0, 0))).toBe('e');
expect(getNetworkClass(new IpAddress(255, 0, 0, 0))).toBe('e');
});
});
describe('IpAddressWithSubnetMask tests', () => {
it('creates subnetmask ip', () => {
const ip = new IpAddress(127, 0, 0, 1);
expect(new IpAddressWithSubnetMask(ip, 1).createSubnetMaskIp().toString()).toBe('128.0.0.0');
expect(new IpAddressWithSubnetMask(ip, 8).createSubnetMaskIp().toString()).toBe('255.0.0.0');
expect(new IpAddressWithSubnetMask(ip, 10).createSubnetMaskIp().toString()).toBe('255.192.0.0');
expect(new IpAddressWithSubnetMask(ip, 20).createSubnetMaskIp().toString()).toBe('255.255.240.0');
expect(new IpAddressWithSubnetMask(ip, 30).createSubnetMaskIp().toString()).toBe('255.255.255.252');
expect(new IpAddressWithSubnetMask(ip, 32).createSubnetMaskIp().toString()).toBe('255.255.255.255');
});
});

View File

@@ -1,183 +0,0 @@
import formatter from '../core/formatter';
export type OctetNumber = 1 | 2 | 3 | 4;
export type NetworkClass = 'a' | 'b' | 'c' | 'd' | 'e';
export type ParsedIpObject = IpAddress | IpAddressWithSubnetMask;
const ipAddressParser = {
parse: function(input: string) : ParsedIpObject[] | ParsingError | null {
const matches = this.getMaches(input);
const correctInputs = matches.filter(m => m.matches != null);
const incorrectInputs = matches.filter(m => m.matches == null);
if(correctInputs.length == 0)
return null;
if(incorrectInputs.length > 0) {
return new ParsingError(`Value(s) ${incorrectInputs.map(v => v.input).join(',')} was not recognized as valid ip address or ip address with a subnet mask`);
}
const parsedObjects = matches.map(m => this.parseSingle(m.matches!, m.input));
const parsingErrors = parsedObjects.filter(p => p instanceof ParsingError);
if(parsingErrors.length > 0) {
return parsingErrors[0] as ParsingError;
}
return parsedObjects as ParsedIpObject[];
},
getMaches(input : string) : { matches: RegExpExecArray | null, input: string }[] {
return input.
replace(/[\t\s]+/g, ' ')
.split(' ')
.filter(s => s.length>0)
.map(s => {
const ipV4Regex = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(\/\d+)?$/;
const matches = ipV4Regex.exec(s);
if(matches == null || matches.length === 0)
return {matches: null, input: s};
return {matches, input: s};
});
},
parseSingle(matches : RegExpExecArray, input: string) : ParsedIpObject | ParsingError {
const invalid = (n: number) => n < 0 || n > 255;
const first = parseInt(matches[1]);
const second = parseInt(matches[2]);
const third = parseInt(matches[3]);
const fourth = parseInt(matches[4]);
if(invalid(first) || invalid(second) || invalid(third) || invalid(fourth))
return new ParsingError(`${input} value doesn't fall within the valid range of the IP address space`);
const ipAddress = new IpAddress(first, second, third, fourth);
if(matches[5]) {
const maskPart = matches[5].substr(1);
const maskBits = parseInt(maskPart);
if(maskBits > 32) {
return new ParsingError(`Subnet mask value in ${input} is out of range`);
}
return new IpAddressWithSubnetMask(ipAddress, maskBits);
}
return ipAddress;
}
}
export class ParsingError {
errorMessage: string;
constructor(message: string) {
this.errorMessage = message;
}
}
export class IpAddressWithSubnetMask {
maskBits: number;
ipAddress: IpAddress;
constructor(ipAddress : IpAddress, maskBits : number) {
this.ipAddress = ipAddress;
this.maskBits = maskBits;
}
toString() {
return `${this.ipAddress.toString()}/${this.maskBits}`;
}
createSubnetMaskIp() : IpAddress {
const mask = (bits: number) => 255<<(8-bits)&255;
if(this.maskBits <= 8) {
return new IpAddress(mask(this.maskBits), 0, 0, 0);
}
else if(this.maskBits <= 16) {
return new IpAddress(255, mask(this.maskBits-8), 0, 0);
}
else if(this.maskBits <= 24) {
return new IpAddress(255, 255, mask(this.maskBits-16), 0);
}
else {
return new IpAddress(255, 255, 255, mask(this.maskBits-24));
}
}
}
export class IpAddress {
firstByte : number;
secondByte: number;
thirdByte : number;
fourthByte: number
constructor(firstByte : number, secondByte: number, thirdByte : number, fourthByte: number) {
this.firstByte = firstByte;
this.secondByte = secondByte;
this.thirdByte = thirdByte;
this.fourthByte = fourthByte;
}
toString() : string {
return `${this.firstByte}.${this.secondByte}.${this.thirdByte}.${this.fourthByte}`;
}
setOctet(octet: OctetNumber, value : number) {
switch(octet) {
case 1:
this.firstByte = value;
break;
case 2:
this.secondByte = value;
break;
case 3:
this.thirdByte = value;
break;
case 4:
this.fourthByte = value;
break;
}
}
}
const getNetworkClass = function (ipAddress: IpAddress) : NetworkClass {
const byte = ipAddress.firstByte;
const bineryRep = formatter.formatString(ipAddress.firstByte, 'bin');
const firstBitOne = (byte & 128) === 128;
const firstBitZero = (byte & 128) === 0;
const secondBitOne = (byte & 64) === 64;
const thirdBitOne = (byte & 32) === 32;
const thirdBitZero = (byte & 32) === 0;
const forthBitZero = (byte & 16) === 0;
const forthBitOne = (byte & 16) === 16;
// e: 1111
if(firstBitOne && secondBitOne && thirdBitOne && forthBitOne)
return 'e';
if(firstBitOne && secondBitOne && thirdBitOne && forthBitZero) // Start bits: 1110;
return 'd';
if(firstBitOne && secondBitOne && thirdBitZero) // Start bits: 110;
return 'c';
return firstBitOne ? 'b' : 'a';
};
export {ipAddressParser, getNetworkClass};

73
src/networking/models.ts Normal file
View File

@@ -0,0 +1,73 @@
export type OctetNumber = 1 | 2 | 3 | 4;
export type NetworkClass = 'a' | 'b' | 'c' | 'd' | 'e';
export class IpAddressWithSubnetMask {
maskBits: number;
ipAddress: IpAddress;
constructor(ipAddress: IpAddress, maskBits: number) {
this.ipAddress = ipAddress;
this.maskBits = maskBits;
}
toString() {
return `${this.ipAddress.toString()}/${this.maskBits}`;
}
}
export class IpAddress {
firstByte: number;
secondByte: number;
thirdByte: number;
fourthByte: number;
constructor(firstByte: number, secondByte: number, thirdByte: number, fourthByte: number) {
this.firstByte = firstByte;
this.secondByte = secondByte;
this.thirdByte = thirdByte;
this.fourthByte = fourthByte;
}
toString(): string {
return `${this.firstByte}.${this.secondByte}.${this.thirdByte}.${this.fourthByte}`;
}
clone(): IpAddress {
return new IpAddress(this.firstByte, this.secondByte, this.thirdByte, this.fourthByte);
}
setOctet(octet: OctetNumber, value: number) {
switch (octet) {
case 1:
this.firstByte = value;
break;
case 2:
this.secondByte = value;
break;
case 3:
this.thirdByte = value;
break;
case 4:
this.fourthByte = value;
break;
}
}
}
export class SubnetCommand {
input: IpAddressWithSubnetMask;
constructor(definition: IpAddressWithSubnetMask) {
this.input = definition;
}
getAdressSpaceSize(): number {
const spaceLengthInBits = 32 - this.input.maskBits;
return Math.pow(2, spaceLengthInBits) - 2; // 0 - network address, 1 - multicast address
}
toString() {
return this.input.toString();
}
}

View File

@@ -3,8 +3,11 @@ import AppState from '../shell/AppState';
import { CmdShell, CommandInput } from '../shell/cmd';
import ErrorResultView from '../shell/components/ErrorResultView';
import IpAddressView from './components/IpAddressView';
import { ipAddressParser, ParsingError, IpAddress, ParsedIpObject, IpAddressWithSubnetMask } from './ip';
import ipAddressParser, {ParsingError, ParsedIpObject} from './ip-parser';
import { IpAddress, IpAddressWithSubnetMask, SubnetCommand } from "./models";
import log from 'loglevel';
import SubnetView from './components/SubnetView';
import { createSubnetMaskIp } from './subnet-utils';
const networkingAppModule = {
setup: function(appState: AppState, cmd: CmdShell) {
@@ -23,13 +26,18 @@ const networkingAppModule = {
return;
}
if(result instanceof SubnetCommand) {
appState.addCommandResult(c.input, <SubnetView subnet={result} />);
return;
}
const ipAddresses : IpAddress[] = [];
(result as ParsedIpObject[]).forEach(r => {
if(r instanceof IpAddressWithSubnetMask)
{
ipAddresses.push(r.ipAddress);
ipAddresses.push(r.createSubnetMaskIp());
ipAddresses.push(createSubnetMaskIp(r));
}
else if(r instanceof IpAddress) {
ipAddresses.push(r);

View File

@@ -0,0 +1,90 @@
import { IpAddress, IpAddressWithSubnetMask } from "./models";
import {createSubnetMaskIp,getBroadCastAddress,getNetworkAddress, getNetworkClass} from './subnet-utils';
describe('utils', () => {
it('createSubnetMaskIp', () => {
const ip = new IpAddress(127, 0, 0, 1);
const mask = (n: number) => new IpAddressWithSubnetMask(ip, n);
expect(createSubnetMaskIp(mask(1)).toString()).toBe('128.0.0.0');
expect(createSubnetMaskIp(mask(8)).toString()).toBe('255.0.0.0');
expect(createSubnetMaskIp(mask(10)).toString()).toBe('255.192.0.0');
expect(createSubnetMaskIp(mask(20)).toString()).toBe('255.255.240.0');
expect(createSubnetMaskIp(mask(30)).toString()).toBe('255.255.255.252');
expect(createSubnetMaskIp(mask(32)).toString()).toBe('255.255.255.255');
});
it('getNetworkAddress', () => {
const ipm = (f:number, s:number, t:number, fr:number, m:number) =>
new IpAddressWithSubnetMask(new IpAddress(f,s,t,fr), m);
expect(getNetworkAddress(ipm(192,188,107,11, 12)).toString())
.toBe('192.176.0.0');
expect(getNetworkAddress(ipm(192,168,123,1, 20)).toString())
.toBe('192.168.112.0');
expect(getNetworkAddress(ipm(192,168,123,1, 23)).toString())
.toBe('192.168.122.0');
expect(getNetworkAddress(ipm(192,168,5,125, 28)).toString())
.toBe('192.168.5.112');
expect(getNetworkAddress(ipm(255,255,255,253, 30)).toString())
.toBe('255.255.255.252');
});
it('getBroadcastAddress', () => {
const ipm = (f:number, s:number, t:number, fr:number, m:number) =>
new IpAddressWithSubnetMask(new IpAddress(f,s,t,fr), m);
expect(getBroadCastAddress(ipm(192,188,107,11, 12)).toString())
.toBe('192.191.255.255');
expect(getBroadCastAddress(ipm(192,168,123,1, 20)).toString())
.toBe('192.168.127.255');
expect(getBroadCastAddress(ipm(192,168,123,1, 23)).toString())
.toBe('192.168.123.255');
expect(getBroadCastAddress(ipm(192,168,5,125, 28)).toString())
.toBe('192.168.5.127');
expect(getBroadCastAddress(ipm(255,255,255,253, 30)).toString())
.toBe('255.255.255.255');
});
describe('getNetworkClass tests', () => {
it('detects class a', () => {
expect(getNetworkClass(new IpAddress(1, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(55, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(97, 0, 0, 0))).toBe('a');
expect(getNetworkClass(new IpAddress(127, 0, 0, 0))).toBe('a');
});
it('detects class b', () => {
expect(getNetworkClass(new IpAddress(128, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(134, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(180, 0, 0, 0))).toBe('b');
expect(getNetworkClass(new IpAddress(191, 0, 0, 0))).toBe('b');
});
it('detects class c', () => {
expect(getNetworkClass(new IpAddress(192, 0, 0, 0))).toBe('c');
expect(getNetworkClass(new IpAddress(218, 0, 0, 0))).toBe('c');
expect(getNetworkClass(new IpAddress(223, 0, 0, 0))).toBe('c');
});
it('detects class d', () => {
expect(getNetworkClass(new IpAddress(224, 0, 0, 0))).toBe('d');
expect(getNetworkClass(new IpAddress(234, 0, 0, 0))).toBe('d');
expect(getNetworkClass(new IpAddress(239, 0, 0, 0))).toBe('d');
});
it('detects class e', () => {
expect(getNetworkClass(new IpAddress(240, 0, 0, 0))).toBe('e');
expect(getNetworkClass(new IpAddress(241, 0, 0, 0))).toBe('e');
expect(getNetworkClass(new IpAddress(255, 0, 0, 0))).toBe('e');
});
});
});

View File

@@ -0,0 +1,83 @@
import { createSubnetMaskByte } from "../core/byte";
import { flipBitsToOne, flipBitsToZero } from '../core/byte';
import { IpAddress, IpAddressWithSubnetMask, NetworkClass } from "./models";
function createSubnetMaskIp(ipm: IpAddressWithSubnetMask) : IpAddress {
const mask = createSubnetMaskByte;
const maskBits = ipm.maskBits;
if (maskBits <= 8) {
return new IpAddress(mask(maskBits), 0, 0, 0);
}
else if (maskBits <= 16) {
return new IpAddress(255, mask(maskBits - 8), 0, 0);
}
else if (maskBits <= 24) {
return new IpAddress(255, 255, mask(maskBits - 16), 0);
}
else {
return new IpAddress(255, 255, 255, mask(maskBits - 24));
}
}
function getNetworkAddress(ipm: IpAddressWithSubnetMask) : IpAddress {
return flipSubnetMaskBits(ipm, flipBitsToZero, 0);
}
function getBroadCastAddress(ipm: IpAddressWithSubnetMask) : IpAddress {
return flipSubnetMaskBits(ipm, flipBitsToOne, 255);
}
function flipSubnetMaskBits(ipm: IpAddressWithSubnetMask, flipper : FlipFunction, fullByte: number) {
// Cannot treat ip address as a single number operation because 244 << 24 results in a negative number in JS
const flip = (maskBits: number, byte: number) => flipper(byte, 8 - maskBits);
const ip = ipm.ipAddress;
const maskBits = ipm.maskBits;
if (maskBits <= 8) {
return new IpAddress(flip(maskBits, ip.firstByte), fullByte, fullByte, fullByte);
}
else if (maskBits <= 16) {
return new IpAddress(ip.firstByte, flip(maskBits - 8, ip.secondByte), fullByte, fullByte);
}
else if (maskBits <= 24) {
return new IpAddress(ip.firstByte, ip.secondByte, flip(maskBits - 16, ip.thirdByte), fullByte);
}
else
return new IpAddress(ip.firstByte, ip.secondByte, ip.thirdByte, flip(maskBits - 24, ip.fourthByte));
}
function getNetworkClass (ipAddress: IpAddress) : NetworkClass {
const byte = ipAddress.firstByte;
const firstBitOne = (byte & 128) === 128;
const firstBitZero = (byte & 128) === 0;
const secondBitOne = (byte & 64) === 64;
const thirdBitOne = (byte & 32) === 32;
const thirdBitZero = (byte & 32) === 0;
const forthBitZero = (byte & 16) === 0;
const forthBitOne = (byte & 16) === 16;
// e: 1111
if(firstBitOne && secondBitOne && thirdBitOne && forthBitOne)
return 'e';
if(firstBitOne && secondBitOne && thirdBitOne && forthBitZero) // Start bits: 1110;
return 'd';
if(firstBitOne && secondBitOne && thirdBitZero) // Start bits: 110;
return 'c';
return firstBitOne ? 'b' : 'a';
};
type FlipFunction = (byte: number, numberOfBits: number) => number;
export {createSubnetMaskIp, getBroadCastAddress, getNetworkAddress, getNetworkClass};

View File

@@ -0,0 +1 @@
.debug-indicators { position: absolute; top: 1em; left: 1em}

View File

@@ -1,5 +1,6 @@
import AppState from "../AppState";
import React from "react";
import './DebugIndicators.css';
function DebugIndicators(props: {appState: AppState}) {
@@ -21,8 +22,8 @@ function DebugIndicators(props: {appState: AppState}) {
if(list.length == 0)
return null;
return <div>
{list.map(i => <span>{i}&nbsp;</span>)}
return <div className="debug-indicators">
{list.map(i => <span title={i}>[{i.substring(0,1)}]&nbsp;</span>)}
</div>
}

View File

@@ -1,5 +1,5 @@
.top-links { position: absolute; right: 10px; top: 10px; list-style-type: none; margin: 0 }
.top-links { position: absolute; right: 1em; top: 1em; list-style-type: none; margin: 0 }
.top-links li { float: left; }
.top-links a { display: inline-block; padding: 10px 15px}
.top-links .icon { margin-right: 5px; vertical-align: middle; }

View File

@@ -7,6 +7,7 @@ import ErrorResultView from './components/ErrorResultView';
import HelpResultView from './components/HelpResultView';
import TextResultView from './components/TextResultView';
import WhatsnewResultView from './components/WhatsNewResultView';
import {STARTUP_COMMAND_KEY} from './startup';
const shellModule = {
setup: function(appState: AppState, cmd: CmdShell) {
@@ -29,6 +30,35 @@ const shellModule = {
appState.addCommandResult(c.input, <TextResultView text={`Debug Mode: ${appState.debugMode}`}/>);
});
if(appState.env !== 'prod') {
// Default command for development purposes
cmd.command({
canHandle: (s: string) => s.indexOf('default') === 0,
handle: (s: CommandInput) => {
const executeCommand = (c: string) => {
console.log(c);
if(c.length === 0) {
return "Default comand: " + localStorage.getItem(STARTUP_COMMAND_KEY);
}
else if(c === 'clear') {
localStorage.removeItem(STARTUP_COMMAND_KEY);
return "Default startup command cleared";
}
localStorage.setItem(STARTUP_COMMAND_KEY, c);
return `Default startup command saved: ${c}`;
};
const command = s.input.substring(7).trim();
const result = executeCommand(command);
appState.addCommandResult(s.input, <TextResultView text={result} />);
}
});
};
cmd.onError((input: string, err: Error) => appState.addCommandResult(input, <ErrorResultView errorMessage={err.toString()} />));
}
}

View File

@@ -3,12 +3,16 @@ import hash from '../core/hash';
import AppState from './AppState';
import { Env } from './interfaces';
import appStateStore from './appStateStore';
import CommandLink from '../core/components/CommandLink';
export type StartupAppData = {
appState: AppState,
startupCommands: string[]
}
const STARTUP_COMMAND_KEY = 'StartupCommand';
const DEFAULT_COMMANDS = ['help', '127.0.0.1 192.168.0.0/8', '1|2&6','4 0b1000000 0x80'];
function bootstrapAppData() : StartupAppData {
const env = window.location.host === "bitwisecmd.com" ? 'prod' : 'stage';
@@ -35,7 +39,10 @@ function createAppState(env:string) {
function getStartupCommands(appState : AppState) : string[] {
var hashArgs = hash.getArgs(window.location.hash);
var startupCommands = ['help', '127.0.0.1 192.168.0.0/8', '1|2&6','4 0b1000000 0x80'];
var startupCommands = loadStoredCommands();
if(startupCommands.length == 0)
startupCommands = DEFAULT_COMMANDS;
if(appState.wasOldVersion) {
startupCommands = ["whatsnew"];
@@ -50,6 +57,11 @@ function getStartupCommands(appState : AppState) : string[] {
return startupCommands;
}
function loadStoredCommands() : string[] {
const json = localStorage.getItem(STARTUP_COMMAND_KEY);
return json != null ? [json] : [];
}
function setupLogger(env: Env) {
if(env != 'prod'){
log.setLevel("debug");
@@ -59,4 +71,5 @@ function setupLogger(env: Env) {
}
}
export {STARTUP_COMMAND_KEY};
export default bootstrapAppData;