~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Alexander Belchenko
  • Date: 2006-07-30 16:43:12 UTC
  • mto: (1711.2.111 jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1906.
  • Revision ID: bialix@ukr.net-20060730164312-b025fd3ff0cee59e
rename  gpl.txt => COPYING.txt

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
 
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
 
2
# Copyright (C) 2005, 2006 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
5
5
# it under the terms of the GNU General Public License as published by
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
 
 
27
20
import errno
 
21
import getpass
 
22
import itertools
28
23
import os
29
24
import random
 
25
import re
30
26
import select
31
27
import socket
32
28
import stat
 
29
import subprocess
33
30
import sys
34
31
import time
35
32
import urllib
36
33
import urlparse
 
34
import weakref
37
35
 
38
 
from bzrlib import (
39
 
    errors,
40
 
    urlutils,
41
 
    )
42
 
from bzrlib.errors import (FileExists,
43
 
                           NoSuchFile, PathNotChild,
 
36
from bzrlib.config import config_dir, ensure_config_dir_exists
 
37
from bzrlib.errors import (ConnectionError,
 
38
                           FileExists, 
 
39
                           TransportNotPossible, NoSuchFile, PathNotChild,
44
40
                           TransportError,
45
 
                           LockError,
 
41
                           LockError, 
46
42
                           PathError,
47
43
                           ParamikoNotPresent,
48
44
                           )
49
45
from bzrlib.osutils import pathjoin, fancy_rename, getcwd
50
 
from bzrlib.symbol_versioning import (
51
 
        deprecated_function,
52
 
        zero_nineteen,
53
 
        )
54
 
from bzrlib.trace import mutter, warning
 
46
from bzrlib.trace import mutter, warning, error
55
47
from bzrlib.transport import (
56
 
    local,
57
48
    register_urlparse_netloc_protocol,
58
49
    Server,
59
 
    ssh,
60
 
    ConnectedTransport,
 
50
    split_url,
 
51
    Transport,
61
52
    )
 
53
import bzrlib.ui
 
54
import bzrlib.urlutils as urlutils
62
55
 
63
56
try:
64
57
    import paramiko
70
63
                               CMD_HANDLE, CMD_OPEN)
71
64
    from paramiko.sftp_attr import SFTPAttributes
72
65
    from paramiko.sftp_file import SFTPFile
 
66
    from paramiko.sftp_client import SFTPClient
73
67
 
74
68
 
75
69
register_urlparse_netloc_protocol('sftp')
76
70
 
77
71
 
 
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
 
78
106
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
79
107
# don't use prefetch unless paramiko version >= 1.5.5 (there were bugs earlier)
80
108
_default_do_prefetch = (_paramiko_version >= (1, 5, 5))
81
109
 
82
 
 
83
 
@deprecated_function(zero_nineteen)
 
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
# This is a weakref dictionary, so that we can reuse connections
 
229
# that are still active. Long term, it might be nice to have some
 
230
# sort of expiration policy, such as disconnect if inactive for
 
231
# X seconds. But that requires a lot more fanciness.
 
232
_connected_hosts = weakref.WeakValueDictionary()
 
233
 
84
234
def clear_connection_cache():
85
235
    """Remove all hosts from the SFTP connection cache.
86
236
 
87
237
    Primarily useful for test cases wanting to force garbage collection.
88
 
    We don't have a global connection cache anymore.
89
 
    """
 
238
    """
 
239
    _connected_hosts.clear()
 
240
 
 
241
 
 
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
 
90
278
 
91
279
class SFTPLock(object):
92
 
    """This fakes a lock in a remote location.
93
 
    
94
 
    A present lock is indicated just by the existence of a file.  This
95
 
    doesn't work well on all transports and they are only used in 
96
 
    deprecated storage formats.
97
 
    """
98
 
    
 
280
    """This fakes a lock in a remote location."""
99
281
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
100
 
 
101
282
    def __init__(self, path, transport):
102
283
        assert isinstance(transport, SFTPTransport)
103
284
 
130
311
            pass
131
312
 
132
313
 
133
 
class SFTPTransport(ConnectedTransport):
134
 
    """Transport implementation for SFTP access."""
 
314
class SFTPTransport (Transport):
 
315
    """Transport implementation for SFTP access"""
135
316
 
136
317
    _do_prefetch = _default_do_prefetch
137
318
    # TODO: jam 20060717 Conceivably these could be configurable, either
151
332
    # up the request itself, rather than us having to worry about it
152
333
    _max_request_size = 32768
153
334
 
154
 
    def __init__(self, base, _from_transport=None):
 
335
    def __init__(self, base, clone_from=None):
155
336
        assert base.startswith('sftp://')
156
 
        super(SFTPTransport, self).__init__(base,
157
 
                                            _from_transport=_from_transport)
158
 
 
 
337
        self._parse_url(base)
 
338
        base = self._unparse_url()
 
339
        if base[-1] != '/':
 
340
            base += '/'
 
341
        super(SFTPTransport, self).__init__(base)
 
342
        if clone_from is None:
 
343
            self._sftp_connect()
 
344
        else:
 
345
            # use the same ssh connection, etc
 
346
            self._sftp = clone_from._sftp
 
347
        # super saves 'self.base'
 
348
    
 
349
    def should_cache(self):
 
350
        """
 
351
        Return True if the data pulled across should be cached locally.
 
352
        """
 
353
        return True
 
354
 
 
355
    def clone(self, offset=None):
 
356
        """
 
357
        Return a new SFTPTransport with root at self.base + offset.
 
358
        We share the same SFTP session between such transports, because it's
 
359
        fairly expensive to set them up.
 
360
        """
 
361
        if offset is None:
 
362
            return SFTPTransport(self.base, self)
 
363
        else:
 
364
            return SFTPTransport(self.abspath(offset), self)
 
365
 
 
366
    def abspath(self, relpath):
 
367
        """
 
368
        Return the full url to the given relative path.
 
369
        
 
370
        @param relpath: the relative path or path components
 
371
        @type relpath: str or list
 
372
        """
 
373
        return self._unparse_url(self._remote_path(relpath))
 
374
    
159
375
    def _remote_path(self, relpath):
160
376
        """Return the path to be passed along the sftp protocol for relpath.
161
377
        
162
 
        :param relpath: is a urlencoded string.
163
 
        """
164
 
        relative = urlutils.unescape(relpath).encode('utf-8')
165
 
        remote_path = self._combine_paths(self._path, relative)
166
 
        # the initial slash should be removed from the path, and treated as a
167
 
        # homedir relative path (the path begins with a double slash if it is
168
 
        # absolute).  see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
169
 
        # RBC 20060118 we are not using this as its too user hostile. instead
170
 
        # we are following lftp and using /~/foo to mean '~/foo'
171
 
        # vila--20070602 and leave absolute paths begin with a single slash.
172
 
        if remote_path.startswith('/~/'):
173
 
            remote_path = remote_path[3:]
174
 
        elif remote_path == '/~':
175
 
            remote_path = ''
176
 
        return remote_path
177
 
 
178
 
    def _create_connection(self, credentials=None):
179
 
        """Create a new connection with the provided credentials.
180
 
 
181
 
        :param credentials: The credentials needed to establish the connection.
182
 
 
183
 
        :return: The created connection and its associated credentials.
184
 
 
185
 
        The credentials are only the password as it may have been entered
186
 
        interactively by the user and may be different from the one provided
187
 
        in base url at transport creation time.
188
 
        """
189
 
        if credentials is None:
190
 
            password = self._password
191
 
        else:
192
 
            password = credentials
193
 
 
194
 
        vendor = ssh._get_ssh_vendor()
195
 
        connection = vendor.connect_sftp(self._user, password,
196
 
                                         self._host, self._port)
197
 
        return connection, password
198
 
 
199
 
    def _get_sftp(self):
200
 
        """Ensures that a connection is established"""
201
 
        connection = self._get_connection()
202
 
        if connection is None:
203
 
            # First connection ever
204
 
            connection, credentials = self._create_connection()
205
 
            self._set_connection(connection, credentials)
206
 
        return connection
207
 
 
208
 
 
209
 
    def should_cache(self):
210
 
        """
211
 
        Return True if the data pulled across should be cached locally.
212
 
        """
213
 
        return True
 
378
        relpath is a urlencoded string.
 
379
        """
 
380
        # FIXME: share the common code across transports
 
381
        assert isinstance(relpath, basestring)
 
382
        relpath = urlutils.unescape(relpath).split('/')
 
383
        basepath = self._path.split('/')
 
384
        if len(basepath) > 0 and basepath[-1] == '':
 
385
            basepath = basepath[:-1]
 
386
 
 
387
        for p in relpath:
 
388
            if p == '..':
 
389
                if len(basepath) == 0:
 
390
                    # In most filesystems, a request for the parent
 
391
                    # of root, just returns root.
 
392
                    continue
 
393
                basepath.pop()
 
394
            elif p == '.':
 
395
                continue # No-op
 
396
            else:
 
397
                basepath.append(p)
 
398
 
 
399
        path = '/'.join(basepath)
 
400
        # mutter('relpath => remotepath %s => %s', relpath, path)
 
401
        return path
 
402
 
 
403
    def relpath(self, abspath):
 
404
        username, password, host, port, path = self._split_url(abspath)
 
405
        error = []
 
406
        if (username != self._username):
 
407
            error.append('username mismatch')
 
408
        if (host != self._host):
 
409
            error.append('host mismatch')
 
410
        if (port != self._port):
 
411
            error.append('port mismatch')
 
412
        if (not path.startswith(self._path)):
 
413
            error.append('path mismatch')
 
414
        if error:
 
415
            extra = ': ' + ', '.join(error)
 
416
            raise PathNotChild(abspath, self.base, extra=extra)
 
417
        pl = len(self._path)
 
418
        return path[pl:].strip('/')
214
419
 
215
420
    def has(self, relpath):
216
421
        """
217
422
        Does the target location exist?
218
423
        """
219
424
        try:
220
 
            self._get_sftp().stat(self._remote_path(relpath))
 
425
            self._sftp.stat(self._remote_path(relpath))
221
426
            return True
222
427
        except IOError:
223
428
            return False
230
435
        """
231
436
        try:
232
437
            path = self._remote_path(relpath)
233
 
            f = self._get_sftp().file(path, mode='rb')
 
438
            f = self._sftp.file(path, mode='rb')
234
439
            if self._do_prefetch and (getattr(f, 'prefetch', None) is not None):
235
440
                f.prefetch()
236
441
            return f
237
442
        except (IOError, paramiko.SSHException), e:
238
 
            self._translate_io_exception(e, path, ': error retrieving',
239
 
                failure_exc=errors.ReadError)
 
443
            self._translate_io_exception(e, path, ': error retrieving')
240
444
 
241
445
    def readv(self, relpath, offsets):
242
446
        """See Transport.readv()"""
248
452
 
249
453
        try:
250
454
            path = self._remote_path(relpath)
251
 
            fp = self._get_sftp().file(path, mode='rb')
 
455
            fp = self._sftp.file(path, mode='rb')
252
456
            readv = getattr(fp, 'readv', None)
253
457
            if readv:
254
 
                return self._sftp_readv(fp, offsets, relpath)
 
458
                return self._sftp_readv(fp, offsets)
255
459
            mutter('seek and read %s offsets', len(offsets))
256
 
            return self._seek_and_read(fp, offsets, relpath)
 
460
            return self._seek_and_read(fp, offsets)
257
461
        except (IOError, paramiko.SSHException), e:
258
462
            self._translate_io_exception(e, path, ': error retrieving')
259
463
 
260
 
    def _sftp_readv(self, fp, offsets, relpath='<unknown>'):
 
464
    def _sftp_readv(self, fp, offsets):
261
465
        """Use the readv() member of fp to do async readv.
262
466
 
263
467
        And then read them using paramiko.readv(). paramiko.readv()
350
554
                yield cur_offset_and_size[0], this_data
351
555
                cur_offset_and_size = offset_stack.next()
352
556
 
353
 
            # We read a coalesced entry, so mark it as done
354
 
            cur_coalesced = None
355
557
            # Now that we've read all of the data for this coalesced section
356
558
            # on to the next
357
559
            cur_coalesced = cur_coalesced_stack.next()
358
560
 
359
 
        if cur_coalesced is not None:
360
 
            raise errors.ShortReadvError(relpath, cur_coalesced.start,
361
 
                cur_coalesced.length, len(data))
362
 
 
363
 
    def put_file(self, relpath, f, mode=None):
 
561
    def put(self, relpath, f, mode=None):
364
562
        """
365
 
        Copy the file-like object into the location.
 
563
        Copy the file-like or string object into the location.
366
564
 
367
565
        :param relpath: Location to put the contents, relative to base.
368
 
        :param f:       File-like object.
 
566
        :param f:       File-like or string object.
369
567
        :param mode: The final mode for the file
370
568
        """
371
569
        final_path = self._remote_path(relpath)
383
581
                self._pump(f, fout)
384
582
            except (IOError, paramiko.SSHException), e:
385
583
                self._translate_io_exception(e, tmp_abspath)
386
 
            # XXX: This doesn't truly help like we would like it to.
387
 
            #      The problem is that openssh strips sticky bits. So while we
388
 
            #      can properly set group write permission, we lose the group
389
 
            #      sticky bit. So it is probably best to stop chmodding, and
390
 
            #      just tell users that they need to set the umask correctly.
391
 
            #      The attr.st_mode = mode, in _sftp_open_exclusive
392
 
            #      will handle when the user wants the final mode to be more 
393
 
            #      restrictive. And then we avoid a round trip. Unless 
394
 
            #      paramiko decides to expose an async chmod()
395
 
 
396
 
            # This is designed to chmod() right before we close.
397
 
            # Because we set_pipelined() earlier, theoretically we might 
398
 
            # avoid the round trip for fout.close()
399
584
            if mode is not None:
400
 
                self._get_sftp().chmod(tmp_abspath, mode)
 
585
                self._sftp.chmod(tmp_abspath, mode)
401
586
            fout.close()
402
587
            closed = True
403
588
            self._rename_and_overwrite(tmp_abspath, abspath)
412
597
            try:
413
598
                if not closed:
414
599
                    fout.close()
415
 
                self._get_sftp().remove(tmp_abspath)
 
600
                self._sftp.remove(tmp_abspath)
416
601
            except:
417
602
                # raise the saved except
418
603
                raise e
419
604
            # raise the original with its traceback if we can.
420
605
            raise
421
606
 
422
 
    def _put_non_atomic_helper(self, relpath, writer, mode=None,
423
 
                               create_parent_dir=False,
424
 
                               dir_mode=None):
425
 
        abspath = self._remote_path(relpath)
426
 
 
427
 
        # TODO: jam 20060816 paramiko doesn't publicly expose a way to
428
 
        #       set the file mode at create time. If it does, use it.
429
 
        #       But for now, we just chmod later anyway.
430
 
 
431
 
        def _open_and_write_file():
432
 
            """Try to open the target file, raise error on failure"""
433
 
            fout = None
434
 
            try:
435
 
                try:
436
 
                    fout = self._get_sftp().file(abspath, mode='wb')
437
 
                    fout.set_pipelined(True)
438
 
                    writer(fout)
439
 
                except (paramiko.SSHException, IOError), e:
440
 
                    self._translate_io_exception(e, abspath,
441
 
                                                 ': unable to open')
442
 
 
443
 
                # This is designed to chmod() right before we close.
444
 
                # Because we set_pipelined() earlier, theoretically we might 
445
 
                # avoid the round trip for fout.close()
446
 
                if mode is not None:
447
 
                    self._get_sftp().chmod(abspath, mode)
448
 
            finally:
449
 
                if fout is not None:
450
 
                    fout.close()
451
 
 
452
 
        if not create_parent_dir:
453
 
            _open_and_write_file()
454
 
            return
455
 
 
456
 
        # Try error handling to create the parent directory if we need to
457
 
        try:
458
 
            _open_and_write_file()
459
 
        except NoSuchFile:
460
 
            # Try to create the parent directory, and then go back to
461
 
            # writing the file
462
 
            parent_dir = os.path.dirname(abspath)
463
 
            self._mkdir(parent_dir, dir_mode)
464
 
            _open_and_write_file()
465
 
 
466
 
    def put_file_non_atomic(self, relpath, f, mode=None,
467
 
                            create_parent_dir=False,
468
 
                            dir_mode=None):
469
 
        """Copy the file-like object into the target location.
470
 
 
471
 
        This function is not strictly safe to use. It is only meant to
472
 
        be used when you already know that the target does not exist.
473
 
        It is not safe, because it will open and truncate the remote
474
 
        file. So there may be a time when the file has invalid contents.
475
 
 
476
 
        :param relpath: The remote location to put the contents.
477
 
        :param f:       File-like object.
478
 
        :param mode:    Possible access permissions for new file.
479
 
                        None means do not set remote permissions.
480
 
        :param create_parent_dir: If we cannot create the target file because
481
 
                        the parent directory does not exist, go ahead and
482
 
                        create it, and then try again.
483
 
        """
484
 
        def writer(fout):
485
 
            self._pump(f, fout)
486
 
        self._put_non_atomic_helper(relpath, writer, mode=mode,
487
 
                                    create_parent_dir=create_parent_dir,
488
 
                                    dir_mode=dir_mode)
489
 
 
490
 
    def put_bytes_non_atomic(self, relpath, bytes, mode=None,
491
 
                             create_parent_dir=False,
492
 
                             dir_mode=None):
493
 
        def writer(fout):
494
 
            fout.write(bytes)
495
 
        self._put_non_atomic_helper(relpath, writer, mode=mode,
496
 
                                    create_parent_dir=create_parent_dir,
497
 
                                    dir_mode=dir_mode)
498
 
 
499
607
    def iter_files_recursive(self):
500
608
        """Walk the relative paths of all files in this transport."""
501
609
        queue = list(self.list_dir('.'))
502
610
        while queue:
503
 
            relpath = queue.pop(0)
 
611
            relpath = urllib.quote(queue.pop(0))
504
612
            st = self.stat(relpath)
505
613
            if stat.S_ISDIR(st.st_mode):
506
614
                for i, basename in enumerate(self.list_dir(relpath)):
508
616
            else:
509
617
                yield relpath
510
618
 
511
 
    def _mkdir(self, abspath, mode=None):
512
 
        if mode is None:
513
 
            local_mode = 0777
514
 
        else:
515
 
            local_mode = mode
516
 
        try:
517
 
            self._get_sftp().mkdir(abspath, local_mode)
518
 
            if mode is not None:
519
 
                self._get_sftp().chmod(abspath, mode=mode)
520
 
        except (paramiko.SSHException, IOError), e:
521
 
            self._translate_io_exception(e, abspath, ': unable to mkdir',
522
 
                failure_exc=FileExists)
523
 
 
524
619
    def mkdir(self, relpath, mode=None):
525
620
        """Create a directory at the given path."""
526
 
        self._mkdir(self._remote_path(relpath), mode=mode)
 
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)
527
634
 
