1use log::{debug, trace};
2use std::ffi::CString;
3use std::io::Write;
4use std::mem;
5use std::path::Path;
6use std::process::{Command, Stdio};
7use std::ptr;
8
9use crate::util::Binding;
10use crate::{raw, Config, Error, IntoCString};
11
12pub struct Cred {
14 raw: *mut raw::git_cred,
15}
16
17pub struct CredentialHelper {
19 pub username: Option<String>,
22 protocol: Option<String>,
23 host: Option<String>,
24 port: Option<u16>,
25 path: Option<String>,
26 url: String,
27 commands: Vec<String>,
28}
29
30impl Cred {
31 pub fn default() -> Result<Cred, Error> {
34 crate::init();
35 let mut out = ptr::null_mut();
36 unsafe {
37 try_call!(raw::git_cred_default_new(&mut out));
38 Ok(Binding::from_raw(out))
39 }
40 }
41
42 pub fn ssh_key_from_agent(username: &str) -> Result<Cred, Error> {
46 crate::init();
47 let mut out = ptr::null_mut();
48 let username = CString::new(username)?;
49 unsafe {
50 try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username));
51 Ok(Binding::from_raw(out))
52 }
53 }
54
55 pub fn ssh_key(
57 username: &str,
58 publickey: Option<&Path>,
59 privatekey: &Path,
60 passphrase: Option<&str>,
61 ) -> Result<Cred, Error> {
62 crate::init();
63 let username = CString::new(username)?;
64 let publickey = crate::opt_cstr(publickey)?;
65 let privatekey = privatekey.into_c_string()?;
66 let passphrase = crate::opt_cstr(passphrase)?;
67 let mut out = ptr::null_mut();
68 unsafe {
69 try_call!(raw::git_cred_ssh_key_new(
70 &mut out, username, publickey, privatekey, passphrase
71 ));
72 Ok(Binding::from_raw(out))
73 }
74 }
75
76 pub fn ssh_key_from_memory(
78 username: &str,
79 publickey: Option<&str>,
80 privatekey: &str,
81 passphrase: Option<&str>,
82 ) -> Result<Cred, Error> {
83 crate::init();
84 let username = CString::new(username)?;
85 let publickey = crate::opt_cstr(publickey)?;
86 let privatekey = CString::new(privatekey)?;
87 let passphrase = crate::opt_cstr(passphrase)?;
88 let mut out = ptr::null_mut();
89 unsafe {
90 try_call!(raw::git_cred_ssh_key_memory_new(
91 &mut out, username, publickey, privatekey, passphrase
92 ));
93 Ok(Binding::from_raw(out))
94 }
95 }
96
97 pub fn userpass_plaintext(username: &str, password: &str) -> Result<Cred, Error> {
99 crate::init();
100 let username = CString::new(username)?;
101 let password = CString::new(password)?;
102 let mut out = ptr::null_mut();
103 unsafe {
104 try_call!(raw::git_cred_userpass_plaintext_new(
105 &mut out, username, password
106 ));
107 Ok(Binding::from_raw(out))
108 }
109 }
110
111 pub fn credential_helper(
122 config: &Config,
123 url: &str,
124 username: Option<&str>,
125 ) -> Result<Cred, Error> {
126 match CredentialHelper::new(url)
127 .config(config)
128 .username(username)
129 .execute()
130 {
131 Some((username, password)) => Cred::userpass_plaintext(&username, &password),
132 None => Err(Error::from_str(
133 "failed to acquire username/password \
134 from local configuration",
135 )),
136 }
137 }
138
139 pub fn username(username: &str) -> Result<Cred, Error> {
144 crate::init();
145 let username = CString::new(username)?;
146 let mut out = ptr::null_mut();
147 unsafe {
148 try_call!(raw::git_cred_username_new(&mut out, username));
149 Ok(Binding::from_raw(out))
150 }
151 }
152
153 pub fn has_username(&self) -> bool {
155 unsafe { raw::git_cred_has_username(self.raw) == 1 }
156 }
157
158 pub fn credtype(&self) -> raw::git_credtype_t {
160 unsafe { (*self.raw).credtype }
161 }
162
163 pub unsafe fn unwrap(mut self) -> *mut raw::git_cred {
165 mem::replace(&mut self.raw, ptr::null_mut())
166 }
167}
168
169impl Binding for Cred {
170 type Raw = *mut raw::git_cred;
171
172 unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred {
173 Cred { raw }
174 }
175 fn raw(&self) -> *mut raw::git_cred {
176 self.raw
177 }
178}
179
180impl Drop for Cred {
181 fn drop(&mut self) {
182 if !self.raw.is_null() {
183 unsafe {
184 if let Some(f) = (*self.raw).free {
185 f(self.raw)
186 }
187 }
188 }
189 }
190}
191
192impl CredentialHelper {
193 pub fn new(url: &str) -> CredentialHelper {
199 let mut ret = CredentialHelper {
200 protocol: None,
201 host: None,
202 port: None,
203 path: None,
204 username: None,
205 url: url.to_string(),
206 commands: Vec::new(),
207 };
208
209 if let Ok(url) = url::Url::parse(url) {
211 if let Some(url::Host::Domain(s)) = url.host() {
212 ret.host = Some(s.to_string());
213 }
214 ret.port = url.port();
215 ret.protocol = Some(url.scheme().to_string());
216 }
217 ret
218 }
219
220 pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper {
224 self.username = username.map(|s| s.to_string());
225 self
226 }
227
228 pub fn config(&mut self, config: &Config) -> &mut CredentialHelper {
231 if self.username.is_none() {
235 self.config_username(config);
236 }
237 self.config_helper(config);
238 self.config_use_http_path(config);
239 self
240 }
241
242 fn config_username(&mut self, config: &Config) {
244 let key = self.exact_key("username");
245 self.username = config
246 .get_string(&key)
247 .ok()
248 .or_else(|| {
249 self.url_key("username")
250 .and_then(|s| config.get_string(&s).ok())
251 })
252 .or_else(|| config.get_string("credential.username").ok())
253 }
254
255 fn config_helper(&mut self, config: &Config) {
257 let exact = config.get_string(&self.exact_key("helper"));
258 self.add_command(exact.as_ref().ok().map(|s| &s[..]));
259 if let Some(key) = self.url_key("helper") {
260 let url = config.get_string(&key);
261 self.add_command(url.as_ref().ok().map(|s| &s[..]));
262 }
263 let global = config.get_string("credential.helper");
264 self.add_command(global.as_ref().ok().map(|s| &s[..]));
265 }
266
267 fn config_use_http_path(&mut self, config: &Config) {
269 let mut use_http_path = false;
270 if let Some(value) = config.get_bool(&self.exact_key("useHttpPath")).ok() {
271 use_http_path = value;
272 } else if let Some(value) = self
273 .url_key("useHttpPath")
274 .and_then(|key| config.get_bool(&key).ok())
275 {
276 use_http_path = value;
277 } else if let Some(value) = config.get_bool("credential.useHttpPath").ok() {
278 use_http_path = value;
279 }
280
281 if use_http_path {
282 if let Ok(url) = url::Url::parse(&self.url) {
283 let path = url.path();
284 self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string());
286 }
287 }
288 }
289
290 fn add_command(&mut self, cmd: Option<&str>) {
295 let cmd = match cmd {
296 Some("") | None => return,
297 Some(s) => s,
298 };
299
300 if cmd.starts_with('!') {
301 self.commands.push(cmd[1..].to_string());
302 } else if is_absolute_path(cmd) {
303 self.commands.push(cmd.to_string());
304 } else {
305 self.commands.push(format!("git credential-{}", cmd));
306 }
307 }
308
309 fn exact_key(&self, name: &str) -> String {
310 format!("credential.{}.{}", self.url, name)
311 }
312
313 fn url_key(&self, name: &str) -> Option<String> {
314 match (&self.host, &self.protocol) {
315 (&Some(ref host), &Some(ref protocol)) => {
316 Some(format!("credential.{}://{}.{}", protocol, host, name))
317 }
318 _ => None,
319 }
320 }
321
322 pub fn execute(&self) -> Option<(String, String)> {
327 let mut username = self.username.clone();
328 let mut password = None;
329 for cmd in &self.commands {
330 let (u, p) = self.execute_cmd(cmd, &username);
331 if u.is_some() && username.is_none() {
332 username = u;
333 }
334 if p.is_some() && password.is_none() {
335 password = p;
336 }
337 if username.is_some() && password.is_some() {
338 break;
339 }
340 }
341
342 match (username, password) {
343 (Some(u), Some(p)) => Some((u, p)),
344 _ => None,
345 }
346 }
347
348 fn execute_cmd(
351 &self,
352 cmd: &str,
353 username: &Option<String>,
354 ) -> (Option<String>, Option<String>) {
355 macro_rules! my_try( ($e:expr) => (
356 match $e {
357 Ok(e) => e,
358 Err(e) => {
359 debug!("{} failed with {}", stringify!($e), e);
360 return (None, None)
361 }
362 }
363 ) );
364
365 let mut c = Command::new("sh");
375 #[cfg(windows)]
376 {
377 use std::os::windows::process::CommandExt;
378 const CREATE_NO_WINDOW: u32 = 0x08000000;
379 c.creation_flags(CREATE_NO_WINDOW);
380 }
381 c.arg("-c")
382 .arg(&format!("{} get", cmd))
383 .stdin(Stdio::piped())
384 .stdout(Stdio::piped())
385 .stderr(Stdio::piped());
386 debug!("executing credential helper {:?}", c);
387 let mut p = match c.spawn() {
388 Ok(p) => p,
389 Err(e) => {
390 debug!("`sh` failed to spawn: {}", e);
391 let mut parts = cmd.split_whitespace();
392 let mut c = Command::new(parts.next().unwrap());
393 #[cfg(windows)]
394 {
395 use std::os::windows::process::CommandExt;
396 const CREATE_NO_WINDOW: u32 = 0x08000000;
397 c.creation_flags(CREATE_NO_WINDOW);
398 }
399 for arg in parts {
400 c.arg(arg);
401 }
402 c.arg("get")
403 .stdin(Stdio::piped())
404 .stdout(Stdio::piped())
405 .stderr(Stdio::piped());
406 debug!("executing credential helper {:?}", c);
407 match c.spawn() {
408 Ok(p) => p,
409 Err(e) => {
410 debug!("fallback of {:?} failed with {}", cmd, e);
411 return (None, None);
412 }
413 }
414 }
415 };
416
417 {
420 let stdin = p.stdin.as_mut().unwrap();
421 if let Some(ref p) = self.protocol {
422 let _ = writeln!(stdin, "protocol={}", p);
423 }
424 if let Some(ref p) = self.host {
425 if let Some(ref p2) = self.port {
426 let _ = writeln!(stdin, "host={}:{}", p, p2);
427 } else {
428 let _ = writeln!(stdin, "host={}", p);
429 }
430 }
431 if let Some(ref p) = self.path {
432 let _ = writeln!(stdin, "path={}", p);
433 }
434 if let Some(ref p) = *username {
435 let _ = writeln!(stdin, "username={}", p);
436 }
437 }
438 let output = my_try!(p.wait_with_output());
439 if !output.status.success() {
440 debug!(
441 "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}",
442 output.status,
443 String::from_utf8_lossy(&output.stdout),
444 String::from_utf8_lossy(&output.stderr)
445 );
446 return (None, None);
447 }
448 trace!(
449 "credential helper stderr ---\n{}",
450 String::from_utf8_lossy(&output.stderr)
451 );
452 self.parse_output(output.stdout)
453 }
454
455 fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) {
457 let mut username = None;
459 let mut password = None;
460 for line in output.split(|t| *t == b'\n') {
461 let mut parts = line.splitn(2, |t| *t == b'=');
462 let key = parts.next().unwrap();
463 let value = match parts.next() {
464 Some(s) => s,
465 None => {
466 trace!("ignoring output line: {}", String::from_utf8_lossy(line));
467 continue;
468 }
469 };
470 let value = match String::from_utf8(value.to_vec()) {
471 Ok(s) => s,
472 Err(..) => continue,
473 };
474 match key {
475 b"username" => username = Some(value),
476 b"password" => password = Some(value),
477 _ => {}
478 }
479 }
480 (username, password)
481 }
482}
483
484fn is_absolute_path(path: &str) -> bool {
485 path.starts_with('/')
486 || path.starts_with('\\')
487 || cfg!(windows) && path.chars().nth(1).is_some_and(|x| x == ':')
488}
489
490#[cfg(test)]
491mod test {
492 use std::env;
493 use std::fs::File;
494 use std::io::prelude::*;
495 use std::path::Path;
496 use tempfile::TempDir;
497
498 use crate::{Config, ConfigLevel, Cred, CredentialHelper};
499
500 macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({
501 let td = TempDir::new().unwrap();
502 let mut cfg = Config::new().unwrap();
503 cfg.add_file(&td.path().join("cfg"), ConfigLevel::App, false).unwrap();
504 $(cfg.set_str($k, $v).unwrap();)*
505 cfg
506 }) );
507
508 #[test]
509 fn smoke() {
510 Cred::default().unwrap();
511 }
512
513 #[test]
514 fn credential_helper1() {
515 let cfg = test_cfg! {
516 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
517 };
518 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
519 .config(&cfg)
520 .execute()
521 .unwrap();
522 assert_eq!(u, "a");
523 assert_eq!(p, "b");
524 }
525
526 #[test]
527 fn credential_helper2() {
528 let cfg = test_cfg! {};
529 assert!(CredentialHelper::new("https://example.com/foo/bar")
530 .config(&cfg)
531 .execute()
532 .is_none());
533 }
534
535 #[test]
536 fn credential_helper3() {
537 let cfg = test_cfg! {
538 "credential.https://example.com.helper" =>
539 "!f() { echo username=c; }; f",
540 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
541 };
542 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
543 .config(&cfg)
544 .execute()
545 .unwrap();
546 assert_eq!(u, "c");
547 assert_eq!(p, "b");
548 }
549
550 #[test]
551 fn credential_helper4() {
552 if cfg!(windows) {
553 return;
554 } let td = TempDir::new().unwrap();
557 let path = td.path().join("script");
558 File::create(&path)
559 .unwrap()
560 .write(
561 br"\
562#!/bin/sh
563echo username=c
564",
565 )
566 .unwrap();
567 chmod(&path);
568 let cfg = test_cfg! {
569 "credential.https://example.com.helper" =>
570 &path.display().to_string()[..],
571 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
572 };
573 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
574 .config(&cfg)
575 .execute()
576 .unwrap();
577 assert_eq!(u, "c");
578 assert_eq!(p, "b");
579 }
580
581 #[test]
582 fn credential_helper5() {
583 if cfg!(windows) {
584 return;
585 } let td = TempDir::new().unwrap();
587 let path = td.path().join("git-credential-some-script");
588 File::create(&path)
589 .unwrap()
590 .write(
591 br"\
592#!/bin/sh
593echo username=$1
594",
595 )
596 .unwrap();
597 chmod(&path);
598
599 let paths = env::var("PATH").unwrap();
600 let paths =
601 env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter());
602 env::set_var("PATH", &env::join_paths(paths).unwrap());
603
604 let cfg = test_cfg! {
605 "credential.https://example.com.helper" => "some-script \"value/with\\slashes\"",
606 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
607 };
608 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
609 .config(&cfg)
610 .execute()
611 .unwrap();
612 assert_eq!(u, "value/with\\slashes");
613 assert_eq!(p, "b");
614 }
615
616 #[test]
617 fn credential_helper6() {
618 let cfg = test_cfg! {
619 "credential.helper" => ""
620 };
621 assert!(CredentialHelper::new("https://example.com/foo/bar")
622 .config(&cfg)
623 .execute()
624 .is_none());
625 }
626
627 #[test]
628 fn credential_helper7() {
629 if cfg!(windows) {
630 return;
631 } let td = TempDir::new().unwrap();
633 let path = td.path().join("script");
634 File::create(&path)
635 .unwrap()
636 .write(
637 br"\
638#!/bin/sh
639echo username=$1
640echo password=$2
641",
642 )
643 .unwrap();
644 chmod(&path);
645 let cfg = test_cfg! {
646 "credential.helper" => &format!("{} a b", path.display())
647 };
648 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
649 .config(&cfg)
650 .execute()
651 .unwrap();
652 assert_eq!(u, "a");
653 assert_eq!(p, "b");
654 }
655
656 #[test]
657 fn credential_helper8() {
658 let cfg = test_cfg! {
659 "credential.useHttpPath" => "true"
660 };
661 let mut helper = CredentialHelper::new("https://example.com/foo/bar");
662 helper.config(&cfg);
663 assert_eq!(helper.path.as_deref(), Some("foo/bar"));
664 }
665
666 #[test]
667 fn credential_helper9() {
668 let cfg = test_cfg! {
669 "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f"
670 };
671 let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar")
672 .config(&cfg)
673 .execute()
674 .unwrap();
675 assert_eq!(u, "a");
676 assert_eq!(p, "b");
677 }
678
679 #[test]
680 #[cfg(feature = "ssh")]
681 fn ssh_key_from_memory() {
682 let cred = Cred::ssh_key_from_memory(
683 "test",
684 Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"),
685 r#"
686 -----BEGIN RSA PRIVATE KEY-----
687 Proc-Type: 4,ENCRYPTED
688 DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8
689
690 3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd
691 H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4
692 RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2
693 vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD
694 aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS
695 os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L
696 g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p
697 VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz
698 YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn
699 M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2
700 kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw
701 1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk
702 g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF
703 b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E
704 tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r
705 HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7
706 UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq
707 COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb
708 37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX
709 qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5
710 f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY
711 Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434
712 BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq
713 c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY
714 8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O
715 -----END RSA PRIVATE KEY-----
716 "#,
717 Some("test123"));
718 assert!(cred.is_ok());
719 }
720
721 #[cfg(unix)]
722 fn chmod(path: &Path) {
723 use std::fs;
724 use std::os::unix::prelude::*;
725 let mut perms = fs::metadata(path).unwrap().permissions();
726 perms.set_mode(0o755);
727 fs::set_permissions(path, perms).unwrap();
728 }
729 #[cfg(windows)]
730 fn chmod(_path: &Path) {}
731}