/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: John Arbash Meinel
  • Date: 2006-09-15 00:44:57 UTC
  • mfrom: (2009 +trunk)
  • mto: This revision was merged to the branch mainline in revision 2050.
  • Revision ID: john@arbash-meinel.com-20060915004457-902cec0526a39337
[merge] bzr.dev 2009

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
 
18
18
"""Implementation of Transport over SFTP, using paramiko."""
19
19
 
 
20
# TODO: Remove the transport-based lock_read and lock_write methods.  They'll
 
21
# then raise TransportNotPossible, which will break remote access to any
 
22
# formats which rely on OS-level locks.  That should be fine as those formats
 
23
# are pretty old, but these combinations may have to be removed from the test
 
24
# suite.  Those formats all date back to 0.7; so we should be able to remove
 
25
# these methods when we officially drop support for those formats.
 
26
 
20
27
import errno
21
 
import getpass
22
 
import itertools
23
28
import os
24
29
import random
25
 
import re
26
30
import select
27
31
import socket
28
32
import stat
33
37
import urlparse
34
38
import weakref
35
39
 
36
 
from bzrlib.config import config_dir, ensure_config_dir_exists
37
 
from bzrlib.errors import (ConnectionError,
38
 
                           FileExists, 
39
 
                           TransportNotPossible, NoSuchFile, PathNotChild,
 
40
from bzrlib.errors import (FileExists, 
 
41
                           NoSuchFile, PathNotChild,
40
42
                           TransportError,
41
43
                           LockError, 
42
44
                           PathError,
43
45
                           ParamikoNotPresent,
 
46
                           UnknownSSH,
44
47
                           )
45
48
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
46
 
from bzrlib.trace import mutter, warning, error
 
49
from bzrlib.trace import mutter, warning
47
50
from bzrlib.transport import (
48
51
    register_urlparse_netloc_protocol,
49
52
    Server,
50
53
    split_url,
 
54
    ssh,
51
55
    Transport,
52
56
    )
53
 
import bzrlib.ui
54
57
import bzrlib.urlutils as urlutils
55
58
 
56
59
try:
63
66
                               CMD_HANDLE, CMD_OPEN)
64
67
    from paramiko.sftp_attr import SFTPAttributes
65
68
    from paramiko.sftp_file import SFTPFile
66
 
    from paramiko.sftp_client import SFTPClient
67
69
 
68
70
 
69
71
register_urlparse_netloc_protocol('sftp')
70
72
 
71
73
 
72
 
def _ignore_sigint():
73
 
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
74
 
    # doesn't handle it itself.
75
 
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
76
 
    import signal
77
 
    signal.signal(signal.SIGINT, signal.SIG_IGN)
78
 
    
79
 
 
80
 
def os_specific_subprocess_params():
81
 
    """Get O/S specific subprocess parameters."""
82
 
    if sys.platform == 'win32':
83
 
        # setting the process group and closing fds is not supported on 
84
 
        # win32
85
 
        return {}
86
 
    else:
87
 
        # We close fds other than the pipes as the child process does not need 
88
 
        # them to be open.
89
 
        #
90
 
        # We also set the child process to ignore SIGINT.  Normally the signal
91
 
        # would be sent to every process in the foreground process group, but
92
 
        # this causes it to be seen only by bzr and not by ssh.  Python will
93
 
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
94
 
        # to release locks or do other cleanup over ssh before the connection
95
 
        # goes away.  
96
 
        # <https://launchpad.net/products/bzr/+bug/5987>
97
 
        #
98
 
        # Running it in a separate process group is not good because then it
99
 
        # can't get non-echoed input of a password or passphrase.
100
 
        # <https://launchpad.net/products/bzr/+bug/40508>
101
 
        return {'preexec_fn': _ignore_sigint,
102
 
                'close_fds': True,
103
 
                }
104
 
 
105
 
 
106
 
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
107
 
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
108
 
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
109
 
 
110
 
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
111
 
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
112
 
# so we get an AttributeError exception. So we will not try to
113
 
# connect to an agent if we are on win32 and using Paramiko older than 1.6
114
 
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
115
 
 
116
 
 
117
 
_ssh_vendor = None
118
 
def _get_ssh_vendor():
119
 
    """Find out what version of SSH is on the system."""
