import * as plugins from './plugins.js'; import type { IAttributeDefinition, IRadiusAttribute, IParsedAttribute, IVendorSpecificAttribute, TAttributeValueType, } from './interfaces.js'; import { ERadiusAttributeType } from './interfaces.js'; /** * RADIUS Attribute Dictionary * Based on RFC 2865 and RFC 2866 */ export class RadiusAttributes { /** * Standard RADIUS attribute definitions */ private static readonly attributeDefinitions: Map = new Map([ // RFC 2865 Authentication Attributes [ERadiusAttributeType.UserName, { type: 1, name: 'User-Name', valueType: 'text' }], [ERadiusAttributeType.UserPassword, { type: 2, name: 'User-Password', valueType: 'string', encrypted: true }], [ERadiusAttributeType.ChapPassword, { type: 3, name: 'CHAP-Password', valueType: 'string' }], [ERadiusAttributeType.NasIpAddress, { type: 4, name: 'NAS-IP-Address', valueType: 'address' }], [ERadiusAttributeType.NasPort, { type: 5, name: 'NAS-Port', valueType: 'integer' }], [ERadiusAttributeType.ServiceType, { type: 6, name: 'Service-Type', valueType: 'integer' }], [ERadiusAttributeType.FramedProtocol, { type: 7, name: 'Framed-Protocol', valueType: 'integer' }], [ERadiusAttributeType.FramedIpAddress, { type: 8, name: 'Framed-IP-Address', valueType: 'address' }], [ERadiusAttributeType.FramedIpNetmask, { type: 9, name: 'Framed-IP-Netmask', valueType: 'address' }], [ERadiusAttributeType.FramedRouting, { type: 10, name: 'Framed-Routing', valueType: 'integer' }], [ERadiusAttributeType.FilterId, { type: 11, name: 'Filter-Id', valueType: 'text' }], [ERadiusAttributeType.FramedMtu, { type: 12, name: 'Framed-MTU', valueType: 'integer' }], [ERadiusAttributeType.FramedCompression, { type: 13, name: 'Framed-Compression', valueType: 'integer' }], [ERadiusAttributeType.LoginIpHost, { type: 14, name: 'Login-IP-Host', valueType: 'address' }], [ERadiusAttributeType.LoginService, { type: 15, name: 'Login-Service', valueType: 'integer' }], [ERadiusAttributeType.LoginTcpPort, { type: 16, name: 'Login-TCP-Port', valueType: 'integer' }], [ERadiusAttributeType.ReplyMessage, { type: 18, name: 'Reply-Message', valueType: 'text' }], [ERadiusAttributeType.CallbackNumber, { type: 19, name: 'Callback-Number', valueType: 'text' }], [ERadiusAttributeType.CallbackId, { type: 20, name: 'Callback-Id', valueType: 'text' }], [ERadiusAttributeType.FramedRoute, { type: 22, name: 'Framed-Route', valueType: 'text' }], [ERadiusAttributeType.FramedIpxNetwork, { type: 23, name: 'Framed-IPX-Network', valueType: 'integer' }], [ERadiusAttributeType.State, { type: 24, name: 'State', valueType: 'string' }], [ERadiusAttributeType.Class, { type: 25, name: 'Class', valueType: 'string' }], [ERadiusAttributeType.VendorSpecific, { type: 26, name: 'Vendor-Specific', valueType: 'vsa' }], [ERadiusAttributeType.SessionTimeout, { type: 27, name: 'Session-Timeout', valueType: 'integer' }], [ERadiusAttributeType.IdleTimeout, { type: 28, name: 'Idle-Timeout', valueType: 'integer' }], [ERadiusAttributeType.TerminationAction, { type: 29, name: 'Termination-Action', valueType: 'integer' }], [ERadiusAttributeType.CalledStationId, { type: 30, name: 'Called-Station-Id', valueType: 'text' }], [ERadiusAttributeType.CallingStationId, { type: 31, name: 'Calling-Station-Id', valueType: 'text' }], [ERadiusAttributeType.NasIdentifier, { type: 32, name: 'NAS-Identifier', valueType: 'text' }], [ERadiusAttributeType.ProxyState, { type: 33, name: 'Proxy-State', valueType: 'string' }], [ERadiusAttributeType.LoginLatService, { type: 34, name: 'Login-LAT-Service', valueType: 'text' }], [ERadiusAttributeType.LoginLatNode, { type: 35, name: 'Login-LAT-Node', valueType: 'text' }], [ERadiusAttributeType.LoginLatGroup, { type: 36, name: 'Login-LAT-Group', valueType: 'string' }], [ERadiusAttributeType.FramedAppleTalkLink, { type: 37, name: 'Framed-AppleTalk-Link', valueType: 'integer' }], [ERadiusAttributeType.FramedAppleTalkNetwork, { type: 38, name: 'Framed-AppleTalk-Network', valueType: 'integer' }], [ERadiusAttributeType.FramedAppleTalkZone, { type: 39, name: 'Framed-AppleTalk-Zone', valueType: 'text' }], [ERadiusAttributeType.ChapChallenge, { type: 60, name: 'CHAP-Challenge', valueType: 'string' }], [ERadiusAttributeType.NasPortType, { type: 61, name: 'NAS-Port-Type', valueType: 'integer' }], [ERadiusAttributeType.PortLimit, { type: 62, name: 'Port-Limit', valueType: 'integer' }], [ERadiusAttributeType.LoginLatPort, { type: 63, name: 'Login-LAT-Port', valueType: 'text' }], // RFC 2866 Accounting Attributes [ERadiusAttributeType.AcctStatusType, { type: 40, name: 'Acct-Status-Type', valueType: 'integer' }], [ERadiusAttributeType.AcctDelayTime, { type: 41, name: 'Acct-Delay-Time', valueType: 'integer' }], [ERadiusAttributeType.AcctInputOctets, { type: 42, name: 'Acct-Input-Octets', valueType: 'integer' }], [ERadiusAttributeType.AcctOutputOctets, { type: 43, name: 'Acct-Output-Octets', valueType: 'integer' }], [ERadiusAttributeType.AcctSessionId, { type: 44, name: 'Acct-Session-Id', valueType: 'text' }], [ERadiusAttributeType.AcctAuthentic, { type: 45, name: 'Acct-Authentic', valueType: 'integer' }], [ERadiusAttributeType.AcctSessionTime, { type: 46, name: 'Acct-Session-Time', valueType: 'integer' }], [ERadiusAttributeType.AcctInputPackets, { type: 47, name: 'Acct-Input-Packets', valueType: 'integer' }], [ERadiusAttributeType.AcctOutputPackets, { type: 48, name: 'Acct-Output-Packets', valueType: 'integer' }], [ERadiusAttributeType.AcctTerminateCause, { type: 49, name: 'Acct-Terminate-Cause', valueType: 'integer' }], [ERadiusAttributeType.AcctMultiSessionId, { type: 50, name: 'Acct-Multi-Session-Id', valueType: 'text' }], [ERadiusAttributeType.AcctLinkCount, { type: 51, name: 'Acct-Link-Count', valueType: 'integer' }], // EAP support [ERadiusAttributeType.EapMessage, { type: 79, name: 'EAP-Message', valueType: 'string' }], [ERadiusAttributeType.MessageAuthenticator, { type: 80, name: 'Message-Authenticator', valueType: 'string' }], ]); /** * Attribute name to type mapping */ private static readonly nameToType: Map = new Map( Array.from(RadiusAttributes.attributeDefinitions.entries()).map(([type, def]) => [def.name, type]) ); /** * Get attribute definition by type */ public static getDefinition(type: number): IAttributeDefinition | undefined { return this.attributeDefinitions.get(type); } /** * Get attribute type by name */ public static getTypeByName(name: string): number | undefined { return this.nameToType.get(name); } /** * Get attribute name by type */ public static getNameByType(type: number): string { const def = this.attributeDefinitions.get(type); return def ? def.name : `Unknown-Attribute-${type}`; } /** * Parse attribute value based on its type */ public static parseValue(type: number, value: Buffer): string | number | Buffer { const def = this.attributeDefinitions.get(type); if (!def) { return value; } switch (def.valueType) { case 'text': return value.toString('utf8'); case 'address': return this.parseAddress(value); case 'integer': return this.parseInteger(value); case 'time': return this.parseInteger(value); case 'string': case 'vsa': default: return value; } } /** * Encode attribute value */ public static encodeValue(type: number | string, value: string | number | Buffer): Buffer { const attrType = typeof type === 'string' ? this.getTypeByName(type) : type; if (attrType === undefined) { throw new Error(`Unknown attribute type: ${type}`); } const def = this.attributeDefinitions.get(attrType); if (!def) { if (Buffer.isBuffer(value)) { return value; } return Buffer.from(String(value), 'utf8'); } switch (def.valueType) { case 'text': return Buffer.from(String(value), 'utf8'); case 'address': return this.encodeAddress(String(value)); case 'integer': case 'time': return this.encodeInteger(Number(value)); case 'string': case 'vsa': default: if (Buffer.isBuffer(value)) { return value; } return Buffer.from(String(value), 'utf8'); } } /** * Parse IPv4 address from buffer (4 bytes) */ private static parseAddress(buffer: Buffer): string { if (buffer.length < 4) { return '0.0.0.0'; } return `${buffer[0]}.${buffer[1]}.${buffer[2]}.${buffer[3]}`; } /** * Encode IPv4 address to buffer */ private static encodeAddress(address: string): Buffer { const parts = address.split('.').map((p) => parseInt(p, 10)); if (parts.length !== 4 || parts.some((p) => isNaN(p) || p < 0 || p > 255)) { throw new Error(`Invalid IP address: ${address}`); } return Buffer.from(parts); } /** * Parse 32-bit unsigned integer from buffer (big-endian) */ private static parseInteger(buffer: Buffer): number { if (buffer.length < 4) { return 0; } return buffer.readUInt32BE(0); } /** * Encode 32-bit unsigned integer to buffer (big-endian) */ private static encodeInteger(value: number): Buffer { const buffer = Buffer.allocUnsafe(4); buffer.writeUInt32BE(value >>> 0, 0); return buffer; } /** * Encode a complete attribute (type + length + value) */ public static encodeAttribute(type: number | string, value: string | number | Buffer): Buffer { const attrType = typeof type === 'string' ? this.getTypeByName(type) : type; if (attrType === undefined) { throw new Error(`Unknown attribute type: ${type}`); } const encodedValue = this.encodeValue(attrType, value); const length = 2 + encodedValue.length; if (length > 255) { throw new Error(`Attribute value too long: ${length} bytes (max 253 value bytes)`); } const buffer = Buffer.allocUnsafe(length); buffer.writeUInt8(attrType, 0); buffer.writeUInt8(length, 1); encodedValue.copy(buffer, 2); return buffer; } /** * Parse raw attribute into named attribute */ public static parseAttribute(attr: IRadiusAttribute): IParsedAttribute { return { type: attr.type, name: this.getNameByType(attr.type), value: this.parseValue(attr.type, attr.value), rawValue: attr.value, }; } /** * Parse Vendor-Specific Attribute (RFC 2865 Section 5.26) * Format: Vendor-Id (4 bytes) + Vendor-Type (1 byte) + Vendor-Length (1 byte) + Value */ public static parseVSA(buffer: Buffer): IVendorSpecificAttribute | null { if (buffer.length < 6) { return null; } const vendorId = buffer.readUInt32BE(0); const vendorType = buffer.readUInt8(4); const vendorLength = buffer.readUInt8(5); if (buffer.length < 4 + vendorLength) { return null; } const vendorValue = buffer.subarray(6, 4 + vendorLength); return { vendorId, vendorType, vendorValue, }; } /** * Encode Vendor-Specific Attribute */ public static encodeVSA(vsa: IVendorSpecificAttribute): Buffer { const valueLength = vsa.vendorValue.length; const vendorLength = 2 + valueLength; // vendor-type + vendor-length + value const totalLength = 4 + vendorLength; // vendor-id + vendor sub-attributes const buffer = Buffer.allocUnsafe(totalLength); buffer.writeUInt32BE(vsa.vendorId, 0); buffer.writeUInt8(vsa.vendorType, 4); buffer.writeUInt8(vendorLength, 5); vsa.vendorValue.copy(buffer, 6); return buffer; } /** * Create a complete Vendor-Specific attribute (type 26) */ public static createVendorAttribute(vendorId: number, vendorType: number, vendorValue: Buffer): Buffer { const vsaValue = this.encodeVSA({ vendorId, vendorType, vendorValue }); return this.encodeAttribute(ERadiusAttributeType.VendorSpecific, vsaValue); } /** * Check if an attribute is encrypted (e.g., User-Password) */ public static isEncrypted(type: number): boolean { const def = this.attributeDefinitions.get(type); return def?.encrypted === true; } } export default RadiusAttributes;