duo_auth/
request.rs

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(&parameters_str))
66        }
67
68        let signature = self.build_signature(skey, &parameters_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(&parameters_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}