~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

  • Committer: Martin Pool
  • Date: 2005-09-16 09:56:24 UTC
  • Revision ID: mbp@sourcefrog.net-20050916095623-ca0dff452934f21f
- make progress bar more tolerant of out-of-range values

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Robey Pointer <robey@lag.net>, Canonical Ltd
2
 
 
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
 
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
 
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
"""Implementation of Transport over SFTP, using paramiko."""
18
 
 
19
 
import errno
20
 
import getpass
21
 
import os
22
 
import re
23
 
import stat
24
 
import sys
25
 
import urllib
26
 
import urlparse
27
 
import time
28
 
import random
29
 
import subprocess
30
 
import weakref
31
 
 
32
 
from bzrlib.config import config_dir, ensure_config_dir_exists
33
 
from bzrlib.errors import (ConnectionError,
34
 
                           FileExists, 
35
 
                           TransportNotPossible, NoSuchFile, PathNotChild,
36
 
                           TransportError,
37
 
                           LockError, ParamikoNotPresent
38
 
                           )
39
 
from bzrlib.osutils import pathjoin, fancy_rename
40
 
from bzrlib.trace import mutter, warning, error
41
 
from bzrlib.transport import Transport, Server, urlescape
42
 
import bzrlib.ui
43
 
 
44
 
try:
45
 
    import paramiko
46
 
except ImportError, e:
47
 
    raise ParamikoNotPresent(e)
48
 
else:
49
 
    from paramiko.sftp import (SFTP_FLAG_WRITE, SFTP_FLAG_CREATE,
50
 
                               SFTP_FLAG_EXCL, SFTP_FLAG_TRUNC,
51
 
                               CMD_HANDLE, CMD_OPEN)
52
 
    from paramiko.sftp_attr import SFTPAttributes
53
 
    from paramiko.sftp_file import SFTPFile
54
 
    from paramiko.sftp_client import SFTPClient
55
 
 
56
 
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
57
 
 
58
 
 
59
 
_close_fds = True
60
 
if sys.platform == 'win32':
61
 
    # close_fds not supported on win32
62
 
    _close_fds = False
63
 
 
64
 
_ssh_vendor = None
65
 
def _get_ssh_vendor():
66
 
    """Find out what version of SSH is on the system."""
67
 
    global _ssh_vendor
68
 
    if _ssh_vendor is not None:
69
 
        return _ssh_vendor
70
 
 
71
 
    _ssh_vendor = 'none'
72
 
 
73
 
    try:
74
 
        p = subprocess.Popen(['ssh', '-V'],
75
 
                             close_fds=_close_fds,
76
 
                             stdin=subprocess.PIPE,
77
 
                             stdout=subprocess.PIPE,
78
 
                             stderr=subprocess.PIPE)
79
 
        returncode = p.returncode
80
 
        stdout, stderr = p.communicate()
81
 
    except OSError:
82
 
        returncode = -1
83
 
        stdout = stderr = ''
84
 
    if 'OpenSSH' in stderr:
85
 
        mutter('ssh implementation is OpenSSH')
86
 
        _ssh_vendor = 'openssh'
87
 
    elif 'SSH Secure Shell' in stderr:
88
 
        mutter('ssh implementation is SSH Corp.')
89
 
        _ssh_vendor = 'ssh'
90
 
 
91
 
    if _ssh_vendor != 'none':
92
 
        return _ssh_vendor
93
 
 
94
 
    # XXX: 20051123 jamesh
95
 
    # A check for putty's plink or lsh would go here.
96
 
 
97
 
    mutter('falling back to paramiko implementation')
98
 
    return _ssh_vendor
99
 
 
100
 
 
101
 
class SFTPSubprocess:
102
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
103
 
    def __init__(self, hostname, vendor, port=None, user=None):
104
 
        assert vendor in ['openssh', 'ssh']
105
 
        if vendor == 'openssh':
106
 
            args = ['ssh',
107
 
                    '-oForwardX11=no', '-oForwardAgent=no',
108
 
                    '-oClearAllForwardings=yes', '-oProtocol=2',
109
 
                    '-oNoHostAuthenticationForLocalhost=yes']
110
 
            if port is not None:
111
 
                args.extend(['-p', str(port)])
112
 
            if user is not None:
113
 
                args.extend(['-l', user])
114
 
            args.extend(['-s', hostname, 'sftp'])
115
 
        elif vendor == 'ssh':
116
 
            args = ['ssh', '-x']
117
 
            if port is not None:
118
 
                args.extend(['-p', str(port)])
119
 
            if user is not None:
120
 
                args.extend(['-l', user])
121
 
            args.extend(['-s', 'sftp', hostname])
122
 
 
123
 
        self.proc = subprocess.Popen(args, close_fds=_close_fds,
124
 
                                     stdin=subprocess.PIPE,
125
 
                                     stdout=subprocess.PIPE)
