mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-21 20:19:47 -05:00
remove TypeScript code, pedal to the gopher metal
This commit is contained in:
parent
cca7d63e20
commit
14d363ba87
33 changed files with 6 additions and 1860 deletions
18
package.json
18
package.json
|
@ -1,22 +1,12 @@
|
|||
{
|
||||
"name": "aim-oscar-server",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/src/index.js",
|
||||
"license": "MIT",
|
||||
"license": "GPLv4",
|
||||
"scripts": {
|
||||
"dev:tsc": "tsc --watch",
|
||||
"dev:nodemon:auth-go": "nodemon --watch ./ -e go --ignore '*_test.go' --delay 200ms --exec 'go build && ./aim-oscar || exit 1' --signal SIGTERM",
|
||||
"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"
|
||||
"dev:": "nodemon --watch ./ -e go --ignore '*_test.go' --delay 200ms --exec 'go build && ./aim-oscar || exit 1' --signal SIGTERM",
|
||||
"start": "go build && ./aim-oscar"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node14": "^1.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"nodemon": "^2.0.12",
|
||||
"typescript": "^4.4.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"table-layout": "^3.0.0"
|
||||
"nodemon": "^2.0.12"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,142 +0,0 @@
|
|||
import net from "net";
|
||||
import { FLAP, SNAC, TLV, TLVType } from './structures';
|
||||
import { logDataStream } from './util';
|
||||
|
||||
import BaseService from "./services/base";
|
||||
|
||||
export interface User {
|
||||
uin: string,
|
||||
username: string,
|
||||
password: string,
|
||||
memberSince: Date,
|
||||
}
|
||||
|
||||
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) {}
|
||||
|
||||
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
|
||||
this.messageBuffer = Buffer.concat([this.messageBuffer, data]);
|
||||
|
||||
while (this.messageBuffer.length > 0) {
|
||||
try {
|
||||
const flap = FLAP.fromBuffer(this.messageBuffer);
|
||||
console.log('DATA-----------------------');
|
||||
console.log('RAW\n' + logDataStream(flap.toBuffer()));
|
||||
console.log('RECV', flap.toString());
|
||||
this.messageBuffer = this.messageBuffer.slice(flap.length);
|
||||
this.handleMessage(flap);
|
||||
console.log('-----------------------DATA');
|
||||
} catch (e) {
|
||||
console.error("Error handling message:", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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 negotiating a connection
|
||||
const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01]));
|
||||
this.send(hello);
|
||||
}
|
||||
|
||||
registerServices(services : BaseService[] = []) {
|
||||
// Make a map of the service number to the service handler
|
||||
this.services = {};
|
||||
services.forEach((service) => {
|
||||
this.services[service.service] = service;
|
||||
});
|
||||
}
|
||||
|
||||
get nextReqID() {
|
||||
return ++this._sequenceNumber & 0xFFFF;
|
||||
}
|
||||
|
||||
send(message : FLAP) {
|
||||
console.log('SEND', message.toString());
|
||||
console.log('RAW\n' + logDataStream(message.toBuffer()));
|
||||
this.socket.write(message.toBuffer());
|
||||
}
|
||||
|
||||
handleMessage(message : FLAP) {
|
||||
switch (message.channel) {
|
||||
case 1:
|
||||
// No SNACs on channel 1
|
||||
if (!(message.payload instanceof Buffer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const protocol = message.payload.readUInt32BE();
|
||||
|
||||
if (protocol !== 1) {
|
||||
console.log('Unsupported protocol:', protocol);
|
||||
this.socket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.length <= 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tlv = TLV.fromBuffer(message.payload.slice(4));
|
||||
console.log('thing sent to channel 1:');
|
||||
console.log(tlv.toString());
|
||||
|
||||
if (tlv.type === 0x06) {
|
||||
// client sent us a cookie
|
||||
const {cookie, user} = JSON.parse(tlv.payload.toString());
|
||||
console.log('cookie:', cookie);
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
if (tlv.type === TLVType.GetServices) { // Requesting available services
|
||||
// this is just a dword list of subtype families
|
||||
const servicesOffered : Buffer[] = [];
|
||||
Object.values(this.services).forEach((subtype) => {
|
||||
servicesOffered.push(Buffer.from([0x00, subtype.service]));
|
||||
});
|
||||
const resp = new FLAP(2, this.nextReqID,
|
||||
new SNAC(0x01, 0x03, Buffer.concat(servicesOffered)));
|
||||
this.send(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
case 2:
|
||||
if (!(message.payload instanceof SNAC)) {
|
||||
console.error('Expected SNAC payload');
|
||||
return;
|
||||
}
|
||||
|
||||
const familyService = this.services[message.payload.service];
|
||||
if (!familyService) {
|
||||
console.warn('no handler for service', message.payload.service);
|
||||
return;
|
||||
}
|
||||
|
||||
familyService.handleMessage(message);
|
||||
return;
|
||||
default:
|
||||
console.warn('No handlers for channel', message.channel);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
export const AIM_MD5_STRING = "AOL Instant Messenger (SM)";
|
||||
export const FLAGS_EMPTY = Buffer.from([0x00, 0x00, 0x00, 0x00]);
|
||||
|
||||
export const enum USER_STATUS_VARIOUS {
|
||||
/**
|
||||
* Status webaware flag
|
||||
*/
|
||||
WEBAWARE = 0x0001,
|
||||
/**
|
||||
* Status show ip flag
|
||||
*/
|
||||
SHOWIP = 0x0002,
|
||||
/**
|
||||
* User birthday flag
|
||||
*/
|
||||
BIRTHDAY = 0x0008,
|
||||
/**
|
||||
* User active webfront flag
|
||||
*/
|
||||
WEBFRONT = 0x0020,
|
||||
/**
|
||||
* Direct connection not supported
|
||||
*/
|
||||
DCDISABLED = 0x0100,
|
||||
/**
|
||||
* Direct connection upon authorization
|
||||
*/
|
||||
DCAUTH = 0x1000,
|
||||
/**
|
||||
* DC only with contact users
|
||||
*/
|
||||
DCCONT = 0x2000,
|
||||
}
|
||||
|
||||
export const enum USER_STATUS {
|
||||
/**
|
||||
* Status is online
|
||||
*/
|
||||
ONLINE = 0x0000,
|
||||
/**
|
||||
* Status is away
|
||||
*/
|
||||
AWAY = 0x0001,
|
||||
/**
|
||||
* Status is no not disturb (DND)
|
||||
*/
|
||||
DND = 0x0002,
|
||||
/**
|
||||
* Status is not available (N/A)
|
||||
*/
|
||||
NA = 0x0004,
|
||||
/**
|
||||
* Status is occupied (BISY)
|
||||
*/
|
||||
OCCUPIED = 0x0010,
|
||||
/**
|
||||
* Status is free for chat
|
||||
*/
|
||||
FREE4CHAT = 0x0020,
|
||||
/**
|
||||
* Status is invisible
|
||||
*/
|
||||
INVISIBLE = 0x0100,
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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');
|
||||
})
|
|
@ -1,38 +0,0 @@
|
|||
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(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 AuthorizationRegistrationService(comm),
|
||||
];
|
||||
comm.registerServices(services);
|
||||
comm.startListening();
|
||||
});
|
||||
|
||||
server.on('error', (err) => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
server.listen(5190, () => {
|
||||
console.log('AUTH ready on :5190');
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
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');
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
import { FLAP, Rate, RateClass, RatedServiceGroup, RateGroupPair, SNAC, TLV } from '../structures';
|
||||
import { USER_STATUS_VARIOUS, USER_STATUS } from '../consts';
|
||||
import { char, word, dword, dot2num } from '../structures/bytes';
|
||||
|
||||
export default class GenericServiceControls extends BaseService {
|
||||
private allowViewIdle = false;
|
||||
private allowViewMemberSince = false;
|
||||
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x01, version: 0x03}, [0x06, 0x0e, 0x14, 0x17], communicator)
|
||||
}
|
||||
|
||||
override handleMessage(message : FLAP) {
|
||||
if (!(message.payload instanceof SNAC)) {
|
||||
throw new Error('Require SNAC');
|
||||
}
|
||||
|
||||
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 subtype of service.supportedSubtypes) {
|
||||
for (let subtype = 0; subtype < 0x21; subtype++) {
|
||||
pairs.push(new RateGroupPair(service.service, subtype));
|
||||
}
|
||||
});
|
||||
|
||||
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, 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;
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x0e) { // Client requests own online information
|
||||
const uin = this.communicator.user?.username || 'user';
|
||||
const warning = 0;
|
||||
const since = +(new Date('December 17, 1998 03:24:00'));
|
||||
const externalIP = dot2num(this.communicator.socket.remoteAddress!.split(':').pop()!);
|
||||
|
||||
const tlvs : TLV[] = [
|
||||
new TLV(0x01, char(0x80)),
|
||||
new TLV(0x06, dword(USER_STATUS_VARIOUS.WEBAWARE | USER_STATUS_VARIOUS.DCDISABLED << 2 + USER_STATUS.ONLINE)),
|
||||
new TLV(0x0A, dword(externalIP)),
|
||||
new TLV(0x0F, dword(0)), // TODO: track idle time,
|
||||
new TLV(0x03, dword(Math.floor(Date.now() / 1000))),
|
||||
new TLV(0x1E, dword(0)), // Unknown
|
||||
new TLV(0x05, dword(Math.floor(since / 1000))),
|
||||
new TLV(0x0C, Buffer.concat([
|
||||
dword(externalIP),
|
||||
dword(5700), // DC TCP Port
|
||||
dword(0x04000000), // DC Type,
|
||||
word(0x0400), // DC Protocol Version
|
||||
dword(0), // DC Auth Cookie
|
||||
dword(0), // Web Front port
|
||||
dword(0x300), // Client Features ?
|
||||
dword(0), // Last Info Update Time
|
||||
dword(0), // last EXT info update time,
|
||||
dword(0), // last ext status update time
|
||||
]))
|
||||
];
|
||||
|
||||
const payloadHeader = Buffer.alloc(1 + uin.length + 2 + 2);
|
||||
payloadHeader.writeInt8(uin.length);
|
||||
payloadHeader.set(Buffer.from(uin), 1);
|
||||
payloadHeader.writeInt16BE(warning, 1 + uin.length);
|
||||
payloadHeader.writeInt16BE(tlvs.length, 1 + uin.length + 2);
|
||||
|
||||
const buf = Buffer.concat([payloadHeader, ...tlvs.map((tlv) => tlv.toBuffer())])
|
||||
|
||||
const resp = new FLAP(0x02, this.nextReqID,
|
||||
new SNAC(0x01, 0x0f, buf));
|
||||
|
||||
this.send(resp);
|
||||
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) => {
|
||||
serviceVersions.push(Buffer.from([0x00, subtype.service, 0x00, subtype.version]));
|
||||
});
|
||||
const resp = new FLAP(0x02, this.nextReqID,
|
||||
new SNAC(0x01, 0x18, Buffer.concat(serviceVersions)));
|
||||
this.send(resp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
import { FLAP, SNAC, TLV } from '../structures';
|
||||
import { char, word, dword, dot2num } from '../structures/bytes';
|
||||
import { USER_STATUS, USER_STATUS_VARIOUS } from '../consts';
|
||||
|
||||
export default class LocationServices extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x02, version: 0x01}, [0x02, 0x04], communicator)
|
||||
}
|
||||
|
||||
override handleMessage(message : FLAP) {
|
||||
if (!(message.payload instanceof SNAC)) {
|
||||
throw new Error('Expecting SNACs for LocationServices')
|
||||
}
|
||||
|
||||
// request location service parameters and limitations
|
||||
if (message.payload.subtype === 0x02) {
|
||||
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
|
||||
new TLV(0x04, word(0x1000)),
|
||||
]));
|
||||
this.send(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x04) {
|
||||
// Client use this snac to set its location information (like client
|
||||
// profile string, client directory profile string, client capabilities).
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
import { FLAGS_EMPTY } from '../consts';
|
||||
import { FLAP, SNAC, TLV } from '../structures';
|
||||
import { word } from '../structures/bytes';
|
||||
|
||||
export default class BuddyListManagement extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x03, version: 0x01}, [0x02], communicator)
|
||||
}
|
||||
|
||||
override handleMessage(message : FLAP) {
|
||||
if (!(message.payload instanceof SNAC)) {
|
||||
throw new Error('Expected SNACs')
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x02) {
|
||||
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 ?
|
||||
]));
|
||||
|
||||
this.send(resp);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,157 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
import { FLAGS_EMPTY } from '../consts';
|
||||
import { FLAP, SNAC, TLV } from '../structures';
|
||||
import { char, dword, word } from '../structures/bytes';
|
||||
|
||||
interface ChannelSettings {
|
||||
channel: number,
|
||||
messageFlags: number,
|
||||
maxMessageSnacSize: number,
|
||||
maxSenderWarningLevel: number,
|
||||
maxReceiverWarningLevel: number,
|
||||
minimumMessageInterval: number,
|
||||
unknown : number
|
||||
}
|
||||
|
||||
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,
|
||||
maxMessageSnacSize: 512,
|
||||
maxSenderWarningLevel: 999,
|
||||
maxReceiverWarningLevel: 999,
|
||||
minimumMessageInterval: 0,
|
||||
unknown: 1000,
|
||||
};
|
||||
|
||||
private channels : ChannelSettings[] = [];
|
||||
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x04, version: 0x01}, [0x02, 0x04], communicator)
|
||||
}
|
||||
|
||||
override handleMessage(message : FLAP) {
|
||||
if (!(message.payload instanceof SNAC)) {
|
||||
throw new Error('Expected SNACs')
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x02) {
|
||||
// client is telling us about it's ICBM capabilities (whatever)
|
||||
/*
|
||||
xx xx word channel to setup
|
||||
xx xx xx xx dword message flags
|
||||
xx xx word max message snac size
|
||||
xx xx word max sender warning level
|
||||
xx xx word max receiver warning level
|
||||
xx xx word minimum message interval (sec)
|
||||
00 00 word unknown parameter (also seen 03 E8)
|
||||
*/
|
||||
|
||||
if (!(message.payload.payload instanceof Buffer)) {
|
||||
throw new Error('Expected Buffer payload for this SNAC');
|
||||
}
|
||||
|
||||
const payload = message.payload.payload;
|
||||
const channel = payload.readUInt16BE(0);
|
||||
|
||||
// TODO: set settings based on channel provided
|
||||
this.channel = {
|
||||
channel,
|
||||
messageFlags: payload.readUInt32BE(2),
|
||||
maxMessageSnacSize: payload.readUInt16BE(6),
|
||||
maxSenderWarningLevel: payload.readUInt16BE(8),
|
||||
maxReceiverWarningLevel: payload.readUInt16BE(10),
|
||||
minimumMessageInterval: payload.readUInt16BE(12),
|
||||
unknown: 1000, //payload.readUInt16BE(14),
|
||||
}
|
||||
console.log("ICBM set channel", this.channel);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x04) {
|
||||
const payload = Buffer.alloc(16, 0x00);
|
||||
payload.writeInt16BE(this.channel.channel, 0);
|
||||
payload.writeInt32BE(this.channel.messageFlags, 2);
|
||||
payload.writeInt16BE(this.channel.maxMessageSnacSize, 6);
|
||||
payload.writeInt16BE(this.channel.maxSenderWarningLevel, 8);
|
||||
payload.writeInt16BE(this.channel.maxReceiverWarningLevel, 10);
|
||||
payload.writeInt16BE(this.channel.minimumMessageInterval, 12);
|
||||
payload.writeInt16BE(this.channel.unknown, 14);
|
||||
|
||||
const resp = new FLAP(0x02, this.nextReqID,
|
||||
new SNAC(0x04, 0x05, payload));
|
||||
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);
|
||||
|
||||
// The client usually wants a response that the server got the message. It checks that the message
|
||||
// back has the same message ID that was sent and the user it was sent to.
|
||||
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);
|
||||
}
|
||||
|
||||
// TODO: find the connection that the receiver is connected to and send them the message
|
||||
// this is the rest of the owl for this part
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class Invitation extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x06, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class Administration extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x07, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class Popups extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x08, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
import { FLAGS_EMPTY } from '../consts';
|
||||
import { FLAP, SNAC, TLV } from '../structures';
|
||||
import { word } from '../structures/bytes';
|
||||
|
||||
|
||||
export default class PrivacyManagement extends BaseService {
|
||||
private permissionMask: number = 0xffff; // everyone
|
||||
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x09, version: 0x01}, [0x02, 0x04], communicator)
|
||||
}
|
||||
|
||||
override handleMessage(message : FLAP) {
|
||||
if (!(message.payload instanceof SNAC)) {
|
||||
throw new Error('Expected SNACs')
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x02) {
|
||||
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
|
||||
]));
|
||||
this.send(resp);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.subtype === 0x04) {
|
||||
// Client sends permission mask for classes of users that can talk to the client
|
||||
this.permissionMask = (message.payload.payload as Buffer).readUInt32BE();
|
||||
console.log('set permission mask', this.permissionMask.toString(16));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
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}, [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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class UsageStats extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x0b, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class ChatNavigation extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x0d, version: 0x02}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class Chat extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x0e, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class DirectorySearch extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x0f, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
export default class ServerStoredBuddyIcons extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x10, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import BaseService from './base';
|
||||
import Communicator from '../communicator';
|
||||
|
||||
// SSI is Server Stored Information
|
||||
export default class SSI extends BaseService {
|
||||
constructor(communicator : Communicator) {
|
||||
super({service: 0x13, version: 0x01}, [], communicator)
|
||||
}
|
||||
}
|
|
@ -1,117 +0,0 @@
|
|||
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');
|
||||
|
||||
const users : {[key: string]: User} = {
|
||||
'toof': {
|
||||
uin: '156089',
|
||||
username: 'toof',
|
||||
password: 'foo',
|
||||
memberSince: new Date('December 17, 1998 03:24:00'),
|
||||
}
|
||||
};
|
||||
|
||||
export default class AuthorizationRegistrationService extends BaseService {
|
||||
private cipher : string;
|
||||
|
||||
constructor(communicator : Communicator) {
|
||||
super({ service: 0x17, version: 0x01 }, [0x02, 0x06], communicator);
|
||||
this.cipher = "HARDY";
|
||||
}
|
||||
|
||||
override handleMessage(message : FLAP) {
|
||||
if (message.payload instanceof Buffer) {
|
||||
console.log('Wont handle Buffer payload');
|
||||
return;
|
||||
}
|
||||
|
||||
switch (message.payload.subtype) {
|
||||
case 0x02: // Client login request (md5 login sequence)
|
||||
const payload = message.payload.payload;
|
||||
const clientNameTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.ClientName);
|
||||
if (!clientNameTLV || !(clientNameTLV instanceof TLV)) {
|
||||
return;
|
||||
}
|
||||
console.log("Attempting connection from", clientNameTLV.payload.toString('ascii'));
|
||||
|
||||
const userTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.User);
|
||||
if (!userTLV || !(userTLV instanceof TLV)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const username = userTLV.payload.toString('ascii');
|
||||
|
||||
if (!users[username]) {
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordHashTLV = payload.find((tlv) => tlv instanceof TLV && tlv.type === TLVType.PasswordHash);
|
||||
if (!passwordHashTLV || !(passwordHashTLV instanceof TLV)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pwHash = crypto.createHash('md5');
|
||||
pwHash.update(this.cipher);
|
||||
pwHash.update(users[username].password);
|
||||
pwHash.update(AIM_MD5_STRING);
|
||||
const digest = pwHash.digest('hex');
|
||||
|
||||
if (digest !== (passwordHashTLV as TLV).payload.toString('hex')) {
|
||||
console.log('Invalid password for', username);
|
||||
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 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(chatHost), // BOS address
|
||||
TLV.forCookie(JSON.stringify({cookie: 'uwu', user: users[username]})) // Authorization cookie
|
||||
]));
|
||||
|
||||
this.communicator.user = Object.assign({username}, users[username]);
|
||||
console.log(this.communicator.user);
|
||||
|
||||
this.send(authResp);
|
||||
|
||||
// tell them to leave
|
||||
const disconnectResp = new FLAP(4, this.nextReqID, Buffer.alloc(0));
|
||||
this.send(disconnectResp);
|
||||
|
||||
return;
|
||||
case 0x06: // Request md5 authkey
|
||||
const MD5AuthKeyHeader = Buffer.alloc(2, 0xFF, 'hex');
|
||||
MD5AuthKeyHeader.writeUInt16BE(this.cipher.length);
|
||||
const md5ReqResp = new FLAP(2, this.nextReqID,
|
||||
new SNAC(0x17, 0x07,
|
||||
Buffer.concat([MD5AuthKeyHeader, Buffer.from(this.cipher, 'binary')]),
|
||||
));
|
||||
this.send(md5ReqResp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AuthorizationRegistrationService;
|
|
@ -1,32 +0,0 @@
|
|||
import Communicator from "../communicator";
|
||||
import { FLAP } from "../structures";
|
||||
|
||||
interface ServiceFamilyVersion {
|
||||
service : number,
|
||||
version : number,
|
||||
}
|
||||
|
||||
export default abstract class BaseService {
|
||||
public service : number;
|
||||
public version : number;
|
||||
|
||||
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) {
|
||||
this.communicator.send(message);
|
||||
}
|
||||
|
||||
get nextReqID() {
|
||||
return this.communicator.nextReqID;
|
||||
}
|
||||
|
||||
handleMessage(message : FLAP) : void {
|
||||
throw new Error(''+
|
||||
`Unhandled message for service ${this.service.toString(16)} supporting version ${this.version.toString(16)}`);
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
export const enum ErrorCode {
|
||||
IncorrectNick = 0x04,
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import assert from "assert"
|
||||
|
||||
import { SNAC } from "./SNAC";
|
||||
import { logDataStream } from '../util';
|
||||
|
||||
export class FLAP {
|
||||
static fromBuffer(buf : Buffer) {
|
||||
assert.equal(buf[0], 0x2a, 'Expected 0x2a at start of FLAP header');
|
||||
assert(buf.length >= 6, 'Expected at least 6 bytes for FLAP header');
|
||||
const channel = buf.readInt8(1);
|
||||
const sequenceNumber = buf.slice(2,4).readInt16BE(0);
|
||||
const payloadLength = buf.slice(4, 6).readInt16BE(0);
|
||||
|
||||
if (buf.length < (6 + payloadLength)) {
|
||||
throw new Error('FLAP payload larger than available buffer data');
|
||||
}
|
||||
|
||||
let payload : Buffer | SNAC = buf.slice(6, 6 + payloadLength);
|
||||
|
||||
if (channel === 2 && payloadLength > 0) {
|
||||
payload = SNAC.fromBuffer(payload, payloadLength);
|
||||
}
|
||||
|
||||
return new FLAP(channel, sequenceNumber, payload)
|
||||
}
|
||||
|
||||
payloadLength: number;
|
||||
|
||||
constructor(public channel: number, public sequenceNumber: number, public payload: Buffer | SNAC) {
|
||||
this.channel = channel;
|
||||
this.sequenceNumber = sequenceNumber;
|
||||
|
||||
this.payload = payload;
|
||||
|
||||
if (payload instanceof SNAC) {
|
||||
this.payloadLength = payload.toBuffer().length;
|
||||
} else {
|
||||
this.payloadLength = payload.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Returns the byte length of this FLAP, includes header and payload
|
||||
*/
|
||||
get length() {
|
||||
return 6 + this.payloadLength;
|
||||
}
|
||||
|
||||
toString() {
|
||||
let payload = this.payload.toString();
|
||||
if (this.payload instanceof Buffer) {
|
||||
payload = logDataStream(this.payload).split('\n').join('\n ');
|
||||
}
|
||||
return `ch:${this.channel}, dn: ${this.sequenceNumber}, len: ${this.payloadLength}, payload:\n ${payload}`
|
||||
}
|
||||
|
||||
toBuffer() {
|
||||
const FLAPHeader = Buffer.alloc(6, 0, 'hex');
|
||||
FLAPHeader.writeInt8(0x2a, 0);
|
||||
FLAPHeader.writeInt8(this.channel, 1);
|
||||
FLAPHeader.writeInt16BE(this.sequenceNumber, 2);
|
||||
FLAPHeader.writeInt16BE(this.payloadLength, 4);
|
||||
|
||||
let payload = this.payload;
|
||||
if (payload instanceof SNAC) {
|
||||
payload = payload.toBuffer();
|
||||
}
|
||||
|
||||
return Buffer.concat([FLAPHeader, payload]);
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
import assert from "assert";
|
||||
import { FLAGS_EMPTY } from "../consts";
|
||||
import { TLV } from "./TLV";
|
||||
|
||||
export class RateClass {
|
||||
constructor(
|
||||
public ID: number,
|
||||
public WindowSize: number,
|
||||
public ClearLevel: number,
|
||||
public AlertLevel: number,
|
||||
public LimitLevel: number,
|
||||
public DisconnectLevel: number,
|
||||
public CurrentLevel: number,
|
||||
public MaxLevel: number,
|
||||
public LastTime: number,
|
||||
public CurrentStat: number,
|
||||
){}
|
||||
|
||||
toBuffer() : Buffer {
|
||||
const buf = Buffer.alloc(35, 0x00);
|
||||
buf.writeUInt16BE(this.ID, 0);
|
||||
buf.writeUInt32BE(this.WindowSize, 2);
|
||||
buf.writeUInt32BE(this.ClearLevel, 6);
|
||||
buf.writeUInt32BE(this.AlertLevel, 10);
|
||||
buf.writeUInt32BE(this.LimitLevel, 14);
|
||||
buf.writeUInt32BE(this.DisconnectLevel,18);
|
||||
buf.writeUInt32BE(this.CurrentLevel, 22);
|
||||
buf.writeUInt32BE(this.MaxLevel, 26);
|
||||
buf.writeUInt32BE(this.LastTime, 30);
|
||||
buf.writeUInt8(this.CurrentStat, 34);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
export class RateGroupPair {
|
||||
constructor(public service : number, public subtype : number) {}
|
||||
toBuffer() : Buffer {
|
||||
const buf = Buffer.alloc(4, 0x00);
|
||||
buf.writeInt16BE(this.service, 0);
|
||||
buf.writeInt16BE(this.subtype, 2);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
export class RatedServiceGroup {
|
||||
constructor(public rateGroupID : number, public pairs : RateGroupPair[]){}
|
||||
|
||||
toBuffer() : Buffer {
|
||||
const ratedServiceGroupHeader = Buffer.alloc(4, 0x00);
|
||||
ratedServiceGroupHeader.writeInt16BE(this.rateGroupID);
|
||||
ratedServiceGroupHeader.writeInt16BE(this.pairs.length, 2);
|
||||
const pairs = this.pairs.map((pair) => pair.toBuffer());
|
||||
return Buffer.concat([ratedServiceGroupHeader, ...pairs]);
|
||||
}
|
||||
}
|
||||
|
||||
export class Rate {
|
||||
constructor(public rateClass : RateClass, public ratedServiceGroup : RatedServiceGroup) {}
|
||||
toBuffer() : Buffer {
|
||||
return Buffer.concat([this.rateClass.toBuffer(), this.ratedServiceGroup.toBuffer()]);
|
||||
}
|
||||
}
|
||||
|
||||
let snacID = 0x2000;
|
||||
|
||||
export class SNAC {
|
||||
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.payload = payload;
|
||||
|
||||
this.requestID = requestID || (snacID++);
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
static fromBuffer(buf : Buffer, payloadLength = 0) {
|
||||
assert(buf.length >= 10, 'Expected 10 bytes for SNAC header');
|
||||
const service = buf.slice(0,2).readInt16BE(0);
|
||||
const subtype = buf.slice(2,4).readInt16BE(0);
|
||||
const flags = buf.slice(4, 6);
|
||||
const requestID = buf.slice(6, 10).readInt32BE(0);
|
||||
let payload : Buffer | TLV[]; // SNACs can have multiple payload
|
||||
|
||||
// Some SNACs don't have TLV payloads
|
||||
// Maybe this should be something that the service does itself when it
|
||||
// wants to respond to a message;
|
||||
if (service === 0x01 && subtype === 0x17 ||
|
||||
service === 0x01 && subtype === 0x14 ||
|
||||
service === 0x01 && subtype === 0x07 ||
|
||||
service === 0x01 && subtype === 0x08 ||
|
||||
service === 0x01 && subtype === 0x0e ||
|
||||
service === 0x04 && subtype === 0x02 ||
|
||||
service === 0x09 && subtype === 0x04 ||
|
||||
service === 0x0a && subtype === 0x02 ||
|
||||
service === 0x04 && subtype === 0x06) {
|
||||
payload = buf.slice(10, 10 + payloadLength);
|
||||
} else {
|
||||
payload = TLV.fromBufferBlob(buf.slice(10));
|
||||
}
|
||||
|
||||
return new SNAC(service, subtype, payload, requestID, flags);
|
||||
}
|
||||
|
||||
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, payload);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `SNAC(${this.service.toString(16)},${this.subtype.toString(16)}) #${this.requestID}\n ${this.payload}`;
|
||||
}
|
||||
|
||||
toBuffer() {
|
||||
const SNACHeader = Buffer.alloc(10, 0, 'hex');
|
||||
SNACHeader.writeUInt16BE(this.service);
|
||||
SNACHeader.writeUInt16BE(this.subtype, 2);
|
||||
SNACHeader.set(this.flags, 4);
|
||||
SNACHeader.writeUInt32BE(this.requestID, 6);
|
||||
|
||||
let payload : Buffer[] = [];
|
||||
if (this.payload instanceof Buffer) {
|
||||
payload = [this.payload];
|
||||
} else if (this.payload.length && this.payload[0] instanceof TLV) {
|
||||
payload = (this.payload as TLV[]).map((thing : TLV) => thing.toBuffer());
|
||||
}
|
||||
|
||||
return Buffer.concat([SNACHeader, ...payload]);
|
||||
}
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
import {ErrorCode} from "./ErrorCode";
|
||||
|
||||
import { USER_STATUS_VARIOUS, USER_STATUS } from "../consts";
|
||||
import { char, word, dword } from './bytes';
|
||||
|
||||
export const enum TLVType {
|
||||
User = 0x01,
|
||||
ClientName = 0x03,
|
||||
GetServices = 0x06,
|
||||
PasswordHash = 0x25,
|
||||
}
|
||||
|
||||
export class TLV {
|
||||
get length() : number {
|
||||
return this.payload.length;
|
||||
}
|
||||
|
||||
static fromBuffer(buf : Buffer) {
|
||||
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));
|
||||
}
|
||||
|
||||
static forBOSAddress(address : string ) : TLV {
|
||||
return new TLV(0x05, Buffer.from(address));
|
||||
}
|
||||
|
||||
static forCookie(cookie : string) : TLV {
|
||||
return new TLV(0x06, Buffer.from(cookie));
|
||||
}
|
||||
|
||||
static forError(errorCode : ErrorCode) : TLV {
|
||||
return new TLV(0x08, Buffer.from([0x00, errorCode]));
|
||||
}
|
||||
|
||||
static forStatus(various : USER_STATUS_VARIOUS, status: USER_STATUS) {
|
||||
const varbuf = Buffer.alloc(2, 0x00);
|
||||
varbuf.writeUInt16BE(various);
|
||||
const statbuf = Buffer.alloc(2, 0x00);
|
||||
statbuf.writeUInt16BE(status);
|
||||
return new TLV(0x06, Buffer.concat([varbuf, statbuf]));
|
||||
}
|
||||
|
||||
constructor(public type : number, public payload : Buffer) {
|
||||
this.type = type;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `TLV(0x${this.type.toString(16).padStart(2, '0')}, ${this.length}, ${this.payload.toString('ascii')})`;
|
||||
}
|
||||
|
||||
toBuffer() {
|
||||
return Buffer.concat([
|
||||
word(this.type),
|
||||
word(this.length),
|
||||
this.payload,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
export function char(num : number) : Buffer {
|
||||
const buf = Buffer.alloc(1, 0x00);
|
||||
buf.writeUInt8(num);
|
||||
return buf;
|
||||
}
|
||||
|
||||
export function word(num : number) : Buffer {
|
||||
const buf = Buffer.alloc(2, 0x00);
|
||||
buf.writeUInt16BE(num);
|
||||
return buf;
|
||||
}
|
||||
|
||||
export function dword(num : number) : Buffer {
|
||||
const buf = Buffer.alloc(4, 0x00);
|
||||
buf.writeUInt32BE(num);
|
||||
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
|
||||
* @param ip IP address string
|
||||
* @returns IP address as a number
|
||||
*/
|
||||
export function dot2num(ip : string) : number {
|
||||
const d = ip.split('.');
|
||||
return ((((((+d[0])*256)+(+d[1]))*256)+(+d[2]))*256)+(+d[3]);
|
||||
}
|
||||
|
||||
export function num2dot(num : number) : string {
|
||||
let d = '' + num%256;
|
||||
for (var i = 3; i > 0; i--) {
|
||||
num = Math.floor(num/256);
|
||||
d = num%256 + '.' + d;
|
||||
}
|
||||
return d;
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
export * from "./ErrorCode";
|
||||
export * from "./TLV";
|
||||
export * from "./SNAC";
|
||||
export * from "./FLAP";
|
295
src/util.ts
295
src/util.ts
|
@ -1,295 +0,0 @@
|
|||
import { FLAP } from './structures'
|
||||
import { dot2num, num2dot } from './structures/bytes'
|
||||
export function chunkString(str : string, len : number) {
|
||||
const size = Math.ceil(str.length/len)
|
||||
const r = Array(size)
|
||||
let offset = 0
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
r[i] = str.substr(offset, len)
|
||||
offset += len
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
export function logDataStream(data : Buffer){
|
||||
const strs = chunkString(data.toString('hex'), 16);
|
||||
return strs.map((str) => chunkString(str, 2).join(' ')).join('\n');
|
||||
}
|
||||
|
||||
// Experiment to provide descriptive print-outs of the data structures
|
||||
// @ts-ignore
|
||||
import Table from 'table-layout';
|
||||
|
||||
type DataSize = -1 | 8 | 16 | 32;
|
||||
interface Spec {
|
||||
description: string,
|
||||
size : DataSize,
|
||||
isRepeat? : boolean,
|
||||
isParam? : boolean,
|
||||
isTLV? : boolean,
|
||||
dump? : boolean,
|
||||
repeatSpecs?: Spec[],
|
||||
}
|
||||
|
||||
function byte(description : string) : Spec {
|
||||
return {size: 8, description}
|
||||
}
|
||||
|
||||
function word(description : string) : Spec {
|
||||
return {size: 16, description};
|
||||
}
|
||||
|
||||
function dword(description : string) : Spec {
|
||||
return {size: 32, description};
|
||||
}
|
||||
|
||||
function repeat(size: DataSize, description : string, specs : Spec[]) : Spec {
|
||||
return {size, description, isRepeat: true, repeatSpecs: specs};
|
||||
}
|
||||
|
||||
function param(size: DataSize, description: string) : Spec {
|
||||
return {size, description, isParam: true};
|
||||
}
|
||||
|
||||
function tlv(description : string) : Spec {
|
||||
return {size : -1, description, isTLV: true};
|
||||
}
|
||||
|
||||
function dump() : Spec {
|
||||
return {size: -1, description: '', dump: true};
|
||||
}
|
||||
|
||||
function parseBuffer(buf : Buffer, spec : Spec[], repeatTimes = 0) {
|
||||
let offset = 0;
|
||||
let rows = [];
|
||||
let repeat = repeatTimes;
|
||||
|
||||
for (let section of spec) {
|
||||
let value : any = 0;
|
||||
let bufStr : string = '';
|
||||
|
||||
if (section.dump) {
|
||||
rows.push({raw: logDataStream(buf.slice(offset))});
|
||||
break;
|
||||
}
|
||||
|
||||
if (section.size === 8) {
|
||||
bufStr = buf.slice(offset, offset + 1).toString('hex');
|
||||
value = buf.readInt8(offset);
|
||||
offset += 1;
|
||||
} else if (section.size === 16) {
|
||||
bufStr = buf.slice(offset, offset + 2).toString('hex');
|
||||
value = buf.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
} else if (section.size === 32) {
|
||||
bufStr = buf.slice(offset, offset + 4).toString('hex');
|
||||
value = buf.readUInt32BE(offset);
|
||||
offset += 4;
|
||||
}
|
||||
|
||||
if (section.description.includes("IP")) {
|
||||
value = num2dot(value);
|
||||
}
|
||||
|
||||
|
||||
if (section.isParam) {
|
||||
const paramBuf = buf.slice(offset, offset + value);
|
||||
offset += value;
|
||||
rows.push([chunkString(paramBuf.toString('hex'), 2), paramBuf.toString('ascii'), section.description])
|
||||
} else if (section.isTLV) {
|
||||
const tlvType = buf.slice(offset, offset + 2).toString('hex');
|
||||
offset += 2;
|
||||
const tlvLength = buf.slice(offset, offset + 2).readUInt16BE(0);
|
||||
offset += 2;
|
||||
const tlvData = buf.slice(offset, offset + tlvLength);
|
||||
offset += tlvLength;
|
||||
|
||||
let data = tlvData.toString('ascii') as string;
|
||||
if (section.description.includes("IP")) {
|
||||
data = num2dot(tlvData.readUInt32BE(0));
|
||||
}
|
||||
|
||||
rows.push([
|
||||
chunkString(tlvData.toString('hex'), 2),
|
||||
data,
|
||||
tlvType + ':' + section.description,
|
||||
]);
|
||||
} else if (section.isRepeat && section.repeatSpecs) {
|
||||
if (section.size !== -1) {
|
||||
repeat = value;
|
||||
}
|
||||
|
||||
let specs : Spec[] = [];
|
||||
for (let i = 0; i < repeat; i++) {
|
||||
specs.push(...section.repeatSpecs);
|
||||
}
|
||||
|
||||
const subrows : any[] = parseBuffer(buf.slice(offset), specs, repeat);
|
||||
rows.push(...subrows);
|
||||
} else {
|
||||
rows.push([chunkString(bufStr, 2).join(' '), value, section.description]);
|
||||
}
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function printBuffer(buf : Buffer, spec : Spec[]) {
|
||||
const rows = parseBuffer(buf, spec);
|
||||
|
||||
const lastRow = rows[rows.length - 1];
|
||||
|
||||
if (!!lastRow.raw) {
|
||||
console.log((new Table(rows.slice(0, -1))).toString());
|
||||
console.log(lastRow.raw);
|
||||
} else {
|
||||
console.log((new Table(rows)).toString());
|
||||
}
|
||||
}
|
||||
|
||||
function bufferFromWebText(webtext : string) : Buffer {
|
||||
return Buffer.from(webtext.replace(/\s/g, ''), 'hex');
|
||||
}
|
||||
|
||||
const SNAC_01_0F = [
|
||||
byte("FLAP Header"),
|
||||
byte("Channel"),
|
||||
word("Sequence ID"),
|
||||
word("Payload Length"),
|
||||
word("SNAC Family"),
|
||||
word("SNAC Subtype"),
|
||||
word("SNAC Flags"),
|
||||
dword("SNAC Request-ID"),
|
||||
param(8, "UIN String Length"),
|
||||
word("Warning Level"),
|
||||
word("Number of TLV in list"),
|
||||
|
||||
tlv("User Class"),
|
||||
tlv("User Status"),
|
||||
tlv("External IP Address"),
|
||||
tlv("Client Idle Time"),
|
||||
tlv("Signon Time"),
|
||||
tlv("Unknown Value"),
|
||||
tlv("Member Since"),
|
||||
|
||||
word("DC Info"),
|
||||
word("DC Info Length"),
|
||||
dword("DC Internal IP Address"),
|
||||
dword("DC TCP Port"),
|
||||
dword("DC Type"),
|
||||
word("DC Protocol Version"),
|
||||
dword("DC Auth Cookie"),
|
||||
dword("Web Front Port"),
|
||||
dword("Client Features"),
|
||||
dword("Last Info Update Time"),
|
||||
dword("Last EXT info update time"),
|
||||
dword("Last EXT status update time"),
|
||||
];
|
||||
|
||||
const exSNAC_01_0F = ''+
|
||||
`
|
||||
2a 02 00 05 00 71 00 01
|
||||
00 0f 00 00 00 00 00 00
|
||||
03 34 30 30 00 00 00 08
|
||||
00 01 00 01 80 00 06 00
|
||||
04 00 00 04 01 00 0a 00
|
||||
04 c0 a8 01 fe 00 0f 00
|
||||
04 00 00 00 00 00 03 00
|
||||
04 61 3f aa 53 00 1e 00
|
||||
04 00 00 00 00 00 05 00
|
||||
04 36 78 bf a0 00 0c 00
|
||||
26 c0 a8 01 fe 00 00 16
|
||||
44 04 00 00 00 04 00 00
|
||||
00 00 00 00 00 00 00 00
|
||||
00 03 00 00 00 00 00 00
|
||||
00 00 00 00 00 00 00
|
||||
`;
|
||||
|
||||
const SNAC_01_07 = [
|
||||
byte("FLAP Header"),
|
||||
byte("Channel"),
|
||||
word("Sequence ID"),
|
||||
word("Payload Length"),
|
||||
word("SNAC Family"),
|
||||
word("SNAC Subtype"),
|
||||
word("SNAC Flags"),
|
||||
dword("SNAC Request-ID"),
|
||||
|
||||
repeat(16, "Number of Rate Classes", [
|
||||
word('Rate class ID'),
|
||||
dword('Window size'),
|
||||
dword('Clear level'),
|
||||
dword('Alert level'),
|
||||
dword('Limit level'),
|
||||
dword('Disconnect level'),
|
||||
dword('Current level'),
|
||||
dword('Max level'),
|
||||
dword('Last time'),
|
||||
byte('Current State'),
|
||||
]),
|
||||
|
||||
dump(),
|
||||
];
|
||||
|
||||
const exSNAC_01_07 = ''+
|
||||
`
|
||||
2a 02 00 05 03 3b 00 01 00 07 00 00 00 00
|
||||
00 00 00 05 00 01 00 00 00 50 00 00 09 c4 00 00
|
||||
07 d0 00 00 05 dc 00 00 03 20 00 00 16 dc 00 00
|
||||
17 70 00 00 00 00 00 00 02 00 00 00 50 00 00 0b
|
||||
b8 00 00 07 d0 00 00 05 dc 00 00 03 e8 00 00 17
|
||||
70 00 00 17 70 00 00 00 7b 00 00 03 00 00 00 1e
|
||||
00 00 0e 74 00 00 0f a0 00 00 05 dc 00 00 03 e8
|
||||
00 00 17 70 00 00 17 70 00 00 00 00 00 00 04 00
|
||||
00 00 14 00 00 15 7c 00 00 14 b4 00 00 10 68 00
|
||||
00 0b b8 00 00 17 70 00 00 1f 40 00 00 00 7b 00
|
||||
00 05 00 00 00 0a 00 00 15 7c 00 00 14 b4 00 00
|
||||
10 68 00 00 0b b8 00 00 17 70 00 00 1f 40 00 00
|
||||
00 7b 00 00 01 00 91 00 01 00 01 00 01 00 02 00
|
||||
01 00 03 00 01 00 04 00 01 00 05 00 01 00 06 00
|
||||
01 00 07 00 01 00 08 00 01 00 09 00 01 00 0a 00
|
||||
01 00 0b 00 01 00 0c 00 01 00 0d 00 01 00 0e 00
|
||||
01 00 0f 00 01 00 10 00 01 00 11 00 01 00 12 00
|
||||
01 00 13 00 01 00 14 00 01 00 15 00 01 00 16 00
|
||||
01 00 17 00 01 00 18 00 01 00 19 00 01 00 1a 00
|
||||
01 00 1b 00 01 00 1c 00 01 00 1d 00 01 00 1e 00
|
||||
01 00 1f 00 01 00 20 00 01 00 21 00 02 00 01 00
|
||||
02 00 02 00 02 00 03 00 02 00 04 00 02 00 06 00
|
||||
02 00 07 00 02 00 08 00 02 00 0a 00 02 00 0c 00
|
||||
02 00 0d 00 02 00 0e 00 02 00 0f 00 02 00 10 00
|
||||
02 00 11 00 02 00 12 00 02 00 13 00 02 00 14 00
|
||||
02 00 15 00 03 00 01 00 03 00 02 00 03 00 03 00
|
||||
03 00 06 00 03 00 07 00 03 00 08 00 03 00 09 00
|
||||
03 00 0a 00 03 00 0b 00 03 00 0c 00 04 00 01 00
|
||||
04 00 02 00 04 00 03 00 04 00 04 00 04 00 05 00
|
||||
04 00 07 00 04 00 08 00 04 00 09 00 04 00 0a 00
|
||||
04 00 0b 00 04 00 0c 00 04 00 0d 00 04 00 0e 00
|
||||
04 00 0f 00 04 00 10 00 04 00 11 00 04 00 12 00
|
||||
04 00 13 00 04 00 14 00 06 00 01 00 06 00 02 00
|
||||
06 00 03 00 08 00 01 00 08 00 02 00 09 00 01 00
|
||||
09 00 02 00 09 00 03 00 09 00 04 00 09 00 09 00
|
||||
09 00 0a 00 09 00 0b 00 0a 00 01 00 0a 00 02 00
|
||||
0a 00 03 00 0b 00 01 00 0b 00 02 00 0b 00 03 00
|
||||
0b 00 04 00 0c 00 01 00 0c 00 02 00 0c 00 03 00
|
||||
13 00 01 00 13 00 02 00 13 00 03 00 13 00 04 00
|
||||
13 00 05 00 13 00 06 00 13 00 07 00 13 00 08 00
|
||||
13 00 09 00 13 00 0a 00 13 00 0b 00 13 00 0c 00
|
||||
13 00 0d 00 13 00 0e 00 13 00 0f 00 13 00 10 00
|
||||
13 00 11 00 13 00 12 00 13 00 13 00 13 00 14 00
|
||||
13 00 15 00 13 00 16 00 13 00 17 00 13 00 18 00
|
||||
13 00 19 00 13 00 1a 00 13 00 1b 00 13 00 1c 00
|
||||
13 00 1d 00 13 00 1e 00 13 00 1f 00 13 00 20 00
|
||||
13 00 21 00 13 00 22 00 13 00 23 00 13 00 24 00
|
||||
13 00 25 00 13 00 26 00 13 00 27 00 13 00 28 00
|
||||
15 00 01 00 15 00 02 00 15 00 03 00 02 00 06 00
|
||||
03 00 04 00 03 00 05 00 09 00 05 00 09 00 06 00
|
||||
09 00 07 00 09 00 08 00 03 00 02 00 02 00 05 00
|
||||
04 00 06 00 04 00 02 00 02 00 09 00 02 00 0b 00
|
||||
05 00 00
|
||||
`;
|
||||
|
||||
if (require.main === module) {
|
||||
printBuffer(bufferFromWebText(exSNAC_01_07), SNAC_01_07);
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import { FLAP, SNAC, TLV } from '../src/structures';
|
||||
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, [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);
|
||||
assert(md5_auth_req.payload.subtype === 6);
|
||||
assert(md5_auth_req.payload.payload.length === 1);
|
||||
assert.equal(md5_auth_req.payload.payload.length, 1);
|
||||
assert(md5_auth_req.payload.payload[0] instanceof TLV);
|
||||
},
|
||||
() => {
|
||||
// Test FLAP.length calculation and consuming multiple messages
|
||||
const dataStr = `
|
||||
2a 02 4b 11 00 0a 00 01
|
||||
00 0e 00 00 00 00 00 00
|
||||
2a 02 4b 12 00 0a 00 02
|
||||
00 02 00 00 00 00 00 00
|
||||
`.trim().replace(/\s+/g, '');
|
||||
let data = Buffer.from(dataStr, 'hex');
|
||||
|
||||
const message = FLAP.fromBuffer(data);
|
||||
assert(message.channel === 2);
|
||||
assert(message.payloadLength === 10);
|
||||
assert(message.length === 16);
|
||||
assert(message.payload instanceof SNAC);
|
||||
assert((message.payload as SNAC).service === 1);
|
||||
assert((message.payload as SNAC).subtype === 0x0e);
|
||||
|
||||
data = data.slice(message.length);
|
||||
const secondMessage = FLAP.fromBuffer(data);
|
||||
assert(secondMessage.length === 16);
|
||||
assert(secondMessage.payload instanceof SNAC);
|
||||
assert((secondMessage.payload as SNAC).service === 2);
|
||||
assert((secondMessage.payload as SNAC).subtype === 0x02);
|
||||
}
|
||||
];
|
||||
|
||||
tests.forEach((testFn) => {
|
||||
testFn();
|
||||
});
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@tsconfig/node14/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"preserveConstEnums": true,
|
||||
"esModuleInterop": true,
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": ["src/**/*", "tests/**/*"],
|
||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
||||
}
|
173
yarn.lock
173
yarn.lock
|
@ -2,14 +2,6 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@75lb/deep-merge@^1.1.0":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@75lb/deep-merge/-/deep-merge-1.1.1.tgz#3b06155b90d34f5f8cc2107d796f1853ba02fd6d"
|
||||
integrity sha512-xvgv6pkMGBA6GwdyJbNAnDmfAIR/DfWhrj9jgWh3TY7gRm3KO46x/GPjRg6wJ0nOepwqrNxFfojebh0Df4h4Tw==
|
||||
dependencies:
|
||||
lodash.assignwith "^4.2.0"
|
||||
typical "^7.1.1"
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
|
@ -22,16 +14,6 @@
|
|||
dependencies:
|
||||
defer-to-connect "^1.0.1"
|
||||
|
||||
"@tsconfig/node14@^1.0.1":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2"
|
||||
integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==
|
||||
|
||||
"@types/node@^16.7.13":
|
||||
version "16.7.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.7.13.tgz#86fae356b03b5a12f2506c6cf6cd9287b205973f"
|
||||
integrity sha512-pLUPDn+YG3FYEt/pHI74HmnJOWzeR+tOIQzUx93pi9M7D8OE7PSLr97HboXwk5F+JS+TLtWuzCOW97AHjmOXXA==
|
||||
|
||||
abbrev@1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
|
||||
|
@ -54,13 +36,6 @@ ansi-regex@^5.0.0:
|
|||
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75"
|
||||
integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==
|
||||
|
||||
ansi-styles@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
|
||||
integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==
|
||||
dependencies:
|
||||
color-convert "^1.9.0"
|
||||
|
||||
ansi-styles@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
|
||||
|
@ -76,21 +51,6 @@ anymatch@~3.1.2:
|
|||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
array-back@^3.0.1, array-back@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/array-back/-/array-back-3.1.0.tgz#b8859d7a508871c9a7b2cf42f99428f65e96bfb0"
|
||||
integrity sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==
|
||||
|
||||
array-back@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-back/-/array-back-4.0.2.tgz#8004e999a6274586beeb27342168652fdb89fa1e"
|
||||
integrity sha512-NbdMezxqf94cnNfWLL7V/im0Ub+Anbb0IoZhvzie8+4HJ4nMQuzHuy49FkGYCJK2yAloZ3meiB6AVMClbrI1vg==
|
||||
|
||||
array-back@^6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/array-back/-/array-back-6.2.0.tgz#83cc80fbef5a46269b1f6ecc82011cfc19cf1c1e"
|
||||
integrity sha512-mixVv03GOOn/ubHE4STQ+uevX42ETdk0JoMVEjNkSOCT7WgERh7C8/+NyhWYNpE3BN69pxFyJIBcF7CxWz/+4A==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
|
@ -148,15 +108,6 @@ camelcase@^5.3.1:
|
|||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
|
||||
integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
|
||||
|
||||
chalk@^2.4.2:
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
|
||||
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
|
||||
dependencies:
|
||||
ansi-styles "^3.2.1"
|
||||
escape-string-regexp "^1.0.5"
|
||||
supports-color "^5.3.0"
|
||||
|
||||
chalk@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4"
|
||||
|
@ -197,13 +148,6 @@ clone-response@^1.0.2:
|
|||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
color-convert@^1.9.0:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
dependencies:
|
||||
color-name "1.1.3"
|
||||
|
||||
color-convert@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
|
||||
|
@ -211,36 +155,11 @@ color-convert@^2.0.1:
|
|||
dependencies:
|
||||
color-name "~1.1.4"
|
||||
|
||||
color-name@1.1.3:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=
|
||||
|
||||
color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
command-line-args@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-5.2.0.tgz#087b02748272169741f1fd7c785b295df079b9be"
|
||||
integrity sha512-4zqtU1hYsSJzcJBOcNZIbW5Fbk9BkjCp1pZVhQKoRaWL5J7N4XphDLwo8aWwdQpTugxwu+jf9u2ZhkXiqp5Z6A==
|
||||
dependencies:
|
||||
array-back "^3.1.0"
|
||||
find-replace "^3.0.0"
|
||||
lodash.camelcase "^4.3.0"
|
||||
typical "^4.0.0"
|
||||
|
||||
command-line-usage@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/command-line-usage/-/command-line-usage-6.1.1.tgz#c908e28686108917758a49f45efb4f02f76bc03f"
|
||||
integrity sha512-F59pEuAR9o1SF/bD0dQBDluhpT4jJQNWUHEuVBqpDmCUo6gPjCi+m9fCWnWZVR/oG6cMTUms4h+3NPl74wGXvA==
|
||||
dependencies:
|
||||
array-back "^4.0.1"
|
||||
chalk "^2.4.2"
|
||||
table-layout "^1.0.1"
|
||||
typical "^5.2.0"
|
||||
|
||||
concat-map@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
|
@ -284,7 +203,7 @@ decompress-response@^3.3.0:
|
|||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
deep-extend@^0.6.0, deep-extend@~0.6.0:
|
||||
deep-extend@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||
|
@ -328,11 +247,6 @@ escape-goat@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675"
|
||||
integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==
|
||||
|
||||
escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
fill-range@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
|
||||
|
@ -340,13 +254,6 @@ fill-range@^7.0.1:
|
|||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
find-replace@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
|
||||
integrity sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==
|
||||
dependencies:
|
||||
array-back "^3.0.1"
|
||||
|
||||
fsevents@~2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
|
@ -540,16 +447,6 @@ latest-version@^5.0.0:
|
|||
dependencies:
|
||||
package-json "^6.3.0"
|
||||
|
||||
lodash.assignwith@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.assignwith/-/lodash.assignwith-4.2.0.tgz#127a97f02adc41751a954d24b0de17e100e038eb"
|
||||
integrity sha1-EnqX8CrcQXUalU0ksN4X4QDgOOs=
|
||||
|
||||
lodash.camelcase@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
|
||||
|
||||
lowercase-keys@^1.0.0, lowercase-keys@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f"
|
||||
|
@ -696,11 +593,6 @@ readdirp@~3.6.0:
|
|||
dependencies:
|
||||
picomatch "^2.2.1"
|
||||
|
||||
reduce-flatten@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/reduce-flatten/-/reduce-flatten-2.0.0.tgz#734fd84e65f375d7ca4465c69798c25c9d10ae27"
|
||||
integrity sha512-EJ4UNY/U1t2P/2k6oqotuX2Cc3T6nxJwsM0N0asT7dhrtH1ltUxDn4NalSYmPE2rCkVpcf/X6R0wDwcFpzhd4w==
|
||||
|
||||
registry-auth-token@^4.0.0:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.2.1.tgz#6d7b4006441918972ccd5fedcd41dc322c79b250"
|
||||
|
@ -744,11 +636,6 @@ signal-exit@^3.0.2:
|
|||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||
|
||||
stream-read-all@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/stream-read-all/-/stream-read-all-3.0.1.tgz#60762ae45e61d93ba0978cda7f3913790052ad96"
|
||||
integrity sha512-EWZT9XOceBPlVJRrYcykW8jyRSZYbkb/0ZK36uLEmoWVO5gxBOnntNTseNzfREsqxqdfEGQrD8SXQ3QWbBmq8A==
|
||||
|
||||
string-width@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
|
||||
|
@ -786,7 +673,7 @@ strip-json-comments@~2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
||||
supports-color@^5.3.0, supports-color@^5.5.0:
|
||||
supports-color@^5.5.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||
|
@ -800,29 +687,6 @@ supports-color@^7.1.0:
|
|||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
table-layout@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-1.0.2.tgz#c4038a1853b0136d63365a734b6931cf4fad4a04"
|
||||
integrity sha512-qd/R7n5rQTRFi+Zf2sk5XVVd9UQl6ZkduPFC3S7WEGJAmetDTjY3qPN50eSKzwuzEyQKy5TN2TiZdkIjos2L6A==
|
||||
dependencies:
|
||||
array-back "^4.0.1"
|
||||
deep-extend "~0.6.0"
|
||||
typical "^5.2.0"
|
||||
wordwrapjs "^4.0.0"
|
||||
|
||||
table-layout@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/table-layout/-/table-layout-3.0.0.tgz#bd0f207ff73eb4dba79019e4f672e61874181b7f"
|
||||
integrity sha512-chMCAvqzsw2k0WVZLQwyiNfHV3lwLdrmTKaofEt3PhedSVdEwOK8/nGetZFaQEg9HjPL9CrhkJWc4ZauFCihXw==
|
||||
dependencies:
|
||||
"@75lb/deep-merge" "^1.1.0"
|
||||
array-back "^6.2.0"
|
||||
command-line-args "^5.2.0"
|
||||
command-line-usage "^6.1.1"
|
||||
stream-read-all "^3.0.1"
|
||||
typical "^7.1.1"
|
||||
wordwrapjs "^5.1.0"
|
||||
|
||||
term-size@^2.1.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.1.tgz#2a6a54840432c2fb6320fea0f415531e90189f54"
|
||||
|
@ -859,26 +723,6 @@ typedarray-to-buffer@^3.1.5:
|
|||
dependencies:
|
||||
is-typedarray "^1.0.0"
|
||||
|
||||
typescript@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.4.2.tgz#6d618640d430e3569a1dfb44f7d7e600ced3ee86"
|
||||
integrity sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==
|
||||
|
||||
typical@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/typical/-/typical-4.0.0.tgz#cbeaff3b9d7ae1e2bbfaf5a4e6f11eccfde94fc4"
|
||||
integrity sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==
|
||||
|
||||
typical@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/typical/-/typical-5.2.0.tgz#4daaac4f2b5315460804f0acf6cb69c52bb93066"
|
||||
integrity sha512-dvdQgNDNJo+8B2uBQoqdb11eUCE1JQXhvjC/CZtgvZseVd5TYMXnq0+vuUemXbd/Se29cTaUuPX3YIc2xgbvIg==
|
||||
|
||||
typical@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/typical/-/typical-7.1.1.tgz#ba177ab7ab103b78534463ffa4c0c9754523ac1f"
|
||||
integrity sha512-T+tKVNs6Wu7IWiAce5BgMd7OZfNYUndHwc5MknN+UHOudi7sGZzuHdCadllRuqJ3fPtgFtIH9+lt9qRv6lmpfA==
|
||||
|
||||
undefsafe@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.3.tgz#6b166e7094ad46313b2202da7ecc2cd7cc6e7aae"
|
||||
|
@ -926,19 +770,6 @@ widest-line@^3.1.0:
|
|||
dependencies:
|
||||
string-width "^4.0.0"
|
||||
|
||||
wordwrapjs@^4.0.0:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-4.0.1.tgz#d9790bccfb110a0fc7836b5ebce0937b37a8b98f"
|
||||
integrity sha512-kKlNACbvHrkpIw6oPeYDSmdCTu2hdMHoyXLTcUKala++lx5Y+wjJ/e474Jqv5abnVmwxw08DiTuHmw69lJGksA==
|
||||
dependencies:
|
||||
reduce-flatten "^2.0.0"
|
||||
typical "^5.2.0"
|
||||
|
||||
wordwrapjs@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrapjs/-/wordwrapjs-5.1.0.tgz#4c4d20446dcc670b14fa115ef4f8fd9947af2b3a"
|
||||
integrity sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
|
|
Loading…
Reference in a new issue