user authentication, start building out services

This commit is contained in:
Artem Titoulenko 2021-09-08 12:33:57 -04:00
parent de67f624bf
commit f762506bf4
6 changed files with 253 additions and 83 deletions

106
src/communicator.js Normal file
View file

@ -0,0 +1,106 @@
const { FLAP, SNAC, TLV } = require('./structures');
const { logDataStream } = require('./util');
const { FLAGS_EMPTY } = require('./consts');
const AuthorizationRegistrationService = require("./services/authorization-registration");
class Communicator {
constructor(socket) {
// Hold on to the socket
this.socket = socket;
this.socket.on('data', (data) => {
console.log('DATA-----------------------');
console.log('RECV', logDataStream(Buffer.from(data, 'hex')));
const flap = FLAP.fromBuffer(Buffer.from(data, 'hex'));
console.log('RECV', flap.toString());
this.handleMessage(flap);
});
this._sequenceNumber = 0;
this.registerServices();
this.start();
}
start() {
// Start negotiating a connection
const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01]));
this.send(hello);
}
registerServices() {
const services = [
new AuthorizationRegistrationService(this),
];
this.services = {};
services.forEach((service) => {
this.services[service.family] = service;
});
}
_getNewSequenceNumber() {
return ++this._sequenceNumber;
}
send(message) {
console.log('SEND', message.toString());
console.log('SEND', logDataStream(message.toBuffer()));
console.log('-----------------------DATA');
this.socket.write(message.toBuffer());
}
handleMessage(message) {
switch (message.channel) {
case 1:
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(tlv.toString());
switch (tlv.type) {
case 0x06: // Requesting available services
const resp = new FLAP(2, this._getNewSequenceNumber(), new SNAC(0x01, 0x03, FLAGS_EMPTY, 0, [
Buffer.from([0x00, 0x01, 0x00, 0x02, 0x00, 0x03, 0x00, 0x04,
0x00, 0x06, 0x00, 0x08, 0x00, 0x09, 0x00, 0x0A,
0x00, 0x0B, 0x00, 0x0C, 0x00, 0x13, 0x00, 0x15,
]), // Buddy List management service
]));
this.send(resp);
return;
}
return;
case 2:
if (!message.payload) {
console.error('No SNAC');
return;
}
const familyService = this.services[message.payload.family];
if (!familyService) {
console.warn('no handler for family', message.payload.family);
return;
}
familyService.handleMessage(message);
default:
console.warn('No handlers for channel', message.channel);
return;
}
}
}
module.exports = Communicator;

7
src/consts.js Normal file
View file

@ -0,0 +1,7 @@
const AIM_MD5_STRING = "AOL Instant Messenger (SM)";
const FLAGS_EMPTY = Buffer.from([0x00, 0x00, 0x00, 0x00]);
module.exports = {
AIM_MD5_STRING,
FLAGS_EMPTY,
};

View file

