diff --git a/src/index.js b/src/index.js index ffae389..794c502 100644 --- a/src/index.js +++ b/src/index.js @@ -1,109 +1,5 @@ const net = require('net'); -const assert = require('assert'); - -function chunkString(str, len) { - const size = Math.ceil(str.length/len) - const r = Array(size) - let offset = 0 - - for (let i = 0; i < size; i++) { - r[i] = str.substr(offset, len) - offset += len - } - - return r -} - -function logDataStream(data){ - const strs = chunkString(data.toString('hex'), 16); - return strs.map((str) => chunkString(str, 2).join(' ')).join('\n'); -} - -class TLV { - static fromBuffer(buf) { - const type = buf.slice(0, 2).readInt16BE(0); - const len = buf.slice(2, 4).readInt16BE(0) - const payload = buf.slice(4, 4 + len); - - return new TLV(type, payload); - } - - constructor(type, payload) { - this.type = type; - this.len = payload.length; - this.payload = payload; - } - - toString() { - return `TLV(${this.type}, ${this.len}, ${this.payload.toString('ascii')})`; - } -} - -class SNAC { - static fromBuffer(buf, payloadLength = 0) { - 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 payload = []; // SNACs can have multiple TLVs - - let payloadIdx = 10; - let cb = 0, cbLimit = 10; //circuit breaker - while (payloadIdx < payloadLength && cb < cbLimit) { - const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); - payload.push(tlv); - payloadIdx += tlv.len + 4; // 4 bytes for TLV type + payload length - cb++; - } - if (cb === cbLimit) { - console.error('Application error, cb limit reached'); - process.exit(1); - } - - return new SNAC(family, service, flags, requestID, payload); - } - - constructor(family, service, flags, requestID, payload) { - this.family = family; - this.service = service; - this.flags = flags; - this.requestID = requestID; - this.payload = payload; - } - - toString() { - return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.payload}`; - } -} - -class FLAP { - static fromBuffer(buf) { - assert.equal(buf[0], 0x2a, 'Expected 0x2a FLAP header'); - const channel = parseInt(buf[1], 16); - const datagramNumber = buf.slice(2,4).readInt16BE(0); - const payloadLength = buf.slice(4, 6).readInt16BE(0); - const payload = buf.slice(6, 6 + payloadLength); - - return new FLAP(channel, datagramNumber, payload) - } - - constructor(channel, datagramNumber, payload) { - this.channel = channel; - this.datagramNumber = datagramNumber; - this.payload = payload; - this.payloadLength = this.payload.length; - - if (channel === 2) { - this.payload = SNAC.fromBuffer(this.payload, this.payloadLength); - } - } - - toString() { - const hasSnac = this.payload instanceof SNAC; - const payload = hasSnac ? this.payload.toString() : logDataStream(this.payload).split('\n').join('\n '); - return `ch:${this.channel}, dn: ${this.datagramNumber}, len: ${this.payloadLength}, payload:\n ${payload}` - } -} +const { logDataStream } = require('./util'); const server = net.createServer((socket) => { console.log('client connected...'); @@ -115,6 +11,7 @@ const server = net.createServer((socket) => { socket.on('data', (data) => { const flap = FLAP.fromBuffer(Buffer.from(data, 'hex')); + console.log(logDataStream(flap.toBuffer())); console.log('RECV', flap.toString()); }); @@ -127,8 +24,11 @@ const server = net.createServer((socket) => { console.log('client disconnected...'); }); - const hello = Buffer.from(new Uint8Array([0x2a, 0x01, 0, 0x01, 0, 0x04, 0x00, 0x00, 0x00, 0x01])); - socket.write(hello); + // const hello = Buffer.from(new Uint8Array([0x2a, 0x01, 0, 0x01, 0, 0x04, 0x00, 0x00, 0x00, 0x01])); + + const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01])); + // console.log(logDataStream(hello.toBuffer())); + socket.write(hello.toBuffer()); /* 1. on connection, server sends diff --git a/src/structures.js b/src/structures.js new file mode 100644 index 0000000..63bc5a7 --- /dev/null +++ b/src/structures.js @@ -0,0 +1,133 @@ +const assert = require('assert'); + +const { logDataStream } = require('./util'); + +class TLV { + static fromBuffer(buf) { + const type = buf.slice(0, 2).readInt16BE(0); + const len = buf.slice(2, 4).readInt16BE(0) + const payload = buf.slice(4, 4 + len); + + return new TLV(type, payload); + } + + constructor(type, payload) { + this.type = type; + this.len = payload.length; + this.payload = payload; + } + + toString() { + return `TLV(${this.type}, ${this.len}, ${this.payload.toString('ascii')})`; + } + + toBuffer() { + const TLVHeader = Buffer.alloc(4, 0, 'hex'); + TLVHeader.writeUInt16BE(this.type); + TLVHeader.writeUInt16BE(this.len, 2); + return Buffer.concat([TLVHeader, this.payload]); + } +} + +class SNAC { + static fromBuffer(buf, payloadLength = 0) { + 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 payload = []; // SNACs can have multiple TLVs + + let payloadIdx = 10; + let cb = 0, cbLimit = 10; //circuit breaker + while (payloadIdx < payloadLength && cb < cbLimit) { + const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); + payload.push(tlv); + payloadIdx += tlv.len + 4; // 4 bytes for TLV type + payload length + cb++; + } + if (cb === cbLimit) { + console.error('Application error, cb limit reached'); + process.exit(1); + } + + return new SNAC(family, service, flags, requestID, payload); + } + + constructor(family, service, flags, requestID, payload) { + this.family = family; + this.service = service; + this.flags = flags; + this.requestID = requestID; + this.payload = payload; + } + + toString() { + return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.payload}`; + } + + toBuffer() { + const SNACHeader = Buffer.alloc(10, 0, 'hex'); + SNACHeader.writeUInt16BE(this.family); + SNACHeader.writeUInt16BE(this.service, 2); + SNACHeader.writeUInt16BE(this.flags, 4); + SNACHeader.writeUInt32BE(this.requestID, 6); + + const payload = this.payload.map((tlv) => tlv.toBuffer()); + return Buffer.concat([SNACHeader, ...payload]); + } +} + +class FLAP { + static fromBuffer(buf) { + assert.equal(buf[0], 0x2a, 'Expected 0x2a FLAP header'); + const channel = parseInt(buf[1], 16); + const datagramNumber = buf.slice(2,4).readInt16BE(0); + const payloadLength = buf.slice(4, 6).readInt16BE(0); + const payload = buf.slice(6, 6 + payloadLength); + + return new FLAP(channel, datagramNumber, payload) + } + + constructor(channel, datagramNumber, payload) { + this.channel = channel; + this.datagramNumber = datagramNumber; + + this.payload = payload; + this.payloadLength = this.payload.length; + + if (payload instanceof SNAC) { + this.payloadLength = payload.toBuffer().length; + } + + if (channel === 2 && !(payload instanceof SNAC)) { + this.payload = SNAC.fromBuffer(this.payload, this.payloadLength); + } + } + + toString() { + const hasSnac = this.payload instanceof SNAC; + const payload = hasSnac ? this.payload.toString() : logDataStream(this.payload).split('\n').join('\n '); + return `ch:${this.channel}, dn: ${this.datagramNumber}, len: ${this.payloadLength}, payload:\n ${payload}` + } + + toBuffer() { + const FLAPHeader = Buffer.alloc(6, 0, 'hex'); + FLAPHeader.writeInt8(0x2a, 0); + FLAPHeader.writeInt8(this.channel, 1); + FLAPHeader.writeInt16BE(this.datagramNumber, 2); + FLAPHeader.writeInt16BE(this.payloadLength, 4); + + let payload = this.payload; + if (payload instanceof SNAC) { + payload = payload.toBuffer(); + } + + return Buffer.concat([FLAPHeader, payload]); + } +} + +module.exports = { + TLV, + SNAC, + FLAP, +}; diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..477d0c7 --- /dev/null +++ b/src/util.js @@ -0,0 +1,21 @@ +function chunkString(str, len) { + const size = Math.ceil(str.length/len) + const r = Array(size) + let offset = 0 + + for (let i = 0; i < size; i++) { + r[i] = str.substr(offset, len) + offset += len + } + + return r +} + +function logDataStream(data){ + const strs = chunkString(data.toString('hex'), 16); + return strs.map((str) => chunkString(str, 2).join(' ')).join('\n'); +} + +module.exports = { + logDataStream, +}; diff --git a/tests/data-structures.js b/tests/data-structures.js new file mode 100644 index 0000000..25b5f06 --- /dev/null +++ b/tests/data-structures.js @@ -0,0 +1,20 @@ +const assert = require('assert'); + +const { FLAP, SNAC, TLV } = require('../src/structures'); + +const tests = [ + () => { + // Construct and test a CLI_AUTH_REQUEST + const md5_auth_req = new FLAP(0x02, 0, new SNAC(0x17, 0x06, 0x0000, 0, [new TLV(0x0001, Buffer.from("toof"))])); + assert(md5_auth_req.channel === 2); + assert(md5_auth_req.payload instanceof SNAC); + assert(md5_auth_req.payload.family === 23); + assert(md5_auth_req.payload.service === 6); + assert(md5_auth_req.payload.payload.length === 1); + assert(md5_auth_req.payload.payload[0].len === 4); + } +]; + +tests.forEach((testFn) => { + testFn(); +});