use super::types::{RadiusCodeKind, RadiusMessage};
pub fn parse(payload: &[u8]) -> Option<RadiusMessage> {
let (_rem, data) = radius_parser::parse_radius_data(payload).ok()?;
let code = RadiusCodeKind::from_raw(data.code.0);
let mut msg = RadiusMessage {
code,
identifier: data.identifier,
authenticator_len: data.authenticator.len(),
username: None,
calling_station_id: None,
called_station_id: None,
nas_ip_address: None,
framed_ip_address: None,
has_user_password: false,
};
if let Some(attrs) = data.attributes {
for attr in attrs {
use radius_parser::RadiusAttribute as A;
match attr {
A::UserName(b) => {
msg.username = Some(String::from_utf8_lossy(b).into_owned());
}
A::UserPassword(_) => {
msg.has_user_password = true;
}
A::CallingStationId(b) => {
msg.calling_station_id = Some(String::from_utf8_lossy(b).into_owned());
}
A::CalledStationId(b) => {
msg.called_station_id = Some(String::from_utf8_lossy(b).into_owned());
}
A::NasIPAddress(ip) => {
msg.nas_ip_address = Some(ip);
}
A::FramedIPAddress(ip) => {
msg.framed_ip_address = Some(ip);
}
_ => {}
}
}
}
Some(msg)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_access_request() -> Vec<u8> {
let mut buf = Vec::new();
buf.push(1); buf.push(0x42); buf.extend_from_slice(&[0, 0]);
buf.extend_from_slice(&[0u8; 16]); buf.extend_from_slice(&[1, 7, b'a', b'l', b'i', b'c', b'e']);
buf.push(2);
buf.push(18);
buf.extend_from_slice(&[0xab; 16]);
buf.extend_from_slice(&[4, 6, 10, 0, 0, 5]);
buf.push(31);
let csi = b"00:11:22:33:44:55";
buf.push((csi.len() + 2) as u8);
buf.extend_from_slice(csi);
let total = buf.len() as u16;
buf[2..4].copy_from_slice(&total.to_be_bytes());
buf
}
#[test]
fn parses_access_request() {
let payload = sample_access_request();
let msg = parse(&payload).expect("parse");
assert_eq!(msg.code, RadiusCodeKind::AccessRequest);
assert_eq!(msg.identifier, 0x42);
assert_eq!(msg.username.as_deref(), Some("alice"));
assert!(msg.has_user_password);
assert_eq!(
msg.nas_ip_address,
Some(std::net::Ipv4Addr::new(10, 0, 0, 5))
);
assert_eq!(msg.calling_station_id.as_deref(), Some("00:11:22:33:44:55"));
assert_eq!(msg.authenticator_len, 16);
}
#[test]
fn rejects_truncated() {
assert!(parse(&[0u8; 5]).is_none());
}
#[test]
fn rejects_garbage() {
assert!(parse(b"not radius").is_none());
}
#[test]
fn code_slugs_match() {
assert_eq!(RadiusCodeKind::AccessRequest.as_str(), "access_request");
assert_eq!(RadiusCodeKind::AccessAccept.as_str(), "access_accept");
assert_eq!(RadiusCodeKind::AccessReject.as_str(), "access_reject");
assert_eq!(
RadiusCodeKind::AccountingRequest.as_str(),
"accounting_request"
);
assert_eq!(RadiusCodeKind::Other(99).as_str(), "other");
}
#[test]
fn message_with_no_attrs_still_parses() {
let mut buf = vec![1u8, 0x00];
buf.extend_from_slice(&20u16.to_be_bytes());
buf.extend_from_slice(&[0u8; 16]);
let msg = parse(&buf).expect("header-only parses");
assert_eq!(msg.code, RadiusCodeKind::AccessRequest);
assert!(msg.username.is_none());
assert!(!msg.has_user_password);
}
}