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:
531
ts_client/classes.radiusclient.ts
Normal file
531
ts_client/classes.radiusclient.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import type {
|
||||
IRadiusClientOptions,
|
||||
IClientAuthRequest,
|
||||
IClientAuthResponse,
|
||||
IClientAccountingRequest,
|
||||
IClientAccountingResponse,
|
||||
} from './interfaces.js';
|
||||
import {
|
||||
RadiusPacket,
|
||||
RadiusAuthenticator,
|
||||
RadiusAttributes,
|
||||
ERadiusCode,
|
||||
ERadiusAttributeType,
|
||||
} from '../ts_server/index.js';
|
||||
|
||||
/**
|
||||
* Pending request tracking
|
||||
*/
|
||||
interface IPendingRequest {
|
||||
identifier: number;
|
||||
requestAuthenticator: Buffer;
|
||||
resolve: (response: Buffer) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeoutId?: ReturnType<typeof setTimeout>;
|
||||
retries: number;
|
||||
packet: Buffer;
|
||||
port: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* RADIUS Client implementation
|
||||
* Supports PAP, CHAP authentication and accounting
|
||||
*/
|
||||
export class RadiusClient {
|
||||
private socket?: plugins.dgram.Socket;
|
||||
private readonly options: Required<IRadiusClientOptions>;
|
||||
private currentIdentifier = 0;
|
||||
private readonly pendingRequests: Map<number, IPendingRequest> = new Map();
|
||||
private isConnected = false;
|
||||
|
||||
constructor(options: IRadiusClientOptions) {
|
||||
this.options = {
|
||||
host: options.host,
|
||||
authPort: options.authPort ?? 1812,
|
||||
acctPort: options.acctPort ?? 1813,
|
||||
secret: options.secret,
|
||||
timeout: options.timeout ?? 5000,
|
||||
retries: options.retries ?? 3,
|
||||
retryDelay: options.retryDelay ?? 1000,
|
||||
nasIpAddress: options.nasIpAddress ?? '0.0.0.0',
|
||||
nasIdentifier: options.nasIdentifier ?? 'smartradius-client',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect the client (bind UDP socket)
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
if (this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.socket = plugins.dgram.createSocket('udp4');
|
||||
|
||||
this.socket.on('error', (err) => {
|
||||
if (!this.isConnected) {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('message', (msg) => {
|
||||
this.handleResponse(msg);
|
||||
});
|
||||
|
||||
this.socket.bind(0, () => {
|
||||
this.isConnected = true;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect the client
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
if (!this.isConnected || !this.socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reject all pending requests
|
||||
for (const [, pending] of this.pendingRequests) {
|
||||
if (pending.timeoutId) {
|
||||
clearTimeout(pending.timeoutId);
|
||||
}
|
||||
pending.reject(new Error('Client disconnected'));
|
||||
}
|
||||
this.pendingRequests.clear();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
this.socket!.close(() => {
|
||||
this.socket = undefined;
|
||||
this.isConnected = false;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user using PAP
|
||||
*/
|
||||
public async authenticatePap(username: string, password: string): Promise<IClientAuthResponse> {
|
||||
return this.authenticate({
|
||||
username,
|
||||
password,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate a user using CHAP
|
||||
*/
|
||||
public async authenticateChap(
|
||||
username: string,
|
||||
password: string,
|
||||
challenge?: Buffer
|
||||
): Promise<IClientAuthResponse> {
|
||||
// Generate challenge if not provided
|
||||
const chapChallenge = challenge || plugins.crypto.randomBytes(16);
|
||||
const chapId = Math.floor(Math.random() * 256);
|
||||
|
||||
// Calculate CHAP response
|
||||
const chapResponse = RadiusAuthenticator.calculateChapResponse(chapId, password, chapChallenge);
|
||||
|
||||
// CHAP-Password = CHAP Ident (1 byte) + Response (16 bytes)
|
||||
const chapPassword = Buffer.allocUnsafe(17);
|
||||
chapPassword.writeUInt8(chapId, 0);
|
||||
chapResponse.copy(chapPassword, 1);
|
||||
|
||||
return this.authenticate({
|
||||
username,
|
||||
chapPassword,
|
||||
chapChallenge,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an authentication request
|
||||
*/
|
||||
public async authenticate(request: IClientAuthRequest): Promise<IClientAuthResponse> {
|
||||
await this.ensureConnected();
|
||||
|
||||
const identifier = this.nextIdentifier();
|
||||
const attributes: Array<{ type: number | string; value: string | number | Buffer }> = [];
|
||||
|
||||
// Add User-Name
|
||||
attributes.push({ type: ERadiusAttributeType.UserName, value: request.username });
|
||||
|
||||
// Add NAS-IP-Address or NAS-Identifier
|
||||
if (this.options.nasIpAddress && this.options.nasIpAddress !== '0.0.0.0') {
|
||||
attributes.push({ type: ERadiusAttributeType.NasIpAddress, value: this.options.nasIpAddress });
|
||||
}
|
||||
if (this.options.nasIdentifier) {
|
||||
attributes.push({ type: ERadiusAttributeType.NasIdentifier, value: this.options.nasIdentifier });
|
||||
}
|
||||
|
||||
// Add PAP password or CHAP credentials
|
||||
if (request.password !== undefined) {
|
||||
// PAP - password will be encrypted in createAccessRequest
|
||||
attributes.push({ type: ERadiusAttributeType.UserPassword, value: request.password });
|
||||
} else if (request.chapPassword) {
|
||||
// CHAP
|
||||
attributes.push({ type: ERadiusAttributeType.ChapPassword, value: request.chapPassword });
|
||||
if (request.chapChallenge) {
|
||||
attributes.push({ type: ERadiusAttributeType.ChapChallenge, value: request.chapChallenge });
|
||||
}
|
||||
}
|
||||
|
||||
// Add optional attributes
|
||||
if (request.nasPort !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.NasPort, value: request.nasPort });
|
||||
}
|
||||
if (request.nasPortType !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.NasPortType, value: request.nasPortType });
|
||||
}
|
||||
if (request.serviceType !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.ServiceType, value: request.serviceType });
|
||||
}
|
||||
if (request.calledStationId) {
|
||||
attributes.push({ type: ERadiusAttributeType.CalledStationId, value: request.calledStationId });
|
||||
}
|
||||
if (request.callingStationId) {
|
||||
attributes.push({ type: ERadiusAttributeType.CallingStationId, value: request.callingStationId });
|
||||
}
|
||||
if (request.state) {
|
||||
attributes.push({ type: ERadiusAttributeType.State, value: request.state });
|
||||
}
|
||||
|
||||
// Add custom attributes
|
||||
if (request.customAttributes) {
|
||||
attributes.push(...request.customAttributes);
|
||||
}
|
||||
|
||||
// Create packet
|
||||
const packet = RadiusPacket.createAccessRequest(identifier, this.options.secret, attributes);
|
||||
|
||||
// Extract request authenticator from packet (bytes 4-20)
|
||||
const requestAuthenticator = packet.subarray(4, 20);
|
||||
|
||||
// Send and wait for response
|
||||
const responseBuffer = await this.sendRequest(
|
||||
identifier,
|
||||
packet,
|
||||
requestAuthenticator,
|
||||
this.options.authPort
|
||||
);
|
||||
|
||||
// Verify response authenticator
|
||||
if (!RadiusAuthenticator.verifyResponseAuthenticator(
|
||||
responseBuffer,
|
||||
requestAuthenticator,
|
||||
this.options.secret
|
||||
)) {
|
||||
throw new Error('Invalid response authenticator');
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const response = RadiusPacket.decodeAndParse(responseBuffer);
|
||||
|
||||
return this.buildAuthResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an accounting request
|
||||
*/
|
||||
public async accounting(request: IClientAccountingRequest): Promise<IClientAccountingResponse> {
|
||||
await this.ensureConnected();
|
||||
|
||||
const identifier = this.nextIdentifier();
|
||||
const attributes: Array<{ type: number | string; value: string | number | Buffer }> = [];
|
||||
|
||||
// Add required attributes
|
||||
attributes.push({ type: ERadiusAttributeType.AcctStatusType, value: request.statusType });
|
||||
attributes.push({ type: ERadiusAttributeType.AcctSessionId, value: request.sessionId });
|
||||
|
||||
// Add NAS identification
|
||||
if (this.options.nasIpAddress && this.options.nasIpAddress !== '0.0.0.0') {
|
||||
attributes.push({ type: ERadiusAttributeType.NasIpAddress, value: this.options.nasIpAddress });
|
||||
}
|
||||
if (this.options.nasIdentifier) {
|
||||
attributes.push({ type: ERadiusAttributeType.NasIdentifier, value: this.options.nasIdentifier });
|
||||
}
|
||||
|
||||
// Add optional attributes
|
||||
if (request.username) {
|
||||
attributes.push({ type: ERadiusAttributeType.UserName, value: request.username });
|
||||
}
|
||||
if (request.nasPort !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.NasPort, value: request.nasPort });
|
||||
}
|
||||
if (request.nasPortType !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.NasPortType, value: request.nasPortType });
|
||||
}
|
||||
if (request.sessionTime !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.AcctSessionTime, value: request.sessionTime });
|
||||
}
|
||||
if (request.inputOctets !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.AcctInputOctets, value: request.inputOctets });
|
||||
}
|
||||
if (request.outputOctets !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.AcctOutputOctets, value: request.outputOctets });
|
||||
}
|
||||
if (request.inputPackets !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.AcctInputPackets, value: request.inputPackets });
|
||||
}
|
||||
if (request.outputPackets !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.AcctOutputPackets, value: request.outputPackets });
|
||||
}
|
||||
if (request.terminateCause !== undefined) {
|
||||
attributes.push({ type: ERadiusAttributeType.AcctTerminateCause, value: request.terminateCause });
|
||||
}
|
||||
if (request.calledStationId) {
|
||||
attributes.push({ type: ERadiusAttributeType.CalledStationId, value: request.calledStationId });
|
||||
}
|
||||
if (request.callingStationId) {
|
||||
attributes.push({ type: ERadiusAttributeType.CallingStationId, value: request.callingStationId });
|
||||
}
|
||||
|
||||
// Add custom attributes
|
||||
if (request.customAttributes) {
|
||||
attributes.push(...request.customAttributes);
|
||||
}
|
||||
|
||||
// Create packet
|
||||
const packet = RadiusPacket.createAccountingRequest(identifier, this.options.secret, attributes);
|
||||
|
||||
// Extract request authenticator from packet
|
||||
const requestAuthenticator = packet.subarray(4, 20);
|
||||
|
||||
// Send and wait for response
|
||||
const responseBuffer = await this.sendRequest(
|
||||
identifier,
|
||||
packet,
|
||||
requestAuthenticator,
|
||||
this.options.acctPort
|
||||
);
|
||||
|
||||
// Verify response authenticator
|
||||
if (!RadiusAuthenticator.verifyResponseAuthenticator(
|
||||
responseBuffer,
|
||||
requestAuthenticator,
|
||||
this.options.secret
|
||||
)) {
|
||||
throw new Error('Invalid response authenticator');
|
||||
}
|
||||
|
||||
// Parse response
|
||||
const response = RadiusPacket.decodeAndParse(responseBuffer);
|
||||
|
||||
return {
|
||||
success: response.code === ERadiusCode.AccountingResponse,
|
||||
attributes: response.parsedAttributes,
|
||||
rawPacket: response,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send accounting start
|
||||
*/
|
||||
public async accountingStart(sessionId: string, username?: string): Promise<IClientAccountingResponse> {
|
||||
const { EAcctStatusType } = await import('../ts_server/index.js');
|
||||
return this.accounting({
|
||||
statusType: EAcctStatusType.Start,
|
||||
sessionId,
|
||||
username,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send accounting stop
|
||||
*/
|
||||
public async accountingStop(
|
||||
sessionId: string,
|
||||
options?: {
|
||||
username?: string;
|
||||
sessionTime?: number;
|
||||
inputOctets?: number;
|
||||
outputOctets?: number;
|
||||
terminateCause?: number;
|
||||
}
|
||||
): Promise<IClientAccountingResponse> {
|
||||
const { EAcctStatusType } = await import('../ts_server/index.js');
|
||||
return this.accounting({
|
||||
statusType: EAcctStatusType.Stop,
|
||||
sessionId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send accounting interim update
|
||||
*/
|
||||
public async accountingUpdate(
|
||||
sessionId: string,
|
||||
options?: {
|
||||
username?: string;
|
||||
sessionTime?: number;
|
||||
inputOctets?: number;
|
||||
outputOctets?: number;
|
||||
}
|
||||
): Promise<IClientAccountingResponse> {
|
||||
const { EAcctStatusType } = await import('../ts_server/index.js');
|
||||
return this.accounting({
|
||||
statusType: EAcctStatusType.InterimUpdate,
|
||||
sessionId,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureConnected(): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
await this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
private nextIdentifier(): number {
|
||||
this.currentIdentifier = (this.currentIdentifier + 1) % 256;
|
||||
return this.currentIdentifier;
|
||||
}
|
||||
|
||||
private sendRequest(
|
||||
identifier: number,
|
||||
packet: Buffer,
|
||||
requestAuthenticator: Buffer,
|
||||
port: number
|
||||
): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const pending: IPendingRequest = {
|
||||
identifier,
|
||||
requestAuthenticator,
|
||||
resolve,
|
||||
reject,
|
||||
retries: 0,
|
||||
packet,
|
||||
port,
|
||||
};
|
||||
|
||||
this.pendingRequests.set(identifier, pending);
|
||||
this.sendWithRetry(pending);
|
||||
});
|
||||
}
|
||||
|
||||
private sendWithRetry(pending: IPendingRequest): void {
|
||||
// Send packet
|
||||
this.socket!.send(pending.packet, pending.port, this.options.host, (err) => {
|
||||
if (err) {
|
||||
this.pendingRequests.delete(pending.identifier);
|
||||
pending.reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set timeout
|
||||
pending.timeoutId = setTimeout(() => {
|
||||
pending.retries++;
|
||||
if (pending.retries >= this.options.retries) {
|
||||
this.pendingRequests.delete(pending.identifier);
|
||||
pending.reject(new Error(`Request timed out after ${this.options.retries} retries`));
|
||||
return;
|
||||
}
|
||||
|
||||
// Exponential backoff
|
||||
const delay = this.options.retryDelay * Math.pow(2, pending.retries - 1);
|
||||
setTimeout(() => {
|
||||
if (this.pendingRequests.has(pending.identifier)) {
|
||||
this.sendWithRetry(pending);
|
||||
}
|
||||
}, delay);
|
||||
}, this.options.timeout);
|
||||
});
|
||||
}
|
||||
|
||||
private handleResponse(msg: Buffer): void {
|
||||
if (msg.length < 20) {
|
||||
return;
|
||||
}
|
||||
|
||||
const identifier = msg.readUInt8(1);
|
||||
const pending = this.pendingRequests.get(identifier);
|
||||
|
||||
if (!pending) {
|
||||
// Response for unknown request
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear timeout
|
||||
if (pending.timeoutId) {
|
||||
clearTimeout(pending.timeoutId);
|
||||
}
|
||||
|
||||
// Remove from pending
|
||||
this.pendingRequests.delete(identifier);
|
||||
|
||||
// Resolve with response
|
||||
pending.resolve(msg);
|
||||
}
|
||||
|
||||
private buildAuthResponse(packet: any): IClientAuthResponse {
|
||||
const code = packet.code as ERadiusCode;
|
||||
const accepted = code === ERadiusCode.AccessAccept;
|
||||
const rejected = code === ERadiusCode.AccessReject;
|
||||
const challenged = code === ERadiusCode.AccessChallenge;
|
||||
|
||||
// Extract common attributes
|
||||
let replyMessage: string | undefined;
|
||||
let sessionTimeout: number | undefined;
|
||||
let idleTimeout: number | undefined;
|
||||
let state: Buffer | undefined;
|
||||
let classAttr: Buffer | undefined;
|
||||
let framedIpAddress: string | undefined;
|
||||
let framedIpNetmask: string | undefined;
|
||||
const framedRoutes: string[] = [];
|
||||
|
||||
for (const attr of packet.parsedAttributes) {
|
||||
switch (attr.type) {
|
||||
case ERadiusAttributeType.ReplyMessage:
|
||||
replyMessage = attr.value as string;
|
||||
break;
|
||||
case ERadiusAttributeType.SessionTimeout:
|
||||
sessionTimeout = attr.value as number;
|
||||
break;
|
||||
case ERadiusAttributeType.IdleTimeout:
|
||||
idleTimeout = attr.value as number;
|
||||
break;
|
||||
case ERadiusAttributeType.State:
|
||||
state = attr.rawValue;
|
||||
break;
|
||||
case ERadiusAttributeType.Class:
|
||||
classAttr = attr.rawValue;
|
||||
break;
|
||||
case ERadiusAttributeType.FramedIpAddress:
|
||||
framedIpAddress = attr.value as string;
|
||||
break;
|
||||
case ERadiusAttributeType.FramedIpNetmask:
|
||||
framedIpNetmask = attr.value as string;
|
||||
break;
|
||||
case ERadiusAttributeType.FramedRoute:
|
||||
framedRoutes.push(attr.value as string);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
accepted,
|
||||
rejected,
|
||||
challenged,
|
||||
replyMessage,
|
||||
sessionTimeout,
|
||||
idleTimeout,
|
||||
state,
|
||||
class: classAttr,
|
||||
framedIpAddress,
|
||||
framedIpNetmask,
|
||||
framedRoutes: framedRoutes.length > 0 ? framedRoutes : undefined,
|
||||
attributes: packet.parsedAttributes,
|
||||
rawPacket: packet,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default RadiusClient;
|
||||
4
ts_client/index.ts
Normal file
4
ts_client/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// RADIUS Client Module
|
||||
|
||||
export * from './interfaces.js';
|
||||
export { RadiusClient } from './classes.radiusclient.js';
|
||||
96
ts_client/interfaces.ts
Normal file
96
ts_client/interfaces.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* RADIUS Client Interfaces
|
||||
*/
|
||||
|
||||
import type {
|
||||
ERadiusCode,
|
||||
IRadiusPacket,
|
||||
IParsedAttribute,
|
||||
ENasPortType,
|
||||
EServiceType,
|
||||
EAcctStatusType,
|
||||
} from '../ts_shared/index.js';
|
||||
|
||||
// Re-export all shared types for backwards compatibility
|
||||
export * from '../ts_shared/index.js';
|
||||
|
||||
/**
|
||||
* RADIUS Client options
|
||||
*/
|
||||
export interface IRadiusClientOptions {
|
||||
host: string;
|
||||
authPort?: number;
|
||||
acctPort?: number;
|
||||
secret: string;
|
||||
timeout?: number;
|
||||
retries?: number;
|
||||
retryDelay?: number;
|
||||
nasIpAddress?: string;
|
||||
nasIdentifier?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication request for the client
|
||||
*/
|
||||
export interface IClientAuthRequest {
|
||||
username: string;
|
||||
password?: string; // For PAP
|
||||
chapPassword?: Buffer; // For CHAP (CHAP Ident + Response)
|
||||
chapChallenge?: Buffer; // For CHAP
|
||||
nasPort?: number;
|
||||
nasPortType?: ENasPortType;
|
||||
serviceType?: EServiceType;
|
||||
calledStationId?: string;
|
||||
callingStationId?: string;
|
||||
state?: Buffer; // For multi-round authentication
|
||||
customAttributes?: Array<{ type: number | string; value: string | number | Buffer }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication response from the server
|
||||
*/
|
||||
export interface IClientAuthResponse {
|
||||
code: ERadiusCode;
|
||||
accepted: boolean;
|
||||
rejected: boolean;
|
||||
challenged: boolean;
|
||||
replyMessage?: string;
|
||||
sessionTimeout?: number;
|
||||
idleTimeout?: number;
|
||||
state?: Buffer;
|
||||
class?: Buffer;
|
||||
framedIpAddress?: string;
|
||||
framedIpNetmask?: string;
|
||||
framedRoutes?: string[];
|
||||
attributes: IParsedAttribute[];
|
||||
rawPacket: IRadiusPacket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounting request for the client
|
||||
*/
|
||||
export interface IClientAccountingRequest {
|
||||
statusType: EAcctStatusType;
|
||||
sessionId: string;
|
||||
username?: string;
|
||||
nasPort?: number;
|
||||
nasPortType?: ENasPortType;
|
||||
sessionTime?: number;
|
||||
inputOctets?: number;
|
||||
outputOctets?: number;
|
||||
inputPackets?: number;
|
||||
outputPackets?: number;
|
||||
terminateCause?: number;
|
||||
calledStationId?: string;
|
||||
callingStationId?: string;
|
||||
customAttributes?: Array<{ type: number | string; value: string | number | Buffer }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounting response from the server
|
||||
*/
|
||||
export interface IClientAccountingResponse {
|
||||
success: boolean;
|
||||
attributes: IParsedAttribute[];
|
||||
rawPacket: IRadiusPacket;
|
||||
}
|
||||
13
ts_client/plugins.ts
Normal file
13
ts_client/plugins.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as crypto from 'crypto';
|
||||
import * as dgram from 'dgram';
|
||||
|
||||
// Import from smartpromise for deferred promises
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
|
||||
export {
|
||||
crypto,
|
||||
dgram,
|
||||
smartpromise,
|
||||
smartdelay,
|
||||
};
|
||||
151
ts_client/readme.md
Normal file
151
ts_client/readme.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# @push.rocks/smartradius/client
|
||||
|
||||
> 📱 RADIUS Client Implementation - Connect to RADIUS servers with PAP, CHAP, and accounting support
|
||||
|
||||
## Overview
|
||||
|
||||
This module provides a RADIUS client implementation for connecting to RADIUS servers. It supports PAP and CHAP authentication methods, accounting operations, and includes automatic retry with exponential backoff.
|
||||
|
||||
## Features
|
||||
|
||||
- ✅ **PAP Authentication** - Password Authentication Protocol
|
||||
- ✅ **CHAP Authentication** - Challenge-Handshake Authentication Protocol
|
||||
- ✅ **Accounting** - Session start, stop, and interim updates
|
||||
- ✅ **Automatic Retries** - Configurable retry count with exponential backoff
|
||||
- ✅ **Timeout Handling** - Per-request timeouts
|
||||
- ✅ **Custom Attributes** - Support for adding custom RADIUS attributes
|
||||
- ✅ **Response Validation** - Authenticator verification for security
|
||||
|
||||
## Exports
|
||||
|
||||
### Classes
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `RadiusClient` | Main client class for RADIUS operations |
|
||||
|
||||
### Interfaces (Client-Specific)
|
||||
|
||||
| Interface | Description |
|
||||
|-----------|-------------|
|
||||
| `IRadiusClientOptions` | Client configuration options |
|
||||
| `IClientAuthRequest` | Authentication request parameters |
|
||||
| `IClientAuthResponse` | Authentication response from server |
|
||||
| `IClientAccountingRequest` | Accounting request parameters |
|
||||
| `IClientAccountingResponse` | Accounting response from server |
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
```typescript
|
||||
import { RadiusClient } from '@push.rocks/smartradius';
|
||||
|
||||
const client = new RadiusClient({
|
||||
host: '192.168.1.1',
|
||||
secret: 'shared-secret',
|
||||
timeout: 5000,
|
||||
retries: 3,
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
// PAP Authentication
|
||||
const papResult = await client.authenticatePap('username', 'password');
|
||||
if (papResult.accepted) {
|
||||
console.log('Login successful!');
|
||||
console.log('Session timeout:', papResult.sessionTimeout);
|
||||
}
|
||||
|
||||
// CHAP Authentication
|
||||
const chapResult = await client.authenticateChap('username', 'password');
|
||||
if (chapResult.accepted) {
|
||||
console.log('CHAP login successful!');
|
||||
}
|
||||
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
### Accounting
|
||||
|
||||
```typescript
|
||||
import { RadiusClient, EAcctStatusType } from '@push.rocks/smartradius';
|
||||
|
||||
const client = new RadiusClient({
|
||||
host: '192.168.1.1',
|
||||
secret: 'shared-secret',
|
||||
});
|
||||
|
||||
await client.connect();
|
||||
|
||||
// Session start
|
||||
await client.accountingStart('session-123', 'username');
|
||||
|
||||
// Interim update
|
||||
await client.accountingUpdate('session-123', {
|
||||
username: 'username',
|
||||
sessionTime: 300,
|
||||
inputOctets: 1024000,
|
||||
outputOctets: 2048000,
|
||||
});
|
||||
|
||||
// Session stop
|
||||
await client.accountingStop('session-123', {
|
||||
username: 'username',
|
||||
sessionTime: 600,
|
||||
inputOctets: 2048000,
|
||||
outputOctets: 4096000,
|
||||
terminateCause: 1, // User-Request
|
||||
});
|
||||
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
### Custom Attributes
|
||||
|
||||
```typescript
|
||||
const result = await client.authenticate({
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
nasPort: 1,
|
||||
calledStationId: 'AA-BB-CC-DD-EE-FF',
|
||||
callingStationId: '11-22-33-44-55-66',
|
||||
customAttributes: [
|
||||
{ type: 'Service-Type', value: 2 }, // Framed
|
||||
{ type: 26, value: Buffer.from('vendor-data') }, // VSA
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## Client Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `host` | string | *required* | RADIUS server address |
|
||||
| `authPort` | number | 1812 | Authentication port |
|
||||
| `acctPort` | number | 1813 | Accounting port |
|
||||
| `secret` | string | *required* | Shared secret |
|
||||
| `timeout` | number | 5000 | Request timeout (ms) |
|
||||
| `retries` | number | 3 | Number of retries |
|
||||
| `retryDelay` | number | 1000 | Base delay between retries (ms) |
|
||||
| `nasIpAddress` | string | '0.0.0.0' | NAS-IP-Address attribute |
|
||||
| `nasIdentifier` | string | 'smartradius-client' | NAS-Identifier attribute |
|
||||
|
||||
## Response Properties
|
||||
|
||||
### IClientAuthResponse
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `code` | ERadiusCode | Response packet code |
|
||||
| `accepted` | boolean | True if Access-Accept |
|
||||
| `rejected` | boolean | True if Access-Reject |
|
||||
| `challenged` | boolean | True if Access-Challenge |
|
||||
| `replyMessage` | string | Reply-Message attribute |
|
||||
| `sessionTimeout` | number | Session-Timeout in seconds |
|
||||
| `framedIpAddress` | string | Assigned IP address |
|
||||
| `attributes` | IParsedAttribute[] | All response attributes |
|
||||
|
||||
## Re-exports
|
||||
|
||||
This module re-exports all types from `ts_shared` for convenience.
|
||||
1
ts_client/tspublish.json
Normal file
1
ts_client/tspublish.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "order": 3 }
|
||||
Reference in New Issue
Block a user