| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
""" |
|---|
| 6 |
Various classes and functions for implementing user-interaction in the |
|---|
| 7 |
command-line conch client. |
|---|
| 8 |
|
|---|
| 9 |
You probably shouldn't use anything in this module directly, since it assumes |
|---|
| 10 |
you are sitting at an interactive terminal. For example, to programmatically |
|---|
| 11 |
interact with a known_hosts database, use L{twisted.conch.client.knownhosts}. |
|---|
| 12 |
""" |
|---|
| 13 |
|
|---|
| 14 |
from twisted.python import log |
|---|
| 15 |
from twisted.python.filepath import FilePath |
|---|
| 16 |
|
|---|
| 17 |
from twisted.conch.error import ConchError |
|---|
| 18 |
from twisted.conch.ssh import common, keys, userauth |
|---|
| 19 |
from twisted.internet import defer, protocol, reactor |
|---|
| 20 |
|
|---|
| 21 |
from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI |
|---|
| 22 |
|
|---|
| 23 |
from twisted.conch.client import agent |
|---|
| 24 |
|
|---|
| 25 |
import os, sys, base64, getpass |
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
_open = open |
|---|
| 29 |
|
|---|
| 30 |
def verifyHostKey(transport, host, pubKey, fingerprint): |
|---|
| 31 |
""" |
|---|
| 32 |
Verify a host's key. |
|---|
| 33 |
|
|---|
| 34 |
This function is a gross vestige of some bad factoring in the client |
|---|
| 35 |
internals. The actual implementation, and a better signature of this logic |
|---|
| 36 |
is in L{KnownHostsFile.verifyHostKey}. This function is not deprecated yet |
|---|
| 37 |
because the callers have not yet been rehabilitated, but they should |
|---|
| 38 |
eventually be changed to call that method instead. |
|---|
| 39 |
|
|---|
| 40 |
However, this function does perform two functions not implemented by |
|---|
| 41 |
L{KnownHostsFile.verifyHostKey}. It determines the path to the user's |
|---|
| 42 |
known_hosts file based on the options (which should really be the options |
|---|
| 43 |
object's job), and it provides an opener to L{ConsoleUI} which opens |
|---|
| 44 |
'/dev/tty' so that the user will be prompted on the tty of the process even |
|---|
| 45 |
if the input and output of the process has been redirected. This latter |
|---|
| 46 |
part is, somewhat obviously, not portable, but I don't know of a portable |
|---|
| 47 |
equivalent that could be used. |
|---|
| 48 |
|
|---|
| 49 |
@param host: Due to a bug in L{SSHClientTransport.verifyHostKey}, this is |
|---|
| 50 |
always the dotted-quad IP address of the host being connected to. |
|---|
| 51 |
@type host: L{str} |
|---|
| 52 |
|
|---|
| 53 |
@param transport: the client transport which is attempting to connect to |
|---|
| 54 |
the given host. |
|---|
| 55 |
@type transport: L{SSHClientTransport} |
|---|
| 56 |
|
|---|
| 57 |
@param fingerprint: the fingerprint of the given public key, in |
|---|
| 58 |
xx:xx:xx:... format. This is ignored in favor of getting the fingerprint |
|---|
| 59 |
from the key itself. |
|---|
| 60 |
@type fingerprint: L{str} |
|---|
| 61 |
|
|---|
| 62 |
@param pubKey: The public key of the server being connected to. |
|---|
| 63 |
@type pubKey: L{str} |
|---|
| 64 |
|
|---|
| 65 |
@return: a L{Deferred} which fires with C{1} if the key was successfully |
|---|
| 66 |
verified, or fails if the key could not be successfully verified. Failure |
|---|
| 67 |
types may include L{HostKeyChanged}, L{UserRejectedKey}, L{IOError} or |
|---|
| 68 |
L{KeyboardInterrupt}. |
|---|
| 69 |
""" |
|---|
| 70 |
actualHost = transport.factory.options['host'] |
|---|
| 71 |
actualKey = keys.Key.fromString(pubKey) |
|---|
| 72 |
kh = KnownHostsFile.fromPath(FilePath( |
|---|
| 73 |
transport.factory.options['known-hosts'] |
|---|
| 74 |
or os.path.expanduser("~/.ssh/known_hosts") |
|---|
| 75 |
)) |
|---|
| 76 |
ui = ConsoleUI(lambda : _open("/dev/tty", "r+b")) |
|---|
| 77 |
return kh.verifyHostKey(ui, actualHost, host, actualKey) |
|---|
| 78 |
|
|---|
| 79 |
|
|---|
| 80 |
|
|---|
| 81 |
def isInKnownHosts(host, pubKey, options): |
|---|
| 82 |
"""checks to see if host is in the known_hosts file for the user. |
|---|
| 83 |
returns 0 if it isn't, 1 if it is and is the same, 2 if it's changed. |
|---|
| 84 |
""" |
|---|
| 85 |
keyType = common.getNS(pubKey)[0] |
|---|
| 86 |
retVal = 0 |
|---|
| 87 |
|
|---|
| 88 |
if not options['known-hosts'] and not os.path.exists(os.path.expanduser('~/.ssh/')): |
|---|
| 89 |
print 'Creating ~/.ssh directory...' |
|---|
| 90 |
os.mkdir(os.path.expanduser('~/.ssh')) |
|---|
| 91 |
kh_file = options['known-hosts'] or '~/.ssh/known_hosts' |
|---|
| 92 |
try: |
|---|
| 93 |
known_hosts = open(os.path.expanduser(kh_file)) |
|---|
| 94 |
except IOError: |
|---|
| 95 |
return 0 |
|---|
| 96 |
for line in known_hosts.xreadlines(): |
|---|
| 97 |
split = line.split() |
|---|
| 98 |
if len(split) < 3: |
|---|
| 99 |
continue |
|---|
| 100 |
hosts, hostKeyType, encodedKey = split[:3] |
|---|
| 101 |
if host not in hosts.split(','): |
|---|
| 102 |
continue |
|---|
| 103 |
if hostKeyType != keyType: |
|---|
| 104 |
continue |
|---|
| 105 |
try: |
|---|
| 106 |
decodedKey = base64.decodestring(encodedKey) |
|---|
| 107 |
except: |
|---|
| 108 |
continue |
|---|
| 109 |
if decodedKey == pubKey: |
|---|
| 110 |
return 1 |
|---|
| 111 |
else: |
|---|
| 112 |
retVal = 2 |
|---|
| 113 |
return retVal |
|---|
| 114 |
|
|---|
| 115 |
|
|---|
| 116 |
|
|---|
| 117 |
class SSHUserAuthClient(userauth.SSHUserAuthClient): |
|---|
| 118 |
|
|---|
| 119 |
def __init__(self, user, options, *args): |
|---|
| 120 |
userauth.SSHUserAuthClient.__init__(self, user, *args) |
|---|
| 121 |
self.keyAgent = None |
|---|
| 122 |
self.options = options |
|---|
| 123 |
self.usedFiles = [] |
|---|
| 124 |
if not options.identitys: |
|---|
| 125 |
options.identitys = ['~/.ssh/id_rsa', '~/.ssh/id_dsa'] |
|---|
| 126 |
|
|---|
| 127 |
def serviceStarted(self): |
|---|
| 128 |
if 'SSH_AUTH_SOCK' in os.environ and not self.options['noagent']: |
|---|
| 129 |
log.msg('using agent') |
|---|
| 130 |
cc = protocol.ClientCreator(reactor, agent.SSHAgentClient) |
|---|
| 131 |
d = cc.connectUNIX(os.environ['SSH_AUTH_SOCK']) |
|---|
| 132 |
d.addCallback(self._setAgent) |
|---|
| 133 |
d.addErrback(self._ebSetAgent) |
|---|
| 134 |
else: |
|---|
| 135 |
userauth.SSHUserAuthClient.serviceStarted(self) |
|---|
| 136 |
|
|---|
| 137 |
def serviceStopped(self): |
|---|
| 138 |
if self.keyAgent: |
|---|
| 139 |
self.keyAgent.transport.loseConnection() |
|---|
| 140 |
self.keyAgent = None |
|---|
| 141 |
|
|---|
| 142 |
def _setAgent(self, a): |
|---|
| 143 |
self.keyAgent = a |
|---|
| 144 |
d = self.keyAgent.getPublicKeys() |
|---|
| 145 |
d.addBoth(self._ebSetAgent) |
|---|
| 146 |
return d |
|---|
| 147 |
|
|---|
| 148 |
def _ebSetAgent(self, f): |
|---|
| 149 |
userauth.SSHUserAuthClient.serviceStarted(self) |
|---|
| 150 |
|
|---|
| 151 |
def _getPassword(self, prompt): |
|---|
| 152 |
try: |
|---|
| 153 |
oldout, oldin = sys.stdout, sys.stdin |
|---|
| 154 |
sys.stdin = sys.stdout = open('/dev/tty','r+') |
|---|
| 155 |
p=getpass.getpass(prompt) |
|---|
| 156 |
sys.stdout,sys.stdin=oldout,oldin |
|---|
| 157 |
return p |
|---|
| 158 |
except (KeyboardInterrupt, IOError): |
|---|
| 159 |
print |
|---|
| 160 |
raise ConchError('PEBKAC') |
|---|
| 161 |
|
|---|
| 162 |
def getPassword(self, prompt = None): |
|---|
| 163 |
if not prompt: |
|---|
| 164 |
prompt = "%s@%s's password: " % (self.user, self.transport.transport.getPeer().host) |
|---|
| 165 |
try: |
|---|
| 166 |
p = self._getPassword(prompt) |
|---|
| 167 |
return defer.succeed(p) |
|---|
| 168 |
except ConchError: |
|---|
| 169 |
return defer.fail() |
|---|
| 170 |
|
|---|
| 171 |
|
|---|
| 172 |
def getPublicKey(self): |
|---|
| 173 |
""" |
|---|
| 174 |
Get a public key from the key agent if possible, otherwise look in |
|---|
| 175 |
the next configured identity file for one. |
|---|
| 176 |
""" |
|---|
| 177 |
if self.keyAgent: |
|---|
| 178 |
key = self.keyAgent.getPublicKey() |
|---|
| 179 |
if key is not None: |
|---|
| 180 |
return key |
|---|
| 181 |
files = [x for x in self.options.identitys if x not in self.usedFiles] |
|---|
| 182 |
log.msg(str(self.options.identitys)) |
|---|
| 183 |
log.msg(str(files)) |
|---|
| 184 |
if not files: |
|---|
| 185 |
return None |
|---|
| 186 |
file = files[0] |
|---|
| 187 |
log.msg(file) |
|---|
| 188 |
self.usedFiles.append(file) |
|---|
| 189 |
file = os.path.expanduser(file) |
|---|
| 190 |
file += '.pub' |
|---|
| 191 |
if not os.path.exists(file): |
|---|
| 192 |
return self.getPublicKey() |
|---|
| 193 |
try: |
|---|
| 194 |
return keys.Key.fromFile(file) |
|---|
| 195 |
except keys.BadKeyError: |
|---|
| 196 |
return self.getPublicKey() |
|---|
| 197 |
|
|---|
| 198 |
|
|---|
| 199 |
def signData(self, publicKey, signData): |
|---|
| 200 |
""" |
|---|
| 201 |
Extend the base signing behavior by using an SSH agent to sign the |
|---|
| 202 |
data, if one is available. |
|---|
| 203 |
|
|---|
| 204 |
@type publicKey: L{Key} |
|---|
| 205 |
@type signData: C{str} |
|---|
| 206 |
""" |
|---|
| 207 |
if not self.usedFiles: |
|---|
| 208 |
return self.keyAgent.signData(publicKey.blob(), signData) |
|---|
| 209 |
else: |
|---|
| 210 |
return userauth.SSHUserAuthClient.signData(self, publicKey, signData) |
|---|
| 211 |
|
|---|
| 212 |
|
|---|
| 213 |
def getPrivateKey(self): |
|---|
| 214 |
""" |
|---|
| 215 |
Try to load the private key from the last used file identified by |
|---|
| 216 |
C{getPublicKey}, potentially asking for the passphrase if the key is |
|---|
| 217 |
encrypted. |
|---|
| 218 |
""" |
|---|
| 219 |
file = os.path.expanduser(self.usedFiles[-1]) |
|---|
| 220 |
if not os.path.exists(file): |
|---|
| 221 |
return None |
|---|
| 222 |
try: |
|---|
| 223 |
return defer.succeed(keys.Key.fromFile(file)) |
|---|
| 224 |
except keys.EncryptedKeyError: |
|---|
| 225 |
for i in range(3): |
|---|
| 226 |
prompt = "Enter passphrase for key '%s': " % \ |
|---|
| 227 |
self.usedFiles[-1] |
|---|
| 228 |
try: |
|---|
| 229 |
p = self._getPassword(prompt) |
|---|
| 230 |
return defer.succeed(keys.Key.fromFile(file, passphrase=p)) |
|---|
| 231 |
except (keys.BadKeyError, ConchError): |
|---|
| 232 |
pass |
|---|
| 233 |
return defer.fail(ConchError('bad password')) |
|---|
| 234 |
raise |
|---|
| 235 |
except KeyboardInterrupt: |
|---|
| 236 |
print |
|---|
| 237 |
reactor.stop() |
|---|
| 238 |
|
|---|
| 239 |
|
|---|
| 240 |
def getGenericAnswers(self, name, instruction, prompts): |
|---|
| 241 |
responses = [] |
|---|
| 242 |
try: |
|---|
| 243 |
oldout, oldin = sys.stdout, sys.stdin |
|---|
| 244 |
sys.stdin = sys.stdout = open('/dev/tty','r+') |
|---|
| 245 |
if name: |
|---|
| 246 |
print name |
|---|
| 247 |
if instruction: |
|---|
| 248 |
print instruction |
|---|
| 249 |
for prompt, echo in prompts: |
|---|
| 250 |
if echo: |
|---|
| 251 |
responses.append(raw_input(prompt)) |
|---|
| 252 |
else: |
|---|
| 253 |
responses.append(getpass.getpass(prompt)) |
|---|
| 254 |
finally: |
|---|
| 255 |
sys.stdout,sys.stdin=oldout,oldin |
|---|
| 256 |
return defer.succeed(responses) |
|---|