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:
190
test/server/test.packet.ts
Normal file
190
test/server/test.packet.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import {
|
||||
RadiusPacket,
|
||||
ERadiusCode,
|
||||
ERadiusAttributeType,
|
||||
} from '../../ts_server/index.js';
|
||||
|
||||
tap.test('should encode and decode a basic Access-Request packet', async () => {
|
||||
const identifier = 42;
|
||||
const secret = 'testing123';
|
||||
const attributes = [
|
||||
{ type: ERadiusAttributeType.UserName, value: 'testuser' },
|
||||
{ type: ERadiusAttributeType.UserPassword, value: 'testpass' },
|
||||
{ type: ERadiusAttributeType.NasIpAddress, value: '192.168.1.1' },
|
||||
{ type: ERadiusAttributeType.NasPort, value: 1 },
|
||||
];
|
||||
|
||||
const packet = RadiusPacket.createAccessRequest(identifier, secret, attributes);
|
||||
|
||||
// Verify minimum packet size (20 bytes header)
|
||||
expect(packet.length).toBeGreaterThanOrEqual(RadiusPacket.MIN_PACKET_SIZE);
|
||||
|
||||
// Verify maximum packet size
|
||||
expect(packet.length).toBeLessThanOrEqual(RadiusPacket.MAX_PACKET_SIZE);
|
||||
|
||||
// Verify header
|
||||
expect(packet[0]).toEqual(ERadiusCode.AccessRequest);
|
||||
expect(packet[1]).toEqual(identifier);
|
||||
|
||||
// Decode the packet
|
||||
const decoded = RadiusPacket.decode(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccessRequest);
|
||||
expect(decoded.identifier).toEqual(identifier);
|
||||
expect(decoded.authenticator.length).toEqual(16);
|
||||
expect(decoded.attributes.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('should handle packet length validation', async () => {
|
||||
// Packet too short
|
||||
const shortPacket = Buffer.alloc(19);
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
RadiusPacket.decode(shortPacket);
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.message).toInclude('too short');
|
||||
});
|
||||
|
||||
tap.test('should handle invalid length in header', async () => {
|
||||
const packet = Buffer.alloc(20);
|
||||
packet[0] = ERadiusCode.AccessRequest;
|
||||
packet[1] = 1; // identifier
|
||||
packet.writeUInt16BE(10, 2); // length too small
|
||||
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
RadiusPacket.decode(packet);
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
tap.test('should handle maximum packet size', async () => {
|
||||
const secret = 'testing123';
|
||||
const identifier = 1;
|
||||
|
||||
// Create a packet that would exceed max size
|
||||
const hugeAttributes: Array<{ type: number; value: Buffer }> = [];
|
||||
// Each attribute can be max 255 bytes. Create enough to exceed 4096
|
||||
for (let i = 0; i < 20; i++) {
|
||||
hugeAttributes.push({
|
||||
type: ERadiusAttributeType.ReplyMessage,
|
||||
value: Buffer.alloc(250, 65), // 250 bytes of 'A'
|
||||
});
|
||||
}
|
||||
|
||||
// This should throw because packet would be too large
|
||||
let error: Error | undefined;
|
||||
try {
|
||||
// Manually build the packet to test size limit
|
||||
const rawAttrs = hugeAttributes.map((a) => ({
|
||||
type: a.type,
|
||||
value: a.value,
|
||||
}));
|
||||
|
||||
RadiusPacket.encode({
|
||||
code: ERadiusCode.AccessRequest,
|
||||
identifier,
|
||||
authenticator: Buffer.alloc(16),
|
||||
attributes: rawAttrs,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).toBeDefined();
|
||||
expect(error!.message).toInclude('too large');
|
||||
});
|
||||
|
||||
tap.test('should parse and encode attributes correctly', async () => {
|
||||
const secret = 'testing123';
|
||||
const identifier = 1;
|
||||
|
||||
// Create packet with various attribute types
|
||||
const packet = RadiusPacket.createAccessRequest(identifier, secret, [
|
||||
{ type: 'User-Name', value: 'john.doe' }, // text
|
||||
{ type: 'NAS-IP-Address', value: '10.0.0.1' }, // address
|
||||
{ type: 'NAS-Port', value: 5060 }, // integer
|
||||
{ type: 'NAS-Identifier', value: 'nas01.example.com' }, // text
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decodeAndParse(packet);
|
||||
|
||||
// Find username
|
||||
const usernameAttr = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.UserName
|
||||
);
|
||||
expect(usernameAttr).toBeDefined();
|
||||
expect(usernameAttr!.value).toEqual('john.doe');
|
||||
|
||||
// Find NAS-IP-Address
|
||||
const nasIpAttr = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.NasIpAddress
|
||||
);
|
||||
expect(nasIpAttr).toBeDefined();
|
||||
expect(nasIpAttr!.value).toEqual('10.0.0.1');
|
||||
|
||||
// Find NAS-Port
|
||||
const nasPortAttr = decoded.parsedAttributes.find(
|
||||
(a) => a.type === ERadiusAttributeType.NasPort
|
||||
);
|
||||
expect(nasPortAttr).toBeDefined();
|
||||
expect(nasPortAttr!.value).toEqual(5060);
|
||||
});
|
||||
|
||||
tap.test('should create Access-Accept packet', async () => {
|
||||
const identifier = 1;
|
||||
const requestAuth = Buffer.alloc(16);
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccessAccept(identifier, requestAuth, secret, [
|
||||
{ type: ERadiusAttributeType.ReplyMessage, value: 'Welcome!' },
|
||||
{ type: ERadiusAttributeType.SessionTimeout, value: 3600 },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decode(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccessAccept);
|
||||
expect(decoded.identifier).toEqual(identifier);
|
||||
});
|
||||
|
||||
tap.test('should create Access-Reject packet', async () => {
|
||||
const identifier = 1;
|
||||
const requestAuth = Buffer.alloc(16);
|
||||
const secret = 'testing123';
|
||||
|
||||
const packet = RadiusPacket.createAccessReject(identifier, requestAuth, secret, [
|
||||
{ type: ERadiusAttributeType.ReplyMessage, value: 'Invalid credentials' },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decode(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccessReject);
|
||||
});
|
||||
|
||||
tap.test('should create Access-Challenge packet', async () => {
|
||||
const identifier = 1;
|
||||
const requestAuth = Buffer.alloc(16);
|
||||
const secret = 'testing123';
|
||||
const state = Buffer.from('challenge-state-123');
|
||||
|
||||
const packet = RadiusPacket.createAccessChallenge(identifier, requestAuth, secret, [
|
||||
{ type: ERadiusAttributeType.ReplyMessage, value: 'Enter OTP' },
|
||||
{ type: ERadiusAttributeType.State, value: state },
|
||||
]);
|
||||
|
||||
const decoded = RadiusPacket.decode(packet);
|
||||
expect(decoded.code).toEqual(ERadiusCode.AccessChallenge);
|
||||
});
|
||||
|
||||
tap.test('should get code name', async () => {
|
||||
expect(RadiusPacket.getCodeName(ERadiusCode.AccessRequest)).toEqual('Access-Request');
|
||||
expect(RadiusPacket.getCodeName(ERadiusCode.AccessAccept)).toEqual('Access-Accept');
|
||||
expect(RadiusPacket.getCodeName(ERadiusCode.AccessReject)).toEqual('Access-Reject');
|
||||
expect(RadiusPacket.getCodeName(ERadiusCode.AccountingRequest)).toEqual('Accounting-Request');
|
||||
expect(RadiusPacket.getCodeName(ERadiusCode.AccountingResponse)).toEqual('Accounting-Response');
|
||||
expect(RadiusPacket.getCodeName(ERadiusCode.AccessChallenge)).toEqual('Access-Challenge');
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
Reference in New Issue
Block a user