| 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 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) {} |
| } |