120
 
    global _ssh_vendor
121
 
    if _ssh_vendor is not None:
122
 
        return _ssh_vendor
123
 
 
124
 
    _ssh_vendor = 'none'
125
 
 
126
 
    if 'BZR_SSH' in os.environ:
127
 
        _ssh_vendor = os.environ['BZR_SSH']
128
 
        if _ssh_vendor == 'paramiko':
129
 
            _ssh_vendor = 'none'
130
 
        return _ssh_vendor
131
 
 
132
 
    try:
133
 
        p = subprocess.Popen(['ssh', '-V'],
134
 
                             stdin=subprocess.PIPE,
135
 
                             stdout=subprocess.PIPE,
136
 
                             stderr=subprocess.PIPE,
137
 
                             **os_specific_subprocess_params())
138
 
        returncode = p.returncode
139
 
        stdout, stderr = p.communicate()
140
 
    except OSError:
141
 
        returncode = -1
142
 
        stdout = stderr = ''
143
 
    if 'OpenSSH' in stderr:
144
 
        mutter('ssh implementation is OpenSSH')
145
 
        _ssh_vendor = 'openssh'
146
 
    elif 'SSH Secure Shell' in stderr:
147
 
        mutter('ssh implementation is SSH Corp.')
148
 
        _ssh_vendor = 'ssh'
149
 
 
150
 
    if _ssh_vendor != 'none':
151
 
        return _ssh_vendor
152
 
 
153
 
    # XXX: 20051123 jamesh
154
 
    # A check for putty's plink or lsh would go here.
155
 
 
156
 
    mutter('falling back to paramiko implementation')
157
 
    return _ssh_vendor
158
 
 
159
 
 
160
 
class SFTPSubprocess:
161
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
162
 
    def __init__(self, hostname, vendor, port=None, user=None):
163
 
        assert vendor in ['openssh', 'ssh']
164
 
        if vendor == 'openssh':
165
 
            args = ['ssh',
166
 
                    '-oForwardX11=no', '-oForwardAgent=no',
167
 
                    '-oClearAllForwardings=yes', '-oProtocol=2',
168
 
                    '-oNoHostAuthenticationForLocalhost=yes']
169
 
            if port is not None:
170
 
                args.extend(['-p', str(port)])
171
 
            if user is not None:
172
 
                args.extend(['-l', user])
173
 
            args.extend(['-s', hostname, 'sftp'])
174
 
        elif vendor == 'ssh':
175
 
            args = ['ssh', '-x']
176
 
            if port is not None:
177
 
                args.extend(['-p', str(port)])
178
 
            if user is not None:
179
 
                args.extend(['-l', user])
180
 
            args.extend(['-s', 'sftp', hostname])
181
 
 
182
 
        self.proc = subprocess.Popen(args,
183
 
                                     stdin=subprocess.PIPE,
184
 
                                     stdout=subprocess.PIPE,
185
 
                                     **os_specific_subprocess_params())
186
 
 
187
 
    def send(self, data):
188
 
        return os.write(self.proc.stdin.fileno(), data)
189
 
 
190
 
    def recv_ready(self):
191
 
        # TODO: jam 20051215 this function is necessary to support the
192
 
        # pipelined() function. In reality, it probably should use
193
 
        # poll() or select() to actually return if there is data
194
 
        # available, otherwise we probably don't get any benefit
195
 
        return True
196
 
 
197
 
    def recv(self, count):
198
 
        return os.read(self.proc.stdout.fileno(), count)
199
 
 
200
 
    def close(self):
201
 
        self.proc.stdin.close()
202
 
        self.proc.stdout.close()
203
 
        self.proc.wait()
204
 
 
205
 
 
206
 
class LoopbackSFTP(object):
207
 
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
208
 
 
209
 
    def __init__(self, sock):
210
 
        self.__socket = sock
211
 
 
212
 
    def send(self, data):
213
 
        return self.__socket.send(data)
214
 
 
215
 
    def recv(self, n):
216
 
        return self.__socket.recv(n)
217
 
 
218
 
    def recv_ready(self):
219
 
        return True
220
 
 
221
 
    def close(self):
222
 
        self.__socket.close()
223
 
 
224
 
 
225
 
SYSTEM_HOSTKEYS = {}
226
 
