/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

Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
"""Implementation of Transport over SFTP, using paramiko."""
19
19
 
20
20
import errno
21
 
import getpass
22
 
import itertools
23
21
import os
24
22
import random
25
 
import re
26
23
import select
27
24
import socket
28
25
import stat
33
30
import urlparse
34
31
import weakref
35
32
 
36
 
from bzrlib.config import config_dir, ensure_config_dir_exists
37
 
from bzrlib.errors import (ConnectionError,
38
 
                           FileExists, 
39
 
                           TransportNotPossible, NoSuchFile, PathNotChild,
 
33
from bzrlib.errors import (FileExists, 
 
34
                           NoSuchFile, PathNotChild,
40
35
                           TransportError,
41
36
                           LockError, 
42
37
                           PathError,
43
38
                           ParamikoNotPresent,
 
39
                           UnknownSSH,
44
40
                           )
45
41
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
46
 
from bzrlib.trace import mutter, warning, error
 
42
from bzrlib.trace import mutter, warning
47
43
from bzrlib.transport import (
48
44
    register_urlparse_netloc_protocol,
49
45
    Server,
50
46
    split_url,
 
47
    ssh,
51
48
    Transport,
52
49
    )
53
 
import bzrlib.ui
54
50
import bzrlib.urlutils as urlutils
55
51
 
56
52
try:
63
59
                               CMD_HANDLE, CMD_OPEN)
64
60
    from paramiko.sftp_attr import SFTPAttributes
65
61
    from paramiko.sftp_file import SFTPFile
66
 
    from paramiko.sftp_client import SFTPClient
67
62
 
68
63
 
69
64
register_urlparse_netloc_protocol('sftp')
70
65
 
71
66
 
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
67
# This is a weakref dictionary, so that we can reuse connections
229
68
# that are still active. Long term, it might be nice to have some
230
69
# sort of expiration policy, such as disconnect if inactive for
231
70
# X seconds. But that requires a lot more fanciness.
232
71
_connected_hosts = weakref.WeakValueDictionary()
233
72
 
 
73
 
 
74
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
 
75
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
 
76
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
 
77
 
 
78
 
234
79
def clear_connection_cache():
235
80
    """Remove all hosts from the SFTP connection cache.
236
81
 
239
84
    _connected_hosts.clear()
240
85
 
241
86
 
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
87
class SFTPLock(object):
280
88
    """This fakes a lock in a remote location."""
281
89
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
311
119
            pass
312
120
 
313
121
 
314
 
class SFTPTransport (Transport):
315
 
    """Transport implementation for SFTP access"""
 
122
class SFTPTransport(Transport):
 
123
    """Transport implementation for SFTP access."""
316
124
 
317
125
    _do_prefetch = _default_do_prefetch
318
126
    # TODO: jam 20060717 Conceivably these could be configurable, either
558
366
            # on to the next
559
367
            cur_coalesced = cur_coalesced_stack.next()
560
368
 
561
 
    def put(self, relpath, f, mode=None):
 
369
    def put_file(self, relpath, f, mode=None):
562
370
        """
563
 
        Copy the file-like or string object into the location.
 
371
        Copy the file-like object into the location.
564
372
 
565
373
        :param relpath: Location to put the contents, relative to base.
566
 
        :param f:       File-like or string object.
 
374
        :param f:       File-like object.
567
375
        :param mode: The final mode for the file