@ -1,78 +1,29 @@
const net = require('net');
const { logDataStream } = require('./util');
const { FLAP } = require('./structures');
const Communicator = require('./communicator');
const communicators = {};
const server = net.createServer((socket) => {
console.log('client connected...');
socket.setTimeout(5000);
socket.setTimeout(30000);
socket.on('error', (e) => {
console.error('socket encountered an error:', e);
});
socket.on('data', (data) => {
const flap = FLAP.fromBuffer(Buffer.from(data, 'hex'));
console.log('RECV', flap.toString());
delete communicators[socket];
});
socket.on('timeout', () => {
console.log('socket timeout');
socket.end();
})
delete communicators[socket];
});
socket.on('end', () => {
console.log('client disconnected...');
});
const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01]));
socket.write(hello.toBuffer());
/* 1. on connection, server sends
2a FLAP
01 channel 1
00 01 datagram #1
00 04 4 bytes of data
00 00 00 1
*/
/* 2. client responds
2a FLAP
01 channel 1
51 11 datagram 11
00 04 4 bytes
00 00 00 01
*/
/* 3. client sends username
2a FLAP
02 channel 2 (SNAC)
51 12 datagram 12
00 12 18 bytes of data
00 17 Service (Authorization/registration service)
00 06 Family (Request md5 authkey)
00 00 Flags
00 00 00 00 SNAC request id
00 01 TLV.Type(0x01) - screen name
00 04 TLV.Length (4)
74 6f 6f 66 toof
*/
/* 4. server responds
2a FLAP
02 Channel 2 (SNAC)
00 02 datagram 2
00 16 22 bytes
00 17 Service (Authorization/registration service)
00 07 Server md5 authkey response
This snac contain server generated auth key. Client should use it to crypt password.
00 00 Flags
00 00 00 00 SNAC request ID
00 0a Length (10 bytes)
...
*/
const c = new Communicator(socket);
communicators[socket] = c;
});
server.on('error', (err) => {

View file

@ -0,0 +1,79 @@
const crypto = require('crypto');
const BaseService = require('./base');
const { FLAP, SNAC, TLV } = require('../structures');
const { AIM_MD5_STRING, FLAGS_EMPTY } = require('../consts');
const users = {
toof: 'foo',
};
class AuthorizationRegistrationService extends BaseService {
constructor(communicator) {
super({ family: 0x17, version: 0x01 }, communicator);
this.cipher = "HARDY";
}
handleMessage(message) {
switch (message.payload.service) {
case 0x02: // Client login request (md5 login sequence)
const tlvs = message.payload.tlvs;
const clientNameTLV = tlvs.find((tlv) => tlv.type === 0x03);
console.log("Attempting connection from", clientNameTLV.payload.toString('ascii'));
const userTLV = tlvs.find((tlv) => tlv.type === 0x01);
const username = userTLV.payload.toString('ascii');
if (!users[username]) {
const authResp = new FLAP(2, this._getNewSequenceNumber(),
new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [
new TLV(0x0001, Buffer.from(username)), // username
new TLV(0x0008, Buffer.from([0x00, 0x04])) // incorrect nick/password
]));
this.send(authResp);
return;
}
const passwordHashTLV = tlvs.find((tlv) => tlv.type === 0x25);
const pwHash = crypto.createHash('md5');
pwHash.update(this.cipher);
pwHash.update(users[username]);
pwHash.update(AIM_MD5_STRING);
const digest = pwHash.digest('hex');
if (digest !== passwordHashTLV.payload.toString('hex')) {
console.log('Invalid password for', username);
const authResp = new FLAP(2, this._getNewSequenceNumber(),
new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [
new TLV(0x0001, Buffer.from(username)), // username
new TLV(0x0008, Buffer.from([0x00, 0x04])) // incorrect nick/password
]));
this.send(authResp);
return;
}
const authResp = new FLAP(2, this._getNewSequenceNumber(),
new SNAC(0x17, 0x03, FLAGS_EMPTY, 0, [
new TLV(0x01, Buffer.from(username)), // username
new TLV(0x05, Buffer.from('10.0.1.29:5190')), // BOS address
new TLV(0x06, Buffer.from('im a cookie uwu')) // Authorization cookie
]));
this.send(authResp);
return;
case 0x06: // Request md5 authkey
const payload = Buffer.alloc(2, 0xFF, 'hex');
payload.writeUInt16BE(this.cipher.length);
const md5ReqResp = new FLAP(2, this._getNewSequenceNumber(),
new SNAC(0x17, 0x07, FLAGS_EMPTY, 0, [
Buffer.concat([payload, Buffer.from(this.cipher, 'binary')]),
]));
this.send(md5ReqResp);
break;
}
}
}
module.exports = AuthorizationRegistrationService;

21
src/services/base.js Normal file
View file

@ -0,0 +1,21 @@
class BaseService {
constructor({family, version}, communicator) {
this.family = family;
this.version = version;
this.communicator = communicator;
}
send(message) {
this.communicator.send(message);
}
_getNewSequenceNumber() {
return this.communicator._getNewSequenceNumber();
}
handleMessage(message) {
return null;
}
}
module.exports = BaseService;

View file