126
 
 
127
 
    def send(self, data):
128
 
        return os.write(self.proc.stdin.fileno(), data)
129
 
 
130
 
    def recv_ready(self):
131
 
        # TODO: jam 20051215 this function is necessary to support the
132
 
        # pipelined() function. In reality, it probably should use
133
 
        # poll() or select() to actually return if there is data
134
 
        # available, otherwise we probably don't get any benefit
135
 
        return True
136
 
 
137
 
    def recv(self, count):
138
 
        return os.read(self.proc.stdout.fileno(), count)
139
 
 
140
 
    def close(self):
141
 
        self.proc.stdin.close()
142
 
        self.proc.stdout.close()
143
 
        self.proc.wait()
144
 
 
145
 
 
146
 
class LoopbackSFTP(object):
147
 
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
148
 
 
149
 
    def __init__(self, sock):
150
 
        self.__socket = sock
151
 
 
152
 
    def send(self, data):
153
 
        return self.__socket.send(data)
154
 
 
155
 
    def recv(self, n):
156
 
        return self.__socket.recv(n)
157
 
 
158
 
    def recv_ready(self):
159
 
        return True
160
 
 
161
 
    def close(self):
162
 
        self.__socket.close()
163
 
 
164
 
 
165
 
SYSTEM_HOSTKEYS = {}
166
 
BZR_HOSTKEYS = {}
167
 
 
168
 
# This is a weakref dictionary, so that we can reuse connections
169
 
# that are still active. Long term, it might be nice to have some
170
 
# sort of expiration policy, such as disconnect if inactive for
171
 
# X seconds. But that requires a lot more fanciness.
172
 
_connected_hosts = weakref.WeakValueDictionary()
173
 
 
174
 
 
175
 
def load_host_keys():
176
 
    """
177
 
    Load system host keys (probably doesn't work on windows) and any
178
 
    "discovered" keys from previous sessions.
179
 
    """
180
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
181
 
    try:
182
 
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
183
 
    except Exception, e:
184
 
        mutter('failed to load system host keys: ' + str(e))
185
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
186
 
    try:
187
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
188
 
    except Exception, e:
189
 
        mutter('failed to load bzr host keys: ' + str(e))
190
 
        save_host_keys()
191
 
 
192
 
 
193
 
def save_host_keys():
194
 
    """
195
 
    Save "discovered" host keys in $(config)/ssh_host_keys/.
196
 
    """
197
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
198
 
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
199
 
    ensure_config_dir_exists()
200
 
 
201
 
    try:
202
 
        f = open(bzr_hostkey_path, 'w')
203
 
        f.write('# SSH host keys collected by bzr\n')
204
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
205
 
            for keytype, key in keys.iteritems():
206
 
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
207
 
        f.close()
208
 
    except IOError, e:
209
 
        mutter('failed to save bzr host keys: ' + str(e))
210
 
 
211
 
 
212
 
class SFTPLock(object):
213
 
    """This fakes a lock in a remote location."""
214
 
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
215
 
    def __init__(self, path, transport):
216
 
        assert isinstance(transport, SFTPTransport)
217
 
 
218
 
        self.lock_file = None
219
 
        self.path = path
220
 
        self.lock_path = path + '.write-lock'
221
 
        self.transport = transport
222
 
        try:
223
 
            # RBC 20060103 FIXME should we be using private methods here ?
224
 
            abspath = transport._remote_path(self.lock_path)
225
 
            self.lock_file = transport._sftp_open_exclusive(abspath)
226
 
        except FileExists:
227
 
            raise LockError('File %r already locked' % (self.path,))
228
 
 
229
 
    def __del__(self):
230
 
        """Should this warn, or actually try to cleanup?"""
231
 
        if self.lock_file:
232
 
            warning("SFTPLock %r not explicitly unlocked" % (self.path,))
233
 
            self.unlock()
234
 
 
235
 
    def unlock(self):
236
 
        if not self.lock_file:
237
 
            return
238
 
        self.lock_file.close()
239
 
        self.lock_file = None
240
 
        try:
241
 
            self.transport.delete(self.lock_path)
242
 
        except (NoSuchFile,):
243
 
            # What specific errors should we catch here?
244
 
            pass
245
 
 
246
 
 
247
 
class SFTPTransport (Transport):
248
 
    """
249
 
    Transport implementation for SFTP access.
250
 
    """
251
 
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
252
 
 
253
 
    def __init__(self, base, clone_from=None):
254
 
        assert base.startswith('sftp://')
255
 
        self._parse_url(base)
256
 
        base = self._unparse_url()
257
 
        if base[-1] != '/':
258
 
            base = base + '/'
259
 
        super(SFTPTransport, self).__init__(base)
260
 
        if clone_from is None:
261
 
            self._sftp_connect()
262
 
        else:
263
 
            # use the same ssh connection, etc
264
 
            self._sftp = clone_from._sftp
265
 
        # super saves 'self.base'
