#![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;
pub type Result<T> = std::result::Result<T, failure::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
};
}
#[derive(Clone, Debug)]
pub struct Token<'a> {
pub key: Cow<'a, str>,
pub secret: Cow<'a, str>,
}
impl<'a> Token<'a> {
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(),
}
}
}
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;
impl percent_encoding::EncodeSet for StrictEncodeSet {
#[inline]
fn contains(&self, byte: u8) -> bool {
!((byte >= 0x61 && byte <= 0x7a) ||
(byte >= 0x41 && byte <= 0x5a) ||
(byte >= 0x30 && byte <= 0x39) ||
(byte == 0x2d) ||
(byte == 0x2e) ||
(byte == 0x5f) ||
(byte == 0x7e))
}
}
fn encode(s: &str) -> String {
percent_encoding::percent_encode(s.as_bytes(), StrictEncodeSet).collect()
}
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())
}
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(", "))
}
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("&"))
}
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(¶m).as_ref(),
consumer.secret.as_ref(),
token.map(|t| t.secret.as_ref()),
);
let _ = insert_param(&mut param, "oauth_signature", sign);
(header(¶m), body(¶m))
}
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)
}
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))
}
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))
}
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);
}
}