528
 
    def _translate_io_exception(self, e, path, more_info='',
 
635
    def _translate_io_exception(self, e, path, more_info='', 
529
636
                                failure_exc=PathError):
530
637
        """Translate a paramiko or IOError into a friendlier exception.
531
638
 
541
648
        """
542
649
        # paramiko seems to generate detailless errors.
543
650
        self._translate_error(e, path, raise_generic=False)
544
 
        if getattr(e, 'args', None) is not None:
 
651
        if hasattr(e, 'args'):
545
652
            if (e.args == ('No such file or directory',) or
546
653
                e.args == ('No such file',)):
547
654
                raise NoSuchFile(path, str(e) + more_info)
551
658
            if (e.args == ('Failure',)):
552
659
                raise failure_exc(path, str(e) + more_info)
553
660
            mutter('Raising exception with args %s', e.args)
554
 
        if getattr(e, 'errno', None) is not None:
 
661
        if hasattr(e, 'errno'):
555
662
            mutter('Raising exception with errno %s', e.errno)
556
663
        raise e
557
664
 
558
 
    def append_file(self, relpath, f, mode=None):
 
665
    def append(self, relpath, f, mode=None):
559
666
        """
560
667
        Append the text in the file-like object into the final
561
668
        location.
562
669
        """
563
670
        try:
564
671
            path = self._remote_path(relpath)
565
 
            fout = self._get_sftp().file(path, 'ab')
 
672
            fout = self._sftp.file(path, 'ab')
566
673
            if mode is not None:
567
 
                self._get_sftp().chmod(path, mode)
 
674
                self._sftp.chmod(path, mode)
568
675
            result = fout.tell()
569
676
            self._pump(f, fout)
570
677
            return result
574
681
    def rename(self, rel_from, rel_to):
575
682
        """Rename without special overwriting"""
576
683
        try:
577
 
            self._get_sftp().rename(self._remote_path(rel_from),
 
684
            self._sftp.rename(self._remote_path(rel_from),
578
685
                              self._remote_path(rel_to))
579
686
        except (IOError, paramiko.SSHException), e:
580
687
            self._translate_io_exception(e, rel_from,
586
693
        Using the implementation provided by osutils.
587
694
        """
588
695
        try:
589
 
            sftp = self._get_sftp()
590
696
            fancy_rename(abs_from, abs_to,
591
 
                         rename_func=sftp.rename,
592
 
                         unlink_func=sftp.remove)
 
697
                    rename_func=self._sftp.rename,
 
698
                    unlink_func=self._sftp.remove)
593
699
        except (IOError, paramiko.SSHException), e:
594
 
            self._translate_io_exception(e, abs_from,
595
 
                                         ': unable to rename to %r' % (abs_to))
 
700
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
596
701
 
597
702
    def move(self, rel_from, rel_to):
598
703
        """Move the item at rel_from to the location at rel_to"""
604
709
        """Delete the item at relpath"""
605
710
        path = self._remote_path(relpath)
606
711
        try:
607
 
            self._get_sftp().remove(path)
 
712
            self._sftp.remove(path)
608
713
        except (IOError, paramiko.SSHException), e:
609
714
            self._translate_io_exception(e, path, ': unable to delete')
610
715
            
611
 
    def external_url(self):
612
 
        """See bzrlib.transport.Transport.external_url."""
613
 
        # the external path for SFTP is the base
614
 
        return self.base
615
 
 
616
716
    def listable(self):
617
717
        """Return True if this store supports listing."""
618
718
        return True
622
722
        Return a list of all files at the given location.
623
723
        """
624
724
        # does anything actually use this?
625
 
        # -- Unknown
626
 
        # This is at least used by copy_tree for remote upgrades.
627
 
        # -- David Allouche 2006-08-11
628
725
        path = self._remote_path(relpath)
629
726
        try:
630
 
            entries = self._get_sftp().listdir(path)
 
727
            return self._sftp.listdir(path)
631
728
        except (IOError, paramiko.SSHException), e:
632
729
            self._translate_io_exception(e, path, ': failed to list_dir')
633
 
        return [urlutils.escape(entry) for entry in entries]
634
730
 
635
731
    def rmdir(self, relpath):
636
732
        """See Transport.rmdir."""
637
733
        path = self._remote_path(relpath)
638
734
        try:
639
 
            return self._get_sftp().rmdir(path)
 
735
            return self._sftp.rmdir(path)
640
736
        except (IOError, paramiko.SSHException), e:
641
737
            self._translate_io_exception(e, path, ': failed to rmdir')
642
738
 
644
740
        """Return the stat information for a file."""
645
741
        path = self._remote_path(relpath)
646
742
        try:
647
 
            return self._get_sftp().stat(path)
 
743
            return self._sftp.stat(path)
648
744
        except (IOError, paramiko.SSHException), e:
649
745
            self._translate_io_exception(e, path, ': unable to stat')
650
746
 
674
770
        # that we have taken the lock.
675
771
        return SFTPLock(relpath, self)
676
772
 
 
773
    def _unparse_url(self, path=None):
 
774
        if path is None:
 
775
            path = self._path
 
776
        path = urllib.quote(path)
 
777
        # handle homedir paths
 
778
        if not path.startswith('/'):
 
779
            path = "/~/" + path
 
780
        netloc = urllib.quote(self._host)
 
781
        if self._username is not None:
 
782
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
783
        if self._port is not None:
 
784
            netloc = '%s:%d' % (netloc, self._port)
 
785
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
 
786
 
 
787
    def _split_url(self, url):
 
788
        (scheme, username, password, host, port, path) = split_url(url)
 
789
        assert scheme == 'sftp'
 
790
 
 
791
        # the initial slash should be removed from the path, and treated
 
792
        # as a homedir relative path (the path begins with a double slash
 
793
        # if it is absolute).
 
794
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
795
        # RBC 20060118 we are not using this as its too user hostile. instead
 
796
        # we are following lftp and using /~/foo to mean '~/foo'.
 
797
        # handle homedir paths
 
798
        if path.startswith('/~/'):
 
799
            path = path[3:]
 
800
        elif path == '/~':
 
801
            path = ''
 
802
        return (username, password, host, port, path)
 
803
 
 
804
    def _parse_url(self, url):
 
805
        (self._username, self._password,
 
806
         self._host, self._port, self._path) = self._split_url(url)
 
807
 
 
808
    def _sftp_connect(self):
 
809
        """Connect to the remote sftp server.
 
810
        After this, self._sftp should have a valid connection (or
 
811
        we raise an TransportError 'could not connect').
 
812
 
 
813
        TODO: Raise a more reasonable ConnectionFailed exception
 
814
        """
 
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
            sock = SFTPSubprocess(self._host, vendor, self._port,
 
835
                                  self._username)
 
836
            self._sftp = SFTPClient(sock)
 
837
        else:
 
838
            self._paramiko_connect()
 
839
 
 
840
        _connected_hosts[idx] = self._sftp
 
841
 
 
842
    def _paramiko_connect(self):
 
843
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
844
        
 
845
        load_host_keys()
 
846
 
 
847
        try:
 
848
            t = paramiko.Transport((self._host, self._port or 22))
 
849
            t.set_log_channel('bzr.paramiko')
 
850
            t.start_client()
 
851
        except paramiko.SSHException, e:
 
852
            raise ConnectionError('Unable to reach SSH host %s:%s: %s' 
 
853
                                  % (self._host, self._port, e))
 
854
            
 
855
        server_key = t.get_remote_server_key()
 
856
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
857
        keytype = server_key.get_name()
 
858
        if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
 
859
            our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
 
860
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
861
        elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
 
862
            our_server_key = BZR_HOSTKEYS[self._host][keytype]
 
863
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
864
        else:
 
865
            warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
 
866
            if not BZR_HOSTKEYS.has_key(self._host):
 
867
                BZR_HOSTKEYS[self._host] = {}
 
868
            BZR_HOSTKEYS[self._host][keytype] = server_key
 
869
            our_server_key = server_key
 
870
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
 
871
            save_host_keys()
 
872
        if server_key != our_server_key:
 
873
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
 
874
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
 
875
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
 
876
                (self._host, our_server_key_hex, server_key_hex),
 
877
                ['Try editing %s or %s' % (filename1, filename2)])
 
878
 
 
879
        self._sftp_auth(t)
 
880
        
 
881
        try:
 
882
            self._sftp = t.open_sftp_client()
 
883
        except paramiko.SSHException, e:
 
884
            raise ConnectionError('Unable to start sftp client %s:%d' %
 
885
                                  (self._host, self._port), e)
 
886
 
 
887
    def _sftp_auth(self, transport):
 
888
        # paramiko requires a username, but it might be none if nothing was supplied
 
889
        # use the local username, just in case.
 
890
        # We don't override self._username, because if we aren't using paramiko,
 
891
        # the username might be specified in ~/.ssh/config and we don't want to
 
892
        # force it to something else
 
893
        # Also, it would mess up the self.relpath() functionality
 
894
        username = self._username or getpass.getuser()
 
895
 
 
896
        if _use_ssh_agent:
 
897
            agent = paramiko.Agent()
 
898
            for key in agent.get_keys():
 
899
                mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
900
                try:
 
901
                    transport.auth_publickey(username, key)
 
902
                    return
 
903
                except paramiko.SSHException, e:
 
904
                    pass
 
905
        
 
906
        # okay, try finding id_rsa or id_dss?  (posix only)
 
907
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
 
908
            return
 
909
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
 
910
            return
 
911
 
 
912
        if self._password:
 
913
            try:
 
914
                transport.auth_password(username, self._password)
 
915
                return
 
916
            except paramiko.SSHException, e:
 
917
                pass
 
918
 
 
919
            # FIXME: Don't keep a password held in memory if you can help it
 
920
            #self._password = None
 
921
 
 
922
        # give up and ask for a password
 
923
        password = bzrlib.ui.ui_factory.get_password(
 
924
                prompt='SSH %(user)s@%(host)s password',
 
925
                user=username, host=self._host)
 
926
        try:
 
927
            transport.auth_password(username, password)
 
928
        except paramiko.SSHException, e:
 
929
            raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
930
                                  (username, self._host), e)
 
931
 
 
932
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
 
933
        filename = os.path.expanduser('~/.ssh/' + filename)
 
934
        try:
 
935
            key = pkey_class.from_private_key_file(filename)
 
936
            transport.auth_publickey(username, key)
 
937
            return True
 
938
        except paramiko.PasswordRequiredException:
 
939
            password = bzrlib.ui.ui_factory.get_password(
 
940
                    prompt='SSH %(filename)s password',
 
941
                    filename=filename)
 
942
            try:
 
943
                key = pkey_class.from_private_key_file(filename, password)
 
944
                transport.auth_publickey(username, key)
 
945
                return True
 
946
            except paramiko.SSHException:
 
947
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
948
        except paramiko.SSHException:
 
949
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
 
950
        except IOError:
 
951
            pass
 
952
        return False
 
953
 
677
954
    def _sftp_open_exclusive(self, abspath, mode=None):
678
955
        """Open a remote path exclusively.
679
956
 
688
965
        :param abspath: The remote absolute path where the file should be opened
689
966
        :param mode: The mode permissions bits for the new file
690
967
        """
691
 
        # TODO: jam 20060816 Paramiko >= 1.6.2 (probably earlier) supports
692
 
        #       using the 'x' flag to indicate SFTP_FLAG_EXCL.
693
 
        #       However, there is no way to set the permission mode at open 
694
 
        #       time using the sftp_client.file() functionality.
695
 
        path = self._get_sftp()._adjust_cwd(abspath)
 
968
        path = self._sftp._adjust_cwd(abspath)
696
969
        # mutter('sftp abspath %s => %s', abspath, path)
697
970
        attr = SFTPAttributes()
698
971
        if mode is not None:
700
973
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
701
974
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
702
975
        try:
703
 
            t, msg = self._get_sftp()._request(CMD_OPEN, path, omode, attr)
 
976
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
704
977
            if t != CMD_HANDLE:
705
978
                raise TransportError('Expected an SFTP handle')
706
979
            handle = msg.get_string()
707
 
            return SFTPFile(self._get_sftp(), handle, 'wb', -1)
 
980
            return SFTPFile(self._sftp, handle, 'wb', -1)
708
981
        except (paramiko.SSHException, IOError), e:
709
982
            self._translate_io_exception(e, abspath, ': unable to open',
710
983
                failure_exc=FileExists)
711
984
 
712
 
    def _can_roundtrip_unix_modebits(self):
713
 
        if sys.platform == 'win32':
714
 
            # anyone else?
715
 
            return False
716
 
        else:
717
 
            return True
718
985
 
719
986
# ------------- server test implementation --------------
 
987
import socket
720
988
import threading
721
989
 
722
990
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
785
1053
                        x)