568
376
        """
569
377
        final_path = self._remote_path(relpath)
581
389
                self._pump(f, fout)
582
390
            except (IOError, paramiko.SSHException), e:
583
391
                self._translate_io_exception(e, tmp_abspath)
 
392
            # XXX: This doesn't truly help like we would like it to.
 
393
            #      The problem is that openssh strips sticky bits. So while we
 
394
            #      can properly set group write permission, we lose the group
 
395
            #      sticky bit. So it is probably best to stop chmodding, and
 
396
            #      just tell users that they need to set the umask correctly.
 
397
            #      The attr.st_mode = mode, in _sftp_open_exclusive
 
398
            #      will handle when the user wants the final mode to be more 
 
399
            #      restrictive. And then we avoid a round trip. Unless 
 
400
            #      paramiko decides to expose an async chmod()
 
401
 
 
402
            # This is designed to chmod() right before we close.
 
403
            # Because we set_pipelined() earlier, theoretically we might 
 
404
            # avoid the round trip for fout.close()
584
405
            if mode is not None:
585
406
                self._sftp.chmod(tmp_abspath, mode)
586
407
            fout.close()
604
425
            # raise the original with its traceback if we can.
605
426
            raise
606
427
 
 
428
    def _put_non_atomic_helper(self, relpath, writer, mode=None,
 
429
                               create_parent_dir=False,
 
430
                               dir_mode=None):
 
431
        abspath = self._remote_path(relpath)
 
432
 
 
433
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
 
434
        #       set the file mode at create time. If it does, use it.
 
435
        #       But for now, we just chmod later anyway.
 
436
 
 
437
        def _open_and_write_file():
 
438
            """Try to open the target file, raise error on failure"""
 
439
            fout = None
 
440
            try:
 
441
                try:
 
442
                    fout = self._sftp.file(abspath, mode='wb')
 
443
                    fout.set_pipelined(True)
 
444
                    writer(fout)
 
445
                except (paramiko.SSHException, IOError), e:
 
446
                    self._translate_io_exception(e, abspath,
 
447
                                                 ': unable to open')
 
448
 
 
449
                # This is designed to chmod() right before we close.
 
450
                # Because we set_pipelined() earlier, theoretically we might 
 
451
                # avoid the round trip for fout.close()
 
452
                if mode is not None:
 
453
                    self._sftp.chmod(abspath, mode)
 
454
            finally:
 
455
                if fout is not None:
 
456
                    fout.close()
 
457
 
 
458
        if not create_parent_dir:
 
459
            _open_and_write_file()
 
460
            return
 
461
 
 
462
        # Try error handling to create the parent directory if we need to
 
463
        try:
 
464
            _open_and_write_file()
 
465
        except NoSuchFile:
 
466
            # Try to create the parent directory, and then go back to
 
467
            # writing the file
 
468
            parent_dir = os.path.dirname(abspath)
 
469
            self._mkdir(parent_dir, dir_mode)
 
470
            _open_and_write_file()
 
471
 
 
472
    def put_file_non_atomic(self, relpath, f, mode=None,
 
473
                            create_parent_dir=False,
 
474
                            dir_mode=None):
 
475
        """Copy the file-like object into the target location.
 
476
 
 
477
        This function is not strictly safe to use. It is only meant to
 
478
        be used when you already know that the target does not exist.
 
479
        It is not safe, because it will open and truncate the remote
 
480
        file. So there may be a time when the file has invalid contents.
 
481
 
 
482
        :param relpath: The remote location to put the contents.
 
483
        :param f:       File-like object.
 
484
        :param mode:    Possible access permissions for new file.
 
485
                        None means do not set remote permissions.
 
486
        :param create_parent_dir: If we cannot create the target file because
 
487
                        the parent directory does not exist, go ahead and
 
488
                        create it, and then try again.
 
