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 net = require('net');
const { logDataStream } = require('./util'); const Communicator = require('./communicator');
const { FLAP } = require('./structures');
const communicators = {};
const server = net.createServer((socket) => { const server = net.createServer((socket) => {
console.log('client connected...'); console.log('client connected...');
socket.setTimeout(5000); socket.setTimeout(30000);
socket.on('error', (e) => { socket.on('error', (e) => {
console.error('socket encountered an error:', e); console.error('socket encountered an error:', e);
}); delete communicators[socket];
socket.on('data', (data) => {
const flap = FLAP.fromBuffer(Buffer.from(data, 'hex'));
console.log('RECV', flap.toString());
}); });
socket.on('timeout', () => { socket.on('timeout', () => {
console.log('socket timeout'); console.log('socket timeout');
socket.end(); socket.end();
}) delete communicators[socket];
});
socket.on('end', () => { socket.on('end', () => {
console.log('client disconnected...'); console.log('client disconnected...');
}); });
const hello = new FLAP(0x01, 0, Buffer.from([0x00, 0x00, 0x00, 0x01])); const c = new Communicator(socket);
socket.write(hello.toBuffer()); communicators[socket] = c;
/* 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)
...
*/
}); });
server.on('error', (err) => { 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() { 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() { toBuffer() {
@ -35,14 +35,14 @@ class SNAC {
const service = buf.slice(2,4).readInt16BE(0); const service = buf.slice(2,4).readInt16BE(0);
const flags = buf.slice(4, 6); const flags = buf.slice(4, 6);
const requestID = buf.slice(6, 10).readInt32BE(0); 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 tlvsIdx = 10;
let cb = 0, cbLimit = 10; //circuit breaker let cb = 0, cbLimit = 20; //circuit breaker
while (payloadIdx < payloadLength && cb < cbLimit) { while (tlvsIdx < payloadLength && cb < cbLimit) {
const tlv = TLV.fromBuffer(buf.slice(payloadIdx)); const tlv = TLV.fromBuffer(buf.slice(tlvsIdx));
payload.push(tlv); tlvs.push(tlv);
payloadIdx += tlv.len + 4; // 4 bytes for TLV type + payload length tlvsIdx += tlv.len + 4; // 4 bytes for TLV type + tlvs length
cb++; cb++;
} }
if (cb === cbLimit) { if (cb === cbLimit) {
@ -50,19 +50,19 @@ class SNAC {
process.exit(1); 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.family = family;
this.service = service; this.service = service;
this.flags = flags; this.flags = flags;
this.requestID = requestID; this.requestID = requestID;
this.payload = payload; this.tlvs = tlvs;
} }
toString() { 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() { toBuffer() {
@ -72,7 +72,13 @@ class SNAC {
SNACHeader.writeUInt16BE(this.flags, 4); SNACHeader.writeUInt16BE(this.flags, 4);
SNACHeader.writeUInt32BE(this.requestID, 6); 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]); return Buffer.concat([SNACHeader, ...payload]);
} }
} }
@ -80,17 +86,21 @@ class SNAC {
class FLAP { class FLAP {
static fromBuffer(buf) { static fromBuffer(buf) {
assert.equal(buf[0], 0x2a, 'Expected 0x2a FLAP header'); assert.equal(buf[0], 0x2a, 'Expected 0x2a FLAP header');
const channel = parseInt(buf[1], 16); const channel = buf.readInt8(1);
const datagramNumber = buf.slice(2,4).readInt16BE(0); const sequenceNumber = buf.slice(2,4).readInt16BE(0);
const payloadLength = buf.slice(4, 6).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);
}
return new FLAP(channel, sequenceNumber, payload)
} }
constructor(channel, datagramNumber, payload) { constructor(channel, sequenceNumber, payload) {
this.channel = channel; this.channel = channel;
this.datagramNumber = datagramNumber; this.sequenceNumber = sequenceNumber;
this.payload = payload; this.payload = payload;
this.payloadLength = this.payload.length; this.payloadLength = this.payload.length;
@ -98,23 +108,19 @@ class FLAP {
if (payload instanceof SNAC) { if (payload instanceof SNAC) {
this.payloadLength = payload.toBuffer().length; this.payloadLength = payload.toBuffer().length;
} }
if (channel === 2 && !(payload instanceof SNAC)) {
this.payload = SNAC.fromBuffer(this.payload, this.payloadLength);
}
} }
toString() { toString() {
const hasSnac = this.payload instanceof SNAC; const hasSnac = this.payload instanceof SNAC;
const payload = hasSnac ? this.payload.toString() : logDataStream(this.payload).split('\n').join('\n '); 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() { toBuffer() {
const FLAPHeader = Buffer.alloc(6, 0, 'hex'); const FLAPHeader = Buffer.alloc(6, 0, 'hex');
FLAPHeader.writeInt8(0x2a, 0); FLAPHeader.writeInt8(0x2a, 0);
FLAPHeader.writeInt8(this.channel, 1); FLAPHeader.writeInt8(this.channel, 1);
FLAPHeader.writeInt16BE(this.datagramNumber, 2); FLAPHeader.writeInt16BE(this.sequenceNumber, 2);
FLAPHeader.writeInt16BE(this.payloadLength, 4); FLAPHeader.writeInt16BE(this.payloadLength, 4);
let payload = this.payload; let payload = this.payload;