266
 
    
267
 
    def should_cache(self):
268
 
        """
269
 
        Return True if the data pulled across should be cached locally.
270
 
        """
271
 
        return True
272
 
 
273
 
    def clone(self, offset=None):
274
 
        """
275
 
        Return a new SFTPTransport with root at self.base + offset.
276
 
        We share the same SFTP session between such transports, because it's
277
 
        fairly expensive to set them up.
278
 
        """
279
 
        if offset is None:
280
 
            return SFTPTransport(self.base, self)
281
 
        else:
282
 
            return SFTPTransport(self.abspath(offset), self)
283
 
 
284
 
    def abspath(self, relpath):
285
 
        """
286
 
        Return the full url to the given relative path.
287
 
        
288
 
        @param relpath: the relative path or path components
289
 
        @type relpath: str or list
290
 
        """
291
 
        return self._unparse_url(self._remote_path(relpath))
292
 
    
293
 
    def _remote_path(self, relpath):
294
 
        """Return the path to be passed along the sftp protocol for relpath.
295
 
        
296
 
        relpath is a urlencoded string.
297
 
        """
298
 
        # FIXME: share the common code across transports
299
 
        assert isinstance(relpath, basestring)
300
 
        relpath = urllib.unquote(relpath).split('/')
301
 
        basepath = self._path.split('/')
302
 
        if len(basepath) > 0 and basepath[-1] == '':
303
 
            basepath = basepath[:-1]
304
 
 
305
 
        for p in relpath:
306
 
            if p == '..':
307
 
                if len(basepath) == 0:
308
 
                    # In most filesystems, a request for the parent
309
 
                    # of root, just returns root.
310
 
                    continue
311
 
                basepath.pop()
312
 
            elif p == '.':
313
 
                continue # No-op
314
 
            else:
315
 
                basepath.append(p)
316
 
 
317
 
        path = '/'.join(basepath)
318
 
        return path
319
 
 
320
 
    def relpath(self, abspath):
321
 
        username, password, host, port, path = self._split_url(abspath)
322
 
        error = []
323
 
        if (username != self._username):
324
 
            error.append('username mismatch')
325
 
        if (host != self._host):
326
 
            error.append('host mismatch')
327
 
        if (port != self._port):
328
 
            error.append('port mismatch')
329
 
        if (not path.startswith(self._path)):
330
 
            error.append('path mismatch')
331
 
        if error:
332
 
            extra = ': ' + ', '.join(error)
333
 
            raise PathNotChild(abspath, self.base, extra=extra)
334
 
        pl = len(self._path)
335
 
        return path[pl:].strip('/')
336
 
 
337
 
    def has(self, relpath):
338
 
        """
339
 
        Does the target location exist?
340
 
        """
341
 
        try:
342
 
            self._sftp.stat(self._remote_path(relpath))
343
 
            return True
344
 
        except IOError:
345
 
            return False
346
 
 
347
 
    def get(self, relpath, decode=False):
348
 
        """
349
 
        Get the file at the given relative path.
350
 
 
351
 
        :param relpath: The relative path to the file
352
 
        """
353
 
        try:
354
 
            path = self._remote_path(relpath)
355
 
            f = self._sftp.file(path, mode='rb')
356
 
            if self._do_prefetch and hasattr(f, 'prefetch'):
357
 
                f.prefetch()
358
 
            return f
359
 
        except (IOError, paramiko.SSHException), e:
360
 
            self._translate_io_exception(e, path, ': error retrieving')
361
 
 
362
 
    def get_partial(self, relpath, start, length=None):
363
 
        """
364
 
        Get just part of a file.
365
 
 
366
 
        :param relpath: Path to the file, relative to base
367
 
        :param start: The starting position to read from
368
 
        :param length: The length to read. A length of None indicates
369
 
                       read to the end of the file.
370
 
        :return: A file-like object containing at least the specified bytes.
371
 
                 Some implementations may return objects which can be read
372
 
                 past this length, but this is not guaranteed.
373
 
        """
374
 
        # TODO: implement get_partial_multi to help with knit support
375
 
        f = self.get(relpath)
376
 
        f.seek(start)
377
 
        if self._do_prefetch and hasattr(f, 'prefetch'):
378
 
            f.prefetch()
379
 
        return f
380
 
 
381
 
    def put(self, relpath, f, mode=None):
382
 
        """
383
 
        Copy the file-like or string object into the location.
384
 
 
385
 
        :param relpath: Location to put the contents, relative to base.
386
 
        :param f:       File-like or string object.
387
 
        :param mode: The final mode for the file
388
 
        """
389
 
        final_path = self._remote_path(relpath)
390
 
        self._put(final_path, f, mode=mode)
391
 
 
392
 
    def _put(self, abspath, f, mode=None):
393
 
        """Helper function so both put() and copy_abspaths can reuse the code"""
394
 
        tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
