feat(smartradius): Implement full RADIUS server and client with RFC 2865/2866 compliance, including packet handling, authenticators, attributes, secrets manager, client APIs, and comprehensive tests and documentation
This commit is contained in:
211
test/server/test.attributes.ts
Normal file
211
test/server/test.attributes.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
RadiusAttributes,
|
||||
ERadiusAttributeType,
|
||||
} from '../../ts_server/index.js';
|
||||
|
||||
tap.test('should get attribute definitions by type', async () => {
|
||||
const userNameDef = RadiusAttributes.getDefinition(ERadiusAttributeType.UserName);
|
||||
expect(userNameDef).toBeDefined();
|
||||
expect(userNameDef!.name).toEqual('User-Name');
|
||||
expect(userNameDef!.valueType).toEqual('text');
|
||||
|
||||
const nasIpDef = RadiusAttributes.getDefinition(ERadiusAttributeType.NasIpAddress);
|
||||
expect(nasIpDef).toBeDefined();
|
||||
expect(nasIpDef!.name).toEqual('NAS-IP-Address');
|
||||
expect(nasIpDef!.valueType).toEqual('address');
|
||||
|
||||
const nasPortDef = RadiusAttributes.getDefinition(ERadiusAttributeType.NasPort);
|
||||
expect(nasPortDef).toBeDefined();
|
||||
expect(nasPortDef!.name).toEqual('NAS-Port');
|
||||
expect(nasPortDef!.valueType).toEqual('integer');
|
||||
});
|
||||
|
||||
tap.test('should get attribute type by name', async () => {
|
||||
expect(RadiusAttributes.getTypeByName('User-Name')).toEqual(ERadiusAttributeType.UserName);
|
||||
expect(RadiusAttributes.getTypeByName('NAS-IP-Address')).toEqual(ERadiusAttributeType.NasIpAddress);
|
||||
expect(RadiusAttributes.getTypeByName('NAS-Port')).toEqual(ERadiusAttributeType.NasPort);
|
||||
expect(RadiusAttributes.getTypeByName('Unknown-Attribute')).toBeUndefined();
|
||||
});
|
||||
|
||||
tap.test('should get attribute name by type', async () => {
|
||||
expect(RadiusAttributes.getNameByType(ERadiusAttributeType.UserName)).toEqual('User-Name');
|
||||
expect(RadiusAttributes.getNameByType(ERadiusAttributeType.UserPassword)).toEqual('User-Password');
|
||||
expect(RadiusAttributes.getNameByType(255)).toInclude('Unknown-Attribute');
|
||||
});
|
||||
|
||||
tap.test('should parse text attributes', async () => {
|
||||
const textBuffer = Buffer.from('testuser', 'utf8');
|
||||
const parsed = RadiusAttributes.parseValue(ERadiusAttributeType.UserName, textBuffer);
|
||||
expect(parsed).toEqual('testuser');
|
||||
});
|
||||
|
||||
tap.test('should parse address attributes', async () => {
|
||||
const addressBuffer = Buffer.from([192, 168, 1, 100]);
|
||||
const parsed = RadiusAttributes.parseValue(ERadiusAttributeType.NasIpAddress, addressBuffer);
|
||||
expect(parsed).toEqual('192.168.1.100');
|
||||
});
|
||||
|
||||
tap.test('should parse integer attributes', async () => {
|
||||
const intBuffer = Buffer.allocUnsafe(4);
|
||||
intBuffer.writeUInt32BE(12345, 0);
|
||||
const parsed = RadiusAttributes.parseValue(ERadiusAttributeType.NasPort, intBuffer);
|
||||
expect(parsed).toEqual(12345);
|
||||
});
|
||||
|
||||
tap.test('should encode text attributes', async () => {
|
||||
const encoded = RadiusAttributes.encodeValue(ERadiusAttributeType.UserName, 'testuser');
|
||||
expect(encoded.toString('utf8')).toEqual('testuser');
|
||||
});
|
||||
|
||||
tap.test('should encode address attributes', async () => {
|
||||
const encoded = RadiusAttributes.encodeValue(ERadiusAttributeType.NasIpAddress, '10.20.30.40');
|
||||
expect(encoded.length).toEqual(4);
|
||||
expect(encoded[0]).toEqual(10);
|
||||
expect(encoded[1]).toEqual(20);
|
||||
expect(encoded[2]).toEqual(30);
|
||||
expect(encoded[3]).toEqual(40);
|
||||
});
|
||||
|
||||
tap.test('should encode integer attributes', async () => {
|
||||
const encoded = RadiusAttributes.encodeValue(ERadiusAttributeType.NasPort, 65535);
|
||||
expect(encoded.length).toEqual(4);
|
||||
expect(encoded.readUInt32BE(0)).toEqual(65535);
|
||||
});
|
||||
|
||||
tap.test('should encode complete attribute with TLV format', async () => {
|
||||
const encoded = RadiusAttributes.encodeAttribute(ERadiusAttributeType.UserName, 'test');
|
||||
expect(encoded[0]).toEqual(ERadiusAttributeType.UserName); // Type
|
||||
expect(encoded[1]).toEqual(6); // Length (2 + 4)
|
||||
expect(encoded.subarray(2).toString('utf8')).toEqual('test');
|
||||
});
|
||||
|
||||
tap.test('should handle attribute value too long', async () => {
|
||||
const longValue = Buffer.alloc(254); // Max is 253 bytes
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
RadiusAttributes.encodeAttribute(ERadiusAttributeType.UserName, longValue);
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.message).toInclude('too long');
|
||||
});
|
||||
|
||||
tap.test('should parse raw attribute', async () => {
|
||||
const rawAttr = {
|
||||
type: ERadiusAttributeType.UserName,
|
||||
value: Buffer.from('john.doe', 'utf8'),
|
||||
};
|
||||
const parsed = RadiusAttributes.parseAttribute(rawAttr);
|
||||
expect(parsed.type).toEqual(ERadiusAttributeType.UserName);
|
||||
expect(parsed.name).toEqual('User-Name');
|
||||
expect(parsed.value).toEqual('john.doe');
|
||||
});
|
||||
|
||||
tap.test('should handle Vendor-Specific Attributes', async () => {
|
||||
// Vendor-Id: 9 (Cisco), Vendor-Type: 1, Value: 'test'
|
||||
const vendorId = 9;
|
||||
const vendorType = 1;
|
||||
const vendorValue = Buffer.from('cisco-av-pair', 'utf8');
|
||||
|
||||
const vsaBuffer = RadiusAttributes.encodeVSA({ vendorId, vendorType, vendorValue });
|
||||
|
||||
// Parse it back
|
||||
const parsed = RadiusAttributes.parseVSA(vsaBuffer);
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed!.vendorId).toEqual(vendorId);
|
||||
expect(parsed!.vendorType).toEqual(vendorType);
|
||||
expect(parsed!.vendorValue.toString('utf8')).toEqual('cisco-av-pair');
|
||||
});
|
||||
|
||||
tap.test('should create complete Vendor-Specific attribute', async () => {
|
||||
const vendorId = 311; // Microsoft
|
||||
const vendorType = 1;
|
||||
const vendorValue = Buffer.from('test-value', 'utf8');
|
||||
|
||||
const attr = RadiusAttributes.createVendorAttribute(vendorId, vendorType, vendorValue);
|
||||
|
||||
// First byte should be type 26 (Vendor-Specific)
|
||||
expect(attr[0]).toEqual(ERadiusAttributeType.VendorSpecific);
|
||||
|
||||
// Second byte is total length
|
||||
expect(attr[1]).toBeGreaterThan(6);
|
||||
});
|
||||
|
||||
tap.test('should check if attribute is encrypted', async () => {
|
||||
expect(RadiusAttributes.isEncrypted(ERadiusAttributeType.UserPassword)).toBeTruthy();
|
||||
expect(RadiusAttributes.isEncrypted(ERadiusAttributeType.UserName)).toBeFalsy();
|
||||
});
|
||||
|
||||
tap.test('should handle all standard RFC 2865 attributes', async () => {
|
||||
// Test that all standard attributes have definitions
|
||||
const standardAttributes = [
|
||||
ERadiusAttributeType.UserName,
|
||||
ERadiusAttributeType.UserPassword,
|
||||
ERadiusAttributeType.ChapPassword,
|
||||
ERadiusAttributeType.NasIpAddress,
|
||||
ERadiusAttributeType.NasPort,
|
||||
ERadiusAttributeType.ServiceType,
|
||||
ERadiusAttributeType.FramedProtocol,
|
||||
ERadiusAttributeType.FramedIpAddress,
|
||||
ERadiusAttributeType.FramedIpNetmask,
|
||||
ERadiusAttributeType.FramedRouting,
|
||||
ERadiusAttributeType.FilterId,
|
||||
ERadiusAttributeType.FramedMtu,
|
||||
ERadiusAttributeType.FramedCompression,
|
||||
ERadiusAttributeType.LoginIpHost,
|
||||
ERadiusAttributeType.LoginService,
|
||||
ERadiusAttributeType.LoginTcpPort,
|
||||
ERadiusAttributeType.ReplyMessage,
|
||||
ERadiusAttributeType.CallbackNumber,
|
||||
ERadiusAttributeType.CallbackId,
|
||||
ERadiusAttributeType.FramedRoute,
|
||||
ERadiusAttributeType.FramedIpxNetwork,
|
||||
ERadiusAttributeType.State,
|
||||
ERadiusAttributeType.Class,
|
||||
ERadiusAttributeType.VendorSpecific,
|
||||
ERadiusAttributeType.SessionTimeout,
|
||||
ERadiusAttributeType.IdleTimeout,
|
||||
ERadiusAttributeType.TerminationAction,
|
||||
ERadiusAttributeType.CalledStationId,
|
||||
ERadiusAttributeType.CallingStationId,
|
||||
ERadiusAttributeType.NasIdentifier,
|
||||
ERadiusAttributeType.ProxyState,
|
||||
ERadiusAttributeType.ChapChallenge,
|
||||
ERadiusAttributeType.NasPortType,
|
||||
ERadiusAttributeType.PortLimit,
|
||||
];
|
||||
|
||||
for (const attrType of standardAttributes) {
|
||||
const def = RadiusAttributes.getDefinition(attrType);
|
||||
expect(def).toBeDefined();
|
||||
expect(def!.name).toBeDefined();
|
||||
expect(def!.valueType).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should handle all RFC 2866 accounting attributes', async () => {
|
||||
const accountingAttributes = [
|
||||
ERadiusAttributeType.AcctStatusType,
|
||||
ERadiusAttributeType.AcctDelayTime,
|
||||
ERadiusAttributeType.AcctInputOctets,
|
||||
ERadiusAttributeType.AcctOutputOctets,
|
||||
ERadiusAttributeType.AcctSessionId,
|
||||
ERadiusAttributeType.AcctAuthentic,
|
||||
ERadiusAttributeType.AcctSessionTime,
|
||||
ERadiusAttributeType.AcctInputPackets,
|
||||
ERadiusAttributeType.AcctOutputPackets,
|
||||
ERadiusAttributeType.AcctTerminateCause,
|
||||
ERadiusAttributeType.AcctMultiSessionId,
|
||||
ERadiusAttributeType.AcctLinkCount,
|
||||
];
|
||||
|
||||
for (const attrType of accountingAttributes) {
|
||||
const def = RadiusAttributes.getDefinition(attrType);
|
||||
expect(def).toBeDefined();
|
||||
expect(def!.name).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user