[go: nahoru, domu]

Skip to content

Commit

Permalink
Refactoring common Oscar protocol bits out of auth server
Browse files Browse the repository at this point in the history
  • Loading branch information
DrewML committed Dec 22, 2020
1 parent aabff7f commit d8ef82c
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 191 deletions.
284 changes: 97 additions & 187 deletions src/AIMAuthServer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import net, { Socket, Server } from 'net';
import { OscarServer, OscarSocket } from './OscarServer';
import assert from 'assert';
import { Flap } from './types';
import crypto from 'crypto';
import { hashClientPassword } from './hashClientLogin';
import { buildFlap, parseFlap } from './flapUtils';
import { matchSnac, parseSnac } from './snacUtils';
import {
authKeyResponseSnac,
Expand All @@ -13,224 +11,136 @@ import {
import { parseAuthRequest, parseMD5LoginRequest } from './clientSnacs';
import { LOGIN_ERRORS } from './constants';

interface AIMAuthServerOpts {
port?: number;
host?: string;
}

/**
* @summary The first server an Oscar Protocol client
* connects to when signing on. Confirms client
* credentials, and returns a cookie and address
* to contact the next service (the BOSS server)
*/
export class AIMAuthServer {
private server: Server;
private host: string;
private port: number;
private socketState: WeakMap<
Socket,
InstanceType<typeof SocketState>
> = new WeakMap();

constructor(opts: AIMAuthServerOpts = {}) {
this.host = opts.host ?? '0.0.0.0';
this.port = opts.port ?? 5190;
this.server = net.createServer(this.onNewConnection.bind(this));
this.server.on('error', this.onServerError.bind(this));
}
export class AIMAuthServer extends OscarServer {
private socketState = new WeakMap<OscarSocket, SocketState>();

private onServerError(error: Error) {
console.error('Server Error: ', error);
}

private getState(socket: Socket) {
const state = this.socketState.get(socket);
private getState(oscarSocket: OscarSocket) {
const state = this.socketState.get(oscarSocket);
assert(state, 'Missing SocketState in AIMAuthServer');
return state;
}

/**
* @see http://web.archive.org/web/20080308233204/http://dev.aol.com/aim/oscar/#FLAP__SIGNON_FRAME
*/
private onNewConnection(socket: Socket) {
const address = socket.remoteAddress;
const port = socket.remotePort;
console.log(`AIMAuthServer: New connection from ${address}:${port}`);
onConnection(oscarSocket: OscarSocket) {
const { host, port } = oscarSocket.remoteAddress;
console.log(`AIMAuthServer: New connection from ${host}:${port}`);

socket.once('connect', () => console.log('new socket connection'));
socket.on('error', () => console.log('socket error'));
socket.on('end', () => console.log('socked connection ending'));
socket.on('close', () => console.log('socket connection closed'));
socket.on('data', (data) => this.onSocketData(data, socket));
oscarSocket.onChannel(0x1, (flap) => {
const flapVersion = flap.data.readUInt32BE(0);
assert(flapVersion === 0x1, 'Incorrect client FLAP version');
});

// Initialize state needed for auth requests
const socketState = new SocketState();
this.socketState.set(socket, socketState);

socket.write(
buildFlap({
channel: 1,
// TODO: Sequence numbers should be generated in an abstraction around socket.write,
// to prevent out-of-order sequences, which is a fatal error for an OSCAR client
sequence: socketState.claimSequenceID(),
data: Buffer.from([0x1]), // Flap version 1
}),
);
}
oscarSocket.onChannel(0x2, (flap) => {
const state = this.getState(oscarSocket);
const snac = parseSnac(flap.data);

private onChannel1(flap: Flap) {
const flapVersion = flap.data.readUInt32BE(0);
assert(flapVersion === 0x1, 'Incorrect client FLAP version');
}
if (matchSnac(snac, 'AUTH', 'MD5_AUTH_REQUEST')) {
const authReq = parseAuthRequest(snac.data);
// Can be _any_ server generated string with len < size of u32 int
const authKey = state.setSalt(
crypto.randomInt(100000000, 9999999999).toString(),
);

private onChannel2(flap: Flap, socket: Socket) {
const snac = parseSnac(flap.data);
const state = this.getState(socket);

if (matchSnac(snac, 'AUTH', 'MD5_AUTH_REQUEST')) {
const authReq = parseAuthRequest(snac.data);
// Can be _any_ server generated string with len < size of u32 int
const authKey = state.setSalt(
crypto.randomInt(100000000, 9999999999).toString(),
);

state.setScreenname(authReq.screenname);
const responseFlap = buildFlap({
channel: 2,
sequence: this.getState(socket).claimSequenceID(),
data: authKeyResponseSnac(authKey, snac.requestID),
});

socket.write(responseFlap);
return;
}

if (matchSnac(snac, 'AUTH', 'LOGIN_REQUEST')) {
const payload = parseMD5LoginRequest(snac.data);
assert(
payload.newHashStrategy,
'Pre-5.2 authentication not yet supported',
);
assert(
payload.screenname === state.getScreenname(),
'Challenge issued for one screenname, but used by another',
);

// TODO: Add persistence so we can have
// non-hardcoded accounts
const hashToMatch = hashClientPassword({
password: 'password',
salt: state.getSalt(),
});
const isValidUsername = true; // TODO: Real lookup
const isValidPass = payload.passwordHash.equals(hashToMatch);

// TODO: handle various diff error types,
// rather than mapping all to INCORRECT_NICK_OR_PASS
if (!(isValidUsername && isValidPass)) {
const responseFlap = buildFlap({
state.setScreenname(authReq.screenname);
const responseFlap = {
channel: 2,
data: authKeyResponseSnac(authKey, snac.requestID),
};

oscarSocket.write(responseFlap);
return;
}

if (matchSnac(snac, 'AUTH', 'LOGIN_REQUEST')) {
const payload = parseMD5LoginRequest(snac.data);
assert(
payload.newHashStrategy,
'Pre-5.2 authentication not yet supported',
);
assert(
payload.screenname === state.getScreenname(),
'Challenge issued for one screenname, but used by another',
);

// TODO: Add persistence so we can have
// non-hardcoded accounts
const hashToMatch = hashClientPassword({
password: 'password',
salt: state.getSalt(),
});
const isValidUsername = true; // TODO: Real lookup
const isValidPass = payload.passwordHash.equals(hashToMatch);

// TODO: handle various diff error types,
// rather than mapping all to INCORRECT_NICK_OR_PASS
if (!(isValidUsername && isValidPass)) {
const responseFlap = {
channel: 2,
data: loginErrorSnac({
screenname: payload.screenname,
errorCode: LOGIN_ERRORS.INCORRECT_NICK_OR_PASS,
errorURL: 'https://drewml.com',
reqID: snac.requestID,
}),
};

oscarSocket.write(responseFlap);
return;
}

const responseFlap = {
channel: 2,
sequence: state.claimSequenceID(),
data: loginErrorSnac({
data: loginSuccessSnac({
screenname: payload.screenname,
errorCode: LOGIN_ERRORS.INCORRECT_NICK_OR_PASS,
errorURL: 'https://drewml.com',
// TODO: Should be pulled from DB when real
// persistence is added
email: 'DrewML@users.noreply.github.com',
// Point to BOSS host/port
bosAddress: 'host.test:5190',
// TODO: Stop hardcoding BOSS cookie
authCookie: '111111111',
latestBetaVersion: '8.1.4',
latestBetaChecksum: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
passwordChangeURL: 'https://drewml.com',
reqID: snac.requestID,
}),
});
};

socket.write(responseFlap);
oscarSocket.write(responseFlap);
return;
}
console.log('Unhandled Channel 2 Flap: ', flap);
});

const responseFlap = buildFlap({
channel: 2,
sequence: state.claimSequenceID(),
data: loginSuccessSnac({
screenname: payload.screenname,
// TODO: Should be pulled from DB when real
// persistence is added
email: 'DrewML@users.noreply.github.com',
bosAddress: 'host.test:5190',
authCookie: '111111111',
latestBetaVersion: '8.1.4',
latestBetaChecksum: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
passwordChangeURL: 'https://drewml.com',
reqID: snac.requestID,
}),
});

socket.write(responseFlap);
return;
}
console.log('Unhandled Channel 2 Flap: ', flap);
}

private onChannel3(flap: Flap, socket: Socket) {
console.log('Channel 3 Flap: ', flap);
}
oscarSocket.onChannel(0x3, (flap) => {
console.log('Unimplemented channel 3 flap: ', flap);
});

private onChannel4(flap: Flap, socket: Socket) {
console.log('Channel 4 Flap: ', flap);
}
oscarSocket.onChannel(0x4, (flap) => {
console.log('Unimplemented channel 4 flap: ', flap);
});

private onChannel5(flap: Flap, socket: Socket) {
console.log('Channel 5 Flap: ', flap);
}
oscarSocket.onChannel(0x5, (flap) => {
console.log('Unimplemented channel 5 flap: ', flap);
});

private onSocketData(data: Buffer, socket: Socket) {
const flap = parseFlap(data);
const handlers: ChannelHandlers = {
1: this.onChannel1,
2: this.onChannel2,
3: this.onChannel3,
4: this.onChannel4,
5: this.onChannel5,
};
const handler = handlers[flap.channel];

if (!handler) {
console.warn(
`AIMAuthServer: Unrecognized FLAP channel "${flap.channel}". FLAP will be skipped`,
flap,
);
return;
}

handler.call(this, flap, socket);
}
// Initialize state needed for auth requests
const socketState = new SocketState();
this.socketState.set(oscarSocket, socketState);

listen(): Promise<{ host: string; port: number }> {
return new Promise((res) => {
this.server.listen(this.port, this.host, () => {
const address = this.server.address();
assert(
address && typeof address !== 'string',
'Unexpected net.AddressInfo in AIMAuthServer.listen',
);
res({ host: address.address, port: address.port });
});
});
oscarSocket.sendStartFlap();
}
}

/**
* @see http://web.archive.org/web/20080308233204/http://dev.aol.com/aim/oscar/#FLAP__FRAME_TYPE
*/
interface ChannelHandlers {
[key: number]: (flap: Flap, socket: Socket) => void;
}

class SocketState {
private lastSequenceID: number = 0;
private salt: string | undefined;
private screenname: string | undefined;

claimSequenceID() {
return this.lastSequenceID++;
}

setScreenname(screenname: string) {
return (this.screenname = screenname);
}
Expand Down
Loading

0 comments on commit d8ef82c

Please sign in to comment.