395
 
                        os.getpid(), random.randint(0,0x7FFFFFFF))
396
 
        fout = self._sftp_open_exclusive(tmp_abspath, mode=mode)
397
 
        closed = False
398
 
        try:
399
 
            try:
400
 
                fout.set_pipelined(True)
401
 
                self._pump(f, fout)
402
 
            except (IOError, paramiko.SSHException), e:
403
 
                self._translate_io_exception(e, tmp_abspath)
404
 
            if mode is not None:
405
 
                self._sftp.chmod(tmp_abspath, mode)
406
 
            fout.close()
407
 
            closed = True
408
 
            self._rename(tmp_abspath, abspath)
409
 
        except Exception, e:
410
 
            # If we fail, try to clean up the temporary file
411
 
            # before we throw the exception
412
 
            # but don't let another exception mess things up
413
 
            # Write out the traceback, because otherwise
414
 
            # the catch and throw destroys it
415
 
            import traceback
416
 
            mutter(traceback.format_exc())
417
 
            try:
418
 
                if not closed:
419
 
                    fout.close()
420
 
                self._sftp.remove(tmp_abspath)
421
 
            except:
422
 
                # raise the saved except
423
 
                raise e
424
 
            # raise the original with its traceback if we can.
425
 
            raise
426
 
 
427
 
    def iter_files_recursive(self):
428
 
        """Walk the relative paths of all files in this transport."""
429
 
        queue = list(self.list_dir('.'))
430
 
        while queue:
431
 
            relpath = urllib.quote(queue.pop(0))
432
 
            st = self.stat(relpath)
433
 
            if stat.S_ISDIR(st.st_mode):
434
 
                for i, basename in enumerate(self.list_dir(relpath)):
435
 
                    queue.insert(i, relpath+'/'+basename)
436
 
            else:
437
 
                yield relpath
438
 
 
439
 
    def mkdir(self, relpath, mode=None):
440
 
        """Create a directory at the given path."""
441
 
        try:
442
 
            path = self._remote_path(relpath)
443
 
            # In the paramiko documentation, it says that passing a mode flag 
444
 
            # will filtered against the server umask.
445
 
            # StubSFTPServer does not do this, which would be nice, because it is
446
 
            # what we really want :)
447
 
            # However, real servers do use umask, so we really should do it that way
448
 
            self._sftp.mkdir(path)
449
 
            if mode is not None:
450
 
                self._sftp.chmod(path, mode=mode)
451
 
        except (paramiko.SSHException, IOError), e:
452
 
            self._translate_io_exception(e, path, ': unable to mkdir',
453
 
                failure_exc=FileExists)
454
 
 
455
 
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
456
 
        """Translate a paramiko or IOError into a friendlier exception.
457
 
 
458
 
        :param e: The original exception
459
 
        :param path: The path in question when the error is raised
460
 
        :param more_info: Extra information that can be included,
461
 
                          such as what was going on
462
 
        :param failure_exc: Paramiko has the super fun ability to raise completely
463
 
                           opaque errors that just set "e.args = ('Failure',)" with
464
 
                           no more information.
465
 
                           This sometimes means FileExists, but it also sometimes
466
 
                           means NoSuchFile
467
 
        """
468
 
        # paramiko seems to generate detailless errors.
469
 
        self._translate_error(e, path, raise_generic=False)
470
 
        if hasattr(e, 'args'):
471
 
            if (e.args == ('No such file or directory',) or
472
 
                e.args == ('No such file',)):
473
 
                raise NoSuchFile(path, str(e) + more_info)
474
 
            if (e.args == ('mkdir failed',)):
475
 
                raise FileExists(path, str(e) + more_info)
476
 
            # strange but true, for the paramiko server.
477
 
            if (e.args == ('Failure',)):
478
 
                raise failure_exc(path, str(e) + more_info)
479
 
            mutter('Raising exception with args %s', e.args)
480
 
        if hasattr(e, 'errno'):
481
 
            mutter('Raising exception with errno %s', e.errno)
482
 
        raise e
483
 
 
484
 
    def append(self, relpath, f):
485
 
        """
486
 
        Append the text in the file-like object into the final
487
 
        location.
488
 
        """
489
 
        try:
490
 
            path = self._remote_path(relpath)
491
 
            fout = self._sftp.file(path, 'ab')
492
 
            self._pump(f, fout)
493
 
        except (IOError, paramiko.SSHException), e:
494
 
            self._translate_io_exception(e, relpath, ': unable to append')
495
 
 
496
 
    def _rename(self, abs_from, abs_to):
497
 
        """Do a fancy rename on the remote server.
498
 
        
499
 
        Using the implementation provided by osutils.
500
 
        """
501
 
        try:
502
 
            fancy_rename(abs_from, abs_to,
503
 
                    rename_func=self._sftp.rename,
504
 
                    unlink_func=self._sftp.remove)
505
 
        except (IOError, paramiko.SSHException), e:
