ICBM - client can send message

This commit is contained in:
Artem Titoulenko 2021-11-10 00:27:20 -05:00
parent 099ea1a69b
commit 11d76158ce
22 changed files with 184 additions and 63 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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