use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use http::Uri;
use http::header::{AUTHORIZATION, HeaderValue};
use crate::error::AioductBody;
use crate::middleware::Middleware;
#[derive(Debug, Clone)]
pub struct Netrc {
entries: Arc<HashMap<String, NetrcEntry>>,
default: Option<Arc<NetrcEntry>>,
}
#[derive(Debug, Clone)]
struct NetrcEntry {
login: String,
password: String,
}
impl Netrc {
pub fn load_default() -> Result<Self, io::Error> {
let path = default_netrc_path()?;
Self::load(&path)
}
pub fn load(path: &Path) -> Result<Self, io::Error> {
let content = std::fs::read_to_string(path)?;
Ok(Self::parse(&content))
}
pub fn parse(content: &str) -> Self {
let mut entries = HashMap::new();
let mut default = None;
let tokens: Vec<&str> = content.split_whitespace().collect();
let mut i = 0;
while i < tokens.len() {
match tokens[i] {
"machine" => {
i += 1;
if i >= tokens.len() {
break;
}
let machine = tokens[i].to_string();
i += 1;
let entry = parse_entry(&tokens, &mut i);
if let Some(entry) = entry {
entries.insert(machine, entry);
}
}
"default" => {
i += 1;
let entry = parse_entry(&tokens, &mut i);
if let Some(entry) = entry {
default = Some(Arc::new(entry));
}
}
_ => {
i += 1;
}
}
}
Self {
entries: Arc::new(entries),
default,
}
}
fn lookup(&self, host: &str) -> Option<(&str, &str)> {
if let Some(entry) = self.entries.get(host) {
return Some((&entry.login, &entry.password));
}
if let Some(ref entry) = self.default {
return Some((&entry.login, &entry.password));
}
None
}
}
fn parse_entry(tokens: &[&str], i: &mut usize) -> Option<NetrcEntry> {
let mut login = None;
let mut password = None;
while *i < tokens.len() {
match tokens[*i] {
"login" => {
*i += 1;
if *i < tokens.len() {
login = Some(tokens[*i].to_string());
}
*i += 1;
}
"password" | "passwd" => {
*i += 1;
if *i < tokens.len() {
password = Some(tokens[*i].to_string());
}
*i += 1;
}
"account" | "macdef" => {
*i += 1;
if *i < tokens.len() {
*i += 1;
}
}
"machine" | "default" => {
break;
}
_ => {
*i += 1;
}
}
}
Some(NetrcEntry {
login: login.unwrap_or_default(),
password: password.unwrap_or_default(),
})
}
fn default_netrc_path() -> Result<PathBuf, io::Error> {
if let Ok(path) = std::env::var("NETRC") {
return Ok(PathBuf::from(path));
}
let home = std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map_err(|_| io::Error::new(io::ErrorKind::NotFound, "no home directory"))?;
let path = PathBuf::from(home).join(".netrc");
if path.exists() {
return Ok(path);
}
let alt_path = PathBuf::from(
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.unwrap_or_default(),
)
.join("_netrc");
if alt_path.exists() {
return Ok(alt_path);
}
Ok(path)
}
#[derive(Debug, Clone)]
pub struct NetrcMiddleware {
netrc: Netrc,
}
impl NetrcMiddleware {
pub fn from_default() -> Result<Self, io::Error> {
Ok(Self {
netrc: Netrc::load_default()?,
})
}
pub fn from_path(path: &Path) -> Result<Self, io::Error> {
Ok(Self {
netrc: Netrc::load(path)?,
})
}
pub fn new(netrc: Netrc) -> Self {
Self { netrc }
}
}
impl Middleware for NetrcMiddleware {
fn on_request(&self, request: &mut http::Request<AioductBody>, uri: &Uri) {
if request.headers().contains_key(AUTHORIZATION) {
return;
}
let host = uri.host().unwrap_or("");
if let Some((login, password)) = self.netrc.lookup(host) {
let encoded = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
format!("{login}:{password}"),
);
if let Ok(val) = HeaderValue::from_str(&format!("Basic {encoded}")) {
request.headers_mut().insert(AUTHORIZATION, val);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_netrc() {
let netrc = Netrc::parse(
"machine example.com login user1 password pass1\n\
machine api.test.io login user2 password pass2\n",
);
assert_eq!(netrc.lookup("example.com"), Some(("user1", "pass1")));
assert_eq!(netrc.lookup("api.test.io"), Some(("user2", "pass2")));
assert_eq!(netrc.lookup("unknown.com"), None);
}
#[test]
fn parse_with_default() {
let netrc = Netrc::parse(
"machine specific.com login alice password secret\n\
default login anon password anon\n",
);
assert_eq!(netrc.lookup("specific.com"), Some(("alice", "secret")));
assert_eq!(netrc.lookup("anything.else"), Some(("anon", "anon")));
}
#[test]
fn parse_multiline_format() {
let netrc = Netrc::parse("machine host.example.com\n login myuser\n password mypass\n");
assert_eq!(netrc.lookup("host.example.com"), Some(("myuser", "mypass")));
}
#[test]
fn empty_netrc() {
let netrc = Netrc::parse("");
assert_eq!(netrc.lookup("any"), None);
}
#[test]
fn comments_and_extra_whitespace() {
let netrc = Netrc::parse(" machine example.com login user1 password pass1 \n");
assert_eq!(netrc.lookup("example.com"), Some(("user1", "pass1")));
}
}