506
 
            self._translate_io_exception(e, abs_from, ': unable to rename to %r' % (abs_to))
507
 
 
508
 
    def move(self, rel_from, rel_to):
509
 
        """Move the item at rel_from to the location at rel_to"""
510
 
        path_from = self._remote_path(rel_from)
511
 
        path_to = self._remote_path(rel_to)
512
 
        self._rename(path_from, path_to)
513
 
 
514
 
    def delete(self, relpath):
515
 
        """Delete the item at relpath"""
516
 
        path = self._remote_path(relpath)
517
 
        try:
518
 
            self._sftp.remove(path)
519
 
        except (IOError, paramiko.SSHException), e:
520
 
            self._translate_io_exception(e, path, ': unable to delete')
521
 
            
522
 
    def listable(self):
523
 
        """Return True if this store supports listing."""
524
 
        return True
525
 
 
526
 
    def list_dir(self, relpath):
527
 
        """
528
 
        Return a list of all files at the given location.
529
 
        """
530
 
        # does anything actually use this?
531
 
        path = self._remote_path(relpath)
532
 
        try:
533
 
            return self._sftp.listdir(path)
534
 
        except (IOError, paramiko.SSHException), e:
535
 
            self._translate_io_exception(e, path, ': failed to list_dir')
536
 
 
537
 
    def rmdir(self, relpath):
538
 
        """See Transport.rmdir."""
539
 
        path = self._remote_path(relpath)
540
 
        try:
541
 
            return self._sftp.rmdir(path)
542
 
        except (IOError, paramiko.SSHException), e:
543
 
            self._translate_io_exception(e, path, ': failed to rmdir')
544
 
 
545
 
    def stat(self, relpath):
546
 
        """Return the stat information for a file."""
547
 
        path = self._remote_path(relpath)
548
 
        try:
549
 
            return self._sftp.stat(path)
550
 
        except (IOError, paramiko.SSHException), e:
551
 
            self._translate_io_exception(e, path, ': unable to stat')
552
 
 
553
 
    def lock_read(self, relpath):
554
 
        """
555
 
        Lock the given file for shared (read) access.
556
 
        :return: A lock object, which has an unlock() member function
557
 
        """
558
 
        # FIXME: there should be something clever i can do here...
559
 
        class BogusLock(object):
560
 
            def __init__(self, path):
561
 
                self.path = path
562
 
            def unlock(self):
563
 
                pass
564
 
        return BogusLock(relpath)
565
 
 
566
 
    def lock_write(self, relpath):
567
 
        """
568
 
        Lock the given file for exclusive (write) access.
569
 
        WARNING: many transports do not support this, so trying avoid using it
570
 
 
571
 
        :return: A lock object, which has an unlock() member function
572
 
        """
573
 
        # This is a little bit bogus, but basically, we create a file
574
 
        # which should not already exist, and if it does, we assume
575
 
        # that there is a lock, and if it doesn't, the we assume
576
 
        # that we have taken the lock.
577
 
        return SFTPLock(relpath, self)
578
 
 
579
 
    def _unparse_url(self, path=None):
580
 
        if path is None:
581
 
            path = self._path
582
 
        path = urllib.quote(path)
583
 
        # handle homedir paths
584
 
        if not path.startswith('/'):
585
 
            path = "/~/" + path
586
 
        netloc = urllib.quote(self._host)
587
 
        if self._username is not None:
588
 
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
589
 
        if self._port is not None:
590
 
            netloc = '%s:%d' % (netloc, self._port)
591
 
 
592
 
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
593
 
 
594
 
    def _split_url(self, url):
595
 
        if isinstance(url, unicode):
596
 
            url = url.encode('utf-8')
