mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-21 12:09:48 -05:00
ICBM - client can send message
This commit is contained in:
parent
099ea1a69b
commit
11d76158ce
22 changed files with 184 additions and 63 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue