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 cmd.contains("/") || cmd.contains("\\") {
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
484#[cfg(test)]
485mod test {
486 use std::env;
487 use std::fs::File;
488 use std::io::prelude::*;
489 use std::path::Path;
490 use tempfile::TempDir;
491
492 use crate::{Config, ConfigLevel, Cred, CredentialHelper};
493
494 macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({
495 let td = TempDir::new().unwrap();
496 let mut cfg = Config::new().unwrap();
497 cfg.add_file(&td.path().join("cfg"), ConfigLevel::App, false).unwrap();
498 $(cfg.set_str($k, $v).unwrap();)*
499 cfg
500 }) );
501
502 #[test]
503 fn smoke() {
504 Cred::default().unwrap();
505 }
506
507 #[test]
508 fn credential_helper1() {
509 let cfg = test_cfg! {
510 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
511 };
512 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
513 .config(&cfg)
514 .execute()
515 .unwrap();
516 assert_eq!(u, "a");
517 assert_eq!(p, "b");
518 }
519
520 #[test]
521 fn credential_helper2() {
522 let cfg = test_cfg! {};
523 assert!(CredentialHelper::new("https://example.com/foo/bar")
524 .config(&cfg)
525 .execute()
526 .is_none());
527 }
528
529 #[test]
530 fn credential_helper3() {
531 let cfg = test_cfg! {
532 "credential.https://example.com.helper" =>
533 "!f() { echo username=c; }; f",
534 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
535 };
536 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
537 .config(&cfg)
538 .execute()
539 .unwrap();
540 assert_eq!(u, "c");
541 assert_eq!(p, "b");
542 }
543
544 #[test]
545 fn credential_helper4() {
546 if cfg!(windows) {
547 return;
548 } let td = TempDir::new().unwrap();
551 let path = td.path().join("script");
552 File::create(&path)
553 .unwrap()
554 .write(
555 br"\
556#!/bin/sh
557echo username=c
558",
559 )
560 .unwrap();
561 chmod(&path);
562 let cfg = test_cfg! {
563 "credential.https://example.com.helper" =>
564 &path.display().to_string()[..],
565 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
566 };
567 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
568 .config(&cfg)
569 .execute()
570 .unwrap();
571 assert_eq!(u, "c");
572 assert_eq!(p, "b");
573 }
574
575 #[test]
576 fn credential_helper5() {
577 if cfg!(windows) {
578 return;
579 } let td = TempDir::new().unwrap();
581 let path = td.path().join("git-credential-script");
582 File::create(&path)
583 .unwrap()
584 .write(
585 br"\
586#!/bin/sh
587echo username=c
588",
589 )
590 .unwrap();
591 chmod(&path);
592
593 let paths = env::var("PATH").unwrap();
594 let paths =
595 env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter());
596 env::set_var("PATH", &env::join_paths(paths).unwrap());
597
598 let cfg = test_cfg! {
599 "credential.https://example.com.helper" => "script",
600 "credential.helper" => "!f() { echo username=a; echo password=b; }; f"
601 };
602 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
603 .config(&cfg)
604 .execute()
605 .unwrap();
606 assert_eq!(u, "c");
607 assert_eq!(p, "b");
608 }
609
610 #[test]
611 fn credential_helper6() {
612 let cfg = test_cfg! {
613 "credential.helper" => ""
614 };
615 assert!(CredentialHelper::new("https://example.com/foo/bar")
616 .config(&cfg)
617 .execute()
618 .is_none());
619 }
620
621 #[test]
622 fn credential_helper7() {
623 if cfg!(windows) {
624 return;
625 } let td = TempDir::new().unwrap();
627 let path = td.path().join("script");
628 File::create(&path)
629 .unwrap()
630 .write(
631 br"\
632#!/bin/sh
633echo username=$1
634echo password=$2
635",
636 )
637 .unwrap();
638 chmod(&path);
639 let cfg = test_cfg! {
640 "credential.helper" => &format!("{} a b", path.display())
641 };
642 let (u, p) = CredentialHelper::new("https://example.com/foo/bar")
643 .config(&cfg)
644 .execute()
645 .unwrap();
646 assert_eq!(u, "a");
647 assert_eq!(p, "b");
648 }
649
650 #[test]
651 fn credential_helper8() {
652 let cfg = test_cfg! {
653 "credential.useHttpPath" => "true"
654 };
655 let mut helper = CredentialHelper::new("https://example.com/foo/bar");
656 helper.config(&cfg);
657 assert_eq!(helper.path.as_deref(), Some("foo/bar"));
658 }
659
660 #[test]
661 fn credential_helper9() {
662 let cfg = test_cfg! {
663 "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f"
664 };
665 let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar")
666 .config(&cfg)
667 .execute()
668 .unwrap();
669 assert_eq!(u, "a");
670 assert_eq!(p, "b");
671 }
672
673 #[test]
674 #[cfg(feature = "ssh")]
675 fn ssh_key_from_memory() {
676 let cred = Cred::ssh_key_from_memory(
677 "test",
678 Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"),
679 r#"
680 -----BEGIN RSA PRIVATE KEY-----
681 Proc-Type: 4,ENCRYPTED
682 DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8
683
684 3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd
685 H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4
686 RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2
687 vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD
688 aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS
689 os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L
690 g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p
691 VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz
692 YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn
693 M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2
694 kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw
695 1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk
696 g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF
697 b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E
698 tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r
699 HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7
700 UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq
701 COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb
702 37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX
703 qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5
704 f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY
705 Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434
706 BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq
707 c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY
708 8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O
709 -----END RSA PRIVATE KEY-----
710 "#,
711 Some("test123"));
712 assert!(cred.is_ok());
713 }
714
715 #[cfg(unix)]
716 fn chmod(path: &Path) {
717 use std::fs;
718 use std::os::unix::prelude::*;
719 let mut perms = fs::metadata(path).unwrap().permissions();
720 perms.set_mode(0o755);
721 fs::set_permissions(path, perms).unwrap();
722 }
723 #[cfg(windows)]
724 fn chmod(_path: &Path) {}
725}