489
        """
 
490
        def writer(fout):
 
491
            self._pump(f, fout)
 
492
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
493
                                    create_parent_dir=create_parent_dir,
 
494
                                    dir_mode=dir_mode)
 
495
 
 
496
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
 
497
                             create_parent_dir=False,
 
498
                             dir_mode=None):
 
499
        def writer(fout):
 
500
            fout.write(bytes)
 
501
        self._put_non_atomic_helper(relpath, writer, mode=mode,
 
502
                                    create_parent_dir=create_parent_dir,
 
503
                                    dir_mode=dir_mode)
 
504
 
607
505
    def iter_files_recursive(self):
608
506
        """Walk the relative paths of all files in this transport."""
609
507
        queue = list(self.list_dir('.'))
610
508
        while queue:
611
 
            relpath = urllib.quote(queue.pop(0))
 
509
            relpath = queue.pop(0)
612
510
            st = self.stat(relpath)
613
511
            if stat.S_ISDIR(st.st_mode):
614
512
                for i, basename in enumerate(self.list_dir(relpath)):
616
514
            else:
617
515
                yield relpath
618
516
 
 
517
    def _mkdir(self, abspath, mode=None):
 
518
        if mode is None:
 
519
            local_mode = 0777
 
520
        else:
 
521
            local_mode = mode
 
522
        try:
 
523
            self._sftp.mkdir(abspath, local_mode)
 
524
            if mode is not None:
 
525
                self._sftp.chmod(abspath, mode=mode)
 
526
        except (paramiko.SSHException, IOError), e:
 
527
            self._translate_io_exception(e, abspath, ': unable to mkdir',
 
528
                failure_exc=FileExists)
 
529
 
619
530
    def mkdir(self, relpath, mode=None):
620
531
        """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)
 
532
        self._mkdir(self._remote_path(relpath), mode=mode)
634
533
 
635
534
    def _translate_io_exception(self, e, path, more_info='', 
636
535
                                failure_exc=PathError):
662
561
            mutter('Raising exception with errno %s', e.errno)
663
562
        raise e
664
563
 
665
 
    def append(self, relpath, f, mode=None):
 
564
    def append_file(self, relpath, f, mode=None):
666
565
        """
667
566
        Append the text in the file-like object into the final
668
567
        location.
722
621
        Return a list of all files at the given location.
723
622
        """
724
623
        # does anything actually use this?
 
624
        # -- Unknown
 
625
        # This is at least used by copy_tree for remote upgrades.
 
626
        # -- David Allouche 2006-08-11
725
627
        path = self._remote_path(relpath)
726
628
        try:
727
 
            return self._sftp.listdir(path)
 
629
            entries = self._sftp.listdir(path)
728
630
        except (IOError, paramiko.SSHException), e:
729
631
            self._translate_io_exception(e, path, ': failed to list_dir')
 
632
        return [urlutils.escape(entry) for entry in entries]
730
633
 
731
634
    def rmdir(self, relpath):
732
635
        """See Transport.rmdir."""
812
715
 
813
716
        TODO: Raise a more reasonable ConnectionFailed exception
814
717
        """
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
 
718
        self._sftp = _sftp_connect(self._host, self._port, self._username,
 
719
                self._password)
965
720
 
966
721
    def _sftp_open_exclusive(self, abspath, mode=None):
967
722
        """Open a remote path exclusively.
977
732
        :param abspath: The remote absolute path where the file should be opened
978
733
        :param mode: The mode permissions bits for the new file
979
734
        """
 
735
        # TODO: jam 20060816 Paramiko >= 1.6.2 (probably earlier) supports
 
736
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
 
737
        #       However, there is no way to set the permission mode at open 
 
738
        #       time using the sftp_client.file() functionality.
980
739
        path = self._sftp._adjust_cwd(abspath)
981
740
        # mutter('sftp abspath %s => %s', abspath, path)
982
741
        attr = SFTPAttributes()
994
753
            self._translate_io_exception(e, abspath, ': unable to open',
995
754
                failure_exc=FileExists)
996
755
 
 
756
    def _can_roundtrip_unix_modebits(self):
 
757
        if sys.platform == 'win32':
 
758
            # anyone else?
 
759
            return False
 
760
        else:
 
761
            return True
997
762
 
998
763
# ------------- server test implementation --------------
999
 
import socket
1000
764
import threading
1001
765
 
1002
766
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
1065
829
                        x)
1066
830
 
1067
831
 
 
832
class SocketDelay(object):
 
833
    """A socket decorator to make TCP appear slower.
 
