1#![warn(missing_docs)]
6
7use std::fmt;
8use std::sync::Arc;
9
10use futures::lock::Mutex;
11use futures::{AsyncRead, AsyncWrite};
12use rand::Rng;
13
14use tacacs_plus_protocol::Arguments;
15use tacacs_plus_protocol::{authentication, authorization};
16use tacacs_plus_protocol::{AuthenticationContext, AuthenticationService};
17use tacacs_plus_protocol::{HeaderInfo, MajorVersion, MinorVersion, Version};
18use tacacs_plus_protocol::{Packet, PacketFlags};
19
20mod inner;
21pub use inner::{ConnectionFactory, ConnectionFuture};
22
23mod response;
24pub use response::{
25 AccountingResponse, AuthenticationResponse, AuthorizationResponse, ResponseStatus,
26};
27
28mod context;
29pub use context::{ContextBuilder, SessionContext};
30
31mod error;
32pub use error::ClientError;
33
34mod task;
35pub use task::AccountingTask;
36
37pub use tacacs_plus_protocol as protocol;
39pub use tacacs_plus_protocol::{Argument, AuthenticationMethod, FieldText};
40
41#[derive(Clone)]
43pub struct Client<S> {
44 inner: Arc<Mutex<inner::ClientInner<S>>>,
46
47 secret: Option<Vec<u8>>,
49}
50
51#[non_exhaustive]
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum AuthenticationType {
58 Pap,
60 Chap,
62}
63
64impl<S: AsyncRead + AsyncWrite + Unpin> Client<S> {
65 pub fn new<K: AsRef<[u8]>>(
88 connection_factory: ConnectionFactory<S>,
89 secret: Option<K>,
90 ) -> Self {
91 let inner = inner::ClientInner::new(connection_factory);
92
93 Self {
94 inner: Arc::new(Mutex::new(inner)),
95 secret: secret.map(|s| s.as_ref().to_owned()),
96 }
97 }
98
99 fn make_header(&self, sequence_number: u8, minor_version: MinorVersion) -> HeaderInfo {
100 let session_id: u32 = rand::thread_rng().gen();
103
104 let flags = if self.secret.is_some() {
106 PacketFlags::SINGLE_CONNECTION
107 } else {
108 PacketFlags::SINGLE_CONNECTION | PacketFlags::UNENCRYPTED
109 };
110
111 HeaderInfo::new(
112 Version::new(MajorVersion::RFC8907, minor_version),
113 sequence_number,
114 flags,
115 session_id,
116 )
117 }
118
119 fn pap_login_start_packet<'packet>(
120 &self,
121 context: &'packet SessionContext,
122 password: &'packet str,
123 ) -> Result<Packet<authentication::Start<'packet>>, ClientError> {
124 use protocol::authentication::BadStart;
125
126 Ok(Packet::new(
127 self.make_header(1, MinorVersion::V1),
130 authentication::Start::new(
131 authentication::Action::Login,
132 AuthenticationContext {
133 privilege_level: context.privilege_level,
134 authentication_type: protocol::AuthenticationType::Pap,
135 service: AuthenticationService::Login,
136 },
137 context.as_user_information()?,
138 Some(password.as_bytes().try_into()?),
139 )
140 .map_err(|err| match err {
141 BadStart::AuthTypeNotSet | BadStart::IncompatibleActionAndType => unreachable!(),
143 _ => ClientError::InvalidPacketData,
145 })?,
146 ))
147 }
148
149 fn chap_login_start_packet<'packet>(
150 &self,
151 context: &'packet SessionContext,
152 password: &'packet str,
153 ) -> Result<Packet<authentication::Start<'packet>>, ClientError> {
154 use md5::{Digest, Md5};
155 use protocol::authentication::BadStart;
156
157 let ppp_id: u8 = rand::thread_rng().gen();
159 let challenge = uuid::Uuid::new_v4();
160
161 let mut hasher = Md5::new();
168 hasher.update([ppp_id]);
169 hasher.update(password.as_bytes()); hasher.update(challenge);
171 let response = hasher.finalize();
172
173 let mut data = vec![ppp_id];
176 data.extend(challenge.as_bytes());
177 data.extend(response);
178
179 Ok(Packet::new(
180 self.make_header(1, MinorVersion::V1),
181 authentication::Start::new(
182 authentication::Action::Login,
183 AuthenticationContext {
184 privilege_level: context.privilege_level,
185 authentication_type: protocol::AuthenticationType::Chap,
186 service: AuthenticationService::Login,
187 },
188 context.as_user_information()?,
189 Some(data.try_into()?),
190 )
191 .map_err(|err| match err {
192 BadStart::AuthTypeNotSet | BadStart::IncompatibleActionAndType => unreachable!(),
194 _ => ClientError::InvalidPacketData,
195 })?,
196 ))
197 }
198
199 pub async fn authenticate(
201 &self,
202 context: SessionContext,
203 password: &str,
204 authentication_type: AuthenticationType,
205 ) -> Result<AuthenticationResponse, ClientError> {
206 use protocol::authentication::ReplyOwned;
207
208 let start_packet = match authentication_type {
209 AuthenticationType::Pap => self.pap_login_start_packet(&context, password),
210 AuthenticationType::Chap => self.chap_login_start_packet(&context, password),
211 }?;
212
213 let reply = {
215 let secret_key = self.secret.as_deref();
216
217 let mut inner = self.inner.lock().await;
218 inner.send_packet(start_packet, secret_key).await?;
219
220 let reply = inner.receive_packet::<ReplyOwned>(secret_key, 2).await?;
222
223 inner.set_internal_single_connect_status(reply.header());
224 inner
225 .post_session_cleanup(reply.body().status == authentication::Status::Error)
226 .await?;
227
228 reply
229 };
230
231 let reply_status = ResponseStatus::try_from(reply.body().status);
232 let user_message = reply.body().server_message.clone();
233 let data = reply.body().data.clone();
234
235 match reply_status {
236 Ok(status) => Ok(AuthenticationResponse {
237 status,
238 user_message,
239 data,
240 }),
241 Err(response::BadAuthenticationStatus(status)) => {
242 Err(ClientError::AuthenticationError {
243 status,
244 data,
245 user_message,
246 })
247 }
248 }
249 }
250
251 pub async fn authorize(
257 &self,
258 context: SessionContext,
259 arguments: Vec<Argument<'_>>,
260 ) -> Result<AuthorizationResponse, ClientError> {
261 use authorization::ReplyOwned;
262
263 let request_packet = Packet::new(
264 self.make_header(1, MinorVersion::Default),
266 authorization::Request::new(
267 context.authentication_method(),
268 AuthenticationContext {
269 privilege_level: context.privilege_level,
270 authentication_type: protocol::AuthenticationType::NotSet,
271 service: AuthenticationService::Login,
273 },
274 context.as_user_information()?,
275 Arguments::new(&arguments).ok_or(ClientError::TooManyArguments)?,
276 ),
277 );
278
279 let reply = {
281 let secret_key = self.secret.as_deref();
282
283 let mut inner = self.inner.lock().await;
284 inner.send_packet(request_packet, secret_key).await?;
285
286 let reply: Packet<ReplyOwned> = inner.receive_packet(secret_key, 2).await?;
287
288 inner.set_internal_single_connect_status(reply.header());
290 inner
291 .post_session_cleanup(reply.body().status == authorization::Status::Error)
292 .await?;
293
294 reply
295 };
296
297 let packet_status = reply.body().status;
298 let user_message = reply.body().server_message.clone();
299 let admin_message = reply.body().data.clone();
300
301 match ResponseStatus::try_from(packet_status) {
302 Ok(status) => {
303 let owned_arguments = arguments.into_iter().map(Argument::into_owned).collect();
304
305 let merged_arguments = merge_authorization_arguments(
306 packet_status == authorization::Status::PassReplace,
307 owned_arguments,
308 reply.body().arguments.clone(),
309 );
310
311 Ok(AuthorizationResponse {
312 status,
313 arguments: merged_arguments,
314 user_message,
315 admin_message,
316 })
317 }
318 Err(response::BadAuthorizationStatus(status)) => Err(ClientError::AuthorizationError {
319 status,
320 user_message,
321 admin_message,
322 }),
323 }
324 }
325
326 pub async fn account_begin<'args, A: AsRef<[Argument<'args>]>>(
337 &self,
338 context: SessionContext,
339 arguments: A,
340 ) -> Result<(AccountingTask<&Self>, AccountingResponse), ClientError> {
341 AccountingTask::start(self, context, arguments).await
342 }
343}
344
345impl<S: fmt::Debug> fmt::Debug for Client<S> {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 let inner_debug = match self.inner.try_lock() {
349 Some(inner) => format!("{inner:?}"),
350 None => String::from("(locked)"),
351 };
352
353 f.debug_struct("Client")
355 .field("inner", &inner_debug)
356 .finish_non_exhaustive()
357 }
358}
359
360fn merge_authorization_arguments(
365 replacing: bool,
366 mut sent_arguments: Vec<Argument<'static>>,
367 mut received_arguments: Vec<Argument<'static>>,
368) -> Vec<Argument<'static>> {
369 if replacing {
370 for received in received_arguments.into_iter() {
371 if let Some(sent) = sent_arguments
372 .iter_mut()
373 .find(|arg| arg.name() == received.name())
374 {
375 sent.set_value(received.value().clone());
376 } else {
377 sent_arguments.push(received);
378 }
379 }
380 } else {
381 sent_arguments.append(&mut received_arguments);
382 }
383 sent_arguments
384}