786
1054
 
787
1055
 
788
 
class SocketDelay(object):
789
 
    """A socket decorator to make TCP appear slower.
790
 
 
791
 
    This changes recv, send, and sendall to add a fixed latency to each python
792
 
    call if a new roundtrip is detected. That is, when a recv is called and the
793
 
    flag new_roundtrip is set, latency is charged. Every send and send_all
794
 
    sets this flag.
795
 
 
796
 
    In addition every send, sendall and recv sleeps a bit per character send to
797
 
    simulate bandwidth.
798
 
 
799
 
    Not all methods are implemented, this is deliberate as this class is not a
800
 
    replacement for the builtin sockets layer. fileno is not implemented to
801
 
    prevent the proxy being bypassed. 
802
 
    """
803
 
 
804
 
    simulated_time = 0
805
 
    _proxied_arguments = dict.fromkeys([
806
 
        "close", "getpeername", "getsockname", "getsockopt", "gettimeout",
807
 
        "setblocking", "setsockopt", "settimeout", "shutdown"])
808
 
 
809
 
    def __init__(self, sock, latency, bandwidth=1.0, 
810
 
                 really_sleep=True):
811
 
        """ 
812
 
        :param bandwith: simulated bandwith (MegaBit)
813
 
        :param really_sleep: If set to false, the SocketDelay will just
814
 
        increase a counter, instead of calling time.sleep. This is useful for
815
 
        unittesting the SocketDelay.
816
 
        """
