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