diff --git a/package.json b/package.json index 6a60266..5f0f807 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "main": "src/index.js", "license": "MIT", "scripts": { - "dev": "nodemon --ignore ./dist/ --watch ./src -e ts --exec 'tsc && node ./dist/index.js'", + "dev:tsc": "tsc --watch", + "dev:nodemon": "nodemon --watch ./dist ./dist/index.js", "start": "tsc && node ./dist/index.js" }, "devDependencies": { @@ -12,5 +13,8 @@ "@types/node": "^16.7.13", "nodemon": "^2.0.12", "typescript": "^4.4.2" + }, + "dependencies": { + "table-layout": "^3.0.0" } } diff --git a/src/communicator.ts b/src/communicator.ts index a568232..c729987 100644 --- a/src/communicator.ts +++ b/src/communicator.ts @@ -3,13 +3,29 @@ import { FLAP, SNAC, TLV, TLVType } from './structures'; import { logDataStream } from './util'; import { FLAGS_EMPTY } from './consts'; -import AuthorizationRegistrationService from "./services/authorization-registration"; +import GenericServiceControls from "./services/0x01-GenericServiceControls"; +import LocationServices from "./services/0x02-LocationSerices"; +import BuddyListManagement from "./services/0x03-BuddyListManagement"; +import ICBM from "./services/0x04-ICBM"; +import Invitation from "./services/0x06-Invitation"; +import Administration from "./services/0x07-Administration"; +import Popups from "./services/0x08-Popups"; +import PrivacyManagement from "./services/0x09-PrivacyManagement"; +import UserLookup from "./services/0x0a-UserLookup"; +import UsageStats from "./services/0x0b-UsageStats"; +import ChatNavigation from "./services/0x0d-ChatNavigation"; +import Chat from "./services/0x0e-Chat";; +import DirectorySearch from "./services/0x0f-DirectorySearch"; +import ServerStoredBuddyIcons from "./services/0x10-ServerStoredBuddyIcons"; +import SSI from "./services/0x13-SSI"; +import AuthorizationRegistrationService from "./services/0x17-AuthorizationRegistration"; + import BaseService from "./services/base"; export default class Communicator { private _sequenceNumber = 0; - private services : {[key: number]: BaseService} = {}; + public services : {[key: number]: BaseService} = {}; constructor(public socket : net.Socket) { // Hold on to the socket @@ -17,9 +33,9 @@ export default class Communicator { this.socket.on('data', (data : Buffer) => { console.log('DATA-----------------------'); + console.log('RAW\n' + logDataStream(data)); const flap = FLAP.fromBuffer(data); console.log('RECV', flap.toString()); - console.log('RAW\n' + logDataStream(data)); this.handleMessage(flap); }); @@ -29,7 +45,6 @@ export default class Communicator { start() { // Start negotiating a connection - console.log(FLAP, typeof FLAP); const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01])); this.send(hello); } @@ -37,6 +52,21 @@ export default class Communicator { registerServices() { const services = [ new AuthorizationRegistrationService(this), + new GenericServiceControls(this), + new LocationServices(this), + new BuddyListManagement(this), + new ICBM(this), + new Invitation(this), + new Administration(this), + new Popups(this), + new PrivacyManagement(this), + new UserLookup(this), + new UsageStats(this), + new ChatNavigation(this), + new Chat(this), + new DirectorySearch(this), + new ServerStoredBuddyIcons(this), + new SSI(this), ]; this.services = {}; @@ -86,9 +116,7 @@ export default class Communicator { servicesOffered.push(Buffer.from([0x00, service.family])); }); const resp = new FLAP(2, this._getNewSequenceNumber(), - new SNAC(0x01, 0x03, FLAGS_EMPTY, 0, [ - Buffer.concat(servicesOffered), - ])); + new SNAC(0x01, 0x03, FLAGS_EMPTY, 0, Buffer.concat(servicesOffered))); this.send(resp); return; } @@ -107,6 +135,7 @@ export default class Communicator { } familyService.handleMessage(message); + return; default: console.warn('No handlers for channel', message.channel); return; diff --git a/src/services/0x01-GenericServiceControls.ts b/src/services/0x01-GenericServiceControls.ts new file mode 100644 index 0000000..2ffdfde --- /dev/null +++ b/src/services/0x01-GenericServiceControls.ts @@ -0,0 +1,40 @@ +import BaseService from './base'; +import Communicator from '../communicator'; +import { FLAP, Rate, RateClass, RatedServiceGroup, RateGroupPair, SNAC } from '../structures'; +import { FLAGS_EMPTY } from '../consts'; + +export default class GenericServiceControls extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x01, version: 0x03}, communicator) + } + + override handleMessage(message : FLAP) { + if (!(message.payload instanceof SNAC)) { + throw new Error('Require SNAC'); + } + + if (message.payload.service === 0x06) { // Client ask server for rate limits info + const resp = new FLAP(0x02, this._getNewSequenceNumber(), + SNAC.forRateClass(0x01, 0x07, FLAGS_EMPTY, 0, [ + new Rate( + new RateClass(1, 80, 2500, 2000, 1500, 800, 3400 /*fake*/, 6000, 0, 0), + new RatedServiceGroup(1, [new RateGroupPair(0x00, 0x00)]) + ) + ])) + this.send(resp); + return; + } + + + if (message.payload.service === 0x17) { + const serviceVersions : Buffer[] = []; + Object.values(this.communicator.services).forEach((service) => { + serviceVersions.push(Buffer.from([0x00, service.family, 0x00, service.version])); + }); + const resp = new FLAP(0x02, this._getNewSequenceNumber(), + new SNAC(0x01, 0x18, FLAGS_EMPTY, 0, Buffer.concat(serviceVersions))); + this.send(resp); + return; + } + } +} diff --git a/src/services/0x02-LocationSerices.ts b/src/services/0x02-LocationSerices.ts new file mode 100644 index 0000000..823cf34 --- /dev/null +++ b/src/services/0x02-LocationSerices.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class LocationServices extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x02, version: 0x01}, communicator) + } +} diff --git a/src/services/0x03-BuddyListManagement.ts b/src/services/0x03-BuddyListManagement.ts new file mode 100644 index 0000000..8611f86 --- /dev/null +++ b/src/services/0x03-BuddyListManagement.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class BuddyListManagement extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x03, version: 0x01}, communicator) + } +} diff --git a/src/services/0x04-ICBM.ts b/src/services/0x04-ICBM.ts new file mode 100644 index 0000000..6e6ce2d --- /dev/null +++ b/src/services/0x04-ICBM.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class ICBM extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x04, version: 0x01}, communicator) + } +} diff --git a/src/services/0x06-Invitation.ts b/src/services/0x06-Invitation.ts new file mode 100644 index 0000000..2781758 --- /dev/null +++ b/src/services/0x06-Invitation.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class Invitation extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x06, version: 0x01}, communicator) + } +} diff --git a/src/services/0x07-Administration.ts b/src/services/0x07-Administration.ts new file mode 100644 index 0000000..c3f8f41 --- /dev/null +++ b/src/services/0x07-Administration.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class Administration extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x07, version: 0x01}, communicator) + } +} diff --git a/src/services/0x08-Popups.ts b/src/services/0x08-Popups.ts new file mode 100644 index 0000000..6f1e4d3 --- /dev/null +++ b/src/services/0x08-Popups.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class Popups extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x08, version: 0x01}, communicator) + } +} diff --git a/src/services/0x09-PrivacyManagement.ts b/src/services/0x09-PrivacyManagement.ts new file mode 100644 index 0000000..d0329e6 --- /dev/null +++ b/src/services/0x09-PrivacyManagement.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class PrivacyManagement extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x09, version: 0x01}, communicator) + } +} diff --git a/src/services/0x0a-UserLookup.ts b/src/services/0x0a-UserLookup.ts new file mode 100644 index 0000000..b1d7d66 --- /dev/null +++ b/src/services/0x0a-UserLookup.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class UserLookup extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x0a, version: 0x01}, communicator) + } +} diff --git a/src/services/0x0b-UsageStats.ts b/src/services/0x0b-UsageStats.ts new file mode 100644 index 0000000..29d5b07 --- /dev/null +++ b/src/services/0x0b-UsageStats.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class UsageStats extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x0b, version: 0x01}, communicator) + } +} diff --git a/src/services/0x0d-ChatNavigation.ts b/src/services/0x0d-ChatNavigation.ts new file mode 100644 index 0000000..b805c0f --- /dev/null +++ b/src/services/0x0d-ChatNavigation.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class ChatNavigation extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x0d, version: 0x02}, communicator) + } +} diff --git a/src/services/0x0e-Chat.ts b/src/services/0x0e-Chat.ts new file mode 100644 index 0000000..23e7740 --- /dev/null +++ b/src/services/0x0e-Chat.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class Chat extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x0e, version: 0x01}, communicator) + } +} diff --git a/src/services/0x0f-DirectorySearch.ts b/src/services/0x0f-DirectorySearch.ts new file mode 100644 index 0000000..6f49a89 --- /dev/null +++ b/src/services/0x0f-DirectorySearch.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class DirectorySearch extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x0f, version: 0x01}, communicator) + } +} diff --git a/src/services/0x10-ServerStoredBuddyIcons.ts b/src/services/0x10-ServerStoredBuddyIcons.ts new file mode 100644 index 0000000..b61eb19 --- /dev/null +++ b/src/services/0x10-ServerStoredBuddyIcons.ts @@ -0,0 +1,8 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +export default class ServerStoredBuddyIcons extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x10, version: 0x01}, communicator) + } +} diff --git a/src/services/0x13-SSI.ts b/src/services/0x13-SSI.ts new file mode 100644 index 0000000..c388542 --- /dev/null +++ b/src/services/0x13-SSI.ts @@ -0,0 +1,9 @@ +import BaseService from './base'; +import Communicator from '../communicator'; + +// SSI is Server Stored Information +export default class SSI extends BaseService { + constructor(communicator : Communicator) { + super({family: 0x10, version: 0x01}, communicator) + } +} diff --git a/src/services/authorization-registration.ts b/src/services/0x17-AuthorizationRegistration.ts similarity index 79% rename from src/services/authorization-registration.ts rename to src/services/0x17-AuthorizationRegistration.ts index 99e7acf..8dedb05 100644 --- a/src/services/authorization-registration.ts +++ b/src/services/0x17-AuthorizationRegistration.ts @@ -25,14 +25,14 @@ export default class AuthorizationRegistrationService extends BaseService { switch (message.payload.service) { case 0x02: // Client login request (md5 login sequence) - const tlvs = message.payload.tlvs; - const clientNameTLV = tlvs.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.ClientName); + const payload = message.payload.payload; + const clientNameTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.ClientName); if (!clientNameTLV || !(clientNameTLV instanceof TLV)) { return; } console.log("Attempting connection from", clientNameTLV.payload.toString('ascii')); - const userTLV = tlvs.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.User); + const userTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.User); if (!userTLV || !(userTLV instanceof TLV)) { return; } @@ -50,7 +50,7 @@ export default class AuthorizationRegistrationService extends BaseService { return; } - const passwordHashTLV = tlvs.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.PasswordHash); + const passwordHashTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.PasswordHash); if (!passwordHashTLV || !(passwordHashTLV instanceof TLV)) { return; } @@ -76,18 +76,18 @@ export default class AuthorizationRegistrationService extends BaseService { new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ TLV.forUsername(username), // username TLV.forBOSAddress('10.0.1.29:5190'), // BOS address - TLV.forCookie('im a cookie uwu') // Authorization cookie + TLV.forCookie(JSON.stringify({cookie: 'uwu', user: 'toof'})) // Authorization cookie ])); this.send(authResp); return; case 0x06: // Request md5 authkey - const payload = Buffer.alloc(2, 0xFF, 'hex'); - payload.writeUInt16BE(this.cipher.length); + const MD5AuthKeyHeader = Buffer.alloc(2, 0xFF, 'hex'); + MD5AuthKeyHeader.writeUInt16BE(this.cipher.length); const md5ReqResp = new FLAP(2, this._getNewSequenceNumber(), - new SNAC(0x17, 0x07, FLAGS_EMPTY, 0, [ - Buffer.concat([payload, Buffer.from(this.cipher, 'binary')]), - ])); + new SNAC(0x17, 0x07, FLAGS_EMPTY, 0, + Buffer.concat([MD5AuthKeyHeader, Buffer.from(this.cipher, 'binary')]), + )); this.send(md5ReqResp); break; } diff --git a/src/services/base.ts b/src/services/base.ts index 51c4505..f08643d 100644 --- a/src/services/base.ts +++ b/src/services/base.ts @@ -25,6 +25,7 @@ export default class BaseService { } handleMessage(message : FLAP) : void { - return; + throw new Error(''+ + `Unhandled message for family ${this.family.toString(16)} supporting version ${this.version.toString(16)}`); } } diff --git a/src/structures/SNAC.ts b/src/structures/SNAC.ts index 4224b13..518a9f8 100644 --- a/src/structures/SNAC.ts +++ b/src/structures/SNAC.ts @@ -1,41 +1,120 @@ import assert from "assert"; import { TLV } from "./TLV"; +export class RateClass { + constructor( + public ID: number, + public WindowSize: number, + public ClearLevel: number, + public AlertLevel: number, + public LimitLevel: number, + public DisconnectLevel: number, + public CurrentLevel: number, + public MaxLevel: number, + public LastTime: number, + public CurrentStat: number, + ){} + + toBuffer() : Buffer { + const buf = Buffer.alloc(35, 0x00); + buf.writeUInt16BE(this.ID, 0); + buf.writeUInt32BE(this.WindowSize, 2); + buf.writeUInt32BE(this.ClearLevel, 6); + buf.writeUInt32BE(this.AlertLevel, 10); + buf.writeUInt32BE(this.LimitLevel, 14); + buf.writeUInt32BE(this.DisconnectLevel,18); + buf.writeUInt32BE(this.CurrentLevel, 22); + buf.writeUInt32BE(this.MaxLevel, 26); + buf.writeUInt32BE(this.LastTime, 30); + buf.writeUInt8(this.CurrentStat, 34); + return buf; + } +} + +export class RateGroupPair { + constructor(public family : number, public service : number) {} + toBuffer() : Buffer { + const buf = Buffer.alloc(4, 0x00); + buf.writeInt16BE(this.family, 0); + buf.writeInt16BE(this.service, 2); + return buf; + } +} + +export class RatedServiceGroup { + constructor(public rateGroupID : number, public pairs : RateGroupPair[]){} + + toBuffer() : Buffer { + const ratedServiceGroupHeader = Buffer.alloc(4, 0x00); + ratedServiceGroupHeader.writeInt16BE(this.rateGroupID); + ratedServiceGroupHeader.writeInt16BE(this.pairs.length, 2); + const pairs = this.pairs.map((pair) => pair.toBuffer()); + return Buffer.concat([ratedServiceGroupHeader, ...pairs]); + } +} + +export class Rate { + constructor(public rateClass : RateClass, public ratedServiceGroup : RatedServiceGroup) {} + toBuffer() : Buffer { + return Buffer.concat([this.rateClass.toBuffer(), this.ratedServiceGroup.toBuffer()]); + } +} + export class SNAC { + constructor(public family : number, public service : number, public flags : Buffer, public requestID : number , public payload : (TLV[] | Buffer) = Buffer.alloc(0)) { + this.family = family; + this.service = service; + this.flags = flags; + this.requestID = requestID; + this.payload = payload; + } + static fromBuffer(buf : Buffer, payloadLength = 0) { assert(buf.length >= 10, 'Expected 10 bytes for SNAC header'); const family = buf.slice(0,2).readInt16BE(0); const service = buf.slice(2,4).readInt16BE(0); const flags = buf.slice(4, 6); const requestID = buf.slice(6, 10).readInt32BE(0); - const tlvs : TLV[] = []; // SNACs can have multiple TLVs + let payload : Buffer | TLV[]; // SNACs can have multiple payload - let tlvsIdx = 10; - let cb = 0, cbLimit = 20; //circuit breaker - while (tlvsIdx < payloadLength && cb < cbLimit) { - const tlv = TLV.fromBuffer(buf.slice(tlvsIdx)); - tlvs.push(tlv); - tlvsIdx += tlv.length + 4; // 4 bytes for TLV type + tlvs length - cb++; + // Some SNACs don't have TLV payloads + if (family === 0x01 && service === 0x17 || + family === 0x01 && service === 0x07 || + family === 0x01 && service === 0x08 || + family === 0x01 && service === 0x0e) { + payload = buf.slice(10, 10 + payloadLength); + } else { + payload = []; + // Try to parse TLVs + let payloadIdx = 10; + let cb = 0, cbLimit = 20; //circuit breaker + while (payloadIdx < payloadLength && cb < cbLimit) { + const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); + payload.push(tlv); + payloadIdx += tlv.length + 4; // 4 bytes for TLV type + payload length + cb++; + } + if (cb === cbLimit) { + console.error('Application error, cb limit reached'); + process.exit(1); + } } - if (cb === cbLimit) { - console.error('Application error, cb limit reached'); - process.exit(1); - } - - return new SNAC(family, service, flags, requestID, tlvs); + + return new SNAC(family, service, flags, requestID, payload); } - constructor(public family : number, public service : number, public flags : Buffer, public requestID : number , public tlvs : Array = []) { - this.family = family; - this.service = service; - this.flags = flags; - this.requestID = requestID; - this.tlvs = tlvs; + static forRateClass(family : number, service : number, flags : Buffer, requestID : number, rates : Rate[]) : SNAC { + const payloadHeader = Buffer.alloc(2, 0x00); + payloadHeader.writeUInt16BE(rates.length); + + const payloadBody = rates.map((rateClass) => rateClass.toBuffer()); + const payload = Buffer.concat([payloadHeader, ...payloadBody]); + + return new SNAC(family, service, flags, requestID, payload); } toString() { - return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.tlvs}`; + return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.payload}`; } toBuffer() { @@ -45,12 +124,12 @@ export class SNAC { SNACHeader.set(this.flags, 4); SNACHeader.writeUInt32BE(this.requestID, 6); - const payload = this.tlvs.map((thing) => { - if (thing instanceof TLV) { - return thing.toBuffer(); - } - return thing; - }); + let payload : Buffer[] = []; + if (this.payload instanceof Buffer) { + payload = [this.payload]; + } else if (this.payload.length && this.payload[0] instanceof TLV) { + payload = (this.payload as TLV[]).map((thing : TLV) => thing.toBuffer()); + } return Buffer.concat([SNACHeader, ...payload]); } diff --git a/src/util.ts b/src/util.ts index b9396ad..916f15e 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,4 +1,5 @@ -function chunkString(str : string, len : number) { +import { FLAP } from './structures' +export function chunkString(str : string, len : number) { const size = Math.ceil(str.length/len) const r = Array(size) let offset = 0 @@ -15,3 +16,126 @@ export function logDataStream(data : Buffer){ const strs = chunkString(data.toString('hex'), 16); return strs.map((str) => chunkString(str, 2).join(' ')).join('\n'); } + +// Experiment to provide descriptive print-outs of the data structures +// @ts-ignore +import Table from 'table-layout'; + +type DataSize = -1 | 8 | 16 | 32; +interface Spec { + description: string, + size : DataSize, + isRepeat? : boolean, + repeatSpecs?: Spec[], +} + +function byte(description : string) : Spec { + return {size: 8, description} +} + +function word(description : string) : Spec { + return {size: 16, description}; +} + +function dword(description : string) : Spec { + return {size: 32, description}; +} + +function repeat(size: DataSize, description : string, specs : Spec[]) : Spec { + return {size, description, isRepeat: true, repeatSpecs: specs}; +} + +const FLAPSpec = [ + byte("FLAP Header"), + byte("Channel"), + word("Sequence ID"), + word("Payload Length"), + word("SNAC Family"), + word("SNAC Subtype"), + word("SNAC Flags"), + dword("SNAC Request-ID"), + repeat(16, "Number of rate classes", [ + word("Rate Class ID"), + dword("Window Size"), + dword("Clear Level"), + dword("Alert Level"), + dword("Limit Level"), + dword("Disconnect Level"), + dword("Current Level"), + dword("Max Level"), + dword("Last Time"), + byte("Current State") + ]), + repeat(-1, "", [ + word("Rate Group ID"), + repeat(16, "Number of pairs in group", [ + dword("Family/Subtype pair"), + ]), + ]), +]; + +function parseBuffer(buf : Buffer, spec : Spec[], repeatTimes = 0) { + let offset = 0; + let rows = []; + let repeat = repeatTimes; + + for (let section of spec) { + let value : number = 0; + if (section.size === 8) { + const bufStr = buf.slice(offset, offset + 1).toString('hex'); + value = buf.readInt8(offset); + rows.push([chunkString(bufStr, 2).join(' '), value, section.description]); + offset += 1; + } else if (section.size === 16) { + const bufStr = buf.slice(offset, offset + 2).toString('hex'); + value = buf.readUInt16BE(offset); + rows.push([chunkString(bufStr, 2).join(' '), value, section.description]); + offset += 2; + } else if (section.size === 32) { + const bufStr = buf.slice(offset, offset + 4).toString('hex'); + value = buf.readUInt32BE(offset); + rows.push([chunkString(bufStr, 2).join(' '), value, section.description]); + offset += 4; + } + + if (section.isRepeat && section.repeatSpecs) { + if (section.size !== -1) { + repeat = value; + } + + let specs : Spec[] = []; + for (let i = 0; i < repeat; i++) { + specs.push(...section.repeatSpecs); + } + + const subrows : any[] = parseBuffer(buf.slice(offset), specs, repeat); + rows.push(...subrows); + } + } + + return rows; +} + +function printBuffer(buf : Buffer, spec : Spec[]) { + const rows = parseBuffer(buf, spec); + console.log((new Table(rows)).toString()); +} + +function bufferFromWebText(webtext : string) : Buffer { + return Buffer.from(webtext.replace(/\s/g, ''), 'hex'); +} + +const exampleWebText = ''+ +` +2a 02 00 03 00 37 00 01 +00 07 00 00 00 00 00 00 +00 01 00 01 00 00 00 50 +00 00 09 c4 00 00 07 d0 +00 00 05 dc 00 00 03 20 +00 00 0d 48 00 00 17 70 +00 00 00 00 00 00 01 00 +01 00 00 00 00 ` + +if (require.main === module) { + printBuffer(bufferFromWebText(exampleWebText), FLAPSpec); +} diff --git a/yarn.lock b/yarn.lock index 1c3c4e7..3fe0547 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,14 @@ # yarn lockfile v1 +"@75lb/deep-merge@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@75lb/deep-merge/-/deep-merge-1.1.1.tgz#3b06155b90d34f5f8cc2107d796f1853ba02fd6d" + integrity sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw== + dependencies: + lodash.assignwith "^4.2.0" + typical "^7.1.1" + "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -46,6 +54,13 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -61,6 +76,21 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +array-back@^3.0.1, array-back@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0" + integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q== + +array-back@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e" + integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg== + +array-back@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.0.tgz#83cc80fbef5a46269b1f6ecc82011cfc19cf1c1e" + integrity sha512-mixVv03GOOn/ubHE4STQ+uevX42ETdk0JoMVEjNkSOCT7WgERh7C8/+NyhWYNpE3BN69pxFyJIBcF7CxWz/+4A== + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -118,6 +148,15 @@ camelcase@^5.3.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + chalk@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" @@ -158,6 +197,13 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -165,11 +211,36 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +command-line-args@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.0.tgz#087b02748272169741f1fd7c785b295df079b9be" + integrity sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A== + dependencies: + array-back "^3.1.0" + find-replace "^3.0.0" + lodash.camelcase "^4.3.0" + typical "^4.0.0" + +command-line-usage@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.1.tgz#c908e28686108917758a49f45efb4f02f76bc03f" + integrity sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA== + dependencies: + array-back "^4.0.1" + chalk "^2.4.2" + table-layout "^1.0.1" + typical "^5.2.0" + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -213,7 +284,7 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -deep-extend@^0.6.0: +deep-extend@^0.6.0, deep-extend@~0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== @@ -257,6 +328,11 @@ escape-goat@^2.0.0: resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -264,6 +340,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +find-replace@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38" + integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ== + dependencies: + array-back "^3.0.1" + fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -457,6 +540,16 @@ latest-version@^5.0.0: dependencies: package-json "^6.3.0" +lodash.assignwith@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb" + integrity sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs= + +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= + lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" @@ -603,6 +696,11 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reduce-flatten@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27" + integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w== + registry-auth-token@^4.0.0: version "4.2.1" resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250" @@ -646,6 +744,11 @@ signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== +stream-read-all@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/stream-read-all/-/stream-read-all-3.0.1.tgz#60762ae45e61d93ba0978cda7f3913790052ad96" + integrity sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A== + string-width@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" @@ -683,7 +786,7 @@ strip-json-comments@~2.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -supports-color@^5.5.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== @@ -697,6 +800,29 @@ supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +table-layout@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04" + integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A== + dependencies: + array-back "^4.0.1" + deep-extend "~0.6.0" + typical "^5.2.0" + wordwrapjs "^4.0.0" + +table-layout@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-3.0.0.tgz#bd0f207ff73eb4dba79019e4f672e61874181b7f" + integrity sha512-chMCAvqzsw2k0WVZLQwyiNfHV3lwLdrmTKaofEt3PhedSVdEwOK8/nGetZFaQEg9HjPL9CrhkJWc4ZauFCihXw== + dependencies: + "@75lb/deep-merge" "^1.1.0" + array-back "^6.2.0" + command-line-args "^5.2.0" + command-line-usage "^6.1.1" + stream-read-all "^3.0.1" + typical "^7.1.1" + wordwrapjs "^5.1.0" + term-size@^2.1.0: version "2.2.1" resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54" @@ -738,6 +864,21 @@ typescript@^4.4.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86" integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ== +typical@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4" + integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw== + +typical@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066" + integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg== + +typical@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/typical/-/typical-7.1.1.tgz#ba177ab7ab103b78534463ffa4c0c9754523ac1f" + integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA== + undefsafe@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae" @@ -785,6 +926,19 @@ widest-line@^3.1.0: dependencies: string-width "^4.0.0" +wordwrapjs@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f" + integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA== + dependencies: + reduce-flatten "^2.0.0" + typical "^5.2.0" + +wordwrapjs@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-5.1.0.tgz#4c4d20446dcc670b14fa115ef4f8fd9947af2b3a" + integrity sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg== + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"