Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite auth mechanism proposal #203

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 63 additions & 51 deletions Exscript/protocols/ssh2.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,15 @@
for key in keymap:
PrivateKey.keytypes.add(key)

auth_types = {'publickey': ('_paramiko_auth_agent', '_paramiko_auth_autokey'),
'keyboard-interactive': ('_paramiko_auth_interactive',),
'password': ('_paramiko_auth_password',)}
auth_types = [
# This one automatically falls back to keyb-interecative with internal handler

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (82 > 79 characters)

{'ssh_method': 'password', 'function': '_paramiko_auth_password'},
{'ssh_method': 'none', 'function': '_paramiko_auth_none'},
{'ssh_method': 'publickey', 'function': '_paramiko_auth_agent', 'display_name': 'publickey (agent)'},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (105 > 79 characters)

{'ssh_method': 'publickey', 'function': '_paramiko_auth_autokey', 'display_name': 'publickey (autokey)'},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (109 > 79 characters)

# https://superuser.com/questions/894608/ssh-o-preferredauthentications-whats-the-difference-between-password-and-k
{'ssh_method': 'keyboard-interactive', 'function': '_paramiko_auth_interactive'}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (84 > 79 characters)

]


class SSH2(Protocol):
Expand Down Expand Up @@ -189,7 +195,7 @@ def _paramiko_connect(self):
return t

def _paramiko_auth_none(self, username, password=None):
self.client.auth_none(username)
return self.client.auth_none(username)

def _paramiko_auth_interactive(self, username, password=None):
if password is None:
Expand All @@ -213,10 +219,10 @@ def handler(title, instructions, prompt_list):
response.append(password)
break
return response
self.client.auth_interactive(username, handler)
return self.client.auth_interactive(username, handler)

def _paramiko_auth_password(self, username, password):
self.client.auth_password(username, password or '')
return self.client.auth_password(username, password or '')

def _paramiko_auth_agent(self, username, password=None):
keys = paramiko.Agent().get_keys()
Expand All @@ -230,8 +236,7 @@ def _paramiko_auth_agent(self, username, password=None):
try:
fp = hexlify(key.get_fingerprint())
self._dbg(1, 'Trying SSH agent key %s' % fp)
self.client.auth_publickey(username, key)
return
return self.client.auth_publickey(username, key)
except SSHException as e:
saved_exception = e
raise saved_exception
Expand All @@ -248,8 +253,7 @@ def _paramiko_auth_key(self, username, keys, password):
key = pkey_class.from_private_key_file(filename, password)
fp = hexlify(key.get_fingerprint())
self._dbg(1, 'Trying key %s in %s' % (fp, filename))
self.client.auth_publickey(username, key)
return
return self.client.auth_publickey(username, key)
except SSHException as e:
saved_exception = e
except IOError as e:
Expand All @@ -265,57 +269,58 @@ def _paramiko_auth_autokey(self, username, password):
file = os.path.expanduser(file)
if os.path.isfile(file):
keyfiles.append((cls, file))
self._paramiko_auth_key(username, keyfiles, password)
return self._paramiko_auth_key(username, keyfiles, password)

def _get_auth_methods(self, allowed_types):
auth_methods = []
for method in allowed_types:
if method not in auth_types:
self._dbg(1, 'Unsupported auth method %s' % repr(method))
continue
for type_name in auth_types[method]:
auth_methods.append(getattr(self, type_name))
return auth_methods
def _add_auth_error(self, error, level=1):
self._dbg(level, error)
self.auth_errors.append(error)

def _paramiko_auth(self, username, password):
# Try authentication using auth_none. This should (almost) always fail,
# but provides us with info about allowed authentication types.
try:
self.client.auth_none(username)
except BadAuthenticationType as err:
self._dbg(1, 'auth_none failed, supported: %s' % err.allowed_types)
auth_methods = self._get_auth_methods(err.allowed_types)
def _paramiko_auth(self, username, password, partial_methods=[]):
if partial_methods == []:
self.auth_errors = []
allowed_methods = None
else:
return

# Finally try all supported login methods.
errors = []
for method in auth_methods:
allowed_methods = partial_methods
for method in auth_types:
method_name = method.get('display_name', method['ssh_method'])
if allowed_methods is not None and method_name not in allowed_methods:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (82 > 79 characters)

self._dbg(1, 'Skipping authentification method %s (unsupported)' % method_name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (95 > 79 characters)

continue
# Some OSes (e.g. JunOS ERX OS, Huawei) do not accept further login
# attempts after failing one. So in this hack, we
# re-connect after each attempt...
if self.get_driver().reconnect_between_auth_methods or not self.client.active:
# But do not reconnect in case of partial auth
if self.get_driver().reconnect_between_auth_methods or (partial_methods == [] and not self.client.active):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (118 > 79 characters)

self.close(force=True)
self.client = self._paramiko_connect()

self._dbg(1, 'Authenticating with %s' % method.__name__)
try:
method(username, password)
return
except BadHostKeyException as e:
msg = '%s: Bad host key: %s' % (method.__name__, str(e))
self._dbg(1, msg)
errors.append(msg)
except AuthenticationException as e:
msg = 'Authentication with %s failed' % method.__name__
msg += ': ' + str(e)
self._dbg(1, msg)
errors.append(msg)
except SSHException as e:
msg = '%s: SSHException: %s' % (method.__name__, str(e))
self._dbg(1, msg)
errors.append(msg)
raise LoginFailure('Login failed: ' + '; '.join(errors))
self._dbg(1, 'Authenticating with %s' % method_name)
next_methods = getattr(self, method['function'])(username, password)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (84 > 79 characters)

if next_methods == []:
self.guess_os_from_banner(authenticated=True) # Auth done, get banner !

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (92 > 79 characters)

self._dbg(1, 'Authentication successful')
return
elif partial_methods == []: # Only attempt partial methods once (1 recursion)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (94 > 79 characters)

self.guess_os_from_banner() # Try to get banner before trying next method

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (94 > 79 characters)

self._dbg(1, 'Partial authentication done, attempting next part')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (85 > 79 characters)

self._paramiko_auth(username, password, next_methods)
except (BadAuthenticationType, AuthenticationException, BadHostKeyException, SSHException) as e:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (108 > 79 characters)

if type(e) is BadAuthenticationType: # Should happen only once
self._add_auth_error('{} reported as bad method, supported: {}'.format(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (91 > 79 characters)

method['ssh_method'],
e.allowed_types
))
elif type(e) is AuthenticationException:
self._add_auth_error('Authentication with {} failed: {}'.format(method_name, e))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (100 > 79 characters)

elif type(e) is BadHostKeyException:
self._add_auth_error('{}: Bad host key: {}'.format(method_name, e))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (87 > 79 characters)

elif type(e) is SSHException:
self._add_auth_error('{}: SSHException: {}'.format(method_name, e))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (87 > 79 characters)

if partial_methods == [] and hasattr(e, 'allowed_types'):
allowed_methods = e.allowed_types
self.guess_os_from_banner() # Log banner sent DURING auth
raise LoginFailure('Login failed: ' + '; '.join(self.auth_errors))

def _paramiko_shell(self):
rows, cols = get_terminal_size()
Expand Down Expand Up @@ -368,6 +373,13 @@ def get_banner(self):
return None
return self.client.get_banner()

def guess_os_from_banner(self, authenticated=False):
banner = self.get_banner()
if banner is not None:
if not isinstance(banner, str):
banner = banner.decode(self.encoding)
self.os_guesser.data_received(banner, authenticated)

def get_remote_version(self):
if not self.client:
return None
Expand Down