Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

apt-auth-config refactor #299

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion apt-auth-config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ license = "MIT"
[dependencies]
thiserror = "2"
tracing = "0.1"
rustix = { version = "0.38", features = ["process"] }
winnow = { version = "0.6.20" }
url = "2.5"
ahash = "0.8.11"
197 changes: 58 additions & 139 deletions apt-auth-config/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
mod parser;

use std::{
fs::{self, read_dir},
io::{self},
path::{Path, PathBuf},
str::FromStr,
};

use rustix::process;
use ahash::HashMap;
use parser::{line, multiline};
use thiserror::Error;
use tracing::debug;
use url::Url;

#[derive(Debug, Error)]
pub enum AuthConfigError {
Expand All @@ -19,12 +22,12 @@ pub enum AuthConfigError {
OpenFile { path: PathBuf, err: io::Error },
#[error("Auth config file missing entry: {0}")]
MissingEntry(&'static str),
#[error("Parse failed, unknown line: {0}")]
ParseError(String),
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AuthConfig {
pub inner: Vec<AuthConfigEntry>,
}
pub struct AuthConfig(pub HashMap<Box<str>, AuthConfigEntry>);

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct AuthConfigEntry {
Expand All @@ -37,83 +40,65 @@ impl FromStr for AuthConfigEntry {
type Err = AuthConfigError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let entry = s
.split_ascii_whitespace()
.filter(|x| !x.starts_with("#"))
.collect::<Vec<_>>();

let mut host = None;
let mut login = None;
let mut password = None;

for (i, c) in entry.iter().enumerate() {
if *c == "machine" {
let Some(h) = entry.get(i + 1) else {
return Err(AuthConfigError::MissingEntry("machine"));
};

host = Some(h);
continue;
}
let mut s = s;
let parse = line(&mut s).map_err(|e| AuthConfigError::ParseError(e.to_string()))?;

if *c == "login" {
let Some(l) = entry.get(i + 1) else {
return Err(AuthConfigError::MissingEntry("login"));
};
Ok(parse_entry_inner(parse).1)
}
}

login = Some(l);
continue;
}
impl FromStr for AuthConfig {
type Err = AuthConfigError;

if *c == "password" {
let Some(p) = entry.get(i + 1) else {
return Err(AuthConfigError::MissingEntry("password"));
};
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = s;
let parse = multiline(&mut s).map_err(|e| AuthConfigError::ParseError(e.to_string()))?;
let mut res = HashMap::with_hasher(ahash::RandomState::new());

password = Some(p);
continue;
}
for r in parse {
let (k, v) = parse_entry_inner(r);
res.insert(k, v);
}

let Some(host) = host else {
return Err(AuthConfigError::MissingEntry("machine"));
};
Ok(AuthConfig(res))
}
}

let Some(login) = login else {
return Err(AuthConfigError::MissingEntry("login"));
};
fn parse_entry_inner(input: Vec<(&str, &str)>) -> (Box<str>, AuthConfigEntry) {
let mut machine = None;
let mut login = None;
let mut password = None;

for i in input {
match i.0 {
"machine" => machine = Some(i.1),
"login" => login = Some(i.1),
"password" => password = Some(i.1),
x => panic!("unexcept {x}"),
}
}

let Some(password) = password else {
return Err(AuthConfigError::MissingEntry("password"));
};
let machine: Box<str> = machine.unwrap().into();

Ok(Self {
host: (*host).into(),
user: (*login).into(),
password: (*password).into(),
})
}
(
machine.clone(),
AuthConfigEntry {
host: machine,
user: login.unwrap().into(),
password: password.unwrap().into(),
},
)
}

impl AuthConfig {
/// Read system auth.conf.d config (/etc/apt/auth.conf.d)
///
/// Note that this function returns empty vector if run as a non-root user.
pub fn system(sysroot: impl AsRef<Path>) -> Result<Self, AuthConfigError> {
// 在 auth.conf.d 的使用惯例中
// 配置文件的权限一般为 600,并且所有者为 root
// 以普通用户身份下载文件时,会没有权限读取 auth 配置
// 因此,在以普通用户访问时,不读取 auth 配置
if !process::geteuid().is_root() {
return Ok(Self { inner: vec![] });
}

let p = sysroot.as_ref().join("etc/apt/auth.conf.d");
Self::from_path(p)
}

pub fn from_path(p: impl AsRef<Path>) -> Result<Self, AuthConfigError> {
let mut v = vec![];
let mut v = HashMap::with_hasher(ahash::RandomState::new());

for i in read_dir(p.as_ref()).map_err(|e| AuthConfigError::ReadDir {
path: p.as_ref().to_path_buf(),
Expand All @@ -130,86 +115,20 @@ impl AuthConfig {
err: e,
})?;

let config = AuthConfig::from_str(&s)?;
v.extend(config.inner);
let config: AuthConfig = s.parse()?;
v.extend(config.0);
}

Ok(Self { inner: v })
}

pub fn find(&self, url: &str) -> Option<&AuthConfigEntry> {
let url = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url);

debug!("auth find url is: {}", url);

self.inner.iter().find(|x| {
let mut host = x.host.to_string();
while host.ends_with('/') {
host.pop();
}

let mut url = url.to_string();
while url.ends_with('/') {
url.pop();
}

host == url
})
Ok(Self(v))
}

pub fn find_package_url(&self, url: &str) -> Option<&AuthConfigEntry> {
let url = url
.strip_prefix("http://")
.or_else(|| url.strip_prefix("https://"))
.unwrap_or(url);

debug!("auth find package url is: {}", url);
pub fn get_match_auth(&self, url: Url) -> Option<&AuthConfigEntry> {
let host = url.host_str()?;
let path = url.path();
let url_without_schema = [host, path].concat();

self.inner.iter().find(|x| url.starts_with(x.host.as_ref()))
self.0
.values()
.find(|x| url_without_schema.starts_with(&*x.host))
}
}

impl FromStr for AuthConfig {
type Err = AuthConfigError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut v = vec![];

for i in s.lines().filter(|x| !x.starts_with('#')) {
let entry = AuthConfigEntry::from_str(i)?;
v.push(entry);
}

Ok(Self { inner: v })
}
}

#[test]
fn test_config_parser() {
let config = r#"machine esm.ubuntu.com/apps/ubuntu/ login bearer password qaq # ubuntu-pro-client
machine esm.ubuntu.com/infra/ubuntu/ login bearer password qaq # ubuntu-pro-client
"#;

let config = AuthConfig::from_str(config).unwrap();

assert_eq!(
config,
AuthConfig {
inner: vec![
AuthConfigEntry {
host: "esm.ubuntu.com/apps/ubuntu/".into(),
user: "bearer".into(),
password: "qaq".into(),
},
AuthConfigEntry {
host: "esm.ubuntu.com/infra/ubuntu/".into(),
user: "bearer".into(),
password: "qaq".into(),
},
]
}
);
}
Loading
Loading