@ -18,7 +18,7 @@ class TLV {
}
toString() {
return `TLV(${this.type}, ${this.len}, ${this.payload.toString('ascii')})`;
return `TLV(0x${this.type.toString(16).padStart(2, '0')}, ${this.len}, ${this.payload.toString('ascii')})`;
}
toBuffer() {
@ -35,14 +35,14 @@ class SNAC {
const service = buf.slice(2,4).readInt16BE(0);
const flags = buf.slice(4, 6);
const requestID = buf.slice(6, 10).readInt32BE(0);
const payload = []; // SNACs can have multiple TLVs
const tlvs = []; // SNACs can have multiple TLVs
let payloadIdx = 10;
let cb = 0, cbLimit = 10; //circuit breaker
while (payloadIdx < payloadLength && cb < cbLimit) {
const tlv = TLV.fromBuffer(buf.slice(payloadIdx));
payload.push(tlv);
payloadIdx += tlv.len + 4; // 4 bytes for TLV type + payload length
let tlvsIdx = 10;
let cb = 0, cbLimit = 20; //circuit breaker
while (tlvsIdx < payloadLength && cb < cbLimit) {
const tlv = TLV.fromBuffer(buf.slice(tlvsIdx));
tlvs.push(tlv);
tlvsIdx += tlv.len + 4; // 4 bytes for TLV type + tlvs length
cb++;
}
if (cb === cbLimit) {
@ -50,19 +50,19 @@ class SNAC {
process.exit(1);
}
return new SNAC(family, service, flags, requestID, payload);
return new SNAC(family, service, flags, requestID, tlvs);
}
constructor(family, service, flags, requestID, payload) {
constructor(family, service, flags, requestID, tlvs = []) {
this.family = family;
this.service = service;
this.flags = flags;
this.requestID = requestID;
this.payload = payload;
this.tlvs = tlvs;
}
toString() {
return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.payload}`;
return `SNAC(${this.family.toString(16)},${this.service.toString(16)}) #${this.requestID}\n ${this.tlvs}`;
}
toBuffer() {
@ -72,7 +72,13 @@ class SNAC {
SNACHeader.writeUInt16BE(this.flags, 4);
SNACHeader.writeUInt32BE(this.requestID, 6);
const payload = this.payload.map((tlv) => tlv.toBuffer());
const payload = this.tlvs.map((thing) => {
if (thing instanceof TLV) {
return thing.toBuffer();
}
return thing;
});
return Buffer.concat([SNACHeader, ...payload]);
}
}
@ -80,17 +86,21 @@ class SNAC {
class FLAP {
static fromBuffer(buf) {
assert.equal(buf[0], 0x2a, 'Expected 0x2a FLAP header');
const channel = parseInt(buf[1], 16);
const datagramNumber = buf.slice(2,4).readInt16BE(0);
const channel = buf.readInt8(1);
const sequenceNumber = buf.slice(2,4).readInt16BE(0);
const payloadLength = buf.slice(4, 6).readInt16BE(0);
const payload = buf.slice(6, 6 + payloadLength);
let payload = buf.slice(6, 6 + payloadLength);
return new FLAP(channel, datagramNumber, payload)
if (channel === 2) {
payload = SNAC.fromBuffer(payload, payloadLength);
}
constructor(channel, datagramNumber, payload) {
return new FLAP(channel, sequenceNumber, payload)
}
constructor(channel, sequenceNumber, payload) {
this.channel = channel;
this.datagramNumber = datagramNumber;
this.sequenceNumber = sequenceNumber;
this.payload = payload;
this.payloadLength = this.payload.length;
@ -98,23 +108,19 @@ class FLAP {
if (payload instanceof SNAC) {
this.payloadLength = payload.toBuffer().length;
}
if (channel === 2 && !(payload instanceof SNAC)) {
this.payload = SNAC.fromBuffer(this.payload, this.payloadLength);
}
}
toString() {
const hasSnac = this.payload instanceof SNAC;
const payload = hasSnac ? this.payload.toString() : logDataStream(this.payload).split('\n').join('\n ');
return `ch:${this.channel}, dn: ${this.datagramNumber}, len: ${this.payloadLength}, payload:\n ${payload}`
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.datagramNumber, 2);
FLAPHeader.writeInt16BE(this.sequenceNumber, 2);
FLAPHeader.writeInt16BE(this.payloadLength, 4);
let payload = this.payload;