split auth and chat servers

This commit is contained in:
Artem Titoulenko 2021-09-24 00:47:41 -04:00
parent aad0acfd15
commit 19e26cddbb
15 changed files with 175 additions and 97 deletions

View file

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

View file

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

21
src/haxor-proxy.ts Normal file
View file

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

View file

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

66
src/main-chat.ts Normal file
View file

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

View file

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

View file

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

View file

@ -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 ?

View file

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

View file

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

View file

@ -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 chatHost = this.communicator.socket.localAddress.split(':').pop() + ':5191';
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.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);

View file

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

View file

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

View file

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

View file

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