remitano_api/
lib.rs

1use std::time::{Duration, SystemTime};
2
3use derive_builder::Builder;
4use hmac::{Hmac, Mac};
5use hyperx::header::HttpDate;
6use md5::{Digest, Md5};
7use reqwest::header::{HeaderMap, ACCEPT, AUTHORIZATION, CONTENT_TYPE, DATE, USER_AGENT};
8use serde::de::DeserializeOwned;
9use serde_json::{Map, Value};
10use sha1::Sha1;
11
12const API_URL: &str = "https://api.remitano.com";
13
14type HmacSha1 = Hmac<Sha1>;
15
16#[derive(Default, Builder, Debug)]
17pub struct RemitanoApi {
18    pub key: String,
19
20    pub secret: String,
21
22    #[builder(default = r#"API_URL.to_string()"#)]
23    pub api_url: String,
24
25    #[builder(default = "3000")]
26    pub timeout_ms: u64,
27}
28
29pub use reqwest::Method;
30
31impl RemitanoApi {
32    fn hmac(&self, data: &Option<Value>) -> anyhow::Result<String> {
33        let value = match data {
34            Some(data) => match data {
35                Value::String(data) => data.as_bytes().to_vec(),
36                _ => serde_json::to_vec(&data)?,
37            },
38            None => vec![],
39        };
40
41        let mut mac = HmacSha1::new_from_slice(self.secret.as_bytes())?;
42        mac.update(&value);
43        let result = mac.finalize().into_bytes();
44
45        Ok(base64::encode(result))
46    }
47
48    fn md5(&self, data: &Option<Value>) -> anyhow::Result<String> {
49        let value = match data {
50            Some(data) => match data {
51                Value::String(data) => data.as_bytes().to_vec(),
52                _ => serde_json::to_vec(&data)?,
53            },
54            None => vec![],
55        };
56
57        let mut hasher = Md5::new();
58        hasher.update(&value);
59        let result = hasher.finalize();
60
61        Ok(base64::encode(result))
62    }
63
64    pub async fn request<T: DeserializeOwned>(
65        &self,
66        method: Method,
67        endpoint: &str,
68        params: Option<Map<String, Value>>,
69        body: Option<Value>,
70    ) -> anyhow::Result<T> {
71        let mut headers = HeaderMap::new();
72        headers.insert(
73            USER_AGENT,
74            "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:85.0) Gecko/20100101 Firefox/85.0"
75                .parse()?,
76        );
77        headers.insert(ACCEPT, "application/json".parse()?);
78        headers.insert(CONTENT_TYPE, "application/json".parse()?);
79        headers.insert("Content-MD5", self.md5(&body)?.parse()?);
80        headers.insert(DATE, HttpDate::from(SystemTime::now()).to_string().parse()?);
81
82        let query = if let Some(params) = &params {
83            format!("?{}", &serde_qs::to_string(&params)?)
84        } else {
85            "".to_string()
86        };
87
88        let request_url = format!("api/v1/{}{}", &endpoint, &query);
89        let request_str = format!(
90            "{},application/json,{},/{},{}",
91            &method,
92            headers
93                .get("Content-MD5")
94                .map_or_else(|| Some(""), |v| v.to_str().ok())
95                .unwrap(),
96            &request_url,
97            headers
98                .get(DATE)
99                .map_or_else(|| Some(""), |v| v.to_str().ok())
100                .unwrap(),
101        );
102        let sig = self.hmac(&Some(Value::String(request_str)))?;
103        headers.insert(
104            AUTHORIZATION,
105            format!("APIAuth {}:{}", &self.key, &sig).parse()?,
106        );
107
108        let client = reqwest::Client::new();
109        let resp: T = client
110            .request(method, format!("{}/{}", &self.api_url, &request_url))
111            .headers(headers)
112            .json(&body.unwrap_or_default())
113            .timeout(Duration::from_millis(self.timeout_ms))
114            .send()
115            .await?
116            .json()
117            .await?;
118
119        Ok(resp)
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use serde_json::json;
126
127    use crate::*;
128
129    #[test]
130    fn test_md5() {
131        let remitano_api = RemitanoApiBuilder::default()
132            .key("key".to_string())
133            .secret("secret".to_string())
134            .build()
135            .unwrap();
136
137        let input = "hash me";
138        let result = remitano_api.md5(&Some(json!(input))).unwrap();
139        assert_eq!("F7Mdzpa51sbQprqV9HeW+w==", result);
140    }
141
142    #[test]
143    fn test_hmac256() {
144        let remitano_api = RemitanoApiBuilder::default()
145            .key("key".to_string())
146            .secret("secret".to_string())
147            .build()
148            .unwrap();
149
150        let input = "hash me";
151        let result = remitano_api.hmac(&Some(json!(input))).unwrap();
152        assert_eq!("oSVlCBpf9BqviWbUjOm4DXEcgRo=", result);
153    }
154}