Skip to main content

twapi_v2/
oauth10a.rs

1use crate::api::Authentication;
2use base64::{Engine as _, engine::general_purpose};
3use chrono::prelude::*;
4use hmac::KeyInit;
5use hmac::{Hmac, Mac};
6use rand::distr::{Alphanumeric, SampleString};
7use reqwest::RequestBuilder;
8use sha1::Sha1;
9
10type HmacSha1 = Hmac<Sha1>;
11
12pub struct OAuthAuthentication {
13    consumer_key: String,
14    consumer_secret: String,
15    access_key: String,
16    access_secret: String,
17}
18
19impl OAuthAuthentication {
20    pub fn new<T: Into<String>>(
21        consumer_key: T,
22        consumer_secret: T,
23        access_key: T,
24        access_secret: T,
25    ) -> Self {
26        Self {
27            consumer_key: consumer_key.into(),
28            consumer_secret: consumer_secret.into(),
29            access_key: access_key.into(),
30            access_secret: access_secret.into(),
31        }
32    }
33}
34
35impl Authentication for OAuthAuthentication {
36    fn execute(
37        &self,
38        builder: RequestBuilder,
39        method: &str,
40        uri: &str,
41        options: &[(&str, &str)],
42    ) -> RequestBuilder {
43        let auth = oauth1_authorization_header(
44            &self.consumer_key,
45            &self.consumer_secret,
46            &self.access_key,
47            &self.access_secret,
48            method,
49            uri,
50            &options.to_vec(),
51        );
52        builder.header(reqwest::header::AUTHORIZATION, auth)
53    }
54}
55
56fn oauth1_authorization_header(
57    consumer_key: &str,
58    consumer_secret: &str,
59    access_token: &str,
60    access_token_secret: &str,
61    method: &str,
62    uri: &str,
63    options: &Vec<(&str, &str)>,
64) -> String {
65    let res = calc_oauth_header(
66        &format!("{}&{}", consumer_secret, access_token_secret),
67        consumer_key,
68        &vec![("oauth_token", access_token)],
69        method,
70        uri,
71        options,
72    );
73    format!("OAuth {}", res)
74}
75
76fn calc_oauth_header(
77    sign_key: &str,
78    consumer_key: &str,
79    header_options: &Vec<(&str, &str)>,
80    method: &str,
81    uri: &str,
82    options: &Vec<(&str, &str)>,
83) -> String {
84    let mut param0: Vec<(&str, String)> = vec![
85        ("oauth_consumer_key", String::from(consumer_key)),
86        ("oauth_nonce", nonce()),
87        ("oauth_signature_method", String::from("HMAC-SHA1")),
88        ("oauth_timestamp", timestamp()),
89        ("oauth_version", String::from("1.0")),
90    ];
91    for header_option in header_options {
92        param0.push((header_option.0, encode(header_option.1)));
93    }
94    let mut param1 = param0.clone();
95    for option in options {
96        param1.push((option.0, encode(option.1)));
97    }
98    param1.sort();
99    let parameter = make_query(&param1, "&");
100    let base = format!("{}&{}&{}", method, encode(uri), encode(&parameter));
101    let mut param2 = param0.clone();
102    param2.push(("oauth_signature", encode(&sign(&base, sign_key))));
103    make_query(&param2, ", ")
104}
105
106fn nonce() -> String {
107    let mut rng = &mut rand::rng();
108    Alphanumeric.sample_string(&mut rng, 32)
109}
110
111fn timestamp() -> String {
112    format!("{}", Utc::now().timestamp())
113}
114
115pub fn encode(s: &str) -> String {
116    // Twitter API URL encode space is %20 not +
117    form_urlencoded::byte_serialize(s.as_bytes())
118        .collect::<String>()
119        .replace('+', "%20")
120        .replace('*', "%2A")
121        .replace("%7E", "~")
122}
123
124fn sign(base: &str, key: &str) -> String {
125    let mut mac = HmacSha1::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
126    mac.update(base.as_bytes());
127    let result = mac.finalize();
128    general_purpose::STANDARD.encode(result.into_bytes())
129}
130
131fn make_query(list: &Vec<(&str, String)>, separator: &str) -> String {
132    let mut result = String::from("");
133    for item in list {
134        if !result.is_empty() {
135            result.push_str(separator);
136        }
137        result.push_str(&format!("{}={}", item.0, item.1));
138    }
139    result
140}
141
142#[cfg(test)]
143mod tests {
144    use crate::oauth10a::oauth1_authorization_header;
145    #[test]
146    fn it_oauth2_authorization_header() {
147        println!(
148            "{}",
149            oauth1_authorization_header("a", "b", "c", "d", "GET", "http://localhost", &vec![])
150        );
151    }
152}