817
 
        self.sock = sock
818
 
        self.latency = latency
819
 
        self.really_sleep = really_sleep
820
 
        self.time_per_byte = 1 / (bandwidth / 8.0 * 1024 * 1024) 
821
 
        self.new_roundtrip = False
822
 
 
823
 
    def sleep(self, s):
824
 
        if self.really_sleep:
825
 
            time.sleep(s)
826
 
        else:
827
 
            SocketDelay.simulated_time += s
828
 
 
829
 
    def __getattr__(self, attr):
830
 
        if attr in SocketDelay._proxied_arguments:
831
 
            return getattr(self.sock, attr)
832
 
        raise AttributeError("'SocketDelay' object has no attribute %r" %
833
 
                             attr)
834
 
 
835
 
    def dup(self):
836
 
        return SocketDelay(self.sock.dup(), self.latency, self.time_per_byte,
837
 
                           self._sleep)
838
 
 
839
 
    def recv(self, *args):
840
 
        data = self.sock.recv(*args)
841
 
        if data and self.new_roundtrip:
842
 
            self.new_roundtrip = False
843
 
            self.sleep(self.latency)
844
 
        self.sleep(len(data) * self.time_per_byte)
845
 
        return data
846
 
 
847
 
    def sendall(self, data, flags=0):
