///|
/// A transport-independent RADIUS request dispatcher.
///
/// This is intentionally a small protocol server core: it owns packet
/// validation, policy dispatch, and response signing, while the application
/// owns sockets, users, persistence, and logging.
pub enum ServerResult {
Response(Packet)
Drop(Array[String])
}
///|
pub struct ServerConfig {
secret : Bytes
access_policy : AccessPolicy
accounting_enabled : Bool
reject_malformed : Bool
}
///|
pub fn ServerConfig::new(secret : Bytes, policy : AccessPolicy) -> ServerConfig {
ServerConfig::{
secret,
access_policy: policy,
accounting_enabled: true,
reject_malformed: true,
}
}
///|
pub fn ServerConfig::without_accounting(self : ServerConfig) -> ServerConfig {
ServerConfig::{
secret: self.secret,
access_policy: self.access_policy,
accounting_enabled: false,
reject_malformed: self.reject_malformed,
}
}
///|
pub fn ServerConfig::allow_malformed(self : ServerConfig) -> ServerConfig {
ServerConfig::{
secret: self.secret,
access_policy: self.access_policy,
accounting_enabled: self.accounting_enabled,
reject_malformed: false,
}
}
///|
pub struct RadiusServer {
config : ServerConfig
mut handled : Int
mut rejected : Int
mut accounting : Int
}
///|
pub fn RadiusServer::new(config : ServerConfig) -> RadiusServer {
RadiusServer::{ config, handled: 0, rejected: 0, accounting: 0, }
}
///|
pub fn RadiusServer::handled_count(self : RadiusServer) -> Int {
self.handled
}
///|
pub fn RadiusServer::rejected_count(self : RadiusServer) -> Int {
self.rejected
}
///|
pub fn RadiusServer::accounting_count(self : RadiusServer) -> Int {
self.accounting
}
///|
/// Process one already-decoded request.
pub fn RadiusServer::handle(
self : RadiusServer,
packet : Packet,
) -> ServerResult raise RadiusError {
self.handled = self.handled + 1
let diagnostics = packet.diagnostics()
if diagnostics.length() > 0 && self.config.reject_malformed {
self.rejected = self.rejected + 1
return Drop(["malformed packet rejected before policy evaluation"])
}
match packet.header.code {
AccessRequest => {
let decision = decide_access(
packet,
self.config.access_policy,
self.config.secret,
)
Response(access_response(packet, decision, self.config.secret))
}
AccountingRequest =>
if !self.config.accounting_enabled {
self.rejected = self.rejected + 1
Drop(["accounting is disabled"])
} else {
let errors = validate_accounting_request(packet)
if errors.length() > 0 {
self.rejected = self.rejected + 1
Drop(errors)
} else {
self.accounting = self.accounting + 1
Response(accounting_response(packet, self.config.secret))
}
}
_ => {
self.rejected = self.rejected + 1
Drop(["unsupported request code"])
}
}
}
///|
/// Decode and dispatch a datagram.
pub fn RadiusServer::handle_bytes(
self : RadiusServer,
wire : Bytes,
) -> ServerResult raise RadiusError {
match decode(wire) {
packet => self.handle(packet)
}
}
///|
/// Encode the response from a server result, if one exists.
pub fn ServerResult::encode(self : ServerResult) -> Bytes? raise RadiusError {
match self {
Response(packet) => Some(encode(packet))
Drop(_) => None
}
}
///|
/// Run a complete request/response exchange through the memory transport.
pub fn serve_once(
server : RadiusServer,
transport : MemoryTransport,
) -> ServerResult raise RadiusError {
let request = transport.receive() catch {
_ => raise Unsupported("transport receive failed")
}
let result = server.handle(request)
match result {
Response(response) => {
transport.send(response) catch {
_ => raise Unsupported("transport send failed")
}
result
}
Drop(_) => result
}
}