427 lines
12 KiB
TypeScript
427 lines
12 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import type { IRadiusPacket, IParsedRadiusPacket, IRadiusAttribute, IParsedAttribute } from './interfaces.js';
|
|
import { ERadiusCode, ERadiusAttributeType } from './interfaces.js';
|
|
import { RadiusAttributes } from './classes.radiusattributes.js';
|
|
import { RadiusAuthenticator } from './classes.radiusauthenticator.js';
|
|
|
|
/**
|
|
* RADIUS Packet encoder/decoder
|
|
* Implements RFC 2865 Section 3 packet format
|
|
*/
|
|
export class RadiusPacket {
|
|
/**
|
|
* Minimum packet size (RFC 2865: 20 bytes)
|
|
*/
|
|
public static readonly MIN_PACKET_SIZE = 20;
|
|
|
|
/**
|
|
* Maximum packet size (RFC 2865: 4096 bytes)
|
|
*/
|
|
public static readonly MAX_PACKET_SIZE = 4096;
|
|
|
|
/**
|
|
* Header size (Code + Identifier + Length + Authenticator)
|
|
*/
|
|
public static readonly HEADER_SIZE = 20;
|
|
|
|
/**
|
|
* Authenticator size
|
|
*/
|
|
public static readonly AUTHENTICATOR_SIZE = 16;
|
|
|
|
/**
|
|
* Decode a RADIUS packet from a buffer
|
|
*/
|
|
public static decode(buffer: Buffer): IRadiusPacket {
|
|
if (buffer.length < this.MIN_PACKET_SIZE) {
|
|
throw new Error(`Packet too short: ${buffer.length} bytes (minimum ${this.MIN_PACKET_SIZE})`);
|
|
}
|
|
|
|
const code = buffer.readUInt8(0);
|
|
const identifier = buffer.readUInt8(1);
|
|
const length = buffer.readUInt16BE(2);
|
|
|
|
// Validate code
|
|
if (!this.isValidCode(code)) {
|
|
throw new Error(`Invalid packet code: ${code}`);
|
|
}
|
|
|
|
// Validate length
|
|
if (length < this.MIN_PACKET_SIZE) {
|
|
throw new Error(`Invalid packet length in header: ${length}`);
|
|
}
|
|
if (length > this.MAX_PACKET_SIZE) {
|
|
throw new Error(`Packet too large: ${length} bytes (maximum ${this.MAX_PACKET_SIZE})`);
|
|
}
|
|
if (buffer.length < length) {
|
|
throw new Error(`Buffer too short for declared length: ${buffer.length} < ${length}`);
|
|
}
|
|
|
|
const authenticator = Buffer.allocUnsafe(this.AUTHENTICATOR_SIZE);
|
|
buffer.copy(authenticator, 0, 4, 20);
|
|
|
|
const attributes = this.decodeAttributes(buffer.subarray(20, length));
|
|
|
|
return {
|
|
code,
|
|
identifier,
|
|
authenticator,
|
|
attributes,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decode packet and parse attributes
|
|
*/
|
|
public static decodeAndParse(buffer: Buffer): IParsedRadiusPacket {
|
|
const packet = this.decode(buffer);
|
|
const parsedAttributes = packet.attributes.map((attr) => RadiusAttributes.parseAttribute(attr));
|
|
|
|
return {
|
|
...packet,
|
|
parsedAttributes,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Decode attributes from buffer
|
|
*/
|
|
private static decodeAttributes(buffer: Buffer): IRadiusAttribute[] {
|
|
const attributes: IRadiusAttribute[] = [];
|
|
let offset = 0;
|
|
|
|
while (offset < buffer.length) {
|
|
if (offset + 2 > buffer.length) {
|
|
throw new Error('Malformed attribute: truncated header');
|
|
}
|
|
|
|
const type = buffer.readUInt8(offset);
|
|
const length = buffer.readUInt8(offset + 1);
|
|
|
|
if (length < 2) {
|
|
throw new Error(`Invalid attribute length: ${length}`);
|
|
}
|
|
if (offset + length > buffer.length) {
|
|
throw new Error('Malformed attribute: truncated value');
|
|
}
|
|
|
|
const value = Buffer.allocUnsafe(length - 2);
|
|
buffer.copy(value, 0, offset + 2, offset + length);
|
|
|
|
attributes.push({ type, value });
|
|
offset += length;
|
|
}
|
|
|
|
return attributes;
|
|
}
|
|
|
|
/**
|
|
* Encode a RADIUS packet to a buffer
|
|
*/
|
|
public static encode(packet: IRadiusPacket): Buffer {
|
|
const attributesBuffer = this.encodeAttributes(packet.attributes);
|
|
const length = this.HEADER_SIZE + attributesBuffer.length;
|
|
|
|
if (length > this.MAX_PACKET_SIZE) {
|
|
throw new Error(`Packet too large: ${length} bytes (maximum ${this.MAX_PACKET_SIZE})`);
|
|
}
|
|
|
|
const buffer = Buffer.allocUnsafe(length);
|
|
buffer.writeUInt8(packet.code, 0);
|
|
buffer.writeUInt8(packet.identifier, 1);
|
|
buffer.writeUInt16BE(length, 2);
|
|
packet.authenticator.copy(buffer, 4);
|
|
attributesBuffer.copy(buffer, 20);
|
|
|
|
return buffer;
|
|
}
|
|
|
|
/**
|
|
* Encode attributes to buffer
|
|
*/
|
|
private static encodeAttributes(attributes: IRadiusAttribute[]): Buffer {
|
|
const buffers = attributes.map((attr) => {
|
|
const length = 2 + attr.value.length;
|
|
if (length > 255) {
|
|
throw new Error(`Attribute value too long: ${attr.value.length} bytes (max 253)`);
|
|
}
|
|
const buffer = Buffer.allocUnsafe(length);
|
|
buffer.writeUInt8(attr.type, 0);
|
|
buffer.writeUInt8(length, 1);
|
|
attr.value.copy(buffer, 2);
|
|
return buffer;
|
|
});
|
|
|
|
return Buffer.concat(buffers);
|
|
}
|
|
|
|
/**
|
|
* Create an Access-Request packet
|
|
*/
|
|
public static createAccessRequest(
|
|
identifier: number,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }>
|
|
): Buffer {
|
|
const requestAuthenticator = RadiusAuthenticator.generateRequestAuthenticator();
|
|
const rawAttributes: IRadiusAttribute[] = [];
|
|
|
|
for (const attr of attributes) {
|
|
const attrType = typeof attr.type === 'string'
|
|
? RadiusAttributes.getTypeByName(attr.type)
|
|
: attr.type;
|
|
|
|
if (attrType === undefined) {
|
|
throw new Error(`Unknown attribute type: ${attr.type}`);
|
|
}
|
|
|
|
let value: Buffer;
|
|
if (attrType === ERadiusAttributeType.UserPassword && typeof attr.value === 'string') {
|
|
// Encrypt password
|
|
value = RadiusAuthenticator.encryptPassword(attr.value, requestAuthenticator, secret);
|
|
} else {
|
|
value = RadiusAttributes.encodeValue(attrType, attr.value);
|
|
}
|
|
|
|
rawAttributes.push({ type: attrType, value });
|
|
}
|
|
|
|
const packet: IRadiusPacket = {
|
|
code: ERadiusCode.AccessRequest,
|
|
identifier,
|
|
authenticator: requestAuthenticator,
|
|
attributes: rawAttributes,
|
|
};
|
|
|
|
return this.encode(packet);
|
|
}
|
|
|
|
/**
|
|
* Create an Access-Accept packet
|
|
*/
|
|
public static createAccessAccept(
|
|
identifier: number,
|
|
requestAuthenticator: Buffer,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }> = []
|
|
): Buffer {
|
|
return this.createResponse(
|
|
ERadiusCode.AccessAccept,
|
|
identifier,
|
|
requestAuthenticator,
|
|
secret,
|
|
attributes
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create an Access-Reject packet
|
|
*/
|
|
public static createAccessReject(
|
|
identifier: number,
|
|
requestAuthenticator: Buffer,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }> = []
|
|
): Buffer {
|
|
return this.createResponse(
|
|
ERadiusCode.AccessReject,
|
|
identifier,
|
|
requestAuthenticator,
|
|
secret,
|
|
attributes
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create an Access-Challenge packet
|
|
*/
|
|
public static createAccessChallenge(
|
|
identifier: number,
|
|
requestAuthenticator: Buffer,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }> = []
|
|
): Buffer {
|
|
return this.createResponse(
|
|
ERadiusCode.AccessChallenge,
|
|
identifier,
|
|
requestAuthenticator,
|
|
secret,
|
|
attributes
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create an Accounting-Request packet
|
|
*/
|
|
public static createAccountingRequest(
|
|
identifier: number,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }>
|
|
): Buffer {
|
|
const rawAttributes: IRadiusAttribute[] = [];
|
|
|
|
for (const attr of attributes) {
|
|
const attrType = typeof attr.type === 'string'
|
|
? RadiusAttributes.getTypeByName(attr.type)
|
|
: attr.type;
|
|
|
|
if (attrType === undefined) {
|
|
throw new Error(`Unknown attribute type: ${attr.type}`);
|
|
}
|
|
|
|
const value = RadiusAttributes.encodeValue(attrType, attr.value);
|
|
rawAttributes.push({ type: attrType, value });
|
|
}
|
|
|
|
// Calculate authenticator with zero placeholder
|
|
const attributesBuffer = this.encodeAttributes(rawAttributes);
|
|
const authenticator = RadiusAuthenticator.calculateAccountingRequestAuthenticator(
|
|
ERadiusCode.AccountingRequest,
|
|
identifier,
|
|
attributesBuffer,
|
|
secret
|
|
);
|
|
|
|
const packet: IRadiusPacket = {
|
|
code: ERadiusCode.AccountingRequest,
|
|
identifier,
|
|
authenticator,
|
|
attributes: rawAttributes,
|
|
};
|
|
|
|
return this.encode(packet);
|
|
}
|
|
|
|
/**
|
|
* Create an Accounting-Response packet
|
|
*/
|
|
public static createAccountingResponse(
|
|
identifier: number,
|
|
requestAuthenticator: Buffer,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }> = []
|
|
): Buffer {
|
|
return this.createResponse(
|
|
ERadiusCode.AccountingResponse,
|
|
identifier,
|
|
requestAuthenticator,
|
|
secret,
|
|
attributes
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create a response packet (Accept, Reject, Challenge, Accounting-Response)
|
|
*/
|
|
private static createResponse(
|
|
code: ERadiusCode,
|
|
identifier: number,
|
|
requestAuthenticator: Buffer,
|
|
secret: string,
|
|
attributes: Array<{ type: number | string; value: string | number | Buffer }>
|
|
): Buffer {
|
|
const rawAttributes: IRadiusAttribute[] = [];
|
|
|
|
for (const attr of attributes) {
|
|
const attrType = typeof attr.type === 'string'
|
|
? RadiusAttributes.getTypeByName(attr.type)
|
|
: attr.type;
|
|
|
|
if (attrType === undefined) {
|
|
throw new Error(`Unknown attribute type: ${attr.type}`);
|
|
}
|
|
|
|
const value = RadiusAttributes.encodeValue(attrType, attr.value);
|
|
rawAttributes.push({ type: attrType, value });
|
|
}
|
|
|
|
const attributesBuffer = this.encodeAttributes(rawAttributes);
|
|
|
|
// Calculate response authenticator
|
|
const responseAuthenticator = RadiusAuthenticator.calculateResponseAuthenticator(
|
|
code,
|
|
identifier,
|
|
requestAuthenticator,
|
|
attributesBuffer,
|
|
secret
|
|
);
|
|
|
|
const packet: IRadiusPacket = {
|
|
code,
|
|
identifier,
|
|
authenticator: responseAuthenticator,
|
|
attributes: rawAttributes,
|
|
};
|
|
|
|
return this.encode(packet);
|
|
}
|
|
|
|
/**
|
|
* Get attribute value from a packet
|
|
*/
|
|
public static getAttribute(packet: IRadiusPacket, type: number | string): Buffer | undefined {
|
|
const attrType = typeof type === 'string' ? RadiusAttributes.getTypeByName(type) : type;
|
|
if (attrType === undefined) {
|
|
return undefined;
|
|
}
|
|
const attr = packet.attributes.find((a) => a.type === attrType);
|
|
return attr?.value;
|
|
}
|
|
|
|
/**
|
|
* Get all attribute values from a packet
|
|
*/
|
|
public static getAttributes(packet: IRadiusPacket, type: number | string): Buffer[] {
|
|
const attrType = typeof type === 'string' ? RadiusAttributes.getTypeByName(type) : type;
|
|
if (attrType === undefined) {
|
|
return [];
|
|
}
|
|
return packet.attributes.filter((a) => a.type === attrType).map((a) => a.value);
|
|
}
|
|
|
|
/**
|
|
* Get parsed attribute value
|
|
*/
|
|
public static getParsedAttribute(packet: IRadiusPacket, type: number | string): string | number | Buffer | undefined {
|
|
const value = this.getAttribute(packet, type);
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
const attrType = typeof type === 'string' ? RadiusAttributes.getTypeByName(type)! : type;
|
|
return RadiusAttributes.parseValue(attrType, value);
|
|
}
|
|
|
|
/**
|
|
* Check if a packet code is valid
|
|
*/
|
|
private static isValidCode(code: number): boolean {
|
|
return code === ERadiusCode.AccessRequest ||
|
|
code === ERadiusCode.AccessAccept ||
|
|
code === ERadiusCode.AccessReject ||
|
|
code === ERadiusCode.AccountingRequest ||
|
|
code === ERadiusCode.AccountingResponse ||
|
|
code === ERadiusCode.AccessChallenge ||
|
|
code === ERadiusCode.StatusServer ||
|
|
code === ERadiusCode.StatusClient;
|
|
}
|
|
|
|
/**
|
|
* Get code name
|
|
*/
|
|
public static getCodeName(code: ERadiusCode): string {
|
|
const names: Record<number, string> = {
|
|
[ERadiusCode.AccessRequest]: 'Access-Request',
|
|
[ERadiusCode.AccessAccept]: 'Access-Accept',
|
|
[ERadiusCode.AccessReject]: 'Access-Reject',
|
|
[ERadiusCode.AccountingRequest]: 'Accounting-Request',
|
|
[ERadiusCode.AccountingResponse]: 'Accounting-Response',
|
|
[ERadiusCode.AccessChallenge]: 'Access-Challenge',
|
|
[ERadiusCode.StatusServer]: 'Status-Server',
|
|
[ERadiusCode.StatusClient]: 'Status-Client',
|
|
};
|
|
return names[code] || `Unknown-Code-${code}`;
|
|
}
|
|
}
|
|
|
|
export default RadiusPacket;
|