radius_rust/client/
client.rs

1//! RADIUS Generic Client implementation
2
3
4use crate::protocol::dictionary::Dictionary;
5use crate::protocol::error::RadiusError;
6use crate::protocol::host::Host;
7use crate::protocol::radius_packet::{ RadiusAttribute, RadiusPacket, RadiusMsgType, TypeCode };
8
9use hmac::{ Hmac, Mac };
10use md5::{ Digest, Md5 };
11
12
13type HmacMd5 = Hmac<Md5>;
14
15
16#[derive(Debug)]
17/// Represents RADIUS Generic Client instance
18pub struct Client {
19    host:           Host,
20    server:         String,
21    secret:         String,
22    retries:        u16,
23    timeout:        u16,
24}
25
26impl Client {
27    // === Builder for Client ===
28    /// Initialise Client instance with dictionary (other fields would be set to default values)
29    ///
30    /// To be called **first** when creating RADIUS Client instance
31    pub fn with_dictionary(dictionary: Dictionary) -> Client {
32        let host = Host::with_dictionary(dictionary);
33
34        Client {
35            host,
36            server:  String::from(""),
37            secret:  String::from(""),
38            retries: 1,
39            timeout: 2
40        }
41    }
42
43    /// **Required**
44    ///
45    /// Sets hostname to which client would attempt to send RADIUS packets
46    pub fn set_server(mut self, server: String) -> Client {
47        self.server = server;
48        self
49    }
50
51    /// **Required**
52    ///
53    /// Sets secret which is used to encode/decode RADIUS packet
54    pub fn set_secret(mut self, secret: String) -> Client {
55        self.secret = secret;
56        self
57    }
58
59    /// **Required/Optional**
60    ///
61    /// Sets remote port, that responsible for specific RADIUS Message Type
62    pub fn set_port(mut self, msg_type: RadiusMsgType, port: u16) -> Client {
63        self.host.set_port(msg_type, port);
64        self
65    }
66
67    /// **Optional**
68    ///
69    /// Sets socket retries, otherwise you would have a default value of 1
70    pub fn set_retries(mut self, retries: u16) -> Client {
71        self.retries = retries;
72        self
73    }
74
75    /// **Optional**
76    ///
77    /// Sets socket timeout, otherwise you would have a default value of 2
78    pub fn set_timeout(mut self, timeout: u16) -> Client {
79        self.timeout = timeout;
80        self
81    }
82    // ===================
83
84    /// Returns port of RADIUS server, that receives given type of RADIUS message/packet
85    pub fn port(&self, code: &TypeCode) -> Option<u16> {
86        self.host.port(code)
87    }
88
89    /// Returns hostname/FQDN of RADIUS Server
90    pub fn server(&self) -> &str {
91        &self.server
92    }
93
94    /// Returns secret
95    pub fn secret(&self) -> &str {
96        &self.secret
97    }
98
99    /// Returns retries
100    pub fn retries(&self) -> u16 {
101        self.retries
102    }
103
104    /// Returns timeout
105    pub fn timeout(&self) -> u16 {
106        self.timeout
107    }
108
109    /// Creates RADIUS packet with any TypeCode without attributes
110    ///
111    /// You would need to set attributes manually via *set_attributes()* function
112    pub fn create_packet(&self, code: TypeCode) -> RadiusPacket {
113        RadiusPacket::initialise_packet(code)
114    }
115
116    /// Creates RADIUS Access Request packet
117    ///
118    /// You would need to set attributes manually via *set_attributes()* function
119    pub fn create_auth_packet(&self) -> RadiusPacket {
120        RadiusPacket::initialise_packet(TypeCode::AccessRequest)
121    }
122
123    /// Creates RADIUS Accounting Request packet without attributes
124    ///
125    /// You would need to set attributes manually via *set_attributes()* function
126    pub fn create_acct_packet(&self) -> RadiusPacket {
127        RadiusPacket::initialise_packet(TypeCode::AccountingRequest)
128    }
129
130    /// Creates RADIUS CoA Request packet without attributes
131    ///
132    /// You would need to set attributes manually via *set_attributes()* function
133    pub fn create_coa_packet(&self) -> RadiusPacket {
134        RadiusPacket::initialise_packet(TypeCode::CoARequest)
135    }
136
137    /// Creates RADIUS packet attribute by name, that is defined in dictionary file
138    ///
139    /// # Examples
140    ///
141    /// ```
142    /// use radius_rust::client::client::Client;
143    /// use radius_rust::protocol::dictionary::Dictionary;
144    /// use radius_rust::protocol::radius_packet::TypeCode;
145    ///
146    /// fn main() {
147    ///     let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
148    ///     let client     = Client::with_dictionary(dictionary)
149    ///        .set_server(String::from("127.0.0.1"))
150    ///        .set_secret(String::from("secret"))
151    ///        .set_retries(1)
152    ///        .set_timeout(2);
153    ///
154    ///     client.create_attribute_by_name("User-Name", String::from("testing").into_bytes());
155    /// }
156    /// ```
157    pub fn create_attribute_by_name(&self, attribute_name: &str, value: Vec<u8>) -> Result<RadiusAttribute, RadiusError> {
158        self.host.create_attribute_by_name(attribute_name, value)
159    }
160
161    /// Creates RADIUS packet attribute by ID, that is defined in dictionary file
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use radius_rust::client::client::Client;
167    /// use radius_rust::protocol::dictionary::Dictionary;
168    /// use radius_rust::protocol::radius_packet::TypeCode;
169    ///
170    /// fn main() {
171    ///     let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
172    ///     let client     = Client::with_dictionary(dictionary)
173    ///        .set_server(String::from("127.0.0.1"))
174    ///        .set_secret(String::from("secret"))
175    ///        .set_retries(1)
176    ///        .set_timeout(2);
177    ///
178    ///     client.create_attribute_by_id(1, String::from("testing").into_bytes());
179    /// }
180    /// ```
181    pub fn create_attribute_by_id(&self, attribute_id: u8, value: Vec<u8>) -> Result<RadiusAttribute, RadiusError> {
182        self.host.create_attribute_by_id(attribute_id, value)
183    }
184
185    /// Generates HMAC-MD5 hash for Message-Authenticator attribute
186    ///
187    /// Note 1: this function assumes that RadiusAttribute Message-Authenticator already exists in RadiusPacket
188    /// Note 2: this function only works correctly when Message-Authenticator is set to exactly [0;16]
189    /// Note 3: this function would be removed in 0.5.0
190    #[deprecated(since="0.4.1", note="This function may work incorrectly, please use radius_packet's `generate_message_authenticator` instead")]
191    pub fn generate_message_hash(&self, packet: &mut RadiusPacket) -> Vec<u8> {
192        let mut hash = HmacMd5::new_from_slice(self.secret.as_bytes()).unwrap();
193
194        hash.update(&packet.to_bytes());
195        hash.finalize().into_bytes().to_vec()
196    }
197
198    /// Gets the original value as a String
199    ///
200    /// If the RadiusAttribute respresents dictionary attribute of type: string, ipaddr, ipv6addr or ipv6prefix
201    pub fn radius_attr_original_string_value(&self, attribute: &RadiusAttribute) -> Result<String, RadiusError> {
202        let dict_attr = self.host.dictionary_attribute_by_id(attribute.id()).ok_or_else(|| RadiusError::MalformedAttributeError {error: format!("No attribute with ID: {} found in dictionary", attribute.id())} )?;
203        attribute.original_string_value(dict_attr.code_type())
204    }
205
206    /// Gets the original value as an Integer
207    ///
208    /// If the RadiusAttribute respresents dictionary attribute of type: integer or date
209    pub fn radius_attr_original_integer_value(&self, attribute: &RadiusAttribute) -> Result<u32, RadiusError> {
210        let dict_attr = self.host.dictionary_attribute_by_id(attribute.id()).ok_or_else(|| RadiusError::MalformedAttributeError {error: format!("No attribute with ID: {} found in dictionary", attribute.id())} )?;
211        attribute.original_integer_value(dict_attr.code_type())
212    }
213
214    /// Initialises RadiusPacket from bytes
215    pub fn initialise_packet_from_bytes(&self, reply: &[u8]) -> Result<RadiusPacket, RadiusError> {
216        self.host.initialise_packet_from_bytes(reply)
217    }
218
219    /// Verifies that reply packet's ID and authenticator are a match
220    pub fn verify_reply(&self, request: &RadiusPacket, reply: &[u8]) -> Result<(), RadiusError> {
221        if reply.is_empty() {
222            return Err( RadiusError::ValidationError { error: String::from("Empty reply") } )
223        }
224        if request.id() != reply[1] {
225            return Err( RadiusError::ValidationError { error: String::from("Packet identifier mismatch") } )
226        };
227
228        let mut md5_hasher = Md5::new();
229
230        md5_hasher.update(&reply[0..4]);             // Append reply type code, reply ID and reply length
231        md5_hasher.update(&request.authenticator()); // Append request authenticator
232        md5_hasher.update(&reply[20..]);             // Append rest of the reply
233        md5_hasher.update(&self.secret.as_bytes());  // Append secret
234
235        if md5_hasher.finalize().as_slice() == &reply[4..20] {
236            Ok(())
237        } else {
238            Err( RadiusError::ValidationError { error: String::from("Packet authenticator mismatch") } )
239        }
240    }
241
242    /// Verifies that reply packet's Message-Authenticator attribute is valid
243    pub fn verify_message_authenticator(&self, packet: &[u8]) -> Result<(), RadiusError> {
244        self.host.verify_message_authenticator(&self.secret, &packet)
245    }
246
247    /// Verifies that reply packet's attributes have valid values
248    pub fn verify_packet_attributes(&self, packet: &[u8]) -> Result<(), RadiusError> {
249        self.host.verify_packet_attributes(&packet)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::tools::integer_to_bytes;
257
258    #[test]
259    fn test_get_radius_attr_original_string_value() {
260        let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
261        let client     = Client::with_dictionary(dictionary)
262            .set_server(String::from("127.0.0.1"))
263            .set_secret(String::from("secret"))
264            .set_retries(1)
265            .set_timeout(2)
266            .set_port(RadiusMsgType::AUTH, 1812)
267            .set_port(RadiusMsgType::ACCT, 1813)
268            .set_port(RadiusMsgType::COA,  3799);
269
270        let attributes = vec![client.create_attribute_by_name("User-Name", String::from("testing").into_bytes()).unwrap()];
271
272        match client.radius_attr_original_string_value(&attributes[0]) {
273            Ok(value) => assert_eq!(String::from("testing"), value),
274            _         => assert!(false)
275        }
276    }
277
278    #[test]
279    fn test_get_radius_attr_original_string_value_error() {
280        let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
281        let client     = Client::with_dictionary(dictionary)
282            .set_server(String::from("127.0.0.1"))
283            .set_secret(String::from("secret"))
284            .set_retries(1)
285            .set_timeout(2)
286            .set_port(RadiusMsgType::AUTH, 1812)
287            .set_port(RadiusMsgType::ACCT, 1813)
288            .set_port(RadiusMsgType::COA,  3799);
289
290        let invalid_string = vec![215, 189, 213, 172, 57, 94, 141, 70, 134, 121, 101, 57, 187, 220, 227, 73];
291        let attributes     = vec![client.create_attribute_by_name("User-Name", invalid_string).unwrap()];
292
293        match client.radius_attr_original_string_value(&attributes[0]) {
294            Ok(_)      => assert!(false),
295            Err(error) => assert_eq!(String::from("Attribute in Radius packet is malformed: invalid ASCII bytes"), error.to_string())
296        }
297    }
298
299    #[test]
300    fn test_get_radius_attr_original_integer_value() {
301        let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
302        let client     = Client::with_dictionary(dictionary)
303            .set_server(String::from("127.0.0.1"))
304            .set_secret(String::from("secret"))
305            .set_retries(1)
306            .set_timeout(2)
307            .set_port(RadiusMsgType::AUTH, 1812)
308            .set_port(RadiusMsgType::ACCT, 1813)
309            .set_port(RadiusMsgType::COA,  3799);
310
311        let attributes = vec![client.create_attribute_by_name("NAS-Port-Id", integer_to_bytes(0)).unwrap()];
312
313        match client.radius_attr_original_integer_value(&attributes[0]) {
314            Ok(value) => assert_eq!(0, value),
315            _         => assert!(false)
316        }
317    }
318
319    #[test]
320    fn test_get_radius_attr_original_integer_value_error() {
321        let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
322        let client     = Client::with_dictionary(dictionary)
323            .set_server(String::from("127.0.0.1"))
324            .set_secret(String::from("secret"))
325            .set_retries(1)
326            .set_timeout(2)
327            .set_port(RadiusMsgType::AUTH, 1812)
328            .set_port(RadiusMsgType::ACCT, 1813)
329            .set_port(RadiusMsgType::COA,  3799);
330
331        let invalid_integer = vec![215, 189, 213, 172, 57, 94, 141, 70, 134, 121, 101, 57, 187, 220, 227, 73];
332        let attributes      = vec![client.create_attribute_by_name("NAS-Port-Id", invalid_integer).unwrap()];
333
334        match client.radius_attr_original_integer_value(&attributes[0]) {
335            Ok(_)      => assert!(false),
336            Err(error) => assert_eq!(String::from("Attribute in Radius packet is malformed: invalid Integer bytes"), error.to_string())
337        }
338    }
339
340    #[test]
341    fn test_verify_empty_reply() {
342        let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
343        let attributes = vec![ RadiusAttribute::create_by_name(&dictionary, "Calling-Station-Id", String::from("00-01-24-80-B3-9C").into_bytes()).unwrap() ];
344
345        let client     = Client::with_dictionary(dictionary)
346            .set_server(String::from("127.0.0.1"))
347            .set_secret(String::from("secret"))
348            .set_retries(1)
349            .set_timeout(2)
350            .set_port(RadiusMsgType::AUTH, 1812)
351            .set_port(RadiusMsgType::ACCT, 1813)
352            .set_port(RadiusMsgType::COA,  3799);
353
354        let authenticator     = vec![215, 189, 213, 172, 57, 94, 141, 70, 134, 121, 101, 57, 187, 220, 227, 73];
355        let reply             = vec![];
356        let mut radius_packet = RadiusPacket::initialise_packet(TypeCode::AccountingRequest);
357
358        radius_packet.set_attributes(attributes);
359        radius_packet.override_id(43);
360        radius_packet.override_authenticator(authenticator);
361
362        match client.verify_reply(&radius_packet, &reply) {
363            Err(error) => assert_eq!(String::from("Verification failed for incoming Radius packet: Empty reply"), error.to_string()),
364            _          => assert!(false)
365        }
366    }
367
368    #[test]
369    fn test_verify_malformed_reply() {
370        let dictionary = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
371        let attributes = vec![ RadiusAttribute::create_by_name(&dictionary, "Calling-Station-Id", String::from("00-01-24-80-B3-9C").into_bytes()).unwrap() ];
372
373        let client     = Client::with_dictionary(dictionary)
374            .set_server(String::from("127.0.0.1"))
375            .set_secret(String::from("secret"))
376            .set_retries(1)
377            .set_timeout(2)
378            .set_port(RadiusMsgType::AUTH, 1812)
379            .set_port(RadiusMsgType::ACCT, 1813)
380            .set_port(RadiusMsgType::COA,  3799);
381
382        let authenticator     = vec![215, 189, 213, 172, 57, 94, 141, 70, 134, 121, 101, 57, 187, 220, 227, 73];
383        let reply             = vec![43, 215, 189, 213, 172, 57, 94, 141, 70, 134, 121, 101, 57, 187, 220, 227, 73];
384        let mut radius_packet = RadiusPacket::initialise_packet(TypeCode::AccountingRequest);
385
386        radius_packet.set_attributes(attributes);
387        radius_packet.override_id(43);
388        radius_packet.override_authenticator(authenticator);
389
390        match client.verify_reply(&radius_packet, &reply) {
391            Err(error) => assert_eq!(String::from("Verification failed for incoming Radius packet: Packet identifier mismatch"), error.to_string()),
392            _          => assert!(false)
393        }
394    }
395
396    #[test]
397    fn test_verify_reply() {
398        let dictionary    = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
399        let authenticator = vec![152, 137, 115, 14, 56, 250, 103, 56, 57, 57, 104, 246, 226, 80, 71, 167];
400        let attributes    = vec![ RadiusAttribute::create_by_name(&dictionary, "User-Name", String::from("testing").into_bytes()).unwrap() ];
401
402        let reply             = vec![2, 220, 0, 52, 165, 196, 239, 87, 197, 230, 219, 74, 148, 177, 209, 155, 35, 36, 236, 63, 6, 6, 0, 0, 0, 2, 8, 6, 192, 168, 0, 1, 97, 20, 0, 64, 252, 102, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1];
403        let mut radius_packet = RadiusPacket::initialise_packet(TypeCode::AccessRequest);
404        radius_packet.set_attributes(attributes);
405        radius_packet.override_id(220);
406        radius_packet.override_authenticator(authenticator);
407
408        let client = Client::with_dictionary(dictionary)
409            .set_server(String::from("127.0.0.1"))
410            .set_secret(String::from("secret"))
411            .set_retries(1)
412            .set_timeout(2)
413            .set_port(RadiusMsgType::AUTH, 1812)
414            .set_port(RadiusMsgType::ACCT, 1813)
415            .set_port(RadiusMsgType::COA,  3799);
416
417        match client.verify_reply(&radius_packet, &reply) {
418            Err(_error) => assert!(false),
419            _           => assert!(true)
420        }
421    }
422
423    #[test]
424    fn test_generate_message_hash() {
425        let expected_message_autthenticator = vec![85, 134, 2, 170, 83, 101, 202, 79, 109, 163, 59, 12, 66, 170, 183, 220];
426
427        let dictionary    = Dictionary::from_file("./dict_examples/integration_dict").unwrap();
428        let authenticator = vec![152, 137, 115, 14, 56, 250, 103, 56, 57, 57, 104, 246, 226, 80, 71, 167];
429        let attributes    = vec![
430            RadiusAttribute::create_by_name(&dictionary, "User-Name", String::from("testing").into_bytes()).unwrap(),
431            RadiusAttribute::create_by_name(&dictionary, "Message-Authenticator", [0;16].to_vec()).unwrap()
432        ];
433
434        let mut radius_packet = RadiusPacket::initialise_packet(TypeCode::AccessRequest);
435        radius_packet.set_attributes(attributes);
436        radius_packet.override_id(220);
437        radius_packet.override_authenticator(authenticator);
438
439        let client = Client::with_dictionary(dictionary)
440            .set_server(String::from("127.0.0.1"))
441            .set_secret(String::from("secret"))
442            .set_retries(1)
443            .set_timeout(2)
444            .set_port(RadiusMsgType::AUTH, 1812)
445            .set_port(RadiusMsgType::ACCT, 1813)
446            .set_port(RadiusMsgType::COA,  3799);
447
448        let generated_message_authenticator = client.generate_message_hash(&mut radius_packet);
449
450        assert_eq!(expected_message_autthenticator, generated_message_authenticator)
451    }
452}