834
 
 
835
    This changes recv, send, and sendall to add a fixed latency to each python
 
836
    call if a new roundtrip is detected. That is, when a recv is called and the
 
837
    flag new_roundtrip is set, latency is charged. Every send and send_all
 
838
    sets this flag.
 
839
 
 
840
    In addition every send, sendall and recv sleeps a bit per character send to
 
841
    simulate bandwidth.
 
842
 
 
843
    Not all methods are implemented, this is deliberate as this class is not a
 
844
    replacement for the builtin sockets layer. fileno is not implemented to
 
845
    prevent the proxy being bypassed. 
 
846
    """
 
847
 
 
848
    simulated_time = 0
 
849
    _proxied_arguments = dict.fromkeys([
 
850
        "close", "getpeername", "getsockname", "getsockopt", "gettimeout",
 
851
        "setblocking", "setsockopt", "settimeout", "shutdown"])
 
852
 
 
853
    def __init__(self, sock, latency, bandwidth=1.0, 
 
854
                 really_sleep=True):
 
855
        """ 
 
856
        :param bandwith: simulated bandwith (MegaBit)
 
857
        :param really_sleep: If set to false, the SocketDelay will just
 
858
        increase a counter, instead of calling time.sleep. This is useful for
 
859
        unittesting the SocketDelay.
 
860
        """
 
861
        self.sock = sock
 
862
        self.latency = latency
 
863
        self.really_sleep = really_sleep
 
864
        self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024) 
 
865
        self.new_roundtrip = False
 
866
 
 
867
    def sleep(self, s):
 
868
        if self.really_sleep:
 
869
            time.sleep(s)
 
870
        else:
 
871
            SocketDelay.simulated_time += s
 
872
 
 
873
    def __getattr__(self, attr):
 
874
        if attr in SocketDelay._proxied_arguments:
 
875
            return getattr(self.sock, attr)
 
876
        raise AttributeError("'SocketDelay' object has no attribute %r" %
 
877
                             attr)
 
878
 
 
879
    def dup(self):
 
880
        return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
 
881
                           self._sleep)
 
882
 
 
883
    def recv(self, *args):
 
884
        data = self.sock.recv(*args)
 
885
        if data and self.new_roundtrip:
 
886
            self.new_roundtrip = False
 
887
            self.sleep(self.latency)
 
888
        self.sleep(len(data) * self.time_per_byte)
 
889
        return data
 
890
 
 
891
    def sendall(self, data, flags=0):
 
892
        if not self.new_roundtrip:
 
893
            self.new_roundtrip = True
 
894
            self.sleep(self.latency)
 
895
        self.sleep(len(data) * self.time_per_byte)
 
896
        return self.sock.sendall(data, flags)
 
897
 
 
898
    def send(self, data, flags=0):
 
899
        if not self.new_roundtrip:
 
900
            self.new_roundtrip = True
 
901
            self.sleep(self.latency)
 
902
        bytes_sent = self.sock.send(data, flags)
 
903
        self.sleep(bytes_sent * self.time_per_byte)
 
904
        return bytes_sent
 
905
 
 
906
 
1068
907
class SFTPServer(Server):
1069
908
    """Common code for SFTP server facilities."""
1070
909
 
1074
913
        self._server_homedir = None
1075
914
        self._listener = None
1076
915
        self._root = None
1077
 
        self._vendor = 'none'
 
916
        self._vendor = ssh.ParamikoVendor()
1078
917
        # sftp server logs
1079
918
        self.logs = []
 
919
        self.add_latency = 0
1080
920
 
1081
921
    def _get_sftp_url(self, path):
1082
922
        """Calculate an sftp url to this server for path."""
1086
926
        """StubServer uses this to log when a new server is created."""
1087
927
        self.logs.append(message)
1088
928
 
 
929
    def _run_server_entry(self, sock):
 
930
        """Entry point for all implementations of _run_server.
 
