root/trunk/twisted/conch/client/default.py

Revision 30752, 9.0 KB (checked in by exarkun, 15 months ago)

Rewrite the copyright headers to exclude date information.

Author: exarkun
Reviewer: glyph
Fixes: #4857

To avoid the need to perpetually update copyright dates in each file in Twisted,
remove the dates from most files and just leave them in the LICENSE file.

As a side effect, some files also have had a trailing newline added where it was
missing before.

Line 
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"""
6Various classes and functions for implementing user-interaction in the
7command-line conch client.
8
9You probably shouldn't use anything in this module directly, since it assumes
10you are sitting at an interactive terminal.  For example, to programmatically
11interact with a known_hosts database, use L{twisted.conch.client.knownhosts}.
12"""
13
14from twisted.python import log
15from twisted.python.filepath import FilePath
16
17from twisted.conch.error import ConchError
18from twisted.conch.ssh import common, keys, userauth
19from twisted.internet import defer, protocol, reactor
20
21from twisted.conch.client.knownhosts import KnownHostsFile, ConsoleUI
22
23from twisted.conch.client import agent
24
25import 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
30def 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
81def 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
117class 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)
Note: See TracBrowser for help on using the browser.