2
0
mirror of https://github.com/xcat2/confluent.git synced 2025-07-12 07:41:32 +00:00
Files
confluent/confluent_server/confluent/plugins/shell/ssh.py
Jarrod Johnson 70d8a1059c Consistently treat bytes as bytes in ssh
In Python3 systems,
there would be confusion
about bytes versus str.

Fix this so that ssh can work more consistently.
2023-02-24 15:47:20 -05:00

259 lines
10 KiB
Python

# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2015-2018 Lenovo
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# This plugin provides an ssh implementation comforming to the 'console'
# specification. consoleserver or shellserver would be equally likely
# to use this.
import confluent.exceptions as cexc
import confluent.interface.console as conapi
import confluent.log as log
try:
import cryptography
except ImportError:
# Using older, non-crypography based paramiko
cryptography = None
import eventlet
import hashlib
import sys
sys.modules['gssapi'] = None
paramiko = eventlet.import_patched('paramiko')
warnhostkey = False
if cryptography and cryptography.__version__.split('.') < ['1', '5']:
# older cryptography with paramiko breaks most key support except
# ed25519
warnhostkey = True
paramiko.transport.Transport._preferred_keys = filter(
lambda x: 'ed25519' in x,
paramiko.transport.Transport._preferred_keys)
class HostKeyHandler(paramiko.client.MissingHostKeyPolicy):
def __init__(self, configmanager, node):
self.cfm = configmanager
self.node = node
def missing_host_key(self, client, hostname, key):
fingerprint = 'sha512$' + hashlib.sha512(key.asbytes()).hexdigest()
cfg = self.cfm.get_node_attributes(
self.node, ('pubkeys.ssh', 'pubkeys.addpolicy'))
if 'pubkeys.ssh' not in cfg[self.node]:
if ('pubkeys.addpolicy' in cfg[self.node] and
cfg[self.node]['pubkeys.addpolicy'] and
cfg[self.node]['pubkeys.addpolicy']['value'] == 'manual'):
raise cexc.PubkeyInvalid('New ssh key detected',
key.asbytes(), fingerprint,
'pubkeys.ssh', 'newkey')
auditlog = log.Logger('audit')
auditlog.log({'node': self.node, 'event': 'sshautoadd',
'fingerprint': fingerprint})
self.cfm.set_node_attributes(
{self.node: {'pubkeys.ssh': fingerprint}})
return True
elif cfg[self.node]['pubkeys.ssh']['value'] == fingerprint:
return True
raise cexc.PubkeyInvalid(
'Mismatched SSH host key detected', key.asbytes(), fingerprint,
'pubkeys.ssh', 'mismatch'
)
class SshShell(conapi.Console):
def __init__(self, node, config, username=b'', password=b''):
self.node = node
self.ssh = None
self.datacallback = None
self.nodeconfig = config
self.username = username
self.password = password
self.connected = False
self.width = 80
self.height = 24
self.inputmode = 0 # 0 = username, 1 = password...
def resize(self, width, height):
self.width = width
self.height = height
if not self.connected:
return
self.shell.resize_pty(width=width, height=height)
def recvdata(self):
while self.connected:
pendingdata = self.shell.recv(8192)
if not pendingdata:
self.ssh.close()
if self.datacallback:
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
self.datacallback(pendingdata)
def connect(self, callback):
# for now, we just use the nodename as the presumptive ssh destination
# TODO(jjohnson2): use a 'nodeipget' utility function for architectures
# that would rather not use the nodename as anything but an opaque
# identifier
self.datacallback = callback
if self.username is not b'':
self.logon()
else:
self.inputmode = 0
callback('\r\nlogin as: ')
return
def logon(self):
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(
HostKeyHandler(self.nodeconfig, self.node))
try:
self.datacallback('\r\nConnecting to {}...'.format(self.node))
self.ssh.connect(self.node, username=self.username,
password=self.password, allow_agent=False,
look_for_keys=False)
except paramiko.AuthenticationException as e:
self.ssh.close()
self.inputmode = 0
self.username = b''
self.password = b''
self.datacallback('\r\nError connecting to {0}:\r\n {1}\r\n'.format(self.node, str(e)))
self.datacallback('\r\nlogin as: ')
return
except paramiko.ssh_exception.NoValidConnectionsError as e:
self.ssh.close()
self.datacallback('\r\nError connecting to {0}:\r\n {1}\r\n'.format(self.node, str(e)))
self.inputmode = 0
self.username = b''
self.password = b''
self.datacallback('\r\nlogin as: ')
return
except cexc.PubkeyInvalid as pi:
self.ssh.close()
self.keyaction = b''
self.candidatefprint = pi.fingerprint
self.datacallback(pi.message)
self.keyattrname = pi.attrname
self.datacallback('\r\nNew fingerprint: ' + pi.fingerprint)
self.inputmode = -1
self.datacallback('\r\nEnter "disconnect" or "accept": ')
return
except paramiko.SSHException as pi:
self.ssh.close()
self.inputmode = -2
warn = str(pi)
if warnhostkey:
warn += ' (Older cryptography package on this host only ' \
'works with ed25519, check ssh startup on target ' \
'and permissions on /etc/ssh/*key)\r\n' \
'Press Enter to close...'
self.datacallback('\r\n' + warn)
return
except Exception as e:
self.ssh.close()
self.ssh.close()
self.inputmode = 0
self.username = b''
self.password = b''
warn = 'Error connecting to {0}:\r\n {1}\r\n'.format(self.node, str(e))
self.datacallback('\r\n' + warn)
self.datacallback('\r\nlogin as: ')
return
self.inputmode = 2
self.connected = True
self.datacallback('Connected\r\n')
self.shell = self.ssh.invoke_shell(width=self.width,
height=self.height)
self.rxthread = eventlet.spawn(self.recvdata)
def write(self, data):
if self.inputmode == -2:
self.datacallback(conapi.ConsoleEvent.Disconnect)
return
elif self.inputmode == -1:
while len(data) and data[0:1] == b'\x7f' and len(self.keyaction):
self.datacallback('\b \b') # erase previously echoed value
self.keyaction = self.keyaction[:-1]
data = data[1:]
while len(data) and data[0:1] == b'\x7f':
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.keyaction += data
if b'\r' in self.keyaction:
action = self.keyaction.split(b'\r')[0]
if action.lower() == b'accept':
self.nodeconfig.set_node_attributes(
{self.node:
{self.keyattrname: self.candidatefprint}})
self.datacallback('\r\n')
self.logon()
elif action.lower() == b'disconnect':
self.datacallback(conapi.ConsoleEvent.Disconnect)
else:
self.keyaction = b''
self.datacallback('\r\nEnter "disconnect" or "accept": ')
elif len(data) > 0:
self.datacallback(data)
elif self.inputmode == 0:
while len(data) and data[0:1] == b'\x7f' and len(self.username):
self.datacallback('\b \b') # erase previously echoed value
self.username = self.username[:-1]
data = data[1:]
while len(data) and data[0:1] == b'\x7f':
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.username += data
if b'\r' in self.username:
self.username, self.password = self.username.split(b'\r')[:2]
lastdata = data.split(b'\r')[0]
if lastdata != '':
self.datacallback(lastdata)
self.datacallback('\r\nEnter password: ')
self.inputmode = 1
elif len(data) > 0:
# echo back typed data
self.datacallback(data)
elif self.inputmode == 1:
while len(data) > 0 and data[0:1] == b'\x7f':
self.password = self.password[:-1]
data = data[1:]
while b'\x7f' in data:
delidx = data.index(b'\x7f')
data = data[:delidx - 1] + data[delidx + 1:]
self.password += data
if b'\r' in self.password:
self.password = self.password.split(b'\r')[0]
self.datacallback(b'\r\n')
self.logon()
else:
self.shell.sendall(data)
def close(self):
if self.ssh is not None:
self.ssh.close()
self.datacallback = None
def create(nodes, element, configmanager, inputdata):
if len(nodes) == 1:
return SshShell(nodes[0], configmanager)