diff options
author | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:16:14 +0300 |
---|---|---|
committer | shmel1k <shmel1k@ydb.tech> | 2023-11-26 18:43:30 +0300 |
commit | b8cf9e88f4c5c64d9406af533d8948deb050d695 (patch) | |
tree | 218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py2/twisted/conch/client | |
parent | 523f645a83a0ec97a0332dbc3863bb354c92a328 (diff) | |
download | ydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz |
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py2/twisted/conch/client')
7 files changed, 1294 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py2/twisted/conch/client/__init__.py b/contrib/python/Twisted/py2/twisted/conch/client/__init__.py new file mode 100644 index 0000000000..f55d474db4 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/__init__.py @@ -0,0 +1,9 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# +""" +Client support code for Conch. + +Maintainer: Paul Swartz +""" diff --git a/contrib/python/Twisted/py2/twisted/conch/client/agent.py b/contrib/python/Twisted/py2/twisted/conch/client/agent.py new file mode 100644 index 0000000000..fdf08356f1 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/agent.py @@ -0,0 +1,73 @@ +# -*- test-case-name: twisted.conch.test.test_default -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Accesses the key agent for user authentication. + +Maintainer: Paul Swartz +""" + +import os + +from twisted.conch.ssh import agent, channel, keys +from twisted.internet import protocol, reactor +from twisted.python import log + + + +class SSHAgentClient(agent.SSHAgentClient): + + def __init__(self): + agent.SSHAgentClient.__init__(self) + self.blobs = [] + + + def getPublicKeys(self): + return self.requestIdentities().addCallback(self._cbPublicKeys) + + + def _cbPublicKeys(self, blobcomm): + log.msg('got %i public keys' % len(blobcomm)) + self.blobs = [x[0] for x in blobcomm] + + + def getPublicKey(self): + """ + Return a L{Key} from the first blob in C{self.blobs}, if any, or + return L{None}. + """ + if self.blobs: + return keys.Key.fromString(self.blobs.pop(0)) + return None + + + +class SSHAgentForwardingChannel(channel.SSHChannel): + + def channelOpen(self, specificData): + cc = protocol.ClientCreator(reactor, SSHAgentForwardingLocal) + d = cc.connectUNIX(os.environ['SSH_AUTH_SOCK']) + d.addCallback(self._cbGotLocal) + d.addErrback(lambda x:self.loseConnection()) + self.buf = '' + + + def _cbGotLocal(self, local): + self.local = local + self.dataReceived = self.local.transport.write + self.local.dataReceived = self.write + + + def dataReceived(self, data): + self.buf += data + + + def closed(self): + if self.local: + self.local.loseConnection() + self.local = None + + +class SSHAgentForwardingLocal(protocol.Protocol): + pass diff --git a/contrib/python/Twisted/py2/twisted/conch/client/connect.py b/contrib/python/Twisted/py2/twisted/conch/client/connect.py new file mode 100644 index 0000000000..ac47187e49 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/connect.py @@ -0,0 +1,21 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# +from twisted.conch.client import direct + +connectTypes = {"direct" : direct.connect} + +def connect(host, port, options, verifyHostKey, userAuthObject): + useConnects = ['direct'] + return _ebConnect(None, useConnects, host, port, options, verifyHostKey, + userAuthObject) + +def _ebConnect(f, useConnects, host, port, options, vhk, uao): + if not useConnects: + return f + connectType = useConnects.pop(0) + f = connectTypes[connectType] + d = f(host, port, options, vhk, uao) + d.addErrback(_ebConnect, useConnects, host, port, options, vhk, uao) + return d diff --git a/contrib/python/Twisted/py2/twisted/conch/client/default.py b/contrib/python/Twisted/py2/twisted/conch/client/default.py new file mode 100644 index 0000000000..ff2d635314 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/default.py @@ -0,0 +1,349 @@ +# -*- test-case-name: twisted.conch.test.test_knownhosts,twisted.conch.test.test_default -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +Various classes and functions for implementing user-interaction in the +command-line conch client. + +You probably shouldn't use anything in this module directly, since it assumes +you are sitting at an interactive terminal. For example, to programmatically +interact with a known_hosts database, use L{twisted.conch.client.knownhosts}. +""" + +from __future__ import print_function + +from twisted.python import log +from twisted.python.compat import ( + nativeString, raw_input, _PY3, _b64decodebytes as decodebytes) +from twisted.python.filepath import FilePath + +from twisted.conch.error import ConchError +from twisted.conch.ssh import common, keys, userauth +from twisted.internet import defer, protocol, reactor + +from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI + +from twisted.conch.client import agent + +import os, sys, getpass, contextlib + +if _PY3: + import io + +# The default location of the known hosts file (probably should be parsed out +# of an ssh config file someday). +_KNOWN_HOSTS = "~/.ssh/known_hosts" + + +# This name is bound so that the unit tests can use 'patch' to override it. +_open = open + +def verifyHostKey(transport, host, pubKey, fingerprint): + """ + Verify a host's key. + + This function is a gross vestige of some bad factoring in the client + internals. The actual implementation, and a better signature of this logic + is in L{KnownHostsFile.verifyHostKey}. This function is not deprecated yet + because the callers have not yet been rehabilitated, but they should + eventually be changed to call that method instead. + + However, this function does perform two functions not implemented by + L{KnownHostsFile.verifyHostKey}. It determines the path to the user's + known_hosts file based on the options (which should really be the options + object's job), and it provides an opener to L{ConsoleUI} which opens + '/dev/tty' so that the user will be prompted on the tty of the process even + if the input and output of the process has been redirected. This latter + part is, somewhat obviously, not portable, but I don't know of a portable + equivalent that could be used. + + @param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is + always the dotted-quad IP address of the host being connected to. + @type host: L{str} + + @param transport: the client transport which is attempting to connect to + the given host. + @type transport: L{SSHClientTransport} + + @param fingerprint: the fingerprint of the given public key, in + xx:xx:xx:... format. This is ignored in favor of getting the fingerprint + from the key itself. + @type fingerprint: L{str} + + @param pubKey: The public key of the server being connected to. + @type pubKey: L{str} + + @return: a L{Deferred} which fires with C{1} if the key was successfully + verified, or fails if the key could not be successfully verified. Failure + types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or + L{KeyboardInterrupt}. + """ + actualHost = transport.factory.options['host'] + actualKey = keys.Key.fromString(pubKey) + kh = KnownHostsFile.fromPath(FilePath( + transport.factory.options['known-hosts'] + or os.path.expanduser(_KNOWN_HOSTS) + )) + ui = ConsoleUI(lambda : _open("/dev/tty", "r+b", buffering=0)) + return kh.verifyHostKey(ui, actualHost, host, actualKey) + + + +def isInKnownHosts(host, pubKey, options): + """ + Checks to see if host is in the known_hosts file for the user. + + @return: 0 if it isn't, 1 if it is and is the same, 2 if it's changed. + @rtype: L{int} + """ + keyType = common.getNS(pubKey)[0] + retVal = 0 + + if not options['known-hosts'] and not os.path.exists(os.path.expanduser('~/.ssh/')): + print('Creating ~/.ssh directory...') + os.mkdir(os.path.expanduser('~/.ssh')) + kh_file = options['known-hosts'] or _KNOWN_HOSTS + try: + known_hosts = open(os.path.expanduser(kh_file), 'rb') + except IOError: + return 0 + with known_hosts: + for line in known_hosts.readlines(): + split = line.split() + if len(split) < 3: + continue + hosts, hostKeyType, encodedKey = split[:3] + if host not in hosts.split(b','): # incorrect host + continue + if hostKeyType != keyType: # incorrect type of key + continue + try: + decodedKey = decodebytes(encodedKey) + except: + continue + if decodedKey == pubKey: + return 1 + else: + retVal = 2 + return retVal + + + +def getHostKeyAlgorithms(host, options): + """ + Look in known_hosts for a key corresponding to C{host}. + This can be used to change the order of supported key types + in the KEXINIT packet. + + @type host: L{str} + @param host: the host to check in known_hosts + @type options: L{twisted.conch.client.options.ConchOptions} + @param options: options passed to client + @return: L{list} of L{str} representing key types or L{None}. + """ + knownHosts = KnownHostsFile.fromPath(FilePath( + options['known-hosts'] + or os.path.expanduser(_KNOWN_HOSTS) + )) + keyTypes = [] + for entry in knownHosts.iterentries(): + if entry.matchesHost(host): + if entry.keyType not in keyTypes: + keyTypes.append(entry.keyType) + return keyTypes or None + + + +class SSHUserAuthClient(userauth.SSHUserAuthClient): + + def __init__(self, user, options, *args): + userauth.SSHUserAuthClient.__init__(self, user, *args) + self.keyAgent = None + self.options = options + self.usedFiles = [] + if not options.identitys: + options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa'] + + + def serviceStarted(self): + if 'SSH_AUTH_SOCK' in os.environ and not self.options['noagent']: + log.msg('using agent') + cc = protocol.ClientCreator(reactor, agent.SSHAgentClient) + d = cc.connectUNIX(os.environ['SSH_AUTH_SOCK']) + d.addCallback(self._setAgent) + d.addErrback(self._ebSetAgent) + else: + userauth.SSHUserAuthClient.serviceStarted(self) + + + def serviceStopped(self): + if self.keyAgent: + self.keyAgent.transport.loseConnection() + self.keyAgent = None + + + def _setAgent(self, a): + self.keyAgent = a + d = self.keyAgent.getPublicKeys() + d.addBoth(self._ebSetAgent) + return d + + + def _ebSetAgent(self, f): + userauth.SSHUserAuthClient.serviceStarted(self) + + + def _getPassword(self, prompt): + """ + Prompt for a password using L{getpass.getpass}. + + @param prompt: Written on tty to ask for the input. + @type prompt: L{str} + @return: The input. + @rtype: L{str} + """ + with self._replaceStdoutStdin(): + try: + p = getpass.getpass(prompt) + return p + except (KeyboardInterrupt, IOError): + print() + raise ConchError('PEBKAC') + + + def getPassword(self, prompt = None): + if prompt: + prompt = nativeString(prompt) + else: + prompt = ("%s@%s's password: " % + (nativeString(self.user), self.transport.transport.getPeer().host)) + try: + # We don't know the encoding the other side is using, + # signaling that is not part of the SSH protocol. But + # using our defaultencoding is better than just going for + # ASCII. + p = self._getPassword(prompt).encode(sys.getdefaultencoding()) + return defer.succeed(p) + except ConchError: + return defer.fail() + + + def getPublicKey(self): + """ + Get a public key from the key agent if possible, otherwise look in + the next configured identity file for one. + """ + if self.keyAgent: + key = self.keyAgent.getPublicKey() + if key is not None: + return key + files = [x for x in self.options.identitys if x not in self.usedFiles] + log.msg(str(self.options.identitys)) + log.msg(str(files)) + if not files: + return None + file = files[0] + log.msg(file) + self.usedFiles.append(file) + file = os.path.expanduser(file) + file += '.pub' + if not os.path.exists(file): + return self.getPublicKey() # try again + try: + return keys.Key.fromFile(file) + except keys.BadKeyError: + return self.getPublicKey() # try again + + + def signData(self, publicKey, signData): + """ + Extend the base signing behavior by using an SSH agent to sign the + data, if one is available. + + @type publicKey: L{Key} + @type signData: L{bytes} + """ + if not self.usedFiles: # agent key + return self.keyAgent.signData(publicKey.blob(), signData) + else: + return userauth.SSHUserAuthClient.signData(self, publicKey, signData) + + + def getPrivateKey(self): + """ + Try to load the private key from the last used file identified by + C{getPublicKey}, potentially asking for the passphrase if the key is + encrypted. + """ + file = os.path.expanduser(self.usedFiles[-1]) + if not os.path.exists(file): + return None + try: + return defer.succeed(keys.Key.fromFile(file)) + except keys.EncryptedKeyError: + for i in range(3): + prompt = "Enter passphrase for key '%s': " % self.usedFiles[-1] + try: + p = self._getPassword(prompt).encode( + sys.getfilesystemencoding()) + return defer.succeed(keys.Key.fromFile(file, passphrase=p)) + except (keys.BadKeyError, ConchError): + pass + return defer.fail(ConchError('bad password')) + raise + except KeyboardInterrupt: + print() + reactor.stop() + + + def getGenericAnswers(self, name, instruction, prompts): + responses = [] + with self._replaceStdoutStdin(): + if name: + print(name.decode("utf-8")) + if instruction: + print(instruction.decode("utf-8")) + for prompt, echo in prompts: + prompt = prompt.decode("utf-8") + if echo: + responses.append(raw_input(prompt)) + else: + responses.append(getpass.getpass(prompt)) + return defer.succeed(responses) + + + @classmethod + def _openTty(cls): + """ + Open /dev/tty as two streams one in read, one in write mode, + and return them. + + @return: File objects for reading and writing to /dev/tty, + corresponding to standard input and standard output. + @rtype: A L{tuple} of L{io.TextIOWrapper} on Python 3. + A L{tuple} of binary files on Python 2. + """ + stdin = open("/dev/tty", "rb") + stdout = open("/dev/tty", "wb") + if _PY3: + stdin = io.TextIOWrapper(stdin) + stdout = io.TextIOWrapper(stdout) + return stdin, stdout + + + @classmethod + @contextlib.contextmanager + def _replaceStdoutStdin(cls): + """ + Contextmanager that replaces stdout and stdin with /dev/tty + and resets them when it is done. + """ + oldout, oldin = sys.stdout, sys.stdin + sys.stdin, sys.stdout = cls._openTty() + try: + yield + finally: + sys.stdout.close() + sys.stdin.close() + sys.stdout, sys.stdin = oldout, oldin diff --git a/contrib/python/Twisted/py2/twisted/conch/client/direct.py b/contrib/python/Twisted/py2/twisted/conch/client/direct.py new file mode 100644 index 0000000000..601a9d2dc3 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/direct.py @@ -0,0 +1,109 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + + +from __future__ import print_function + +from twisted.internet import defer, protocol, reactor +from twisted.conch import error +from twisted.conch.ssh import transport +from twisted.python import log + + + +class SSHClientFactory(protocol.ClientFactory): + + def __init__(self, d, options, verifyHostKey, userAuthObject): + self.d = d + self.options = options + self.verifyHostKey = verifyHostKey + self.userAuthObject = userAuthObject + + + def clientConnectionLost(self, connector, reason): + if self.options['reconnect']: + connector.connect() + + + def clientConnectionFailed(self, connector, reason): + if self.d is None: + return + d, self.d = self.d, None + d.errback(reason) + + + def buildProtocol(self, addr): + trans = SSHClientTransport(self) + if self.options['ciphers']: + trans.supportedCiphers = self.options['ciphers'] + if self.options['macs']: + trans.supportedMACs = self.options['macs'] + if self.options['compress']: + trans.supportedCompressions[0:1] = ['zlib'] + if self.options['host-key-algorithms']: + trans.supportedPublicKeys = self.options['host-key-algorithms'] + return trans + + + +class SSHClientTransport(transport.SSHClientTransport): + + def __init__(self, factory): + self.factory = factory + self.unixServer = None + + + def connectionLost(self, reason): + if self.unixServer: + d = self.unixServer.stopListening() + self.unixServer = None + else: + d = defer.succeed(None) + d.addCallback(lambda x: + transport.SSHClientTransport.connectionLost(self, reason)) + + + def receiveError(self, code, desc): + if self.factory.d is None: + return + d, self.factory.d = self.factory.d, None + d.errback(error.ConchError(desc, code)) + + + def sendDisconnect(self, code, reason): + if self.factory.d is None: + return + d, self.factory.d = self.factory.d, None + transport.SSHClientTransport.sendDisconnect(self, code, reason) + d.errback(error.ConchError(reason, code)) + + + def receiveDebug(self, alwaysDisplay, message, lang): + log.msg('Received Debug Message: %s' % message) + if alwaysDisplay: # XXX what should happen here? + print(message) + + + def verifyHostKey(self, pubKey, fingerprint): + return self.factory.verifyHostKey(self, self.transport.getPeer().host, pubKey, + fingerprint) + + + def setService(self, service): + log.msg('setting client server to %s' % service) + transport.SSHClientTransport.setService(self, service) + if service.name != 'ssh-userauth' and self.factory.d is not None: + d, self.factory.d = self.factory.d, None + d.callback(None) + + + def connectionSecure(self): + self.requestService(self.factory.userAuthObject) + + + +def connect(host, port, options, verifyHostKey, userAuthObject): + d = defer.Deferred() + factory = SSHClientFactory(d, options, verifyHostKey, userAuthObject) + reactor.connectTCP(host, port, factory) + return d diff --git a/contrib/python/Twisted/py2/twisted/conch/client/knownhosts.py b/contrib/python/Twisted/py2/twisted/conch/client/knownhosts.py new file mode 100644 index 0000000000..aa0a622d41 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/knownhosts.py @@ -0,0 +1,630 @@ +# -*- test-case-name: twisted.conch.test.test_knownhosts -*- +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +""" +An implementation of the OpenSSH known_hosts database. + +@since: 8.2 +""" + +from __future__ import absolute_import, division + +import hmac +from binascii import Error as DecodeError, b2a_base64, a2b_base64 +from contextlib import closing +from hashlib import sha1 +import sys + +from zope.interface import implementer + +from twisted.conch.interfaces import IKnownHostEntry +from twisted.conch.error import HostKeyChanged, UserRejectedKey, InvalidEntry +from twisted.conch.ssh.keys import Key, BadKeyError, FingerprintFormats +from twisted.internet import defer +from twisted.python import log +from twisted.python.compat import nativeString, unicode +from twisted.python.randbytes import secureRandom +from twisted.python.util import FancyEqMixin + + +def _b64encode(s): + """ + Encode a binary string as base64 with no trailing newline. + + @param s: The string to encode. + @type s: L{bytes} + + @return: The base64-encoded string. + @rtype: L{bytes} + """ + return b2a_base64(s).strip() + + + +def _extractCommon(string): + """ + Extract common elements of base64 keys from an entry in a hosts file. + + @param string: A known hosts file entry (a single line). + @type string: L{bytes} + + @return: a 4-tuple of hostname data (L{bytes}), ssh key type (L{bytes}), key + (L{Key}), and comment (L{bytes} or L{None}). The hostname data is + simply the beginning of the line up to the first occurrence of + whitespace. + @rtype: L{tuple} + """ + elements = string.split(None, 2) + if len(elements) != 3: + raise InvalidEntry() + hostnames, keyType, keyAndComment = elements + splitkey = keyAndComment.split(None, 1) + if len(splitkey) == 2: + keyString, comment = splitkey + comment = comment.rstrip(b"\n") + else: + keyString = splitkey[0] + comment = None + key = Key.fromString(a2b_base64(keyString)) + return hostnames, keyType, key, comment + + + +class _BaseEntry(object): + """ + Abstract base of both hashed and non-hashed entry objects, since they + represent keys and key types the same way. + + @ivar keyType: The type of the key; either ssh-dss or ssh-rsa. + @type keyType: L{bytes} + + @ivar publicKey: The server public key indicated by this line. + @type publicKey: L{twisted.conch.ssh.keys.Key} + + @ivar comment: Trailing garbage after the key line. + @type comment: L{bytes} + """ + + def __init__(self, keyType, publicKey, comment): + self.keyType = keyType + self.publicKey = publicKey + self.comment = comment + + + def matchesKey(self, keyObject): + """ + Check to see if this entry matches a given key object. + + @param keyObject: A public key object to check. + @type keyObject: L{Key} + + @return: C{True} if this entry's key matches C{keyObject}, C{False} + otherwise. + @rtype: L{bool} + """ + return self.publicKey == keyObject + + + +@implementer(IKnownHostEntry) +class PlainEntry(_BaseEntry): + """ + A L{PlainEntry} is a representation of a plain-text entry in a known_hosts + file. + + @ivar _hostnames: the list of all host-names associated with this entry. + @type _hostnames: L{list} of L{bytes} + """ + + def __init__(self, hostnames, keyType, publicKey, comment): + self._hostnames = hostnames + super(PlainEntry, self).__init__(keyType, publicKey, comment) + + + @classmethod + def fromString(cls, string): + """ + Parse a plain-text entry in a known_hosts file, and return a + corresponding L{PlainEntry}. + + @param string: a space-separated string formatted like "hostname + key-type base64-key-data comment". + + @type string: L{bytes} + + @raise DecodeError: if the key is not valid encoded as valid base64. + + @raise InvalidEntry: if the entry does not have the right number of + elements and is therefore invalid. + + @raise BadKeyError: if the key, once decoded from base64, is not + actually an SSH key. + + @return: an IKnownHostEntry representing the hostname and key in the + input line. + + @rtype: L{PlainEntry} + """ + hostnames, keyType, key, comment = _extractCommon(string) + self = cls(hostnames.split(b","), keyType, key, comment) + return self + + + def matchesHost(self, hostname): + """ + Check to see if this entry matches a given hostname. + + @param hostname: A hostname or IP address literal to check against this + entry. + @type hostname: L{bytes} + + @return: C{True} if this entry is for the given hostname or IP address, + C{False} otherwise. + @rtype: L{bool} + """ + if isinstance(hostname, unicode): + hostname = hostname.encode("utf-8") + return hostname in self._hostnames + + + def toString(self): + """ + Implement L{IKnownHostEntry.toString} by recording the comma-separated + hostnames, key type, and base-64 encoded key. + + @return: The string representation of this entry, with unhashed hostname + information. + @rtype: L{bytes} + """ + fields = [b','.join(self._hostnames), + self.keyType, + _b64encode(self.publicKey.blob())] + if self.comment is not None: + fields.append(self.comment) + return b' '.join(fields) + + + +@implementer(IKnownHostEntry) +class UnparsedEntry(object): + """ + L{UnparsedEntry} is an entry in a L{KnownHostsFile} which can't actually be + parsed; therefore it matches no keys and no hosts. + """ + + def __init__(self, string): + """ + Create an unparsed entry from a line in a known_hosts file which cannot + otherwise be parsed. + """ + self._string = string + + + def matchesHost(self, hostname): + """ + Always returns False. + """ + return False + + + def matchesKey(self, key): + """ + Always returns False. + """ + return False + + + def toString(self): + """ + Returns the input line, without its newline if one was given. + + @return: The string representation of this entry, almost exactly as was + used to initialize this entry but without a trailing newline. + @rtype: L{bytes} + """ + return self._string.rstrip(b"\n") + + + +def _hmacedString(key, string): + """ + Return the SHA-1 HMAC hash of the given key and string. + + @param key: The HMAC key. + @type key: L{bytes} + + @param string: The string to be hashed. + @type string: L{bytes} + + @return: The keyed hash value. + @rtype: L{bytes} + """ + hash = hmac.HMAC(key, digestmod=sha1) + if isinstance(string, unicode): + string = string.encode("utf-8") + hash.update(string) + return hash.digest() + + + +@implementer(IKnownHostEntry) +class HashedEntry(_BaseEntry, FancyEqMixin): + """ + A L{HashedEntry} is a representation of an entry in a known_hosts file + where the hostname has been hashed and salted. + + @ivar _hostSalt: the salt to combine with a hostname for hashing. + + @ivar _hostHash: the hashed representation of the hostname. + + @cvar MAGIC: the 'hash magic' string used to identify a hashed line in a + known_hosts file as opposed to a plaintext one. + """ + + MAGIC = b'|1|' + + compareAttributes = ( + "_hostSalt", "_hostHash", "keyType", "publicKey", "comment") + + def __init__(self, hostSalt, hostHash, keyType, publicKey, comment): + self._hostSalt = hostSalt + self._hostHash = hostHash + super(HashedEntry, self).__init__(keyType, publicKey, comment) + + + @classmethod + def fromString(cls, string): + """ + Load a hashed entry from a string representing a line in a known_hosts + file. + + @param string: A complete single line from a I{known_hosts} file, + formatted as defined by OpenSSH. + @type string: L{bytes} + + @raise DecodeError: if the key, the hostname, or the is not valid + encoded as valid base64 + + @raise InvalidEntry: if the entry does not have the right number of + elements and is therefore invalid, or the host/hash portion contains + more items than just the host and hash. + + @raise BadKeyError: if the key, once decoded from base64, is not + actually an SSH key. + + @return: The newly created L{HashedEntry} instance, initialized with the + information from C{string}. + """ + stuff, keyType, key, comment = _extractCommon(string) + saltAndHash = stuff[len(cls.MAGIC):].split(b"|") + if len(saltAndHash) != 2: + raise InvalidEntry() + hostSalt, hostHash = saltAndHash + self = cls(a2b_base64(hostSalt), a2b_base64(hostHash), + keyType, key, comment) + return self + + + def matchesHost(self, hostname): + """ + Implement L{IKnownHostEntry.matchesHost} to compare the hash of the + input to the stored hash. + + @param hostname: A hostname or IP address literal to check against this + entry. + @type hostname: L{bytes} + + @return: C{True} if this entry is for the given hostname or IP address, + C{False} otherwise. + @rtype: L{bool} + """ + return (_hmacedString(self._hostSalt, hostname) == self._hostHash) + + + def toString(self): + """ + Implement L{IKnownHostEntry.toString} by base64-encoding the salt, host + hash, and key. + + @return: The string representation of this entry, with the hostname part + hashed. + @rtype: L{bytes} + """ + fields = [self.MAGIC + b'|'.join([_b64encode(self._hostSalt), + _b64encode(self._hostHash)]), + self.keyType, + _b64encode(self.publicKey.blob())] + if self.comment is not None: + fields.append(self.comment) + return b' '.join(fields) + + + +class KnownHostsFile(object): + """ + A structured representation of an OpenSSH-format ~/.ssh/known_hosts file. + + @ivar _added: A list of L{IKnownHostEntry} providers which have been added + to this instance in memory but not yet saved. + + @ivar _clobber: A flag indicating whether the current contents of the save + path will be disregarded and potentially overwritten or not. If + C{True}, this will be done. If C{False}, entries in the save path will + be read and new entries will be saved by appending rather than + overwriting. + @type _clobber: L{bool} + + @ivar _savePath: See C{savePath} parameter of L{__init__}. + """ + + def __init__(self, savePath): + """ + Create a new, empty KnownHostsFile. + + Unless you want to erase the current contents of C{savePath}, you want + to use L{KnownHostsFile.fromPath} instead. + + @param savePath: The L{FilePath} to which to save new entries. + @type savePath: L{FilePath} + """ + self._added = [] + self._savePath = savePath + self._clobber = True + + + @property + def savePath(self): + """ + @see: C{savePath} parameter of L{__init__} + """ + return self._savePath + + + def iterentries(self): + """ + Iterate over the host entries in this file. + + @return: An iterable the elements of which provide L{IKnownHostEntry}. + There is an element for each entry in the file as well as an element + for each added but not yet saved entry. + @rtype: iterable of L{IKnownHostEntry} providers + """ + for entry in self._added: + yield entry + + if self._clobber: + return + + try: + fp = self._savePath.open() + except IOError: + return + + with fp: + for line in fp: + try: + if line.startswith(HashedEntry.MAGIC): + entry = HashedEntry.fromString(line) + else: + entry = PlainEntry.fromString(line) + except (DecodeError, InvalidEntry, BadKeyError): + entry = UnparsedEntry(line) + yield entry + + + def hasHostKey(self, hostname, key): + """ + Check for an entry with matching hostname and key. + + @param hostname: A hostname or IP address literal to check for. + @type hostname: L{bytes} + + @param key: The public key to check for. + @type key: L{Key} + + @return: C{True} if the given hostname and key are present in this file, + C{False} if they are not. + @rtype: L{bool} + + @raise HostKeyChanged: if the host key found for the given hostname + does not match the given key. + """ + for lineidx, entry in enumerate(self.iterentries(), -len(self._added)): + if entry.matchesHost(hostname) and entry.keyType == key.sshType(): + if entry.matchesKey(key): + return True + else: + # Notice that lineidx is 0-based but HostKeyChanged.lineno + # is 1-based. + if lineidx < 0: + line = None + path = None + else: + line = lineidx + 1 + path = self._savePath + raise HostKeyChanged(entry, path, line) + return False + + + def verifyHostKey(self, ui, hostname, ip, key): + """ + Verify the given host key for the given IP and host, asking for + confirmation from, and notifying, the given UI about changes to this + file. + + @param ui: The user interface to request an IP address from. + + @param hostname: The hostname that the user requested to connect to. + + @param ip: The string representation of the IP address that is actually + being connected to. + + @param key: The public key of the server. + + @return: a L{Deferred} that fires with True when the key has been + verified, or fires with an errback when the key either cannot be + verified or has changed. + @rtype: L{Deferred} + """ + hhk = defer.maybeDeferred(self.hasHostKey, hostname, key) + def gotHasKey(result): + if result: + if not self.hasHostKey(ip, key): + ui.warn("Warning: Permanently added the %s host key for " + "IP address '%s' to the list of known hosts." % + (key.type(), nativeString(ip))) + self.addHostKey(ip, key) + self.save() + return result + else: + def promptResponse(response): + if response: + self.addHostKey(hostname, key) + self.addHostKey(ip, key) + self.save() + return response + else: + raise UserRejectedKey() + + keytype = key.type() + + if keytype == "EC": + keytype = "ECDSA" + + prompt = ( + "The authenticity of host '%s (%s)' " + "can't be established.\n" + "%s key fingerprint is SHA256:%s.\n" + "Are you sure you want to continue connecting (yes/no)? " % + (nativeString(hostname), nativeString(ip), keytype, + key.fingerprint(format=FingerprintFormats.SHA256_BASE64))) + proceed = ui.prompt(prompt.encode(sys.getdefaultencoding())) + return proceed.addCallback(promptResponse) + return hhk.addCallback(gotHasKey) + + + def addHostKey(self, hostname, key): + """ + Add a new L{HashedEntry} to the key database. + + Note that you still need to call L{KnownHostsFile.save} if you wish + these changes to be persisted. + + @param hostname: A hostname or IP address literal to associate with the + new entry. + @type hostname: L{bytes} + + @param key: The public key to associate with the new entry. + @type key: L{Key} + + @return: The L{HashedEntry} that was added. + @rtype: L{HashedEntry} + """ + salt = secureRandom(20) + keyType = key.sshType() + entry = HashedEntry(salt, _hmacedString(salt, hostname), + keyType, key, None) + self._added.append(entry) + return entry + + + def save(self): + """ + Save this L{KnownHostsFile} to the path it was loaded from. + """ + p = self._savePath.parent() + if not p.isdir(): + p.makedirs() + + if self._clobber: + mode = "wb" + else: + mode = "ab" + + with self._savePath.open(mode) as hostsFileObj: + if self._added: + hostsFileObj.write( + b"\n".join([entry.toString() for entry in self._added]) + + b"\n") + self._added = [] + self._clobber = False + + + @classmethod + def fromPath(cls, path): + """ + Create a new L{KnownHostsFile}, potentially reading existing known + hosts information from the given file. + + @param path: A path object to use for both reading contents from and + later saving to. If no file exists at this path, it is not an + error; a L{KnownHostsFile} with no entries is returned. + @type path: L{FilePath} + + @return: A L{KnownHostsFile} initialized with entries from C{path}. + @rtype: L{KnownHostsFile} + """ + knownHosts = cls(path) + knownHosts._clobber = False + return knownHosts + + + +class ConsoleUI(object): + """ + A UI object that can ask true/false questions and post notifications on the + console, to be used during key verification. + """ + def __init__(self, opener): + """ + @param opener: A no-argument callable which should open a console + binary-mode file-like object to be used for reading and writing. + This initializes the C{opener} attribute. + @type opener: callable taking no arguments and returning a read/write + file-like object + """ + self.opener = opener + + + def prompt(self, text): + """ + Write the given text as a prompt to the console output, then read a + result from the console input. + + @param text: Something to present to a user to solicit a yes or no + response. + @type text: L{bytes} + + @return: a L{Deferred} which fires with L{True} when the user answers + 'yes' and L{False} when the user answers 'no'. It may errback if + there were any I/O errors. + """ + d = defer.succeed(None) + def body(ignored): + with closing(self.opener()) as f: + f.write(text) + while True: + answer = f.readline().strip().lower() + if answer == b'yes': + return True + elif answer == b'no': + return False + else: + f.write(b"Please type 'yes' or 'no': ") + return d.addCallback(body) + + + def warn(self, text): + """ + Notify the user (non-interactively) of the provided text, by writing it + to the console. + + @param text: Some information the user is to be made aware of. + @type text: L{bytes} + """ + try: + with closing(self.opener()) as f: + f.write(text) + except: + log.err() diff --git a/contrib/python/Twisted/py2/twisted/conch/client/options.py b/contrib/python/Twisted/py2/twisted/conch/client/options.py new file mode 100644 index 0000000000..5630fce250 --- /dev/null +++ b/contrib/python/Twisted/py2/twisted/conch/client/options.py @@ -0,0 +1,103 @@ +# Copyright (c) Twisted Matrix Laboratories. +# See LICENSE for details. + +# +from twisted.conch.ssh.transport import SSHClientTransport, SSHCiphers +from twisted.python import usage +from twisted.python.compat import unicode + +import sys + +class ConchOptions(usage.Options): + + optParameters = [['user', 'l', None, 'Log in using this user name.'], + ['identity', 'i', None], + ['ciphers', 'c', None], + ['macs', 'm', None], + ['port', 'p', None, 'Connect to this port. Server must be on the same port.'], + ['option', 'o', None, 'Ignored OpenSSH options'], + ['host-key-algorithms', '', None], + ['known-hosts', '', None, 'File to check for host keys'], + ['user-authentications', '', None, 'Types of user authentications to use.'], + ['logfile', '', None, 'File to log to, or - for stdout'], + ] + + optFlags = [['version', 'V', 'Display version number only.'], + ['compress', 'C', 'Enable compression.'], + ['log', 'v', 'Enable logging (defaults to stderr)'], + ['nox11', 'x', 'Disable X11 connection forwarding (default)'], + ['agent', 'A', 'Enable authentication agent forwarding'], + ['noagent', 'a', 'Disable authentication agent forwarding (default)'], + ['reconnect', 'r', 'Reconnect to the server if the connection is lost.'], + ] + + compData = usage.Completions( + mutuallyExclusive=[("agent", "noagent")], + optActions={ + "user": usage.CompleteUsernames(), + "ciphers": usage.CompleteMultiList( + SSHCiphers.cipherMap.keys(), + descr='ciphers to choose from'), + "macs": usage.CompleteMultiList( + SSHCiphers.macMap.keys(), + descr='macs to choose from'), + "host-key-algorithms": usage.CompleteMultiList( + SSHClientTransport.supportedPublicKeys, + descr='host key algorithms to choose from'), + #"user-authentications": usage.CompleteMultiList(? + # descr='user authentication types' ), + }, + extraActions=[usage.CompleteUserAtHost(), + usage.Completer(descr="command"), + usage.Completer(descr='argument', + repeat=True)] + ) + + def __init__(self, *args, **kw): + usage.Options.__init__(self, *args, **kw) + self.identitys = [] + self.conns = None + + def opt_identity(self, i): + """Identity for public-key authentication""" + self.identitys.append(i) + + def opt_ciphers(self, ciphers): + "Select encryption algorithms" + ciphers = ciphers.split(',') + for cipher in ciphers: + if cipher not in SSHCiphers.cipherMap: + sys.exit("Unknown cipher type '%s'" % cipher) + self['ciphers'] = ciphers + + + def opt_macs(self, macs): + "Specify MAC algorithms" + if isinstance(macs, unicode): + macs = macs.encode("utf-8") + macs = macs.split(b',') + for mac in macs: + if mac not in SSHCiphers.macMap: + sys.exit("Unknown mac type '%r'" % mac) + self['macs'] = macs + + def opt_host_key_algorithms(self, hkas): + "Select host key algorithms" + if isinstance(hkas, unicode): + hkas = hkas.encode("utf-8") + hkas = hkas.split(b',') + for hka in hkas: + if hka not in SSHClientTransport.supportedPublicKeys: + sys.exit("Unknown host key type '%r'" % hka) + self['host-key-algorithms'] = hkas + + def opt_user_authentications(self, uas): + "Choose how to authenticate to the remote server" + if isinstance(uas, unicode): + uas = uas.encode("utf-8") + self['user-authentications'] = uas.split(b',') + +# def opt_compress(self): +# "Enable compression" +# self.enableCompression = 1 +# SSHClientTransport.supportedCompressions[0:1] = ['zlib'] |