~bzr/ubuntu/lucid/bzr/beta-ppa

« back to all changes in this revision

Viewing changes to bzrlib/transport/ssh.py

  • Committer: Martin Pool
  • Date: 2010-07-02 07:29:40 UTC
  • mfrom: (129.1.7 packaging-karmic)
  • Revision ID: mbp@sourcefrog.net-20100702072940-hpzq5elg8wjve8rh
* PPA rebuild.
* PPA rebuild for Karmic.
* PPA rebuild for Jaunty.
* PPA rebuild for Hardy.
* From postinst, actually remove the example bash completion scripts.
  (LP: #249452)
* New upstream release.
* New upstream release.
* New upstream release.
* Revert change to Build-depends: Dapper does not have python-central.
  Should be python-support..
* Target ppa..
* Target ppa..
* Target ppa..
* Target ppa..
* New upstream release.
* Switch to dpkg-source 3.0 (quilt) format.
* Bump standards version to 3.8.4.
* Remove embedded copy of python-configobj. Closes: #555336
* Remove embedded copy of python-elementtree. Closes: #555343
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* Change section from 'Devel' to 'Vcs'..
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* debian/control: Fix obsolete-relation-form-in-source
  lintian warning. 
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Split out docs into bzr-doc package.
* New upstream release.
* Added John Francesco Ferlito to Uploaders.
* Fix install path to quick-reference guide
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to doc paths changing
* New upstream release.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to doc paths changing
* New upstream release.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to doc paths changing
* New upstream release.
* Fix FTBFS due to path changes, again, again.
* Fix FTBFS due to path changes, again.
* Fix FTBFS due to path changes.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Bump standards version to 3.8.3.
* Remove unused patch system.
* New upstream release.
* New upstream release.
* New upstream release.
* Fix copy and paste tab error in .install file
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
 + Fixes compatibility with Python 2.4. Closes: #537708
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream version.
* Bump standards version to 3.8.2.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Add python-pyrex to build-deps to ensure C extensions are always build.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Split documentation into bzr-doc package. ((LP: #385074)
* Multiple packaging changes to make us more linitan clean.
* New upstream release.
* Split documentation into bzr-doc package. ((LP: #385074)
* Multiple packaging changes to make us more linitan clean.
* New upstream release.
* Split documentation into bzr-doc package. ((LP: #385074)
* Multiple packaging changes to make us more linitan clean.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Fix API compatibility version. (Closes: #526233)
* New upstream release.
  + Fixes default format for upgrade command. (Closes: #464688)
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Add missing dependency on zlib development library. (Closes:
  #523595)
* Add zlib build-depends.
* Add zlib build-depends.
* Add zlib build-depends.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Move to section vcs.
* Bump standards version to 3.8.1.
* New upstream release.
* Remove temporary patch for missing .c files from distribution
* New upstream release.
* Remove temporary patch for missing .c files from distribution
* New upstream release.
* Remove temporary patch for missing .c files from distribution
* Add temporary patch for missing .c files from distribution
* Add temporary patch for missing .c files from distribution
* Add temporary patch for missing .c files from distribution
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Recommend ca-certificates. (Closes: #452024)
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Update watch file. bazaar now uses launchpad to host its sources.
* Remove patch for inventory root revision copy, applied upstream.
* New upstream release.
* New upstream release.
* New upstream release
* Force removal of files installed in error to /etc/bash_completion.d/
  (LP: #249452)
* New upstream release.
* New upstream release
* New upstream release.
* Bump standards version.
* Include patch for inventory root revision copy, required for bzr-svn.
* New upstream release.
* Remove unused lintian overrides.
* Correct the package version not to be native.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* New upstream release.
* Final 1.5 release.
* New upstream release.
* New upstream release.
* New upstream release.
* Add myself as a co-maintainer.
* Add a Dm-Upload-Allowed: yes header.
* New upstream bugfix release.
* New upstream release.
* Final 1.3 release.
* New upstream release.
* First release candidate of the upcoming 1.3 release.
* Rebuild to fix the problem caused by a build with a broken python-central.
* New upstream release.
* Rebuild for dapper PPA.
* Apply Lamont's patches to fix build-dependencies on dapper.
  (See: https://bugs.launchpad.net/bzr/+bug/189915)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2006-2010 Robey Pointer <robey@lag.net>
 
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
17
 
 
18
"""Foundation SSH support for SFTP and smart server."""
 
19
 
 
20
import errno
 
21
import getpass
 
22
import logging
 
23
import os
 
24
import socket
 
25
import subprocess
 
26
import sys
 
27
 
 
28
from bzrlib import (
 
29
    config,
 
30
    errors,
 
31
    osutils,
 
32
    trace,
 
33
    ui,
 
34
    )
 
35
 
 
36
try:
 
37
    import paramiko
 
38
except ImportError, e:
 
39
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
 
40
    # access
 
41
    paramiko = None
 
42
else:
 
43
    from paramiko.sftp_client import SFTPClient
 
44
 
 
45
 
 
46
SYSTEM_HOSTKEYS = {}
 
47
BZR_HOSTKEYS = {}
 
48
 
 
49
 
 
50
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
51
 
 
52
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
 
53
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
54
# so we get an AttributeError exception. So we will not try to
 
55
# connect to an agent if we are on win32 and using Paramiko older than 1.6
 
56
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
 
57
 
 
58
 
 
59
class SSHVendorManager(object):
 
60
    """Manager for manage SSH vendors."""
 
61
 
 
62
    # Note, although at first sign the class interface seems similar to
 
63
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
64
    # the Registry because the class just has "get()" interface instead of the
 
65
    # Registry's "get(key)".
 
66
 
 
67
    def __init__(self):
 
68
        self._ssh_vendors = {}
 
69
        self._cached_ssh_vendor = None
 
70
        self._default_ssh_vendor = None
 
71
 
 
72
    def register_default_vendor(self, vendor):
 
73
        """Register default SSH vendor."""
 
74
        self._default_ssh_vendor = vendor
 
75
 
 
76
    def register_vendor(self, name, vendor):
 
77
        """Register new SSH vendor by name."""
 
78
        self._ssh_vendors[name] = vendor
 
79
 
 
80
    def clear_cache(self):
 
81
        """Clear previously cached lookup result."""
 
82
        self._cached_ssh_vendor = None
 
83
 
 
84
    def _get_vendor_by_environment(self, environment=None):
 
85
        """Return the vendor or None based on BZR_SSH environment variable.
 
86
 
 
87
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
88
                            unknown vendor name
 
89
        """
 
90
        if environment is None:
 
91
            environment = os.environ
 
92
        if 'BZR_SSH' in environment:
 
93
            vendor_name = environment['BZR_SSH']
 
94
            try:
 
95
                vendor = self._ssh_vendors[vendor_name]
 
96
            except KeyError:
 
97
                vendor = self._get_vendor_from_path(vendor_name)
 
98
                if vendor is None:
 
99
                    raise errors.UnknownSSH(vendor_name)
 
100
                vendor.executable_path = vendor_name
 
101
            return vendor
 
102
        return None
 
103
 
 
104
    def _get_ssh_version_string(self, args):
 
105
        """Return SSH version string from the subprocess."""
 
106
        try:
 
107
            p = subprocess.Popen(args,
 
108
                                 stdout=subprocess.PIPE,
 
109
                                 stderr=subprocess.PIPE,
 
110
                                 **os_specific_subprocess_params())
 
111
            stdout, stderr = p.communicate()
 
112
        except OSError:
 
113
            stdout = stderr = ''
 
114
        return stdout + stderr
 
115
 
 
116
    def _get_vendor_by_version_string(self, version, progname):
 
117
        """Return the vendor or None based on output from the subprocess.
 
118
 
 
119
        :param version: The output of 'ssh -V' like command.
 
120
        :param args: Command line that was run.
 
121
        """
 
122
        vendor = None
 
123
        if 'OpenSSH' in version:
 
124
            trace.mutter('ssh implementation is OpenSSH')
 
125
            vendor = OpenSSHSubprocessVendor()
 
126
        elif 'SSH Secure Shell' in version:
 
127
            trace.mutter('ssh implementation is SSH Corp.')
 
128
            vendor = SSHCorpSubprocessVendor()
 
129
        # As plink user prompts are not handled currently, don't auto-detect
 
130
        # it by inspection below, but keep this vendor detection for if a path
 
131
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
 
132
        elif 'plink' in version and progname == 'plink':
 
133
            # Checking if "plink" was the executed argument as Windows
 
134
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
135
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
 
136
            trace.mutter("ssh implementation is Putty's plink.")
 
137
            vendor = PLinkSubprocessVendor()
 
138
        return vendor
 
139
 
 
140
    def _get_vendor_by_inspection(self):
 
141
        """Return the vendor or None by checking for known SSH implementations."""
 
142
        version = self._get_ssh_version_string(['ssh', '-V'])
 
143
        return self._get_vendor_by_version_string(version, "ssh")
 
144
 
 
145
    def _get_vendor_from_path(self, path):
 
146
        """Return the vendor or None using the program at the given path"""
 
147
        version = self._get_ssh_version_string([path, '-V'])
 
148
        return self._get_vendor_by_version_string(version, 
 
149
            os.path.splitext(os.path.basename(path))[0])
 
150
 
 
151
    def get_vendor(self, environment=None):
 
152
        """Find out what version of SSH is on the system.
 
153
 
 
154
        :raises SSHVendorNotFound: if no any SSH vendor is found
 
155
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
156
                            unknown vendor name
 
157
        """
 
158
        if self._cached_ssh_vendor is None:
 
159
            vendor = self._get_vendor_by_environment(environment)
 
160
            if vendor is None:
 
161
                vendor = self._get_vendor_by_inspection()
 
162
                if vendor is None:
 
163
                    trace.mutter('falling back to default implementation')
 
164
                    vendor = self._default_ssh_vendor
 
165
                    if vendor is None:
 
166
                        raise errors.SSHVendorNotFound()
 
167
            self._cached_ssh_vendor = vendor
 
168
        return self._cached_ssh_vendor
 
169
 
 
170
_ssh_vendor_manager = SSHVendorManager()
 
171
_get_ssh_vendor = _ssh_vendor_manager.get_vendor
 
172
register_default_ssh_vendor = _ssh_vendor_manager.register_default_vendor
 
173
register_ssh_vendor = _ssh_vendor_manager.register_vendor
 
174
 
 
175
 
 
176
def _ignore_sigint():
 
177
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
 
178
    # doesn't handle it itself.
 
179
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
 
180
    import signal
 
181
    signal.signal(signal.SIGINT, signal.SIG_IGN)
 
182
 
 
183
 
 
184
class SocketAsChannelAdapter(object):
 
185
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
 
186
 
 
187
    def __init__(self, sock):
 
188
        self.__socket = sock
 
189
 
 
190
    def get_name(self):
 
191
        return "bzr SocketAsChannelAdapter"
 
192
 
 
193
    def send(self, data):
 
194
        return self.__socket.send(data)
 
195
 
 
196
    def recv(self, n):
 
197
        try:
 
198
            return self.__socket.recv(n)
 
199
        except socket.error, e:
 
200
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
 
201
                             errno.EBADF):
 
202
                # Connection has closed.  Paramiko expects an empty string in
 
203
                # this case, not an exception.
 
204
                return ''
 
205
            raise
 
206
 
 
207
    def recv_ready(self):
 
208
        # TODO: jam 20051215 this function is necessary to support the
 
209
        # pipelined() function. In reality, it probably should use
 
210
        # poll() or select() to actually return if there is data
 
211
        # available, otherwise we probably don't get any benefit
 
212
        return True
 
213
 
 
214
    def close(self):
 
215
        self.__socket.close()
 
216
 
 
217
 
 
218
class SSHVendor(object):
 
219
    """Abstract base class for SSH vendor implementations."""
 
220
 
 
221
    def connect_sftp(self, username, password, host, port):
 
222
        """Make an SSH connection, and return an SFTPClient.
 
223
 
 
224
        :param username: an ascii string
 
225
        :param password: an ascii string
 
226
        :param host: a host name as an ascii string
 
227
        :param port: a port number
 
228
        :type port: int
 
229
 
 
230
        :raises: ConnectionError if it cannot connect.
 
231
 
 
232
        :rtype: paramiko.sftp_client.SFTPClient
 
233
        """
 
234
        raise NotImplementedError(self.connect_sftp)
 
235
 
 
236
    def connect_ssh(self, username, password, host, port, command):
 
237
        """Make an SSH connection.
 
238
 
 
239
        :returns: something with a `close` method, and a `get_filelike_channels`
 
240
            method that returns a pair of (read, write) filelike objects.
 
241
        """
 
242
        raise NotImplementedError(self.connect_ssh)
 
243
 
 
244
    def _raise_connection_error(self, host, port=None, orig_error=None,
 
245
                                msg='Unable to connect to SSH host'):
 
246
        """Raise a SocketConnectionError with properly formatted host.
 
247
 
 
248
        This just unifies all the locations that try to raise ConnectionError,
 
249
        so that they format things properly.
 
250
        """
 
251
        raise errors.SocketConnectionError(host=host, port=port, msg=msg,
 
252
                                           orig_error=orig_error)
 
253
 
 
254
 
 
255
class LoopbackVendor(SSHVendor):
 
256
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
 
257
 
 
258
    def connect_sftp(self, username, password, host, port):
 
259
        sock = socket.socket()
 
260
        try:
 
261
            sock.connect((host, port))
 
262
        except socket.error, e:
 
263
            self._raise_connection_error(host, port=port, orig_error=e)
 
264
        return SFTPClient(SocketAsChannelAdapter(sock))
 
265
 
 
266
register_ssh_vendor('loopback', LoopbackVendor())
 
267
 
 
268
 
 
269
class _ParamikoSSHConnection(object):
 
270
    def __init__(self, channel):
 
271
        self.channel = channel
 
272
 
 
273
    def get_filelike_channels(self):
 
274
        return self.channel.makefile('rb'), self.channel.makefile('wb')
 
275
 
 
276
    def close(self):
 
277
        return self.channel.close()
 
278
 
 
279
 
 
280
class ParamikoVendor(SSHVendor):
 
281
    """Vendor that uses paramiko."""
 
282
 
 
283
    def _connect(self, username, password, host, port):
 
284
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
285
 
 
286
        load_host_keys()
 
287
 
 
288
        try:
 
289
            t = paramiko.Transport((host, port or 22))
 
290
            t.set_log_channel('bzr.paramiko')
 
291
            t.start_client()
 
292
        except (paramiko.SSHException, socket.error), e:
 
293
            self._raise_connection_error(host, port=port, orig_error=e)
 
294
 
 
295
        server_key = t.get_remote_server_key()
 
296
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
297
        keytype = server_key.get_name()
 
298
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
 
299
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
 
300
            our_server_key_hex = paramiko.util.hexify(
 
301
                our_server_key.get_fingerprint())
 
302
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
 
303
            our_server_key = BZR_HOSTKEYS[host][keytype]
 
304
            our_server_key_hex = paramiko.util.hexify(
 
305
                our_server_key.get_fingerprint())
 
306
        else:
 
307
            trace.warning('Adding %s host key for %s: %s'
 
308
                          % (keytype, host, server_key_hex))
 
309
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
310
            if add is not None: # paramiko >= 1.X.X
 
311
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
312
            else:
 
313
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
314
            our_server_key = server_key
 
315
            our_server_key_hex = paramiko.util.hexify(
 
316
                our_server_key.get_fingerprint())
 
317
            save_host_keys()
 
318
        if server_key != our_server_key:
 
319
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
320
            filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
321
            raise errors.TransportError(
 
322
                'Host keys for %s do not match!  %s != %s' %
 
323
                (host, our_server_key_hex, server_key_hex),
 
324
                ['Try editing %s or %s' % (filename1, filename2)])
 
325
 
 
326
        _paramiko_auth(username, password, host, port, t)
 
327
        return t
 
328
 
 
329
    def connect_sftp(self, username, password, host, port):
 
330
        t = self._connect(username, password, host, port)
 
331
        try:
 
332
            return t.open_sftp_client()
 
333
        except paramiko.SSHException, e:
 
334
            self._raise_connection_error(host, port=port, orig_error=e,
 
335
                                         msg='Unable to start sftp client')
 
336
 
 
337
    def connect_ssh(self, username, password, host, port, command):
 
338
        t = self._connect(username, password, host, port)
 
339
        try:
 
340
            channel = t.open_session()
 
341
            cmdline = ' '.join(command)
 
342
            channel.exec_command(cmdline)
 
343
            return _ParamikoSSHConnection(channel)
 
344
        except paramiko.SSHException, e:
 
345
            self._raise_connection_error(host, port=port, orig_error=e,
 
346
                                         msg='Unable to invoke remote bzr')
 
347
 
 
348
if paramiko is not None:
 
349
    vendor = ParamikoVendor()
 
350
    register_ssh_vendor('paramiko', vendor)
 
351
    register_ssh_vendor('none', vendor)
 
352
    register_default_ssh_vendor(vendor)
 
353
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
354
    del vendor
 
355
else:
 
356
    _sftp_connection_errors = (EOFError,)
 
357
 
 
358
 
 
359
class SubprocessVendor(SSHVendor):
 
360
    """Abstract base class for vendors that use pipes to a subprocess."""
 
361
 
 
362
    def _connect(self, argv):
 
363
        proc = subprocess.Popen(argv,
 
364
                                stdin=subprocess.PIPE,
 
365
                                stdout=subprocess.PIPE,
 
366
                                **os_specific_subprocess_params())
 
367
        return SSHSubprocess(proc)
 
368
 
 
369
    def connect_sftp(self, username, password, host, port):
 
370
        try:
 
371
            argv = self._get_vendor_specific_argv(username, host, port,
 
372
                                                  subsystem='sftp')
 
373
            sock = self._connect(argv)
 
374
            return SFTPClient(SocketAsChannelAdapter(sock))
 
375
        except _sftp_connection_errors, e:
 
376
            self._raise_connection_error(host, port=port, orig_error=e)
 
377
        except (OSError, IOError), e:
 
378
            # If the machine is fast enough, ssh can actually exit
 
379
            # before we try and send it the sftp request, which
 
380
            # raises a Broken Pipe
 
381
            if e.errno not in (errno.EPIPE,):
 
382
                raise
 
383
            self._raise_connection_error(host, port=port, orig_error=e)
 
384
 
 
385
    def connect_ssh(self, username, password, host, port, command):
 
386
        try:
 
387
            argv = self._get_vendor_specific_argv(username, host, port,
 
388
                                                  command=command)
 
389
            return self._connect(argv)
 
390
        except (EOFError), e:
 
391
            self._raise_connection_error(host, port=port, orig_error=e)
 
392
        except (OSError, IOError), e:
 
393
            # If the machine is fast enough, ssh can actually exit
 
394
            # before we try and send it the sftp request, which
 
395
            # raises a Broken Pipe
 
396
            if e.errno not in (errno.EPIPE,):
 
397
                raise
 
398
            self._raise_connection_error(host, port=port, orig_error=e)
 
399
 
 
400
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
401
                                  command=None):
 
402
        """Returns the argument list to run the subprocess with.
 
403
 
 
404
        Exactly one of 'subsystem' and 'command' must be specified.
 
405
        """
 
406
        raise NotImplementedError(self._get_vendor_specific_argv)
 
407
 
 
408
 
 
409
class OpenSSHSubprocessVendor(SubprocessVendor):
 
410
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
 
411
 
 
412
    executable_path = 'ssh'
 
413
 
 
414
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
415
                                  command=None):
 
416
        args = [self.executable_path,
 
417
                '-oForwardX11=no', '-oForwardAgent=no',
 
418
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
419
                '-oNoHostAuthenticationForLocalhost=yes']
 
420
        if port is not None:
 
421
            args.extend(['-p', str(port)])
 
422
        if username is not None:
 
423
            args.extend(['-l', username])
 
424
        if subsystem is not None:
 
425
            args.extend(['-s', host, subsystem])
 
426
        else:
 
427
            args.extend([host] + command)
 
428
        return args
 
429
 
 
430
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
 
431
 
 
432
 
 
433
class SSHCorpSubprocessVendor(SubprocessVendor):
 
434
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
 
435
 
 
436
    executable_path = 'ssh'
 
437
 
 
438
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
439
                                  command=None):
 
440
        args = [self.executable_path, '-x']
 
441
        if port is not None:
 
442
            args.extend(['-p', str(port)])
 
443
        if username is not None:
 
444
            args.extend(['-l', username])
 
445
        if subsystem is not None:
 
446
            args.extend(['-s', subsystem, host])
 
447
        else:
 
448
            args.extend([host] + command)
 
449
        return args
 
450
 
 
451
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
 
452
 
 
453
 
 
454
class PLinkSubprocessVendor(SubprocessVendor):
 
455
    """SSH vendor that uses the 'plink' executable from Putty."""
 
456
 
 
457
    executable_path = 'plink'
 
458
 
 
459
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
460
                                  command=None):
 
461
        args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
 
462
        if port is not None:
 
463
            args.extend(['-P', str(port)])
 
464
        if username is not None:
 
465
            args.extend(['-l', username])
 
466
        if subsystem is not None:
 
467
            args.extend(['-s', host, subsystem])
 
468
        else:
 
469
            args.extend([host] + command)
 
470
        return args
 
471
 
 
472
register_ssh_vendor('plink', PLinkSubprocessVendor())
 
473
 
 
474
 
 
475
def _paramiko_auth(username, password, host, port, paramiko_transport):
 
476
    auth = config.AuthenticationConfig()
 
477
    # paramiko requires a username, but it might be none if nothing was
 
478
    # supplied.  If so, use the local username.
 
479
    if username is None:
 
480
        username = auth.get_user('ssh', host, port=port,
 
481
                                 default=getpass.getuser())
 
482
    if _use_ssh_agent:
 
483
        agent = paramiko.Agent()
 
484
        for key in agent.get_keys():
 
485
            trace.mutter('Trying SSH agent key %s'
 
486
                         % paramiko.util.hexify(key.get_fingerprint()))
 
487
            try:
 
488
                paramiko_transport.auth_publickey(username, key)
 
489
                return
 
490
            except paramiko.SSHException, e:
 
491
                pass
 
492
 
 
493
    # okay, try finding id_rsa or id_dss?  (posix only)
 
494
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
 
495
        return
 
496
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
 
497
        return
 
498
 
 
499
    # If we have gotten this far, we are about to try for passwords, do an
 
500
    # auth_none check to see if it is even supported.
 
501
    supported_auth_types = []
 
502
    try:
 
503
        # Note that with paramiko <1.7.5 this logs an INFO message:
 
504
        #    Authentication type (none) not permitted.
 
505
        # So we explicitly disable the logging level for this action
 
506
        old_level = paramiko_transport.logger.level
 
507
        paramiko_transport.logger.setLevel(logging.WARNING)
 
508
        try:
 
509
            paramiko_transport.auth_none(username)
 
510
        finally:
 
511
            paramiko_transport.logger.setLevel(old_level)
 
512
    except paramiko.BadAuthenticationType, e:
 
513
        # Supported methods are in the exception
 
514
        supported_auth_types = e.allowed_types
 
515
    except paramiko.SSHException, e:
 
516
        # Don't know what happened, but just ignore it
 
517
        pass
 
518
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
 
519
    # because Paramiko's auth_password method will automatically try
 
520
    # 'keyboard-interactive' auth (using the password as the response) if
 
521
    # 'password' auth is not available.  Apparently some Debian and Gentoo
 
522
    # OpenSSH servers require this.
 
523
    # XXX: It's possible for a server to require keyboard-interactive auth that
 
524
    # requires something other than a single password, but we currently don't
 
525
    # support that.
 
526
    if ('password' not in supported_auth_types and
 
527
        'keyboard-interactive' not in supported_auth_types):
 
528
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
529
            '\n  %s@%s\nsupported auth types: %s'
 
530
            % (username, host, supported_auth_types))
 
531
 
 
532
    if password:
 
533
        try:
 
534
            paramiko_transport.auth_password(username, password)
 
535
            return
 
536
        except paramiko.SSHException, e:
 
537
            pass
 
538
 
 
539
    # give up and ask for a password
 
540
    password = auth.get_password('ssh', host, username, port=port)
 
541
    # get_password can still return None, which means we should not prompt
 
542
    if password is not None:
 
543
        try:
 
544
            paramiko_transport.auth_password(username, password)
 
545
        except paramiko.SSHException, e:
 
546
            raise errors.ConnectionError(
 
547
                'Unable to authenticate to SSH host as'
 
548
                '\n  %s@%s\n' % (username, host), e)
 
549
    else:
 
550
        raise errors.ConnectionError('Unable to authenticate to SSH host as'
 
551
                                     '  %s@%s' % (username, host))
 
552
 
 
553
 
 
554
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
 
555
    filename = os.path.expanduser('~/.ssh/' + filename)
 
556
    try:
 
557
        key = pkey_class.from_private_key_file(filename)
 
558
        paramiko_transport.auth_publickey(username, key)
 
559
        return True
 
560
    except paramiko.PasswordRequiredException:
 
561
        password = ui.ui_factory.get_password(
 
562
            prompt='SSH %(filename)s password', filename=filename)
 
563
        try:
 
564
            key = pkey_class.from_private_key_file(filename, password)
 
565
            paramiko_transport.auth_publickey(username, key)
 
566
            return True
 
567
        except paramiko.SSHException:
 
568
            trace.mutter('SSH authentication via %s key failed.'
 
569
                         % (os.path.basename(filename),))
 
570
    except paramiko.SSHException:
 
571
        trace.mutter('SSH authentication via %s key failed.'
 
572
                     % (os.path.basename(filename),))
 
573
    except IOError:
 
574
        pass
 
575
    return False
 
576
 
 
577
 
 
578
def load_host_keys():
 
579
    """
 
580
    Load system host keys (probably doesn't work on windows) and any
 
581
    "discovered" keys from previous sessions.
 
582
    """
 
583
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
584
    try:
 
585
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
 
586
            os.path.expanduser('~/.ssh/known_hosts'))
 
587
    except IOError, e:
 
588
        trace.mutter('failed to load system host keys: ' + str(e))
 
589
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
590
    try:
 
591
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
 
592
    except IOError, e:
 
593
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
594
        save_host_keys()
 
595
 
 
596
 
 
597
def save_host_keys():
 
598
    """
 
599
    Save "discovered" host keys in $(config)/ssh_host_keys/.
 
600
    """
 
601
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
602
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
603
    config.ensure_config_dir_exists()
 
604
 
 
605
    try:
 
606
        f = open(bzr_hostkey_path, 'w')
 
607
        f.write('# SSH host keys collected by bzr\n')
 
608
        for hostname, keys in BZR_HOSTKEYS.iteritems():
 
609
            for keytype, key in keys.iteritems():
 
610
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
 
611
        f.close()
 
612
    except IOError, e:
 
613
        trace.mutter('failed to save bzr host keys: ' + str(e))
 
614
 
 
615
 
 
616
def os_specific_subprocess_params():
 
617
    """Get O/S specific subprocess parameters."""
 
618
    if sys.platform == 'win32':
 
619
        # setting the process group and closing fds is not supported on
 
620
        # win32
 
621
        return {}
 
622
    else:
 
623
        # We close fds other than the pipes as the child process does not need
 
624
        # them to be open.
 
625
        #
 
626
        # We also set the child process to ignore SIGINT.  Normally the signal
 
627
        # would be sent to every process in the foreground process group, but
 
628
        # this causes it to be seen only by bzr and not by ssh.  Python will
 
629
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
 
630
        # to release locks or do other cleanup over ssh before the connection
 
631
        # goes away.
 
632
        # <https://launchpad.net/products/bzr/+bug/5987>
 
633
        #
 
634
        # Running it in a separate process group is not good because then it
 
635
        # can't get non-echoed input of a password or passphrase.
 
636
        # <https://launchpad.net/products/bzr/+bug/40508>
 
637
        return {'preexec_fn': _ignore_sigint,
 
638
                'close_fds': True,
 
639
                }
 
640
 
 
641
import weakref
 
642
_subproc_weakrefs = set()
 
643
 
 
644
def _close_ssh_proc(proc):
 
645
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
646
        try:
 
647
            func()
 
648
        except OSError:
 
649
            pass
 
650
 
 
651
 
 
652
class SSHSubprocess(object):
 
653
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
654
 
 
655
    def __init__(self, proc):
 
656
        self.proc = proc
 
657
        # Add a weakref to proc that will attempt to do the same as self.close
 
658
        # to avoid leaving processes lingering indefinitely.
 
659
        def terminate(ref):
 
660
            _subproc_weakrefs.remove(ref)
 
661
            _close_ssh_proc(proc)
 
662
        _subproc_weakrefs.add(weakref.ref(self, terminate))
 
663
 
 
664
    def send(self, data):
 
665
        return os.write(self.proc.stdin.fileno(), data)
 
666
 
 
667
    def recv(self, count):
 
668
        return os.read(self.proc.stdout.fileno(), count)
 
669
 
 
670
    def close(self):
 
671
        _close_ssh_proc(self.proc)
 
672
 
 
673
    def get_filelike_channels(self):
 
674
        return (self.proc.stdout, self.proc.stdin)
 
675