refactor: major code cleanups :-)
first working version
This commit is contained in:
204
src/app.ts
204
src/app.ts
@@ -1,186 +1,42 @@
|
||||
import * as dgram from 'dgram';
|
||||
import * as radius from 'radius';
|
||||
// import * as dgram from "dgram";
|
||||
// import * as fs from 'fs';
|
||||
import { EAPHandler } from './eap';
|
||||
import { IDeferredPromise, makeid, MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from './helpers';
|
||||
|
||||
import { GoogleLDAPAuth } from './auth/google-ldap';
|
||||
import { AdditionalAuthHandler } from './types/Handler';
|
||||
import { UDPServer } from './server/UDPServer';
|
||||
import { RadiusService } from './radius/RadiusService';
|
||||
|
||||
const server = dgram.createSocket('udp4');
|
||||
import * as config from '../config';
|
||||
|
||||
const { argv } = require('yargs')
|
||||
.usage('RADIUS Server\nUsage: $0')
|
||||
.example('$0 --port 1812 -s radiussecret')
|
||||
.default({
|
||||
port: 1812,
|
||||
s: 'testing123',
|
||||
baseDN: 'dc=hokify,dc=com',
|
||||
ldapServer: 'ldap://127.0.0.1:1636'
|
||||
})
|
||||
.describe('baseDN', 'LDAP Base DN')
|
||||
.describe('ldapServer', 'LDAP Server')
|
||||
.describe('port', 'RADIUS server listener port')
|
||||
.alias('s', 'secret')
|
||||
.describe('secret', 'RADIUS secret')
|
||||
.string(['secret', 'baseDN'])
|
||||
.demand('secret');
|
||||
|
||||
console.log(`Listener Port: ${argv.port}`);
|
||||
console.log(`RADIUS Secret: ${argv.secret}`);
|
||||
console.log(`LDAP Base DN: ${argv.baseDN}`);
|
||||
console.log(`LDAP Server: ${argv.ldapServer}`);
|
||||
console.log(`Listener Port: ${config.port || 1812}`);
|
||||
console.log(`RADIUS Secret: ${config.secret}`);
|
||||
console.log(`Auth Mode: ${config.authentication}`);
|
||||
|
||||
// const ldap = new LDAPAuth({url: 'ldap://ldap.google.com', base: 'dc=hokify,dc=com', uid: 'uid', tlsOptions});
|
||||
|
||||
const ldap = new GoogleLDAPAuth(argv.ldapServer, argv.baseDN);
|
||||
const ldap = new GoogleLDAPAuth(
|
||||
config.authenticationOptions.url,
|
||||
config.authenticationOptions.base
|
||||
);
|
||||
|
||||
const eapHandler = new EAPHandler();
|
||||
const timeout: { [key: string]: NodeJS.Timeout } = {};
|
||||
const waitForNextMsg: { [key: string]: IDeferredPromise } = {};
|
||||
const server = new UDPServer(config.port);
|
||||
const radiusService = new RadiusService(config.secret, ldap);
|
||||
|
||||
function sendToClient(
|
||||
msg: string | Uint8Array,
|
||||
offset: number,
|
||||
length: number,
|
||||
port?: number,
|
||||
address?: string,
|
||||
callback?: (error: Error | null, bytes: number) => void,
|
||||
stateForRetry?: string
|
||||
): void {
|
||||
let retried = 0;
|
||||
(async () => {
|
||||
server.on('message', async (msg, rinfo) => {
|
||||
const response = await radiusService.handleMessage(msg);
|
||||
|
||||
function sendResponse() {
|
||||
console.log(`sending response... (try: ${retried})`);
|
||||
server.send(msg, offset, length, port, address, (error: Error | null, bytes: number) => {
|
||||
// all good
|
||||
|
||||
if (callback) callback(error, bytes);
|
||||
});
|
||||
|
||||
if (stateForRetry && retried < 3) {
|
||||
// timeout[stateForRetry] = setTimeout(sendResponse, 600 * (retried+1));
|
||||
}
|
||||
retried++;
|
||||
}
|
||||
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
server.on('message', async function(msg, rinfo) {
|
||||
const packet = radius.decode({ packet: msg, secret: argv.secret });
|
||||
|
||||
if (packet.code !== 'Access-Request') {
|
||||
console.log('unknown packet type: ', packet.code);
|
||||
return;
|
||||
}
|
||||
// console.log('packet.attributes', packet.attributes);
|
||||
|
||||
// console.log('rinfo', rinfo);
|
||||
|
||||
async function checkAuth(
|
||||
username: string,
|
||||
password: string,
|
||||
additionalAuthHandler?: AdditionalAuthHandler
|
||||
) {
|
||||
console.log(`Access-Request for ${username}`);
|
||||
let success = false;
|
||||
try {
|
||||
await ldap.authenticate(username, password);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const attributes: any[] = [];
|
||||
|
||||
if (additionalAuthHandler) {
|
||||
await additionalAuthHandler(success, { packet, attributes, secret: argv.secret });
|
||||
}
|
||||
|
||||
const response = radius.encode_response({
|
||||
packet,
|
||||
code: success ? 'Access-Accept' : 'Access-Reject',
|
||||
secret: argv.secret,
|
||||
attributes
|
||||
});
|
||||
console.log(`Sending ${success ? 'accept' : 'reject'} for user ${username}`);
|
||||
|
||||
sendToClient(response, 0, response.length, rinfo.port, rinfo.address, function(err, _bytes) {
|
||||
if (err) {
|
||||
console.log('Error sending response to ', rinfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (packet.attributes['EAP-Message']) {
|
||||
const state = (packet.attributes.State && packet.attributes.State.toString()) || makeid(16);
|
||||
|
||||
if (timeout[state]) {
|
||||
clearTimeout(timeout[state]);
|
||||
}
|
||||
|
||||
const handlers = {
|
||||
response: (EAPMessage: Buffer) => {
|
||||
const attributes: any = [['State', Buffer.from(state)]];
|
||||
let sentDataSize = 0;
|
||||
do {
|
||||
if (EAPMessage.length > 0) {
|
||||
attributes.push([
|
||||
'EAP-Message',
|
||||
EAPMessage.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
|
||||
]);
|
||||
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
|
||||
if (response) {
|
||||
server.sendToClient(
|
||||
response.data,
|
||||
rinfo.port,
|
||||
rinfo.address,
|
||||
(err, _bytes) => {
|
||||
if (err) {
|
||||
console.log('Error sending response to ', rinfo);
|
||||
}
|
||||
} while (sentDataSize < EAPMessage.length);
|
||||
|
||||
const response = radius.encode_response({
|
||||
packet,
|
||||
code: 'Access-Challenge',
|
||||
secret: argv.secret,
|
||||
attributes
|
||||
});
|
||||
|
||||
waitForNextMsg[state] = newDeferredPromise();
|
||||
|
||||
sendToClient(
|
||||
response,
|
||||
0,
|
||||
response.length,
|
||||
rinfo.port,
|
||||
rinfo.address,
|
||||
function(err, _bytes) {
|
||||
if (err) {
|
||||
console.log('Error sending response to ', rinfo);
|
||||
}
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
return waitForNextMsg[state].promise;
|
||||
},
|
||||
checkAuth
|
||||
};
|
||||
|
||||
if (waitForNextMsg[state]) {
|
||||
const identifier = packet.attributes['EAP-Message'].slice(1, 2).readUInt8(0); // .toString('hex');
|
||||
waitForNextMsg[state].resolve({ response: handlers.response, identifier });
|
||||
},
|
||||
response.expectAcknowledgment
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// EAP MESSAGE
|
||||
eapHandler.handleEAPMessage(packet.attributes['EAP-Message'], state, handlers);
|
||||
} else {
|
||||
const username = packet.attributes['User-Name'];
|
||||
const password = packet.attributes['User-Password'];
|
||||
|
||||
checkAuth(username, password);
|
||||
}
|
||||
});
|
||||
|
||||
server.on('listening', function() {
|
||||
const address = server.address();
|
||||
console.log(`radius server listening ${address.address}:${address.port}`);
|
||||
});
|
||||
|
||||
server.bind(argv.port);
|
||||
// start server
|
||||
await server.start();
|
||||
})();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as NodeCache from 'node-cache';
|
||||
|
||||
import { createClient, Client } from 'ldapjs';
|
||||
import { Client, createClient } from 'ldapjs';
|
||||
import { IAuthentication } from '../types/Authentication';
|
||||
|
||||
const usernameFields = ['posixUid', 'mail'];
|
||||
@@ -58,7 +58,7 @@ export class GoogleLDAPAuth implements IAuthentication {
|
||||
});
|
||||
|
||||
res.on('end', result => {
|
||||
console.log(`status: ${result?.status}`);
|
||||
console.log(`ldap status: ${result?.status}`);
|
||||
|
||||
// replace with new dns
|
||||
this.allValidDNsCache = dns;
|
||||
|
||||
219
src/eap.ts
219
src/eap.ts
@@ -1,219 +0,0 @@
|
||||
// https://tools.ietf.org/html/rfc3748#section-4.1
|
||||
|
||||
// https://tools.ietf.org/html/rfc5281 TTLS v0
|
||||
// https://tools.ietf.org/html/draft-funk-eap-ttls-v1-00 TTLS v1 (not implemented)
|
||||
import { IResponseHandlers, ResponseHandler } from './types/Handler';
|
||||
import { EAPTTLS } from './eap/eap-ttls';
|
||||
import { MAX_RADIUS_ATTRIBUTE_SIZE } from './helpers';
|
||||
|
||||
export class EAPHandler {
|
||||
eapTTLS: EAPTTLS;
|
||||
|
||||
constructor() {
|
||||
this.eapTTLS = new EAPTTLS(this.sendEAPResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @param type 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK
|
||||
*/
|
||||
private async sendEAPResponse(
|
||||
response: ResponseHandler,
|
||||
identifier: number,
|
||||
data?: Buffer,
|
||||
msgType = 21,
|
||||
msgFlags = 0x00
|
||||
) {
|
||||
let i = 0;
|
||||
|
||||
const maxSize = (MAX_RADIUS_ATTRIBUTE_SIZE - 5) * 4;
|
||||
|
||||
let sentDataSize = 0;
|
||||
|
||||
let currentIdentifier = identifier;
|
||||
let currentResponse = response;
|
||||
|
||||
do {
|
||||
// SLICE
|
||||
|
||||
// const fragmentMaxPart =
|
||||
// data && (i + 1) * maxFragmentSize > data.length ? (i + 1) * maxFragmentSize : undefined;
|
||||
|
||||
const dataPart = data && data.length > 0 && data.slice(sentDataSize, sentDataSize + maxSize);
|
||||
|
||||
/* it's the first one and we have more, therefore include length */
|
||||
const includeLength = data && i === 0 && sentDataSize < data.length;
|
||||
|
||||
sentDataSize += maxSize;
|
||||
|
||||
i += 1;
|
||||
|
||||
/*
|
||||
0 1 2 3 4 5 6 7 8
|
||||
+-+-+-+-+-+-+-+-+
|
||||
|L M R R R R R R|
|
||||
+-+-+-+-+-+-+-+-+
|
||||
|
||||
L = Length included
|
||||
M = More fragments
|
||||
R = Reserved
|
||||
|
||||
The L bit (length included) is set to indicate the presence of the
|
||||
four-octet TLS Message Length field, and MUST be set for the first
|
||||
fragment of a fragmented TLS message or set of messages. The M
|
||||
bit (more fragments) is set on all but the last fragment.
|
||||
Implementations of this specification MUST set the reserved bits
|
||||
to zero, and MUST ignore them on reception.
|
||||
*/
|
||||
|
||||
const flags =
|
||||
msgFlags +
|
||||
(includeLength ? 0b10000000 : 0) + // set L bit
|
||||
(data && sentDataSize < data.length /* we have more */ ? 0b01000000 : 0); // set M bit
|
||||
|
||||
currentIdentifier++;
|
||||
|
||||
let buffer = Buffer.from([
|
||||
1, // request
|
||||
currentIdentifier,
|
||||
0, // length (1/2)
|
||||
0, // length (2/2)
|
||||
msgType, // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
|
||||
flags // flags: 000000 (L include lenghts, M .. more to come)
|
||||
]);
|
||||
|
||||
// append length
|
||||
if (includeLength && data) {
|
||||
const length = Buffer.alloc(4);
|
||||
length.writeInt32BE(data.byteLength, 0);
|
||||
|
||||
buffer = Buffer.concat([buffer, length]);
|
||||
}
|
||||
|
||||
const resBuffer = dataPart ? Buffer.concat([buffer, dataPart]) : buffer;
|
||||
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
|
||||
|
||||
console.log('<<<<<<<<<<<< EAP RESPONSE TO CLIENT', {
|
||||
code: 1,
|
||||
currentIdentifier,
|
||||
includeLength,
|
||||
length: (data && data.byteLength) || 0,
|
||||
msgType: msgType.toString(10),
|
||||
flags: `00000000${flags.toString(2)}`.substr(-8),
|
||||
data
|
||||
});
|
||||
|
||||
// uffer.from([1,identifier, 0, 0, 21, 0]);
|
||||
// buffer.writeUInt16BE(sslResponse.length, 2); // length
|
||||
// buffer.writeInt8(21, 4); // eap-ttls
|
||||
// buffer.writeInt8(0, 5); // flags
|
||||
|
||||
console.log('sending message with identifier', currentIdentifier);
|
||||
({ identifier: currentIdentifier, response: currentResponse } = await currentResponse(
|
||||
resBuffer
|
||||
));
|
||||
console.log('next message got identifier', currentIdentifier);
|
||||
} while (data && sentDataSize < data.length);
|
||||
|
||||
console.log('DONE', sentDataSize, data && data.length);
|
||||
}
|
||||
|
||||
handleEAPMessage(msg: Buffer, state: string, handlers: IResponseHandlers) {
|
||||
// const b = Buffer.from([2,0x242,0x0,0x18,0x1,0x115,0x105,0x109,0x111,0x110,0x46,0x116,0x114,0x101,0x116,0x116,0x101,0x114]);
|
||||
// const msg = Buffer.from([2, 162, 0, 18, 1, 115, 105, 109, 111, 110, 46, 116, 114, 101, 116, 116, 101, 114])
|
||||
|
||||
/*
|
||||
1 Request
|
||||
2 Response
|
||||
3 Success
|
||||
4 Failure
|
||||
*/
|
||||
|
||||
const code = msg.slice(0, 1).readUInt8(0);
|
||||
const identifier = msg.slice(1, 2).readUInt8(0); // .toString('hex');
|
||||
// const length = msg.slice(2, 4).readInt16BE(0); // .toString('binary');
|
||||
const type = msg.slice(4, 5).readUInt8(0); // .slice(3,0x5).toString('hex');
|
||||
|
||||
/*
|
||||
console.log("CODE", code);
|
||||
console.log('ID', identifier);
|
||||
console.log('length', length);
|
||||
*/
|
||||
|
||||
switch (code) {
|
||||
case 1: // for request
|
||||
case 2: // for response
|
||||
switch (type) {
|
||||
case 1: // identifiy
|
||||
/**
|
||||
* The EAP-TTLS packet format is shown below. The fields are
|
||||
transmitted left to right.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Code | Identifier | Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Type | Flags | Message Length
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
Message Length | Data...
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', {});
|
||||
|
||||
this.sendEAPResponse(handlers.response, identifier, undefined, 21, 0x20);
|
||||
|
||||
/*
|
||||
handlers.response(
|
||||
Buffer.from([
|
||||
1, // request
|
||||
identifier + 1,
|
||||
0, // length (1/2)
|
||||
6, // length (2/2)
|
||||
21, // EAP-TTLS
|
||||
0x20 // flags: 001000 start flag
|
||||
])
|
||||
); */
|
||||
break;
|
||||
case 21: // EAP TTLS
|
||||
this.eapTTLS.handleMessage(msg, state, handlers, identifier);
|
||||
break;
|
||||
case 3: // nak
|
||||
// this.sendEAPResponse(handlers.response, identifier, undefined, 3);
|
||||
break;
|
||||
case 2: // notification
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {});
|
||||
console.info('notification');
|
||||
break;
|
||||
case 4: // md5-challenge
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {});
|
||||
|
||||
console.info('md5-challenge');
|
||||
break;
|
||||
case 254: // expanded type
|
||||
console.error('not implemented type', type);
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// we do not support this auth type, ask for TTLS
|
||||
console.error('unsupported type', type, 'requesting TTLS (21)');
|
||||
|
||||
this.sendEAPResponse(handlers.response, identifier, Buffer.from([21]), 3);
|
||||
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
console.log('Client Auth Success');
|
||||
break;
|
||||
case 4:
|
||||
console.log('Client Auth FAILURE');
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
// silently ignor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import * as events from 'events';
|
||||
import * as tls from 'tls';
|
||||
import { encodeTunnelPW, openTLSSockets, startTLSServer } from '../tls/crypt';
|
||||
import { AdditionalAuthHandler, ResponseAuthHandler } from '../types/Handler';
|
||||
import { PAPChallenge } from './challenges/pap';
|
||||
import { IEAPType } from '../types/EAPType';
|
||||
|
||||
interface IEAPResponseHandlers {
|
||||
response: (respData?: Buffer, msgType?: number) => void;
|
||||
checkAuth: ResponseAuthHandler;
|
||||
}
|
||||
|
||||
export class EAPTTLS implements IEAPType {
|
||||
papChallenge: PAPChallenge;
|
||||
|
||||
constructor(private sendEAPResponse) {
|
||||
this.papChallenge = new PAPChallenge();
|
||||
}
|
||||
|
||||
decode(msg: Buffer) {
|
||||
const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex');
|
||||
|
||||
// if (flags)
|
||||
// @todo check if "L" flag is set in flags
|
||||
const decodedFlags = {
|
||||
lengthIncluded: flags & 0b010000000,
|
||||
moreFragments: flags & 0b001000000,
|
||||
start: flags & 0b000100000,
|
||||
reserved: flags & 0b000011000,
|
||||
version: flags & 0b010000111
|
||||
};
|
||||
let msglength;
|
||||
if (decodedFlags.lengthIncluded) {
|
||||
msglength = msg.slice(6, 10).readInt32BE(0); // .readDoubleLE(0); // .toString('hex');
|
||||
}
|
||||
const data = msg.slice(decodedFlags.lengthIncluded ? 10 : 6, msg.length);
|
||||
|
||||
return {
|
||||
decodedFlags,
|
||||
msglength,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
handleMessage(msg: Buffer, state: string, handlers, identifier: number) {
|
||||
const { decodedFlags, msglength, data } = this.decode(msg);
|
||||
|
||||
// check if no data package is there and we have something in the queue, if so.. empty the queue first
|
||||
if (!data || data.length === 0) {
|
||||
// @todo: queue processing
|
||||
console.warn(
|
||||
`>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS, ACK / NACK (no data, just a confirmation, ID: ${identifier})`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', {
|
||||
// flags: `00000000${flags.toString(2)}`.substr(-8),
|
||||
decodedFlags,
|
||||
identifier,
|
||||
/*
|
||||
0 1 2 3 4 5 6 7
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| L | M | S | R | R | V |
|
||||
+---+---+---+---+---+---+---+---+
|
||||
|
||||
L = Length included
|
||||
M = More fragments
|
||||
S = Start
|
||||
R = Reserved
|
||||
V = Version (000 for EAP-TTLSv0)
|
||||
*/
|
||||
|
||||
msglength,
|
||||
data,
|
||||
dataStr: data.toString()
|
||||
});
|
||||
|
||||
let currentConnection = openTLSSockets.get(state) as
|
||||
| { events: events.EventEmitter; tls: tls.TLSSocket; currentHandlers: IEAPResponseHandlers }
|
||||
| undefined;
|
||||
if (!currentConnection) {
|
||||
const connection = startTLSServer();
|
||||
currentConnection = {
|
||||
events: connection.events,
|
||||
tls: connection.tls,
|
||||
currentHandlers: handlers
|
||||
};
|
||||
openTLSSockets.set(state, currentConnection);
|
||||
|
||||
// register event listeners
|
||||
currentConnection.events.on('incoming', (incomingData: Buffer) => {
|
||||
const type = incomingData.slice(3, 4).readUInt8(0);
|
||||
// const code = data.slice(4, 5).readUInt8(0);
|
||||
|
||||
switch (type) {
|
||||
case 1: // PAP / CHAP
|
||||
try {
|
||||
const { username, password } = this.papChallenge.decode(incomingData);
|
||||
currentConnection!.currentHandlers.checkAuth(username, password);
|
||||
} catch (err) {
|
||||
// pwd not found..
|
||||
console.error('pwd not found', err);
|
||||
// NAK
|
||||
currentConnection!.currentHandlers.response(undefined, 3);
|
||||
|
||||
/*
|
||||
this.sendEAPResponse(
|
||||
currentConnection!.currentHandlers.response,
|
||||
identifier,
|
||||
undefined,
|
||||
3
|
||||
); */
|
||||
currentConnection!.events.emit('end');
|
||||
throw new Error(`pwd not found`);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('data', incomingData);
|
||||
console.log('data str', incomingData.toString());
|
||||
|
||||
// currentConnection!.events.emit('end');
|
||||
|
||||
console.log('UNSUPPORTED AUTH TYPE, requesting PAP');
|
||||
// throw new Error(`unsupported auth type${type}`);
|
||||
currentConnection!.currentHandlers.response(Buffer.from([1]), 3);
|
||||
|
||||
/*
|
||||
this.sendEAPResponse(
|
||||
currentConnection!.currentHandlers.response,
|
||||
identifier,
|
||||
Buffer.from([1]),
|
||||
3
|
||||
); */
|
||||
}
|
||||
});
|
||||
|
||||
currentConnection.events.on('response', (responseData: Buffer) => {
|
||||
// console.log('sending encrypted data back to client', responseData);
|
||||
|
||||
// send back...
|
||||
currentConnection!.currentHandlers.response(responseData);
|
||||
// this.sendEAPResponse(currentConnection!.currentHandlers.response, identifier, responseData);
|
||||
// this.sendMessage(TYPE.PRELOGIN, data, false);
|
||||
});
|
||||
|
||||
currentConnection.events.on('end', () => {
|
||||
// cleanup socket
|
||||
console.log('ENDING SOCKET');
|
||||
openTLSSockets.del(state);
|
||||
});
|
||||
} /* else {
|
||||
console.log('using existing socket');
|
||||
} */
|
||||
|
||||
// update handlers
|
||||
currentConnection.currentHandlers = {
|
||||
response: (respData?: Buffer, msgType?: number) =>
|
||||
this.sendEAPResponse(handlers.response, identifier, respData, msgType),
|
||||
checkAuth: (username: string, password: string) => {
|
||||
const additionalAuthHandler: AdditionalAuthHandler = (success, params) => {
|
||||
const buffer = Buffer.from([
|
||||
success ? 3 : 4, // 3.. success, 4... failure
|
||||
identifier,
|
||||
0, // length (1/2)
|
||||
4 // length (2/2)
|
||||
]);
|
||||
|
||||
params.attributes.push(['EAP-Message', buffer]);
|
||||
|
||||
if (params.packet.attributes && params.packet.attributes['User-Name']) {
|
||||
// reappend username to response
|
||||
params.attributes.push(['User-Name', params.packet.attributes['User-Name']]);
|
||||
}
|
||||
|
||||
/*
|
||||
if (sess->eap_if->eapKeyDataLen > 64) {
|
||||
len = 32;
|
||||
} else {
|
||||
len = sess->eap_if->eapKeyDataLen / 2;
|
||||
}
|
||||
*/
|
||||
const keyingMaterial = (currentConnection?.tls as any).exportKeyingMaterial(
|
||||
128,
|
||||
'ttls keying material'
|
||||
);
|
||||
|
||||
// console.log('keyingMaterial', keyingMaterial);
|
||||
|
||||
// eapKeyData + len
|
||||
params.attributes.push([
|
||||
'Vendor-Specific',
|
||||
311,
|
||||
[
|
||||
[
|
||||
16,
|
||||
encodeTunnelPW(
|
||||
keyingMaterial.slice(64),
|
||||
(params.packet as any).authenticator,
|
||||
// params.packet.attributes['Message-Authenticator'],
|
||||
params.secret
|
||||
)
|
||||
]
|
||||
]
|
||||
]); // MS-MPPE-Send-Key
|
||||
|
||||
// eapKeyData
|
||||
params.attributes.push([
|
||||
'Vendor-Specific',
|
||||
311,
|
||||
[
|
||||
[
|
||||
17,
|
||||
encodeTunnelPW(
|
||||
keyingMaterial.slice(0, 64),
|
||||
(params.packet as any).authenticator,
|
||||
// params.packet.attributes['Message-Authenticator'],
|
||||
params.secret
|
||||
)
|
||||
]
|
||||
]
|
||||
]); // MS-MPPE-Recv-Key
|
||||
};
|
||||
|
||||
return handlers.checkAuth(username, password, additionalAuthHandler);
|
||||
}
|
||||
};
|
||||
|
||||
// emit data to tls server
|
||||
currentConnection.events.emit('send', data);
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ export function makeid(length) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// by RFC Radius attributes have a max length
|
||||
// https://tools.ietf.org/html/rfc6929#section-1.2
|
||||
export const MAX_RADIUS_ATTRIBUTE_SIZE = 253;
|
||||
|
||||
export interface IDeferredPromise {
|
||||
|
||||
95
src/radius/RadiusService.ts
Normal file
95
src/radius/RadiusService.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import * as radius from 'radius';
|
||||
import { IAuthentication } from '../types/Authentication';
|
||||
import { EAPPacketHandler } from './handler/EAPPacketHandler';
|
||||
import { DefaultPacketHandler } from './handler/DefaultPacketHandler';
|
||||
import { IPacketHandler, IPacketHandlerResult, PacketResponseCode } from '../types/PacketHandler';
|
||||
|
||||
export class RadiusService {
|
||||
radiusPacketHandlers: IPacketHandler[] = [];
|
||||
|
||||
constructor(private secret: string, private authentication: IAuthentication) {
|
||||
this.radiusPacketHandlers.push(new EAPPacketHandler(authentication));
|
||||
this.radiusPacketHandlers.push(new DefaultPacketHandler(authentication));
|
||||
}
|
||||
|
||||
async handleMessage(
|
||||
msg: Buffer
|
||||
): Promise<{ data: Buffer; expectAcknowledgment?: boolean } | undefined> {
|
||||
const packet = radius.decode({ packet: msg, secret: this.secret });
|
||||
|
||||
if (packet.code !== 'Access-Request') {
|
||||
console.log('unknown packet type: ', packet.code);
|
||||
return undefined;
|
||||
}
|
||||
// console.log('packet.attributes', packet.attributes);
|
||||
|
||||
// console.log('rinfo', rinfo);
|
||||
/*
|
||||
const checkAuth = async (
|
||||
username: string,
|
||||
password: string,
|
||||
additionalAuthHandler?: AdditionalAuthHandler
|
||||
) => {
|
||||
console.log(`Access-Request for ${username}`);
|
||||
let success = false;
|
||||
try {
|
||||
await this.authentication.authenticate(username, password);
|
||||
success = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
const attributes: any[] = [];
|
||||
|
||||
if (additionalAuthHandler) {
|
||||
await additionalAuthHandler(success, { packet, attributes, secret: this.secret });
|
||||
}
|
||||
|
||||
const response = radius.encode_response({
|
||||
packet,
|
||||
code: success ? 'Access-Accept' : 'Access-Reject',
|
||||
secret: this.secret,
|
||||
attributes
|
||||
});
|
||||
console.log(`Sending ${success ? 'accept' : 'reject'} for user ${username}`);
|
||||
|
||||
this.server.sendToClient(response, rinfo.port, rinfo.address, function(err, _bytes) {
|
||||
if (err) {
|
||||
console.log('Error sending response to ', rinfo);
|
||||
}
|
||||
});
|
||||
}; */
|
||||
|
||||
let response: IPacketHandlerResult;
|
||||
|
||||
let i = 0;
|
||||
if (!this.radiusPacketHandlers[i]) {
|
||||
throw new Error('no packet handlers registered');
|
||||
}
|
||||
|
||||
// process packet handlers until we get a response
|
||||
do {
|
||||
/* response is of type IPacketHandlerResult */
|
||||
response = await this.radiusPacketHandlers[i].handlePacket(packet.attributes, packet);
|
||||
i++;
|
||||
} while (this.radiusPacketHandlers[i] && (!response || !response.code));
|
||||
|
||||
// still no response, we are done here
|
||||
if (!response || !response.code) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// all fine, return radius encoded response
|
||||
return {
|
||||
data: radius.encode_response({
|
||||
packet,
|
||||
code: response.code,
|
||||
secret: this.secret,
|
||||
attributes: response.attributes
|
||||
}),
|
||||
// if message is accept or reject, we conside this as final message
|
||||
// this means we do not expect a reponse from the client again (acknowledgement for package)
|
||||
expectAcknowledgment: response.code === PacketResponseCode.AccessChallenge
|
||||
};
|
||||
}
|
||||
}
|
||||
36
src/radius/handler/DefaultPacketHandler.ts
Normal file
36
src/radius/handler/DefaultPacketHandler.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IAuthentication } from '../../types/Authentication';
|
||||
import {
|
||||
IPacketHandler,
|
||||
IPacketHandlerResult,
|
||||
PacketResponseCode
|
||||
} from '../../types/PacketHandler';
|
||||
|
||||
export class DefaultPacketHandler implements IPacketHandler {
|
||||
constructor(private authentication: IAuthentication) {}
|
||||
|
||||
async handlePacket(attributes: { [key: string]: Buffer }): Promise<IPacketHandlerResult> {
|
||||
const username = attributes['User-Name'];
|
||||
const password = attributes['User-Password'];
|
||||
|
||||
if (!username || !password) {
|
||||
// params missing, this handler cannot continue...
|
||||
return {};
|
||||
}
|
||||
|
||||
const authenticated = await this.authentication.authenticate(
|
||||
username.toString(),
|
||||
password.toString()
|
||||
);
|
||||
if (authenticated) {
|
||||
// success
|
||||
return {
|
||||
code: PacketResponseCode.AccessAccept
|
||||
};
|
||||
}
|
||||
|
||||
// Failed
|
||||
return {
|
||||
code: PacketResponseCode.AccessReject
|
||||
};
|
||||
}
|
||||
}
|
||||
193
src/radius/handler/EAPPacketHandler.ts
Normal file
193
src/radius/handler/EAPPacketHandler.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
// https://tools.ietf.org/html/rfc3748#section-4.1
|
||||
|
||||
import * as NodeCache from 'node-cache';
|
||||
import { RadiusPacket } from 'radius';
|
||||
import { EAPTTLS } from './eapMethods/EAPTTLS';
|
||||
import { makeid } from '../../helpers';
|
||||
import {
|
||||
IPacketHandler,
|
||||
IPacketHandlerResult,
|
||||
PacketResponseCode
|
||||
} from '../../types/PacketHandler';
|
||||
import { IAuthentication } from '../../types/Authentication';
|
||||
import { IEAPMethod } from '../../types/EAPMethod';
|
||||
|
||||
export class EAPPacketHandler implements IPacketHandler {
|
||||
private eapMethods: IEAPMethod[] = [];
|
||||
|
||||
// private eapConnectionStates: { [key: string]: { validMethods: IEAPMethod[] } } = {};
|
||||
private eapConnectionStates = new NodeCache({ useClones: false, stdTTL: 3600 }); // max for one hour
|
||||
|
||||
constructor(authentication: IAuthentication) {
|
||||
this.eapMethods.push(new EAPTTLS(authentication));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param data
|
||||
* @param msgType 1 = identity, 21 = EAP-TTLS, 2 = notification, 4 = md5-challenge, 3 = NAK
|
||||
*/
|
||||
private async buildEAPResponse(
|
||||
identifier: number,
|
||||
msgType: number,
|
||||
data?: Buffer
|
||||
): Promise<IPacketHandlerResult> {
|
||||
/** build a package according to this:
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Code | Identifier | Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Type | Type-Data ...
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|
||||
*/
|
||||
const buffer = Buffer.from([
|
||||
1, // request
|
||||
identifier,
|
||||
0, // length (1/2)
|
||||
0, // length (2/2)
|
||||
msgType // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
|
||||
]);
|
||||
|
||||
const resBuffer = data ? Buffer.concat([buffer, data]) : buffer;
|
||||
// set EAP length header
|
||||
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
|
||||
|
||||
return {
|
||||
code: PacketResponseCode.AccessChallenge,
|
||||
attributes: [['EAP-Message', buffer]]
|
||||
};
|
||||
}
|
||||
|
||||
private decodeEAPHeader(msg: Buffer) {
|
||||
/**
|
||||
* parse msg according to this:
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Code | Identifier | Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Type | Type-Data ...
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-
|
||||
*/
|
||||
|
||||
/*
|
||||
code:
|
||||
1 Request
|
||||
2 Response
|
||||
3 Success
|
||||
4 Failure
|
||||
*/
|
||||
const code = msg.slice(0, 1).readUInt8(0);
|
||||
/* identifier is a number */
|
||||
const identifier = msg.slice(1, 2).readUInt8(0);
|
||||
const length = msg.slice(2, 4).readInt16BE(0);
|
||||
/* EAP type */
|
||||
const type = msg.slice(4, 5).readUInt8(0);
|
||||
const data = msg.slice(5);
|
||||
|
||||
return {
|
||||
code,
|
||||
identifier,
|
||||
length,
|
||||
type,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
async handlePacket(
|
||||
attributes: { [key: string]: Buffer },
|
||||
orgRadiusPacket: RadiusPacket
|
||||
): Promise<IPacketHandlerResult> {
|
||||
if (!attributes['EAP-Message']) {
|
||||
// not an EAP message
|
||||
return {};
|
||||
}
|
||||
|
||||
const stateID = (attributes.State && attributes.State.toString()) || makeid(16);
|
||||
|
||||
if (!this.eapConnectionStates.get(stateID)) {
|
||||
this.eapConnectionStates.set(stateID, {
|
||||
validMethods: this.eapMethods // on init all registered eap methods are valid, we kick them out in case we get a NAK response
|
||||
});
|
||||
}
|
||||
|
||||
// EAP MESSAGE
|
||||
const msg = attributes['EAP-Message'];
|
||||
|
||||
const { code, type, identifier, data } = this.decodeEAPHeader(msg);
|
||||
|
||||
const currentState = this.eapConnectionStates.get(stateID) as { validMethods: IEAPMethod[] };
|
||||
|
||||
switch (code) {
|
||||
case 1: // for request
|
||||
case 2: // for response
|
||||
switch (type) {
|
||||
case 1: // identifiy
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: IDENTIFY', {});
|
||||
// start identify
|
||||
if (currentState.validMethods.length > 0) {
|
||||
return currentState.validMethods[0].identify(identifier, stateID);
|
||||
}
|
||||
|
||||
return this.buildEAPResponse(identifier, 3);
|
||||
case 2: // notification
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: notification', {});
|
||||
console.info('notification');
|
||||
break;
|
||||
case 4: // md5-challenge
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: md5-challenge', {});
|
||||
|
||||
console.info('md5-challenge');
|
||||
break;
|
||||
case 254: // expanded type
|
||||
console.error('not implemented type', type);
|
||||
break;
|
||||
case 3: // nak
|
||||
if (data) {
|
||||
const supportedEAPMethods: number[] = [];
|
||||
for (const supportedMethod of data) {
|
||||
supportedEAPMethods.push(supportedMethod);
|
||||
}
|
||||
|
||||
this.eapConnectionStates.set(stateID, {
|
||||
...currentState,
|
||||
validMethods: currentState.validMethods.filter(method => {
|
||||
return supportedEAPMethods.includes(method.getEAPType()); // kick it out?
|
||||
})
|
||||
});
|
||||
}
|
||||
// continue with responding a NAK and add rest of supported methods
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default: {
|
||||
const eapMethod = currentState.validMethods.find(method => {
|
||||
return type === method.getEAPType();
|
||||
});
|
||||
|
||||
if (eapMethod) {
|
||||
return eapMethod.handleMessage(identifier, stateID, msg, orgRadiusPacket);
|
||||
}
|
||||
|
||||
// we do not support this auth type, ask for something we support
|
||||
const serverSupportedMethods = currentState.validMethods.map(
|
||||
method => method.getEAPType
|
||||
);
|
||||
|
||||
console.error('unsupported type', type, `requesting: ${serverSupportedMethods}`);
|
||||
|
||||
return this.buildEAPResponse(identifier, 3, Buffer.from(serverSupportedMethods));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
console.log('Client Auth Success');
|
||||
break;
|
||||
case 4:
|
||||
console.log('Client Auth FAILURE');
|
||||
break;
|
||||
default:
|
||||
}
|
||||
// silently ignore;
|
||||
return {};
|
||||
}
|
||||
}
|
||||
460
src/radius/handler/eapMethods/EAPTTLS.ts
Normal file
460
src/radius/handler/eapMethods/EAPTTLS.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import * as tls from 'tls';
|
||||
import * as NodeCache from 'node-cache';
|
||||
import { RadiusPacket } from 'radius';
|
||||
import { encodeTunnelPW, ITLSServer, startTLSServer } from '../../../tls/crypt';
|
||||
import { ResponseAuthHandler } from '../../../types/Handler';
|
||||
import { PAPChallenge } from './challenges/PAPChallenge';
|
||||
import { IPacketHandlerResult, PacketResponseCode } from '../../../types/PacketHandler';
|
||||
import { MAX_RADIUS_ATTRIBUTE_SIZE, newDeferredPromise } from '../../../helpers';
|
||||
import { IEAPMethod } from '../../../types/EAPMethod';
|
||||
import { IAuthentication } from '../../../types/Authentication';
|
||||
import { secret } from '../../../../config';
|
||||
|
||||
interface IEAPResponseHandlers {
|
||||
response: (respData?: Buffer, msgType?: number) => void;
|
||||
checkAuth: ResponseAuthHandler;
|
||||
}
|
||||
|
||||
/* const handlers = {
|
||||
response: (EAPMessage: Buffer) => {
|
||||
const attributes: any = [['State', Buffer.from(state)]];
|
||||
let sentDataSize = 0;
|
||||
do {
|
||||
if (EAPMessage.length > 0) {
|
||||
attributes.push([
|
||||
'EAP-Message',
|
||||
EAPMessage.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
|
||||
]);
|
||||
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
|
||||
}
|
||||
} while (sentDataSize < EAPMessage.length);
|
||||
|
||||
const response = radius.encode_response({
|
||||
packet,
|
||||
code: 'Access-Challenge',
|
||||
secret: this.secret,
|
||||
attributes
|
||||
});
|
||||
|
||||
waitForNextMsg[state] = newDeferredPromise();
|
||||
|
||||
server.sendToClient(
|
||||
response,
|
||||
rinfo.port,
|
||||
rinfo.address,
|
||||
function(err, _bytes) {
|
||||
if (err) {
|
||||
console.log('Error sending response to ', rinfo);
|
||||
}
|
||||
},
|
||||
state
|
||||
);
|
||||
|
||||
return waitForNextMsg[state].promise;
|
||||
},
|
||||
checkAuth
|
||||
};
|
||||
|
||||
|
||||
const attributes: any = [['State', Buffer.from(stateID)]];
|
||||
let sentDataSize = 0;
|
||||
do {
|
||||
if (EAPMessage.length > 0) {
|
||||
attributes.push([
|
||||
'EAP-Message',
|
||||
EAPMessage.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
|
||||
]);
|
||||
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
|
||||
}
|
||||
} while (sentDataSize < EAPMessage.length);
|
||||
|
||||
const response = radius.encode_response({
|
||||
packet,
|
||||
code: 'Access-Challenge',
|
||||
secret: this.secret,
|
||||
attributes
|
||||
});
|
||||
|
||||
waitForNextMsg[stateID] = newDeferredPromise();
|
||||
|
||||
server.sendToClient(
|
||||
response,
|
||||
rinfo.port,
|
||||
rinfo.address,
|
||||
function(err, _bytes) {
|
||||
if (err) {
|
||||
console.log('Error sending response to ', rinfo);
|
||||
}
|
||||
},
|
||||
stateID
|
||||
);
|
||||
|
||||
return waitForNextMsg[stateID].promise;
|
||||
*/
|
||||
/* if (waitForNextMsg[state]) {
|
||||
const identifier = attributes['EAP-Message'].slice(1, 2).readUInt8(0); // .toString('hex');
|
||||
waitForNextMsg[state].resolve({ response: handlers.response, identifier });
|
||||
} */
|
||||
|
||||
function tlsHasExportKeyingMaterial(
|
||||
tlsSocket: any
|
||||
): tlsSocket is {
|
||||
exportKeyingMaterial: (length: number, label: string, context?: Buffer) => Buffer;
|
||||
} {
|
||||
return typeof (tlsSocket as any).exportKeyingMaterial === 'function';
|
||||
}
|
||||
|
||||
export class EAPTTLS implements IEAPMethod {
|
||||
private papChallenge: PAPChallenge = new PAPChallenge();
|
||||
|
||||
// { [key: string]: Buffer } = {};
|
||||
private queueData = new NodeCache({ useClones: false, stdTTL: 60 }); // queue data maximum for 60 seconds
|
||||
|
||||
private openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour
|
||||
|
||||
getEAPType(): number {
|
||||
return 21;
|
||||
}
|
||||
|
||||
identify(identifier: number, stateID: string): IPacketHandlerResult {
|
||||
return this.buildEAPTTLSResponse(identifier, 21, 0x20, stateID);
|
||||
}
|
||||
|
||||
constructor(private authentication: IAuthentication) {}
|
||||
|
||||
private buildEAPTTLSResponse(
|
||||
identifier: number,
|
||||
msgType = 21,
|
||||
msgFlags = 0x00,
|
||||
stateID: string,
|
||||
data?: Buffer,
|
||||
newResponse = true
|
||||
): IPacketHandlerResult {
|
||||
const maxSize = (MAX_RADIUS_ATTRIBUTE_SIZE - 5) * 4;
|
||||
console.log('maxSize', maxSize);
|
||||
|
||||
/* it's the first one and we have more, therefore include length */
|
||||
const includeLength = data && newResponse && data.length > maxSize;
|
||||
|
||||
// extract data party
|
||||
const dataToSend = data && data.length > 0 && data.slice(0, maxSize);
|
||||
const dataToQueue = data && data.length > maxSize && data.slice(maxSize);
|
||||
|
||||
/*
|
||||
0 1 2 3 4 5 6 7 8
|
||||
+-+-+-+-+-+-+-+-+
|
||||
|L M R R R R R R|
|
||||
+-+-+-+-+-+-+-+-+
|
||||
|
||||
L = Length included
|
||||
M = More fragments
|
||||
R = Reserved
|
||||
|
||||
The L bit (length included) is set to indicate the presence of the
|
||||
four-octet TLS Message Length field, and MUST be set for the first
|
||||
fragment of a fragmented TLS message or set of messages. The M
|
||||
bit (more fragments) is set on all but the last fragment.
|
||||
Implementations of this specification MUST set the reserved bits
|
||||
to zero, and MUST ignore them on reception.
|
||||
*/
|
||||
|
||||
const flags =
|
||||
msgFlags +
|
||||
(includeLength ? 0b10000000 : 0) + // set L bit
|
||||
(dataToQueue && dataToQueue.length > 0 ? 0b01000000 : 0); // we have more data to come, set M bit
|
||||
|
||||
let buffer = Buffer.from([
|
||||
1, // request
|
||||
identifier + 1, // increase id by 1
|
||||
0, // length (1/2)
|
||||
0, // length (2/2)
|
||||
msgType, // 1 = identity, 21 = EAP-TTLS, 2 = notificaiton, 4 = md5-challenge, 3 = NAK
|
||||
flags // flags: 000000 (L include lenghts, M .. more to come)
|
||||
]);
|
||||
|
||||
// append length
|
||||
if (includeLength && data) {
|
||||
const length = Buffer.alloc(4);
|
||||
length.writeInt32BE(data.byteLength, 0);
|
||||
|
||||
buffer = Buffer.concat([buffer, length]);
|
||||
}
|
||||
|
||||
// build final buffer with data
|
||||
const resBuffer = dataToSend ? Buffer.concat([buffer, dataToSend]) : buffer;
|
||||
|
||||
// set EAP length header
|
||||
resBuffer.writeUInt16BE(resBuffer.byteLength, 2);
|
||||
|
||||
console.log('<<<<<<<<<<<< EAP RESPONSE TO CLIENT', {
|
||||
code: 1,
|
||||
identifier: identifier + 1,
|
||||
includeLength,
|
||||
dataLength: (data && data.byteLength) || 0,
|
||||
msgType: msgType.toString(10),
|
||||
flags: `00000000${flags.toString(2)}`.substr(-8),
|
||||
data
|
||||
});
|
||||
|
||||
if (dataToQueue) {
|
||||
// we couldn't send all at once, queue the rest and send later
|
||||
this.queueData.set(stateID, dataToQueue);
|
||||
} else {
|
||||
this.queueData.del(stateID);
|
||||
}
|
||||
|
||||
const attributes: any = [['State', Buffer.from(stateID)]];
|
||||
let sentDataSize = 0;
|
||||
do {
|
||||
if (resBuffer.length > 0) {
|
||||
attributes.push([
|
||||
'EAP-Message',
|
||||
resBuffer.slice(sentDataSize, sentDataSize + MAX_RADIUS_ATTRIBUTE_SIZE)
|
||||
]);
|
||||
sentDataSize += MAX_RADIUS_ATTRIBUTE_SIZE;
|
||||
}
|
||||
} while (sentDataSize < resBuffer.length);
|
||||
|
||||
return {
|
||||
code: PacketResponseCode.AccessChallenge,
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
decodeTTLSMessage(msg: Buffer) {
|
||||
/**
|
||||
* The EAP-TTLS packet format is shown below. The fields are
|
||||
transmitted left to right.
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Code | Identifier | Length |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Type | Flags | Message Length
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
Message Length | Data...
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
*/
|
||||
|
||||
const flags = msg.slice(5, 6).readUInt8(0); // .toString('hex');
|
||||
/*
|
||||
0 1 2 3 4 5 6 7
|
||||
+---+---+---+---+---+---+---+---+
|
||||
| L | M | S | R | R | V |
|
||||
+---+---+---+---+---+---+---+---+
|
||||
|
||||
L = Length included
|
||||
M = More fragments
|
||||
S = Start
|
||||
R = Reserved
|
||||
V = Version (000 for EAP-TTLSv0)
|
||||
*/
|
||||
const decodedFlags = {
|
||||
// L
|
||||
lengthIncluded: flags & 0b010000000,
|
||||
// M
|
||||
moreFragments: flags & 0b001000000,
|
||||
// S
|
||||
start: flags & 0b000100000,
|
||||
// R
|
||||
// reserved: flags & 0b000011000,
|
||||
// V
|
||||
version: flags & 0b010000111
|
||||
};
|
||||
|
||||
let msglength;
|
||||
if (decodedFlags.lengthIncluded) {
|
||||
msglength = msg.slice(6, 10).readInt32BE(0); // .readDoubleLE(0); // .toString('hex');
|
||||
}
|
||||
const data = msg.slice(decodedFlags.lengthIncluded ? 10 : 6, msg.length);
|
||||
|
||||
return {
|
||||
decodedFlags,
|
||||
msglength,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
authResponse(
|
||||
identifier: number,
|
||||
success: boolean,
|
||||
socket: tls.TLSSocket,
|
||||
packet: RadiusPacket
|
||||
): IPacketHandlerResult {
|
||||
const buffer = Buffer.from([
|
||||
success ? 3 : 4, // 3.. success, 4... failure
|
||||
identifier + 1,
|
||||
0, // length (1/2)
|
||||
4 // length (2/2)
|
||||
]);
|
||||
|
||||
const attributes: any[] = [];
|
||||
attributes.push(['EAP-Message', buffer]);
|
||||
|
||||
if (packet.attributes && packet.attributes['User-Name']) {
|
||||
// reappend username to response
|
||||
attributes.push(['User-Name', packet.attributes['User-Name']]);
|
||||
}
|
||||
|
||||
/*
|
||||
if (sess->eap_if->eapKeyDataLen > 64) {
|
||||
len = 32;
|
||||
} else {
|
||||
len = sess->eap_if->eapKeyDataLen / 2;
|
||||
}
|
||||
*/
|
||||
if (tlsHasExportKeyingMaterial(socket)) {
|
||||
const keyingMaterial = (socket as any).exportKeyingMaterial(128, 'ttls keying material');
|
||||
|
||||
// console.log('keyingMaterial', keyingMaterial);
|
||||
|
||||
// eapKeyData + len
|
||||
attributes.push([
|
||||
'Vendor-Specific',
|
||||
311,
|
||||
[
|
||||
[
|
||||
16,
|
||||
encodeTunnelPW(
|
||||
keyingMaterial.slice(64),
|
||||
(packet as any).authenticator,
|
||||
// params.packet.attributes['Message-Authenticator'],
|
||||
secret
|
||||
)
|
||||
]
|
||||
]
|
||||
]); // MS-MPPE-Send-Key
|
||||
|
||||
// eapKeyData
|
||||
attributes.push([
|
||||
'Vendor-Specific',
|
||||
311,
|
||||
[
|
||||
[
|
||||
17,
|
||||
encodeTunnelPW(
|
||||
keyingMaterial.slice(0, 64),
|
||||
(packet as any).authenticator,
|
||||
// params.packet.attributes['Message-Authenticator'],
|
||||
secret
|
||||
)
|
||||
]
|
||||
]
|
||||
]); // MS-MPPE-Recv-Key
|
||||
} else {
|
||||
console.error(
|
||||
'FATAL: no exportKeyingMaterial method available!!!, you need latest NODE JS, see https://github.com/nodejs/node/pull/31814'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
code: success ? PacketResponseCode.AccessAccept : PacketResponseCode.AccessReject,
|
||||
attributes
|
||||
};
|
||||
}
|
||||
|
||||
async handleMessage(
|
||||
identifier: number,
|
||||
stateID: string,
|
||||
msg: Buffer,
|
||||
orgRadiusPacket: RadiusPacket
|
||||
): Promise<IPacketHandlerResult> {
|
||||
const { decodedFlags, msglength, data } = this.decodeTTLSMessage(msg);
|
||||
|
||||
// check if no data package is there and we have something in the queue, if so.. empty the queue first
|
||||
if (!data || data.length === 0) {
|
||||
console.warn(
|
||||
`>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS, ACK / NACK (no data, just a confirmation, ID: ${identifier})`
|
||||
);
|
||||
const queuedData = this.queueData.get(stateID);
|
||||
if (queuedData instanceof Buffer && queuedData.length > 0) {
|
||||
return this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, queuedData, false);
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
console.log('>>>>>>>>>>>> REQUEST FROM CLIENT: EAP TTLS', {
|
||||
// flags: `00000000${flags.toString(2)}`.substr(-8),
|
||||
decodedFlags,
|
||||
identifier,
|
||||
msglength
|
||||
// data,
|
||||
// dataStr: data.toString()
|
||||
});
|
||||
|
||||
let connection = this.openTLSSockets.get(stateID) as ITLSServer;
|
||||
|
||||
if (!connection) {
|
||||
connection = startTLSServer();
|
||||
this.openTLSSockets.set(stateID, connection);
|
||||
|
||||
connection.events.on('end', () => {
|
||||
// cleanup socket
|
||||
console.log('ENDING SOCKET');
|
||||
this.openTLSSockets.del(stateID);
|
||||
});
|
||||
}
|
||||
|
||||
const sendResponsePromise = newDeferredPromise();
|
||||
|
||||
const incomingMessageHandler = async (incomingData: Buffer) => {
|
||||
const type = incomingData.slice(3, 4).readUInt8(0);
|
||||
// const code = data.slice(4, 5).readUInt8(0);
|
||||
|
||||
switch (type) {
|
||||
case 1: // PAP / CHAP
|
||||
try {
|
||||
const { username, password } = this.papChallenge.decode(incomingData);
|
||||
const authResult = await this.authentication.authenticate(username, password);
|
||||
|
||||
sendResponsePromise.resolve(
|
||||
this.authResponse(identifier, authResult, connection.tls, orgRadiusPacket)
|
||||
);
|
||||
} catch (err) {
|
||||
// pwd not found..
|
||||
console.error('pwd not found', err);
|
||||
connection.events.emit('end');
|
||||
// NAK
|
||||
sendResponsePromise.resolve(this.buildEAPTTLSResponse(identifier, 3, 0, stateID));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log('data', incomingData);
|
||||
console.log('data str', incomingData.toString());
|
||||
|
||||
// currentConnection!.events.emit('end');
|
||||
|
||||
console.log('UNSUPPORTED AUTH TYPE, requesting PAP');
|
||||
// throw new Error(`unsupported auth type${type}`);
|
||||
sendResponsePromise.resolve(
|
||||
this.buildEAPTTLSResponse(identifier, 3, 0, stateID, Buffer.from([1]))
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const responseHandler = (encryptedResponseData: Buffer) => {
|
||||
// send back...
|
||||
sendResponsePromise.resolve(
|
||||
this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData)
|
||||
);
|
||||
};
|
||||
|
||||
// register event listeners
|
||||
connection.events.on('incoming', incomingMessageHandler);
|
||||
connection.events.on('response', responseHandler);
|
||||
|
||||
// emit data to tls server
|
||||
connection.events.emit('send', data);
|
||||
const responseData = await sendResponsePromise.promise;
|
||||
|
||||
// cleanup
|
||||
connection.events.off('incoming', incomingMessageHandler);
|
||||
connection.events.off('response', responseHandler);
|
||||
|
||||
// send response
|
||||
return responseData; // this.buildEAPTTLSResponse(identifier, 21, 0x00, stateID, encryptedResponseData);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IEAPChallenge } from '../../types/EAPChallenge';
|
||||
import { IEAPChallenge } from '../../../../types/EAPChallenge';
|
||||
|
||||
export class PAPChallenge implements IEAPChallenge {
|
||||
// i couldn't find any documentation about it, therefore best guess how this is processed...
|
||||
80
src/server/UDPServer.ts
Normal file
80
src/server/UDPServer.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import * as dgram from 'dgram';
|
||||
import { SocketType } from 'dgram';
|
||||
import * as events from 'events';
|
||||
import { EventEmitter } from 'events';
|
||||
import { newDeferredPromise } from '../helpers';
|
||||
import { IServer } from '../types/Server';
|
||||
|
||||
export class UDPServer extends events.EventEmitter implements IServer {
|
||||
static MAX_RETRIES = 3;
|
||||
|
||||
private timeout: { [key: string]: NodeJS.Timeout } = {};
|
||||
|
||||
private server: dgram.Socket;
|
||||
|
||||
constructor(private port: number, type: SocketType = 'udp4') {
|
||||
super();
|
||||
this.server = dgram.createSocket(type);
|
||||
}
|
||||
|
||||
sendToClient(
|
||||
msg: string | Uint8Array,
|
||||
port?: number,
|
||||
address?: string,
|
||||
callback?: (error: Error | null, bytes: number) => void,
|
||||
expectAcknowledgment = true
|
||||
): void {
|
||||
let retried = 0;
|
||||
|
||||
const sendResponse = (): void => {
|
||||
if (retried > 0) {
|
||||
console.warn(
|
||||
`no confirmation of last message from ${address}:${port}, re-sending response... (bytes: ${msg.length}, try: ${retried}/${UDPServer.MAX_RETRIES})`
|
||||
);
|
||||
}
|
||||
|
||||
// send message to client
|
||||
this.server.send(msg, 0, msg.length, port, address, callback);
|
||||
|
||||
// retry up to MAX_RETRIES to send this message,
|
||||
// we automatically retry if there is no confirmation (=any incoming message from client)
|
||||
// if expectAcknowledgment (e.g. Access-Accept or Access-Reject) is set, we do not retry
|
||||
const identifierForRetry = `${address}:${port}`;
|
||||
if (expectAcknowledgment && retried < UDPServer.MAX_RETRIES) {
|
||||
this.timeout[identifierForRetry] = setTimeout(sendResponse, 600 * (retried + 1));
|
||||
}
|
||||
retried += 1;
|
||||
};
|
||||
|
||||
sendResponse();
|
||||
}
|
||||
|
||||
async start(): Promise<EventEmitter> {
|
||||
const startServer = newDeferredPromise();
|
||||
this.server.on('listening', () => {
|
||||
const address = this.server.address();
|
||||
console.log(`radius server listening ${address.address}:${address.port}`);
|
||||
|
||||
this.setupListeners();
|
||||
startServer.resolve();
|
||||
});
|
||||
|
||||
this.server.on('message', (_msg, rinfo) => {
|
||||
console.log('incoming message 2');
|
||||
|
||||
// message retrieved, reset timeout handler
|
||||
const identifierForRetry = `${rinfo.address}:${rinfo.port}`;
|
||||
if (this.timeout[identifierForRetry]) {
|
||||
clearTimeout(this.timeout[identifierForRetry]);
|
||||
}
|
||||
});
|
||||
|
||||
this.server.bind(this.port);
|
||||
|
||||
return startServer.promise;
|
||||
}
|
||||
|
||||
private setupListeners() {
|
||||
this.server.on('message', (message, rinfo) => this.emit('message', message, rinfo));
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
import * as NodeCache from 'node-cache';
|
||||
import * as events from 'events';
|
||||
import * as tls from 'tls';
|
||||
import { createSecureContext } from 'tls';
|
||||
import * as fs from 'fs';
|
||||
import * as crypto from 'crypto';
|
||||
import * as DuplexPair from 'native-duplexpair';
|
||||
import * as constants from 'constants';
|
||||
@@ -21,16 +19,20 @@ const tlsOptions: tls.SecureContextOptions = {
|
||||
};
|
||||
console.log('tlsOptions', tlsOptions);
|
||||
const secureContext = createSecureContext(tlsOptions);
|
||||
export const openTLSSockets = new NodeCache({ useClones: false, stdTTL: 3600 }); // keep sockets for about one hour
|
||||
|
||||
export function startTLSServer(): { events: events.EventEmitter; tls: tls.TLSSocket } {
|
||||
export interface ITLSServer {
|
||||
events: events.EventEmitter;
|
||||
tls: tls.TLSSocket;
|
||||
}
|
||||
|
||||
export function startTLSServer(): ITLSServer {
|
||||
const duplexpair = new DuplexPair();
|
||||
const emitter = new events.EventEmitter();
|
||||
|
||||
const cleartext = new tls.TLSSocket(duplexpair.socket1, {
|
||||
secureContext,
|
||||
isServer: true,
|
||||
enableTrace: true,
|
||||
// enableTrace: true,
|
||||
rejectUnauthorized: false,
|
||||
// handshakeTimeout: 10,
|
||||
requestCert: false
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export interface IAuthentication {
|
||||
authenticate(username: string, password: string): Promise<string>;
|
||||
authenticate(username: string, password: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
15
src/types/EAPMethod.ts
Normal file
15
src/types/EAPMethod.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { RadiusPacket } from 'radius';
|
||||
import { IPacketHandlerResult } from './PacketHandler';
|
||||
|
||||
export interface IEAPMethod {
|
||||
getEAPType(): number;
|
||||
|
||||
identify(identifier: number, stateID: string): IPacketHandlerResult;
|
||||
|
||||
handleMessage(
|
||||
identifier: number,
|
||||
stateID: string,
|
||||
msg: Buffer,
|
||||
orgRadiusPacket?: RadiusPacket
|
||||
): Promise<IPacketHandlerResult>;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export interface IEAPType {
|
||||
handleMessage(msg: Buffer, state: string, handlers, identifier: number);
|
||||
}
|
||||
19
src/types/PacketHandler.ts
Normal file
19
src/types/PacketHandler.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { RadiusPacket } from 'radius';
|
||||
|
||||
export enum PacketResponseCode {
|
||||
AccessChallenge = 'Access-Challenge',
|
||||
AccessAccept = 'Access-Accept',
|
||||
AccessReject = 'Access-Reject'
|
||||
}
|
||||
|
||||
export interface IPacketHandlerResult {
|
||||
code?: PacketResponseCode;
|
||||
attributes?: [string, Buffer][];
|
||||
}
|
||||
|
||||
export interface IPacketHandler {
|
||||
handlePacket(
|
||||
attributes: { [key: string]: Buffer },
|
||||
orgRadiusPacket: RadiusPacket
|
||||
): Promise<IPacketHandlerResult>;
|
||||
}
|
||||
32
src/types/Server.ts
Normal file
32
src/types/Server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { RemoteInfo } from 'dgram';
|
||||
|
||||
/**
|
||||
* @fires IServer#message
|
||||
*/
|
||||
export interface IServer {
|
||||
/**
|
||||
*
|
||||
* @param msg
|
||||
* @param port
|
||||
* @param address
|
||||
* @param callback
|
||||
* @param expectAcknowledgment: if set to false, message is not retried to send again if there is no confirmation
|
||||
*/
|
||||
sendToClient(
|
||||
msg: string | Uint8Array,
|
||||
port?: number,
|
||||
address?: string,
|
||||
callback?: (error: Error | null, bytes: number) => void,
|
||||
expectAcknowledgment?: boolean
|
||||
): void;
|
||||
|
||||
/**
|
||||
* Message event.
|
||||
*
|
||||
* @event IServer#message
|
||||
* @type {object}
|
||||
* @property {message} data - the data of the incoming message
|
||||
* @property {rinfo} optionally remote information
|
||||
*/
|
||||
on(event: 'message', listener: (msg: Buffer, rinfo?: RemoteInfo) => void): this;
|
||||
}
|
||||
Reference in New Issue
Block a user