oauth_client_fix/
lib.rs

1// Copyright 2016 oauth-client-rs Developers
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! OAuth 1.0 client library for Rust.
9//!
10//! Dependent on libcurl.
11//!
12//! [Repository](https://github.com/charlag/oauth-client-rs)
13//!
14//! # Examples
15//!
16//! Send request for request token.
17//!
18//! ```
19//! const REQUEST_TOKEN: &'static str = "http://oauthbin.com/v1/request-token";
20//! let consumer = oauth_client::Token::new("key", "secret");
21//! let bytes = oauth_client::get(REQUEST_TOKEN, &consumer, None, None).unwrap();
22//! ```
23
24#![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/// The `Error` type
54#[derive(Debug)]
55pub enum Error {
56    /// Curl error
57    Curl(curl::Error),
58    /// Http status
59    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/// Token structure for the OAuth
94#[derive(Clone, Debug)]
95pub struct Token<'a> {
96    /// 'key' field of the token
97    pub key: Cow<'a, str>,
98    /// 'secret' part of the token
99    pub secret: Cow<'a, str>,
100}
101
102impl<'a> Token<'a> {
103    /// Create new token from `key` and `secret`
104    ///
105    /// # Examples
106    ///
107    /// ```
108    /// let consumer = oauth_client::Token::new("key", "secret");
109    /// ```
110    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
121/// Alias for `HashMap<Cow<'a, str>, Cow<'a, str>>`
122pub 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
142// Encode all but the unreserved characters defined in
143// RFC 3986, section 2.3. "Unreserved Characters"
144// https://tools.ietf.org/html/rfc3986#page-12
145//
146// This is required by
147// OAuth Core 1.0, section 5.1. "Parameter Encoding"
148// https://oauth.net/core/1.0/#encoding_parameters
149impl percent_encoding::EncodeSet for StrictEncodeSet {
150    #[inline]
151    fn contains(&self, byte: u8) -> bool {
152        !((byte >= 0x61 && byte <= 0x7a) || // A-Z
153          (byte >= 0x41 && byte <= 0x5a) || // a-z
154          (byte >= 0x30 && byte <= 0x39) || // 0-9
155          (byte == 0x2d) || // -
156          (byte == 0x2e) || // .
157          (byte == 0x5f) || // _
158          (byte == 0x7e)) // ~
159    }
160}
161
162/// Percent encode string
163fn encode(s: &str) -> String {
164    percent_encoding::percent_encode(s.as_bytes(), StrictEncodeSet).collect()
165}
166
167/// Wrapper function around 'crypto::Hmac'
168fn 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
174/// Create signature. See https://dev.twitter.com/oauth/overview/creating-signatures
175fn 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
196/// Constuct plain-text header
197fn 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
206/// Construct plain-text body from 'PaaramList'
207fn 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
216/// Create header and body
217fn 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(&param).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(&param), body(&param))
250}
251
252/// Create an authorization header.
253/// See https://dev.twitter.com/oauth/overview/authorizing-requests
254///
255/// # Examples
256///
257/// ```
258/// # extern crate curl;
259/// # extern crate oauth_client;
260/// # fn main() {
261/// const REQUEST_TOKEN: &'static str = "http://oauthbin.com/v1/request-token";
262/// let consumer = oauth_client::Token::new("key", "secret");
263/// let header = oauth_client::authorization_header("GET", REQUEST_TOKEN, &consumer, None, None);
264/// # }
265/// ```
266pub 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
275/// Send authorized GET request to the specified URL.
276/// `consumer` is a consumer token.
277///
278/// # Examples
279///
280/// ```
281/// let REQUEST_TOKEN: &'static str = "http://oauthbin.com/v1/request-token";
282/// let consumer = oauth_client::Token::new("key", "secret");
283/// let bytes = oauth_client::get(REQUEST_TOKEN, &consumer, None, None).unwrap();
284/// let resp = String::from_utf8(bytes).unwrap();
285/// ```
286pub 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
319/// Send authorized POST request to the specified URL.
320/// `consumer` is a consumer token.
321///
322/// # Examples
323///
324/// ```
325/// # let request = oauth_client::Token::new("key", "secret");
326/// let ACCESS_TOKEN: &'static str = "http://oauthbin.com/v1/access-token";
327/// let consumer = oauth_client::Token::new("key", "secret");
328/// let bytes = oauth_client::post(ACCESS_TOKEN, &consumer, Some(&request), None).unwrap();
329/// let resp = String::from_utf8(bytes).unwrap();
330/// ```
331pub 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}