diff --git a/package.json b/package.json index 5f0f807..2a52caa 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "aim-oscar-server", "version": "1.0.0", - "main": "src/index.js", + "main": "dist/src/index.js", "license": "MIT", "scripts": { "dev:tsc": "tsc --watch", - "dev:nodemon": "nodemon --watch ./dist ./dist/index.js", + "dev:nodemon": "nodemon --watch ./dist --delay 200ms", "start": "tsc && node ./dist/index.js" }, "devDependencies": { diff --git a/src/communicator.ts b/src/communicator.ts index c729987..90b3b47 100644 --- a/src/communicator.ts +++ b/src/communicator.ts @@ -25,6 +25,7 @@ import BaseService from "./services/base"; export default class Communicator { private _sequenceNumber = 0; + private messageBuffer = Buffer.alloc(0); public services : {[key: number]: BaseService} = {}; constructor(public socket : net.Socket) { @@ -34,9 +35,22 @@ 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()); - this.handleMessage(flap); + + // we could get multiple FLAP messages, keep a running buffer of incoming + // data and shift-off however many successful FLAPs we can make + this.messageBuffer = Buffer.concat([this.messageBuffer, data]); + + while (this.messageBuffer.length > 0) { + try { + const flap = FLAP.fromBuffer(this.messageBuffer); + console.log('RECV', flap.toString()); + this.messageBuffer = this.messageBuffer.slice(flap.length); + this.handleMessage(flap); + } catch (e) { + // Couldn't make a FLAP + break; + } + } }); this.registerServices(); @@ -69,9 +83,10 @@ export default class Communicator { new SSI(this), ]; + // Make a map of the service number to the service handler this.services = {}; services.forEach((service) => { - this.services[service.family] = service; + this.services[service.service] = service; }); } @@ -110,10 +125,10 @@ export default class Communicator { console.log(tlv.toString()); if (tlv.type === TLVType.GetServices) { // Requesting available services - // this is just a dword list of service families + // this is just a dword list of subtype families const servicesOffered : Buffer[] = []; - Object.values(this.services).forEach((service) => { - servicesOffered.push(Buffer.from([0x00, service.family])); + Object.values(this.services).forEach((subtype) => { + servicesOffered.push(Buffer.from([0x00, subtype.service])); }); const resp = new FLAP(2, this._getNewSequenceNumber(), new SNAC(0x01, 0x03, FLAGS_EMPTY, 0, Buffer.concat(servicesOffered))); @@ -128,9 +143,9 @@ export default class Communicator { return; } - const familyService = this.services[message.payload.family]; + const familyService = this.services[message.payload.service]; if (!familyService) { - console.warn('no handler for family', message.payload.family); + console.warn('no handler for service', message.payload.service); return; } diff --git a/src/services/0x01-GenericServiceControls.ts b/src/services/0x01-GenericServiceControls.ts index 2ffdfde..dac7427 100644 --- a/src/services/0x01-GenericServiceControls.ts +++ b/src/services/0x01-GenericServiceControls.ts @@ -5,7 +5,7 @@ import { FLAGS_EMPTY } from '../consts'; export default class GenericServiceControls extends BaseService { constructor(communicator : Communicator) { - super({family: 0x01, version: 0x03}, communicator) + super({service: 0x01, version: 0x03}, communicator) } override handleMessage(message : FLAP) { @@ -13,7 +13,7 @@ export default class GenericServiceControls extends BaseService { throw new Error('Require SNAC'); } - if (message.payload.service === 0x06) { // Client ask server for rate limits info + if (message.payload.subtype === 0x06) { // Client ask server for rate limits info const resp = new FLAP(0x02, this._getNewSequenceNumber(), SNAC.forRateClass(0x01, 0x07, FLAGS_EMPTY, 0, [ new Rate( @@ -24,12 +24,16 @@ export default class GenericServiceControls extends BaseService { this.send(resp); return; } - - if (message.payload.service === 0x17) { + if (message.payload.subtype === 0x0e) { // Client requests own online information + console.log('should send back online presence info'); + return; + } + + if (message.payload.subtype === 0x17) { const serviceVersions : Buffer[] = []; - Object.values(this.communicator.services).forEach((service) => { - serviceVersions.push(Buffer.from([0x00, service.family, 0x00, service.version])); + Object.values(this.communicator.services).forEach((subtype) => { + serviceVersions.push(Buffer.from([0x00, subtype.service, 0x00, subtype.version])); }); const resp = new FLAP(0x02, this._getNewSequenceNumber(), new SNAC(0x01, 0x18, FLAGS_EMPTY, 0, Buffer.concat(serviceVersions))); diff --git a/src/services/0x02-LocationSerices.ts b/src/services/0x02-LocationSerices.ts index 823cf34..c69a53c 100644 --- a/src/services/0x02-LocationSerices.ts +++ b/src/services/0x02-LocationSerices.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class LocationServices extends BaseService { constructor(communicator : Communicator) { - super({family: 0x02, version: 0x01}, communicator) + super({service: 0x02, version: 0x01}, communicator) } } diff --git a/src/services/0x03-BuddyListManagement.ts b/src/services/0x03-BuddyListManagement.ts index 8611f86..5a437a2 100644 --- a/src/services/0x03-BuddyListManagement.ts +++ b/src/services/0x03-BuddyListManagement.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class BuddyListManagement extends BaseService { constructor(communicator : Communicator) { - super({family: 0x03, version: 0x01}, communicator) + super({service: 0x03, version: 0x01}, communicator) } } diff --git a/src/services/0x04-ICBM.ts b/src/services/0x04-ICBM.ts index 6e6ce2d..d11839e 100644 --- a/src/services/0x04-ICBM.ts +++ b/src/services/0x04-ICBM.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class ICBM extends BaseService { constructor(communicator : Communicator) { - super({family: 0x04, version: 0x01}, communicator) + super({service: 0x04, version: 0x01}, communicator) } } diff --git a/src/services/0x06-Invitation.ts b/src/services/0x06-Invitation.ts index 2781758..d804cbe 100644 --- a/src/services/0x06-Invitation.ts +++ b/src/services/0x06-Invitation.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Invitation extends BaseService { constructor(communicator : Communicator) { - super({family: 0x06, version: 0x01}, communicator) + super({service: 0x06, version: 0x01}, communicator) } } diff --git a/src/services/0x07-Administration.ts b/src/services/0x07-Administration.ts index c3f8f41..07a459a 100644 --- a/src/services/0x07-Administration.ts +++ b/src/services/0x07-Administration.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Administration extends BaseService { constructor(communicator : Communicator) { - super({family: 0x07, version: 0x01}, communicator) + super({service: 0x07, version: 0x01}, communicator) } } diff --git a/src/services/0x08-Popups.ts b/src/services/0x08-Popups.ts index 6f1e4d3..137b2cd 100644 --- a/src/services/0x08-Popups.ts +++ b/src/services/0x08-Popups.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Popups extends BaseService { constructor(communicator : Communicator) { - super({family: 0x08, version: 0x01}, communicator) + super({service: 0x08, version: 0x01}, communicator) } } diff --git a/src/services/0x09-PrivacyManagement.ts b/src/services/0x09-PrivacyManagement.ts index d0329e6..92e63b4 100644 --- a/src/services/0x09-PrivacyManagement.ts +++ b/src/services/0x09-PrivacyManagement.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class PrivacyManagement extends BaseService { constructor(communicator : Communicator) { - super({family: 0x09, version: 0x01}, communicator) + super({service: 0x09, version: 0x01}, communicator) } } diff --git a/src/services/0x0a-UserLookup.ts b/src/services/0x0a-UserLookup.ts index b1d7d66..52c70a8 100644 --- a/src/services/0x0a-UserLookup.ts +++ b/src/services/0x0a-UserLookup.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class UserLookup extends BaseService { constructor(communicator : Communicator) { - super({family: 0x0a, version: 0x01}, communicator) + super({service: 0x0a, version: 0x01}, communicator) } } diff --git a/src/services/0x0b-UsageStats.ts b/src/services/0x0b-UsageStats.ts index 29d5b07..e30a922 100644 --- a/src/services/0x0b-UsageStats.ts +++ b/src/services/0x0b-UsageStats.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class UsageStats extends BaseService { constructor(communicator : Communicator) { - super({family: 0x0b, version: 0x01}, communicator) + super({service: 0x0b, version: 0x01}, communicator) } } diff --git a/src/services/0x0d-ChatNavigation.ts b/src/services/0x0d-ChatNavigation.ts index b805c0f..43231cc 100644 --- a/src/services/0x0d-ChatNavigation.ts +++ b/src/services/0x0d-ChatNavigation.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class ChatNavigation extends BaseService { constructor(communicator : Communicator) { - super({family: 0x0d, version: 0x02}, communicator) + super({service: 0x0d, version: 0x02}, communicator) } } diff --git a/src/services/0x0e-Chat.ts b/src/services/0x0e-Chat.ts index 23e7740..8e57d43 100644 --- a/src/services/0x0e-Chat.ts +++ b/src/services/0x0e-Chat.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Chat extends BaseService { constructor(communicator : Communicator) { - super({family: 0x0e, version: 0x01}, communicator) + super({service: 0x0e, version: 0x01}, communicator) } } diff --git a/src/services/0x0f-DirectorySearch.ts b/src/services/0x0f-DirectorySearch.ts index 6f49a89..ce1101a 100644 --- a/src/services/0x0f-DirectorySearch.ts +++ b/src/services/0x0f-DirectorySearch.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class DirectorySearch extends BaseService { constructor(communicator : Communicator) { - super({family: 0x0f, version: 0x01}, communicator) + super({service: 0x0f, version: 0x01}, communicator) } } diff --git a/src/services/0x10-ServerStoredBuddyIcons.ts b/src/services/0x10-ServerStoredBuddyIcons.ts index b61eb19..78d2dd3 100644 --- a/src/services/0x10-ServerStoredBuddyIcons.ts +++ b/src/services/0x10-ServerStoredBuddyIcons.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class ServerStoredBuddyIcons extends BaseService { constructor(communicator : Communicator) { - super({family: 0x10, version: 0x01}, communicator) + super({service: 0x10, version: 0x01}, communicator) } } diff --git a/src/services/0x13-SSI.ts b/src/services/0x13-SSI.ts index c388542..f06d620 100644 --- a/src/services/0x13-SSI.ts +++ b/src/services/0x13-SSI.ts @@ -4,6 +4,6 @@ import Communicator from '../communicator'; // SSI is Server Stored Information export default class SSI extends BaseService { constructor(communicator : Communicator) { - super({family: 0x10, version: 0x01}, communicator) + super({service: 0x10, version: 0x01}, communicator) } } diff --git a/src/services/0x17-AuthorizationRegistration.ts b/src/services/0x17-AuthorizationRegistration.ts index 8dedb05..c49119d 100644 --- a/src/services/0x17-AuthorizationRegistration.ts +++ b/src/services/0x17-AuthorizationRegistration.ts @@ -13,7 +13,7 @@ export default class AuthorizationRegistrationService extends BaseService { private cipher : string; constructor(communicator : Communicator) { - super({ family: 0x17, version: 0x01 }, communicator); + super({ service: 0x17, version: 0x01 }, communicator); this.cipher = "HARDY"; } @@ -23,7 +23,7 @@ export default class AuthorizationRegistrationService extends BaseService { return; } - switch (message.payload.service) { + switch (message.payload.subtype) { case 0x02: // Client login request (md5 login sequence) const payload = message.payload.payload; const clientNameTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.ClientName); diff --git a/src/services/base.ts b/src/services/base.ts index f08643d..30e23c5 100644 --- a/src/services/base.ts +++ b/src/services/base.ts @@ -2,16 +2,16 @@ import Communicator from "../communicator"; import { FLAP } from "../structures"; interface ServiceFamilyVersion { - family : number, + service : number, version : number, } export default class BaseService { - public family : number; + public service : number; public version : number; - constructor({family, version} : ServiceFamilyVersion, public communicator : Communicator) { - this.family = family; + constructor({service, version} : ServiceFamilyVersion, public communicator : Communicator) { + this.service = service; this.version = version; this.communicator = communicator; } @@ -26,6 +26,6 @@ export default class BaseService { handleMessage(message : FLAP) : void { throw new Error(''+ - `Unhandled message for family ${this.family.toString(16)} supporting version ${this.version.toString(16)}`); + `Unhandled message for service ${this.service.toString(16)} supporting version ${this.version.toString(16)}`); } } diff --git a/src/structures/FLAP.ts b/src/structures/FLAP.ts index 215cce2..aa8816c 100644 --- a/src/structures/FLAP.ts +++ b/src/structures/FLAP.ts @@ -10,6 +10,11 @@ export class FLAP { const channel = buf.readInt8(1); const sequenceNumber = buf.slice(2,4).readInt16BE(0); const payloadLength = buf.slice(4, 6).readInt16BE(0); + + if (buf.length < (6 + payloadLength)) { + throw new Error('FLAP payload larger than available buffer data'); + } + let payload : Buffer | SNAC = buf.slice(6, 6 + payloadLength); if (channel === 2) { @@ -34,6 +39,13 @@ export class FLAP { } } + /** + * @returns Returns the byte length of this FLAP, includes header and payload + */ + get length() { + return 6 + this.payloadLength; + } + toString() { let payload = this.payload.toString(); if (this.payload instanceof Buffer) { diff --git a/src/structures/SNAC.ts b/src/structures/SNAC.ts index 518a9f8..e83d43f 100644 --- a/src/structures/SNAC.ts +++ b/src/structures/SNAC.ts @@ -32,11 +32,11 @@ export class RateClass { } export class RateGroupPair { - constructor(public family : number, public service : number) {} + constructor(public service : number, public subtype : number) {} toBuffer() : Buffer { const buf = Buffer.alloc(4, 0x00); - buf.writeInt16BE(this.family, 0); - buf.writeInt16BE(this.service, 2); + buf.writeInt16BE(this.service, 0); + buf.writeInt16BE(this.subtype, 2); return buf; } } @@ -61,9 +61,9 @@ export class Rate { } 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; + constructor(public service : number, public subtype : number, public flags : Buffer, public requestID : number , public payload : (TLV[] | Buffer) = Buffer.alloc(0)) { this.service = service; + this.subtype = subtype; this.flags = flags; this.requestID = requestID; this.payload = payload; @@ -71,17 +71,17 @@ export class SNAC { 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 service = buf.slice(0,2).readInt16BE(0); + const subtype = buf.slice(2,4).readInt16BE(0); const flags = buf.slice(4, 6); const requestID = buf.slice(6, 10).readInt32BE(0); let payload : Buffer | TLV[]; // SNACs can have multiple payload // Some SNACs don't have TLV payloads - if (family === 0x01 && service === 0x17 || - family === 0x01 && service === 0x07 || - family === 0x01 && service === 0x08 || - family === 0x01 && service === 0x0e) { + if (service === 0x01 && subtype === 0x17 || + service === 0x01 && subtype === 0x07 || + service === 0x01 && subtype === 0x08 || + service === 0x01 && subtype === 0x0e) { payload = buf.slice(10, 10 + payloadLength); } else { payload = []; @@ -100,27 +100,27 @@ export class SNAC { } } - return new SNAC(family, service, flags, requestID, payload); + return new SNAC(service, subtype, flags, requestID, payload); } - static forRateClass(family : number, service : number, flags : Buffer, requestID : number, rates : Rate[]) : SNAC { + static forRateClass(service : number, subtype : 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); + return new SNAC(service, subtype, flags, requestID, payload); } toString() { - return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.payload}`; + return `SNAC(${this.service.toString(16)},${this.subtype.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.service); + SNACHeader.writeUInt16BE(this.subtype, 2); SNACHeader.set(this.flags, 4); SNACHeader.writeUInt32BE(this.requestID, 6); diff --git a/tests/data-structures.js b/tests/data-structures.js deleted file mode 100644 index afd3547..0000000 --- a/tests/data-structures.js +++ /dev/null @@ -1,20 +0,0 @@ -import assert from '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(); -}); diff --git a/tests/data-structures.ts b/tests/data-structures.ts new file mode 100644 index 0000000..61e48c6 --- /dev/null +++ b/tests/data-structures.ts @@ -0,0 +1,47 @@ +import assert from 'assert'; + +import { FLAP, SNAC, TLV } from '../src/structures'; +import {FLAGS_EMPTY} from "../src/consts"; + +const tests = [ + () => { + // Construct and test a CLI_AUTH_REQUEST + const md5_auth_req = new FLAP(0x02, 0, new SNAC(0x17, 0x06, FLAGS_EMPTY, 0, [new TLV(0x01, Buffer.from("toof"))])); + assert(md5_auth_req.channel === 2); + assert(md5_auth_req.payload instanceof SNAC); + assert(md5_auth_req.payload.service === 23); + assert(md5_auth_req.payload.subtype === 6); + assert(md5_auth_req.payload.payload.length === 1); + assert.equal(md5_auth_req.payload.payload.length, 1); + assert(md5_auth_req.payload.payload[0] instanceof TLV); + }, + () => { + // Test FLAP.length calculation and consuming multiple messages + const dataStr = ` + 2a 02 4b 11 00 0a 00 01 + 00 0e 00 00 00 00 00 00 + 2a 02 4b 12 00 0a 00 02 + 00 02 00 00 00 00 00 00 + `.trim().replace(/\s+/g, ''); + let data = Buffer.from(dataStr, 'hex'); + + const message = FLAP.fromBuffer(data); + assert(message.channel === 2); + assert(message.payloadLength === 10); + assert(message.length === 16); + assert(message.payload instanceof SNAC); + assert((message.payload as SNAC).service === 1); + assert((message.payload as SNAC).subtype === 0x0e); + + data = data.slice(message.length); + const secondMessage = FLAP.fromBuffer(data); + assert(secondMessage.length === 16); + assert(secondMessage.payload instanceof SNAC); + assert((secondMessage.payload as SNAC).service === 2); + assert((secondMessage.payload as SNAC).subtype === 0x02); + } +]; + +tests.forEach((testFn) => { + testFn(); +}); diff --git a/tsconfig.json b/tsconfig.json index 6bacef8..417478a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,6 @@ "esModuleInterop": true, "outDir": "./dist", }, - "include": ["src/**/*"], + "include": ["src/**/*", "tests/**/*"], "exclude": ["node_modules", "**/*.spec.ts"] }