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 = { [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;