931
        
 
932
        If self.add_latency is > 0.000001 then sock is given a latency adding
 
933
        decorator.
 
934
        """
 
935
        if self.add_latency > 0.000001:
 
936
            sock = SocketDelay(sock, self.add_latency)
 
937
        return self._run_server(sock)
 
938
 
1089
939
    def _run_server(self, s):
1090
940
        ssh_server = paramiko.Transport(s)
1091
941
        key_file = pathjoin(self._homedir, 'test_rsa.key')
1103
953
        event.wait(5.0)
1104
954
    
1105
955
    def setUp(self):
1106
 
        global _ssh_vendor
1107
 
        self._original_vendor = _ssh_vendor
1108
 
        _ssh_vendor = self._vendor
 
956
        self._original_vendor = ssh._ssh_vendor
 
957
        ssh._ssh_vendor = self._vendor
1109
958
        if sys.platform == 'win32':
1110
959
            # Win32 needs to use the UNICODE api
1111
960
            self._homedir = getcwd()
1117
966
        self._root = '/'
1118
967
        if sys.platform == 'win32':
1119
968
            self._root = ''
1120
 
        self._listener = SocketListener(self._run_server)
 
969
        self._listener = SocketListener(self._run_server_entry)
1121
970
        self._listener.setDaemon(True)
1122
971
        self._listener.start()
1123
972
 
1124
973
    def tearDown(self):
1125
974
        """See bzrlib.transport.Server.tearDown."""
1126
 
        global _ssh_vendor
1127
975
        self._listener.stop()
1128
 
        _ssh_vendor = self._original_vendor
 
976
        ssh._ssh_vendor = self._original_vendor
1129
977
 
1130
978
    def get_bogus_url(self):
1131
979
        """See bzrlib.transport.Server.get_bogus_url."""
1150
998
 
1151
999
    def __init__(self):
1152
1000
        super(SFTPServerWithoutSSH, self).__init__()
1153
 
        self._vendor = 'loopback'
 
1001
        self._vendor = ssh.LoopbackVendor()
1154
1002
 
1155
1003
    def _run_server(self, sock):
 
1004
        # Re-import these as locals, so that they're still accessible during
 
1005
        # interpreter shutdown (when all module globals get set to None, leading
 
1006
        # to confusing errors like "'NoneType' object has no attribute 'error'".
 
1007
        import socket, errno
1156
1008
        class FakeChannel(object):
1157
1009
            def get_transport(self):
1158
1010
                return self
1208
1060
        super(SFTPSiblingAbsoluteServer, self).setUp()
1209
1061
 
1210
1062
 
 
1063
def _sftp_connect(host, port, username, password):
 
1064
    """Connect to the remote sftp server.
 
1065
 
 
1066
    :raises: a TransportError 'could not connect'.
 
1067
 
 
1068
    :returns: an paramiko.sftp_client.SFTPClient
 
1069
 
 
1070
    TODO: Raise a more reasonable ConnectionFailed exception
 
1071
    """
 
1072
    idx = (host, port, username)
 
1073
    try:
 
1074
        return _connected_hosts[idx]
 
1075
    except KeyError:
 
1076
        pass
 
1077
    
 
1078
    sftp = _sftp_connect_uncached(host, port, username, password)
 
1079
    _connected_hosts[idx] = sftp
 
1080
    return sftp
 
1081
 
 
1082
def _sftp_connect_uncached(host, port, username, password):
 
1083
    vendor = ssh._get_ssh_vendor()
 
1084
    sftp = vendor.connect_sftp(username, password, host, port)
 
1085
    return sftp
 
1086
 
 
1087
 
1211
1088
def get_test_permutations():
1212
1089
    """Return the permutations to be used in testing."""
1213
1090
    return [(SFTPTransport, SFTPAbsoluteServer),