pub use reqwest::Method;
use result::{MwsError, MwsResult};
use std::cmp::{Ord, Ordering};
use std::path::Path;
use types::ToIso8601;
use url::percent_encoding::{percent_encode, EncodeSet};
#[derive(Clone)]
struct ParameterEncodeSet;
impl EncodeSet for ParameterEncodeSet {
fn contains(&self, byte: u8) -> bool {
if byte >= 0x30 && byte <= 0x39 {
return false;
}
if byte >= 0x41 && byte <= 0x5A {
return false;
}
if byte >= 0x61 && byte <= 0x7A {
return false;
}
if byte == 0x2D || byte == 0x5F || byte == 0x2E || byte == 0x7E {
return false;
}
true
}
}
#[derive(Debug, Eq, PartialEq, PartialOrd, Clone)]
pub struct Param(String, String);
impl Ord for Param {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
pub struct SignedUrl<'a> {
pub host: &'a str,
pub method: Method,
pub path: String,
pub query_string: String,
pub signature: String,
pub pairs: Vec<(String, String)>,
}
impl<'a> Into<String> for SignedUrl<'a> {
fn into(self) -> String {
self.to_string()
}
}
impl<'a> SignedUrl<'a> {
pub fn get_url_without_query(&self) -> String {
format!("https://{host}{path}", host = &self.host, path = self.path,)
}
pub fn to_string(self) -> String {
let mut signature_encoded =
String::with_capacity(((self.signature.len() as f32) * 1.4) as usize);
for part in percent_encode(self.signature.as_bytes(), ParameterEncodeSet) {
signature_encoded.push_str(part);
}
format!(
"https://{host}{path}?{qs}&Signature={signature}",
host = &self.host,
path = self.path,
qs = self.query_string,
signature = signature_encoded
)
}
}
#[derive(Debug, Clone)]
pub struct SignatureV2 {
host: String,
aws_access_key_id: String,
secret_key: String,
auth_token: Option<String>,
pairs: Vec<Param>,
}
impl SignatureV2 {
pub fn new(
host: &str,
aws_access_key_id: &str,
secret_key: &str,
auth_token: Option<&str>,
) -> SignatureV2 {
SignatureV2 {
host: host.into(),
aws_access_key_id: aws_access_key_id.to_string(),
secret_key: secret_key.to_string(),
auth_token: auth_token.map(ToString::to_string),
pairs: Vec::new(),
}
}
pub fn add<T: Into<String>>(&mut self, key: &str, value: T) -> &mut Self {
SignatureV2::set_param(&mut self.pairs, key, value);
self
}
fn set_param<T: Into<String>>(params: &mut Vec<Param>, key: &str, value: T) {
match params.iter().position(|&Param(ref k, _)| k == key) {
Some(pos) => {
params[pos].1 = value.into();
}
None => {
params.push(Param(key.to_owned(), value.into()));
}
}
}
pub fn generate_url<'a, T: AsRef<str>, P: AsRef<Path>>(
&'a self,
method: Method,
path: P,
version: T,
action: T,
) -> MwsResult<SignedUrl<'a>> {
use crypto::hmac::Hmac;
use crypto::mac::Mac;
use crypto::sha2::Sha256;
let mut params = self.pairs.clone();
let mut qs = String::with_capacity(255);
SignatureV2::set_param(&mut params, "AWSAccessKeyId", &self.aws_access_key_id);
if let Some(auth_token) = self.auth_token.as_ref() {
SignatureV2::set_param(&mut params, "MWSAuthToken", auth_token);
}
SignatureV2::set_param(&mut params, "SignatureMethod", "HmacSHA256");
SignatureV2::set_param(&mut params, "SignatureVersion", "2");
SignatureV2::set_param(&mut params, "Version", version.as_ref());
SignatureV2::set_param(&mut params, "Action", action.as_ref());
if !params.iter().any(|pair| pair.0 == "Timestamp") {
SignatureV2::set_param(&mut params, "Timestamp", ::chrono::Utc::now().to_iso8601());
}
let mut pairs = vec![];
params.sort();
for Param(key, value) in params {
if qs.len() > 0 {
qs.push_str("&");
}
for part in percent_encode(key.as_bytes(), ParameterEncodeSet) {
qs.push_str(part);
}
qs.push_str("=");
for part in percent_encode(value.as_bytes(), ParameterEncodeSet) {
qs.push_str(part);
}
pairs.push((key, value));
}
let path_str = path.as_ref().to_str().ok_or_else(|| {
let display = path.as_ref().display();
MwsError::InvalidPath(format!("{}", display))
})?;
let signature = {
let canonical_qs = format!(
"{method}\n{host}\n{path}\n{qs}",
method = &method,
host = &self.host,
path = path_str,
qs = qs
);
let mut hmac = Hmac::new(Sha256::new(), self.secret_key.as_bytes());
hmac.input(canonical_qs.as_bytes());
base64::encode(&hmac.result().code())
};
Ok(SignedUrl {
host: &self.host,
method: method,
path: path_str.to_string(),
query_string: qs,
signature,
pairs,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{DateTime, Utc};
#[test]
fn test_signature() {
let mut s = SignatureV2::new("mws.amazonservices.ca", "3333", "0000", None);
let date: DateTime<Utc> = "2016-12-20T18:42:04Z".parse().expect("parse date");
let url = s
.add("Timestamp", date.to_iso8601())
.add("MarketplaceId", "5555")
.add("ASINList.ASIN.1", "6666")
.add("SellerId", "1111")
.generate_url(
Method::Post,
"/Products/2011-10-01",
"2011-10-01",
"GetMatchingProduct",
)
.expect("generate url");
assert_eq!(
url.signature,
"e7NJFMRLOOpRUp0IP42irtpKzq404KDbjZpKZ/OWRLI="
);
}
}