848
 
        if not self.new_roundtrip:
849
 
            self.new_roundtrip = True
850
 
            self.sleep(self.latency)
851
 
        self.sleep(len(data) * self.time_per_byte)
852
 
        return self.sock.sendall(data, flags)
853
 
 
854
 
    def send(self, data, flags=0):
855
 
        if not self.new_roundtrip:
856
 
            self.new_roundtrip = True
857
 
            self.sleep(self.latency)
858
 
        bytes_sent = self.sock.send(data, flags)
859
 
        self.sleep(bytes_sent * self.time_per_byte)
860
 
        return bytes_sent
861
 
 
862
 
 
863
1056
class SFTPServer(Server):
864
1057
    """Common code for SFTP server facilities."""
865
1058
 
866
 
    def __init__(self, server_interface=StubServer):
 
1059
    def __init__(self):
867
1060
        self._original_vendor = None
868
1061
        self._homedir = None
869
1062
        self._server_homedir = None
870
1063
        self._listener = None
871
1064
        self._root = None
872
 
        self._vendor = ssh.ParamikoVendor()
873
 
        self._server_interface = server_interface
 
1065
        self._vendor = 'none'
874
1066
        # sftp server logs
875
1067
        self.logs = []
876
 
        self.add_latency = 0
877
1068
 
