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 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) => {
|
||||||
|
|
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() {
|
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;
|
||||||
|
|
Loading…
Reference in a new issue