diff --git a/package.json b/package.json index 2a52caa..47c88ec 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "license": "MIT", "scripts": { "dev:tsc": "tsc --watch", - "dev:nodemon": "nodemon --watch ./dist --delay 200ms", + "dev:nodemon:auth": "nodemon --watch ./dist --delay 200ms dist/src/main-auth.js", + "dev:nodemon:chat": "nodemon --watch ./dist --delay 200ms dist/src/main-chat.js", "start": "tsc && node ./dist/index.js" }, "devDependencies": { diff --git a/src/communicator.ts b/src/communicator.ts index 4024299..5e3e008 100644 --- a/src/communicator.ts +++ b/src/communicator.ts @@ -1,24 +1,6 @@ import net from "net"; import { FLAP, SNAC, TLV, TLVType } from './structures'; import { logDataStream } from './util'; -import { FLAGS_EMPTY } from './consts'; - -import GenericServiceControls from "./services/0x01-GenericServiceControls"; -import LocationServices from "./services/0x02-LocationSerices"; -import BuddyListManagement from "./services/0x03-BuddyListManagement"; -import ICBM from "./services/0x04-ICBM"; -import Invitation from "./services/0x06-Invitation"; -import Administration from "./services/0x07-Administration"; -import Popups from "./services/0x08-Popups"; -import PrivacyManagement from "./services/0x09-PrivacyManagement"; -import UserLookup from "./services/0x0a-UserLookup"; -import UsageStats from "./services/0x0b-UsageStats"; -import ChatNavigation from "./services/0x0d-ChatNavigation"; -import Chat from "./services/0x0e-Chat";; -import DirectorySearch from "./services/0x0f-DirectorySearch"; -import ServerStoredBuddyIcons from "./services/0x10-ServerStoredBuddyIcons"; -import SSI from "./services/0x13-SSI"; -import AuthorizationRegistrationService from "./services/0x17-AuthorizationRegistration"; import BaseService from "./services/base"; @@ -30,15 +12,15 @@ export interface User { export default class Communicator { + private keepaliveInterval? : NodeJS.Timer; private _sequenceNumber = 0; private messageBuffer = Buffer.alloc(0); public services : {[key: number]: BaseService} = {}; public user? : User; - constructor(public socket : net.Socket) { - // Hold on to the socket - this.socket = socket; + constructor(public socket : net.Socket) {} + startListening() { this.socket.on('data', (data : Buffer) => { // we could get multiple FLAP messages, keep a running buffer of incoming // data and shift-off however many successful FLAPs we can make @@ -60,36 +42,23 @@ export default class Communicator { } }); - this.registerServices(); - this.start(); - } + this.keepaliveInterval = setInterval(() => { + const keepaliveFlap = new FLAP(0x05, this.nextReqID, Buffer.from("")); + this.socket.write(keepaliveFlap.toBuffer()); + }, 4 * 60 * 1000); + + this.socket.on('close', () => { + if (this.keepaliveInterval) { + clearInterval(this.keepaliveInterval); + } + }); - start() { // Start negotiating a connection const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01])); this.send(hello); } - registerServices() { - const services = [ - new GenericServiceControls(this), - new LocationServices(this), - new BuddyListManagement(this), - new ICBM(this), - new Invitation(this), - new Administration(this), - new Popups(this), - new PrivacyManagement(this), - new UserLookup(this), - new UsageStats(this), - new ChatNavigation(this), - new Chat(this), - new DirectorySearch(this), - new ServerStoredBuddyIcons(this), - // new SSI(this), - new AuthorizationRegistrationService(this), - ]; - + registerServices(services : BaseService[] = []) { // Make a map of the service number to the service handler this.services = {}; services.forEach((service) => { @@ -97,8 +66,8 @@ export default class Communicator { }); } - _getNewSequenceNumber() { - return ++this._sequenceNumber; + get nextReqID() { + return ++this._sequenceNumber & 0xFFFF; } send(message : FLAP) { @@ -136,8 +105,8 @@ export default class Communicator { 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))); + const resp = new FLAP(2, this.nextReqID, + new SNAC(0x01, 0x03, Buffer.concat(servicesOffered))); this.send(resp); return; } diff --git a/src/haxor-proxy.ts b/src/haxor-proxy.ts new file mode 100644 index 0000000..cba9284 --- /dev/null +++ b/src/haxor-proxy.ts @@ -0,0 +1,21 @@ +import net from "net"; + +const server = net.createServer((socket) => { + socket.setTimeout(5 * 60 * 1000); // 5 minute timeout + socket.on('timeout', () => { + console.log('socket timeout'); + socket.end(); + }); + + socket.on('end', () => { + console.log('client disconnected...'); + }); +}); + +server.on('error', (err) => { + console.error(err); +}); + +server.listen(9999, () => { + console.log('proxy ready'); +}) diff --git a/src/index.ts b/src/main-auth.ts similarity index 58% rename from src/index.ts rename to src/main-auth.ts index f26048f..1cece8b 100644 --- a/src/index.ts +++ b/src/main-auth.ts @@ -1,9 +1,11 @@ import net from 'net'; import Communicator from './communicator'; +import AuthorizationRegistrationService from "./services/0x17-AuthorizationRegistration"; + const server = net.createServer((socket) => { console.log('client connected...'); - socket.setTimeout(30000); + socket.setTimeout(5 * 60 * 1000); // 5 minute timeout socket.on('error', (e) => { console.error('socket encountered an error:', e); @@ -19,7 +21,12 @@ const server = net.createServer((socket) => { console.log('client disconnected...'); }); - new Communicator(socket); + const comm = new Communicator(socket); + const services = [ + new AuthorizationRegistrationService(comm), + ]; + comm.registerServices(services); + comm.startListening(); }); server.on('error', (err) => { @@ -27,5 +34,5 @@ server.on('error', (err) => { }); server.listen(5190, () => { - console.log('OSCAR ready on :5190'); + console.log('AUTH ready on :5190'); }); diff --git a/src/main-chat.ts b/src/main-chat.ts new file mode 100644 index 0000000..530b2a0 --- /dev/null +++ b/src/main-chat.ts @@ -0,0 +1,66 @@ +import net from 'net'; +import Communicator from './communicator'; + +import GenericServiceControls from "./services/0x01-GenericServiceControls"; +import LocationServices from "./services/0x02-LocationSerices"; +import BuddyListManagement from "./services/0x03-BuddyListManagement"; +import ICBM from "./services/0x04-ICBM"; +import Invitation from "./services/0x06-Invitation"; +import Administration from "./services/0x07-Administration"; +import Popups from "./services/0x08-Popups"; +import PrivacyManagement from "./services/0x09-PrivacyManagement"; +import UserLookup from "./services/0x0a-UserLookup"; +import UsageStats from "./services/0x0b-UsageStats"; +import ChatNavigation from "./services/0x0d-ChatNavigation"; +import Chat from "./services/0x0e-Chat";; +import DirectorySearch from "./services/0x0f-DirectorySearch"; +import ServerStoredBuddyIcons from "./services/0x10-ServerStoredBuddyIcons"; +import SSI from "./services/0x13-SSI"; + +const server = net.createServer((socket) => { + console.log('client connected...'); + socket.setTimeout(5 * 60 * 1000); // 5 minute timeout + + socket.on('error', (e) => { + console.error('socket encountered an error:', e); + socket.end(); + }); + + socket.on('timeout', () => { + console.log('socket timeout'); + socket.end(); + }); + + socket.on('end', () => { + console.log('client disconnected...'); + }); + + const comm = new Communicator(socket); + const services = [ + new GenericServiceControls(comm), + new LocationServices(comm), + new BuddyListManagement(comm), + new ICBM(comm), + new Invitation(comm), + new Administration(comm), + new Popups(comm), + new PrivacyManagement(comm), + new UserLookup(comm), + new UsageStats(comm), + new ChatNavigation(comm), + new Chat(comm), + new DirectorySearch(comm), + new ServerStoredBuddyIcons(comm), + new SSI(comm), + ]; + comm.registerServices(services); + comm.startListening(); +}); + +server.on('error', (err) => { + throw err; +}); + +server.listen(5191, () => { + console.log('CHAT ready on :5191'); +}); diff --git a/src/services/0x01-GenericServiceControls.ts b/src/services/0x01-GenericServiceControls.ts index f5f4481..faf5514 100644 --- a/src/services/0x01-GenericServiceControls.ts +++ b/src/services/0x01-GenericServiceControls.ts @@ -1,7 +1,7 @@ import BaseService from './base'; import Communicator from '../communicator'; import { FLAP, Rate, RateClass, RatedServiceGroup, RateGroupPair, SNAC, TLV } from '../structures'; -import { FLAGS_EMPTY, USER_STATUS_VARIOUS, USER_STATUS } from '../consts'; +import { USER_STATUS_VARIOUS, USER_STATUS } from '../consts'; import { char, word, dword, dot2num } from '../structures/bytes'; export default class GenericServiceControls extends BaseService { @@ -31,8 +31,8 @@ export default class GenericServiceControls extends BaseService { } 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, [ + const resp = new FLAP(0x02, this.nextReqID, + SNAC.forRateClass(0x01, 0x07, [ new Rate( new RateClass(1, 80, 2500, 2000, 1500, 800, 3400 /*fake*/, 6000, 0, 0), new RatedServiceGroup(1, [new RateGroupPair(0x00, 0x00)]) @@ -40,8 +40,8 @@ export default class GenericServiceControls extends BaseService { ])) this.send(resp); - const motd = new FLAP(0x02, this._getNewSequenceNumber(), - new SNAC(0x01, 0x13, FLAGS_EMPTY, 0, Buffer.concat([ + const motd = new FLAP(0x02, this.nextReqID, + new SNAC(0x01, 0x13, Buffer.concat([ word(0x0004), (new TLV(0x0B, Buffer.from("Hello world!"))).toBuffer(), ]))) @@ -85,8 +85,8 @@ export default class GenericServiceControls extends BaseService { const buf = Buffer.concat([payloadHeader, ...tlvs.map((tlv) => tlv.toBuffer())]) - const resp = new FLAP(0x02, this._getNewSequenceNumber(), - new SNAC(0x01, 0x0f, FLAGS_EMPTY, 0, buf)); + const resp = new FLAP(0x02, this.nextReqID, + new SNAC(0x01, 0x0f, buf)); this.send(resp); return; @@ -97,8 +97,8 @@ export default class GenericServiceControls extends BaseService { 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))); + const resp = new FLAP(0x02, this.nextReqID, + new SNAC(0x01, 0x18, Buffer.concat(serviceVersions))); this.send(resp); return; } diff --git a/src/services/0x02-LocationSerices.ts b/src/services/0x02-LocationSerices.ts index 3c34e46..bfd726f 100644 --- a/src/services/0x02-LocationSerices.ts +++ b/src/services/0x02-LocationSerices.ts @@ -3,7 +3,7 @@ import Communicator from '../communicator'; import { FLAP, SNAC, TLV } from '../structures'; import { char, word, dword, dot2num } from '../structures/bytes'; -import { FLAGS_EMPTY, USER_STATUS, USER_STATUS_VARIOUS } from '../consts'; +import { USER_STATUS, USER_STATUS_VARIOUS } from '../consts'; export default class LocationServices extends BaseService { constructor(communicator : Communicator) { @@ -17,8 +17,8 @@ export default class LocationServices extends BaseService { // request location service parameters and limitations if (message.payload.subtype === 0x02) { - const resp = new FLAP(0x02, this._getNewSequenceNumber(), - new SNAC(0x02,0x03, FLAGS_EMPTY, 0, [ + const resp = new FLAP(0x02, this.nextReqID, + new SNAC(0x02,0x03, [ new TLV(0x01, word(0x400)), // max profile length new TLV(0x02, word(0x10)), // max capabilities new TLV(0x03, word(0xA)), // unknown diff --git a/src/services/0x03-BuddyListManagement.ts b/src/services/0x03-BuddyListManagement.ts index 1f61bc9..1a55326 100644 --- a/src/services/0x03-BuddyListManagement.ts +++ b/src/services/0x03-BuddyListManagement.ts @@ -16,8 +16,8 @@ export default class BuddyListManagement extends BaseService { } if (message.payload.subtype === 0x02) { - const resp = new FLAP(0x02, this._getNewSequenceNumber(), - new SNAC(0x03, 0x03, FLAGS_EMPTY, 0, [ + const resp = new FLAP(0x02, this.nextReqID, + new SNAC(0x03, 0x03, [ new TLV(0x01, word(600)), // 600 max buddies new TLV(0x02, word(750)), // 750 max watchers new TLV(0x03, word(512)), // 512 max online notifications ? diff --git a/src/services/0x04-ICBM.ts b/src/services/0x04-ICBM.ts index 372ac2e..2bf0106 100644 --- a/src/services/0x04-ICBM.ts +++ b/src/services/0x04-ICBM.ts @@ -17,7 +17,7 @@ interface ChannelSettings { export default class ICBM extends BaseService { private channel : ChannelSettings = { - channel: 2, + channel: 0, messageFlags: 3, maxMessageSnacSize: 512, maxSenderWarningLevel: 999, @@ -26,6 +26,8 @@ export default class ICBM extends BaseService { unknown: 1000, }; + private channels : ChannelSettings[] = []; + constructor(communicator : Communicator) { super({service: 0x04, version: 0x01}, communicator) } @@ -52,8 +54,12 @@ export default class ICBM extends BaseService { } const payload = message.payload.payload; + const channel = payload.readUInt16BE(0); + + // TODO: set settings based on channel provided + this.channel = { - channel: payload.readUInt16BE(0), + channel, messageFlags: payload.readUInt32BE(2), maxMessageSnacSize: payload.readUInt16BE(6), maxSenderWarningLevel: payload.readUInt16BE(8), @@ -79,8 +85,8 @@ export default class ICBM extends BaseService { // It's identical to the channel set request the client // sends earlier. Also the 3.x client sends a channel set request // so early - const resp = new FLAP(0x02, this._getNewSequenceNumber(), - new SNAC(0x04, 0x05, FLAGS_EMPTY, 0, payload)); + const resp = new FLAP(0x02, this.nextReqID, + new SNAC(0x04, 0x05, payload)); this.send(resp); return; } diff --git a/src/services/0x09-PrivacyManagement.ts b/src/services/0x09-PrivacyManagement.ts index 34c7e88..a66298e 100644 --- a/src/services/0x09-PrivacyManagement.ts +++ b/src/services/0x09-PrivacyManagement.ts @@ -19,8 +19,8 @@ export default class PrivacyManagement extends BaseService { } if (message.payload.subtype === 0x02) { - const resp = new FLAP(0x02, this._getNewSequenceNumber(), - new SNAC(0x09, 0x03, FLAGS_EMPTY, 0, [ + const resp = new FLAP(0x02, this.nextReqID, + new SNAC(0x09, 0x03, [ new TLV(0x01, word(200)), // max visible list size new TLV(0x02, word(200)) // max invisible list size ])); diff --git a/src/services/0x17-AuthorizationRegistration.ts b/src/services/0x17-AuthorizationRegistration.ts index 9ef1b5d..3f0e109 100644 --- a/src/services/0x17-AuthorizationRegistration.ts +++ b/src/services/0x17-AuthorizationRegistration.ts @@ -2,6 +2,7 @@ import crypto from 'crypto'; import BaseService from './base'; import Communicator, { User } from '../communicator'; import { FLAP, SNAC, TLV, ErrorCode, TLVType } from '../structures'; +import { word } from '../structures/bytes'; const { AIM_MD5_STRING, FLAGS_EMPTY } = require('../consts'); @@ -44,8 +45,8 @@ export default class AuthorizationRegistrationService extends BaseService { const username = userTLV.payload.toString('ascii'); if (!users[username]) { - const authResp = new FLAP(2, this._getNewSequenceNumber(), - new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ + const authResp = new FLAP(2, this.nextReqID, + new SNAC(0x17, 0x03, [ TLV.forUsername(username), // username TLV.forError(ErrorCode.IncorrectNick) // incorrect nick/password ])); @@ -67,22 +68,25 @@ export default class AuthorizationRegistrationService extends BaseService { if (digest !== (passwordHashTLV as TLV).payload.toString('hex')) { console.log('Invalid password for', username); - const authResp = new FLAP(2, this._getNewSequenceNumber(), - new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ + const authResp = new FLAP(2, this.nextReqID, + new SNAC(0x17, 0x03, [ TLV.forUsername(username), // username TLV.forError(ErrorCode.IncorrectNick) // incorrect nick/password ])); this.send(authResp); + + // Close this connection + const plsLeave = new FLAP(4, this.nextReqID, Buffer.from([])); + this.send(plsLeave); return; } - const host = this.communicator.socket.localAddress.split(':').pop(); - const port = this.communicator.socket.localPort; - - const authResp = new FLAP(2, this._getNewSequenceNumber(), - new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [ + const chatHost = this.communicator.socket.localAddress.split(':').pop() + ':5191'; + + const authResp = new FLAP(2, this.nextReqID, + new SNAC(0x17, 0x03, [ TLV.forUsername(username), // username - TLV.forBOSAddress(`${host}:${port}`), // BOS address + TLV.forBOSAddress(chatHost), // BOS address TLV.forCookie(JSON.stringify({cookie: 'uwu', user: 'toof'})) // Authorization cookie ])); @@ -93,8 +97,8 @@ export default class AuthorizationRegistrationService extends BaseService { case 0x06: // Request md5 authkey const MD5AuthKeyHeader = Buffer.alloc(2, 0xFF, 'hex'); MD5AuthKeyHeader.writeUInt16BE(this.cipher.length); - const md5ReqResp = new FLAP(2, this._getNewSequenceNumber(), - new SNAC(0x17, 0x07, FLAGS_EMPTY, 0, + const md5ReqResp = new FLAP(2, this.nextReqID, + new SNAC(0x17, 0x07, Buffer.concat([MD5AuthKeyHeader, Buffer.from(this.cipher, 'binary')]), )); this.send(md5ReqResp); diff --git a/src/services/base.ts b/src/services/base.ts index 30e23c5..83e3195 100644 --- a/src/services/base.ts +++ b/src/services/base.ts @@ -6,7 +6,7 @@ interface ServiceFamilyVersion { version : number, } -export default class BaseService { +export default abstract class BaseService { public service : number; public version : number; @@ -20,8 +20,8 @@ export default class BaseService { this.communicator.send(message); } - _getNewSequenceNumber() { - return this.communicator._getNewSequenceNumber(); + get nextReqID() { + return this.communicator.nextReqID; } handleMessage(message : FLAP) : void { diff --git a/src/structures/SNAC.ts b/src/structures/SNAC.ts index e1b986b..8e9cda7 100644 --- a/src/structures/SNAC.ts +++ b/src/structures/SNAC.ts @@ -1,4 +1,5 @@ import assert from "assert"; +import { FLAGS_EMPTY } from "../consts"; import { TLV } from "./TLV"; export class RateClass { @@ -60,13 +61,16 @@ export class Rate { } } +let snacID = 0x2000; + export class SNAC { - constructor(public service : number, public subtype : number, public flags : Buffer, public requestID : number , public payload : (TLV[] | Buffer) = Buffer.alloc(0)) { + constructor(public service : number, public subtype : number, public payload : (TLV[] | Buffer) = Buffer.alloc(0), public requestID : number = 0, public flags : Buffer = FLAGS_EMPTY) { this.service = service; this.subtype = subtype; - this.flags = flags; - this.requestID = requestID; this.payload = payload; + + this.requestID = requestID || (snacID++); + this.flags = flags; } static fromBuffer(buf : Buffer, payloadLength = 0) { @@ -105,17 +109,17 @@ export class SNAC { } } - return new SNAC(service, subtype, flags, requestID, payload); + return new SNAC(service, subtype, payload, requestID, flags); } - static forRateClass(service : number, subtype : number, flags : Buffer, requestID : number, rates : Rate[]) : SNAC { + static forRateClass(service : number, subtype : 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(service, subtype, flags, requestID, payload); + return new SNAC(service, subtype, payload); } toString() { diff --git a/src/util.ts b/src/util.ts index c7e31d8..314aa83 100644 --- a/src/util.ts +++ b/src/util.ts @@ -134,7 +134,7 @@ function bufferFromWebText(webtext : string) : Buffer { return Buffer.from(webtext.replace(/\s/g, ''), 'hex'); } -const FLAPSpec = [ +const SNAC_01_0F = [ byte("FLAP Header"), byte("Channel"), word("Sequence ID"), @@ -189,5 +189,5 @@ const exampleWebText = ''+ ` if (require.main === module) { - printBuffer(bufferFromWebText(exampleWebText), FLAPSpec); + printBuffer(bufferFromWebText(exampleWebText), SNAC_01_0F); } diff --git a/tests/data-structures.ts b/tests/data-structures.ts index 61e48c6..7dd1b5d 100644 --- a/tests/data-structures.ts +++ b/tests/data-structures.ts @@ -6,7 +6,7 @@ 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"))])); + const md5_auth_req = new FLAP(0x02, 0, new SNAC(0x17, 0x06, [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);