597
 
        (scheme, netloc, path, params,
598
 
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
599
 
        assert scheme == 'sftp'
600
 
        username = password = host = port = None
601
 
        if '@' in netloc:
602
 
            username, host = netloc.split('@', 1)
603
 
            if ':' in username:
604
 
                username, password = username.split(':', 1)
605
 
                password = urllib.unquote(password)
606
 
            username = urllib.unquote(username)
607
 
        else:
608
 
            host = netloc
609
 
 
610
 
        if ':' in host:
611
 
            host, port = host.rsplit(':', 1)
612
 
            try:
613
 
                port = int(port)
614
 
            except ValueError:
615
 
                # TODO: Should this be ConnectionError?
616
 
                raise TransportError('%s: invalid port number' % port)
617
 
        host = urllib.unquote(host)
618
 
 
619
 
        path = urllib.unquote(path)
620
 
 
621
 
        # the initial slash should be removed from the path, and treated
622
 
        # as a homedir relative path (the path begins with a double slash
623
 
        # if it is absolute).
624
 
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
625
 
        # RBC 20060118 we are not using this as its too user hostile. instead
626
 
        # we are following lftp and using /~/foo to mean '~/foo'.
627
 
        # handle homedir paths
628
 
        if path.startswith('/~/'):
629
 
            path = path[3:]
630
 
        elif path == '/~':
631
 
            path = ''
632
 
        return (username, password, host, port, path)
633
 
 
634
 
    def _parse_url(self, url):
635
 
        (self._username, self._password,
636
 
         self._host, self._port, self._path) = self._split_url(url)
637
 
 
638
 
    def _sftp_connect(self):
639
 
        """Connect to the remote sftp server.
640
 
        After this, self._sftp should have a valid connection (or
641
 
        we raise an TransportError 'could not connect').
642
 
 
643
 
        TODO: Raise a more reasonable ConnectionFailed exception
644
 
        """
645
 
        global _connected_hosts
646
 
 
647
 
        idx = (self._host, self._port, self._username)
648
 
        try:
649
 
            self._sftp = _connected_hosts[idx]
650
 
            return
651
 
        except KeyError:
652
 
            pass
653
 
        
654
 
        vendor = _get_ssh_vendor()
655
 
        if vendor == 'loopback':
656
 
            sock = socket.socket()
657
 
            sock.connect((self._host, self._port))
658
 
            self._sftp = SFTPClient(LoopbackSFTP(sock))
659
 
        elif vendor != 'none':
660
 
            sock = SFTPSubprocess(self._host, vendor, self._port,
661
 
                                  self._username)
662
 
            self._sftp = SFTPClient(sock)
663
 
        else:
664
 
            self._paramiko_connect()
665
 
 
666
 
        _connected_hosts[idx] = self._sftp
667
 
 
668
 
    def _paramiko_connect(self):
669
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
670
 
        
671
 
        load_host_keys()
672
 
 
673
 
        try:
674
 
            t = paramiko.Transport((self._host, self._port or 22))
675
 
            t.set_log_channel('bzr.paramiko')
676
 
            t.start_client()
677
 
        except paramiko.SSHException, e:
678
 
            raise ConnectionError('Unable to reach SSH host %s:%d' %
679
 
                                  (self._host, self._port), e)
680
 
            
681
 
        server_key = t.get_remote_server_key()
682
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
683
 
        keytype = server_key.get_name()
684
 
        if SYSTEM_HOSTKEYS.has_key(self._host) and SYSTEM_HOSTKEYS[self._host].has_key(keytype):
685
 
            our_server_key = SYSTEM_HOSTKEYS[self._host][keytype]
686
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
687
 
        elif BZR_HOSTKEYS.has_key(self._host) and BZR_HOSTKEYS[self._host].has_key(keytype):
688
 
            our_server_key = BZR_HOSTKEYS[self._host][keytype]
689
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
690
 
        else:
691
 
            warning('Adding %s host key for %s: %s' % (keytype, self._host, server_key_hex))
692
 
            if not BZR_HOSTKEYS.has_key(self._host):
693
 
                BZR_HOSTKEYS[self._host] = {}
694
 
            BZR_HOSTKEYS[self._host][keytype] = server_key
695
 
            our_server_key = server_key
696
 
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
697
 
            save_host_keys()
698
 
        if server_key != our_server_key:
699
 
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
700
 
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
701
 
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
702
 
                (self._host, our_server_key_hex, server_key_hex),
703
 
                ['Try editing %s or %s' % (filename1, filename2)])
704
 
 
705
 
        self._sftp_auth(t)
706
 
        
707
 
        try:
708
 
            self._sftp = t.open_sftp_client()
709
 
        except paramiko.SSHException, e:
710
 
            raise ConnectionError('Unable to start sftp client %s:%d' %
711
 
                                  (self._host, self._port), e)
712
 
 
713
 
    def _sftp_auth(self, transport):
714
 
        # paramiko requires a username, but it might be none if nothing was supplied
715
 
        # use the local username, just in case.
716
 
        # We don't override self._username, because if we aren't using paramiko,
717
 
        # the username might be specified in ~/.ssh/config and we don't want to
718
 
        # force it to something else
719
 
        # Also, it would mess up the self.relpath() functionality
720
 
        username = self._username or getpass.getuser()
721
 
 
722
 
        # Paramiko tries to open a socket.AF_UNIX in order to connect
723
 
        # to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
724
 
        # so we get an AttributeError exception. For now, just don't try to
725
 
        # connect to an agent if we are on win32
726
 
        if sys.platform != 'win32':
727
 
            agent = paramiko.Agent()
728
 
            for key in agent.get_keys():
729
 
                mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
730
 
                try:
731
 
                    transport.auth_publickey(username, key)
732
 
                    return
733
 
                except paramiko.SSHException, e:
734
 
                    pass
735
 
        
736
 
        # okay, try finding id_rsa or id_dss?  (posix only)
737
 
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
738
 
            return
739
 
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
740
 
            return
741
 
 
742
 
        if self._password:
743
 
            try:
