diff --git a/src/communicator.ts b/src/communicator.ts index d13b65f..59917c6 100644 --- a/src/communicator.ts +++ b/src/communicator.ts @@ -37,8 +37,7 @@ export default class Communicator { this.handleMessage(flap); console.log('-----------------------DATA'); } catch (e) { - // Couldn't make a FLAP - break; + console.error("Error handling message:", e); } } }); diff --git a/src/main-chat.ts b/src/main-chat.ts index 3db9c37..86747bd 100644 --- a/src/main-chat.ts +++ b/src/main-chat.ts @@ -38,20 +38,20 @@ const server = net.createServer((socket) => { const comm = new Communicator(socket); const services = [ new GenericServiceControls(comm), - new LocationServices(comm), - new BuddyListManagement(comm), + // new LocationServices(comm), + // new BuddyListManagement(comm), new ICBM(comm), - new Invitation(comm), - new Administration(comm), - new Popups(comm), - new PrivacyManagement(comm), + // new Invitation(comm), + // new Administration(comm), + // new Popups(comm), + // new PrivacyManagement(comm), new UserLookup(comm), - new UsageStats(comm), + // new UsageStats(comm), // new ChatNavigation(comm), // new Chat(comm), // new DirectorySearch(comm), - new ServerStoredBuddyIcons(comm), - new SSI(comm), + // new ServerStoredBuddyIcons(comm), + // new SSI(comm), ]; comm.registerServices(services); comm.startListening(); diff --git a/src/services/0x01-GenericServiceControls.ts b/src/services/0x01-GenericServiceControls.ts index ac83e46..85258fb 100644 --- a/src/services/0x01-GenericServiceControls.ts +++ b/src/services/0x01-GenericServiceControls.ts @@ -9,7 +9,7 @@ export default class GenericServiceControls extends BaseService { private allowViewMemberSince = false; constructor(communicator : Communicator) { - super({service: 0x01, version: 0x03}, communicator) + super({service: 0x01, version: 0x03}, [0x06, 0x0e, 0x14, 0x17], communicator) } override handleMessage(message : FLAP) { @@ -17,27 +17,15 @@ export default class GenericServiceControls extends BaseService { throw new Error('Require SNAC'); } - if (message.payload.subtype === 0x14) { - /* - Client setting privacy settings - Bit 1 - Allows other AIM users to see how long you've been idle. - Bit 2 - Allows other AIM users to see how long you've been a member. - */ - const mask = (message.payload.payload as Buffer).readUInt32BE(); - this.allowViewIdle = (mask & 0x01) > 0; - this.allowViewMemberSince = (mask & 0x02) > 0; - console.log('allowViewIdle:', this.allowViewIdle, 'allowViewMemberSince', this.allowViewMemberSince); - return; - } - if (message.payload.subtype === 0x06) { // Client ask server for rate limits info // HACK: set rate limits for all services. I can't tell which message subtypes they support so // make it set rate limits for everything under 0x21. const pairs : RateGroupPair[] = []; Object.values(this.communicator.services).forEach((service) => { - for (let i = 0; i < 0x21; i++) { - pairs.push(new RateGroupPair(service.service, i)); + // for (let subtype of service.supportedSubtypes) { + for (let subtype = 0; subtype < 0x21; subtype++) { + pairs.push(new RateGroupPair(service.service, subtype)); } }); @@ -47,14 +35,14 @@ export default class GenericServiceControls extends BaseService { new RateClass(1, 80, 2500, 2000, 1500, 800, 3400 /*fake*/, 6000, 0, 0), new RatedServiceGroup(1, pairs), ) - ])) + ])); this.send(resp); const motd = new FLAP(0x02, this.nextReqID, new SNAC(0x01, 0x13, Buffer.concat([ word(0x0004), (new TLV(0x0B, Buffer.from("Hello world!"))).toBuffer(), - ]))) + ]))); this.send(motd); return; } @@ -102,6 +90,19 @@ export default class GenericServiceControls extends BaseService { return; } + if (message.payload.subtype === 0x14) { + /* + Client setting privacy settings + Bit 1 - Allows other AIM users to see how long you've been idle. + Bit 2 - Allows other AIM users to see how long you've been a member. + */ + const mask = (message.payload.payload as Buffer).readUInt32BE(); + this.allowViewIdle = (mask & 0x01) > 0; + this.allowViewMemberSince = (mask & 0x02) > 0; + console.log('allowViewIdle:', this.allowViewIdle, 'allowViewMemberSince', this.allowViewMemberSince); + return; + } + if (message.payload.subtype === 0x17) { const serviceVersions : Buffer[] = []; Object.values(this.communicator.services).forEach((subtype) => { diff --git a/src/services/0x02-LocationSerices.ts b/src/services/0x02-LocationSerices.ts index bfd726f..3055297 100644 --- a/src/services/0x02-LocationSerices.ts +++ b/src/services/0x02-LocationSerices.ts @@ -7,7 +7,7 @@ import { USER_STATUS, USER_STATUS_VARIOUS } from '../consts'; export default class LocationServices extends BaseService { constructor(communicator : Communicator) { - super({service: 0x02, version: 0x01}, communicator) + super({service: 0x02, version: 0x01}, [0x02, 0x04], communicator) } override handleMessage(message : FLAP) { diff --git a/src/services/0x03-BuddyListManagement.ts b/src/services/0x03-BuddyListManagement.ts index 1a55326..fee5cb5 100644 --- a/src/services/0x03-BuddyListManagement.ts +++ b/src/services/0x03-BuddyListManagement.ts @@ -7,7 +7,7 @@ import { word } from '../structures/bytes'; export default class BuddyListManagement extends BaseService { constructor(communicator : Communicator) { - super({service: 0x03, version: 0x01}, communicator) + super({service: 0x03, version: 0x01}, [0x02], communicator) } override handleMessage(message : FLAP) { diff --git a/src/services/0x04-ICBM.ts b/src/services/0x04-ICBM.ts index 329846f..52af05f 100644 --- a/src/services/0x04-ICBM.ts +++ b/src/services/0x04-ICBM.ts @@ -3,7 +3,7 @@ import Communicator from '../communicator'; import { FLAGS_EMPTY } from '../consts'; import { FLAP, SNAC, TLV } from '../structures'; -import { dword } from '../structures/bytes'; +import { char, dword, word } from '../structures/bytes'; interface ChannelSettings { channel: number, @@ -16,6 +16,11 @@ interface ChannelSettings { } export default class ICBM extends BaseService { + /* Inter-Client Basic Message + + This system passes messages from/to clients through the server + instead of directly between clients. + */ private channel : ChannelSettings = { channel: 0, messageFlags: 3, @@ -29,7 +34,7 @@ export default class ICBM extends BaseService { private channels : ChannelSettings[] = []; constructor(communicator : Communicator) { - super({service: 0x04, version: 0x01}, communicator) + super({service: 0x04, version: 0x01}, [0x02, 0x04], communicator) } override handleMessage(message : FLAP) { @@ -85,5 +90,63 @@ export default class ICBM extends BaseService { this.send(resp); return; } + + if (message.payload.subtype === 0x06) { + // Client sent us a message to deliver to another client + /* + Channel 1 is used for what would commonly be called an "instant message" (plain text messages). + Channel 2 is used for complex messages (rtf, utf8) and negotiating "rendezvous". These transactions end in something more complex happening, such as a chat invitation, or a file transfer. + Channel 3 is used for chat messages (not in the same family as these channels). + */ + + if (!(message.payload.payload instanceof Buffer)) { + throw new Error('this should be a buffer'); + } + + const msgId = message.payload.payload.readBigUInt64BE(0); + const channel = message.payload.payload.readUInt16BE(8); + const screenNameLength = message.payload.payload.readUInt8(10); + const screenName = message.payload.payload.slice(11, 11 + screenNameLength).toString(); + + console.log({ + msgId, channel, screenName, + }); + + if (channel === 1) { + const tlvs = TLV.fromBufferBlob(message.payload.payload.slice(11 + screenNameLength)); + console.log(tlvs); + + // does the client want us to acknowledge that we got the message? + const wantsAck = tlvs.find((tlv) => tlv.type === 3); + + // lets parse the message + const messageTLV = tlvs.find((tlv) => tlv.type === 2); + if (!messageTLV) { + // TODO: send back error response + throw new Error('need a message'); + } + + // Start parsing the message TLV payload + // first is the array of capabilities + const startOfMessageFragment = 2 + messageTLV.payload.readUInt16BE(2); + const lengthOfMessageText = messageTLV.payload.readUInt16BE(startOfMessageFragment + 2); + const messageText = messageTLV.payload.slice(startOfMessageFragment + 8, startOfMessageFragment + 8 + lengthOfMessageText).toString(); + console.log('The user said:', messageText); + + if (wantsAck) { + const sender = this.communicator.user?.username || ""; + const msgIdBuffer = Buffer.alloc(32); + msgIdBuffer.writeBigUInt64BE(msgId); + const ackPayload = Buffer.from([ + ...msgIdBuffer, + ...word(0x02), + ...char(sender.length), + ...Buffer.from(sender), + ]); + const ackResp = new FLAP(2, this.nextReqID, new SNAC(0x04, 0x0c, ackPayload)); + this.send(ackResp); + } + } + } } } diff --git a/src/services/0x06-Invitation.ts b/src/services/0x06-Invitation.ts index d804cbe..5a02d69 100644 --- a/src/services/0x06-Invitation.ts +++ b/src/services/0x06-Invitation.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Invitation extends BaseService { constructor(communicator : Communicator) { - super({service: 0x06, version: 0x01}, communicator) + super({service: 0x06, version: 0x01}, [], communicator) } } diff --git a/src/services/0x07-Administration.ts b/src/services/0x07-Administration.ts index 07a459a..f5a3483 100644 --- a/src/services/0x07-Administration.ts +++ b/src/services/0x07-Administration.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Administration extends BaseService { constructor(communicator : Communicator) { - super({service: 0x07, version: 0x01}, communicator) + super({service: 0x07, version: 0x01}, [], communicator) } } diff --git a/src/services/0x08-Popups.ts b/src/services/0x08-Popups.ts index 137b2cd..66edbd4 100644 --- a/src/services/0x08-Popups.ts +++ b/src/services/0x08-Popups.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Popups extends BaseService { constructor(communicator : Communicator) { - super({service: 0x08, version: 0x01}, communicator) + super({service: 0x08, version: 0x01}, [], communicator) } } diff --git a/src/services/0x09-PrivacyManagement.ts b/src/services/0x09-PrivacyManagement.ts index a66298e..52ede04 100644 --- a/src/services/0x09-PrivacyManagement.ts +++ b/src/services/0x09-PrivacyManagement.ts @@ -10,7 +10,7 @@ export default class PrivacyManagement extends BaseService { private permissionMask: number = 0xffff; // everyone constructor(communicator : Communicator) { - super({service: 0x09, version: 0x01}, communicator) + super({service: 0x09, version: 0x01}, [0x02, 0x04], communicator) } override handleMessage(message : FLAP) { diff --git a/src/services/0x0a-UserLookup.ts b/src/services/0x0a-UserLookup.ts index 52c70a8..1383ede 100644 --- a/src/services/0x0a-UserLookup.ts +++ b/src/services/0x0a-UserLookup.ts @@ -1,8 +1,44 @@ import BaseService from './base'; import Communicator from '../communicator'; +import { FLAP, SNAC, TLV } from '../structures'; +import { word } from '../structures/bytes'; + +const emailToUin : {[key: string]: string[]} = { + 'bob@example.com': ['bobX0X0'], +}; export default class UserLookup extends BaseService { constructor(communicator : Communicator) { - super({service: 0x0a, version: 0x01}, communicator) + super({service: 0x0a, version: 0x01}, [0x02], communicator) + } + + override handleMessage(message: FLAP) { + if (!(message.payload instanceof SNAC)) { + throw new Error('Require SNAC'); + } + + // Search for a user by email address + // TODO: don't return users that don't want to be found via email + if (message.payload.subtype === 0x02) { + if (!(message.payload instanceof Buffer)) { + // 0x0e: Incorrect SNAC format + const incorrectFormatResp = new FLAP(2, this.nextReqID, new SNAC(0x0a, 0x01, word(0x0e))); + this.send(incorrectFormatResp); + return; + } + + const email = message.payload.payload.toString(); + if (!emailToUin[email]) { + // 0x14: No Match + const noResult = new FLAP(2, this.nextReqID, new SNAC(0x0a, 0x01, word(0x14))); + this.send(noResult); + return; + } + + // Return list of TLVs of matching UINs + const results = emailToUin[email].map((uin) => new TLV(0x01, Buffer.from(uin))) + const resp = new FLAP(2, this.nextReqID, new SNAC(0x0a, 0x03, results)); + this.send(resp); + } } } diff --git a/src/services/0x0b-UsageStats.ts b/src/services/0x0b-UsageStats.ts index e30a922..caa8090 100644 --- a/src/services/0x0b-UsageStats.ts +++ b/src/services/0x0b-UsageStats.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class UsageStats extends BaseService { constructor(communicator : Communicator) { - super({service: 0x0b, version: 0x01}, communicator) + super({service: 0x0b, version: 0x01}, [], communicator) } } diff --git a/src/services/0x0d-ChatNavigation.ts b/src/services/0x0d-ChatNavigation.ts index 43231cc..515ebba 100644 --- a/src/services/0x0d-ChatNavigation.ts +++ b/src/services/0x0d-ChatNavigation.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class ChatNavigation extends BaseService { constructor(communicator : Communicator) { - super({service: 0x0d, version: 0x02}, communicator) + super({service: 0x0d, version: 0x02}, [], communicator) } } diff --git a/src/services/0x0e-Chat.ts b/src/services/0x0e-Chat.ts index 8e57d43..8e8cb79 100644 --- a/src/services/0x0e-Chat.ts +++ b/src/services/0x0e-Chat.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class Chat extends BaseService { constructor(communicator : Communicator) { - super({service: 0x0e, version: 0x01}, communicator) + super({service: 0x0e, version: 0x01}, [], communicator) } } diff --git a/src/services/0x0f-DirectorySearch.ts b/src/services/0x0f-DirectorySearch.ts index ce1101a..4527931 100644 --- a/src/services/0x0f-DirectorySearch.ts +++ b/src/services/0x0f-DirectorySearch.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class DirectorySearch extends BaseService { constructor(communicator : Communicator) { - super({service: 0x0f, version: 0x01}, communicator) + super({service: 0x0f, version: 0x01}, [], communicator) } } diff --git a/src/services/0x10-ServerStoredBuddyIcons.ts b/src/services/0x10-ServerStoredBuddyIcons.ts index 78d2dd3..30751bf 100644 --- a/src/services/0x10-ServerStoredBuddyIcons.ts +++ b/src/services/0x10-ServerStoredBuddyIcons.ts @@ -3,6 +3,6 @@ import Communicator from '../communicator'; export default class ServerStoredBuddyIcons extends BaseService { constructor(communicator : Communicator) { - super({service: 0x10, version: 0x01}, communicator) + super({service: 0x10, version: 0x01}, [], communicator) } } diff --git a/src/services/0x13-SSI.ts b/src/services/0x13-SSI.ts index ca8168c..608c16f 100644 --- a/src/services/0x13-SSI.ts +++ b/src/services/0x13-SSI.ts @@ -4,6 +4,6 @@ import Communicator from '../communicator'; // SSI is Server Stored Information export default class SSI extends BaseService { constructor(communicator : Communicator) { - super({service: 0x13, version: 0x01}, communicator) + super({service: 0x13, version: 0x01}, [], communicator) } } diff --git a/src/services/0x17-AuthorizationRegistration.ts b/src/services/0x17-AuthorizationRegistration.ts index 3e439fa..4d72cf0 100644 --- a/src/services/0x17-AuthorizationRegistration.ts +++ b/src/services/0x17-AuthorizationRegistration.ts @@ -19,7 +19,7 @@ export default class AuthorizationRegistrationService extends BaseService { private cipher : string; constructor(communicator : Communicator) { - super({ service: 0x17, version: 0x01 }, communicator); + super({ service: 0x17, version: 0x01 }, [0x02, 0x06], communicator); this.cipher = "HARDY"; } diff --git a/src/services/base.ts b/src/services/base.ts index 83e3195..5f6225d 100644 --- a/src/services/base.ts +++ b/src/services/base.ts @@ -10,10 +10,11 @@ export default abstract class BaseService { public service : number; public version : number; - constructor({service, version} : ServiceFamilyVersion, public communicator : Communicator) { + constructor({service, version} : ServiceFamilyVersion, public supportedSubtypes : number[], public communicator : Communicator) { this.service = service; this.version = version; this.communicator = communicator; + this.supportedSubtypes = supportedSubtypes; } send(message : FLAP) { diff --git a/src/structures/SNAC.ts b/src/structures/SNAC.ts index 8e9cda7..580ffea 100644 --- a/src/structures/SNAC.ts +++ b/src/structures/SNAC.ts @@ -90,23 +90,12 @@ export class SNAC { service === 0x01 && subtype === 0x08 || service === 0x01 && subtype === 0x0e || service === 0x04 && subtype === 0x02 || - service === 0x09 && subtype === 0x04) { + service === 0x09 && subtype === 0x04 || + service === 0x0a && subtype === 0x02 || + service === 0x04 && subtype === 0x06) { payload = buf.slice(10, 10 + payloadLength); } else { - payload = []; - // Try to parse TLVs - let payloadIdx = 10; - let cb = 0, cbLimit = 20; //circuit breaker - while (payloadIdx < payloadLength && cb < cbLimit) { - const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); - payload.push(tlv); - payloadIdx += tlv.length + 4; // 4 bytes for TLV type + payload length - cb++; - } - if (cb === cbLimit) { - console.error('Application error, cb limit reached'); - process.exit(1); - } + payload = TLV.fromBufferBlob(buf.slice(10)); } return new SNAC(service, subtype, payload, requestID, flags); diff --git a/src/structures/TLV.ts b/src/structures/TLV.ts index 6b08d6e..153413c 100644 --- a/src/structures/TLV.ts +++ b/src/structures/TLV.ts @@ -16,13 +16,39 @@ export class TLV { } static fromBuffer(buf : Buffer) { - const type = buf.slice(0, 2).readInt16BE(0) as TLVType; + const type = buf.slice(0, 2).readInt16BE(0); const len = buf.slice(2, 4).readInt16BE(0) const payload = buf.slice(4, 4 + len); return new TLV(type, payload); } + /** + * Extract all TLVs from a given Buffer + * @param buf Buffer that contains multiple TLVs + * @param payloadLength Total stated length of the payload + * @returns all TLVs found in the Buffer + */ + static fromBufferBlob(buf : Buffer) : TLV[] { + const tlvs : TLV[] = []; + + // Try to parse TLVs + let payloadIdx = 0; + let cb = 0, cbLimit = 20; //circuit breaker + while (payloadIdx < buf.length && cb < cbLimit) { + const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); + tlvs.push(tlv); + payloadIdx += tlv.length + 4; // 4 bytes for TLV type + payload length + cb++; + } + if (cb === cbLimit) { + console.error('Application error, cb limit reached'); + process.exit(1); + } + + return tlvs; + } + static forUsername(username : string) : TLV { return new TLV(0x01, Buffer.from(username)); } @@ -47,7 +73,7 @@ export class TLV { return new TLV(0x06, Buffer.concat([varbuf, statbuf])); } - constructor(public type : TLVType, public payload : Buffer) { + constructor(public type : number, public payload : Buffer) { this.type = type; this.payload = payload; } diff --git a/src/structures/bytes.ts b/src/structures/bytes.ts index f69fa79..e62655c 100644 --- a/src/structures/bytes.ts +++ b/src/structures/bytes.ts @@ -16,6 +16,12 @@ export function dword(num : number) : Buffer { return buf; } +export function qword(num : number) : Buffer { + const buf = Buffer.alloc(8, 0x00); + buf.writeUInt32BE(num); + return buf; +} + /** * Converts a string IP address to it's number representation. * From: https://stackoverflow.com/a/8105740