mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-21 20:19:47 -05:00
user authentication, start building out services
This commit is contained in:
parent
de67f624bf
commit
f762506bf4
6 changed files with 253 additions and 83 deletions
106
src/communicator.js
Normal file
106
src/communicator.js
Normal 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
7
src/consts.js
Normal 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,
|
||||
};
|
67
src/index.js
67
src/index.js
|
@ -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) => {
|
||||
|
|
79
src/services/authorization-registration.js
Normal file
79
src/services/authorization-registration.js
Normal 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
21
src/services/base.js
Normal 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;
|
|
@ -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);
|
||||
}
|
||||
|
||||
return new FLAP(channel, sequenceNumber, payload)
|
||||
}
|
||||
|
||||
constructor(channel, datagramNumber, 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;
|
||||
|
|
Loading…
Reference in a new issue