878
1069
    def _get_sftp_url(self, path):
879
1070
        """Calculate an sftp url to this server for path."""
883
1074
        """StubServer uses this to log when a new server is created."""
884
1075
        self.logs.append(message)
885
1076
 
886
 
    def _run_server_entry(self, sock):
887
 
        """Entry point for all implementations of _run_server.
888
 
        
889
 
        If self.add_latency is > 0.000001 then sock is given a latency adding
890
 
        decorator.
891
 
        """
892
 
        if self.add_latency > 0.000001:
893
 
            sock = SocketDelay(sock, self.add_latency)
894
 
        return self._run_server(sock)
895
 
 
896
1077
    def _run_server(self, s):
897
1078
        ssh_server = paramiko.Transport(s)
898
1079
        key_file = pathjoin(self._homedir, 'test_rsa.key')
901
1082
        f.close()
902
1083
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
903
1084
        ssh_server.add_server_key(host_key)
904
 
        server = self._server_interface(self)
 
1085
        server = StubServer(self)
905
1086
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
906
1087
                                         StubSFTPServer, root=self._root,
907
1088
                                         home=self._server_homedir)
909
1090
        ssh_server.start_server(event, server)
910
1091
        event.wait(5.0)
911
1092
    
912
 
    def setUp(self, backing_server=None):
