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 crate::util::Binding;
use crate::{raw, Config, Error, IntoCString};
pub struct Cred {
raw: *mut raw::git_cred,
}
pub struct CredentialHelper {
pub username: Option<String>,
protocol: Option<String>,
host: Option<String>,
port: Option<u16>,
path: Option<String>,
url: String,
commands: Vec<String>,
}
impl Cred {
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))
}
}
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))
}
}
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))
}
}
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))
}
}
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))
}
}
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",
)),
}
}
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))
}
}
pub fn has_username(&self) -> bool {
unsafe { raw::git_cred_has_username(self.raw) == 1 }
}
pub fn credtype(&self) -> raw::git_credtype_t {
unsafe { (*self.raw).credtype }
}
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 {
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(),
};
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
}
pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper {
self.username = username.map(|s| s.to_string());
self
}
pub fn config(&mut self, config: &Config) -> &mut CredentialHelper {
if self.username.is_none() {
self.config_username(config);
}
self.config_helper(config);
self.config_use_http_path(config);
self
}
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())
}
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[..]));
}
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();
self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string());
}
}
}
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,
}
}
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,
}
}
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)
}
}
) );
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);
}
}
}
};
{
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)
}
fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) {
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;
} 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 cfg!(windows) {
return;
} 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;
} 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) {}
}