1 use log::{debug, trace}; 2 use std::ffi::CString; 3 use std::io::Write; 4 use std::mem; 5 use std::path::Path; 6 use std::process::{Command, Stdio}; 7 use std::ptr; 8 use url; 9 10 use crate::util::Binding; 11 use crate::{raw, Config, Error, IntoCString}; 12 13 /// A structure to represent git credentials in libgit2. 14 pub struct Cred { 15 raw: *mut raw::git_cred, 16 } 17 18 /// Management of the gitcredentials(7) interface. 19 pub struct CredentialHelper { 20 /// A public field representing the currently discovered username from 21 /// configuration. 22 pub username: Option<String>, 23 protocol: Option<String>, 24 host: Option<String>, 25 port: Option<u16>, 26 path: Option<String>, 27 url: String, 28 commands: Vec<String>, 29 } 30 31 impl Cred { 32 /// Create a "default" credential usable for Negotiate mechanisms like NTLM 33 /// or Kerberos authentication. default() -> Result<Cred, Error>34 pub fn default() -> Result<Cred, Error> { 35 crate::init(); 36 let mut out = ptr::null_mut(); 37 unsafe { 38 try_call!(raw::git_cred_default_new(&mut out)); 39 Ok(Binding::from_raw(out)) 40 } 41 } 42 43 /// Create a new ssh key credential object used for querying an ssh-agent. 44 /// 45 /// The username specified is the username to authenticate. ssh_key_from_agent(username: &str) -> Result<Cred, Error>46 pub fn ssh_key_from_agent(username: &str) -> Result<Cred, Error> { 47 crate::init(); 48 let mut out = ptr::null_mut(); 49 let username = CString::new(username)?; 50 unsafe { 51 try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username)); 52 Ok(Binding::from_raw(out)) 53 } 54 } 55 56 /// Create a new passphrase-protected ssh key credential object. ssh_key( username: &str, publickey: Option<&Path>, privatekey: &Path, passphrase: Option<&str>, ) -> Result<Cred, Error>57 pub fn ssh_key( 58 username: &str, 59 publickey: Option<&Path>, 60 privatekey: &Path, 61 passphrase: Option<&str>, 62 ) -> Result<Cred, Error> { 63 crate::init(); 64 let username = CString::new(username)?; 65 let publickey = crate::opt_cstr(publickey)?; 66 let privatekey = privatekey.into_c_string()?; 67 let passphrase = crate::opt_cstr(passphrase)?; 68 let mut out = ptr::null_mut(); 69 unsafe { 70 try_call!(raw::git_cred_ssh_key_new( 71 &mut out, username, publickey, privatekey, passphrase 72 )); 73 Ok(Binding::from_raw(out)) 74 } 75 } 76 77 /// Create a new ssh key credential object reading the keys from memory. ssh_key_from_memory( username: &str, publickey: Option<&str>, privatekey: &str, passphrase: Option<&str>, ) -> Result<Cred, Error>78 pub fn ssh_key_from_memory( 79 username: &str, 80 publickey: Option<&str>, 81 privatekey: &str, 82 passphrase: Option<&str>, 83 ) -> Result<Cred, Error> { 84 crate::init(); 85 let username = CString::new(username)?; 86 let publickey = crate::opt_cstr(publickey)?; 87 let privatekey = CString::new(privatekey)?; 88 let passphrase = crate::opt_cstr(passphrase)?; 89 let mut out = ptr::null_mut(); 90 unsafe { 91 try_call!(raw::git_cred_ssh_key_memory_new( 92 &mut out, username, publickey, privatekey, passphrase 93 )); 94 Ok(Binding::from_raw(out)) 95 } 96 } 97 98 /// Create a new plain-text username and password credential object. userpass_plaintext(username: &str, password: &str) -> Result<Cred, Error>99 pub fn userpass_plaintext(username: &str, password: &str) -> Result<Cred, Error> { 100 crate::init(); 101 let username = CString::new(username)?; 102 let password = CString::new(password)?; 103 let mut out = ptr::null_mut(); 104 unsafe { 105 try_call!(raw::git_cred_userpass_plaintext_new( 106 &mut out, username, password 107 )); 108 Ok(Binding::from_raw(out)) 109 } 110 } 111 112 /// Attempt to read `credential.helper` according to gitcredentials(7) [1] 113 /// 114 /// This function will attempt to parse the user's `credential.helper` 115 /// configuration, invoke the necessary processes, and read off what the 116 /// username/password should be for a particular url. 117 /// 118 /// The returned credential type will be a username/password credential if 119 /// successful. 120 /// 121 /// [1]: https://www.kernel.org/pub/software/scm/git/docs/gitcredentials.html credential_helper( config: &Config, url: &str, username: Option<&str>, ) -> Result<Cred, Error>122 pub fn credential_helper( 123 config: &Config, 124 url: &str, 125 username: Option<&str>, 126 ) -> Result<Cred, Error> { 127 match CredentialHelper::new(url) 128 .config(config) 129 .username(username) 130 .execute() 131 { 132 Some((username, password)) => Cred::userpass_plaintext(&username, &password), 133 None => Err(Error::from_str( 134 "failed to acquire username/password \ 135 from local configuration", 136 )), 137 } 138 } 139 140 /// Create a credential to specify a username. 141 /// 142 /// This is used with ssh authentication to query for the username if none is 143 /// specified in the url. username(username: &str) -> Result<Cred, Error>144 pub fn username(username: &str) -> Result<Cred, Error> { 145 crate::init(); 146 let username = CString::new(username)?; 147 let mut out = ptr::null_mut(); 148 unsafe { 149 try_call!(raw::git_cred_username_new(&mut out, username)); 150 Ok(Binding::from_raw(out)) 151 } 152 } 153 154 /// Check whether a credential object contains username information. has_username(&self) -> bool155 pub fn has_username(&self) -> bool { 156 unsafe { raw::git_cred_has_username(self.raw) == 1 } 157 } 158 159 /// Return the type of credentials that this object represents. credtype(&self) -> raw::git_credtype_t160 pub fn credtype(&self) -> raw::git_credtype_t { 161 unsafe { (*self.raw).credtype } 162 } 163 164 /// Unwrap access to the underlying raw pointer, canceling the destructor unwrap(mut self) -> *mut raw::git_cred165 pub unsafe fn unwrap(mut self) -> *mut raw::git_cred { 166 mem::replace(&mut self.raw, ptr::null_mut()) 167 } 168 } 169 170 impl Binding for Cred { 171 type Raw = *mut raw::git_cred; 172 from_raw(raw: *mut raw::git_cred) -> Cred173 unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred { 174 Cred { raw } 175 } raw(&self) -> *mut raw::git_cred176 fn raw(&self) -> *mut raw::git_cred { 177 self.raw 178 } 179 } 180 181 impl Drop for Cred { drop(&mut self)182 fn drop(&mut self) { 183 if !self.raw.is_null() { 184 unsafe { 185 if let Some(f) = (*self.raw).free { 186 f(self.raw) 187 } 188 } 189 } 190 } 191 } 192 193 impl CredentialHelper { 194 /// Create a new credential helper object which will be used to probe git's 195 /// local credential configuration. 196 /// 197 /// The url specified is the namespace on which this will query credentials. 198 /// Invalid urls are currently ignored. new(url: &str) -> CredentialHelper199 pub fn new(url: &str) -> CredentialHelper { 200 let mut ret = CredentialHelper { 201 protocol: None, 202 host: None, 203 port: None, 204 path: None, 205 username: None, 206 url: url.to_string(), 207 commands: Vec::new(), 208 }; 209 210 // Parse out the (protocol, host) if one is available 211 if let Ok(url) = url::Url::parse(url) { 212 if let Some(url::Host::Domain(s)) = url.host() { 213 ret.host = Some(s.to_string()); 214 } 215 ret.port = url.port(); 216 ret.protocol = Some(url.scheme().to_string()); 217 } 218 ret 219 } 220 221 /// Set the username that this credential helper will query with. 222 /// 223 /// By default the username is `None`. username(&mut self, username: Option<&str>) -> &mut CredentialHelper224 pub fn username(&mut self, username: Option<&str>) -> &mut CredentialHelper { 225 self.username = username.map(|s| s.to_string()); 226 self 227 } 228 229 /// Query the specified configuration object to discover commands to 230 /// execute, usernames to query, etc. config(&mut self, config: &Config) -> &mut CredentialHelper231 pub fn config(&mut self, config: &Config) -> &mut CredentialHelper { 232 // Figure out the configured username/helper program. 233 // 234 // see http://git-scm.com/docs/gitcredentials.html#_configuration_options 235 if self.username.is_none() { 236 self.config_username(config); 237 } 238 self.config_helper(config); 239 self.config_use_http_path(config); 240 self 241 } 242 243 // Configure the queried username from `config` config_username(&mut self, config: &Config)244 fn config_username(&mut self, config: &Config) { 245 let key = self.exact_key("username"); 246 self.username = config 247 .get_string(&key) 248 .ok() 249 .or_else(|| { 250 self.url_key("username") 251 .and_then(|s| config.get_string(&s).ok()) 252 }) 253 .or_else(|| config.get_string("credential.username").ok()) 254 } 255 256 // Discover all `helper` directives from `config` config_helper(&mut self, config: &Config)257 fn config_helper(&mut self, config: &Config) { 258 let exact = config.get_string(&self.exact_key("helper")); 259 self.add_command(exact.as_ref().ok().map(|s| &s[..])); 260 if let Some(key) = self.url_key("helper") { 261 let url = config.get_string(&key); 262 self.add_command(url.as_ref().ok().map(|s| &s[..])); 263 } 264 let global = config.get_string("credential.helper"); 265 self.add_command(global.as_ref().ok().map(|s| &s[..])); 266 } 267 268 // Discover `useHttpPath` from `config` config_use_http_path(&mut self, config: &Config)269 fn config_use_http_path(&mut self, config: &Config) { 270 let mut use_http_path = false; 271 if let Some(value) = config.get_bool(&self.exact_key("useHttpPath")).ok() { 272 use_http_path = value; 273 } else if let Some(value) = self 274 .url_key("useHttpPath") 275 .and_then(|key| config.get_bool(&key).ok()) 276 { 277 use_http_path = value; 278 } else if let Some(value) = config.get_bool("credential.useHttpPath").ok() { 279 use_http_path = value; 280 } 281 282 if use_http_path { 283 if let Ok(url) = url::Url::parse(&self.url) { 284 let path = url.path(); 285 // Url::parse always includes a leading slash for rooted URLs, while git does not. 286 self.path = Some(path.strip_prefix('/').unwrap_or(path).to_string()); 287 } 288 } 289 } 290 291 // Add a `helper` configured command to the list of commands to execute. 292 // 293 // see https://www.kernel.org/pub/software/scm/git/docs/technical 294 // /api-credentials.html#_credential_helpers add_command(&mut self, cmd: Option<&str>)295 fn add_command(&mut self, cmd: Option<&str>) { 296 let cmd = match cmd { 297 Some("") | None => return, 298 Some(s) => s, 299 }; 300 301 if cmd.starts_with('!') { 302 self.commands.push(cmd[1..].to_string()); 303 } else if cmd.contains("/") || cmd.contains("\\") { 304 self.commands.push(cmd.to_string()); 305 } else { 306 self.commands.push(format!("git credential-{}", cmd)); 307 } 308 } 309 310 fn exact_key(&self, name: &str) -> String { 311 format!("credential.{}.{}", self.url, name) 312 } 313 314 fn url_key(&self, name: &str) -> Option<String> { 315 match (&self.host, &self.protocol) { 316 (&Some(ref host), &Some(ref protocol)) => { 317 Some(format!("credential.{}://{}.{}", protocol, host, name)) 318 } 319 _ => None, 320 } 321 } 322 323 /// Execute this helper, attempting to discover a username/password pair. 324 /// 325 /// All I/O errors are ignored, (to match git behavior), and this function 326 /// only succeeds if both a username and a password were found execute(&self) -> Option<(String, String)>327 pub fn execute(&self) -> Option<(String, String)> { 328 let mut username = self.username.clone(); 329 let mut password = None; 330 for cmd in &self.commands { 331 let (u, p) = self.execute_cmd(cmd, &username); 332 if u.is_some() && username.is_none() { 333 username = u; 334 } 335 if p.is_some() && password.is_none() { 336 password = p; 337 } 338 if username.is_some() && password.is_some() { 339 break; 340 } 341 } 342 343 match (username, password) { 344 (Some(u), Some(p)) => Some((u, p)), 345 _ => None, 346 } 347 } 348 349 // Execute the given `cmd`, providing the appropriate variables on stdin and 350 // then afterwards parsing the output into the username/password on stdout. execute_cmd( &self, cmd: &str, username: &Option<String>, ) -> (Option<String>, Option<String>)351 fn execute_cmd( 352 &self, 353 cmd: &str, 354 username: &Option<String>, 355 ) -> (Option<String>, Option<String>) { 356 macro_rules! my_try( ($e:expr) => ( 357 match $e { 358 Ok(e) => e, 359 Err(e) => { 360 debug!("{} failed with {}", stringify!($e), e); 361 return (None, None) 362 } 363 } 364 ) ); 365 366 // It looks like the `cmd` specification is typically bourne-shell-like 367 // syntax, so try that first. If that fails, though, we may be on a 368 // Windows machine for example where `sh` isn't actually available by 369 // default. Most credential helper configurations though are pretty 370 // simple (aka one or two space-separated strings) so also try to invoke 371 // the process directly. 372 // 373 // If that fails then it's up to the user to put `sh` in path and make 374 // sure it works. 375 let mut c = Command::new("sh"); 376 c.arg("-c") 377 .arg(&format!("{} get", cmd)) 378 .stdin(Stdio::piped()) 379 .stdout(Stdio::piped()) 380 .stderr(Stdio::piped()); 381 debug!("executing credential helper {:?}", c); 382 let mut p = match c.spawn() { 383 Ok(p) => p, 384 Err(e) => { 385 debug!("`sh` failed to spawn: {}", e); 386 let mut parts = cmd.split_whitespace(); 387 let mut c = Command::new(parts.next().unwrap()); 388 for arg in parts { 389 c.arg(arg); 390 } 391 c.arg("get") 392 .stdin(Stdio::piped()) 393 .stdout(Stdio::piped()) 394 .stderr(Stdio::piped()); 395 debug!("executing credential helper {:?}", c); 396 match c.spawn() { 397 Ok(p) => p, 398 Err(e) => { 399 debug!("fallback of {:?} failed with {}", cmd, e); 400 return (None, None); 401 } 402 } 403 } 404 }; 405 406 // Ignore write errors as the command may not actually be listening for 407 // stdin 408 { 409 let stdin = p.stdin.as_mut().unwrap(); 410 if let Some(ref p) = self.protocol { 411 let _ = writeln!(stdin, "protocol={}", p); 412 } 413 if let Some(ref p) = self.host { 414 if let Some(ref p2) = self.port { 415 let _ = writeln!(stdin, "host={}:{}", p, p2); 416 } else { 417 let _ = writeln!(stdin, "host={}", p); 418 } 419 } 420 if let Some(ref p) = self.path { 421 let _ = writeln!(stdin, "path={}", p); 422 } 423 if let Some(ref p) = *username { 424 let _ = writeln!(stdin, "username={}", p); 425 } 426 } 427 let output = my_try!(p.wait_with_output()); 428 if !output.status.success() { 429 debug!( 430 "credential helper failed: {}\nstdout ---\n{}\nstderr ---\n{}", 431 output.status, 432 String::from_utf8_lossy(&output.stdout), 433 String::from_utf8_lossy(&output.stderr) 434 ); 435 return (None, None); 436 } 437 trace!( 438 "credential helper stderr ---\n{}", 439 String::from_utf8_lossy(&output.stderr) 440 ); 441 self.parse_output(output.stdout) 442 } 443 444 // Parse the output of a command into the username/password found parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>)445 fn parse_output(&self, output: Vec<u8>) -> (Option<String>, Option<String>) { 446 // Parse the output of the command, looking for username/password 447 let mut username = None; 448 let mut password = None; 449 for line in output.split(|t| *t == b'\n') { 450 let mut parts = line.splitn(2, |t| *t == b'='); 451 let key = parts.next().unwrap(); 452 let value = match parts.next() { 453 Some(s) => s, 454 None => { 455 trace!("ignoring output line: {}", String::from_utf8_lossy(line)); 456 continue; 457 } 458 }; 459 let value = match String::from_utf8(value.to_vec()) { 460 Ok(s) => s, 461 Err(..) => continue, 462 }; 463 match key { 464 b"username" => username = Some(value), 465 b"password" => password = Some(value), 466 _ => {} 467 } 468 } 469 (username, password) 470 } 471 } 472 473 #[cfg(test)] 474 mod test { 475 use std::env; 476 use std::fs::File; 477 use std::io::prelude::*; 478 use std::path::Path; 479 use tempfile::TempDir; 480 481 use crate::{Config, ConfigLevel, Cred, CredentialHelper}; 482 483 macro_rules! test_cfg( ($($k:expr => $v:expr),*) => ({ 484 let td = TempDir::new().unwrap(); 485 let mut cfg = Config::new().unwrap(); 486 cfg.add_file(&td.path().join("cfg"), ConfigLevel::Highest, false).unwrap(); 487 $(cfg.set_str($k, $v).unwrap();)* 488 cfg 489 }) ); 490 491 #[test] smoke()492 fn smoke() { 493 Cred::default().unwrap(); 494 } 495 496 #[test] credential_helper1()497 fn credential_helper1() { 498 let cfg = test_cfg! { 499 "credential.helper" => "!f() { echo username=a; echo password=b; }; f" 500 }; 501 let (u, p) = CredentialHelper::new("https://example.com/foo/bar") 502 .config(&cfg) 503 .execute() 504 .unwrap(); 505 assert_eq!(u, "a"); 506 assert_eq!(p, "b"); 507 } 508 509 #[test] credential_helper2()510 fn credential_helper2() { 511 let cfg = test_cfg! {}; 512 assert!(CredentialHelper::new("https://example.com/foo/bar") 513 .config(&cfg) 514 .execute() 515 .is_none()); 516 } 517 518 #[test] credential_helper3()519 fn credential_helper3() { 520 let cfg = test_cfg! { 521 "credential.https://example.com.helper" => 522 "!f() { echo username=c; }; f", 523 "credential.helper" => "!f() { echo username=a; echo password=b; }; f" 524 }; 525 let (u, p) = CredentialHelper::new("https://example.com/foo/bar") 526 .config(&cfg) 527 .execute() 528 .unwrap(); 529 assert_eq!(u, "c"); 530 assert_eq!(p, "b"); 531 } 532 533 #[test] credential_helper4()534 fn credential_helper4() { 535 if cfg!(windows) { 536 return; 537 } // shell scripts don't work on Windows 538 539 let td = TempDir::new().unwrap(); 540 let path = td.path().join("script"); 541 File::create(&path) 542 .unwrap() 543 .write( 544 br"\ 545 #!/bin/sh 546 echo username=c 547 ", 548 ) 549 .unwrap(); 550 chmod(&path); 551 let cfg = test_cfg! { 552 "credential.https://example.com.helper" => 553 &path.display().to_string()[..], 554 "credential.helper" => "!f() { echo username=a; echo password=b; }; f" 555 }; 556 let (u, p) = CredentialHelper::new("https://example.com/foo/bar") 557 .config(&cfg) 558 .execute() 559 .unwrap(); 560 assert_eq!(u, "c"); 561 assert_eq!(p, "b"); 562 } 563 564 #[test] credential_helper5()565 fn credential_helper5() { 566 if cfg!(windows) { 567 return; 568 } // shell scripts don't work on Windows 569 let td = TempDir::new().unwrap(); 570 let path = td.path().join("git-credential-script"); 571 File::create(&path) 572 .unwrap() 573 .write( 574 br"\ 575 #!/bin/sh 576 echo username=c 577 ", 578 ) 579 .unwrap(); 580 chmod(&path); 581 582 let paths = env::var("PATH").unwrap(); 583 let paths = 584 env::split_paths(&paths).chain(path.parent().map(|p| p.to_path_buf()).into_iter()); 585 env::set_var("PATH", &env::join_paths(paths).unwrap()); 586 587 let cfg = test_cfg! { 588 "credential.https://example.com.helper" => "script", 589 "credential.helper" => "!f() { echo username=a; echo password=b; }; f" 590 }; 591 let (u, p) = CredentialHelper::new("https://example.com/foo/bar") 592 .config(&cfg) 593 .execute() 594 .unwrap(); 595 assert_eq!(u, "c"); 596 assert_eq!(p, "b"); 597 } 598 599 #[test] credential_helper6()600 fn credential_helper6() { 601 let cfg = test_cfg! { 602 "credential.helper" => "" 603 }; 604 assert!(CredentialHelper::new("https://example.com/foo/bar") 605 .config(&cfg) 606 .execute() 607 .is_none()); 608 } 609 610 #[test] credential_helper7()611 fn credential_helper7() { 612 if cfg!(windows) { 613 return; 614 } // shell scripts don't work on Windows 615 let td = TempDir::new().unwrap(); 616 let path = td.path().join("script"); 617 File::create(&path) 618 .unwrap() 619 .write( 620 br"\ 621 #!/bin/sh 622 echo username=$1 623 echo password=$2 624 ", 625 ) 626 .unwrap(); 627 chmod(&path); 628 let cfg = test_cfg! { 629 "credential.helper" => &format!("{} a b", path.display()) 630 }; 631 let (u, p) = CredentialHelper::new("https://example.com/foo/bar") 632 .config(&cfg) 633 .execute() 634 .unwrap(); 635 assert_eq!(u, "a"); 636 assert_eq!(p, "b"); 637 } 638 639 #[test] credential_helper8()640 fn credential_helper8() { 641 let cfg = test_cfg! { 642 "credential.useHttpPath" => "true" 643 }; 644 let mut helper = CredentialHelper::new("https://example.com/foo/bar"); 645 helper.config(&cfg); 646 assert_eq!(helper.path.as_deref(), Some("foo/bar")); 647 } 648 649 #[test] credential_helper9()650 fn credential_helper9() { 651 let cfg = test_cfg! { 652 "credential.helper" => "!f() { while read line; do eval $line; done; if [ \"$host\" = example.com:3000 ]; then echo username=a; echo password=b; fi; }; f" 653 }; 654 let (u, p) = CredentialHelper::new("https://example.com:3000/foo/bar") 655 .config(&cfg) 656 .execute() 657 .unwrap(); 658 assert_eq!(u, "a"); 659 assert_eq!(p, "b"); 660 } 661 662 #[test] 663 #[cfg(feature = "ssh")] ssh_key_from_memory()664 fn ssh_key_from_memory() { 665 let cred = Cred::ssh_key_from_memory( 666 "test", 667 Some("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDByAO8uj+kXicj6C2ODMspgmUoVyl5eaw8vR6a1yEnFuJFzevabNlN6Ut+CPT3TRnYk5BW73pyXBtnSL2X95BOnbjMDXc4YIkgs3YYHWnxbqsD4Pj/RoGqhf+gwhOBtL0poh8tT8WqXZYxdJQKLQC7oBqf3ykCEYulE4oeRUmNh4IzEE+skD/zDkaJ+S1HRD8D8YCiTO01qQnSmoDFdmIZTi8MS8Cw+O/Qhym1271ThMlhD6PubSYJXfE6rVbE7A9RzH73A6MmKBlzK8VTb4SlNSrr/DOk+L0uq+wPkv+pm+D9WtxoqQ9yl6FaK1cPawa3+7yRNle3m+72KCtyMkQv"), 668 r#" 669 -----BEGIN RSA PRIVATE KEY----- 670 Proc-Type: 4,ENCRYPTED 671 DEK-Info: AES-128-CBC,818C7722D3B01F2161C2ACF6A5BBAAE8 672 673 3Cht4QB3PcoQ0I55j1B3m2ZzIC/mrh+K5nQeA1Vy2GBTMyM7yqGHqTOv7qLhJscd 674 H+cB0Pm6yCr3lYuNrcKWOCUto+91P7ikyARruHVwyIxKdNx15uNulOzQJHQWNbA4 675 RQHlhjON4atVo2FyJ6n+ujK6QiBg2PR5Vbbw/AtV6zBCFW3PhzDn+qqmHjpBFqj2 676 vZUUe+MkDQcaF5J45XMHahhSdo/uKCDhfbylExp/+ACWkvxdPpsvcARM6X434ucD 677 aPY+4i0/JyLkdbm0GFN9/q3i53qf4kCBhojFl4AYJdGI0AzAgbdTXZ7EJHbAGZHS 678 os5K0oTwDVXMI0sSE2I/qHxaZZsDP1dOKq6di6SFPUp8liYimm7rNintRX88Gl2L 679 g1ko9abp/NlgD0YY/3mad+NNAISDL/YfXq2fklH3En3/7ZrOVZFKfZXwQwas5g+p 680 VQPKi3+ae74iOjLyuPDSc1ePmhUNYeP+9rLSc0wiaiHqls+2blPPDxAGMEo63kbz 681 YPVjdmuVX4VWnyEsfTxxJdFDYGSNh6rlrrO1RFrex7kJvpg5gTX4M/FT8TfCd7Hn 682 M6adXsLMqwu5tz8FuDmAtVdq8zdSrgZeAbpJ9D3EDOmZ70xz4XBL19ImxDp+Qqs2 683 kQX7kobRzeeP2URfRoGr7XZikQWyQ2UASfPcQULY8R58QoZWWsQ4w51GZHg7TDnw 684 1DRo/0OgkK7Gqf215nFmMpB4uyi58cq3WFwWQa1IqslkObpVgBQZcNZb/hKUYPGk 685 g4zehfIgAfCdnQHwZvQ6Fdzhcs3SZeO+zVyuiZN3Gsi9HU0/1vpAKiuuOzcG02vF 686 b6Y6hwsAA9yphF3atI+ARD4ZwXdDfzuGb3yJglMT3Fr/xuLwAvdchRo1spANKA0E 687 tT5okLrK0H4wnHvf2SniVVWRhmJis0lQo9LjGGwRIdsPpVnJSDvaISIVF+fHT90r 688 HvxN8zXI93x9jcPtwp7puQ1C7ehKJK10sZ71OLIZeuUgwt+5DRunqg6evPco9Go7 689 UOGwcVhLY200KT+1k7zWzCS0yVQp2HRm6cxsZXAp4ClBSwIx15eIoLIrjZdJRjCq 690 COp6pZx1fnvJ9ERIvl5hon+Ty+renMcFKz2HmchC7egpcqIxW9Dsv6zjhHle6pxb 691 37GaEKHF2KA3RN+dSV/K8n+C9Yent5tx5Y9a/pMcgRGtgu+G+nyFmkPKn5Zt39yX 692 qDpyM0LtbRVZPs+MgiqoGIwYc/ujoCq7GL38gezsBQoHaTt79yYBqCp6UR0LMuZ5 693 f/7CtWqffgySfJ/0wjGidDAumDv8CK45AURpL/Z+tbFG3M9ar/LZz/Y6EyBcLtGY 694 Wwb4zs8zXIA0qHrjNTnPqHDvezziArYfgPjxCIHMZzms9Yn8+N02p39uIytqg434 695 BAlCqZ7GYdDFfTpWIwX+segTK9ux0KdBqcQv+9Fwwjkq9KySnRKqNl7ZJcefFZJq 696 c6PA1iinZWBjuaO1HKx3PFulrl0bcpR9Kud1ZIyfnh5rwYN8UQkkcR/wZPla04TY 697 8l5dq/LI/3G5sZXwUHKOcuQWTj7Saq7Q6gkKoMfqt0wC5bpZ1m17GHPoMz6GtX9O 698 -----END RSA PRIVATE KEY----- 699 "#, 700 Some("test123")); 701 assert!(cred.is_ok()); 702 } 703 704 #[cfg(unix)] chmod(path: &Path)705 fn chmod(path: &Path) { 706 use std::fs; 707 use std::os::unix::prelude::*; 708 let mut perms = fs::metadata(path).unwrap().permissions(); 709 perms.set_mode(0o755); 710 fs::set_permissions(path, perms).unwrap(); 711 } 712 #[cfg(windows)] chmod(_path: &Path)713 fn chmod(_path: &Path) {} 714 } 715