913
 
        # XXX: TODO: make sftpserver back onto backing_server rather than local
914
 
        # disk.
915
 
        assert (backing_server is None or
916
 
                isinstance(backing_server, local.LocalURLServer)), (
917
 
            "backing_server should not be %r, because this can only serve the "
918
 
            "local current working directory." % (backing_server,))
919
 
        self._original_vendor = ssh._ssh_vendor_manager._cached_ssh_vendor
920
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._vendor
 
1093
    def setUp(self):
 
1094
        global _ssh_vendor
 
1095
        self._original_vendor = _ssh_vendor
 
1096
        _ssh_vendor = self._vendor
921
1097
        if sys.platform == 'win32':
922
1098
            # Win32 needs to use the UNICODE api
923
1099
            self._homedir = getcwd()
929
1105
        self._root = '/'
930
1106
        if sys.platform == 'win32':
931
1107
            self._root = ''
932
 
        self._listener = SocketListener(self._run_server_entry)
 
1108
        self._listener = SocketListener(self._run_server)
933
1109
        self._listener.setDaemon(True)
934
1110
        self._listener.start()
935
1111
 
936
1112
    def tearDown(self):
937
1113
        """See bzrlib.transport.Server.tearDown."""
 
1114
        global _ssh_vendor
938
1115
        self._listener.stop()
