Refactor towards modular structure

This commit is contained in:
Borys_Levytskyi
2021-01-14 20:48:19 +02:00
parent f671b32b63
commit 50d1606105
31 changed files with 221 additions and 154 deletions

View File

@@ -0,0 +1 @@
.ip-address-info { padding-top: 1em; font-size: 0.85em; vertical-align: middle; display: none;}

View File

@@ -0,0 +1,82 @@
import React from 'react';
import { IpAddress, OctetNumber, getNetworkClass } from '../ip';
import formatter from '../../core/formatter'
import BinaryStringView from '../../core/components/BinaryString';
import './IpAddressView.css';
type IpAddressViewProps = {
ipAddresses: IpAddress[]
};
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}>
<td className="label"><strong>{ip.toString()}</strong></td>
<td className="bin">
{this.bin(ip.firstByte, 1, ip)}<span className="soft">.</span>
{this.bin(ip.secondByte, 2, ip)}<span className="soft">.</span>
{this.bin(ip.thirdByte, 3, ip)}<span className="soft">.</span>
{this.bin(ip.fourthByte, 4, ip)}
</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>;
}
bin(value: number, octetNumber: OctetNumber, ip: IpAddress) {
return <BinaryStringView
binaryString={fmt(value)}
key={octetNumber}
emphasizeBytes={false}
allowFlipBits={true}
className={`octet-${octetNumber}`}
onFlipBit={e => this.onFlippedBit(e.newBinaryString, octetNumber, ip)} />;
}
onFlippedBit(binaryString: string, number: OctetNumber, ip : IpAddress) {
ip.setOctet(number, parseInt(binaryString, 2));
this.forceUpdate();
}
};
function fmt(num: number) : string {
return formatter.padLeft(formatter.formatString(num, 'bin'), 8, '0');
}
export default IpAddressView;

104
src/networking/ip.test.ts Normal file
View File

@@ -0,0 +1,104 @@
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');
});
});

183
src/networking/ip.ts Normal file
View File

@@ -0,0 +1,183 @@
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};

47
src/networking/module.tsx Normal file
View File

@@ -0,0 +1,47 @@
import React from 'react';
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 log from 'loglevel';
const networkingAppModule = {
setup: function(appState: AppState, cmd: CmdShell) {
// Add Ip Address commands
cmd.command({
canHandle: (input:string) => ipAddressParser.parse(input) != null,
handle: function(c: CommandInput) {
var result = ipAddressParser.parse(c.input);
if(result == null)
return;
if(result instanceof ParsingError) {
appState.addCommandResult(c.input, <ErrorResultView errorMessage={result.errorMessage} />);
return;
}
const ipAddresses : IpAddress[] = [];
(result as ParsedIpObject[]).forEach(r => {
if(r instanceof IpAddressWithSubnetMask)
{
ipAddresses.push(r.ipAddress);
ipAddresses.push(r.createSubnetMaskIp());
}
else if(r instanceof IpAddress) {
ipAddresses.push(r);
}
});
appState.addCommandResult(c.input, <IpAddressView ipAddresses={ipAddresses} />);
}
});
log.debug();
}
}
export default networkingAppModule;