use std::fmt;
use std::env::*;
use std::env;
use std::error::Error;
use std::fs;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::io::prelude::*;
use std::io::BufReader;
use std::io::Error as IoError;
use std::ascii::AsciiExt;
use std::collections::HashMap;
use std::sync::Mutex;
use std::cell::RefCell;
use std::time::Duration as StdDuration;
use hyper::Client;
use hyper::header::Connection;
use regex::Regex;
use chrono::{Duration, UTC, DateTime, ParseError};
use serde_json::{Value, from_str};
use aws::errors::credentials_error::CredentialsError;
#[derive(Clone, Debug)]
pub struct AwsCredentials {
access_key_id: String,
secret_access_key: String,
token: Option<String>,
expires_at: DateTime<UTC>
}
impl AwsCredentials {
pub fn new<K, S>(access_key_id:K,
secret_access_key:S,
token:Option<String>,
expires_at:DateTime<UTC>)
-> AwsCredentials where K:Into<String>, S:Into<String> {
AwsCredentials {
access_key_id: access_key_id.into(),
secret_access_key: secret_access_key.into(),
token: token,
expires_at: expires_at,
}
}
pub fn aws_access_key_id(&self) -> &str {
&self.access_key_id
}
pub fn aws_secret_access_key(&self) -> &str {
&self.secret_access_key
}
pub fn expires_at(&self) -> &DateTime<UTC> {
&self.expires_at
}
pub fn token(&self) -> &Option<String> {
&self.token
}
fn credentials_are_expired(&self) -> bool {
self.expires_at < UTC::now() + Duration::seconds(20)
}
}
pub trait AwsCredentialsProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError>;
}
pub struct EnvironmentProvider;
impl AwsCredentialsProvider for EnvironmentProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
let env_key = match var("AWS_ACCESS_KEY_ID") {
Ok(val) => val,
Err(_) => return Err(CredentialsError::new("No AWS_ACCESS_KEY_ID in environment"))
};
let env_secret = match var("AWS_SECRET_ACCESS_KEY") {
Ok(val) => val,
Err(_) => return Err(CredentialsError::new("No AWS_SECRET_ACCESS_KEY in environment"))
};
if env_key.is_empty() || env_secret.is_empty() {
return Err(CredentialsError::new("Couldn't find either AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY or both in environment."));
}
let token = match var("AWS_SESSION_TOKEN") {
Ok(val) => {
if val.is_empty() {
None
} else {
Some(val)
}
}
Err(_) => None,
};
Ok(AwsCredentials::new(env_key, env_secret, token, in_ten_minutes()))
}
}
#[derive(Clone, Debug)]
pub struct ProfileProvider {
credentials: Option<AwsCredentials>,
location: PathBuf,
profile: String,
}
impl ProfileProvider {
pub fn new() -> Result<ProfileProvider, CredentialsError> {
let location = match env::home_dir() {
Some(home) => {
let mut credentials_path = PathBuf::from(".aws");
credentials_path.push("credentials");
home.join(credentials_path)
}
None => return Err(CredentialsError::new("The environment variable HOME must be set.")),
};
Ok(ProfileProvider {
credentials: None,
location: location,
profile: "default".to_owned(),
})
}
pub fn with_configuration<F, P>(location: F, profile: P) -> ProfileProvider
where F: Into<PathBuf>, P: Into<String> {
ProfileProvider {
credentials: None,
location: location.into(),
profile: profile.into(),
}
}
pub fn location(&self) -> &Path {
self.location.as_ref()
}
pub fn profile(&self) -> &str {
&self.profile
}
pub fn set_location<F>(&mut self, location: F) where F: Into<PathBuf> {
self.location = location.into();
}
pub fn set_profile<P>(&mut self, profile: P) where P: Into<String> {
self.profile = profile.into();
}
}
impl AwsCredentialsProvider for ProfileProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
parse_credentials_file(self.location()).and_then(|mut profiles| {
profiles.remove(self.profile()).ok_or(CredentialsError::new("Profile not found."))
})
}
}
fn parse_credentials_file(location: &Path) -> Result<HashMap<String, AwsCredentials>, CredentialsError> {
match fs::metadata(location) {
Err(_) => return Err(CredentialsError::new("Could not stat credentials file.")),
Ok(metadata) => {
if !metadata.is_file() {
return Err(CredentialsError::new("Could not open file."));
}
}
};
let file = try!(File::open(location));
let profile_regex = Regex::new(r"^\[([^\]]+)\]$").unwrap();
let mut profiles: HashMap<String, AwsCredentials> = HashMap::new();
let mut access_key_id: Option<String> = None;
let mut secret_access_key: Option<String> = None;
let mut profile_name: Option<String> = None;
let file_lines = BufReader::new(&file);
for line in file_lines.lines() {
let unwrapped_line : String = line.unwrap();
if unwrapped_line.starts_with('#') {
continue;
}
if profile_regex.is_match(&unwrapped_line) {
if profile_name.is_some() && access_key_id.is_some() && secret_access_key.is_some() {
let creds = AwsCredentials::new(access_key_id.unwrap(), secret_access_key.unwrap(), None, in_ten_minutes());
profiles.insert(profile_name.unwrap(), creds);
}
access_key_id = None;
secret_access_key = None;
let caps = profile_regex.captures(&unwrapped_line).unwrap();
profile_name = Some(caps.at(1).unwrap().to_string());
continue;
}
let lower_case_line = unwrapped_line.to_ascii_lowercase().to_string();
if lower_case_line.contains("aws_access_key_id") &&
access_key_id.is_none()
{
let v: Vec<&str> = unwrapped_line.split('=').collect();
if !v.is_empty() {
access_key_id = Some(v[1].trim_matches(' ').to_string());
}
} else if lower_case_line.contains("aws_secret_access_key") &&
secret_access_key.is_none()
{
let v: Vec<&str> = unwrapped_line.split('=').collect();
if !v.is_empty() {
secret_access_key = Some(v[1].trim_matches(' ').to_string());
}
}
}
if profile_name.is_some() && access_key_id.is_some() && secret_access_key.is_some() {
let creds = AwsCredentials::new(access_key_id.unwrap(), secret_access_key.unwrap(), None, in_ten_minutes());
profiles.insert(profile_name.unwrap(), creds);
}
if profiles.is_empty() {
return Err(CredentialsError::new("No credentials found."));
}
Ok(profiles)
}
pub struct IamProvider;
impl AwsCredentialsProvider for IamProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
let mut address : String = "http://169.254.169.254/latest/meta-data/iam/security-credentials".to_string();
let mut client = Client::new();
client.set_read_timeout(Some(StdDuration::from_secs(15)));
let mut response;
match client.get(&address)
.header(Connection::close()).send() {
Err(_) => return Err(CredentialsError::new("Couldn't connect to metadata service")), Ok(received_response) => response = received_response
};
let mut body = String::new();
if let Err(_) = response.read_to_string(&mut body) {
return Err(CredentialsError::new("Didn't get a parsable response body from metadata service"));
}
address.push_str("/");
address.push_str(&body);
body = String::new();
match client.get(&address)
.header(Connection::close()).send() {
Err(_) => return Err(CredentialsError::new("Didn't get a parseable response body from instance role details")),
Ok(received_response) => response = received_response
};
if let Err(_) = response.read_to_string(&mut body) {
return Err(CredentialsError::new("Had issues with reading iam role response: {}"));
}
let json_object: Value;
match from_str(&body) {
Err(_) => return Err(CredentialsError::new("Couldn't parse metadata response body.")),
Ok(val) => json_object = val
};
let access_key_id;
match json_object.find("AccessKeyId") {
None => return Err(CredentialsError::new("Couldn't find AccessKeyId in response.")),
Some(val) => access_key_id = val.as_str().expect("AccessKeyId value was not a string").to_owned().replace("\"", "")
};
let secret_access_key;
match json_object.find("SecretAccessKey") {
None => return Err(CredentialsError::new("Couldn't find SecretAccessKey in response.")),
Some(val) => secret_access_key = val.as_str().expect("SecretAccessKey value was not a string").to_owned().replace("\"", "")
};
let expiration;
match json_object.find("Expiration") {
None => return Err(CredentialsError::new("Couldn't find Expiration in response.")),
Some(val) => expiration = val.as_str().expect("Expiration value was not a string").to_owned().replace("\"", "")
};
let expiration_time = try!(expiration.parse());
let token_from_response;
match json_object.find("Token") {
None => return Err(CredentialsError::new("Couldn't find Token in response.")),
Some(val) => token_from_response = val.as_str().expect("Token value was not a string").to_owned().replace("\"", "")
};
Ok(AwsCredentials::new(access_key_id, secret_access_key, Some(token_from_response), expiration_time))
}
}
pub struct BaseAutoRefreshingProvider<P, T> {
credentials_provider: P,
cached_credentials: T
}
pub type AutoRefreshingProviderSync<P> = BaseAutoRefreshingProvider<P, Mutex<AwsCredentials>>;
impl <P: AwsCredentialsProvider> AutoRefreshingProviderSync<P> {
pub fn with_mutex(provider: P) -> Result<AutoRefreshingProviderSync<P>, CredentialsError> {
let creds = try!(provider.credentials());
Ok(BaseAutoRefreshingProvider {
credentials_provider: provider,
cached_credentials: Mutex::new(creds)
})
}
}
impl <P: AwsCredentialsProvider> AwsCredentialsProvider for BaseAutoRefreshingProvider<P, Mutex<AwsCredentials>> {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
let mut creds = self.cached_credentials.lock().unwrap();
if creds.credentials_are_expired() {
*creds = try!(self.credentials_provider.credentials());
}
Ok(creds.clone())
}
}
pub type AutoRefreshingProvider<P> = BaseAutoRefreshingProvider<P, RefCell<AwsCredentials>>;
impl <P: AwsCredentialsProvider> AutoRefreshingProvider<P> {
pub fn with_refcell(provider: P) -> Result<AutoRefreshingProvider<P>, CredentialsError> {
let creds = try!(provider.credentials());
Ok(BaseAutoRefreshingProvider {
credentials_provider: provider,
cached_credentials: RefCell::new(creds)
})
}
}
impl <P: AwsCredentialsProvider> AwsCredentialsProvider for BaseAutoRefreshingProvider<P, RefCell<AwsCredentials>> {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
let mut creds = self.cached_credentials.borrow_mut();
if creds.credentials_are_expired() {
*creds = try!(self.credentials_provider.credentials());
}
Ok(creds.clone())
}
}
pub type DefaultCredentialsProvider = AutoRefreshingProvider<ChainProvider>;
impl DefaultCredentialsProvider {
pub fn new() -> Result<DefaultCredentialsProvider, CredentialsError> {
Ok(try!(AutoRefreshingProvider::with_refcell(ChainProvider::new())))
}
}
pub type DefaultCredentialsProviderSync = AutoRefreshingProviderSync<ChainProvider>;
impl DefaultCredentialsProviderSync {
pub fn new() -> Result<DefaultCredentialsProviderSync, CredentialsError> {
Ok(try!(AutoRefreshingProviderSync::with_mutex(ChainProvider::new())))
}
}
#[derive(Debug, Clone)]
pub struct ChainProvider {
profile_provider: Option<ProfileProvider>,
}
impl AwsCredentialsProvider for ChainProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
EnvironmentProvider.credentials()
.or_else(|_| {
match self.profile_provider {
Some(ref provider) => provider.credentials(),
None => Err(CredentialsError::new(""))
}
})
.or_else(|_| IamProvider.credentials())
.or_else(|_| Err(CredentialsError::new("Couldn't find AWS credentials in environment, credentials file, or IAM role.")))
}
}
impl ChainProvider {
pub fn new() -> ChainProvider {
ChainProvider {
profile_provider: ProfileProvider::new().ok(),
}
}
pub fn with_profile_provider(profile_provider: ProfileProvider) -> ChainProvider {
ChainProvider {
profile_provider: Some(profile_provider),
}
}
}
fn in_ten_minutes() -> DateTime<UTC> {
UTC::now() + Duration::seconds(600)
}