oauth-client-async 0.4.0

OAuth client for Rust
// Copyright 2016 oauth-client-rs Developers
//
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
// http://opensource.org/licenses/MIT>, at your option. This file may not be
// copied, modified, or distributed except according to those terms.

//! OAuth 1.0 client library for Rust.
//!
//! [Repository](https://github.com/charlag/oauth-client-rs)
//!
//! # Examples
//!
//! Send request for request token.
//!
//! ```rust
//! # extern crate oauth_client; extern crate tokio_core;
//! # fn main() {
//! # let mut core = tokio_core::reactor::Core::new().unwrap();
//! const REQUEST_TOKEN: &'static str = "http://oauthbin.com/v1/request-token";
//! let consumer = oauth_client::Token::new("key", "secret");
//! let bytes_future = oauth_client::get(REQUEST_TOKEN, &consumer, None, None);
//! let bytes = core.run(bytes_future).unwrap();
//! # }
//! ```
#![warn(bad_style)]
#![warn(missing_docs)]
#![warn(unused)]
#![warn(unused_extern_crates)]
#![warn(unused_import_braces)]
#![warn(unused_qualifications)]
#![warn(unused_results)]
#![allow(unused_doc_comments)]

extern crate base64;
extern crate failure;
#[macro_use]
extern crate failure_derive;
#[macro_use]
extern crate lazy_static;
#[macro_use]
extern crate log;
extern crate futures;
extern crate hyper;
extern crate hyper_tls;
extern crate rand;
extern crate ring;
extern crate time;
extern crate url;

use futures::future::{result, Either};
use futures::{Future, Stream};
use hyper::client::HttpConnector;
use hyper::header::{HeaderValue, AUTHORIZATION, CONTENT_TYPE};
use hyper::{Body, Client, Request, StatusCode};
use hyper_tls::HttpsConnector;
use rand::{distributions::Alphanumeric, Rng};
use ring::{digest, hmac};
use std::borrow::Cow;
use std::collections::HashMap;
use std::iter;
use url::percent_encoding;

/// Result type.
pub type Result<T> = std::result::Result<T, failure::Error>;

/// An error happening due to a HTTP status error.
#[derive(Debug, Fail, Clone, Copy)]
#[fail(display = "HTTP status error code {}", _0)]
pub struct HttpStatusError(pub u16);

lazy_static! {
    static ref CLIENT: Client<HttpsConnector<HttpConnector>, Body> = {
        let con = HttpsConnector::new(4).expect("TLS initialization failed");
        let client = Client::builder().build(con);
        client
    };
}

/// Token structure for the OAuth
#[derive(Clone, Debug)]
pub struct Token<'a> {
    /// 'key' field of the token
    pub key: Cow<'a, str>,
    /// 'secret' part of the token
    pub secret: Cow<'a, str>,
}

impl<'a> Token<'a> {
    /// Create new token from `key` and `secret`
    ///
    /// # Examples
    ///
    /// ```
    /// let consumer = oauth_client::Token::new("key", "secret");
    /// ```
    pub fn new<K, S>(key: K, secret: S) -> Token<'a>
    where
        K: Into<Cow<'a, str>>,
        S: Into<Cow<'a, str>>,
    {
        Token {
            key: key.into(),
            secret: secret.into(),
        }
    }
}

/// Alias for `HashMap<Cow<'a, str>, Cow<'a, str>>`
pub type ParamList<'a> = HashMap<Cow<'a, str>, Cow<'a, str>>;

fn insert_param<'a, K, V>(param: &mut ParamList<'a>, key: K, value: V) -> Option<Cow<'a, str>>
where
    K: Into<Cow<'a, str>>,
    V: Into<Cow<'a, str>>,
{
    param.insert(key.into(), value.into())
}

fn join_query<'a>(param: &ParamList<'a>) -> String {
    let mut pairs = param
        .iter()
        .map(|(k, v)| format!("{}={}", encode(&k), encode(&v)))
        .collect::<Vec<_>>();
    pairs.sort();
    pairs.join("&")
}

#[derive(Copy, Clone)]
struct StrictEncodeSet;

// Encode all but the unreserved characters defined in
// RFC 3986, section 2.3. "Unreserved Characters"
// https://tools.ietf.org/html/rfc3986#page-12
//
// This is required by
// OAuth Core 1.0, section 5.1. "Parameter Encoding"
// https://oauth.net/core/1.0/#encoding_parameters
impl percent_encoding::EncodeSet for StrictEncodeSet {
    #[inline]
    fn contains(&self, byte: u8) -> bool {
        !((byte >= 0x61 && byte <= 0x7a) || // A-Z
          (byte >= 0x41 && byte <= 0x5a) || // a-z
          (byte >= 0x30 && byte <= 0x39) || // 0-9
          (byte == 0x2d) || // -
          (byte == 0x2e) || // .
          (byte == 0x5f) || // _
          (byte == 0x7e)) // ~
    }
}

