diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py index 95965fa..953a9cc 100644 --- a/paramiko/auth_handler.py +++ b/paramiko/auth_handler.py @@ -57,6 +57,16 @@ class AuthHandler (object): else: return self.username + def auth_none(self, username, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'none' + self.username = username + self._request_auth() + finally: + self.transport.lock.release() + def auth_publickey(self, username, key, event): self.transport.lock.acquire() try: @@ -79,6 +89,21 @@ class AuthHandler (object): finally: self.transport.lock.release() + def auth_interactive(self, username, handler, event, submethods=''): + """ + response_list = handler(title, instructions, prompt_list) + """ + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'keyboard-interactive' + self.username = username + self.interactive_handler = handler + self.submethods = submethods + self._request_auth() + finally: + self.transport.lock.release() + def abort(self): if self.auth_event is not None: self.auth_event.set() @@ -175,6 +200,11 @@ class AuthHandler (object): blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) sig = self.private_key.sign_ssh_data(self.transport.randpool, blob) m.add_string(str(sig)) + elif self.auth_method == 'keyboard-interactive': + m.add_string('') + m.add_string(self.submethods) + elif self.auth_method == 'none': + pass else: raise SSHException('Unknown auth method "%s"' % self.auth_method) self.transport._send_message(m) @@ -302,6 +332,25 @@ class AuthHandler (object): lang = m.get_string() self.transport._log(INFO, 'Auth banner: ' + banner) # who cares. + + def _parse_userauth_info_request(self, m): + if self.auth_method != 'keyboard-interactive': + raise SSHException('Illegal info request from server') + title = m.get_string() + instructions = m.get_string() + m.get_string() # lang + prompts = m.get_int() + prompt_list = [] + for i in range(prompts): + prompt_list.append((m.get_string(), m.get_boolean())) + response_list = self.interactive_handler(title, instructions, prompt_list) + + m = Message() + m.add_byte(chr(MSG_USERAUTH_INFO_RESPONSE)) + m.add_int(len(response_list)) + for r in response_list: + m.add_string(r) + self.transport._send_message(m) _handler_table = { MSG_SERVICE_REQUEST: _parse_service_request, @@ -310,6 +359,7 @@ class AuthHandler (object): MSG_USERAUTH_SUCCESS: _parse_userauth_success, MSG_USERAUTH_FAILURE: _parse_userauth_failure, MSG_USERAUTH_BANNER: _parse_userauth_banner, + MSG_USERAUTH_INFO_REQUEST: _parse_userauth_info_request, } diff --git a/paramiko/common.py b/paramiko/common.py index d0a0f80..548013d 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -26,6 +26,7 @@ MSG_KEXINIT, MSG_NEWKEYS = range(20, 22) MSG_USERAUTH_REQUEST, MSG_USERAUTH_FAILURE, MSG_USERAUTH_SUCCESS, \ MSG_USERAUTH_BANNER = range(50, 54) MSG_USERAUTH_PK_OK = 60 +MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ @@ -51,7 +52,8 @@ MSG_NAMES = { MSG_USERAUTH_FAILURE: 'userauth-failure', MSG_USERAUTH_SUCCESS: 'userauth-success', MSG_USERAUTH_BANNER: 'userauth--banner', - MSG_USERAUTH_PK_OK: 'userauth-pk-ok', + MSG_USERAUTH_PK_OK: 'userauth-60(pk-ok/info-request)', + MSG_USERAUTH_INFO_RESPONSE: 'userauth-info-response', MSG_GLOBAL_REQUEST: 'global-request', MSG_REQUEST_SUCCESS: 'request-success', MSG_REQUEST_FAILURE: 'request-failure', diff --git a/paramiko/transport.py b/paramiko/transport.py index 3e604d6..a74d56e 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -24,7 +24,7 @@ import sys, os, string, threading, socket, struct, time import weakref from common import * -from ssh_exception import SSHException +from ssh_exception import SSHException, BadAuthenticationType from message import Message from channel import Channel from sftp_client import SFTPClient @@ -869,7 +869,34 @@ class Transport (threading.Thread): return None return self.auth_handler.get_username() - def auth_password(self, username, password, event=None): + def auth_none(self, username): + """ + Try to authenticate to the server using no authentication at all. + This will almost always fail. It may be useful for determining the + list of authentication types supported by the server, by catching the + L{BadAuthenticationType} exception raised. + + @param username: the username to authenticate as + @type username: string + @return: list of auth types permissible for the next stage of + authentication (normally empty) + @rtype: list + + @raise BadAuthenticationType: if "none" authentication isn't allowed + by the server for this user + @raise SSHException: if the authentication failed due to a network + error + + @since: 1.5 + """ + if (not self.active) or (not self.initial_kex_done): + raise SSHException('No existing session') + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_none(username, my_event) + return self.auth_handler.wait_for_response(my_event) + + def auth_password(self, username, password, event=None, fallback=True): """ Authenticate to the server using a password. The username and password are sent over an encrypted link. @@ -883,6 +910,15 @@ class Transport (threading.Thread): authentication succeeds or fails. On failure, an exception is raised. Otherwise, the method simply returns. + Since 1.5, if no event is passed and C{fallback} is C{True} (the + default), if the server doesn't support plain password authentication + but does support so-called "keyboard-interactive" mode, an attempt + will be made to authenticate using this interactive mode. If it fails, + the normal exception will be thrown as if the attempt had never been + made. This is useful for some recent Gentoo and Debian distributions, + which turn off plain password authentication in a misguided belief + that interactive authentication is "more secure". (It's not.) + If the server requires multi-step authentication (which is very rare), this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. @@ -894,6 +930,10 @@ class Transport (threading.Thread): @param event: an event to trigger when the authentication attempt is complete (whether it was successful or not) @type event: threading.Event + @param fallback: C{True} if an attempt at an automated "interactive" + password auth should be made if the server doesn't support normal + password auth + @type fallback: bool @return: list of auth types permissible for the next stage of authentication (normally empty) @rtype: list @@ -915,7 +955,28 @@ class Transport (threading.Thread): if event is not None: # caller wants to wait for event themselves return [] - return self.auth_handler.wait_for_response(my_event) + try: + return self.auth_handler.wait_for_response(my_event) + except BadAuthenticationType, x: + # if password auth isn't allowed, but keyboard-interactive *is*, try to fudge it + if not fallback or not 'keyboard-interactive' in x.allowed_types: + raise + try: + def handler(title, instructions, fields): + self._log(DEBUG, 'title=%r, instructions=%r, fields=%r' % (title, instructions, fields)) + if len(fields) > 1: + raise SSHException('Fallback authentication failed.') + if len(fields) == 0: + # for some reason, at least on os x, a 2nd request will + # be made with zero fields requested. maybe it's just + # to try to fake out automated scripting of the exact + # type we're doing here. *shrug* :) + return [] + return [ password ] + return self.auth_interactive(username, handler) + except SSHException, ignored: + # attempt failed; just raise the original exception + raise x def auth_publickey(self, username, key, event=None): """ @@ -935,9 +996,9 @@ class Transport (threading.Thread): this method will return a list of auth types permissible for the next step. Otherwise, in the normal case, an empty list is returned. - @param username: the username to authenticate as. + @param username: the username to authenticate as @type username: string - @param key: the private key to authenticate with. + @param key: the private key to authenticate with @type key: L{PKey } @param event: an event to trigger when the authentication attempt is complete (whether it was successful or not) @@ -964,6 +1025,59 @@ class Transport (threading.Thread): # caller wants to wait for event themselves return [] return self.auth_handler.wait_for_response(my_event) + + def auth_interactive(self, username, handler, submethods=''): + """ + Authenticate to the server interactively. A handler is used to answer + arbitrary questions from the server. On many servers, this is just a + dumb wrapper around PAM. + + This method will block until the authentication succeeds or fails, + peroidically calling the handler asynchronously to get answers to + authentication questions. The handler may be called more than once + if the server continues to ask questions. + + The handler is expected to be a callable that will handle calls of the + form: C{handler(title, instructions, prompt_list)}. The C{title} is + meant to be a dialog-window title, and the C{instructions} are user + instructions (both are strings). C{prompt_list} will be a list of + prompts, each prompt being a tuple of C{(str, bool)}. The string is + the prompt and the boolean indicates whether the user text should be + echoed. + + A sample call would thus be: + C{handler('title', 'instructions', [('Password:', False)])}. + + The handler should return a list or tuple of answers to the server's + questions. + + If the server requires multi-step authentication (which is very rare), + this method will return a list of auth types permissible for the next + step. Otherwise, in the normal case, an empty list is returned. + + @param username: the username to authenticate as + @type username: string + @param handler: a handler for responding to server questions + @type handler: callable + @param submethods: a string list of desired submethods (optional) + @type submethods: str + @return: list of auth types permissible for the next stage of + authentication (normally empty). + @rtype: list + + @raise BadAuthenticationType: if public-key authentication isn't + allowed by the server for this user + @raise SSHException: if the authentication failed + + @since: 1.5 + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to authenticate unless we're on a secure link + raise SSHException('No existing session') + my_event = threading.Event() + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_interactive(username, handler, my_event, submethods) + return self.auth_handler.wait_for_response(my_event) def set_log_channel(self, name): """