744
 
                transport.auth_password(username, self._password)
745
 
                return
746
 
            except paramiko.SSHException, e:
747
 
                pass
748
 
 
749
 
            # FIXME: Don't keep a password held in memory if you can help it
750
 
            #self._password = None
751
 
 
752
 
        # give up and ask for a password
753
 
        password = bzrlib.ui.ui_factory.get_password(
754
 
                prompt='SSH %(user)s@%(host)s password',
755
 
                user=username, host=self._host)
756
 
        try:
757
 
            transport.auth_password(username, password)
758
 
        except paramiko.SSHException, e:
759
 
            raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
760
 
                                  (username, self._host), e)
761
 
 
762
 
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
763
 
        filename = os.path.expanduser('~/.ssh/' + filename)
764
 
        try:
765
 
            key = pkey_class.from_private_key_file(filename)
766
 
            transport.auth_publickey(username, key)
767
 
            return True
768
 
        except paramiko.PasswordRequiredException:
769
 
            password = bzrlib.ui.ui_factory.get_password(
770
 
                    prompt='SSH %(filename)s password',
771
 
                    filename=filename)
772
 
            try:
773
 
                key = pkey_class.from_private_key_file(filename, password)
774
 
                transport.auth_publickey(username, key)
775
 
                return True
776
 
            except paramiko.SSHException:
777
 
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
778
 
        except paramiko.SSHException:
779
 
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
780
 
        except IOError:
781
 
            pass
782
 
        return False
783
 
 
784
 
    def _sftp_open_exclusive(self, abspath, mode=None):
785
 
        """Open a remote path exclusively.
786
 
 
787
 
        SFTP supports O_EXCL (SFTP_FLAG_EXCL), which fails if
788
 
        the file already exists. However it does not expose this
789
 
        at the higher level of SFTPClient.open(), so we have to
790
 
        sneak away with it.
791
 
 
792
 
        WARNING: This breaks the SFTPClient abstraction, so it
793
 
        could easily break against an updated version of paramiko.
794
 
 
795
 
        :param abspath: The remote absolute path where the file should be opened
796
 
        :param mode: The mode permissions bits for the new file
797
 
        """
798
 
        path = self._sftp._adjust_cwd(abspath)
799
 
        attr = SFTPAttributes()
800
 
        if mode is not None:
801
 
            attr.st_mode = mode
802
 
        omode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
803
 
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
804
 
        try:
805
 
            t, msg = self._sftp._request(CMD_OPEN, path, omode, attr)
806
 
            if t != CMD_HANDLE:
807
 
                raise TransportError('Expected an SFTP handle')
808
 
            handle = msg.get_string()
809
 
            return SFTPFile(self._sftp, handle, 'wb', -1)
810
 
        except (paramiko.SSHException, IOError), e:
811
 
            self._translate_io_exception(e, abspath, ': unable to open',
812
 
                failure_exc=FileExists)
813
 
 
814
 
 
815
 
# ------------- server test implementation --------------
816
 
import socket
817
 
import threading
818
 
 
819
 
from bzrlib.tests.stub_sftp import StubServer, StubSFTPServer
820
 
 
821
 
STUB_SERVER_KEY = """
822
 
-----BEGIN RSA PRIVATE KEY-----
823
 
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
824
 
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
825
 
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
826
 
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
827
 
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
828
 
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
829
 
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
830
 
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
831
 
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
832
 
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
833
 
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
834
 
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
835
 
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
836
 
-----END RSA PRIVATE KEY-----
837
 
"""
838
 
    
839
 
 
840
 
class SingleListener(threading.Thread):
841
 
 
842
 
    def __init__(self, callback):
843
 
        threading.Thread.__init__(self)
844
 
        self._callback = callback
845
 
        self._socket = socket.socket()
846
 
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
847
 
        self._socket.bind(('localhost', 0))
848
 
        self._socket.listen(1)
849
 
        self.port = self._socket.getsockname()[1]
850
 
        self.stop_event = threading.Event()
851
 
 
852
 
    def run(self):
853
 
        s, _ = self._socket.accept()
854
 
        # now close the listen socket
855
 
        self._socket.close()
856
 
        try:
857
 
            self._callback(s, self.stop_event)
858
 
        except socket.error:
859
 
            pass #Ignore socket errors
860
 
        except Exception, x:
861
 
            # probably a failed test
862
 
            warning('Exception from within unit test server thread: %r' % x)
863
 
 
864
 
    def stop(self):
865
 
        self.stop_event.set()
866
 
        # use a timeout here, because if the test fails, the server thread may
867
 
        # never notice the stop_event.
868
 
        self.join(5.0)
869
 
 
870
 
 
871
 
class SFTPServer(Server):
872
 
    """Common code for SFTP server facilities."""
873
 
 
874
 
    def __init__(self):
875
 
        self._original_vendor = None
876
 
        self._homedir = None
877
 
        self._server_homedir = None
