1use std::collections::BTreeMap;
2
3use chrono::{DateTime, Utc};
4use hmac::{Hmac, Mac};
5use reqwest::{Client, Method, Request, Url};
6use sha1::Sha1;
7
8use super::StdError;
9
10#[derive(Default)]
11pub struct Parameters(BTreeMap<String, String>);
12
13impl Parameters {
14 pub fn set<K: Into<String>, V: Into<String>>(&mut self, k: K, v: V) {
15 self.0.insert(k.into(), v.into());
16 }
17
18 pub fn set_opt<K: Into<String>, V: Into<String>>(&mut self, k: K, v: Option<V>) {
19 if let Some(v) = v {
20 self.set(k, v)
21 }
22 }
23
24 pub fn serialize(&self) -> String {
25 self.0
26 .iter()
27 .map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
28 .collect::<Vec<String>>()
29 .join("&")
30 }
31}
32
33impl From<Parameters> for BTreeMap<String, String> {
34 fn from(value: Parameters) -> Self {
35 value.0
36 }
37}
38
39pub struct DuoRequest {
40 url: Url,
41 method: Method,
42 path: String,
43 date: DateTime<Utc>,
44 parameters: Parameters,
45}
46
47impl DuoRequest {
48 pub fn new(url: Url, method: Method, path: impl Into<String>, parameters: Parameters) -> Self {
49 DuoRequest {
50 url,
51 method,
52 path: path.into(),
53 date: Utc::now(),
54 parameters,
55 }
56 }
57
58 pub fn build(&self, client: &Client, ikey: &str, skey: &str) -> Result<Request, StdError> {
59 let no_body = matches!(self.method, Method::GET | Method::HEAD);
60
61 let parameters_str = self.parameters.serialize();
62 let mut url = self.url.clone();
63 url.set_path(&self.path);
64 if no_body {
65 url.set_query(Some(¶meters_str))
66 }
67
68 let signature = self.build_signature(skey, ¶meters_str)?;
69 let mut rb = client
70 .request(self.method.clone(), url)
71 .basic_auth(ikey.clone(), Some(signature))
72 .header("Date", self.date.to_rfc2822());
73
74 if !no_body {
75 rb = rb
76 .header("Content-Type", "application/x-www-form-urlencoded")
77 .body(parameters_str)
78 }
79
80 rb.build().map_err(|e| e.into())
81 }
82
83 pub fn build_no_auth(&self, client: &Client) -> Result<Request, StdError> {
84 let no_body = matches!(self.method, Method::GET | Method::HEAD);
85
86 let parameters_str = self.parameters.serialize();
87 let mut url = self.url.clone();
88 url.set_path(&self.path);
89 if no_body {
90 url.set_query(Some(¶meters_str))
91 }
92
93 let mut rb = client
94 .request(self.method.clone(), url)
95 .header("Date", self.date.to_rfc2822())
96 .header(
97 "User-Agent",
98 concat!("duo-auth-rs/", env!("CARGO_PKG_VERSION")),
99 );
100
101 if !no_body {
102 rb = rb
103 .header("Content-Type", "application/x-www-form-urlencoded")
104 .body(parameters_str)
105 }
106
107 rb.build().map_err(|e| e.into())
108 }
109
110 fn build_signature(&self, skey: &str, parameters_str: &str) -> Result<String, StdError> {
111 let domain = self.url.host_str().unwrap().to_string();
112
113 let payload = &[
114 self.date.to_rfc2822(),
115 self.method.to_string().to_uppercase(),
116 domain,
117 self.path.clone(),
118 parameters_str.into(),
119 ]
120 .join("\n");
121
122 let mut signer = Hmac::<Sha1>::new_from_slice(skey.as_bytes())?;
123 signer.update(payload.as_bytes());
124
125 let signature = hex::encode(signer.finalize().into_bytes());
126
127 Ok(signature)
128 }
129}