939
 
        ssh._ssh_vendor_manager._cached_ssh_vendor = self._original_vendor
 
1116
        _ssh_vendor = self._original_vendor
940
1117
 
941
1118
    def get_bogus_url(self):
942
1119
        """See bzrlib.transport.Server.get_bogus_url."""
943
 
        # this is chosen to try to prevent trouble with proxies, wierd dns, etc
944
 
        # we bind a random socket, so that we get a guaranteed unused port
945
 
        # we just never listen on that port
946
 
        s = socket.socket()
947
 
        s.bind(('localhost', 0))
948
 
        return 'sftp://%s:%s/' % s.getsockname()
 
1120
        # this is chosen to try to prevent trouble with proxies, wierd dns,
 
1121
        # etc
 
1122
        return 'sftp://127.0.0.1:1/'
 
1123
 
949
1124
 
950
1125
 
951
1126
class SFTPFullAbsoluteServer(SFTPServer):
953
1128
 
954
1129
    def get_url(self):
955
1130
        """See bzrlib.transport.Server.get_url."""
956
 
        homedir = self._homedir
957
 
        if sys.platform != 'win32':
958
 
            # Remove the initial '/' on all platforms but win32
959
 
            homedir = homedir[1:]
960
 
        return self._get_sftp_url(urlutils.escape(homedir))
 
1131
        return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
961
1132
 
962
1133
 
963
1134
class SFTPServerWithoutSSH(SFTPServer):
965
1136
 
966
1137
    def __init__(self):
967
1138
        super(SFTPServerWithoutSSH, self).__init__()
968
 
        self._vendor = ssh.LoopbackVendor()
 
1139
        self._vendor = 'loopback'
969
1140
 
970
1141
    def _run_server(self, sock):
971
 
        # Re-import these as locals, so that they're still accessible during
972
 
        # interpreter shutdown (when all module globals get set to None, leading
973
 
        # to confusing errors like "'NoneType' object has no attribute 'error'".
974
1142
        class FakeChannel(object):
975
1143
            def get_transport(self):
976
1144
                return self
995
1163
            else:
996
1164
                raise
997
1165
        except Exception, e:
998
 
            # This typically seems to happen during interpreter shutdown, so
999
 
            # most of the useful ways to report this error are won't work.
1000
 
            # Writing the exception type, and then the text of the exception,
1001
 
            # seems to be the best we can do.
1002
 
            import sys
1003
 
            sys.stderr.write('\nEXCEPTION %r: ' % (e.__class__,))
1004
 
            sys.stderr.write('%s\n\n' % (e,))
 
1166
            import sys; sys.stderr.write('\nEXCEPTION %r\n\n' % e.__class__)
1005
1167
        server.finish_subsystem()
1006
1168
 
1007
1169
 
1010
1172
 
1011
1173
    def get_url(self):
1012
1174
        """See bzrlib.transport.Server.get_url."""
1013
 
        homedir = self._homedir
1014
 
        if sys.platform != 'win32':
1015
 
            # Remove the initial '/' on all platforms but win32
1016
 
            homedir = homedir[1:]
1017
 
        return self._get_sftp_url(urlutils.escape(homedir))
 
1175
        if sys.platform == 'win32':
 
1176
            return self._get_sftp_url(urlutils.escape(self._homedir))
 
1177
        else:
 
1178
            return self._get_sftp_url(urlutils.escape(self._homedir[1:]))
1018
1179
 
1019
1180
 
1020
1181
class SFTPHomeDirServer(SFTPServerWithoutSSH):
1026
1187
 
1027
1188
 
1028
1189
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
1029
 
    """A test server for sftp transports where only absolute paths will work.
1030
 
 
1031
 
    It does this by serving from a deeply-nested directory that doesn't exist.
1032
 
    """
1033
 
 
1034
 
    def setUp(self, backing_server=None):
 
1190
    """A test servere for sftp transports, using absolute urls to non-home."""
 
1191
 
 
1192
    def setUp(self):
1035
1193
        self._server_homedir = '/dev/noone/runs/tests/here'
1036
 
        super(SFTPSiblingAbsoluteServer, self).setUp(backing_server)
 
1194
        super(SFTPSiblingAbsoluteServer, self).setUp()
1037
1195
 
1038
1196
 
1039
1197
def get_test_permutations():