Imported Upstream version 12.1.0
[contrib/python-twisted.git] / twisted / conch / client / default.py
1 # -*- test-case-name: twisted.conch.test.test_knownhosts,twisted.conch.test.test_default -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
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 # This name is bound so that the unit tests can use 'patch' to override it.
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(','): # incorrect host
102             continue
103         if hostKeyType != keyType: # incorrect type of key
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() # try again
193         try:
194             return keys.Key.fromFile(file)
195         except keys.BadKeyError:
196             return self.getPublicKey() # try again
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: # agent key
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)