family -> service, service -> subtype

This commit is contained in:
Artem Titoulenko 2021-09-11 14:46:51 -04:00
parent 3a56dfd923
commit 1fa6d06f7a
24 changed files with 134 additions and 76 deletions

View File

@ -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": {

View File

@ -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;
}

View File

@ -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)));

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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);

View File

@ -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)}`);
}
}

View File

@ -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) {

View File

@ -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);

View File

@ -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();
});

47
tests/data-structures.ts Normal file
View File

@ -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();
});

View File

@ -5,6 +5,6 @@
"esModuleInterop": true,
"outDir": "./dist",
},
"include": ["src/**/*"],
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}