#![allow(dead_code)]
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 hyper::Client;
use hyper::header::Connection;
use regex::Regex;
use chrono::{Duration, UTC, DateTime, ParseError};
use serde_json::{Value, from_str};
use std::time::Duration as StdDuration;
#[derive(Clone, Debug)]
pub struct AwsCredentials {
key: String,
secret: String,
token: Option<String>,
expires_at: DateTime<UTC>
}
impl AwsCredentials {
pub fn new<K, S>(key:K, secret:S, token:Option<String>, expires_at:DateTime<UTC>)
-> AwsCredentials where K:Into<String>, S:Into<String> {
AwsCredentials {
key: key.into(),
secret: secret.into(),
token: token,
expires_at: expires_at,
}
}
pub fn aws_access_key_id(&self) -> &str {
&self.key
}
pub fn aws_secret_access_key(&self) -> &str {
&self.secret
}
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)
}
}
#[derive(Debug, PartialEq)]
pub struct CredentialsError{
pub message: String
}
impl CredentialsError {
fn new(message: &str) -> CredentialsError {
CredentialsError {
message: message.to_string()
}
}
}
impl fmt::Display for CredentialsError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.description())
}
}
impl Error for CredentialsError {
fn description(&self) -> &str {
&self.message
}
}
impl From<ParseError> for CredentialsError {
fn from(err: ParseError) -> CredentialsError {
CredentialsError::new(err.description())
}
}
impl From<IoError> for CredentialsError {
fn from(err: IoError) -> CredentialsError {
CredentialsError::new(err.description())
}
}
pub trait ProvideAwsCredentials {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError>;
}
pub struct EnvironmentProvider;
impl ProvideAwsCredentials for EnvironmentProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
credentials_from_environment()
}
}
fn credentials_from_environment() -> 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>,
file_path: PathBuf,
profile: String,
}
impl ProfileProvider {
pub fn new() -> Result<ProfileProvider, CredentialsError> {
let profile_location = match env::home_dir() {
Some(home_path) => {
let mut credentials_path = PathBuf::from(".aws");
credentials_path.push("credentials");
home_path.join(credentials_path)
}
None => return Err(CredentialsError::new("The environment variable HOME must be set.")),
};
Ok(ProfileProvider {
credentials: None,
file_path: profile_location,
profile: "default".to_owned(),
})
}
pub fn with_configuration<F, P>(file_path: F, profile: P) -> ProfileProvider
where F: Into<PathBuf>, P: Into<String> {
ProfileProvider {
credentials: None,
file_path: file_path.into(),
profile: profile.into(),
}
}
pub fn file_path(&self) -> &Path {
self.file_path.as_ref()
}
pub fn profile(&self) -> &str {
&self.profile
}
pub fn set_file_path<F>(&mut self, file_path: F) where F: Into<PathBuf> {
self.file_path = file_path.into();
}
pub fn set_profile<P>(&mut self, profile: P) where P: Into<String> {
self.profile = profile.into();
}
}
impl ProvideAwsCredentials for ProfileProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
parse_credentials_file(self.file_path()).and_then(|mut profiles| {
profiles.remove(self.profile()).ok_or(CredentialsError::new("profile not found"))
})
}
}
fn parse_credentials_file(file_path: &Path) -> Result<HashMap<String, AwsCredentials>, CredentialsError> {
match fs::metadata(file_path) {
Err(_) => return Err(CredentialsError::new("Couldn't stat credentials file.")),
Ok(metadata) => {
if !metadata.is_file() {
return Err(CredentialsError::new("Couldn't open file."));
}
}
};
let file = try!(File::open(file_path));
let profile_regex = Regex::new(r"^\[([^\]]+)\]$").unwrap();
let mut profiles: HashMap<String, AwsCredentials> = HashMap::new();
let mut access_key: Option<String> = None;
let mut secret_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.is_some() && secret_key.is_some() {
let creds = AwsCredentials::new(access_key.unwrap(), secret_key.unwrap(), None, in_ten_minutes());
profiles.insert(profile_name.unwrap(), creds);
}
access_key = None;
secret_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.is_none()
{
let v: Vec<&str> = unwrapped_line.split('=').collect();
if !v.is_empty() {
access_key = Some(v[1].trim_matches(' ').to_string());
}
} else if lower_case_line.contains("aws_secret_access_key") &&
secret_key.is_none()
{
let v: Vec<&str> = unwrapped_line.split('=').collect();
if !v.is_empty() {
secret_key = Some(v[1].trim_matches(' ').to_string());
}
}
}
if profile_name.is_some() && access_key.is_some() && secret_key.is_some() {
let creds = AwsCredentials::new(access_key.unwrap(), secret_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 ProvideAwsCredentials for IamProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
let mut client = Client::new();
client.set_read_timeout(Some(StdDuration::from_secs(2)));
var("AWS_IAM_CREDENTIALS_URL")
.or_else(|_| {
let mut address = "http://169.254.169.254/latest/meta-data/iam/security-credentials".to_string();
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);
Ok(address)
})
.and_then(|address| {
debug!("Attempting to fetch credentials from {}", address);
let mut body = String::new();
client.get(&address)
.header(Connection::close()).send()
.or(Err(CredentialsError::new("Didn't get a parseable response body from instance role details")))
.and_then(|mut 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;
match json_object.find("AccessKeyId") {
None => return Err(CredentialsError::new("Couldn't find AccessKeyId in response.")),
Some(val) => access_key = val.as_str().expect("AccessKeyId value was not a string").to_owned().replace("\"", "")
};
let secret_key;
match json_object.find("SecretAccessKey") {
None => return Err(CredentialsError::new("Couldn't find SecretAccessKey in response.")),
Some(val) => secret_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, secret_key, Some(token_from_response), expiration_time))
})
})
.or_else(|e| {
warn!("Failed to fetch IAM credentials: {}", e);
Err(e)
})
}
}
pub struct BaseAutoRefreshingProvider<P, T> {
credentials_provider: P,
cached_credentials: T
}
pub type AutoRefreshingProviderSync<P> = BaseAutoRefreshingProvider<P, Mutex<AwsCredentials>>;
impl <P: ProvideAwsCredentials> 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: ProvideAwsCredentials> ProvideAwsCredentials 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: ProvideAwsCredentials> 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: ProvideAwsCredentials> ProvideAwsCredentials 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_providers: Vec<ProfileProvider>,
}
impl ProvideAwsCredentials for ChainProvider {
fn credentials(&self) -> Result<AwsCredentials, CredentialsError> {
EnvironmentProvider.credentials()
.map(|c| { debug!("Using AWS credentials from environment"); c })
.or_else(|_| {
self.profile_providers.iter()
.filter_map(|provider| provider.credentials().ok().map(|c| { debug!("Using AWS credentials from {}, profile {}", provider.file_path().to_string_lossy(), provider.profile()); c }))
.next()
.map(|creds| Ok(creds))
.unwrap_or(Err(CredentialsError::new("")))
})
.or_else(|_| IamProvider.credentials().map(|c| { debug!("Using AWS credentials from IAM"); c }))
.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_providers: ProfileProvider::new().into_iter().collect(),
}
}
pub fn with_profile_providers(profile_providers: Vec<ProfileProvider>)
-> ChainProvider {
ChainProvider {
profile_providers: profile_providers,
}
}
}
fn in_ten_minutes() -> DateTime<UTC> {
UTC::now() + Duration::seconds(600)
}