diff options
Diffstat (limited to 'extra/git2/src/cred.rs')
-rw-r--r-- | extra/git2/src/cred.rs | 717 |
1 files changed, 717 insertions, 0 deletions
diff --git a/extra/git2/src/cred.rs b/extra/git2/src/cred.rs new file mode 100644 index 000000000..49afb4239 --- /dev/null +++ b/extra/git2/src/cred.rs @@ -0,0 +1,717 @@ +use log::{debug, trace}; +use std::ffi::CString; +use std::io::Write; +use std::mem; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::ptr; +use url; + +use crate::util::Binding; +use crate::{raw, Config, Error, IntoCString}; + +/// A structure to represent git credentials in libgit2. +pub struct Cred { + raw: *mut raw::git_cred, +} + +/// Management of the gitcredentials(7) interface. +pub struct CredentialHelper { + /// A public field representing the currently discovered username from + /// configuration. + pub username: Option<String>, + protocol: Option<String>, + host: Option<String>, + port: Option<u16>, + path: Option<String>, + url: String, + commands: Vec<String>, +} + +impl Cred { + /// Create a "default" credential usable for Negotiate mechanisms like NTLM + /// or Kerberos authentication. + pub fn default() -> Result<Cred, Error> { + crate::init(); + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_default_new(&mut out)); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new ssh key credential object used for querying an ssh-agent. + /// + /// The username specified is the username to authenticate. + pub fn ssh_key_from_agent(username: &str) -> Result<Cred, Error> { + crate::init(); + let mut out = ptr::null_mut(); + let username = CString::new(username)?; + unsafe { + try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username)); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new passphrase-protected ssh key credential object. + pub fn ssh_key( + username: &str, + publickey: Option<&Path>, + privatekey: &Path, + passphrase: Option<&str>, + ) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let publickey = crate::opt_cstr(publickey)?; + let privatekey = privatekey.into_c_string()?; + let passphrase = crate::opt_cstr(passphrase)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_ssh_key_new( + &mut out, username, publickey, privatekey, passphrase + )); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new ssh key credential object reading the keys from memory. + pub fn ssh_key_from_memory( + username: &str, + publickey: Option<&str>, + privatekey: &str, + passphrase: Option<&str>, + ) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let publickey = crate::opt_cstr(publickey)?; + let privatekey = CString::new(privatekey)?; + let passphrase = crate::opt_cstr(passphrase)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_ssh_key_memory_new( + &mut out, username, publickey, privatekey, passphrase + )); + Ok(Binding::from_raw(out)) + } + } + + /// Create a new plain-text username and password credential object. + pub fn userpass_plaintext(username: &str, password: &str) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let password = CString::new(password)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_userpass_plaintext_new( + &mut out, username, password + )); + Ok(Binding::from_raw(out)) + } + } + + /// Attempt to read `credential.helper` according to gitcredentials(7) [1] + /// + /// This function will attempt to parse the user's `credential.helper` + /// configuration, invoke the necessary processes, and read off what the + /// username/password should be for a particular URL. + /// + /// The returned credential type will be a username/password credential if + /// successful. + /// + /// [1]: https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html + pub fn credential_helper( + config: &Config, + url: &str, + username: Option<&str>, + ) -> Result<Cred, Error> { + match CredentialHelper::new(url) + .config(config) + .username(username) + .execute() + { + Some((username, password)) => Cred::userpass_plaintext(&username, &password), + None => Err(Error::from_str( + "failed to acquire username/password \ + from local configuration", + )), + } + } + + /// Create a credential to specify a username. + /// + /// This is used with ssh authentication to query for the username if none is + /// specified in the URL. + pub fn username(username: &str) -> Result<Cred, Error> { + crate::init(); + let username = CString::new(username)?; + let mut out = ptr::null_mut(); + unsafe { + try_call!(raw::git_cred_username_new(&mut out, username)); + Ok(Binding::from_raw(out)) + } + } + + /// Check whether a credential object contains username information. + pub fn has_username(&self) -> bool { + unsafe { raw::git_cred_has_username(self.raw) == 1 } + } + + /// Return the type of credentials that this object represents. + pub fn credtype(&self) -> raw::git_credtype_t { + unsafe { (*self.raw).credtype } + } + + /// Unwrap access to the underlying raw pointer, canceling the destructor + pub unsafe fn unwrap(mut self) -> *mut raw::git_cred { + mem::replace(&mut self.raw, ptr::null_mut()) + } +} + +impl Binding for Cred { + type Raw = *mut raw::git_cred; + + unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred { + Cred { raw } + } + fn raw(&self) -> *mut raw::git_cred { + self.raw + } +} + +impl Drop for Cred { + fn drop(&mut self) { + if !self.raw.is_null() { + unsafe { + if let Some(f) = (*self.raw).free { + f(self.raw) + } + } + } + } +} + +impl CredentialHelper { + /// Create a new credential helper object which will be used to probe git's + /// local credential configuration. + /// + /// The URL specified is the namespace on which this will query credentials. + /// Invalid URLs are currently ignored. + pub fn new(url: &str) -> CredentialHelper { + let mut ret = CredentialHelper { + protocol: None, + host: None, + port: None, + path: None, + username: None, + url: url.to_string(), + commands: Vec::new(), + }; + + // Parse out the (protocol, host) if one is available + if let Ok(url) = url::Url::parse(url) { + if let Some(url::Host::Domain(s)) = url.host() { + ret.host = Some(s.to_string()); + } + ret.port = url.port(); + ret.protocol = Some(url.scheme().to_string()); + } + ret + } + + /// Set the username that this credential helper will query with. + /// + /// By default the username is `None`. + pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper { + self.username = username.map(|s| s.to_string()); + self + } + + /// Query the specified configuration object to discover commands to + /// execute, usernames to query, etc. + pub fn config(&mut self, config: &Config) -> &mut CredentialHelper { + // Figure out the configured username/helper program. + // + // see http://git-scm.com/docs/gitcredentials.html#_configuration_options + if self.username.is_none() { + self.config_username(config); + } + self.config_helper(config); + self.config_use_http_path(config); + self + } + + // Configure the queried username from `config` + fn config_username(&mut self, config: &Config) { + let key = self.exact_key("username"); + self.username = config + .get_string(&key) + .ok() + .or_else(|| { + self.url_key("username") + .and_then(|s| config.get_string(&s).ok()) + }) + .or_else(|| config.get_string("credential.username").ok()) + } + + // Discover all `helper` directives from `config` + fn config_helper(&mut self, config: &Config) { + let exact = config.get_string(&self.exact_key("helper")); + self.add_command(exact.as_ref().ok().map(|s| &s[..])); + if let Some(key) = self.url_key("helper") { + let url = config.get_string(&key); + self.add_command(url.as_ref().ok().map(|s| &s[..])); + } + let global = config.get_string("credential.helper"); + self.add_command(global.as_ref().ok().map(|s| &s[..])); + } + + // Discover `useHttpPath` from `config` + fn config_use_http_path(&mut self, config: &Config) { + let mut use_http_path = false; + if let Some(value) = config.get_bool(&self.exact_key("useHttpPath")).ok() { + use_http_path = value; + } else if let Some(value) = self + .url_key("useHttpPath") + .and_then(|key| config.get_bool(&key).ok()) + { + use_http_path = value; + } else if let Some(value) = config.get_bool("credential.useHttpPath").ok() { + use_http_path = value; + } + + if use_http_path { + if let Ok(url) = url::Url::parse(&self.url) { + let path = url.path(); + // Url::parse always includes a leading slash for rooted URLs, while git does not. + self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string()); + } + } + } + + // Add a `helper` configured command to the list of commands to execute. + // + // see https://www.kernel.org/pub/software/scm/git/docs/technical + // /api-credentials.html#_credential_helpers + fn add_command(&mut self, cmd: Option<&str>) { + let cmd = match cmd { + Some("") | None => return, + Some(s) => s, + }; + + if cmd.starts_with('!') { + self.commands.push(cmd[1..].to_string()); + } else if cmd.contains("/") || cmd.contains("\\") { + self.commands.push(cmd.to_string()); + } else { + self.commands.push(format!("git credential-{}", cmd)); + } + } + + fn exact_key(&self, name: &str) -> String { + format!("credential.{}.{}", self.url, name) + } + + fn url_key(&self, name: &str) -> Option<String> { + match (&self.host, &self.protocol) { + (&Some(ref host), &Some(ref protocol)) => { + Some(format!("credential.{}://{}.{}", protocol, host, name)) + } + _ => None, + } + } + + /// Execute this helper, attempting to discover a username/password pair. + /// + /// All I/O errors are ignored, (to match git behavior), and this function + /// only succeeds if both a username and a password were found + pub fn execute(&self) -> Option<(String, String)> { + let mut username = self.username.clone(); + let mut password = None; + for cmd in &self.commands { + let (u, p) = self.execute_cmd(cmd, &username); + if u.is_some() && username.is_none() { + username = u; + } + if p.is_some() && password.is_none() { + password = p; + } + if username.is_some() && password.is_some() { + break; + } + } + + match (username, password) { + (Some(u), Some(p)) => Some((u, p)), + _ => None, + } + } + + // Execute the given `cmd`, providing the appropriate variables on stdin and + // then afterwards parsing the output into the username/password on stdout. + fn execute_cmd( + &self, + cmd: &str, + username: &Option<String>, + ) -> (Option<String>, Option<String>) { + macro_rules! my_try( ($e:expr) => ( + match $e { + Ok(e) => e, + Err(e) => { + debug!("{} failed with {}", stringify!($e), e); + return (None, None) + } + } + ) ); + + // It looks like the `cmd` specification is typically bourne-shell-like + // syntax, so try that first. If that fails, though, we may be on a + // Windows machine for example where `sh` isn't actually available by + // default. Most credential helper configurations though are pretty + // simple (aka one or two space-separated strings) so also try to invoke + // the process directly. + // + // If that fails then it's up to the user to put `sh` in path and make + // sure it works. + let mut c = Command::new("sh"); + c.arg("-c") + .arg(&format!("{} get", cmd)) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + debug!("executing credential helper {:?}", c); + let mut p = match c.spawn() { + Ok(p) => p, + Err(e) => { + debug!("`sh` failed to spawn: {}", e); + let mut parts = cmd.split_whitespace(); + let mut c = Command::new(parts.next().unwrap()); + for arg in parts { + c.arg(arg); + } + c.arg("get") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + debug!("executing credential helper {:?}", c); + match c.spawn() { + Ok(p) => p, + Err(e) => { + debug!("fallback of {:?} failed with {}", cmd, e); + return (None, None); + } + } + } + }; + + // Ignore write errors as the command may not actually be listening for + // stdin + { + let stdin = p.stdin.as_mut().unwrap(); + if let Some(ref p) = self.protocol { + let _ = writeln!(stdin, "protocol={}", p); + } + if let Some(ref p) = self.host { + if let Some(ref p2) = self.port { + let _ = writeln!(stdin, "host={}:{}", p, p2); + } else { + let _ = writeln!(stdin, "host={}", p); + } + } + if let Some(ref p) = self.path { + let _ = writeln!(stdin, "path={}", p); + } + if let Some(ref p) = *username { + let _ = writeln!(stdin, "username={}", p); + } + } + let output = my_try!(p.wait_with_output()); + if !output.status.success() { + debug!( + "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}", + output.status, + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + return (None, None); + } + trace!( + "credential helper stderr ---\n{}", + String::from_utf8_lossy(&output.stderr) + ); + self.parse_output(output.stdout) + } + + // Parse the output of a command into the username/password found + fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) { + // Parse the output of the command, looking for username/password + let mut username = None; + let mut password = None; + for line in output.split(|t| *t == b'\n') { + let mut parts = line.splitn(2, |t| *t == b'='); + let key = parts.next().unwrap(); + let value = match parts.next() { + Some(s) => s, + None => { + trace!("ignoring output line: {}", String::from_utf8_lossy(line)); + continue; + } + }; + let value = match String::from_utf8(value.to_vec()) { + Ok(s) => s, + Err(..) => continue, + }; + match key { + b"username" => username = Some(value), + b"password" => password = Some(value), + _ => {} + } + } + (username, password) + } +} + +#[cfg(test)] +mod test { + use std::env; + use std::fs::File; + use std::io::prelude::*; + use std::path::Path; + use tempfile::TempDir; + + use crate::{Config, ConfigLevel, Cred, CredentialHelper}; + + macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({ + let td = TempDir::new().unwrap(); + let mut cfg = Config::new().unwrap(); + cfg.add_file(&td.path().join("cfg"), ConfigLevel::Highest, false).unwrap(); + $(cfg.set_str($k, $v).unwrap();)* + cfg + }) ); + + #[test] + fn smoke() { + Cred::default().unwrap(); + } + + #[test] + fn credential_helper1() { + let cfg = test_cfg! { + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper2() { + let cfg = test_cfg! {}; + assert!(CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .is_none()); + } + + #[test] + fn credential_helper3() { + let cfg = test_cfg! { + "credential.https://example.com.helper" => + "!f() { echo username=c; }; f", + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "c"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper4() { + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + + let td = TempDir::new().unwrap(); + let path = td.path().join("script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=c +", + ) + .unwrap(); + chmod(&path); + let cfg = test_cfg! { + "credential.https://example.com.helper" => + &path.display().to_string()[..], + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "c"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper5() { + if !Path::new("/usr/bin/git").exists() { + return; + } //this test does not work if git is not installed + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + let td = TempDir::new().unwrap(); + let path = td.path().join("git-credential-script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=c +", + ) + .unwrap(); + chmod(&path); + + let paths = env::var("PATH").unwrap(); + let paths = + env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter()); + env::set_var("PATH", &env::join_paths(paths).unwrap()); + + let cfg = test_cfg! { + "credential.https://example.com.helper" => "script", + "credential.helper" => "!f() { echo username=a; echo password=b; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "c"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper6() { + let cfg = test_cfg! { + "credential.helper" => "" + }; + assert!(CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .is_none()); + } + + #[test] + fn credential_helper7() { + if cfg!(windows) { + return; + } // shell scripts don't work on Windows + let td = TempDir::new().unwrap(); + let path = td.path().join("script"); + File::create(&path) + .unwrap() + .write( + br"\ +#!/bin/sh +echo username=$1 +echo password=$2 +", + ) + .unwrap(); + chmod(&path); + let cfg = test_cfg! { + "credential.helper" => &format!("{} a b", path.display()) + }; + let (u, p) = CredentialHelper::new("https://example.com/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + fn credential_helper8() { + let cfg = test_cfg! { + "credential.useHttpPath" => "true" + }; + let mut helper = CredentialHelper::new("https://example.com/foo/bar"); + helper.config(&cfg); + assert_eq!(helper.path.as_deref(), Some("foo/bar")); + } + + #[test] + fn credential_helper9() { + let cfg = test_cfg! { + "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f" + }; + let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar") + .config(&cfg) + .execute() + .unwrap(); + assert_eq!(u, "a"); + assert_eq!(p, "b"); + } + + #[test] + #[cfg(feature = "ssh")] + fn ssh_key_from_memory() { + let cred = Cred::ssh_key_from_memory( + "test", + Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"), + r#" + -----BEGIN RSA PRIVATE KEY----- + Proc-Type: 4,ENCRYPTED + DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8 + + 3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd + H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4 + RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2 + vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD + aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS + os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L + g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p + VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz + YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn + M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2 + kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw + 1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk + g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF + b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E + tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r + HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7 + UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq + COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb + 37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX + qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5 + f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY + Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434 + BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq + c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY + 8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O + -----END RSA PRIVATE KEY----- + "#, + Some("test123")); + assert!(cred.is_ok()); + } + + #[cfg(unix)] + fn chmod(path: &Path) { + use std::fs; + use std::os::unix::prelude::*; + let mut perms = fs::metadata(path).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(path, perms).unwrap(); + } + #[cfg(windows)] + fn chmod(_path: &Path) {} +} |