/// Percent encode string
fn encode(s: &str) -> String {
    percent_encoding::percent_encode(s.as_bytes(), StrictEncodeSet).collect()
}

/// Create signature. See https://dev.twitter.com/oauth/overview/creating-signatures
fn signature(
    method: &str,
    uri: &str,
    query: &str,
    consumer_secret: &str,
    token_secret: Option<&str>,
) -> String {
    let base = format!("{}&{}&{}", encode(method), encode(uri), encode(query));
    let key = format!(
        "{}&{}",
        encode(consumer_secret),
        encode(token_secret.unwrap_or(""))
    );
    debug!("Signature base string: {}", base);
    debug!("Authorization header: Authorization: {}", base);
    let signing_key = hmac::SigningKey::new(&digest::SHA1, key.as_bytes());
    let signature = hmac::sign(&signing_key, base.as_bytes());
    base64::encode(signature.as_ref())
}

/// Constuct plain-text header
fn header(param: &ParamList) -> String {
    let mut pairs = param
        .iter()
        .filter(|&(k, _)| k.starts_with("oauth_"))
        .map(|(k, v)| format!("{}=\"{}\"", k, encode(&v)))
        .collect::<Vec<_>>();
    pairs.sort();
    format!("OAuth {}", pairs.join(", "))
}

/// Construct plain-text body from 'ParamList'
fn body(param: &ParamList) -> String {
    let mut pairs = param
        .iter()
        .filter(|&(k, _)| !k.starts_with("oauth_"))
        .map(|(k, v)| format!("{}={}", k, encode(&v)))
        .collect::<Vec<_>>();
    pairs.sort();
    format!("{}", pairs.join("&"))
}

/// Create header and body
fn get_header(
    method: &str,
    uri: &str,
    consumer: &Token,
    token: Option<&Token>,
    other_param: Option<&ParamList>,
) -> (String, String) {
    let mut param = HashMap::new();
    let timestamp = format!("{}", time::now_utc().to_timespec().sec);
    let mut rng = rand::thread_rng();
    let nonce = iter::repeat(())
        .map(|()| rng.sample(Alphanumeric))
        .take(32)
        .collect::<String>();

    let _ = insert_param(&mut param, "oauth_consumer_key", consumer.key.to_string());
    let _ = insert_param(&mut param, "oauth_nonce", nonce);
    let _ = insert_param(&mut param, "oauth_signature_method", "HMAC-SHA1");
    let _ = insert_param(&mut param, "oauth_timestamp", timestamp);
    let _ = insert_param(&mut param, "oauth_version", "1.0");
    if let Some(tk) = token {
        let _ = insert_param(&mut param, "oauth_token", tk.key.as_ref());
    }

    if let Some(ps) = other_param {
        for (k, v) in ps.iter() {
            let _ = insert_param(&mut param, k.as_ref(), v.as_ref());
        }
    }

    let sign = signature(
        method,
        uri,
        join_query(&param).as_ref(),
        consumer.secret.as_ref(),
        token.map(|t| t.secret.as_ref()),
    );
    let _ = insert_param(&mut param, "oauth_signature", sign);

    (header(&param), body(&param))
}

/// Create an authorization header.
/// See https://dev.twitter.com/oauth/overview/authorizing-requests
///
/// # Examples
///
/// ```
/// # extern crate oauth_client;
/// # fn main() {
/// const REQUEST_TOKEN: &'static str = "http://oauthbin.com/v1/request-token";
/// let consumer = oauth_client::Token::new("key", "secret");
/// let header = oauth_client::authorization_header("GET", REQUEST_TOKEN, &consumer, None, None);
/// # }
/// ```
pub fn authorization_header(
    method: &str,
    uri: &str,
    consumer: &Token,
    token: Option<&Token>,
    other_param: Option<&ParamList>,
) -> (String, String) {
    get_header(method, uri, consumer, token, other_param)
}

