1use std::{future::Future, sync::Arc, time::Duration};
2
3use reqwest::{Client, Method, Request, Url};
4use serde::{de::DeserializeOwned, Deserialize};
5
6use super::{
7 errors::Error,
8 request::{DuoRequest, Parameters},
9 response::DuoResponse,
10 types::PreauthResponse,
11 types::{
12 AuthRequest, AuthStatusResponse, EnrollResponse, EnrollStatusResponse, PreauthRequest,
13 },
14 StdError,
15};
16
17pub struct DuoClient(Arc<DuoClientInner>);
18
19struct DuoClientInner {
20 base_url: Url,
21 ikey: String,
22 skey: String,
23
24 client: reqwest::Client,
25}
26
27impl DuoClient {
28 pub fn new<D, I, S>(api_domain: D, ikey: I, skey: S) -> Result<DuoClient, Error>
29 where
30 D: Into<String>,
31 I: Into<String>,
32 S: Into<String>,
33 {
34 let client = reqwest::Client::builder()
35 .user_agent(concat!(
36 env!("CARGO_PKG_NAME"),
37 "/",
38 env!("CARGO_PKG_VERSION")
39 ))
40 .build()
41 .map_err(Error::unspecified)?;
42
43 Self::new_with_client(client, api_domain, ikey, skey)
44 }
45
46 pub fn new_with_client<C, D, I, S>(
47 client: C,
48 api_domain: D,
49 ikey: I,
50 skey: S,
51 ) -> Result<DuoClient, Error>
52 where
53 C: Into<Client>,
54 D: Into<String>,
55 I: Into<String>,
56 S: Into<String>,
57 {
58 let api_domain = api_domain.into();
59
60 let base_url = match Url::parse(&api_domain) {
61 Ok(url) => url,
62 Err(err) => {
63 return Err(Error::InvalidApiDomain {
64 domain: api_domain,
65 cause: err.into(),
66 })
67 }
68 };
69
70 let _ = base_url
72 .host_str()
73 .ok_or_else(|| Error::InvalidApiDomain {
74 domain: api_domain,
75 cause: "no domain in url".into(),
76 })?
77 .to_string();
78
79 Ok(DuoClient(Arc::new(DuoClientInner {
80 base_url,
81 ikey: ikey.into(),
82 skey: skey.into(),
83 client: client.into(),
84 })))
85 }
86
87 pub fn auth(&self, data: AuthRequest) -> impl Future<Output = Result<String, Error>> {
88 let this = Arc::clone(&self.0);
89
90 async move { Self::request_auth(this, data).await }
91 }
92
93 pub fn auth_status<S: Into<String>>(
94 &self,
95 tx_id: S,
96 ) -> impl Future<Output = Result<AuthStatusResponse, Error>> {
97 let this = Arc::clone(&self.0);
98
99 async move {
100 let txid: String = tx_id.into();
101 Self::request_auth_status(this, &txid).await
102 }
103 }
104
105 pub fn auth_wait(&self, data: AuthRequest) -> impl Future<Output = Result<bool, StdError>> {
106 let this = Arc::clone(&self.0);
107
108 async move {
109 let txid = Self::request_auth(this.clone(), data).await?;
110 let mut status: Option<bool>;
111
112 loop {
113 status = Self::request_auth_status(this.clone(), &txid)
114 .await?
115 .ready();
116 match status {
117 None => tokio::time::sleep(Duration::from_secs(2)).await,
118 Some(v) => return Ok(v),
119 }
120 }
121 }
122 }
123
124 pub fn check(&self) -> impl Future<Output = Result<u64, Error>> {
125 let this = Arc::clone(&self.0);
126
127 async move {
128 #[derive(Deserialize, Debug)]
129 struct CheckResponse {
130 time: u64,
131 }
132
133 let request =
134 Self::new_request(&this, Method::GET, "/auth/v2/check", Parameters::default())?;
135 Self::send_request_json::<CheckResponse>(&this.client, request)
136 .await
137 .map(|r| r.time)
138 }
139 }
140
141 pub fn enroll<U: Into<String>>(
142 &self,
143 username: Option<U>,
144 valid_secs: Option<u64>,
145 ) -> impl Future<Output = Result<EnrollResponse, Error>> {
146 let this = Arc::clone(&self.0);
147
148 async move { Self::request_enroll(this, username, valid_secs).await }
149 }
150
151 pub fn enroll_status<U: Into<String>, A: Into<String>>(
152 &self,
153 user_id: U,
154 activation_code: A,
155 ) -> impl Future<Output = Result<EnrollStatusResponse, Error>> {
156 let this = Arc::clone(&self.0);
157
158 async move { Self::request_enroll_status(this, user_id, activation_code).await }
159 }
160
161 pub fn ping(&self) -> impl Future<Output = Result<u64, Error>> {
162 let this = Arc::clone(&self.0);
163
164 async move {
165 #[derive(Deserialize, Debug)]
166 struct PingResponse {
167 time: u64,
168 }
169
170 let request =
171 Self::new_request(&this, Method::GET, "/auth/v2/ping", Parameters::default())?;
172 Self::send_request_json::<PingResponse>(&this.client, request)
173 .await
174 .map(|r| r.time)
175 }
176 }
177
178 pub fn preauth(
179 &self,
180 data: PreauthRequest,
181 ) -> impl Future<Output = Result<PreauthResponse, Error>> {
182 let this = Arc::clone(&self.0);
183
184 async move { Self::request_preauth(this, data).await }
185 }
186
187 async fn request_auth(this: Arc<DuoClientInner>, data: AuthRequest) -> Result<String, Error> {
188 let mut parameters = Parameters::default();
189 parameters.set("async", "1");
190 data.apply(&mut parameters);
191
192 #[derive(Deserialize, Debug)]
193 struct AuthResponse {
194 txid: String,
195 }
196
197 let request = Self::new_request(&this, Method::POST, "/auth/v2/auth", parameters)?;
198 Self::send_request_json::<AuthResponse>(&this.client, request)
199 .await
200 .map(|r| r.txid)
201 }
202
203 async fn request_auth_status(
204 this: Arc<DuoClientInner>,
205 tx_id: &str,
206 ) -> Result<AuthStatusResponse, Error> {
207 let mut parameters = Parameters::default();
208 parameters.set("txid", tx_id);
209
210 let request = Self::new_request(&this, Method::GET, "/auth/v2/auth_status", parameters)?;
211 Self::send_request_json(&this.client, request).await
212 }
213
214 async fn request_enroll<U: Into<String>>(
215 this: Arc<DuoClientInner>,
216 username: Option<U>,
217 valid_secs: Option<u64>,
218 ) -> Result<EnrollResponse, Error> {
219 let mut parameters = Parameters::default();
220 parameters.set_opt("username", username);
221 parameters.set_opt("valid_secs", valid_secs.map(|v| v.to_string()));
222
223 let request = Self::new_request(&this, Method::POST, "/auth/v2/enroll", parameters)?;
224 Self::send_request_json(&this.client, request).await
225 }
226
227 async fn request_enroll_status<U: Into<String>, A: Into<String>>(
228 this: Arc<DuoClientInner>,
229 user_id: U,
230 activation_code: A,
231 ) -> Result<EnrollStatusResponse, Error> {
232 let mut parameters = Parameters::default();
233 parameters.set("user_id", user_id);
234 parameters.set("activation_code", activation_code);
235
236 let request = Self::new_request(&this, Method::POST, "/auth/v2/enroll_status", parameters)?;
237 Self::send_request_json(&this.client, request).await
238 }
239
240 async fn request_preauth(
241 this: Arc<DuoClientInner>,
242 data: PreauthRequest,
243 ) -> Result<PreauthResponse, Error> {
244 let mut parameters = Parameters::default();
245 data.apply(&mut parameters);
246
247 let request = Self::new_request(&this, Method::POST, "/auth/v2/preauth", parameters)?;
248 Self::send_request_json(&this.client, request).await
249 }
250
251 fn new_request<P: Into<String>>(
252 this: &Arc<DuoClientInner>,
253 method: Method,
254 path: P,
255 parameters: Parameters,
256 ) -> Result<Request, Error> {
257 DuoRequest::new(this.base_url.clone(), method, path, parameters)
258 .build(&this.client, &this.ikey, &this.skey)
259 .map_err(Error::unspecified)
260 }
261
262 async fn send_request_json<T>(client: &Client, request: Request) -> Result<T, Error>
263 where
264 T: DeserializeOwned + std::fmt::Debug,
265 {
266 let response = client.execute(request).await.map_err(Error::unspecified)?;
267
268 let body = response
269 .json::<DuoResponse<T>>()
270 .await
271 .map_err(Error::unspecified)?;
272
273 body.ok()
274 }
275}