1#![warn(bad_style)]
25#![warn(missing_docs)]
26#![warn(unused)]
27#![warn(unused_extern_crates)]
28#![warn(unused_import_braces)]
29#![warn(unused_qualifications)]
30#![warn(unused_results)]
31
32extern crate crypto;
33extern crate curl;
34#[macro_use]
35extern crate log;
36extern crate rand;
37extern crate rustc_serialize;
38extern crate time;
39extern crate url;
40
41use std::borrow::Cow;
42use std::collections::HashMap;
43use std::io::Read;
44use std::{error, fmt};
45use rand::Rng;
46use rustc_serialize::base64::{self, ToBase64};
47use crypto::hmac::Hmac;
48use crypto::mac::{Mac, MacResult};
49use crypto::sha1::Sha1;
50use curl::easy::{Easy, List};
51use url::percent_encoding;
52
53#[derive(Debug)]
55pub enum Error {
56 Curl(curl::Error),
58 HttpStatus(u32),
60}
61
62impl fmt::Display for Error {
63 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64 match *self {
65 Error::Curl(ref err) => write!(f, "Curl error: {}", err),
66 Error::HttpStatus(ref resp) => write!(f, "HTTP status error: {}", resp),
67 }
68 }
69}
70
71impl error::Error for Error {
72 fn description(&self) -> &str {
73 match *self {
74 Error::Curl(ref err) => err.description(),
75 Error::HttpStatus(_) => "HTTP status error",
76 }
77 }
78
79 fn cause(&self) -> Option<&error::Error> {
80 match *self {
81 Error::Curl(ref err) => Some(err),
82 Error::HttpStatus(_) => None,
83 }
84 }
85}
86
87impl From<curl::Error> for Error {
88 fn from(err: curl::Error) -> Error {
89 Error::Curl(err)
90 }
91}
92
93#[derive(Clone, Debug)]
95pub struct Token<'a> {
96 pub key: Cow<'a, str>,
98 pub secret: Cow<'a, str>,
100}
101
102impl<'a> Token<'a> {
103 pub fn new<K, S>(key: K, secret: S) -> Token<'a>
111 where K: Into<Cow<'a, str>>,
112 S: Into<Cow<'a, str>>
113 {
114 Token {
115 key: key.into(),
116 secret: secret.into(),
117 }
118 }
119}
120
121pub type ParamList<'a> = HashMap<Cow<'a, str>, Cow<'a, str>>;
123
124fn insert_param<'a, K, V>(param: &mut ParamList<'a>, key: K, value: V) -> Option<Cow<'a, str>>
125 where K: Into<Cow<'a, str>>,
126 V: Into<Cow<'a, str>>
127{
128 param.insert(key.into(), value.into())
129}
130
131fn join_query<'a>(param: &ParamList<'a>) -> String {
132 let mut pairs = param.iter()
133 .map(|(k, v)| format!("{}={}", encode(&k), encode(&v)))
134 .collect::<Vec<_>>();
135 pairs.sort();
136 pairs.join("&")
137}
138
139#[derive(Copy, Clone)]
140struct StrictEncodeSet;
141
142impl percent_encoding::EncodeSet for StrictEncodeSet {
150 #[inline]
151 fn contains(&self, byte: u8) -> bool {
152 !((byte >= 0x61 && byte <= 0x7a) || (byte >= 0x41 && byte <= 0x5a) || (byte >= 0x30 && byte <= 0x39) || (byte == 0x2d) || (byte == 0x2e) || (byte == 0x5f) || (byte == 0x7e)) }
160}
161
162fn encode(s: &str) -> String {
164 percent_encoding::percent_encode(s.as_bytes(), StrictEncodeSet).collect()
165}
166
167fn hmac_sha1(key: &[u8], data: &[u8]) -> MacResult {
169 let mut hmac = Hmac::new(Sha1::new(), key);
170 hmac.input(data);
171 hmac.result()
172}
173
174fn signature(method: &str,
176 uri: &str,
177 query: &str,
178 consumer_secret: &str,
179 token_secret: Option<&str>)
180 -> String {
181 let base = format!("{}&{}&{}", encode(method), encode(uri), encode(query));
182 let key = format!("{}&{}",
183 encode(consumer_secret),
184 encode(token_secret.unwrap_or("")));
185 let conf = base64::Config {
186 char_set: base64::CharacterSet::Standard,
187 newline: base64::Newline::LF,
188 pad: true,
189 line_length: None,
190 };
191 debug!("Signature base string: {}", base);
192 debug!("Authorization header: Authorization: {}", base);
193 hmac_sha1(key.as_bytes(), base.as_bytes()).code().to_base64(conf)
194}
195
196fn header(param: &ParamList) -> String {
198 let mut pairs = param.iter()
199 .filter(|&(k, _)| k.starts_with("oauth_"))
200 .map(|(k, v)| format!("{}=\"{}\"", k, encode(&v)))
201 .collect::<Vec<_>>();
202 pairs.sort();
203 format!("OAuth {}", pairs.join(", "))
204}
205
206fn body(param: &ParamList) -> String {
208 let mut pairs = param.iter()
209 .filter(|&(k, _)| !k.starts_with("oauth_"))
210 .map(|(k, v)| format!("{}={}", k, encode(&v)))
211 .collect::<Vec<_>>();
212 pairs.sort();
213 format!("{}", pairs.join("&"))
214}
215
216fn get_header(method: &str,
218 uri: &str,
219 consumer: &Token,
220 token: Option<&Token>,
221 other_param: Option<&ParamList>)
222 -> (String, String) {
223 let mut param = HashMap::new();
224 let timestamp = format!("{}", time::now_utc().to_timespec().sec);
225 let nonce = rand::thread_rng().gen_ascii_chars().take(32).collect::<String>();
226
227 let _ = insert_param(&mut param, "oauth_consumer_key", consumer.key.to_string());
228 let _ = insert_param(&mut param, "oauth_nonce", nonce);
229 let _ = insert_param(&mut param, "oauth_signature_method", "HMAC-SHA1");
230 let _ = insert_param(&mut param, "oauth_timestamp", timestamp);
231 let _ = insert_param(&mut param, "oauth_version", "1.0");
232 if let Some(tk) = token {
233 let _ = insert_param(&mut param, "oauth_token", tk.key.as_ref());
234 }
235
236 if let Some(ps) = other_param {
237 for (k, v) in ps.iter() {
238 let _ = insert_param(&mut param, k.as_ref(), v.as_ref());
239 }
240 }
241
242 let sign = signature(method,
243 uri,
244 join_query(¶m).as_ref(),
245 consumer.secret.as_ref(),
246 token.map(|t| t.secret.as_ref()));
247 let _ = insert_param(&mut param, "oauth_signature", sign);
248
249 (header(¶m), body(¶m))
250}
251
252pub fn authorization_header(method: &str,
267 uri: &str,
268 consumer: &Token,
269 token: Option<&Token>,
270 other_param: Option<&ParamList>)
271 -> String {
272 get_header(method, uri, consumer, token, other_param).0
273}
274
275pub fn get(uri: &str,
287 consumer: &Token,
288 token: Option<&Token>,
289 other_param: Option<&ParamList>)
290 -> Result<Vec<u8>, Error> {
291 let (header, body) = get_header("GET", uri, consumer, token, other_param);
292 let req_uri = if body.len() > 0 {
293 format!("{}?{}", uri, body)
294 } else {
295 format!("{}", uri)
296 };
297 let mut handle = Easy::new();
298 let mut list = List::new();
299 list.append(format!("Authorization: {}", header).as_ref()).unwrap();
300 let mut resp = Vec::new();
301 try!(handle.url(req_uri.as_ref()));
302 try!(handle.http_headers(list));
303 try!(handle.get(true));
304 {
305 let mut transfer = handle.transfer();
306 try!(transfer.write_function(|data| {
307 resp.extend_from_slice(data);
308 Ok(data.len())
309 }));
310 try!(transfer.perform());
311 }
312 let code = try!(handle.response_code());
313 if code != 200 {
314 return Err(Error::HttpStatus(code));
315 }
316 Ok(resp)
317}
318
319pub fn post(uri: &str,
332 consumer: &Token,
333 token: Option<&Token>,
334 other_param: Option<&ParamList>)
335 -> Result<Vec<u8>, Error> {
336 let (header, body) = get_header("POST", uri, consumer, token, other_param);
337 let mut handle = Easy::new();
338 let mut list = List::new();
339 list.append(format!("Authorization: {}", header).as_ref()).unwrap();
340 let mut resp = Vec::new();
341 try!(handle.url(uri.as_ref()));
342 try!(handle.http_headers(list));
343 try!(handle.post(true));
344 try!(handle.post_field_size(body.len() as u64));
345 {
346 let mut transfer = handle.transfer();
347 try!(transfer.read_function(|into| {
348 let mut body = body.as_bytes();
349 Ok(body.read(into).unwrap())
350 }));
351 try!(transfer.write_function(|data| {
352 resp.extend_from_slice(data);
353 Ok(data.len())
354 }));
355 try!(transfer.perform());
356 }
357 let code = try!(handle.response_code());
358 if code != 200 {
359 return Err(Error::HttpStatus(code));
360 }
361 Ok(resp)
362}
363
364
365#[cfg(test)]
366mod tests {
367 use std::collections::HashMap;
368 use super::encode;
369
370 #[test]
371 fn query() {
372 let mut map = HashMap::new();
373 let _ = map.insert("aaa".into(), "AAA".into());
374 let _ = map.insert("bbbb".into(), "BBBB".into());
375 let query = super::join_query(&map);
376 assert_eq!("aaa=AAA&bbbb=BBBB", query);
377 }
378
379
380 #[test]
381 fn test_encode() {
382 let method = "GET";
383 let uri = "http://oauthbin.com/v1/request-token";
384 let encoded_uri = "http%3A%2F%2Foauthbin.com%2Fv1%2Frequest-token";
385 let query = ["oauth_consumer_key=key&",
386 "oauth_nonce=s6HGl3GhmsDsmpgeLo6lGtKs7rQEzzsA&",
387 "oauth_signature_method=HMAC-SHA1&",
388 "oauth_timestamp=1471445561&",
389 "oauth_version=1.0"]
390 .iter()
391 .cloned()
392 .collect::<String>();
393 let encoded_query = ["oauth_consumer_key%3Dkey%26",
394 "oauth_nonce%3Ds6HGl3GhmsDsmpgeLo6lGtKs7rQEzzsA%26",
395 "oauth_signature_method%3DHMAC-SHA1%26",
396 "oauth_timestamp%3D1471445561%26",
397 "oauth_version%3D1.0"]
398 .iter()
399 .cloned()
400 .collect::<String>();
401
402 assert_eq!(encode(method), "GET");
403 assert_eq!(encode(uri), encoded_uri);
404 assert_eq!(encode(&query), encoded_query);
405 }
406}