BZR_HOSTKEYS = {}
227
 
 
228
74
# This is a weakref dictionary, so that we can reuse connections
229
75
# that are still active. Long term, it might be nice to have some
230
76
# sort of expiration policy, such as disconnect if inactive for
231
77
# X seconds. But that requires a lot more fanciness.
232
78
_connected_hosts = weakref.WeakValueDictionary()
233
79
 
 
80
 
 
81
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
82
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
 
83
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
 
84
 
 
85
 
234
86
def clear_connection_cache():
235
87
    """Remove all hosts from the SFTP connection cache.
236
88
 
239
91
    _connected_hosts.clear()
240
92
 
241
93
 
242
 
def load_host_keys():
243
 
    """
244
 
    Load system host keys (probably doesn't work on windows) and any
245
 
    "discovered" keys from previous sessions.
246
 
    """
247
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
248
 
    try:
249
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
250
 
    except Exception, e:
251
 
        mutter('failed to load system host keys: ' + str(e))
252
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
253
 
    try:
254
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
255
 
    except Exception, e:
256
 
        mutter('failed to load bzr host keys: ' + str(e))
257
 
        save_host_keys()
258
 
 
259
 
 
260
 
def save_host_keys():
261
 
    """
262
 
    Save "discovered" host keys in $(config)/ssh_host_keys/.
263
 
    """
264
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
265
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
266
 
    ensure_config_dir_exists()
267
 
 
268
 
    try:
269
 
        f = open(bzr_hostkey_path, 'w')
270
 
        f.write('# SSH host keys collected by bzr\n')
271
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
272
 
            for keytype, key in keys.iteritems():
273
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
274
 
        f.close()
275
 
    except IOError, e:
276
 
        mutter('failed to save bzr host keys: ' + str(e))
277
 
 
278
 
 
279
94
class SFTPLock(object):
280
 
    """This fakes a lock in a remote location."""
 
95
    """This fakes a lock in a remote location.
 
96
    
 
97
    A present lock is indicated just by the existence of a file.  This
 
98
    doesn't work well on all transports and they are only used in 
 
99
    deprecated storage formats.
 
100
    """
 
101
    
281
102
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
 
103
 
282
104
    def __init__(self, path, transport):
283
105
        assert isinstance(transport, SFTPTransport)
284
106
 
558
380
            # on to the next
559
381
            cur_coalesced = cur_coalesced_stack.next()
560
382
 
561
 
    def put(self, relpath, f, mode=None):
 
383
    def put_file(self, relpath, f, mode=None):
562
384
        """
563
 
        Copy the file-like or string object into the location.
 
385
        Copy the file-like object into the location.
564
386
 
565
387
        :param relpath: Location to put the contents, relative to base.
566
 
        :param f:       File-like or string object.
 
388
        :param f:       File-like object.
567
389
        :param mode: The final mode for the file
568
390
        """
569
391
        final_path = self._remote_path(relpath)
581
403
                self._pump(f, fout)
582
404
            except (IOError, paramiko.SSHException), e:
583
405
                self._translate_io_exception(e, tmp_abspath)
 
406
            # XXX: This doesn't truly help like we would like it to.
 
407
            #      The problem is that openssh strips sticky bits. So while we
 
408
            #      can properly set group write permission, we lose the group
 
409
            #      sticky bit. So it is probably best to stop chmodding, and
 
410
            #      just tell users that they need to set the umask correctly.
 
411
            #      The attr.st_mode = mode, in _sftp_open_exclusive
 
412
            #      will handle when the user wants the final mode to be more 
 
413
            #      restrictive. And then we avoid a round trip. Unless 
 
414
            #      paramiko decides to expose an async chmod()
 
415
 
 
416
            # This is designed to chmod() right before we close.
 
417
            # Because we set_pipelined() earlier, theoretically we might 
 
418
            # avoid the round trip for fout.close()
584
419
            if mode is not None:
585
420
                self._sftp.chmod(tmp_abspath, mode)
586
421
            fout.close()
604
439
            # raise the original with its traceback if we can.
605
440
            raise
606
441
 
 
442
    def _put_non_atomic_helper(self, relpath, writer, mode=None,
 
443
                               create_parent_dir=False,
 
444
                               dir_mode=None):
 
445
        abspath = self._remote_path(relpath)
 
446
 
 
447
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
 
448
        #       set the file mode at create time. If it does, use it.
 
449
        #       But for now, we just chmod later anyway.
 
450
 
 
451
        def _open_and_write_file():
 
452
            """Try to open the target file, raise error on failure"""
 
453
            fout = None
 
454
            try:
 
455
                try:
 
456
                    fout = self._sftp.file(abspath, mode='wb')
 
457
                    fout.set_pipelined(True)
 
458
                    writer(fout)
 
459
                except (paramiko.SSHException, IOError), e:
 
460
                    self._translate_io_exception(e, abspath,
 
461
                                                 ': unable to open')
 
462
 
 
463
                # This is designed to chmod() right before we close.
 
464
                # Because we set_pipelined() earlier, theoretically we might 
 
465
                # avoid the round trip for fout.close()
 
466
                if mode is not None:
 
467
                    self._sftp.chmod(abspath, mode)
 
468
            finally:
 
469
                if fout is not None:
 
470
                    fout.close()
 
471
 
 
472
        if not create_parent_dir:
 
473
            _open_and_write_file()
 
474
            return
 
475
 
 
476
        # Try error handling to create the parent directory if we need to
 
477
        try:
 
478
            _open_and_write_file()
 
479
        except NoSuchFile:
 
480
            # Try to create the parent directory, and then go back to
 
481
            # writing the file
 
482
            parent_dir = os.path.dirname(abspath)
 
483
            self._mkdir(parent_dir, dir_mode)
 
484
            _open_and_write_file()
 
485
 
 
486
    def put_file_non_atomic(self, relpath, f, mode=None,
 
487
                            create_parent_dir=False,
 
488
                            dir_mode=None):
 
489
        """Copy the file-like object into the target location.
 
490
 
 
491
        This function is not strictly safe to use. It is only meant to
 
492
        be used when you already know that the target does not exist.
 
493
        It is not safe, because it will open and truncate the remote
 
494
        file. So there may be a time when the file has invalid contents.
 
495
 
 
496
        :param relpath: The remote location to put the contents.
 
497
        :param f:       File-like object.
 
498
        :param mode:    Possible access permissions for new file.
 
499
                        None means do not set remote permissions.
 
500
        :param create_parent_dir: If we cannot create the target file because
 
501
                        the parent directory does not exist, go ahead and
 
502
                        create it, and then try again.
 
503
        """
 
504
        def writer(fout):
 
505
            self._pump(f, fout)
 
506
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
507
                                    create_parent_dir=create_parent_dir,
 
508
                                    dir_mode=dir_mode)
 
509
 
 
510
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
511
                             create_parent_dir=False,
 
512
                             dir_mode=None):
 
513
        def writer(fout):
 
514
            fout.write(bytes)
 
515
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
516
                                    create_parent_dir=create_parent_dir,
 
517
                                    dir_mode=dir_mode)
 
518
 
607
519
    def iter_files_recursive(self):
608
520
        """Walk the relative paths of all files in this transport."""
609
521
        queue = list(self.list_dir('.'))
610
522
        while queue:
611
 
            relpath = urllib.quote(queue.pop(0))
 
523
            relpath = queue.pop(0)
612
524
            st = self.stat(relpath)
613
525
            if stat.S_ISDIR(st.st_mode):
614
526
                for i, basename in enumerate(self.list_dir(relpath)):
616
528
            else:
617
529
                yield relpath
618
530
 
 
531
    def _mkdir(self, abspath, mode=None):
 
532
        if mode is None:
 
533
            local_mode = 0777
 
534
        else:
 
535
            local_mode = mode
 
536
        try:
 
537
            self._sftp.mkdir(abspath, local_mode)
 
538
            if mode is not None:
 
539
                self._sftp.chmod(abspath, mode=mode)
 
540
        except (paramiko.SSHException, IOError), e:
 
541
            self._translate_io_exception(e, abspath, ': unable to mkdir',
 
542
                failure_exc=FileExists)
 
543
 
619
544
    def mkdir(self, relpath, mode=None):
620
545
        """Create a directory at the given path."""
621
 
        path = self._remote_path(relpath)
622
 
        try:
623
 
            # In the paramiko documentation, it says that passing a mode flag 
624
 
            # will filtered against the server umask.
625
 
            # StubSFTPServer does not do this, which would be nice, because it is
626
 
            # what we really want :)
627
 
            # However, real servers do use umask, so we really should do it that way
628
 
            self._sftp.mkdir(path)
629
 
            if mode is not None:
630
 
                self._sftp.chmod(path, mode=mode)
631
 
        except (paramiko.SSHException, IOError), e:
632
 
            self._translate_io_exception(e, path, ': unable to mkdir',
633
 
                failure_exc=FileExists)
 
546
        self._mkdir(self._remote_path(relpath), mode=mode)
634
547
 
635
548
    def _translate_io_exception(self, e, path, more_info='', 
636
549
                                failure_exc=PathError):
648
561
        """
649
562
        # paramiko seems to generate detailless errors.
650
563
        self._translate_error(e, path, raise_generic=False)
651
 
        if hasattr(e, 'args'):
 
564
        if getattr(e, 'args', None) is not None:
652
565
            if (e.args == ('No such file or directory',) or
653
566
                e.args == ('No such file',)):
654
567
                raise NoSuchFile(path, str(e) + more_info)
658
571
            if (e.args == ('Failure',)):
659
572
                raise failure_exc(path, str(e) + more_info)
660
573
            mutter('Raising exception with args %s', e.args)
661
 
        if hasattr(e, 'errno'):
 
574
        if getattr(e, 'errno', None) is not None:
662
575
            mutter('Raising exception with errno %s', e.errno)
663
576
        raise e
664
577
 
665
 
    def append(self, relpath, f, mode=None):
 
578
    def append_file(self, relpath, f, mode=None):
666
579
        """
667
580
        Append the text in the file-like object into the final
668
581
        location.
722
635
        Return a list of all files at the given location.
723
636
        """
724
637
        # does anything actually use this?
 
638
        # -- Unknown
 
639
        # This is at least used by copy_tree for remote upgrades.
 
640
        # -- David Allouche 2006-08-11
725
641
        path = self._remote_path(relpath)
726
642
        try:
727
 
            return self._sftp.listdir(path)
 
643
            entries = self._sftp.listdir(path)
728
644
        except (IOError, paramiko.SSHException), e:
729
645
            self._translate_io_exception(e, path, ': failed to list_dir')
 
646
        return [urlutils.escape(entry) for entry in entries]
730
647
 
731
648
    def rmdir(self, relpath):
732
649
        """See Transport.rmdir."""
812
729
 
813
730
        TODO: Raise a more reasonable ConnectionFailed exception
814
731
        """
815
 
        global _connected_hosts
816
 
 
817
 
        idx = (self._host, self._port, self._username)
818
 
        try:
819
 
            self._sftp = _connected_hosts[idx]
820
 
            return
821
 
        except KeyError:
822
 
            pass
823
 
        
824
 
        vendor = _get_ssh_vendor()
825
 
        if vendor == 'loopback':
826
 
            sock = socket.socket()
827
 
            try:
828
 
                sock.connect((self._host, self._port))
829
 
            except socket.error, e:
830
 
                raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
831
 
                                      % (self._host, self._port, e))
832
 
            self._sftp = SFTPClient(LoopbackSFTP(sock))
833
 
        elif vendor != 'none':
834
 
            try:
835
 
                sock = SFTPSubprocess(self._host, vendor, self._port,
836
 
                                      self._username)
837
 
                self._sftp = SFTPClient(sock)
838
 
            except (EOFError, paramiko.SSHException), e:
839
 
                raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
840
 
                                      % (self._host, self._port, e))
841
 
            except (OSError, IOError), e:
842
 
                # If the machine is fast enough, ssh can actually exit
843
 
                # before we try and send it the sftp request, which
844
 
                # raises a Broken Pipe
845
 
                if e.errno not in (errno.EPIPE,):
846
 
                    raise
847
 
                raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
848
 
                                      % (self._host, self._port, e))
849
 
        else:
850
 
            self._paramiko_connect()
851
 
 
852
 
        _connected_hosts[idx] = self._sftp
853
 
 
854
 
    def _paramiko_connect(self):
855
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
856
 
        
857
 
        load_host_keys()
858
 
 
859
 
        try:
860
 
            t = paramiko.Transport((self._host, self._port or 22))
861
 
            t.set_log_channel('bzr.paramiko')
862
 
            t.start_client()
863
 
        except (paramiko.SSHException, socket.error), e:
864
 
            raise ConnectionError('Unable to reach SSH host %s:%s: %s' 
865
 
                                  % (self._host, self._port, e))
866
 
            
867
 
        server_key = t.get_remote_server_key()
868
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
869
 
        keytype = server_key.get_name()
870
 
        if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
871
 
            our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
872
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
873
 
        elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
874
 
            our_server_key = BZR_HOSTKEYS[self._host][keytype]
875
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
876
 
        else:
877
 
            warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
878
 
            if not BZR_HOSTKEYS.has_key(self._host):
879
 
                BZR_HOSTKEYS[self._host] = {}
880
 
            BZR_HOSTKEYS[self._host][keytype] = server_key
881
 
            our_server_key = server_key
882
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
883
 
            save_host_keys()
884
 
        if server_key != our_server_key:
885
 
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
886
 
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
887
 
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
888
 
                (self._host, our_server_key_hex, server_key_hex),
889
 
                ['Try editing %s or %s' % (filename1, filename2)])
890
 
 
891
 
        self._sftp_auth(t)
892
 
        
893
 
        try:
894
 
            self._sftp = t.open_sftp_client()
895
 
        except paramiko.SSHException, e:
896
 
            raise ConnectionError('Unable to start sftp client %s:%d' %
897
 
                                  (self._host, self._port), e)
898
 
 
899
 
    def _sftp_auth(self, transport):
900
 
        # paramiko requires a username, but it might be none if nothing was supplied
901
 
        # use the local username, just in case.
902
 
        # We don't override self._username, because if we aren't using paramiko,
903
 
        # the username might be specified in ~/.ssh/config and we don't want to
904
 
        # force it to something else
905
 
        # Also, it would mess up the self.relpath() functionality
906
 
        username = self._username or getpass.getuser()
907
 
 
908
 
        if _use_ssh_agent:
909
 
            agent = paramiko.Agent()
910
 
            for key in agent.get_keys():
911
 
                mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
912
 
                try:
913
 
                    transport.auth_publickey(username, key)
914
 
                    return
915
 
                except paramiko.SSHException, e:
916
 
                    pass
917
 
        
918
 
        # okay, try finding id_rsa or id_dss?  (posix only)
919
 
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
920
 
            return
921
 
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
922
 
            return
923
 
 
924
 
        if self._password:
925
 
            try:
926
 
                transport.auth_password(username, self._password)
927
 
                return
928
 
            except paramiko.SSHException, e:
929
 
                pass
930
 
 
931
 
            # FIXME: Don't keep a password held in memory if you can help it
932
 
            #self._password = None
933
 
 
934
 
        # give up and ask for a password
935
 
        password = bzrlib.ui.ui_factory.get_password(
936
 
                prompt='SSH %(user)s@%(host)s password',
937
 
                user=username, host=self._host)
938
 
        try:
939
 
            transport.auth_password(username, password)
940
 
        except paramiko.SSHException, e:
941
 
            raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
942
 
                                  (username, self._host), e)
943
 
 
944
 
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
945
 
        filename = os.path.expanduser('~/.ssh/' + filename)
946
 
        try:
947
 
            key = pkey_class.from_private_key_file(filename)
948
 
            transport.auth_publickey(username, key)
949
 
            return True
950
 
        except paramiko.PasswordRequiredException:
951
 
            password = bzrlib.ui.ui_factory.get_password(
952
 
                    prompt='SSH %(filename)s password',
953
 
                    filename=filename)
954
 
            try:
955
 
                key = pkey_class.from_private_key_file(filename, password)
956
 
                transport.auth_publickey(username, key)
957
 
                return True
958
 
            except paramiko.SSHException:
959
 
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
960
 
        except paramiko.SSHException:
961
 
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
962
 
        except IOError:
963
 
            pass
964
 
        return False
 
732
        self._sftp = _sftp_connect(self._host, self._port, self._username,
 
733
                self._password)
965
734
 
966
735
    def _sftp_open_exclusive(self, abspath, mode=None):
967
736
        """Open a remote path exclusively.
977
746
        :param abspath: The remote absolute path where the file should be opened
978
747
        :param mode: The mode permissions bits for the new file
979
748
        """
 
749
        # TODO: jam 20060816 Paramiko >= 1.6.2 (probably earlier) supports
 
750
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
 
751
        #       However, there is no way to set the permission mode at open 
 
752
        #       time using the sftp_client.file() functionality.
980
753
        path = self._sftp._adjust_cwd(abspath)
981
754
        # mutter('sftp abspath %s => %s', abspath, path)
982
755
        attr = SFTPAttributes()
994
767
            self._translate_io_exception(e, abspath, ': unable to open',
995
768
                failure_exc=FileExists)
996
769
 
 
770
    def _can_roundtrip_unix_modebits(self):
 
771
        if sys.platform == 'win32':
 
772
            # anyone else?
 
773
            return False
 
774
        else:
 
775
            return True
997
776
 
998
777
# ------------- server test implementation --------------
999
 
import socket
1000
778
import threading
1001
779
 
1002
780
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
1149
927
        self._server_homedir = None
1150
928
        self._listener = None
1151
929
        self._root = None
1152
 
        self._vendor = 'none'
 
930
        self._vendor = ssh.ParamikoVendor()
1153
931
        # sftp server logs
1154
932
        self.logs = []
1155
933
        self.add_latency = 0
1189
967
        event.wait(5.0)
1190
968
    
1191
969
    def setUp(self):
1192
 
        global _ssh_vendor
1193
 
        self._original_vendor = _ssh_vendor
1194
 
        _ssh_vendor = self._vendor
 
970
        self._original_vendor = ssh._ssh_vendor
 
971
        ssh._ssh_vendor = self._vendor
1195
972
        if sys.platform == 'win32':
1196
973
            # Win32 needs to use the UNICODE api
1197
974
            self._homedir = getcwd()
1209
986
 
1210
987
    def tearDown(self):
1211
988
        """See bzrlib.transport.Server.tearDown."""
1212
 
        global _ssh_vendor
1213
989
        self._listener.stop()
1214
 
        _ssh_vendor = self._original_vendor
 
990
        ssh._ssh_vendor = self._original_vendor
1215
991
 
1216
992
    def get_bogus_url(self):
1217
993
        """See bzrlib.transport.Server.get_bogus_url."""
1236
1012
 
1237
1013
    def __init__(self):
1238
1014
        super(SFTPServerWithoutSSH, self).__init__()
1239
 
        self._vendor = 'loopback'
 
1015
        self._vendor = ssh.LoopbackVendor()
1240
1016
 
1241
1017
    def _run_server(self, sock):
 
1018
        # Re-import these as locals, so that they're still accessible during
 
1019
        # interpreter shutdown (when all module globals get set to None, leading
 
1020
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
1021
        import socket, errno
1242
1022
        class FakeChannel(object):
1243
1023
            def get_transport(self):
1244
1024
                return self
1294
1074
        super(SFTPSiblingAbsoluteServer, self).setUp()
1295
1075
 
1296
1076
 
 
1077
def _sftp_connect(host, port, username, password):
 
1078
    """Connect to the remote sftp server.
 
1079
 
 
1080
    :raises: a TransportError 'could not connect'.
 
1081
 
 
1082
    :returns: an paramiko.sftp_client.SFTPClient
 
1083
 
 
1084
    TODO: Raise a more reasonable ConnectionFailed exception
 
1085
    """
 
1086
    idx = (host, port, username)
 
1087
    try:
 
1088
        return _connected_hosts[idx]
 
1089
    except KeyError:
 
1090
        pass
 
1091
    
 
1092
    sftp = _sftp_connect_uncached(host, port, username, password)
 
1093
    _connected_hosts[idx] = sftp
 
1094
    return sftp
 
1095
 
 
1096
def _sftp_connect_uncached(host, port, username, password):
 
1097
    vendor = ssh._get_ssh_vendor()
 
1098
    sftp = vendor.connect_sftp(username, password, host, port)
 
1099
    return sftp
 
1100
 
 
1101
 
1297
1102
def get_test_permutations():
1298
1103
    """Return the permutations to be used in testing."""
1299
1104
    return [(SFTPTransport, SFTPAbsoluteServer),