summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--libssh2-sys/lib.rs31
-rw-r--r--src/lib.rs2
-rw-r--r--src/session.rs157
-rw-r--r--systest/build.rs8
-rw-r--r--tests/all/session.rs73
-rwxr-xr-xtests/run_integration_tests.sh1
6 files changed, 267 insertions, 5 deletions
diff --git a/libssh2-sys/lib.rs b/libssh2-sys/lib.rs
index 5ce1dc2..8157dab 100644
--- a/libssh2-sys/lib.rs
+++ b/libssh2-sys/lib.rs
@@ -266,6 +266,30 @@ pub type LIBSSH2_PASSWD_CHANGEREQ_FUNC = extern "C" fn(
abstrakt: *mut *mut c_void,
);
+pub type LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC = extern "C" fn(
+ username: *const c_char,
+ username_len: c_int,
+ instruction: *const c_char,
+ instruction_len: c_int,
+ num_prompts: c_int,
+ prompts: *const LIBSSH2_USERAUTH_KBDINT_PROMPT,
+ responses: *mut LIBSSH2_USERAUTH_KBDINT_RESPONSE,
+ abstrakt: *mut *mut c_void,
+);
+
+#[repr(C)]
+pub struct LIBSSH2_USERAUTH_KBDINT_PROMPT {
+ pub text: *mut c_char,
+ pub length: c_uint,
+ pub echo: c_uchar,
+}
+
+#[repr(C)]
+pub struct LIBSSH2_USERAUTH_KBDINT_RESPONSE {
+ pub text: *mut c_char,
+ pub length: c_uint,
+}
+
#[cfg(unix)]
pub type libssh2_socket_t = c_int;
#[cfg(all(windows, target_arch = "x86"))]
@@ -287,6 +311,7 @@ extern "C" {
realloc: Option<LIBSSH2_REALLOC_FUNC>,
abstrakt: *mut c_void,
) -> *mut LIBSSH2_SESSION;
+ pub fn libssh2_session_abstract(session: *mut LIBSSH2_SESSION) -> *mut *mut c_void;
pub fn libssh2_session_free(sess: *mut LIBSSH2_SESSION) -> c_int;
pub fn libssh2_session_banner_get(sess: *mut LIBSSH2_SESSION) -> *const c_char;
pub fn libssh2_session_banner_set(sess: *mut LIBSSH2_SESSION, banner: *const c_char) -> c_int;
@@ -499,6 +524,12 @@ extern "C" {
password_len: c_uint,
password_change_cb: Option<LIBSSH2_PASSWD_CHANGEREQ_FUNC>,
) -> c_int;
+ pub fn libssh2_userauth_keyboard_interactive_ex(
+ session: *mut LIBSSH2_SESSION,
+ username: *const c_char,
+ username_len: c_uint,
+ callback: Option<LIBSSH2_USERAUTH_KBDINT_RESPONSE_FUNC>,
+ ) -> c_int;
// knownhost
pub fn libssh2_knownhost_free(hosts: *mut LIBSSH2_KNOWNHOSTS);
diff --git a/src/lib.rs b/src/lib.rs
index 22534de..0d5a3b8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -142,7 +142,7 @@ pub use error::Error;
pub use knownhosts::{Host, Hosts, KnownHosts};
pub use listener::Listener;
use session::SessionInner;
-pub use session::{ScpFileStat, Session};
+pub use session::{KeyboardInteractivePrompt, Prompt, ScpFileStat, Session};
pub use sftp::{File, FileStat, FileType, OpenType};
pub use sftp::{OpenFlags, RenameFlags, Sftp};
pub use DisconnectCode::{AuthCancelledByUser, TooManyConnections};
diff --git a/src/session.rs b/src/session.rs
index bce81ad..30be94d 100644
--- a/src/session.rs
+++ b/src/session.rs
@@ -1,6 +1,7 @@
#[cfg(unix)]
use libc::size_t;
-use libc::{self, c_int, c_long, c_uint, c_void};
+use libc::{self, c_char, c_int, c_long, c_uint, c_void};
+use std::borrow::Cow;
use std::cell::{Ref, RefCell};
use std::ffi::CString;
use std::mem;
@@ -14,6 +15,54 @@ use util;
use {raw, ByApplication, DisconnectCode, Error, HostKeyType};
use {Agent, Channel, HashType, KnownHosts, Listener, MethodType, Sftp};
+/// Called by libssh2 to respond to some number of challenges as part of
+/// keyboard interactive authentication.
+pub trait KeyboardInteractivePrompt {
+ /// `username` is the user name to be authenticated. It may not be the
+ /// same as the username passed to `Session::userauth_keyboard_interactive`,
+ /// and may be empty.
+ /// `instructions` is some informational text to be displayed to the user.
+ /// `prompts` is a series of prompts (or challenges) that must be responded
+ /// to.
+ /// The return value should be a Vec that holds one response for each prompt.
+ fn prompt<'a>(
+ &mut self,
+ username: &str,
+ instructions: &str,
+ prompts: &[Prompt<'a>],
+ ) -> Vec<String>;
+}
+
+/// A prompt/challenge returned as part of keyboard-interactive authentication
+#[derive(Debug)]
+pub struct Prompt<'a> {
+ /// The label to show when prompting the user
+ pub text: Cow<'a, str>,
+ /// If true, the response that the user inputs should be displayed
+ /// as they type. If false then treat it as a password entry and
+ /// do not display what is typed in response to this prompt.
+ pub echo: bool,
+}
+
+/// This is a little helper function that is perhaps slightly overkill for the
+/// current needs.
+/// It saves the current sess->abstract pointer and replaces it with a
+/// different values for the duration of the call to the supplied lambda.
+/// When the lambda returns, the original abstract value is restored
+/// and the result of the lambda is returned.
+unsafe fn with_abstract<R, F: FnOnce() -> R>(
+ sess: *mut raw::LIBSSH2_SESSION,
+ new_value: *mut c_void,
+ f: F,
+) -> R {
+ let abstrakt = raw::libssh2_session_abstract(sess);
+ let old_value = *abstrakt;
+ *abstrakt = new_value;
+ let res = f();
+ *abstrakt = old_value;
+ res
+}
+
pub(crate) struct SessionInner {
pub(crate) raw: *mut raw::LIBSSH2_SESSION,
tcp: RefCell<Option<TcpStream>>,
@@ -216,6 +265,112 @@ impl Session {
})
}
+ /// Attempt keyboard interactive authentication.
+ ///
+ /// You must supply a callback function to
+ pub fn userauth_keyboard_interactive<P: KeyboardInteractivePrompt>(
+ &self,
+ username: &str,
+ prompter: &mut P,
+ ) -> Result<(), Error> {
+ // hold on to your hats, this is a bit involved.
+ // The keyboard interactive callback is a bit tricksy, and we want to wrap the
+ // raw C types with something a bit safer and more ergonomic.
+ // Since the interface is defined in terms of a simple function pointer, wrapping
+ // is a bit awkward.
+ //
+ // The session struct has an abstrakt pointer reserved for
+ // the user of the embedding application, and that pointer is passed to the
+ // prompt callback. We can use this to store a pointer to some state so that
+ // we can manage the conversion.
+ //
+ // The prompts and responses are defined to be UTF-8, but we use from_utf8_lossy
+ // to avoid panics in case the server isn't conformant for whatever reason.
+ extern "C" fn prompt<P: KeyboardInteractivePrompt>(
+ username: *const c_char,
+ username_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,
+ abstrakt: *mut *mut c_void,
+ ) {
+ let prompter = unsafe { &mut **(abstrakt as *mut *mut P) };
+
+ let username =
+ unsafe { slice::from_raw_parts(username as *const u8, username_len as usize) };
+ let username = String::from_utf8_lossy(username);
+
+ let instruction = unsafe {
+ slice::from_raw_parts(instruction as *const u8, instruction_len as usize)
+ };
+ let instruction = String::from_utf8_lossy(instruction);
+
+ let prompts = unsafe { slice::from_raw_parts(prompts, num_prompts as usize) };
+ let responses =
+ unsafe { slice::from_raw_parts_mut(responses, num_prompts as usize) };
+
+ let prompts: Vec<Prompt> = prompts
+ .iter()
+ .map(|item| {
+ let data = unsafe {
+ slice::from_raw_parts(item.text as *const u8, item.length as usize)
+ };
+ Prompt {
+ text: String::from_utf8_lossy(data),
+ echo: item.echo != 0,
+ }
+ })
+ .collect();
+
+ // libssh2 wants to be able to free(3) the response strings, so allocate
+ // storage and copy the responses into appropriately owned memory.
+ // We can't simply call strdup(3) here because the rust string types
+ // are not NUL terminated.
+ fn strdup_string(s: &str) -> *mut c_char {
+ let len = s.len();
+ let ptr = unsafe { libc::malloc(len + 1) as *mut c_char };
+ if !ptr.is_null() {
+ unsafe {
+ ::std::ptr::copy_nonoverlapping(
+ s.as_bytes().as_ptr() as *const c_char,
+ ptr,
+ len,
+ );
+ *ptr.offset(len as isize) = 0;
+ }
+ }
+ ptr
+ }
+
+ for (i, response) in (*prompter)
+ .prompt(&username, &instruction, &prompts)
+ .into_iter()
+ .enumerate()
+ {
+ let ptr = strdup_string(&response);
+ if !ptr.is_null() {
+ responses[i].length = response.len() as c_uint;
+ } else {
+ responses[i].length = 0;
+ }
+ responses[i].text = ptr;
+ }
+ }
+
+ unsafe {
+ with_abstract(self.inner.raw, prompter as *mut P as *mut c_void, || {
+ self.rc(raw::libssh2_userauth_keyboard_interactive_ex(
+ self.inner.raw,
+ username.as_ptr() as *const _,
+ username.len() as c_uint,
+ Some(prompt::<P>),
+ ))
+ })
+ }
+ }
+
/// Attempt to perform SSH agent authentication.
///
/// This is a helper method for attempting to authenticate the current
diff --git a/systest/build.rs b/systest/build.rs
index 7d5c281..2b3e66a 100644
--- a/systest/build.rs
+++ b/systest/build.rs
@@ -24,7 +24,11 @@ fn main() {
s.to_string()
}
})
- .skip_type(|t| t.ends_with("FUNC"))
- .skip_fn(|f| f == "libssh2_userauth_password_ex" || f == "libssh2_session_init_ex");
+ .skip_type(|t| t.ends_with("FUNC") || t.contains("KBDINT"))
+ .skip_fn(|f| {
+ f == "libssh2_userauth_password_ex"
+ || f == "libssh2_session_init_ex"
+ || f == "libssh2_userauth_keyboard_interactive_ex"
+ });
cfg.generate("../libssh2-sys/lib.rs", "all.rs");
}
diff --git a/tests/all/session.rs b/tests/all/session.rs
index 2c636a2..5e45d53 100644
--- a/tests/all/session.rs
+++ b/tests/all/session.rs
@@ -4,7 +4,7 @@ use std::io::prelude::*;
use std::path::Path;
use tempdir::TempDir;
-use ssh2::{HashType, MethodType, Session};
+use ssh2::{HashType, KeyboardInteractivePrompt, MethodType, Prompt, Session};
#[test]
fn smoke() {
@@ -48,6 +48,77 @@ fn smoke_handshake() {
}
#[test]
+fn keyboard_interactive() {
+ let user = env::var("USER").unwrap();
+ let socket = ::socket();
+ let mut sess = Session::new().unwrap();
+ sess.handshake(socket).unwrap();
+ sess.host_key().unwrap();
+ let methods = sess.auth_methods(&user).unwrap();
+ assert!(methods.contains("keyboard-interactive"), "{}", methods);
+ assert!(!sess.authenticated());
+
+ // We don't know the correct response for whatever challenges
+ // will be returned to us, but that's ok; the purpose of this
+ // test is to check that we have some basically sane interaction
+ // with the library.
+
+ struct Prompter {
+ some_data: usize,
+ }
+
+ impl KeyboardInteractivePrompt for Prompter {
+ fn prompt<'a>(
+ &mut self,
+ username: &str,
+ instructions: &str,
+ prompts: &[Prompt<'a>],
+ ) -> Vec<String> {
+ // Sanity check that the pointer manipulation resolves and
+ // we read back our member data ok
+ assert_eq!(self.some_data, 42);
+
+ eprintln!("username: {}", username);
+ eprintln!("instructions: {}", instructions);
+ eprintln!("prompts: {:?}", prompts);
+
+ // Unfortunately, we can't make any assertions about username
+ // or instructions, as they can be empty (on my linux system)
+ // or may have arbitrary contents
+ // assert_eq!(username, env::var("USER").unwrap());
+ // assert!(!instructions.is_empty());
+
+ // Hopefully this isn't too brittle an assertion
+ if prompts.len() == 1 {
+ assert_eq!(prompts.len(), 1);
+ // Might be "Password: " or "Password:" or other variations
+ assert!(prompts[0].text.contains("sword"));
+ assert_eq!(prompts[0].echo, false);
+ } else {
+ // maybe there's some PAM configuration that results
+ // in multiple prompts. We can't make any real assertions
+ // in this case, other than that there has to be at least
+ // one prompt.
+ assert!(!prompts.is_empty());
+ }
+
+ prompts.iter().map(|_| "bogus".to_string()).collect()
+ }
+ }
+
+ let mut p = Prompter { some_data: 42 };
+
+ match sess.userauth_keyboard_interactive(&user, &mut p) {
+ Ok(_) => eprintln!("auth succeeded somehow(!)"),
+ Err(err) => eprintln!("auth failed as expected: {}", err),
+ };
+
+ // The only way this assertion will be false is if the person
+ // running these tests has "bogus" as their password
+ assert!(!sess.authenticated());
+}
+
+#[test]
fn keepalive() {
let sess = ::authed_session();
sess.set_keepalive(false, 10);
diff --git a/tests/run_integration_tests.sh b/tests/run_integration_tests.sh
index 784a734..a1f8b0b 100755
--- a/tests/run_integration_tests.sh
+++ b/tests/run_integration_tests.sh
@@ -46,6 +46,7 @@ UsePAM yes
X11Forwarding yes
PrintMotd yes
PermitTunnel yes
+KbdInteractiveAuthentication yes
AllowTcpForwarding yes
MaxStartups 500
# Relax modes when the repo is under eg: /var/tmp