From d37e1157bf2e7ba6c114844bdb41523153bb77c0 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Sat, 26 Dec 2020 22:09:19 -0600 Subject: [PATCH] rename some things based on official oscar protocol docs --- src/AIMAuthServer/index.ts | 25 +++++++++++++------------ src/AIMAuthServer/serverSnacs.ts | 4 ++-- src/BossServer/index.ts | 30 ++++++++++++++++-------------- src/BossServer/serverSnacs.ts | 14 ++++++++++++-- src/OscarServer.ts | 20 ++++++++++---------- src/constants.ts | 14 +++++++++++++- src/flapUtils.ts | 12 ++++++------ src/types.ts | 13 ++++++++++++- 8 files changed, 84 insertions(+), 48 deletions(-) diff --git a/src/AIMAuthServer/index.ts b/src/AIMAuthServer/index.ts index bb9cc7d..3414d7a 100644 --- a/src/AIMAuthServer/index.ts +++ b/src/AIMAuthServer/index.ts @@ -10,6 +10,7 @@ import { } from './serverSnacs'; import { parseAuthRequest, parseMD5LoginRequest } from './clientSnacs'; import { LOGIN_ERRORS } from '../constants'; +import { FlapType } from '../types'; /** * @summary The first server an Oscar Protocol client @@ -30,12 +31,12 @@ export class AIMAuthServer extends OscarServer { const { host, port } = oscarSocket.remoteAddress; console.log(`AIMAuthServer: New connection from ${host}:${port}`); - oscarSocket.onChannel(0x1, (flap) => { + oscarSocket.onFlap(FlapType.SIGNON, (flap) => { const flapVersion = flap.data.readUInt32BE(0); assert(flapVersion === 0x1, 'Incorrect client FLAP version'); }); - oscarSocket.onChannel(0x2, (flap) => { + oscarSocket.onFlap(FlapType.SIGNOFF, (flap) => { const state = this.getState(oscarSocket); const snac = parseSnac(flap.data); @@ -48,7 +49,7 @@ export class AIMAuthServer extends OscarServer { state.setScreenname(authReq.screenname); const responseFlap = { - channel: 2, + type: FlapType.DATA, data: authKeyResponseSnac(authKey, snac.requestID), }; @@ -83,7 +84,7 @@ export class AIMAuthServer extends OscarServer { // rather than mapping all to INCORRECT_NICK_OR_PASS if (!(isValidUsername && isValidPass)) { const responseFlap = { - channel: 2, + type: FlapType.DATA, data: loginErrorSnac({ screenname: payload.screenname, errorCode: LOGIN_ERRORS.INCORRECT_NICK_OR_PASS, @@ -97,7 +98,7 @@ export class AIMAuthServer extends OscarServer { } const responseFlap = { - channel: 2, + type: FlapType.DATA, data: loginSuccessSnac({ screenname: payload.screenname, // TODO: Should be pulled from DB when real @@ -117,20 +118,20 @@ export class AIMAuthServer extends OscarServer { oscarSocket.write(responseFlap); return; } - console.log('AIMAuthServer Unhandled Channel 2 Flap: ', flap); + console.log('AIMAuthServer data Flap: ', flap); }); - oscarSocket.onChannel(0x3, (flap) => { - console.log('AIMAuthServer unimplemented channel 3 flap: ', flap); + oscarSocket.onFlap(FlapType.ERROR, (flap) => { + console.log('AIMAuthServer unhandled error flap: ', flap); }); - oscarSocket.onChannel(0x4, (flap) => { + oscarSocket.onFlap(FlapType.SIGNOFF, (flap) => { // TODO: handle disconnect negotiation - console.log('AIMAuthServer unimplemented channel 4 flap: ', flap); + console.log('AIMAuthServer unhandled signoff flap: ', flap); }); - oscarSocket.onChannel(0x5, (flap) => { - console.log('AIMAuthServer unimplemented channel 5 flap: ', flap); + oscarSocket.onFlap(FlapType.KEEPALIVE, (flap) => { + console.log('AIMAuthServer unhandled keepalive flap: ', flap); }); // Initialize state needed for auth requests diff --git a/src/AIMAuthServer/serverSnacs.ts b/src/AIMAuthServer/serverSnacs.ts index 97745f6..e1bad1c 100644 --- a/src/AIMAuthServer/serverSnacs.ts +++ b/src/AIMAuthServer/serverSnacs.ts @@ -2,6 +2,7 @@ import assert from 'assert'; import { stringTLV, uint16TLV } from '../buildTLV'; import { buildSnac } from '../snacUtils'; import { SNACS, TLVS } from '../constants'; +import { uint16BEBuffer } from '../buf'; /** * @see http://iserverd1.khstu.ru/oscar/snac_17_07.html @@ -14,8 +15,7 @@ export function authKeyResponseSnac(authKey: string, reqID: number) { const authKeyBuf = Buffer.from(authKey, 'utf8'); // Note: The linked docs for this SNAC are wrong. // The length should be a word (2 byte), not a dword (4 bytes) - const authKeyLen = Buffer.alloc(2); - authKeyLen.writeUInt16BE(authKeyBuf.byteLength, 0); + const authKeyLen = uint16BEBuffer([authKeyBuf.byteLength]); return buildSnac({ family: SNACS.AUTH.family, diff --git a/src/BossServer/index.ts b/src/BossServer/index.ts index cf96416..123d3c9 100644 --- a/src/BossServer/index.ts +++ b/src/BossServer/index.ts @@ -1,4 +1,5 @@ import { timingSafeEqual } from 'crypto'; +import { FlapType } from '../types'; import { parseCookieRequest } from './clientSnacs'; import { OscarServer, OscarSocket } from '../OscarServer'; import { @@ -16,51 +17,52 @@ export class BossServer extends OscarServer { oscarSocket.sendStartFlap(); - oscarSocket.onChannel(0x1, (flap) => { + oscarSocket.onFlap(FlapType.SIGNON, (flap) => { const { authCookie } = parseCookieRequest(flap.data); // TODO: Grab cookie from shared storage with auth service const expectedCookie = Buffer.from('111111111', 'ascii'); const validCookie = timingSafeEqual(authCookie, expectedCookie); // TODO: Unsure of what client expects for invalid cookie, - // maybe close via channel 4? + // maybe just a SIGNOFF flap? Let's just crash + // the server for now assert(validCookie, 'BossServer: Invalid auth cookie'); const snac = supportedFamiliesSnac({ reqID: 1 }); - oscarSocket.write({ channel: 2, data: snac }); + oscarSocket.write({ type: FlapType.DATA, data: snac }); }); - oscarSocket.onChannel(0x2, (flap) => { + oscarSocket.onFlap(FlapType.DATA, (flap) => { const snac = parseSnac(flap.data); if (matchSnac(snac, 'GENERAL', 'CLIENT_FAMILY_VERSIONS')) { return oscarSocket.write({ - channel: 2, + type: FlapType.DATA, data: familyVersionsSnac({ reqID: snac.requestID }), }); } - if (matchSnac(snac, 'GENERAL', 'RATE_REQUEST')) { + if (matchSnac(snac, 'GENERAL', 'RATE_INFO_REQUEST')) { assert(false, 'snac 01,07 not implemented yet'); return oscarSocket.write({ - channel: 2, + type: FlapType.DATA, data: rateLimitInfoSnac({ reqID: snac.requestID }), }); } - console.log('boss unhandled channel 2 flap: ', flap); + console.log('boss unhandled data flap: ', flap); }); - oscarSocket.onChannel(0x3, (flap) => { - console.log('boss channel 3 flap: ', flap); + oscarSocket.onFlap(FlapType.ERROR, (flap) => { + console.log('boss error flap: ', flap); }); - oscarSocket.onChannel(0x4, (flap) => { - console.log('boss channel 4 flap: ', flap); + oscarSocket.onFlap(FlapType.SIGNOFF, (flap) => { + console.log('boss signoff flap: ', flap); }); - oscarSocket.onChannel(0x5, (flap) => { - console.log('boss channel 5 flap: ', flap); + oscarSocket.onFlap(FlapType.KEEPALIVE, (flap) => { + console.log('boss keepalive flap: ', flap); }); } } diff --git a/src/BossServer/serverSnacs.ts b/src/BossServer/serverSnacs.ts index 98b5edc..d304915 100644 --- a/src/BossServer/serverSnacs.ts +++ b/src/BossServer/serverSnacs.ts @@ -61,7 +61,17 @@ export function familyVersionsSnac(opts: { reqID: number }) { }); } +/** + * @see http://iserverd.khstu.ru/oscar/snac_01_07.html + */ export function rateLimitInfoSnac(opts: { reqID: number }) { - // TODO: Implement - return Buffer.alloc(0); + // TODO: hardcode a giant buffer here + const data = Buffer.alloc(0); + + return buildSnac({ + family: SNACS.GENERAL.family, + subtype: SNACS.GENERAL.subtypes.RATE_INFO_RESPONSE, + reqID: opts.reqID, + data, + }); } diff --git a/src/OscarServer.ts b/src/OscarServer.ts index 897bb69..ef94cac 100644 --- a/src/OscarServer.ts +++ b/src/OscarServer.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import { Flap } from './types'; +import { Flap, FlapType } from './types'; import { parseFlap, buildFlap } from './flapUtils'; import { createServer, Socket, Server, AddressInfo } from 'net'; import { MultiMap } from './MultiMap'; @@ -46,13 +46,13 @@ export class OscarServer { } } -interface ChannelListener { +interface FlapListener { (flap: Flap): void; } export class OscarSocket { private sequenceID = 0; - private channelListeners = new MultiMap(); + private flapListeners = new MultiMap(); constructor(private socket: Socket) { socket.on('data', this.onData.bind(this)); @@ -73,12 +73,12 @@ export class OscarSocket { */ sendStartFlap() { this.write({ - channel: 1, + type: 1, data: Buffer.from([0x0, 0x0, 0x0, 0x1]), }); } - write(flap: { channel: number; data: Buffer }) { + write(flap: { type: FlapType; data: Buffer }) { const fullFlap = { ...flap, sequence: this.sequenceID++, @@ -86,19 +86,19 @@ export class OscarSocket { this.socket.write(buildFlap(fullFlap)); } - onChannel(channel: number, listener: ChannelListener) { - this.channelListeners.set(channel, listener); + onFlap(type: FlapType, listener: FlapListener) { + this.flapListeners.set(type, listener); return this; } private onData(data: Buffer) { const flap = parseFlap(data); assert( - this.channelListeners.has(flap.channel), - `Channel ${flap.channel} has no handler in OscarSocket}`, + this.flapListeners.has(flap.type), + `No handler for Flap type ${flap.type}`, ); - const listeners = this.channelListeners.get(flap.channel); + const listeners = this.flapListeners.get(flap.type); for (const listener of listeners) { listener(flap); } diff --git a/src/constants.ts b/src/constants.ts index b099d24..76a1c47 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -46,41 +46,53 @@ export const SNACS = { SUPPORTED_FAMILIES: 0x3, CLIENT_FAMILY_VERSIONS: 0x17, SERVER_FAMILY_VERSIONS: 0x18, - RATE_REQUEST: 0x6, + RATE_INFO_REQUEST: 0x6, + RATE_INFO_RESPONSE: 0x7, }, }, LOCATION: { family: 0x2, + subtypes: {}, }, BUDDYLIST: { family: 0x3, + subtypes: {}, }, ICBM: { family: 0x4, + subtypes: {}, }, INVITATION: { family: 0x6, + subtypes: {}, }, ADMINISTRATIVE: { family: 0x7, + subtypes: {}, }, POPUP_NOTICE: { family: 0x8, + subtypes: {}, }, PRIVACY_MGMT: { family: 0x9, + subtypes: {}, }, USER_LOOKUP: { family: 0x0a, + subtypes: {}, }, USAGE_STATS: { family: 0x0b, + subtypes: {}, }, SSI: { family: 0x13, + subtypes: {}, }, OFFLINE: { family: 0x15, + subtypes: {}, }, AUTH: { family: 0x17, diff --git a/src/flapUtils.ts b/src/flapUtils.ts index ac75cc4..48da7a7 100644 --- a/src/flapUtils.ts +++ b/src/flapUtils.ts @@ -1,18 +1,18 @@ import assert from 'assert'; -import { Flap } from './types'; +import { Flap, FlapType } from './types'; interface BuildFlapOpts { - channel: number; + type: FlapType; sequence: number; data: Buffer; } -export function buildFlap({ channel, sequence, data }: BuildFlapOpts) { - assert(channel < 6 && channel > 0, `Unexpected Channel: ${channel}`); +export function buildFlap({ type, sequence, data }: BuildFlapOpts) { + assert(type < 6 && type > 0, `Unexpected Flap Frame Type: ${type}`); const buf = Buffer.alloc(6); buf.writeUInt8(0x2a, 0); // Flap start signal - buf.writeUInt8(channel, 1); + buf.writeUInt8(type, 1); buf.writeUInt16BE(sequence, 2); buf.writeUInt16BE(data.byteLength, 4); @@ -27,7 +27,7 @@ export function parseFlap(rawFlap: Buffer): Flap { assert(id === 0x2a, 'Unexpected Flap ID'); const flap: Flap = { - channel: rawFlap.readUInt8(1), + type: rawFlap.readUInt8(1), sequence: rawFlap.readUInt16BE(2), byteLength: rawFlap.readUInt16BE(4), data: rawFlap.subarray(6), diff --git a/src/types.ts b/src/types.ts index 5c0ea91..eaa95ab 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,19 @@ +/** + * @see http://web.archive.org/web/20080308233204/http://dev.aol.com/aim/oscar/#FLAP__FRAME_TYPE + */ +export const enum FlapType { + SIGNON = 1, + DATA = 2, + ERROR = 3, + SIGNOFF = 4, + KEEPALIVE = 5, +} + /** * @see http://web.archive.org/web/20080308233204/http://dev.aol.com/aim/oscar/#FLAP */ export interface Flap { - channel: number; + type: FlapType; sequence: number; byteLength: number; data: Buffer;