mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-22 04:29:47 -05:00
modules, tests
This commit is contained in:
parent
b6ef343ead
commit
792526bdaf
4 changed files with 181 additions and 107 deletions
114
src/index.js
114
src/index.js
|
@ -1,109 +1,5 @@
|
||||||
const net = require('net');
|
const net = require('net');
|
||||||
const assert = require('assert');
|
const { logDataStream } = require('./util');
|
||||||
|
|
||||||
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 server = net.createServer((socket) => {
|
const server = net.createServer((socket) => {
|
||||||
console.log('client connected...');
|
console.log('client connected...');
|
||||||
|
@ -115,6 +11,7 @@ const server = net.createServer((socket) => {
|
||||||
|
|
||||||
socket.on('data', (data) => {
|
socket.on('data', (data) => {
|
||||||
const flap = FLAP.fromBuffer(Buffer.from(data, 'hex'));
|
const flap = FLAP.fromBuffer(Buffer.from(data, 'hex'));
|
||||||
|
console.log(logDataStream(flap.toBuffer()));
|
||||||
console.log('RECV', flap.toString());
|
console.log('RECV', flap.toString());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -127,8 +24,11 @@ const server = net.createServer((socket) => {
|
||||||
console.log('client disconnected...');
|
console.log('client disconnected...');
|
||||||
});
|
});
|
||||||
|
|
||||||
const hello = Buffer.from(new Uint8Array([0x2a, 0x01, 0, 0x01, 0, 0x04, 0x00, 0x00, 0x00, 0x01]));
|
// const hello = Buffer.from(new Uint8Array([0x2a, 0x01, 0, 0x01, 0, 0x04, 0x00, 0x00, 0x00, 0x01]));
|
||||||
socket.write(hello);
|
|
||||||
|
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
|
/* 1. on connection, server sends
|
||||||
|
|
||||||
|
|
133
src/structures.js
Normal file
133
src/structures.js
Normal file
|
@ -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,
|
||||||
|
};
|
21
src/util.js
Normal file
21
src/util.js
Normal file
|
@ -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,
|
||||||
|
};
|
20
tests/data-structures.js
Normal file
20
tests/data-structures.js
Normal file
|
@ -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();
|
||||||
|
});
|
Loading…
Reference in a new issue