diff --git a/Cargo.toml b/Cargo.toml index 27d9a92638..a89443ea41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ bitflags = "1.1.0" libc = "0.2" log = "0.4.8" libgit2-sys = { path = "libgit2-sys", version = "0.14.0" } +libssh2-sys = { version = "0.2.19", optional = true } [target."cfg(all(unix, not(target_os = \"macos\")))".dependencies] openssl-sys = { version = "0.9.0", optional = true } @@ -35,7 +36,7 @@ paste = "1" [features] unstable = [] default = ["ssh", "https", "ssh_key_from_memory"] -ssh = ["libgit2-sys/ssh"] +ssh = ["libgit2-sys/ssh", "libssh2-sys"] https = ["libgit2-sys/https", "openssl-sys", "openssl-probe"] vendored-libgit2 = ["libgit2-sys/vendored"] vendored-openssl = ["openssl-sys/vendored", "libgit2-sys/vendored-openssl"] diff --git a/src/cred.rs b/src/cred.rs index fdffd61540..370bd6687b 100644 --- a/src/cred.rs +++ b/src/cred.rs @@ -5,16 +5,23 @@ use std::mem; use std::path::Path; use std::process::{Command, Stdio}; use std::ptr; +use std::str; 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, +pub enum CredInner { + Cred(*mut raw::git_cred), + + #[cfg(feature = "ssh")] + Interactive { + username: String, + }, } +/// A structure to represent git credentials in libgit2. +pub struct Cred(pub(crate) CredInner); + /// Management of the gitcredentials(7) interface. pub struct CredentialHelper { /// A public field representing the currently discovered username from @@ -29,6 +36,10 @@ pub struct CredentialHelper { } impl Cred { + pub(crate) unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred { + Cred(CredInner::Cred(raw)) + } + /// Create a "default" credential usable for Negotiate mechanisms like NTLM /// or Kerberos authentication. pub fn default() -> Result { @@ -36,7 +47,7 @@ impl Cred { let mut out = ptr::null_mut(); unsafe { try_call!(raw::git_cred_default_new(&mut out)); - Ok(Binding::from_raw(out)) + Ok(Cred::from_raw(out)) } } @@ -49,7 +60,7 @@ impl Cred { let username = CString::new(username)?; unsafe { try_call!(raw::git_cred_ssh_key_from_agent(&mut out, username)); - Ok(Binding::from_raw(out)) + Ok(Cred::from_raw(out)) } } @@ -70,7 +81,7 @@ impl Cred { try_call!(raw::git_cred_ssh_key_new( &mut out, username, publickey, privatekey, passphrase )); - Ok(Binding::from_raw(out)) + Ok(Cred::from_raw(out)) } } @@ -91,7 +102,7 @@ impl Cred { try_call!(raw::git_cred_ssh_key_memory_new( &mut out, username, publickey, privatekey, passphrase )); - Ok(Binding::from_raw(out)) + Ok(Cred::from_raw(out)) } } @@ -105,7 +116,7 @@ impl Cred { try_call!(raw::git_cred_userpass_plaintext_new( &mut out, username, password )); - Ok(Binding::from_raw(out)) + Ok(Cred::from_raw(out)) } } @@ -147,49 +158,92 @@ impl Cred { let mut out = ptr::null_mut(); unsafe { try_call!(raw::git_cred_username_new(&mut out, username)); - Ok(Binding::from_raw(out)) + Ok(Cred::from_raw(out)) } } + /// Create a credential to react to interactive prompts. + /// + /// The first argument to the callback is the name of the authentication type + /// (eg. "One-time password"); the second argument is the instruction text. + /// + /// The callback can be set using [`RemoteCallbacks::ssh_interactive()`](crate::RemoteCallbacks::ssh_interactive()) + #[cfg(any(doc, feature = "ssh"))] + pub fn ssh_interactive(username: String) -> Cred { + Cred(CredInner::Interactive { username }) + } + /// Check whether a credential object contains username information. pub fn has_username(&self) -> bool { - unsafe { raw::git_cred_has_username(self.raw) == 1 } + match self.0 { + CredInner::Cred(inner) => unsafe { raw::git_cred_has_username(inner) == 1 }, + + #[cfg(feature = "ssh")] + CredInner::Interactive { .. } => true, + } } /// Return the type of credentials that this object represents. pub fn credtype(&self) -> raw::git_credtype_t { - unsafe { (*self.raw).credtype } + match self.0 { + CredInner::Cred(inner) => unsafe { (*inner).credtype }, + + #[cfg(feature = "ssh")] + CredInner::Interactive { .. } => raw::GIT_CREDTYPE_SSH_INTERACTIVE, + } } /// Unwrap access to the underlying raw pointer, canceling the destructor + /// + /// Panics if this was created using [`Self::ssh_interactive()`] pub unsafe fn unwrap(mut self) -> *mut raw::git_cred { - mem::replace(&mut self.raw, ptr::null_mut()) + match &mut self.0 { + CredInner::Cred(cred) => mem::replace(cred, ptr::null_mut()), + + #[cfg(feature = "ssh")] + CredInner::Interactive { .. } => panic!("git2 cred is not a real libgit2 cred"), + } } -} -impl Binding for Cred { - type Raw = *mut raw::git_cred; + /// Unwrap access to the underlying inner enum, canceling the destructor + pub(crate) unsafe fn unwrap_inner(mut self) -> CredInner { + match &mut self.0 { + CredInner::Cred(cred) => CredInner::Cred(mem::replace(cred, ptr::null_mut())), - unsafe fn from_raw(raw: *mut raw::git_cred) -> Cred { - Cred { raw } - } - fn raw(&self) -> *mut raw::git_cred { - self.raw + #[cfg(feature = "ssh")] + CredInner::Interactive { username } => CredInner::Interactive { + username: mem::replace(username, String::new()), + }, + } } } impl Drop for Cred { fn drop(&mut self) { - if !self.raw.is_null() { - unsafe { - if let Some(f) = (*self.raw).free { - f(self.raw) + #[allow(irrefutable_let_patterns)] + if let CredInner::Cred(raw) = self.0 { + if !raw.is_null() { + unsafe { + if let Some(f) = (*raw).free { + f(raw) + } } } } } } +#[cfg(any(doc, feature = "ssh"))] +/// A server-sent prompt for SSH interactive authentication +pub struct SshInteractivePrompt<'a> { + /// The prompt's name or instruction (human-readable) + pub text: std::borrow::Cow<'a, str>, + + /// Whether the user's display should be visible or hidden + /// (usually for passwords) + pub echo: bool, +} + impl CredentialHelper { /// Create a new credential helper object which will be used to probe git's /// local credential configuration. diff --git a/src/lib.rs b/src/lib.rs index c297ffe444..e55a84d28b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -143,6 +143,9 @@ pub use crate::util::IntoCString; pub use crate::version::Version; pub use crate::worktree::{Worktree, WorktreeAddOptions, WorktreeLockStatus, WorktreePruneOptions}; +#[cfg(any(doc, feature = "ssh"))] +pub use crate::cred::SshInteractivePrompt; + // Create a convinience method on bitflag struct which checks the given flag macro_rules! is_bit_set { ($name:ident, $flag:expr) => { diff --git a/src/remote_callbacks.rs b/src/remote_callbacks.rs index bcc73e85e9..db3b7aefc1 100644 --- a/src/remote_callbacks.rs +++ b/src/remote_callbacks.rs @@ -6,6 +6,7 @@ use std::slice; use std::str; use crate::cert::Cert; +use crate::cred::CredInner; use crate::util::Binding; use crate::{ panic, raw, Cred, CredentialType, Error, IndexerProgress, Oid, PackBuilderStage, Progress, @@ -25,6 +26,9 @@ pub struct RemoteCallbacks<'a> { update_tips: Option>>, certificate_check: Option>>, push_update_reference: Option>>, + + #[cfg(feature = "ssh")] + ssh_interactive: Option>>, } /// Callback used to acquire credentials for when a remote is fetched. @@ -76,6 +80,17 @@ pub type PushTransferProgress<'a> = dyn FnMut(usize, usize, usize) + 'a; /// * total pub type PackProgress<'a> = dyn FnMut(PackBuilderStage, usize, usize) + 'a; +#[cfg(feature = "ssh")] +/// Callback for push transfer progress +/// +/// Parameters: +/// * name +/// * instruction +/// * prompts +/// * responses +pub type SshInteractiveCallback<'a> = + dyn FnMut(&str, &str, &[crate::cred::SshInteractivePrompt<'a>], &mut [String]) + 'a; + impl<'a> Default for RemoteCallbacks<'a> { fn default() -> Self { Self::new() @@ -94,6 +109,9 @@ impl<'a> RemoteCallbacks<'a> { certificate_check: None, push_update_reference: None, push_progress: None, + + #[cfg(feature = "ssh")] + ssh_interactive: None, } } @@ -200,6 +218,23 @@ impl<'a> RemoteCallbacks<'a> { self.pack_progress = Some(Box::new(cb) as Box>); self } + + #[cfg(any(doc, feature = "ssh"))] + /// Function to call with SSH interactive prompts to write the responses + /// into the given mutable [String] slice + /// + /// Callback parameters: + /// - name + /// - instruction + /// - prompts + /// - responses + pub fn ssh_interactive(&mut self, cb: F) -> &mut RemoteCallbacks<'a> + where + F: FnMut(&str, &str, &[crate::cred::SshInteractivePrompt<'a>], &mut [String]) + 'a, + { + self.ssh_interactive = Some(Box::new(cb) as Box>); + self + } } impl<'a> Binding for RemoteCallbacks<'a> { @@ -256,11 +291,11 @@ extern "C" fn credentials_cb( url: *const c_char, username_from_url: *const c_char, allowed_types: c_uint, - payload: *mut c_void, + c_payload: *mut c_void, ) -> c_int { unsafe { let ok = panic::wrap(|| { - let payload = &mut *(payload as *mut RemoteCallbacks<'_>); + let payload = &mut *(c_payload as *mut RemoteCallbacks<'_>); let callback = payload .credentials .as_mut() @@ -277,11 +312,29 @@ extern "C" fn credentials_cb( let cred_type = CredentialType::from_bits_truncate(allowed_types as u32); - callback(url, username_from_url, cred_type).map_err(|e| { - let s = CString::new(e.to_string()).unwrap(); - raw::git_error_set_str(e.raw_code() as c_int, s.as_ptr()); - e.raw_code() as c_int - }) + callback(url, username_from_url, cred_type) + .and_then(|cred| match cred.unwrap_inner() { + CredInner::Cred(raw) => Ok(Cred::from_raw(raw)), + + #[cfg(feature = "ssh")] + CredInner::Interactive { username } => { + let username = CString::new(username)?; + let mut out = ptr::null_mut(); + try_call!(raw::git_cred_ssh_interactive_new( + &mut out, + username, + Some(ssh_interactive_cb as _), + c_payload + )); + + Ok(Cred::from_raw(out)) + } + }) + .map_err(|e| { + let s = CString::new(e.to_string()).unwrap(); + raw::git_error_set_str(e.raw_code() as c_int, s.as_ptr()); + e.raw_code() as c_int + }) }); match ok { Some(Ok(cred)) => { @@ -450,3 +503,63 @@ extern "C" fn pack_progress_cb( }) .unwrap_or(-1) } + +#[cfg(feature = "ssh")] +extern "C" fn ssh_interactive_cb( + name: *const c_char, + name_len: c_int, + instruction: *const c_char, + instruction_len: c_int, + num_prompts: c_int, + prompts: *const raw::LIBSSH2_USERAUTH_KBDINT_PROMPT, + responses: *mut raw::LIBSSH2_USERAUTH_KBDINT_RESPONSE, + payload: *mut *mut c_void, +) { + use libc::malloc; + + panic::wrap(|| unsafe { + let prompts = prompts as *const libssh2_sys::LIBSSH2_USERAUTH_KBDINT_PROMPT; + let responses = responses as *mut libssh2_sys::LIBSSH2_USERAUTH_KBDINT_RESPONSE; + + let name = + String::from_utf8_lossy(slice::from_raw_parts(name as *const u8, name_len as usize)); + let instruction = String::from_utf8_lossy(slice::from_raw_parts( + instruction as *const u8, + instruction_len as usize, + )); + + let mut wrapped_prompts = Vec::with_capacity(num_prompts as usize); + for i in 0..num_prompts { + let prompt = &*prompts.offset(i as isize); + wrapped_prompts.push(crate::cred::SshInteractivePrompt { + text: String::from_utf8_lossy(slice::from_raw_parts( + prompt.text as *const u8, + prompt.length as usize, + )), + echo: prompt.echo != 0, + }); + } + + let mut wrapped_responses = vec![String::new(); num_prompts as usize]; + + let payload = &mut *(payload as *mut Box>); + if let Some(callback) = &mut payload.ssh_interactive { + callback( + name.as_ref(), + instruction.as_ref(), + &wrapped_prompts[..], + &mut wrapped_responses[..], + ); + } + + for i in 0..num_prompts { + let response = &mut *responses.offset(i as isize); + let response_bytes = wrapped_responses[i as usize].as_bytes(); + + // libgit2 frees returned strings + let text = malloc(response_bytes.len()); + response.text = text as *mut c_char; + response.length = response_bytes.len() as u32; + } + }); +}