650 lines
22 KiB
TypeScript
650 lines
22 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import type {
|
|
IRadiusServerOptions,
|
|
IRadiusServerStats,
|
|
IRadiusPacket,
|
|
IAuthenticationRequest,
|
|
IAuthenticationResponse,
|
|
IAccountingRequest,
|
|
IAccountingResponse,
|
|
TAuthenticationHandler,
|
|
TAccountingHandler,
|
|
} from './interfaces.js';
|
|
import {
|
|
ERadiusCode,
|
|
ERadiusAttributeType,
|
|
EAcctStatusType,
|
|
ENasPortType,
|
|
EServiceType,
|
|
} from './interfaces.js';
|
|
import { RadiusPacket } from './classes.radiuspacket.js';
|
|
import { RadiusAttributes } from './classes.radiusattributes.js';
|
|
import { RadiusAuthenticator } from './classes.radiusauthenticator.js';
|
|
import { RadiusSecrets } from './classes.radiussecrets.js';
|
|
|
|
/**
|
|
* RADIUS Server implementation
|
|
* Implements RFC 2865 (Authentication) and RFC 2866 (Accounting)
|
|
*/
|
|
export class RadiusServer {
|
|
private authSocket?: plugins.dgram.Socket;
|
|
private acctSocket?: plugins.dgram.Socket;
|
|
private readonly secrets: RadiusSecrets;
|
|
private authenticationHandler?: TAuthenticationHandler;
|
|
private accountingHandler?: TAccountingHandler;
|
|
|
|
private readonly options: Required<
|
|
Pick<IRadiusServerOptions, 'authPort' | 'acctPort' | 'bindAddress' | 'duplicateDetectionWindow' | 'maxPacketSize'>
|
|
>;
|
|
|
|
private readonly stats: IRadiusServerStats = {
|
|
authRequests: 0,
|
|
authAccepts: 0,
|
|
authRejects: 0,
|
|
authChallenges: 0,
|
|
authInvalidPackets: 0,
|
|
authUnknownClients: 0,
|
|
acctRequests: 0,
|
|
acctResponses: 0,
|
|
acctInvalidPackets: 0,
|
|
acctUnknownClients: 0,
|
|
};
|
|
|
|
// Duplicate detection cache: key = clientIp:clientPort:identifier
|
|
private readonly recentRequests: Map<string, { timestamp: number; response?: Buffer }> = new Map();
|
|
private duplicateCleanupInterval?: ReturnType<typeof setInterval>;
|
|
|
|
constructor(options: IRadiusServerOptions = {}) {
|
|
this.options = {
|
|
authPort: options.authPort ?? 1812,
|
|
acctPort: options.acctPort ?? 1813,
|
|
bindAddress: options.bindAddress ?? '0.0.0.0',
|
|
duplicateDetectionWindow: options.duplicateDetectionWindow ?? 10000, // 10 seconds
|
|
maxPacketSize: options.maxPacketSize ?? RadiusPacket.MAX_PACKET_SIZE,
|
|
};
|
|
|
|
this.secrets = new RadiusSecrets({
|
|
defaultSecret: options.defaultSecret,
|
|
resolver: options.secretResolver,
|
|
});
|
|
|
|
if (options.authenticationHandler) {
|
|
this.authenticationHandler = options.authenticationHandler;
|
|
}
|
|
if (options.accountingHandler) {
|
|
this.accountingHandler = options.accountingHandler;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the RADIUS server
|
|
*/
|
|
public async start(): Promise<void> {
|
|
await Promise.all([
|
|
this.startAuthServer(),
|
|
this.startAcctServer(),
|
|
]);
|
|
|
|
// Start duplicate detection cleanup
|
|
this.duplicateCleanupInterval = setInterval(() => {
|
|
this.cleanupDuplicateCache();
|
|
}, this.options.duplicateDetectionWindow);
|
|
}
|
|
|
|
/**
|
|
* Stop the RADIUS server
|
|
*/
|
|
public async stop(): Promise<void> {
|
|
if (this.duplicateCleanupInterval) {
|
|
clearInterval(this.duplicateCleanupInterval);
|
|
this.duplicateCleanupInterval = undefined;
|
|
}
|
|
|
|
const stopPromises: Promise<void>[] = [];
|
|
|
|
if (this.authSocket) {
|
|
stopPromises.push(new Promise<void>((resolve) => {
|
|
this.authSocket!.close(() => {
|
|
this.authSocket = undefined;
|
|
resolve();
|
|
});
|
|
}));
|
|
}
|
|
|
|
if (this.acctSocket) {
|
|
stopPromises.push(new Promise<void>((resolve) => {
|
|
this.acctSocket!.close(() => {
|
|
this.acctSocket = undefined;
|
|
resolve();
|
|
});
|
|
}));
|
|
}
|
|
|
|
await Promise.all(stopPromises);
|
|
}
|
|
|
|
/**
|
|
* Set the authentication handler
|
|
*/
|
|
public setAuthenticationHandler(handler: TAuthenticationHandler): void {
|
|
this.authenticationHandler = handler;
|
|
}
|
|
|
|
/**
|
|
* Set the accounting handler
|
|
*/
|
|
public setAccountingHandler(handler: TAccountingHandler): void {
|
|
this.accountingHandler = handler;
|
|
}
|
|
|
|
/**
|
|
* Set secret for a client
|
|
*/
|
|
public setClientSecret(clientIp: string, secret: string): void {
|
|
this.secrets.setClientSecret(clientIp, secret);
|
|
}
|
|
|
|
/**
|
|
* Get server statistics
|
|
*/
|
|
public getStats(): IRadiusServerStats {
|
|
return { ...this.stats };
|
|
}
|
|
|
|
/**
|
|
* Reset server statistics
|
|
*/
|
|
public resetStats(): void {
|
|
this.stats.authRequests = 0;
|
|
this.stats.authAccepts = 0;
|
|
this.stats.authRejects = 0;
|
|
this.stats.authChallenges = 0;
|
|
this.stats.authInvalidPackets = 0;
|
|
this.stats.authUnknownClients = 0;
|
|
this.stats.acctRequests = 0;
|
|
this.stats.acctResponses = 0;
|
|
this.stats.acctInvalidPackets = 0;
|
|
this.stats.acctUnknownClients = 0;
|
|
}
|
|
|
|
private async startAuthServer(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
this.authSocket = plugins.dgram.createSocket('udp4');
|
|
|
|
this.authSocket.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
|
|
this.authSocket.on('message', (msg, rinfo) => {
|
|
this.handleAuthMessage(msg, rinfo).catch((err) => {
|
|
console.error('Error handling auth message:', err);
|
|
});
|
|
});
|
|
|
|
this.authSocket.bind(this.options.authPort, this.options.bindAddress, () => {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
private async startAcctServer(): Promise<void> {
|
|
return new Promise((resolve, reject) => {
|
|
this.acctSocket = plugins.dgram.createSocket('udp4');
|
|
|
|
this.acctSocket.on('error', (err) => {
|
|
reject(err);
|
|
});
|
|
|
|
this.acctSocket.on('message', (msg, rinfo) => {
|
|
this.handleAcctMessage(msg, rinfo).catch((err) => {
|
|
console.error('Error handling acct message:', err);
|
|
});
|
|
});
|
|
|
|
this.acctSocket.bind(this.options.acctPort, this.options.bindAddress, () => {
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
|
|
private async handleAuthMessage(msg: Buffer, rinfo: plugins.dgram.RemoteInfo): Promise<void> {
|
|
// Validate packet size
|
|
if (msg.length > this.options.maxPacketSize) {
|
|
this.stats.authInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
// Get client secret
|
|
const secret = this.secrets.getSecret(rinfo.address);
|
|
if (!secret) {
|
|
this.stats.authUnknownClients++;
|
|
return;
|
|
}
|
|
|
|
// Parse packet
|
|
let packet: IRadiusPacket;
|
|
try {
|
|
packet = RadiusPacket.decode(msg);
|
|
} catch (err) {
|
|
this.stats.authInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
// Only handle Access-Request
|
|
if (packet.code !== ERadiusCode.AccessRequest) {
|
|
this.stats.authInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
this.stats.authRequests++;
|
|
|
|
// Check for duplicate
|
|
const duplicateKey = `${rinfo.address}:${rinfo.port}:${packet.identifier}`;
|
|
const cached = this.recentRequests.get(duplicateKey);
|
|
if (cached && cached.response) {
|
|
// Retransmit cached response
|
|
this.authSocket!.send(cached.response, rinfo.port, rinfo.address);
|
|
return;
|
|
}
|
|
|
|
// Mark as in-progress (no response yet)
|
|
this.recentRequests.set(duplicateKey, { timestamp: Date.now() });
|
|
|
|
// Build authentication request context
|
|
const authRequest = this.buildAuthRequest(packet, secret, rinfo);
|
|
|
|
// Call authentication handler
|
|
let response: IAuthenticationResponse;
|
|
if (this.authenticationHandler) {
|
|
try {
|
|
response = await this.authenticationHandler(authRequest);
|
|
} catch (err) {
|
|
response = {
|
|
code: ERadiusCode.AccessReject,
|
|
replyMessage: 'Internal server error',
|
|
};
|
|
}
|
|
} else {
|
|
// No handler configured - reject all
|
|
response = {
|
|
code: ERadiusCode.AccessReject,
|
|
replyMessage: 'Authentication service not configured',
|
|
};
|
|
}
|
|
|
|
// Build response packet
|
|
const responsePacket = this.buildAuthResponse(
|
|
response,
|
|
packet.identifier,
|
|
packet.authenticator,
|
|
secret
|
|
);
|
|
|
|
// Update stats
|
|
switch (response.code) {
|
|
case ERadiusCode.AccessAccept:
|
|
this.stats.authAccepts++;
|
|
break;
|
|
case ERadiusCode.AccessReject:
|
|
this.stats.authRejects++;
|
|
break;
|
|
case ERadiusCode.AccessChallenge:
|
|
this.stats.authChallenges++;
|
|
break;
|
|
}
|
|
|
|
// Cache response for duplicate detection
|
|
this.recentRequests.set(duplicateKey, {
|
|
timestamp: Date.now(),
|
|
response: responsePacket,
|
|
});
|
|
|
|
// Send response
|
|
this.authSocket!.send(responsePacket, rinfo.port, rinfo.address);
|
|
}
|
|
|
|
private async handleAcctMessage(msg: Buffer, rinfo: plugins.dgram.RemoteInfo): Promise<void> {
|
|
// Validate packet size
|
|
if (msg.length > this.options.maxPacketSize) {
|
|
this.stats.acctInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
// Get client secret
|
|
const secret = this.secrets.getSecret(rinfo.address);
|
|
if (!secret) {
|
|
this.stats.acctUnknownClients++;
|
|
return;
|
|
}
|
|
|
|
// Verify accounting request authenticator
|
|
if (!RadiusAuthenticator.verifyAccountingRequestAuthenticator(msg, secret)) {
|
|
this.stats.acctInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
// Parse packet
|
|
let packet: IRadiusPacket;
|
|
try {
|
|
packet = RadiusPacket.decode(msg);
|
|
} catch (err) {
|
|
this.stats.acctInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
// Only handle Accounting-Request
|
|
if (packet.code !== ERadiusCode.AccountingRequest) {
|
|
this.stats.acctInvalidPackets++;
|
|
return;
|
|
}
|
|
|
|
this.stats.acctRequests++;
|
|
|
|
// Check for duplicate
|
|
const duplicateKey = `acct:${rinfo.address}:${rinfo.port}:${packet.identifier}`;
|
|
const cached = this.recentRequests.get(duplicateKey);
|
|
if (cached && cached.response) {
|
|
// Retransmit cached response
|
|
this.acctSocket!.send(cached.response, rinfo.port, rinfo.address);
|
|
return;
|
|
}
|
|
|
|
// Mark as in-progress
|
|
this.recentRequests.set(duplicateKey, { timestamp: Date.now() });
|
|
|
|
// Build accounting request context
|
|
const acctRequest = this.buildAcctRequest(packet, rinfo);
|
|
|
|
// Call accounting handler
|
|
let response: IAccountingResponse;
|
|
if (this.accountingHandler) {
|
|
try {
|
|
response = await this.accountingHandler(acctRequest);
|
|
} catch (err) {
|
|
// Don't respond if we can't record the accounting data
|
|
return;
|
|
}
|
|
} else {
|
|
// No handler configured - accept all
|
|
response = { success: true };
|
|
}
|
|
|
|
// Only respond if accounting was successful
|
|
if (!response.success) {
|
|
return;
|
|
}
|
|
|
|
// Build response packet
|
|
const responsePacket = RadiusPacket.createAccountingResponse(
|
|
packet.identifier,
|
|
packet.authenticator,
|
|
secret,
|
|
response.attributes || []
|
|
);
|
|
|
|
this.stats.acctResponses++;
|
|
|
|
// Cache response for duplicate detection
|
|
this.recentRequests.set(duplicateKey, {
|
|
timestamp: Date.now(),
|
|
response: responsePacket,
|
|
});
|
|
|
|
// Send response
|
|
this.acctSocket!.send(responsePacket, rinfo.port, rinfo.address);
|
|
}
|
|
|
|
private buildAuthRequest(
|
|
packet: IRadiusPacket,
|
|
secret: string,
|
|
rinfo: plugins.dgram.RemoteInfo
|
|
): IAuthenticationRequest {
|
|
const getUsernameAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserName);
|
|
const username = getUsernameAttr ? getUsernameAttr.toString('utf8') : '';
|
|
|
|
// Decrypt PAP password if present
|
|
const passwordAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserPassword);
|
|
let password: string | undefined;
|
|
if (passwordAttr) {
|
|
try {
|
|
password = RadiusAuthenticator.decryptPassword(passwordAttr, packet.authenticator, secret);
|
|
} catch {
|
|
password = undefined;
|
|
}
|
|
}
|
|
|
|
// Get CHAP attributes
|
|
const chapPassword = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ChapPassword);
|
|
let chapChallenge = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ChapChallenge);
|
|
// If no CHAP-Challenge attribute, use Request Authenticator as challenge
|
|
if (chapPassword && !chapChallenge) {
|
|
chapChallenge = packet.authenticator;
|
|
}
|
|
|
|
// Get NAS attributes
|
|
const nasIpAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIpAddress);
|
|
const nasIpAddress = nasIpAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, nasIpAttr) as string : undefined;
|
|
|
|
const nasIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIdentifier);
|
|
const nasIdentifier = nasIdAttr ? nasIdAttr.toString('utf8') : undefined;
|
|
|
|
const nasPortAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPort);
|
|
const nasPort = nasPortAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, nasPortAttr) as number : undefined;
|
|
|
|
const nasPortTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPortType);
|
|
const nasPortType = nasPortTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPortType, nasPortTypeAttr) as ENasPortType : undefined;
|
|
|
|
// Get other common attributes
|
|
const calledStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CalledStationId);
|
|
const calledStationId = calledStationIdAttr ? calledStationIdAttr.toString('utf8') : undefined;
|
|
|
|
const callingStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CallingStationId);
|
|
const callingStationId = callingStationIdAttr ? callingStationIdAttr.toString('utf8') : undefined;
|
|
|
|
const serviceTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.ServiceType);
|
|
const serviceType = serviceTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.ServiceType, serviceTypeAttr) as EServiceType : undefined;
|
|
|
|
const framedProtocolAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.FramedProtocol);
|
|
const framedProtocol = framedProtocolAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.FramedProtocol, framedProtocolAttr) as number : undefined;
|
|
|
|
const stateAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.State);
|
|
|
|
return {
|
|
username,
|
|
password,
|
|
chapPassword,
|
|
chapChallenge,
|
|
nasIpAddress,
|
|
nasIdentifier,
|
|
nasPort,
|
|
nasPortType,
|
|
calledStationId,
|
|
callingStationId,
|
|
serviceType,
|
|
framedProtocol,
|
|
state: stateAttr,
|
|
rawPacket: packet,
|
|
clientAddress: rinfo.address,
|
|
clientPort: rinfo.port,
|
|
};
|
|
}
|
|
|
|
private buildAcctRequest(
|
|
packet: IRadiusPacket,
|
|
rinfo: plugins.dgram.RemoteInfo
|
|
): IAccountingRequest {
|
|
const statusTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctStatusType);
|
|
const statusType = statusTypeAttr
|
|
? RadiusAttributes.parseValue(ERadiusAttributeType.AcctStatusType, statusTypeAttr) as EAcctStatusType
|
|
: EAcctStatusType.Start;
|
|
|
|
const sessionIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctSessionId);
|
|
const sessionId = sessionIdAttr ? sessionIdAttr.toString('utf8') : '';
|
|
|
|
const usernameAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.UserName);
|
|
const username = usernameAttr ? usernameAttr.toString('utf8') : undefined;
|
|
|
|
const nasIpAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIpAddress);
|
|
const nasIpAddress = nasIpAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, nasIpAttr) as string : undefined;
|
|
|
|
const nasIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasIdentifier);
|
|
const nasIdentifier = nasIdAttr ? nasIdAttr.toString('utf8') : undefined;
|
|
|
|
const nasPortAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPort);
|
|
const nasPort = nasPortAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, nasPortAttr) as number : undefined;
|
|
|
|
const nasPortTypeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.NasPortType);
|
|
const nasPortType = nasPortTypeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.NasPortType, nasPortTypeAttr) as ENasPortType : undefined;
|
|
|
|
const delayTimeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctDelayTime);
|
|
const delayTime = delayTimeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctDelayTime, delayTimeAttr) as number : undefined;
|
|
|
|
const inputOctetsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctInputOctets);
|
|
const inputOctets = inputOctetsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctInputOctets, inputOctetsAttr) as number : undefined;
|
|
|
|
const outputOctetsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctOutputOctets);
|
|
const outputOctets = outputOctetsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctOutputOctets, outputOctetsAttr) as number : undefined;
|
|
|
|
const sessionTimeAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctSessionTime);
|
|
const sessionTime = sessionTimeAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctSessionTime, sessionTimeAttr) as number : undefined;
|
|
|
|
const inputPacketsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctInputPackets);
|
|
const inputPackets = inputPacketsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctInputPackets, inputPacketsAttr) as number : undefined;
|
|
|
|
const outputPacketsAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctOutputPackets);
|
|
const outputPackets = outputPacketsAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctOutputPackets, outputPacketsAttr) as number : undefined;
|
|
|
|
const terminateCauseAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctTerminateCause);
|
|
const terminateCause = terminateCauseAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctTerminateCause, terminateCauseAttr) as number : undefined;
|
|
|
|
const authenticAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctAuthentic);
|
|
const authentic = authenticAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctAuthentic, authenticAttr) as number : undefined;
|
|
|
|
const multiSessionIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctMultiSessionId);
|
|
const multiSessionId = multiSessionIdAttr ? multiSessionIdAttr.toString('utf8') : undefined;
|
|
|
|
const linkCountAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.AcctLinkCount);
|
|
const linkCount = linkCountAttr ? RadiusAttributes.parseValue(ERadiusAttributeType.AcctLinkCount, linkCountAttr) as number : undefined;
|
|
|
|
const calledStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CalledStationId);
|
|
const calledStationId = calledStationIdAttr ? calledStationIdAttr.toString('utf8') : undefined;
|
|
|
|
const callingStationIdAttr = RadiusPacket.getAttribute(packet, ERadiusAttributeType.CallingStationId);
|
|
const callingStationId = callingStationIdAttr ? callingStationIdAttr.toString('utf8') : undefined;
|
|
|
|
return {
|
|
statusType,
|
|
sessionId,
|
|
username,
|
|
nasIpAddress,
|
|
nasIdentifier,
|
|
nasPort,
|
|
nasPortType,
|
|
delayTime,
|
|
inputOctets,
|
|
outputOctets,
|
|
sessionTime,
|
|
inputPackets,
|
|
outputPackets,
|
|
terminateCause,
|
|
authentic,
|
|
multiSessionId,
|
|
linkCount,
|
|
calledStationId,
|
|
callingStationId,
|
|
rawPacket: packet,
|
|
clientAddress: rinfo.address,
|
|
clientPort: rinfo.port,
|
|
};
|
|
}
|
|
|
|
private buildAuthResponse(
|
|
response: IAuthenticationResponse,
|
|
identifier: number,
|
|
requestAuthenticator: Buffer,
|
|
secret: string
|
|
): Buffer {
|
|
const attributes: Array<{ type: number | string; value: string | number | Buffer }> = [];
|
|
|
|
// Add reply message if present
|
|
if (response.replyMessage) {
|
|
attributes.push({ type: ERadiusAttributeType.ReplyMessage, value: response.replyMessage });
|
|
}
|
|
|
|
// Add session timeout if present
|
|
if (response.sessionTimeout !== undefined) {
|
|
attributes.push({ type: ERadiusAttributeType.SessionTimeout, value: response.sessionTimeout });
|
|
}
|
|
|
|
// Add idle timeout if present
|
|
if (response.idleTimeout !== undefined) {
|
|
attributes.push({ type: ERadiusAttributeType.IdleTimeout, value: response.idleTimeout });
|
|
}
|
|
|
|
// Add state if present
|
|
if (response.state) {
|
|
attributes.push({ type: ERadiusAttributeType.State, value: response.state });
|
|
}
|
|
|
|
// Add class if present
|
|
if (response.class) {
|
|
attributes.push({ type: ERadiusAttributeType.Class, value: response.class });
|
|
}
|
|
|
|
// Add framed IP if present
|
|
if (response.framedIpAddress) {
|
|
attributes.push({ type: ERadiusAttributeType.FramedIpAddress, value: response.framedIpAddress });
|
|
}
|
|
|
|
// Add framed IP netmask if present
|
|
if (response.framedIpNetmask) {
|
|
attributes.push({ type: ERadiusAttributeType.FramedIpNetmask, value: response.framedIpNetmask });
|
|
}
|
|
|
|
// Add framed routes if present
|
|
if (response.framedRoutes) {
|
|
for (const route of response.framedRoutes) {
|
|
attributes.push({ type: ERadiusAttributeType.FramedRoute, value: route });
|
|
}
|
|
}
|
|
|
|
// Add vendor attributes if present
|
|
if (response.vendorAttributes) {
|
|
for (const vsa of response.vendorAttributes) {
|
|
const vsaBuffer = RadiusAttributes.encodeVSA(vsa);
|
|
attributes.push({ type: ERadiusAttributeType.VendorSpecific, value: vsaBuffer });
|
|
}
|
|
}
|
|
|
|
// Add custom attributes if present
|
|
if (response.attributes) {
|
|
attributes.push(...response.attributes);
|
|
}
|
|
|
|
// Create response based on code
|
|
switch (response.code) {
|
|
case ERadiusCode.AccessAccept:
|
|
return RadiusPacket.createAccessAccept(identifier, requestAuthenticator, secret, attributes);
|
|
case ERadiusCode.AccessReject:
|
|
return RadiusPacket.createAccessReject(identifier, requestAuthenticator, secret, attributes);
|
|
case ERadiusCode.AccessChallenge:
|
|
return RadiusPacket.createAccessChallenge(identifier, requestAuthenticator, secret, attributes);
|
|
default:
|
|
return RadiusPacket.createAccessReject(identifier, requestAuthenticator, secret, attributes);
|
|
}
|
|
}
|
|
|
|
private cleanupDuplicateCache(): void {
|
|
const now = Date.now();
|
|
const expiry = this.options.duplicateDetectionWindow;
|
|
|
|
for (const [key, entry] of this.recentRequests) {
|
|
if (now - entry.timestamp > expiry) {
|
|
this.recentRequests.delete(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export default RadiusServer;
|