From 10a354ec00f4d73ac2e2e692eb78d6ccf6a06ed4 Mon Sep 17 00:00:00 2001 From: Matthew Cather Date: Wed, 20 Sep 2023 21:57:26 -0500 Subject: [PATCH] Let user handle keyboard-interactive events Keyboard-interactive events can have multiple steps. Tweak the existing `kbd_callback` to massage prompts into a format that an end user can handle from python. New public method `userauth_keyboardinteractive_callback` added to the session class to maintain backwards compatibility. See new example script for usage. --- examples/keyboard_interactive_auth.py | 61 +++++++++++++++++++++++++++ ssh2/session.pyx | 52 ++++++++++++++++------- 2 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 examples/keyboard_interactive_auth.py diff --git a/examples/keyboard_interactive_auth.py b/examples/keyboard_interactive_auth.py new file mode 100644 index 00000000..0996439b --- /dev/null +++ b/examples/keyboard_interactive_auth.py @@ -0,0 +1,61 @@ +#!/usr/bin/python + +"""Example script for authentication with password""" + +from __future__ import print_function + +import argparse +import socket +import os +import pwd +import functools + + +from ssh2.session import Session + + +USERNAME = pwd.getpwuid(os.geteuid()).pw_name + +parser = argparse.ArgumentParser() + +parser.add_argument('password', help="User password") +parser.add_argument('oauth', help="OAUTH key to use for authentication") +parser.add_argument('cmd', help="Command to run") +parser.add_argument('--host', dest='host', + default='localhost', + help='Host to connect to') +parser.add_argument('--port', dest='port', default=22, help="Port to connect on", type=int) +parser.add_argument('-u', dest='user', default=USERNAME, help="User name to authenticate as") + + +def oauth_handler(name, instruction, prompts, password, oauth): + responses = [] + + for prompt in prompts: + if "Password:" in prompt: + responses.append(password) + if "One-time password (OATH) for" in prompt: + responses.append(oauth) + + return responses + +def main(): + args = parser.parse_args() + + callback = functools.partial(oauth_handler,password=args.password,oauth=args.oauth) + + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.connect((args.host, args.port)) + s = Session() + s.handshake(sock) + s.userauth_keyboardinteractive_callback(args.user, callback) + chan = s.open_session() + chan.execute(args.cmd) + size, data = chan.read() + while size > 0: + print(data) + size, data = chan.read() + + +if __name__ == "__main__": + main() diff --git a/ssh2/session.pyx b/ssh2/session.pyx index c88fb275..e8219008 100644 --- a/ssh2/session.pyx +++ b/ssh2/session.pyx @@ -15,7 +15,8 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA from cpython cimport PyObject_AsFileDescriptor -from libc.stdlib cimport malloc, free +from libc.stdlib cimport calloc, malloc, free +from libc.string cimport memcpy from libc.time cimport time_t from cython.operator cimport dereference as c_dereference @@ -26,7 +27,7 @@ from exceptions import SessionHostKeyError, KnownHostError, \ from listener cimport PyListener from sftp cimport PySFTP from publickey cimport PyPublicKeySystem -from utils cimport to_bytes, to_str, handle_error_codes +from utils cimport to_bytes, to_str, to_str_len, handle_error_codes from statinfo cimport StatInfo from knownhost cimport PyKnownHost from fileinfo cimport FileInfo @@ -79,16 +80,25 @@ cdef void kbd_callback(const char *name, int name_len, py_sess = (c_dereference(abstract)) if py_sess._kbd_callback is None: return - cdef bytes b_password = to_bytes(py_sess._kbd_callback()) - cdef size_t _len = len(b_password) - cdef char *_password = b_password - cdef char *_password_copy - if num_prompts == 1: - _password_copy = malloc(sizeof(char) * _len) - for i in range(_len): - _password_copy[i] = _password[i] - responses[0].text = _password_copy - responses[0].length = _len + + cdef list py_prompts = [] + for i in range(num_prompts): + prompt_len = prompts[i].length + py_prompts.append(to_str_len(prompts[i].text,prompt_len)) + + cdef list py_responses = py_sess._kbd_callback( name[:name_len], instruction[:instruction_len], py_prompts) + + cdef bytes response + for i in range(num_prompts): + response = to_bytes(py_responses[i]) + + _len = len(response) + _buff = calloc(sizeof(char), _len) + for j in range(_len): + _buff[j] = response[j] + + responses[i].text = _buff + responses[i].length = _len cdef class Session: @@ -313,6 +323,19 @@ cdef class Session: password not None): """Perform keyboard-interactive authentication + :param username: User name to authenticate. + :type username: str + :param password: Password + :type password: str + """ + def passwd(*args,password=password): + return [password] + return self.userauth_keyboardinteractive_callback(username, passwd) + + def userauth_keyboardinteractive_callback(self, username not None, + callback not None): + """Perform keyboard-interactive authentication + :param username: User name to authenticate. :type username: str :param password: Password @@ -322,10 +345,7 @@ cdef class Session: cdef bytes b_username = to_bytes(username) cdef const char *_username = b_username - def passwd(): - return password - - self._kbd_callback = passwd + self._kbd_callback = callback rc = c_ssh2.libssh2_userauth_keyboard_interactive( self._session, _username, &kbd_callback) self._kbd_callback = None