mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-21 20:19:47 -05:00
family -> service, service -> subtype
This commit is contained in:
parent
3a56dfd923
commit
1fa6d06f7a
24 changed files with 134 additions and 76 deletions
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
@ -25,11 +25,15 @@ export default class GenericServiceControls extends BaseService {
|
|||
return;
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x0e) { // Client requests own online information
|
||||
console.log('should send back online presence info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.service === 0x17) {
|
||||
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)));
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
47
tests/data-structures.ts
Normal 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();
|
||||
});
|
|
@ -5,6 +5,6 @@
|
|||
"esModuleInterop": true,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"include": ["src/**/*", "tests/**/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue