1#![warn(missing_docs)]
27
28use hmac::{Hmac, Mac};
29pub use http;
30use http::Method;
31use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
32use rand::distributions::Alphanumeric;
33use rand::{thread_rng, Rng};
34use sha1::Sha1;
35use std::collections::HashMap;
36use std::fmt;
37use std::hash::BuildHasher;
38use std::time::{SystemTime, UNIX_EPOCH};
39
40#[derive(Debug)]
42pub struct Credentials<'a> {
43 consumer_key: &'a str,
44 consumer_secret: &'a str,
45 token: &'a str,
46 token_secret: &'a str,
47}
48
49impl<'a> Credentials<'a> {
50 pub fn new(
52 consumer_key: &'a str,
53 consumer_secret: &'a str,
54 token: &'a str,
55 token_secret: &'a str,
56 ) -> Credentials<'a> {
57 Credentials {
58 consumer_key,
59 consumer_secret,
60 token,
61 token_secret,
62 }
63 }
64
65 pub fn auth<S: BuildHasher>(
68 &self,
69 method: &Method,
70 base_url: &str,
71 params: &HashMap<&str, &str, S>,
72 ) -> String {
73 let nonce_string: String = thread_rng().sample_iter(Alphanumeric).take(32).collect();
74 let oauth_header = OAuthHeader {
75 credentials: self,
76 nonce: nonce_string.as_str(),
77 signature_method: "HMAC-SHA1",
78 timestamp: current_timestamp(),
79 version: "1.0",
80 method,
81 base_url,
82 params,
83 };
84 oauth_header.to_string()
85 }
86}
87
88struct OAuthHeader<'a, S: BuildHasher> {
89 credentials: &'a Credentials<'a>,
90 nonce: &'a str,
91 signature_method: &'a str,
92 timestamp: u64,
93 version: &'a str,
94 method: &'a Method,
95 base_url: &'a str,
96 params: &'a HashMap<&'a str, &'a str, S>,
97}
98
99impl<'a, S: BuildHasher> OAuthHeader<'a, S> {
100 fn sign(&self) -> String {
101 let signature_base = format!(
102 "{}&{}&{}",
103 self.method,
104 percent_encode(self.base_url),
105 percent_encode(&self.params_string())
106 );
107 let key = format!(
108 "{}&{}",
109 self.credentials.consumer_secret, self.credentials.token_secret
110 );
111 let mut hmac =
112 Hmac::<Sha1>::new_varkey(key.as_bytes()).expect("HMAC can take keys of any size");
113 hmac.input(signature_base.as_bytes());
114 let signature = hmac.result();
115 percent_encode(&base64::encode(signature.code()))
116 }
117
118 fn params_string(&self) -> String {
119 let timestamp_string = self.timestamp.to_string();
120 let mut oauth_params = HashMap::new();
121 oauth_params.insert("oauth_consumer_key", self.credentials.consumer_key);
122 oauth_params.insert("oauth_nonce", self.nonce);
123 oauth_params.insert("oauth_signature_method", "HMAC-SHA1");
124 oauth_params.insert("oauth_timestamp", timestamp_string.as_str());
125 oauth_params.insert("oauth_token", self.credentials.token);
126 oauth_params.insert("oauth_version", "1.0");
127 let mut params: Vec<String> = oauth_params
128 .iter()
129 .chain(self.params.iter())
130 .map({ |(&k, &v)| format!("{}={}", percent_encode(k), percent_encode(v)) })
131 .collect();
132 params.sort();
133 params.join("&")
134 }
135}
136
137impl<'a, S: BuildHasher> fmt::Display for OAuthHeader<'a, S> {
138 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
139 write!(
140 formatter,
141 "OAuth oauth_consumer_key=\"{}\", oauth_nonce=\"{}\", oauth_signature=\"{}\", \
142 oauth_signature_method=\"{}\", oauth_timestamp=\"{}\", oauth_token=\"{}\", \
143 oauth_version=\"{}\"",
144 self.credentials.consumer_key,
145 self.nonce,
146 self.sign(),
147 self.signature_method,
148 self.timestamp,
149 self.credentials.token,
150 self.version
151 )
152 }
153}
154
155fn current_timestamp() -> u64 {
156 match SystemTime::now().duration_since(UNIX_EPOCH) {
157 Ok(duration) => duration.as_secs(),
158 Err(_) => 1_234_567_890,
159 }
160}
161
162const ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
166 .remove(b'-')
167 .remove(b'.')
168 .remove(b'_')
169 .remove(b'~');
170
171fn percent_encode(string: &str) -> String {
172 utf8_percent_encode(string, ENCODE_SET).to_string()
173}