use std::io::Write;
use std::process::{Command, Stdio};
use crate::helper::{Credentials, Helper, HelperError};
use crate::query::Query;
#[derive(Debug, Clone)]
pub struct AskpassHelper {
program: String,
}
impl AskpassHelper {
pub fn new(program: impl Into<String>) -> Self {
Self {
program: program.into(),
}
}
fn spawn(&self, prompt: &str) -> Result<String, HelperError> {
let mut parts = self.program.split_whitespace();
let prog = parts
.next()
.ok_or_else(|| HelperError::Failed("askpass program is empty".into()))?;
let mut args: Vec<&str> = parts.collect();
args.push(prompt);
let mut e = std::io::stderr().lock();
let _ = write!(e, "creds: filling with GIT_ASKPASS: {prog}");
for a in &args {
let _ = write!(e, " {a}");
}
let _ = writeln!(e);
drop(e);
let out = match Command::new(prog)
.args(&args)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
{
Ok(o) => o,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let mut e2 = std::io::stderr().lock();
let _ = writeln!(e2, "creds: failed to find GIT_ASKPASS command: {prog}");
return Err(e.into());
}
Err(e) => return Err(e.into()),
};
if !out.status.success() {
return Err(HelperError::Failed(format!(
"askpass {prog:?} exited {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim(),
)));
}
if !out.stderr.is_empty() {
return Err(HelperError::Failed(
String::from_utf8_lossy(&out.stderr).trim().to_owned(),
));
}
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
}
}
impl Helper for AskpassHelper {
fn fill(&self, query: &Query) -> Result<Option<Credentials>, HelperError> {
let bare_url = format_url(query, None);
let username = self.spawn(&format!("Username for \"{bare_url}\""))?;
if username.is_empty() {
return Ok(None);
}
let user_url = format_url(query, Some(&username));
let password = self.spawn(&format!("Password for \"{user_url}\""))?;
if password.is_empty() {
return Ok(None);
}
Ok(Some(Credentials::new(username, password)))
}
fn approve(&self, _query: &Query, _creds: &Credentials) -> Result<(), HelperError> {
Ok(())
}
fn reject(&self, _query: &Query, _creds: &Credentials) -> Result<(), HelperError> {
Ok(())
}
}
fn format_url(query: &Query, username: Option<&str>) -> String {
let mut s = String::with_capacity(query.host.len() + query.path.len() + 16);
s.push_str(&query.protocol);
s.push_str("://");
if let Some(u) = username {
s.push_str(u);
s.push('@');
}
s.push_str(&query.host);
if !query.path.is_empty() {
s.push('/');
s.push_str(&query.path);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn format_url_no_username() {
let q = Query {
protocol: "https".into(),
host: "git.example.com".into(),
path: "foo/bar.git".into(),
};
assert_eq!(format_url(&q, None), "https://git.example.com/foo/bar.git");
}
#[test]
fn format_url_with_username() {
let q = Query {
protocol: "https".into(),
host: "git.example.com".into(),
path: "foo/bar.git".into(),
};
assert_eq!(
format_url(&q, Some("alice")),
"https://alice@git.example.com/foo/bar.git",
);
}
#[test]
fn format_url_no_path() {
let q = Query {
protocol: "http".into(),
host: "h:42".into(),
path: String::new(),
};
assert_eq!(format_url(&q, None), "http://h:42");
}
#[test]
fn fill_runs_program_and_returns_credentials() {
let tmp = tempfile::TempDir::new().unwrap();
let prog = tmp.path().join("ask");
std::fs::write(
&prog,
"#!/bin/sh\n\
case \"$1\" in\n\
Username*) echo alice;;\n\
Password*) echo s3cret;;\n\
esac\n",
)
.unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&prog).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&prog, perms).unwrap();
}
let helper = AskpassHelper::new(prog.to_string_lossy().into_owned());
let q = Query {
protocol: "https".into(),
host: "h.example".into(),
path: "repo".into(),
};
let creds = helper.fill(&q).unwrap().expect("creds");
assert_eq!(creds.username, "alice");
assert_eq!(creds.password, "s3cret");
}
#[test]
fn fill_returns_none_on_empty_username() {
let tmp = tempfile::TempDir::new().unwrap();
let prog = tmp.path().join("ask");
std::fs::write(&prog, "#!/bin/sh\necho\n").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(&prog).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&prog, perms).unwrap();
}
let helper = AskpassHelper::new(prog.to_string_lossy().into_owned());
let q = Query {
protocol: "https".into(),
host: "h.example".into(),
path: String::new(),
};
assert_eq!(helper.fill(&q).unwrap(), None);
}
}