/// Send authorized GET request to the specified URL.
/// `consumer` is a consumer token.
///
/// # Examples
///
/// ```rust
/// # extern crate oauth_client; extern crate tokio_core;
/// # fn main() {
/// # let mut core = tokio_core::reactor::Core::new().unwrap();
/// let REQUEST_TOKEN: &'static str = "http://oauthbin.com/v1/request-token";
/// let consumer = oauth_client::Token::new("key", "secret");
/// let bytes_future = oauth_client::get(REQUEST_TOKEN, &consumer, None, None);
/// let bytes = core.run(bytes_future).unwrap();
/// let resp = String::from_utf8(bytes).unwrap();
/// # }
/// ```
pub fn get(
    uri: &str,
    consumer: &Token,
    token: Option<&Token>,
    other_param: Option<&ParamList>,
) -> impl Future<Item = Vec<u8>, Error = failure::Error> {
    let (header, body) = get_header("GET", uri, consumer, token, other_param);
    let req_uri = if body.len() > 0 {
        format!("{}?{}", uri, body)
    } else {
        format!("{}", uri)
    };

    result(
        Request::get(req_uri)
            .header(AUTHORIZATION, header)
            .body(Body::empty())
            .map_err(Into::into),
    ).and_then(|req| send(req))
}

/// Send authorized POST request to the specified URL.
/// `consumer` is a consumer token.
///
/// # Examples
///
/// ```rust,no_run
/// # extern crate oauth_client; extern crate tokio_core;
/// # fn main() {
/// # let mut core = tokio_core::reactor::Core::new().unwrap();
/// # let request = oauth_client::Token::new("key", "secret");
/// let consumer = oauth_client::Token::new("key", "secret");
/// let ACCESS_TOKEN: &'static str = "http://oauthbin.com/v1/access-token";
/// let bytes_future = oauth_client::post(ACCESS_TOKEN, &consumer, Some(&request), None);
/// let bytes = core.run(bytes_future).unwrap();
/// let resp = String::from_utf8(bytes).unwrap();
/// # }
/// ```
pub fn post(
    uri: &str,
    consumer: &Token,
    token: Option<&Token>,
    other_param: Option<&ParamList>,
) -> impl Future<Item = Vec<u8>, Error = failure::Error> {
    let (header, body) = get_header("POST", uri, consumer, token, other_param);

    result(
        Request::post(uri)
            .header(AUTHORIZATION, header)
            .header(
                CONTENT_TYPE,
                HeaderValue::from_static("application/x-www-form-urlencoded"),
            ).body(body.into())
            .map_err(Into::into),
    ).and_then(|req| send(req))
}

/// Send request to the server
fn send(req: Request<Body>) -> impl Future<Item = Vec<u8>, Error = failure::Error> {
    CLIENT
        .request(req)
        .map_err(Into::into)
        .and_then(|resp| {
            let status = resp.status();
            if status == StatusCode::OK {
                let chunks = resp.into_body().concat2().map_err(Into::into);

                Either::A(chunks)
            } else {
                let err = futures::future::err(HttpStatusError(status.into()).into());

                Either::B(err)
            }
        }).map(|chunks| chunks.to_vec())
}

#[cfg(test)]
mod tests {
    use super::encode;
    use std::collections::HashMap;

    #[test]
    fn query() {
        let mut map = HashMap::new();
        let _ = map.insert("aaa".into(), "AAA".into());
        let _ = map.insert("bbbb".into(), "BBBB".into());
        let query = super::join_query(&map);
        assert_eq!("aaa=AAA&bbbb=BBBB", query);
    }

    #[test]
    fn test_encode() {
        let method = "GET";
        let uri = "http://oauthbin.com/v1/request-token";
        let encoded_uri = "http%3A%2F%2Foauthbin.com%2Fv1%2Frequest-token";
        let query = [
            "oauth_consumer_key=key&",
            "oauth_nonce=s6HGl3GhmsDsmpgeLo6lGtKs7rQEzzsA&",
            "oauth_signature_method=HMAC-SHA1&",
            "oauth_timestamp=1471445561&",
            "oauth_version=1.0",
        ]
            .iter()
            .cloned()
            .collect::<String>();
        let encoded_query = [
            "oauth_consumer_key%3Dkey%26",
            "oauth_nonce%3Ds6HGl3GhmsDsmpgeLo6lGtKs7rQEzzsA%26",
            "oauth_signature_method%3DHMAC-SHA1%26",
            "oauth_timestamp%3D1471445561%26",
            "oauth_version%3D1.0",
        ]
            .iter()
            .cloned()
            .collect::<String>();

        assert_eq!(encode(method), "GET");
        assert_eq!(encode(uri), encoded_uri);
        assert_eq!(encode(&query), encoded_query);
    }
}