1use std::io::Write;
22use std::process::{Command, Stdio};
23
24use crate::helper::{Credentials, Helper, HelperError};
25use crate::query::Query;
26
27#[derive(Debug, Clone)]
35pub struct AskpassHelper {
36 program: String,
37}
38
39impl AskpassHelper {
40 pub fn new(program: impl Into<String>) -> Self {
42 Self {
43 program: program.into(),
44 }
45 }
46
47 fn spawn(&self, prompt: &str) -> Result<String, HelperError> {
48 let mut parts = self.program.split_whitespace();
49 let prog = parts
50 .next()
51 .ok_or_else(|| HelperError::Failed("askpass program is empty".into()))?;
52 let mut args: Vec<&str> = parts.collect();
53 args.push(prompt);
54
55 let mut e = std::io::stderr().lock();
60 let _ = write!(e, "creds: filling with GIT_ASKPASS: {prog}");
61 for a in &args {
62 let _ = write!(e, " {a}");
63 }
64 let _ = writeln!(e);
65 drop(e);
66
67 let out = match Command::new(prog)
68 .args(&args)
69 .stdin(Stdio::null())
70 .stdout(Stdio::piped())
71 .stderr(Stdio::piped())
72 .output()
73 {
74 Ok(o) => o,
75 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
76 let mut e2 = std::io::stderr().lock();
82 let _ = writeln!(e2, "creds: failed to find GIT_ASKPASS command: {prog}");
83 return Err(e.into());
84 }
85 Err(e) => return Err(e.into()),
86 };
87 if !out.status.success() {
88 return Err(HelperError::Failed(format!(
89 "askpass {prog:?} exited {}: {}",
90 out.status,
91 String::from_utf8_lossy(&out.stderr).trim(),
92 )));
93 }
94 if !out.stderr.is_empty() {
97 return Err(HelperError::Failed(
98 String::from_utf8_lossy(&out.stderr).trim().to_owned(),
99 ));
100 }
101 Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
102 }
103}
104
105impl Helper for AskpassHelper {
106 fn fill(&self, query: &Query) -> Result<Option<Credentials>, HelperError> {
107 let bare_url = format_url(query, None);
111 let username = self.spawn(&format!("Username for \"{bare_url}\""))?;
112 if username.is_empty() {
113 return Ok(None);
114 }
115 let user_url = format_url(query, Some(&username));
116 let password = self.spawn(&format!("Password for \"{user_url}\""))?;
117 if password.is_empty() {
118 return Ok(None);
119 }
120 Ok(Some(Credentials::new(username, password)))
121 }
122
123 fn approve(&self, _query: &Query, _creds: &Credentials) -> Result<(), HelperError> {
125 Ok(())
126 }
127
128 fn reject(&self, _query: &Query, _creds: &Credentials) -> Result<(), HelperError> {
130 Ok(())
131 }
132}
133
134fn format_url(query: &Query, username: Option<&str>) -> String {
139 let mut s = String::with_capacity(query.host.len() + query.path.len() + 16);
140 s.push_str(&query.protocol);
141 s.push_str("://");
142 if let Some(u) = username {
143 s.push_str(u);
144 s.push('@');
145 }
146 s.push_str(&query.host);
147 if !query.path.is_empty() {
148 s.push('/');
149 s.push_str(&query.path);
150 }
151 s
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn format_url_no_username() {
160 let q = Query {
161 protocol: "https".into(),
162 host: "git.example.com".into(),
163 path: "foo/bar.git".into(),
164 };
165 assert_eq!(format_url(&q, None), "https://git.example.com/foo/bar.git");
166 }
167
168 #[test]
169 fn format_url_with_username() {
170 let q = Query {
171 protocol: "https".into(),
172 host: "git.example.com".into(),
173 path: "foo/bar.git".into(),
174 };
175 assert_eq!(
176 format_url(&q, Some("alice")),
177 "https://alice@git.example.com/foo/bar.git",
178 );
179 }
180
181 #[test]
182 fn format_url_no_path() {
183 let q = Query {
184 protocol: "http".into(),
185 host: "h:42".into(),
186 path: String::new(),
187 };
188 assert_eq!(format_url(&q, None), "http://h:42");
189 }
190
191 #[test]
192 fn fill_runs_program_and_returns_credentials() {
193 let tmp = tempfile::TempDir::new().unwrap();
196 let prog = tmp.path().join("ask");
197 std::fs::write(
198 &prog,
199 "#!/bin/sh\n\
200 case \"$1\" in\n\
201 Username*) echo alice;;\n\
202 Password*) echo s3cret;;\n\
203 esac\n",
204 )
205 .unwrap();
206 #[cfg(unix)]
207 {
208 use std::os::unix::fs::PermissionsExt;
209 let mut perms = std::fs::metadata(&prog).unwrap().permissions();
210 perms.set_mode(0o755);
211 std::fs::set_permissions(&prog, perms).unwrap();
212 }
213 let helper = AskpassHelper::new(prog.to_string_lossy().into_owned());
214 let q = Query {
215 protocol: "https".into(),
216 host: "h.example".into(),
217 path: "repo".into(),
218 };
219 let creds = helper.fill(&q).unwrap().expect("creds");
220 assert_eq!(creds.username, "alice");
221 assert_eq!(creds.password, "s3cret");
222 }
223
224 #[test]
225 fn fill_returns_none_on_empty_username() {
226 let tmp = tempfile::TempDir::new().unwrap();
227 let prog = tmp.path().join("ask");
228 std::fs::write(&prog, "#!/bin/sh\necho\n").unwrap();
229 #[cfg(unix)]
230 {
231 use std::os::unix::fs::PermissionsExt;
232 let mut perms = std::fs::metadata(&prog).unwrap().permissions();
233 perms.set_mode(0o755);
234 std::fs::set_permissions(&prog, perms).unwrap();
235 }
236 let helper = AskpassHelper::new(prog.to_string_lossy().into_owned());
237 let q = Query {
238 protocol: "https".into(),
239 host: "h.example".into(),
240 path: String::new(),
241 };
242 assert_eq!(helper.fill(&q).unwrap(), None);
243 }
244}