878
 
        self._listener = None
879
 
        self._root = None
880
 
        self._vendor = 'none'
881
 
        # sftp server logs
882
 
        self.logs = []
883
 
 
884
 
    def _get_sftp_url(self, path):
885
 
        """Calculate an sftp url to this server for path."""
886
 
        return 'sftp://foo:bar@localhost:%d/%s' % (self._listener.port, path)
887
 
 
888
 
    def log(self, message):
889
 
        """StubServer uses this to log when a new server is created."""
890
 
        self.logs.append(message)
891
 
 
892
 
    def _run_server(self, s, stop_event):
893
 
        ssh_server = paramiko.Transport(s)
894
 
        key_file = os.path.join(self._homedir, 'test_rsa.key')
895
 
        file(key_file, 'w').write(STUB_SERVER_KEY)
896
 
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
897
 
        ssh_server.add_server_key(host_key)
898
 
        server = StubServer(self)
899
 
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer,
900
 
                                         StubSFTPServer, root=self._root,
901
 
                                         home=self._server_homedir)
902
 
        event = threading.Event()
903
 
        ssh_server.start_server(event, server)
904
 
        event.wait(5.0)
905
 
        stop_event.wait(30.0)
906
 
    
907
 
    def setUp(self):
908
 
        global _ssh_vendor
909
 
        self._original_vendor = _ssh_vendor
910
 
        _ssh_vendor = self._vendor
911
 
        self._homedir = os.getcwdu()
912
 
        if self._server_homedir is None:
913
 
            self._server_homedir = self._homedir
914
 
        self._root = '/'
915
 
        # FIXME WINDOWS: _root should be _server_homedir[0]:/
916
 
        self._listener = SingleListener(self._run_server)
917
 
        self._listener.setDaemon(True)
918
 
        self._listener.start()
919
 
 
920
 
    def tearDown(self):
921
 
        """See bzrlib.transport.Server.tearDown."""
922
 
        global _ssh_vendor
923
 
        self._listener.stop()
924
 
        _ssh_vendor = self._original_vendor
925
 
 
926
 
 
927
 
class SFTPFullAbsoluteServer(SFTPServer):
928
 
    """A test server for sftp transports, using absolute urls and ssh."""
929
 
 
930
 
    def get_url(self):
931
 
        """See bzrlib.transport.Server.get_url."""
932
 
        return self._get_sftp_url(urlescape(self._homedir[1:]))
933
 
 
934
 
 
935
 
class SFTPServerWithoutSSH(SFTPServer):
936
 
    """An SFTP server that uses a simple TCP socket pair rather than SSH."""
937
 
 
938
 
    def __init__(self):
939
 
        super(SFTPServerWithoutSSH, self).__init__()
940
 
        self._vendor = 'loopback'
941
 
 
942
 
    def _run_server(self, sock, stop_event):
943
 
        class FakeChannel(object):
944
 
            def get_transport(self):
945
 
                return self
946
 
            def get_log_channel(self):
947
 
                return 'paramiko'
948
 
            def get_name(self):
949
 
                return '1'
950
 
            def get_hexdump(self):
951
 
                return False
952
 
 
953
 
        server = paramiko.SFTPServer(FakeChannel(), 'sftp', StubServer(self), StubSFTPServer,
954
 
                                     root=self._root, home=self._server_homedir)
955
 
        server.start_subsystem('sftp', None, sock)
956
 
        server.finish_subsystem()
957
 
 
958
 
 
959
 
class SFTPAbsoluteServer(SFTPServerWithoutSSH):
960
 
    """A test server for sftp transports, using absolute urls."""
961
 
 
962
 
    def get_url(self):
963
 
        """See bzrlib.transport.Server.get_url."""
964
 
        return self._get_sftp_url(urlescape(self._homedir[1:]))
965
 
 
966
 
 
967
 
class SFTPHomeDirServer(SFTPServerWithoutSSH):
968
 
    """A test server for sftp transports, using homedir relative urls."""
969
 
 
970
 
    def get_url(self):
971
 
        """See bzrlib.transport.Server.get_url."""
972
 
        return self._get_sftp_url("~/")
973
 
 
974
 
 
975
 
class SFTPSiblingAbsoluteServer(SFTPAbsoluteServer):
976
 
    """A test servere for sftp transports, using absolute urls to non-home."""
977
 
 
978
 
    def setUp(self):
979
 
        self._server_homedir = '/dev/noone/runs/tests/here'
980
 
        super(SFTPSiblingAbsoluteServer, self).setUp()
981
 
 
982
 
 
983
 
def get_test_permutations():
984
 
    """Return the permutations to be used in testing."""
985
 
    return [(SFTPTransport, SFTPAbsoluteServer),
986
 
            (SFTPTransport, SFTPHomeDirServer),
987
 
            (SFTPTransport, SFTPSiblingAbsoluteServer),
988
 
            ]