From 94e21d472768ff7d3bcfd0112bdfb48c8ebb596d Mon Sep 17 00:00:00 2001 From: kas Date: Sat, 13 Aug 2022 08:22:22 +0200 Subject: [PATCH 1/2] Add support for SSH interactive authentication --- Cargo.toml | 3 +- src/cred.rs | 85 ++++++++++++++++++++++---------- src/remote_callbacks.rs | 104 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 157 insertions(+), 35 deletions(-) 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..3b35e9f829 100644 --- a/src/cred.rs +++ b/src/cred.rs @@ -1,20 +1,24 @@ use log::{debug, trace}; +use std::borrow::Cow; use std::ffi::CString; use std::io::Write; 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), + 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 +33,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 +44,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 +57,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 +78,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 +99,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 +113,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 +155,74 @@ 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. + #[cfg(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 }, + 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 }, + 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()), + CredInner::Interactive { .. } => panic!("git2 cred is not a real libgit2 cred"), + } } -} -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 + /// 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())), + 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) + if let CredInner::Cred(raw) = self.0 { + if !raw.is_null() { + unsafe { + if let Some(f) = (*raw).free { + f(raw) + } } } } } } +pub struct SshInteractivePrompt<'a> { + pub text: Cow<'a, str>, + 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/remote_callbacks.rs b/src/remote_callbacks.rs index bcc73e85e9..22f7e8157a 100644 --- a/src/remote_callbacks.rs +++ b/src/remote_callbacks.rs @@ -1,4 +1,4 @@ -use libc::{c_char, c_int, c_uint, c_void, size_t}; +use libc::{c_char, c_int, c_uint, c_void, malloc, size_t}; use std::ffi::{CStr, CString}; use std::mem; use std::ptr; @@ -6,6 +6,7 @@ use std::slice; use std::str; use crate::cert::Cert; +use crate::cred::{CredInner, SshInteractivePrompt}; use crate::util::Binding; use crate::{ panic, raw, Cred, CredentialType, Error, IndexerProgress, Oid, PackBuilderStage, Progress, @@ -25,6 +26,7 @@ pub struct RemoteCallbacks<'a> { update_tips: Option>>, certificate_check: Option>>, push_update_reference: Option>>, + ssh_interactive: Option>>, } /// Callback used to acquire credentials for when a remote is fetched. @@ -76,6 +78,16 @@ pub type PushTransferProgress<'a> = dyn FnMut(usize, usize, usize) + 'a; /// * total pub type PackProgress<'a> = dyn FnMut(PackBuilderStage, usize, usize) + 'a; +/// Callback for push transfer progress +/// +/// Parameters: +/// * name +/// * instruction +/// * prompts +/// * responses +pub type SshInteractiveCallback<'a> = + dyn FnMut(&str, &str, &[SshInteractivePrompt<'a>], &mut [String]) + 'static; + impl<'a> Default for RemoteCallbacks<'a> { fn default() -> Self { Self::new() @@ -94,6 +106,7 @@ impl<'a> RemoteCallbacks<'a> { certificate_check: None, push_update_reference: None, push_progress: None, + ssh_interactive: None, } } @@ -256,11 +269,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 +290,28 @@ 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)), + + 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 +480,61 @@ 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, +) { + 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(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; + } + }); +} From d968b2c5644d60a0c41ccfeac5538c31baa71e1a Mon Sep 17 00:00:00 2001 From: kas Date: Sat, 13 Aug 2022 10:25:47 +0200 Subject: [PATCH 2/2] Fix build errors when ssh is not enabled Add missing documentation --- src/cred.rs | 29 +++++++++++++++++++++++++---- src/lib.rs | 3 +++ src/remote_callbacks.rs | 33 +++++++++++++++++++++++++++++---- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/cred.rs b/src/cred.rs index 3b35e9f829..370bd6687b 100644 --- a/src/cred.rs +++ b/src/cred.rs @@ -1,5 +1,4 @@ use log::{debug, trace}; -use std::borrow::Cow; use std::ffi::CString; use std::io::Write; use std::mem; @@ -13,7 +12,11 @@ use crate::{raw, Config, Error, IntoCString}; pub enum CredInner { Cred(*mut raw::git_cred), - Interactive { username: String }, + + #[cfg(feature = "ssh")] + Interactive { + username: String, + }, } /// A structure to represent git credentials in libgit2. @@ -163,7 +166,9 @@ impl Cred { /// /// The first argument to the callback is the name of the authentication type /// (eg. "One-time password"); the second argument is the instruction text. - #[cfg(feature = "ssh")] + /// + /// 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 }) } @@ -172,6 +177,8 @@ impl Cred { pub fn has_username(&self) -> bool { match self.0 { CredInner::Cred(inner) => unsafe { raw::git_cred_has_username(inner) == 1 }, + + #[cfg(feature = "ssh")] CredInner::Interactive { .. } => true, } } @@ -180,15 +187,20 @@ impl Cred { pub fn credtype(&self) -> raw::git_credtype_t { 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 { 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"), } } @@ -197,6 +209,8 @@ impl Cred { pub(crate) unsafe fn unwrap_inner(mut self) -> CredInner { match &mut self.0 { CredInner::Cred(cred) => CredInner::Cred(mem::replace(cred, ptr::null_mut())), + + #[cfg(feature = "ssh")] CredInner::Interactive { username } => CredInner::Interactive { username: mem::replace(username, String::new()), }, @@ -206,6 +220,7 @@ impl Cred { impl Drop for Cred { fn drop(&mut self) { + #[allow(irrefutable_let_patterns)] if let CredInner::Cred(raw) = self.0 { if !raw.is_null() { unsafe { @@ -218,8 +233,14 @@ impl Drop for Cred { } } +#[cfg(any(doc, feature = "ssh"))] +/// A server-sent prompt for SSH interactive authentication pub struct SshInteractivePrompt<'a> { - pub text: Cow<'a, str>, + /// 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, } 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 22f7e8157a..db3b7aefc1 100644 --- a/src/remote_callbacks.rs +++ b/src/remote_callbacks.rs @@ -1,4 +1,4 @@ -use libc::{c_char, c_int, c_uint, c_void, malloc, size_t}; +use libc::{c_char, c_int, c_uint, c_void, size_t}; use std::ffi::{CStr, CString}; use std::mem; use std::ptr; @@ -6,7 +6,7 @@ use std::slice; use std::str; use crate::cert::Cert; -use crate::cred::{CredInner, SshInteractivePrompt}; +use crate::cred::CredInner; use crate::util::Binding; use crate::{ panic, raw, Cred, CredentialType, Error, IndexerProgress, Oid, PackBuilderStage, Progress, @@ -26,6 +26,8 @@ pub struct RemoteCallbacks<'a> { update_tips: Option>>, certificate_check: Option>>, push_update_reference: Option>>, + + #[cfg(feature = "ssh")] ssh_interactive: Option>>, } @@ -78,6 +80,7 @@ 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: @@ -86,7 +89,7 @@ pub type PackProgress<'a> = dyn FnMut(PackBuilderStage, usize, usize) + 'a; /// * prompts /// * responses pub type SshInteractiveCallback<'a> = - dyn FnMut(&str, &str, &[SshInteractivePrompt<'a>], &mut [String]) + 'static; + dyn FnMut(&str, &str, &[crate::cred::SshInteractivePrompt<'a>], &mut [String]) + 'a; impl<'a> Default for RemoteCallbacks<'a> { fn default() -> Self { @@ -106,6 +109,8 @@ impl<'a> RemoteCallbacks<'a> { certificate_check: None, push_update_reference: None, push_progress: None, + + #[cfg(feature = "ssh")] ssh_interactive: None, } } @@ -213,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> { @@ -294,6 +316,7 @@ extern "C" fn credentials_cb( .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(); @@ -492,6 +515,8 @@ extern "C" fn ssh_interactive_cb( 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; @@ -506,7 +531,7 @@ extern "C" fn ssh_interactive_cb( 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(SshInteractivePrompt { + wrapped_prompts.push(crate::cred::SshInteractivePrompt { text: String::from_utf8_lossy(slice::from_raw_parts( prompt.text as *const u8, prompt.length as usize,