use lazy_static::lazy_static;
use regex::{Captures, Regex};
use std::cell::RefCell;
use std::collections::{btree_map, BTreeMap};
use std::env;
use std::fs::File;
use std::io::{self, BufRead};
use std::iter::Iterator;
use std::path::Path;
use std::str::FromStr;
use std::sync::Mutex;
use crate::errors::*;
lazy_static! {
static ref BUILT_IN_SECRETFILE: Mutex<RefCell<Option<Secretfile>>> =
Mutex::new(RefCell::new(None));
}
fn interpolate_env(text: &str) -> Result<String> {
lazy_static! {
static ref RE: Regex = Regex::new(
r"(?x)
\$(?:
(?P<name>[a-zA-Z_][a-zA-Z0-9_]*)
|
\{(?P<name2>[a-zA-Z_][a-zA-Z0-9_]*)\}
)"
)
.unwrap();
}
let mut err = None;
let result = RE.replace_all(text, |caps: &Captures<'_>| {
let name = caps
.name("name")
.or_else(|| caps.name("name2"))
.unwrap()
.as_str();
match env::var(name) {
Ok(s) => s.to_owned(),
Err(env_err) => {
err = Some(Error::UndefinedEnvironmentVariable {
name: name.to_owned(),
cause: env_err,
});
"".to_owned()
}
}
});
match err {
None => Ok(result.into_owned()),
Some(err) => Err(err),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Location {
Path(String),
PathWithKey(String, String),
}
impl Location {
fn from_caps<'a>(caps: &Captures<'a>) -> Result<Location> {
let path_opt = caps.name("path").map(|m| m.as_str());
let key_opt = caps.name("key").map(|m| m.as_str());
match (path_opt, key_opt) {
(Some(path), None) => Ok(Location::Path(interpolate_env(path)?)),
(Some(path), Some(key)) => Ok(Location::PathWithKey(
interpolate_env(path)?,
key.to_owned(),
)),
(_, _) => {
let all = caps.get(0).unwrap().as_str().to_owned();
Err(Error::Parse { input: all })
}
}
}
}
#[derive(Debug, Clone)]
pub struct Secretfile {
varmap: BTreeMap<String, Location>,
filemap: BTreeMap<String, Location>,
}
impl Secretfile {
fn read_internal(read: &mut dyn io::Read) -> Result<Secretfile> {
lazy_static! {
static ref RE: Regex = Regex::new(r"(?x)
^(?:
# Blank line with optional comment.
\s*(?:\#.*)?
|
(?:
# VAR
(?P<var>[a-zA-Z_][a-zA-Z0-9_]*)
|
# >file
>(?P<file>\S+)
)
\s+
# path/to/secret:key
(?P<path>\S+?)(?::(?P<key>\S+))?
\s*
)$").unwrap();
}
let mut sf = Secretfile {
varmap: BTreeMap::new(),
filemap: BTreeMap::new(),
};
let buffer = io::BufReader::new(read);
for line_or_err in buffer.lines() {
let line = line_or_err?;
match RE.captures(&line) {
Some(ref caps) if caps.name("path").is_some() => {
let location = Location::from_caps(caps)?;
if caps.name("file").is_some() {
let file =
interpolate_env(caps.name("file").unwrap().as_str())?;
sf.filemap.insert(file, location);
} else if caps.name("var").is_some() {
let var = caps.name("var").unwrap().as_str().to_owned();
sf.varmap.insert(var, location);
}
}
Some(_) => {
}
_ => {
return Err(Error::Parse {
input: line.to_owned(),
})
}
}
}
Ok(sf)
}
pub fn read(read: &mut dyn io::Read) -> Result<Secretfile> {
Secretfile::read_internal(read).map_err(|err| Error::Secretfile(Box::new(err)))
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Secretfile> {
let path = path.as_ref();
let mut file = File::open(path).map_err(|err| Error::FileRead {
path: path.to_owned(),
cause: Box::new(err.into()),
})?;
Secretfile::read(&mut file).map_err(|err| Error::FileRead {
path: path.to_owned(),
cause: Box::new(err),
})
}
pub fn set_built_in(secretfile: Option<Secretfile>) {
let guard = BUILT_IN_SECRETFILE
.lock()
.expect("Unable to lock `BUILT_IN_SECRETFILE`");
*guard.borrow_mut() = secretfile;
}
pub fn default() -> Result<Secretfile> {
let guard = BUILT_IN_SECRETFILE
.lock()
.expect("Unable to lock `BUILT_IN_SECRETFILE`");
let built_in_opt = guard.borrow().to_owned();
if let Some(built_in) = built_in_opt {
Ok(built_in)
} else {
let mut path = env::current_dir()
.map_err(|err| Error::Secretfile(Box::new(err.into())))?;
path.push("Secretfile");
Secretfile::from_path(path)
}
}
pub fn vars(&self) -> SecretfileKeys<'_> {
SecretfileKeys {
keys: self.varmap.keys(),
}
}
pub fn files(&self) -> SecretfileKeys<'_> {
SecretfileKeys {
keys: self.filemap.keys(),
}
}
}
impl FromStr for Secretfile {
type Err = Error;
fn from_str(s: &str) -> Result<Secretfile> {
let mut cursor = io::Cursor::new(s.as_bytes());
Secretfile::read(&mut cursor)
}
}
pub trait SecretfileLookup {
fn var(&self, name: &str) -> Option<&Location>;
fn file(&self, name: &str) -> Option<&Location>;
}
impl SecretfileLookup for Secretfile {
fn var(&self, name: &str) -> Option<&Location> {
self.varmap.get(name)
}
fn file(&self, name: &str) -> Option<&Location> {
self.filemap.get(name)
}
}
#[derive(Clone)]
pub struct SecretfileKeys<'a> {
keys: btree_map::Keys<'a, String, Location>,
}
impl<'a> Iterator for SecretfileKeys<'a> {
type Item = &'a String;
fn next(&mut self) -> Option<&'a String> {
self.keys.next()
}
}
#[test]
fn test_parse() {
use std::str::FromStr;
let data = "\
# This is a comment.
FOO_USERNAME secret/$SECRET_NAME:username\n\
FOO_PASSWORD secret/${SECRET_NAME}:password\n\
# Try a Keywhiz-style secret, too.
FOO_USERNAME2 ${SECRET_NAME}_username\n\
# Credentials to copy to a file. Interpolation allowed on the left here.
>$SOMEDIR/.conf/key.pem secret/ssl:key_pem\n\
";
env::set_var("SECRET_NAME", "foo");
env::set_var("SOMEDIR", "/home/foo");
let secretfile = Secretfile::from_str(data).unwrap();
assert_eq!(
&Location::PathWithKey("secret/foo".to_owned(), "username".to_owned()),
secretfile.var("FOO_USERNAME").unwrap()
);
assert_eq!(
&Location::PathWithKey("secret/foo".to_owned(), "password".to_owned()),
secretfile.var("FOO_PASSWORD").unwrap()
);
assert_eq!(
&Location::Path("foo_username".to_owned()),
secretfile.var("FOO_USERNAME2").unwrap()
);
assert_eq!(
&Location::PathWithKey("secret/ssl".to_owned(), "key_pem".to_owned()),
secretfile.file("/home/foo/.conf/key.pem").unwrap()
);
assert_eq!(
vec!["FOO_PASSWORD", "FOO_USERNAME", "FOO_USERNAME2"],
secretfile.vars().collect::<Vec<_>>()
);
assert_eq!(
vec!["/home/foo/.conf/key.pem"],
secretfile.files().collect::<Vec<_>>()
);
}