diff options
author | Robey Pointer <robey@lag.net> | 2005-08-09 07:40:07 +0000 |
---|---|---|
committer | Robey Pointer <robey@lag.net> | 2005-08-09 07:40:07 +0000 |
commit | 0f3bf86617999de2aff056b8455aa33c64188e3a (patch) | |
tree | 2bffa163a8ca48420924db7ef7b62d6162e690c4 | |
parent | 1fdec8bd06b22c098de6e731364c707c7292cc1f (diff) |
[project @ Arch-1:robey@lag.net--2005-master-shake%paramiko--dev--1--patch-54]
smooth BaseTransport and Transport together, and move the auth stuff into AuthHandler -- an improvement i made in jaramiko and decided deserved to be backported
-rw-r--r-- | paramiko/__init__.py | 12 | ||||
-rw-r--r-- | paramiko/auth_handler.py | 315 | ||||
-rw-r--r-- | paramiko/auth_transport.py | 441 | ||||
-rw-r--r-- | paramiko/common.py | 2 | ||||
-rw-r--r-- | paramiko/server.py | 8 | ||||
-rw-r--r-- | paramiko/transport.py | 166 |
6 files changed, 474 insertions, 470 deletions
diff --git a/paramiko/__init__.py b/paramiko/__init__.py index dd7ed0ae..e9ed3754 100644 --- a/paramiko/__init__.py +++ b/paramiko/__init__.py @@ -27,8 +27,8 @@ protocol also includes the ability to open arbitrary channels to remote services across an encrypted tunnel. (This is how C{sftp} works, for example.) To use this package, pass a socket (or socket-like object) to a L{Transport}, -and use L{start_server <paramiko.transport.BaseTransport.start_server>} or -L{start_client <paramiko.transport.BaseTransport.start_client>} to negoatite +and use L{start_server <Transport.start_server>} or +L{start_client <Transport.start_client>} to negoatite with the remote host as either a server or client. As a client, you are responsible for authenticating using a password or private key, and checking the server's host key. I{(Key signature and verification is done by paramiko, @@ -64,12 +64,12 @@ __version__ = "1.4 (oddish)" __license__ = "GNU Lesser General Public License (LGPL)" -import transport, auth_transport, channel, rsakey, dsskey, message +import transport, auth_handler, channel, rsakey, dsskey, message import ssh_exception, file, packet, agent, server, util import sftp_client, sftp_attr, sftp_handle, sftp_server, sftp_si -from transport import randpool, SecurityOptions, BaseTransport -from auth_transport import Transport +from transport import randpool, SecurityOptions, Transport +from auth_handler import AuthHandler from channel import Channel, ChannelFile from ssh_exception import SSHException, PasswordRequiredException, BadAuthenticationType from server import ServerInterface, SubsystemHandler @@ -91,7 +91,7 @@ from pkey import PKey # fix module names for epydoc for x in [Transport, SecurityOptions, Channel, SFTPServer, SSHException, \ PasswordRequiredException, BadAuthenticationType, ChannelFile, \ - SubsystemHandler, BaseTransport, RSAKey, DSSKey, SFTPError, \ + SubsystemHandler, AuthHandler, RSAKey, DSSKey, SFTPError, \ SFTP, SFTPClient, SFTPServer, Message, Packetizer, SFTPAttributes, \ SFTPHandle, SFTPServerInterface, BufferedFile, Agent, AgentKey, \ PKey, BaseSFTP, SFTPFile, ServerInterface]: diff --git a/paramiko/auth_handler.py b/paramiko/auth_handler.py new file mode 100644 index 00000000..95965fa8 --- /dev/null +++ b/paramiko/auth_handler.py @@ -0,0 +1,315 @@ +# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net> +# +# This file is part of paramiko. +# +# Paramiko is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Paramiko; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. + +""" +L{AuthHandler} +""" + +import threading + +# this helps freezing utils +import encodings.utf_8 + +from common import * +import util +from message import Message +from ssh_exception import SSHException, BadAuthenticationType, PartialAuthentication + + +class AuthHandler (object): + """ + Internal class to handle the mechanics of authentication. + """ + + def __init__(self, transport): + self.transport = transport + self.username = None + self.authenticated = False + self.auth_event = None + self.auth_method = '' + self.password = None + self.private_key = None + # for server mode: + self.auth_username = None + self.auth_fail_count = 0 + + def is_authenticated(self): + return self.authenticated + + def get_username(self): + if self.transport.server_mode: + return self.auth_username + else: + return self.username + + def auth_publickey(self, username, key, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'publickey' + self.username = username + self.private_key = key + self._request_auth() + finally: + self.transport.lock.release() + + def auth_password(self, username, password, event): + self.transport.lock.acquire() + try: + self.auth_event = event + self.auth_method = 'password' + self.username = username + self.password = password + self._request_auth() + finally: + self.transport.lock.release() + + def abort(self): + if self.auth_event is not None: + self.auth_event.set() + + + ### internals... + + + def _request_auth(self): + m = Message() + m.add_byte(chr(MSG_SERVICE_REQUEST)) + m.add_string('ssh-userauth') + self.transport._send_message(m) + + def _disconnect_service_not_available(self): + m = Message() + m.add_byte(chr(MSG_DISCONNECT)) + m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) + m.add_string('Service not available') + m.add_string('en') + self.transport._send_message(m) + self.transport.close() + + def _disconnect_no_more_auth(self): + m = Message() + m.add_byte(chr(MSG_DISCONNECT)) + m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) + m.add_string('No more auth methods available') + m.add_string('en') + self.transport._send_message(m) + self.transport.close() + + def _get_session_blob(self, key, service, username): + m = Message() + m.add_string(self.transport.session_id) + m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_string(username) + m.add_string(service) + m.add_string('publickey') + m.add_boolean(1) + m.add_string(key.get_name()) + m.add_string(str(key)) + return str(m) + + def wait_for_response(self, event): + while True: + event.wait(0.1) + if not self.transport.is_active(): + e = self.transport.get_exception() + if e is None: + e = SSHException('Authentication failed.') + raise e + if event.isSet(): + break + if not self.is_authenticated(): + e = self.transport.get_exception() + if e is None: + e = SSHException('Authentication failed.') + # this is horrible. python Exception isn't yet descended from + # object, so type(e) won't work. :( + if issubclass(e.__class__, PartialAuthentication): + return e.allowed_types + raise e + return [] + + def _parse_service_request(self, m): + service = m.get_string() + if self.transport.server_mode and (service == 'ssh-userauth'): + # accepted + m = Message() + m.add_byte(chr(MSG_SERVICE_ACCEPT)) + m.add_string(service) + self.transport._send_message(m) + return + # dunno this one + self._disconnect_service_not_available() + + def _parse_service_accept(self, m): + service = m.get_string() + if service == 'ssh-userauth': + self.transport._log(DEBUG, 'userauth is OK') + m = Message() + m.add_byte(chr(MSG_USERAUTH_REQUEST)) + m.add_string(self.username) + m.add_string('ssh-connection') + m.add_string(self.auth_method) + if self.auth_method == 'password': + m.add_boolean(False) + m.add_string(self.password.encode('UTF-8')) + elif self.auth_method == 'publickey': + m.add_boolean(True) + m.add_string(self.private_key.get_name()) + m.add_string(str(self.private_key)) + 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)) + else: + raise SSHException('Unknown auth method "%s"' % self.auth_method) + self.transport._send_message(m) + else: + self.transport._log(DEBUG, 'Service request "%s" accepted (?)' % service) + + def _parse_userauth_request(self, m): + if not self.transport.server_mode: + # er, uh... what? + m = Message() + m.add_byte(chr(MSG_USERAUTH_FAILURE)) + m.add_string('none') + m.add_boolean(0) + self.transport._send_message(m) + return + if self.authenticated: + # ignore + return + username = m.get_string() + service = m.get_string() + method = m.get_string() + self.transport._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username)) + if service != 'ssh-connection': + self._disconnect_service_not_available() + return + if (self.auth_username is not None) and (self.auth_username != username): + self.transport._log(WARNING, 'Auth rejected because the client attempted to change username in mid-flight') + self._disconnect_no_more_auth() + return + self.auth_username = username + + if method == 'none': + result = self.transport.server_object.check_auth_none(username) + elif method == 'password': + changereq = m.get_boolean() + password = m.get_string().decode('UTF-8', 'replace') + if changereq: + # always treated as failure, since we don't support changing passwords, but collect + # the list of valid auth types from the callback anyway + self.transport._log(DEBUG, 'Auth request to change passwords (rejected)') + newpassword = m.get_string().decode('UTF-8', 'replace') + result = AUTH_FAILED + else: + result = self.transport.server_object.check_auth_password(username, password) + elif method == 'publickey': + sig_attached = m.get_boolean() + keytype = m.get_string() + keyblob = m.get_string() + try: + key = self.transport._key_info[keytype](Message(keyblob)) + except SSHException, e: + self.transport._log(INFO, 'Auth rejected: public key: %s' % str(e)) + key = None + except: + self.transport._log(INFO, 'Auth rejected: unsupported or mangled public key') + key = None + if key is None: + self._disconnect_no_more_auth() + return + # first check if this key is okay... if not, we can skip the verify + result = self.transport.server_object.check_auth_publickey(username, key) + if result != AUTH_FAILED: + # key is okay, verify it + if not sig_attached: + # client wants to know if this key is acceptable, before it + # signs anything... send special "ok" message + m = Message() + m.add_byte(chr(MSG_USERAUTH_PK_OK)) + m.add_string(keytype) + m.add_string(keyblob) + self.transport._send_message(m) + return + sig = Message(m.get_string()) + blob = self._get_session_blob(key, service, username) + if not key.verify_ssh_sig(blob, sig): + self.transport._log(INFO, 'Auth rejected: invalid signature') + result = AUTH_FAILED + else: + result = self.transport.server_object.check_auth_none(username) + # okay, send result + m = Message() + if result == AUTH_SUCCESSFUL: + self.transport._log(INFO, 'Auth granted (%s).' % method) + m.add_byte(chr(MSG_USERAUTH_SUCCESS)) + self.authenticated = True + else: + self.transport._log(INFO, 'Auth rejected (%s).' % method) + m.add_byte(chr(MSG_USERAUTH_FAILURE)) + m.add_string(self.transport.server_object.get_allowed_auths(username)) + if result == AUTH_PARTIALLY_SUCCESSFUL: + m.add_boolean(1) + else: + m.add_boolean(0) + self.auth_fail_count += 1 + self.transport._send_message(m) + if self.auth_fail_count >= 10: + self._disconnect_no_more_auth() + + def _parse_userauth_success(self, m): + self.transport._log(INFO, 'Authentication successful!') + self.authenticated = True + if self.auth_event != None: + self.auth_event.set() + + def _parse_userauth_failure(self, m): + authlist = m.get_list() + partial = m.get_boolean() + if partial: + self.transport._log(INFO, 'Authentication continues...') + self.transport._log(DEBUG, 'Methods: ' + str(authlist)) + self.transport.saved_exception = PartialAuthentication(authlist) + elif self.auth_method not in authlist: + self.transport._log(INFO, 'Authentication type not permitted.') + self.transport._log(DEBUG, 'Allowed methods: ' + str(authlist)) + self.transport.saved_exception = BadAuthenticationType('Bad authentication type', authlist) + else: + self.transport._log(INFO, 'Authentication failed.') + self.authenticated = False + self.username = None + if self.auth_event != None: + self.auth_event.set() + + def _parse_userauth_banner(self, m): + banner = m.get_string() + lang = m.get_string() + self.transport._log(INFO, 'Auth banner: ' + banner) + # who cares. + + _handler_table = { + MSG_SERVICE_REQUEST: _parse_service_request, + MSG_SERVICE_ACCEPT: _parse_service_accept, + MSG_USERAUTH_REQUEST: _parse_userauth_request, + MSG_USERAUTH_SUCCESS: _parse_userauth_success, + MSG_USERAUTH_FAILURE: _parse_userauth_failure, + MSG_USERAUTH_BANNER: _parse_userauth_banner, + } + + diff --git a/paramiko/auth_transport.py b/paramiko/auth_transport.py deleted file mode 100644 index f9ecf70b..00000000 --- a/paramiko/auth_transport.py +++ /dev/null @@ -1,441 +0,0 @@ -# Copyright (C) 2003-2005 Robey Pointer <robey@lag.net> -# -# This file is part of paramiko. -# -# Paramiko is free software; you can redistribute it and/or modify it under the -# terms of the GNU Lesser General Public License as published by the Free -# Software Foundation; either version 2.1 of the License, or (at your option) -# any later version. -# -# Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR -# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more -# details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with Paramiko; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. - -""" -L{Transport} is a subclass of L{BaseTransport} that handles authentication. -This separation keeps either class file from being too unwieldy. -""" - -import threading - -# this helps freezing utils -import encodings.utf_8 - -from common import * -import util -from transport import BaseTransport -from message import Message -from ssh_exception import SSHException, BadAuthenticationType, PartialAuthentication - - -class Transport (BaseTransport): - """ - An SSH Transport attaches to a stream (usually a socket), negotiates an - encrypted session, authenticates, and then creates stream tunnels, called - L{Channel}s, across the session. Multiple channels can be multiplexed - across a single session (and often are, in the case of port forwardings). - """ - - def __init__(self, sock): - BaseTransport.__init__(self, sock) - self.username = None - self.authenticated = False - self.auth_event = None - self.auth_method = '' - self.password = None - self.private_key = None - # for server mode: - self.auth_username = None - self.auth_fail_count = 0 - - def __repr__(self): - out = '<paramiko.Transport at %s' % hex(long(id(self)) & 0xffffffffL) - if not self.active: - out += ' (unconnected)' - else: - if self.local_cipher != '': - out += ' (cipher %s, %d bits)' % (self.local_cipher, - self._cipher_info[self.local_cipher]['key-size'] * 8) - if self.authenticated: - if len(self.channels) == 1: - out += ' (active; 1 open channel)' - else: - out += ' (active; %d open channels)' % len(self.channels) - elif self.initial_kex_done: - out += ' (connected; awaiting auth)' - else: - out += ' (connecting)' - out += '>' - return out - - def is_authenticated(self): - """ - Return true if this session is active and authenticated. - - @return: True if the session is still open and has been authenticated successfully; - False if authentication failed and/or the session is closed. - @rtype: bool - """ - return self.authenticated and self.active - - def get_username(self): - """ - Return the username this connection is authenticated for. If the - session is not authenticated (or authentication failed), this method - returns C{None}. - - @return: username that was authenticated, or C{None}. - @rtype: string - - @since: fearow - """ - if self.server_mode: - return self.auth_username - else: - return self.username - - def auth_publickey(self, username, key, event=None): - """ - Authenticate to the server using a private key. The key is used to - sign data from the server, so it must include the private part. - - If an C{event} is passed in, this method will return immediately, and - the event will be triggered once authentication succeeds or fails. On - success, L{is_authenticated} will return C{True}. On failure, you may - use L{get_exception} to get more detailed error information. - - Since 1.1, if no event is passed, this method will block until the - authentication succeeds or fails. On failure, an exception is raised. - Otherwise, the method simply returns. - - 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 key: the private key to authenticate with. - @type key: L{PKey <pkey.PKey>} - @param event: an event to trigger when the authentication attempt is - complete (whether it was successful or not) - @type event: threading.Event - @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 (and no event was passed in). - @raise SSHException: if the authentication failed (and no event was - passed in). - """ - 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') - if event is None: - my_event = threading.Event() - else: - my_event = event - self.lock.acquire() - try: - self.auth_event = my_event - self.auth_method = 'publickey' - self.username = username - self.private_key = key - self._request_auth() - finally: - self.lock.release() - if event is not None: - # caller wants to wait for event themselves - return [] - return self._wait_for_response(my_event) - - def auth_password(self, username, password, event=None): - """ - Authenticate to the server using a password. The username and password - are sent over an encrypted link. - - If an C{event} is passed in, this method will return immediately, and - the event will be triggered once authentication succeeds or fails. On - success, L{is_authenticated} will return C{True}. On failure, you may - use L{get_exception} to get more detailed error information. - - Since 1.1, if no event is passed, this method will block until the - authentication succeeds or fails. On failure, an exception is raised. - Otherwise, the method simply returns. - - 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 password: the password to authenticate with - @type password: string - @param event: an event to trigger when the authentication attempt is - complete (whether it was successful or not) - @type event: threading.Event - @return: list of auth types permissible for the next stage of - authentication (normally empty) - @rtype: list - - @raise BadAuthenticationType: if password authentication isn't - allowed by the server for this user (and no event was passed in) - @raise SSHException: if the authentication failed (and no event was - passed in) - """ - if (not self.active) or (not self.initial_kex_done): - # we should never try to send the password unless we're on a secure link - raise SSHException('No existing session') - if event is None: - my_event = threading.Event() - else: - my_event = event - self.lock.acquire() - try: - self.auth_event = my_event - self.auth_method = 'password' - self.username = username - self.password = password - self._request_auth() - finally: - self.lock.release() - if event is not None: - # caller wants to wait for event themselves - return [] - return self._wait_for_response(my_event) - - - ### internals... - - - def _request_auth(self): - m = Message() - m.add_byte(chr(MSG_SERVICE_REQUEST)) - m.add_string('ssh-userauth') - self._send_message(m) - - def _disconnect_service_not_available(self): - m = Message() - m.add_byte(chr(MSG_DISCONNECT)) - m.add_int(DISCONNECT_SERVICE_NOT_AVAILABLE) - m.add_string('Service not available') - m.add_string('en') - self._send_message(m) - self.close() - - def _disconnect_no_more_auth(self): - m = Message() - m.add_byte(chr(MSG_DISCONNECT)) - m.add_int(DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) - m.add_string('No more auth methods available') - m.add_string('en') - self._send_message(m) - self.close() - - def _get_session_blob(self, key, service, username): - m = Message() - m.add_string(self.session_id) - m.add_byte(chr(MSG_USERAUTH_REQUEST)) - m.add_string(username) - m.add_string(service) - m.add_string('publickey') - m.add_boolean(1) - m.add_string(key.get_name()) - m.add_string(str(key)) - return str(m) - - def _wait_for_response(self, event): - while True: - event.wait(0.1) - if not self.active: - e = self.get_exception() - if e is None: - e = SSHException('Authentication failed.') - raise e - if event.isSet(): - break - if not self.is_authenticated(): - e = self.get_exception() - if e is None: - e = SSHException('Authentication failed.') - # this is horrible. python Exception isn't yet descended from - # object, so type(e) won't work. :( - if issubclass(e.__class__, PartialAuthentication): - return e.allowed_types - raise e - return [] - - def _parse_service_request(self, m): - service = m.get_string() - if self.server_mode and (service == 'ssh-userauth'): - # accepted - m = Message() - m.add_byte(chr(MSG_SERVICE_ACCEPT)) - m.add_string(service) - self._send_message(m) - return - # dunno this one - self._disconnect_service_not_available() - - def _parse_service_accept(self, m): - service = m.get_string() - if service == 'ssh-userauth': - self._log(DEBUG, 'userauth is OK') - m = Message() - m.add_byte(chr(MSG_USERAUTH_REQUEST)) - m.add_string(self.username) - m.add_string('ssh-connection') - m.add_string(self.auth_method) - if self.auth_method == 'password': - m.add_boolean(False) - m.add_string(self.password.encode('UTF-8')) - elif self.auth_method == 'publickey': - m.add_boolean(True) - m.add_string(self.private_key.get_name()) - m.add_string(str(self.private_key)) - blob = self._get_session_blob(self.private_key, 'ssh-connection', self.username) - sig = self.private_key.sign_ssh_data(self.randpool, blob) - m.add_string(str(sig)) - else: - raise SSHException('Unknown auth method "%s"' % self.auth_method) - self._send_message(m) - else: - self._log(DEBUG, 'Service request "%s" accepted (?)' % service) - - def _parse_userauth_request(self, m): - if not self.server_mode: - # er, uh... what? - m = Message() - m.add_byte(chr(MSG_USERAUTH_FAILURE)) - m.add_string('none') - m.add_boolean(0) - self._send_message(m) - return - if self.authenticated: - # ignore - return - username = m.get_string() - service = m.get_string() - method = m.get_string() - self._log(DEBUG, 'Auth request (type=%s) service=%s, username=%s' % (method, service, username)) - if service != 'ssh-connection': - self._disconnect_service_not_available() - return - if (self.auth_username is not None) and (self.auth_username != username): - self._log(WARNING, 'Auth rejected because the client attempted to change username in mid-flight') - self._disconnect_no_more_auth() - return - self.auth_username = username - - if method == 'none': - result = self.server_object.check_auth_none(username) - elif method == 'password': - changereq = m.get_boolean() - password = m.get_string().decode('UTF-8', 'replace') - if changereq: - # always treated as failure, since we don't support changing passwords, but collect - # the list of valid auth types from the callback anyway - self._log(DEBUG, 'Auth request to change passwords (rejected)') - newpassword = m.get_string().decode('UTF-8', 'replace') - result = AUTH_FAILED - else: - result = self.server_object.check_auth_password(username, password) - elif method == 'publickey': - sig_attached = m.get_boolean() - keytype = m.get_string() - keyblob = m.get_string() - try: - key = self._key_info[keytype](Message(keyblob)) - except SSHException, e: - self._log(INFO, 'Auth rejected: public key: %s' % str(e)) - key = None - except: - self._log(INFO, 'Auth rejected: unsupported or mangled public key') - key = None - if key is None: - self._disconnect_no_more_auth() - return - # first check if this key is okay... if not, we can skip the verify - result = self.server_object.check_auth_publickey(username, key) - if result != AUTH_FAILED: - # key is okay, verify it - if not sig_attached: - # client wants to know if this key is acceptable, before it - # signs anything... send special "ok" message - m = Message() - m.add_byte(chr(MSG_USERAUTH_PK_OK)) - m.add_string(keytype) - m.add_string(keyblob) - self._send_message(m) - return - sig = Message(m.get_string()) - blob = self._get_session_blob(key, service, username) - if not key.verify_ssh_sig(blob, sig): - self._log(INFO, 'Auth rejected: invalid signature') - result = AUTH_FAILED - else: - result = self.server_object.check_auth_none(username) - # okay, send result - m = Message() - if result == AUTH_SUCCESSFUL: - self._log(INFO, 'Auth granted (%s).' % method) - m.add_byte(chr(MSG_USERAUTH_SUCCESS)) - self.authenticated = True - else: - self._log(INFO, 'Auth rejected (%s).' % method) - m.add_byte(chr(MSG_USERAUTH_FAILURE)) - m.add_string(self.server_object.get_allowed_auths(username)) - if result == AUTH_PARTIALLY_SUCCESSFUL: - m.add_boolean(1) - else: - m.add_boolean(0) - self.auth_fail_count += 1 - self._send_message(m) - if self.auth_fail_count >= 10: - self._disconnect_no_more_auth() - - def _parse_userauth_success(self, m): - self._log(INFO, 'Authentication successful!') - self.authenticated = True - if self.auth_event != None: - self.auth_event.set() - - def _parse_userauth_failure(self, m): - authlist = m.get_list() - partial = m.get_boolean() - if partial: - self._log(INFO, 'Authentication continues...') - self._log(DEBUG, 'Methods: ' + str(authlist)) - self.saved_exception = PartialAuthentication(authlist) - elif self.auth_method not in authlist: - self._log(INFO, 'Authentication type not permitted.') - self._log(DEBUG, 'Allowed methods: ' + str(authlist)) - self.saved_exception = BadAuthenticationType('Bad authentication type', authlist) - else: - self._log(INFO, 'Authentication failed.') - self.authenticated = False - self.username = None - if self.auth_event != None: - self.auth_event.set() - - def _parse_userauth_banner(self, m): - banner = m.get_string() - lang = m.get_string() - self._log(INFO, 'Auth banner: ' + banner) - # who cares. - - _handler_table = BaseTransport._handler_table.copy() - _handler_table.update({ - MSG_SERVICE_REQUEST: _parse_service_request, - MSG_SERVICE_ACCEPT: _parse_service_accept, - MSG_USERAUTH_REQUEST: _parse_userauth_request, - MSG_USERAUTH_SUCCESS: _parse_userauth_success, - MSG_USERAUTH_FAILURE: _parse_userauth_failure, - MSG_USERAUTH_BANNER: _parse_userauth_banner, - }) - diff --git a/paramiko/common.py b/paramiko/common.py index 42793751..d0a0f80b 100644 --- a/paramiko/common.py +++ b/paramiko/common.py @@ -1,5 +1,3 @@ -#!/usr/bin/python - # Copyright (C) 2003-2005 Robey Pointer <robey@lag.net> # # This file is part of paramiko. diff --git a/paramiko/server.py b/paramiko/server.py index 747a36d3..151dd124 100644 --- a/paramiko/server.py +++ b/paramiko/server.py @@ -25,8 +25,6 @@ L{ServerInterface} is an interface to override for server support. import threading from common import * import util -from transport import BaseTransport -from auth_transport import Transport class ServerInterface (object): """ @@ -286,7 +284,7 @@ class ServerInterface (object): subsystem. An example of a subsystem is C{sftp}. The default implementation checks for a subsystem handler assigned via - L{Transport.set_subsystem_handler <BaseTransport.set_subsystem_handler>}. + L{Transport.set_subsystem_handler}. If one has been set, the handler is invoked and this method returns C{True}. Otherwise it returns C{False}. @@ -338,7 +336,7 @@ class SubsystemHandler (threading.Thread): """ Handler for a subsytem in server mode. If you create a subclass of this class and pass it to - L{Transport.set_subsystem_handler <BaseTransport.set_subsystem_handler>}, + L{Transport.set_subsystem_handler}, an object of this class will be created for each request for this subsystem. Each new object will be executed within its own new thread by calling L{start_subsystem}. @@ -409,7 +407,7 @@ class SubsystemHandler (threading.Thread): @note: It is the responsibility of this method to exit if the underlying L{Transport} is closed. This can be done by checking - L{Transport.is_active <BaseTransport.is_active>} or noticing an EOF + L{Transport.is_active} or noticing an EOF on the L{Channel}. If this method loops forever without checking for this case, your python interpreter may refuse to exit because this thread will still be running. diff --git a/paramiko/transport.py b/paramiko/transport.py index 43837e4a..3e604d64 100644 --- a/paramiko/transport.py +++ b/paramiko/transport.py @@ -17,7 +17,7 @@ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA. """ -L{BaseTransport} handles the core SSH2 protocol. +L{Transport} handles the core SSH2 protocol. """ import sys, os, string, threading, socket, struct, time @@ -35,6 +35,7 @@ from dsskey import DSSKey from kex_group1 import KexGroup1 from kex_gex import KexGex from primes import ModulusPack +from auth_handler import AuthHandler # these come from PyCrypt # http://www.amk.ca/python/writing/pycrypt/ @@ -124,12 +125,14 @@ class SecurityOptions (object): kex = property(_get_kex, _set_kex, None, "Key exchange algorithms") -class BaseTransport (threading.Thread): +class Transport (threading.Thread): """ - Handles protocol negotiation, key exchange, encryption, and the creation - of channels across an SSH session. Basically everything but authentication - is done here. + An SSH Transport attaches to a stream (usually a socket), negotiates an + encrypted session, authenticates, and then creates stream tunnels, called + L{Channel}s, across the session. Multiple channels can be multiplexed + across a single session (and often are, in the case of port forwardings). """ + _PROTO_ID = '2.0' _CLIENT_ID = 'paramiko_1.4' @@ -240,6 +243,7 @@ class BaseTransport (threading.Thread): self.log_name = 'paramiko.transport' self.logger = util.get_logger(self.log_name) self.packetizer.set_log(self.logger) + self.auth_handler = None # user-defined event callbacks: self.completion_event = None # server mode: @@ -259,17 +263,22 @@ class BaseTransport (threading.Thread): @rtype: str """ - out = '<paramiko.BaseTransport at %s' % hex(long(id(self)) & 0xffffffffL) + out = '<paramiko.Transport at %s' % hex(long(id(self)) & 0xffffffffL) if not self.active: out += ' (unconnected)' else: if self.local_cipher != '': out += ' (cipher %s, %d bits)' % (self.local_cipher, self._cipher_info[self.local_cipher]['key-size'] * 8) - if len(self.channels) == 1: - out += ' (active; 1 open channel)' + if self.is_authenticated(): + if len(self.channels) == 1: + out += ' (active; 1 open channel)' + else: + out += ' (active; %d open channels)' % len(self.channels) + elif self.initial_kex_done: + out += ' (connected; awaiting auth)' else: - out += ' (active; %d open channels)' % len(self.channels) + out += ' (connecting)' out += '>' return out @@ -464,19 +473,19 @@ class BaseTransport (threading.Thread): @note: This has no effect when used in client mode. """ - BaseTransport._modulus_pack = ModulusPack(randpool) + Transport._modulus_pack = ModulusPack(randpool) # places to look for the openssh "moduli" file file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ] if filename is not None: file_list.insert(0, filename) for fn in file_list: try: - BaseTransport._modulus_pack.read_file(fn) + Transport._modulus_pack.read_file(fn) return True except IOError: pass # none succeeded - BaseTransport._modulus_pack = None + Transport._modulus_pack = None return False load_server_moduli = staticmethod(load_server_moduli) @@ -833,6 +842,128 @@ class BaseTransport (threading.Thread): self.subsystem_table[name] = (handler, larg, kwarg) finally: self.lock.release() + + def is_authenticated(self): + """ + Return true if this session is active and authenticated. + + @return: True if the session is still open and has been authenticated + successfully; False if authentication failed and/or the session is + closed. + @rtype: bool + """ + return self.active and (self.auth_handler is not None) and self.auth_handler.is_authenticated() + + def get_username(self): + """ + Return the username this connection is authenticated for. If the + session is not authenticated (or authentication failed), this method + returns C{None}. + + @return: username that was authenticated, or C{None}. + @rtype: string + + @since: fearow + """ + if not self.active or (self.auth_handler is None): + return None + return self.auth_handler.get_username() + + def auth_password(self, username, password, event=None): + """ + Authenticate to the server using a password. The username and password + are sent over an encrypted link. + + If an C{event} is passed in, this method will return immediately, and + the event will be triggered once authentication succeeds or fails. On + success, L{is_authenticated} will return C{True}. On failure, you may + use L{get_exception} to get more detailed error information. + + Since 1.1, if no event is passed, this method will block until the + authentication succeeds or fails. On failure, an exception is raised. + Otherwise, the method simply returns. + + 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 password: the password to authenticate with + @type password: string + @param event: an event to trigger when the authentication attempt is + complete (whether it was successful or not) + @type event: threading.Event + @return: list of auth types permissible for the next stage of + authentication (normally empty) + @rtype: list + + @raise BadAuthenticationType: if password authentication isn't + allowed by the server for this user (and no event was passed in) + @raise SSHException: if the authentication failed (and no event was + passed in) + """ + if (not self.active) or (not self.initial_kex_done): + # we should never try to send the password unless we're on a secure link + raise SSHException('No existing session') + if event is None: + my_event = threading.Event() + else: + my_event = event + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_password(username, password, my_event) + if event is not None: + # caller wants to wait for event themselves + return [] + return self.auth_handler.wait_for_response(my_event) + + def auth_publickey(self, username, key, event=None): + """ + Authenticate to the server using a private key. The key is used to + sign data from the server, so it must include the private part. + + If an C{event} is passed in, this method will return immediately, and + the event will be triggered once authentication succeeds or fails. On + success, L{is_authenticated} will return C{True}. On failure, you may + use L{get_exception} to get more detailed error information. + + Since 1.1, if no event is passed, this method will block until the + authentication succeeds or fails. On failure, an exception is raised. + Otherwise, the method simply returns. + + 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 key: the private key to authenticate with. + @type key: L{PKey <pkey.PKey>} + @param event: an event to trigger when the authentication attempt is + complete (whether it was successful or not) + @type event: threading.Event + @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 (and no event was passed in). + @raise SSHException: if the authentication failed (and no event was + passed in). + """ + 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') + if event is None: + my_event = threading.Event() + else: + my_event = event + self.auth_handler = AuthHandler(self) + self.auth_handler.auth_publickey(username, key, my_event) + if event is not None: + # caller wants to wait for event themselves + return [] + return self.auth_handler.wait_for_response(my_event) def set_log_channel(self, name): """ @@ -1023,6 +1154,8 @@ class BaseTransport (threading.Thread): self._log(ERROR, 'Channel request for unknown channel %d' % chanid) self.active = False self.packetizer.close() + elif (self.auth_handler is not None) and self.auth_handler._handler_table.has_key(ptype): + self.auth_handler._handler_table[ptype](self.auth_handler, m) else: self._log(WARNING, 'Oops, unhandled type %d' % ptype) msg = Message() @@ -1056,8 +1189,8 @@ class BaseTransport (threading.Thread): self.packetizer.close() if self.completion_event != None: self.completion_event.set() - if self.auth_event != None: - self.auth_event.set() + if self.auth_handler is not None: + self.auth_handler.abort() for event in self.channel_events.values(): event.set() self.sock.close() @@ -1291,6 +1424,9 @@ class BaseTransport (threading.Thread): self.local_kex_init = self.remote_kex_init = None self.K = None self.kex_engine = None + if self.server_mode and (self.auth_handler is None): + # create auth handler for server mode + self.auth_handler = AuthHandler(self) if not self.initial_kex_done: # this was the first key exchange self.initial_kex_done = True @@ -1472,5 +1608,3 @@ class BaseTransport (threading.Thread): MSG_CHANNEL_EOF: Channel._handle_eof, MSG_CHANNEL_CLOSE: Channel._handle_close, } - -from server import ServerInterface |