~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/sftp.py

Merge from integration.

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
import stat
24
24
import sys
25
25
import urllib
 
26
import urlparse
26
27
import time
27
28
import random
 
29
import subprocess
 
30
import weakref
28
31
 
29
32
from bzrlib.errors import (FileExists, 
30
 
                           TransportNotPossible, NoSuchFile, NonRelativePath,
 
33
                           TransportNotPossible, NoSuchFile, PathNotChild,
31
34
                           TransportError,
32
35
                           LockError)
33
36
from bzrlib.config import config_dir
34
37
from bzrlib.trace import mutter, warning, error
35
38
from bzrlib.transport import Transport, register_transport
 
39
import bzrlib.ui
36
40
 
37
41
try:
38
42
    import paramiko
45
49
                               CMD_HANDLE, CMD_OPEN)
46
50
    from paramiko.sftp_attr import SFTPAttributes
47
51
    from paramiko.sftp_file import SFTPFile
 
52
    from paramiko.sftp_client import SFTPClient
 
53
 
 
54
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
 
55
 
 
56
 
 
57
_close_fds = True
 
58
if sys.platform == 'win32':
 
59
    # close_fds not supported on win32
 
60
    _close_fds = False
 
61
 
 
62
_ssh_vendor = None
 
63
def _get_ssh_vendor():
 
64
    """Find out what version of SSH is on the system."""
 
65
    global _ssh_vendor
 
66
    if _ssh_vendor is not None:
 
67
        return _ssh_vendor
 
68
 
 
69
    _ssh_vendor = 'none'
 
70
 
 
71
    try:
 
72
        p = subprocess.Popen(['ssh', '-V'],
 
73
                             close_fds=_close_fds,
 
74
                             stdin=subprocess.PIPE,
 
75
                             stdout=subprocess.PIPE,
 
76
                             stderr=subprocess.PIPE)
 
77
        returncode = p.returncode
 
78
        stdout, stderr = p.communicate()
 
79
    except OSError:
 
80
        returncode = -1
 
81
        stdout = stderr = ''
 
82
    if 'OpenSSH' in stderr:
 
83
        mutter('ssh implementation is OpenSSH')
 
84
        _ssh_vendor = 'openssh'
 
85
    elif 'SSH Secure Shell' in stderr:
 
86
        mutter('ssh implementation is SSH Corp.')
 
87
        _ssh_vendor = 'ssh'
 
88
 
 
89
    if _ssh_vendor != 'none':
 
90
        return _ssh_vendor
 
91
 
 
92
    # XXX: 20051123 jamesh
 
93
    # A check for putty's plink or lsh would go here.
 
94
 
 
95
    mutter('falling back to paramiko implementation')
 
96
    return _ssh_vendor
 
97
 
 
98
 
 
99
class SFTPSubprocess:
 
100
    """A socket-like object that talks to an ssh subprocess via pipes."""
 
101
    def __init__(self, hostname, port=None, user=None):
 
102
        vendor = _get_ssh_vendor()
 
103
        assert vendor in ['openssh', 'ssh']
 
104
        if vendor == 'openssh':
 
105
            args = ['ssh',
 
106
                    '-oForwardX11=no', '-oForwardAgent=no',
 
107
                    '-oClearAllForwardings=yes', '-oProtocol=2',
 
108
                    '-oNoHostAuthenticationForLocalhost=yes']
 
109
            if port is not None:
 
110
                args.extend(['-p', str(port)])
 
111
            if user is not None:
 
112
                args.extend(['-l', user])
 
113
            args.extend(['-s', hostname, 'sftp'])
 
114
        elif vendor == 'ssh':
 
115
            args = ['ssh', '-x']
 
116
            if port is not None:
 
117
                args.extend(['-p', str(port)])
 
118
            if user is not None:
 
119
                args.extend(['-l', user])
 
120
            args.extend(['-s', 'sftp', hostname])
 
121
 
 
122
        self.proc = subprocess.Popen(args, close_fds=_close_fds,
 
123
                                     stdin=subprocess.PIPE,
 
124
                                     stdout=subprocess.PIPE)
 
125
 
 
126
    def send(self, data):
 
127
        return os.write(self.proc.stdin.fileno(), data)
 
128
 
 
129
    def recv(self, count):
 
130
        return os.read(self.proc.stdout.fileno(), count)
 
131
 
 
132
    def close(self):
 
133
        self.proc.stdin.close()
 
134
        self.proc.stdout.close()
 
135
        self.proc.wait()
48
136
 
49
137
 
50
138
SYSTEM_HOSTKEYS = {}
51
139
BZR_HOSTKEYS = {}
52
140
 
 
141
# This is a weakref dictionary, so that we can reuse connections
 
142
# that are still active. Long term, it might be nice to have some
 
143
# sort of expiration policy, such as disconnect if inactive for
 
144
# X seconds. But that requires a lot more fanciness.
 
145
_connected_hosts = weakref.WeakValueDictionary()
 
146
 
53
147
def load_host_keys():
54
148
    """
55
149
    Load system host keys (probably doesn't work on windows) and any
86
180
        mutter('failed to save bzr host keys: ' + str(e))
87
181
 
88
182
 
89
 
 
90
 
class SFTPTransportError (TransportError):
91
 
    pass
92
 
 
93
183
class SFTPLock(object):
94
184
    """This fakes a lock in a remote location."""
95
185
    __slots__ = ['path', 'lock_path', 'lock_file', 'transport']
126
216
    """
127
217
    Transport implementation for SFTP access.
128
218
    """
 
219
    _do_prefetch = False # Right now Paramiko's prefetch support causes things to hang
129
220
 
130
 
    _url_matcher = re.compile(r'^sftp://([^:@]*(:[^@]*)?@)?(.*?)(:[^/]+)?(/.*)?$')
131
 
    
132
221
    def __init__(self, base, clone_from=None):
133
222
        assert base.startswith('sftp://')
 
223
        self._parse_url(base)
 
224
        base = self._unparse_url()
134
225
        super(SFTPTransport, self).__init__(base)
135
 
        self._parse_url(base)
136
226
        if clone_from is None:
137
227
            self._sftp_connect()
138
228
        else:
192
282
        return path
193
283
 
194
284
    def relpath(self, abspath):
195
 
        # FIXME: this is identical to HttpTransport -- share it
196
 
        m = self._url_matcher.match(abspath)
197
 
        path = m.group(5)
198
 
        if not path.startswith(self._path):
199
 
            raise NonRelativePath('path %r is not under base URL %r'
200
 
                           % (abspath, self.base))
201
 
        pl = len(self.base)
202
 
        return abspath[pl:].lstrip('/')
 
285
        username, password, host, port, path = self._split_url(abspath)
 
286
        error = []
 
287
        if (username != self._username):
 
288
            error.append('username mismatch')
 
289
        if (host != self._host):
 
290
            error.append('host mismatch')
 
291
        if (port != self._port):
 
292
            error.append('port mismatch')
 
293
        if (not path.startswith(self._path)):
 
294
            error.append('path mismatch')
 
295
        if error:
 
296
            extra = ': ' + ', '.join(error)
 
297
            raise PathNotChild(abspath, self.base, extra=extra)
 
298
        pl = len(self._path)
 
299
        return path[pl:].lstrip('/')
203
300
 
204
301
    def has(self, relpath):
205
302
        """
220
317
        try:
221
318
            path = self._abspath(relpath)
222
319
            f = self._sftp.file(path)
223
 
            try:
 
320
            if self._do_prefetch and hasattr(f, 'prefetch'):
224
321
                f.prefetch()
225
 
            except AttributeError:
226
 
                # only works on paramiko 1.5.1 or greater
227
 
                pass
228
322
            return f
229
 
        except (IOError, paramiko.SSHException), x:
230
 
            raise NoSuchFile('Error retrieving %s: %s' % (path, str(x)), x)
 
323
        except (IOError, paramiko.SSHException), e:
 
324
            self._translate_io_exception(e, path, ': error retrieving')
231
325
 
232
326
    def get_partial(self, relpath, start, length=None):
233
327
        """
244
338
        # TODO: implement get_partial_multi to help with knit support
245
339
        f = self.get(relpath)
246
340
        f.seek(start)
247
 
        try:
 
341
        if self._do_prefetch and hasattr(f, 'prefetch'):
248
342
            f.prefetch()
249
 
        except AttributeError:
250
 
            # only works on paramiko 1.5.1 or greater
251
 
            pass
252
343
        return f
253
344
 
254
345
    def put(self, relpath, f):
267
358
        try:
268
359
            try:
269
360
                self._pump(f, fout)
270
 
            except IOError, e:
271
 
                self._translate_io_exception(e, relpath)
272
 
            except paramiko.SSHException, x:
273
 
                raise SFTPTransportError('Unable to write file %r' % (path,), x)
 
361
            except (paramiko.SSHException, IOError), e:
 
362
                self._translate_io_exception(e, relpath, ': unable to write')
274
363
        except Exception, e:
275
364
            # If we fail, try to clean up the temporary file
276
365
            # before we throw the exception
290
379
                file_existed = True
291
380
            except:
292
381
                file_existed = False
 
382
            success = False
293
383
            try:
294
 
                self._sftp.rename(tmp_abspath, final_path)
295
 
            except IOError, e:
296
 
                self._translate_io_exception(e, relpath)
297
 
            except paramiko.SSHException, x:
298
 
                raise SFTPTransportError('Unable to rename into file %r' 
299
 
                                          % (path,), x)
300
 
            if file_existed:
301
 
                self._sftp.unlink(tmp_safety)
 
384
                try:
 
385
                    self._sftp.rename(tmp_abspath, final_path)
 
386
                except (paramiko.SSHException, IOError), e:
 
387
                    self._translate_io_exception(e, relpath, ': unable to rename')
 
388
                else:
 
389
                    success = True
 
390
            finally:
 
391
                if file_existed:
 
392
                    if success:
 
393
                        self._sftp.unlink(tmp_safety)
 
394
                    else:
 
395
                        self._sftp.rename(tmp_safety, final_path)
302
396
 
303
397
    def iter_files_recursive(self):
304
398
        """Walk the relative paths of all files in this transport."""
317
411
        try:
318
412
            path = self._abspath(relpath)
319
413
            self._sftp.mkdir(path)
320
 
        except IOError, e:
321
 
            self._translate_io_exception(e, relpath)
322
 
        except (IOError, paramiko.SSHException), x:
323
 
            raise SFTPTransportError('Unable to mkdir %r' % (path,), x)
324
 
 
325
 
    def _translate_io_exception(self, e, relpath):
 
414
        except (paramiko.SSHException, IOError), e:
 
415
            self._translate_io_exception(e, relpath, ': unable to mkdir',
 
416
                failure_exc=FileExists)
 
417
 
 
418
    def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
 
419
        """Translate a paramiko or IOError into a friendlier exception.
 
420
 
 
421
        :param e: The original exception
 
422
        :param path: The path in question when the error is raised
 
423
        :param more_info: Extra information that can be included,
 
424
                          such as what was going on
 
425
        :param failure_exc: Paramiko has the super fun ability to raise completely
 
426
                           opaque errors that just set "e.args = ('Failure',)" with
 
427
                           no more information.
 
428
                           This sometimes means FileExists, but it also sometimes
 
429
                           means NoSuchFile
 
430
        """
326
431
        # paramiko seems to generate detailless errors.
327
 
        if (e.errno == errno.ENOENT or
328
 
            e.args == ('No such file or directory',) or
329
 
            e.args == ('No such file',)):
330
 
            raise NoSuchFile(relpath)
331
 
        if (e.args == ('mkdir failed',)):
332
 
            raise FileExists(relpath)
333
 
        # strange but true, for the paramiko server.
334
 
        if (e.args == ('Failure',)):
335
 
            raise FileExists(relpath)
336
 
        raise
 
432
        self._translate_error(e, path, raise_generic=False)
 
433
        if hasattr(e, 'args'):
 
434
            if (e.args == ('No such file or directory',) or
 
435
                e.args == ('No such file',)):
 
436
                raise NoSuchFile(path, str(e) + more_info)
 
437
            if (e.args == ('mkdir failed',)):
 
438
                raise FileExists(path, str(e) + more_info)
 
439
            # strange but true, for the paramiko server.
 
440
            if (e.args == ('Failure',)):
 
441
                raise failure_exc(path, str(e) + more_info)
 
442
        raise e
337
443
 
338
444
    def append(self, relpath, f):
339
445
        """
344
450
            path = self._abspath(relpath)
345
451
            fout = self._sftp.file(path, 'ab')
346
452
            self._pump(f, fout)
347
 
        except (IOError, paramiko.SSHException), x:
348
 
            raise SFTPTransportError('Unable to append file %r' % (path,), x)
 
453
        except (IOError, paramiko.SSHException), e:
 
454
            self._translate_io_exception(e, relpath, ': unable to append')
349
455
 
350
456
    def copy(self, rel_from, rel_to):
351
457
        """Copy the item at rel_from to the location at rel_to"""
352
458
        path_from = self._abspath(rel_from)
353
459
        path_to = self._abspath(rel_to)
 
460
        self._copy_abspaths(path_from, path_to)
 
461
 
 
462
    def _copy_abspaths(self, path_from, path_to):
 
463
        """Copy files given an absolute path
 
464
 
 
465
        :param path_from: Path on remote server to read
 
466
        :param path_to: Path on remote server to write
 
467
        :return: None
 
468
 
 
469
        TODO: Should the destination location be atomically created?
 
470
              This has not been specified
 
471
        TODO: This should use some sort of remote copy, rather than
 
472
              pulling the data locally, and then writing it remotely
 
473
        """
354
474
        try:
355
475
            fin = self._sftp.file(path_from, 'rb')
356
476
            try:
362
482
                    fout.close()
363
483
            finally:
364
484
                fin.close()
365
 
        except (IOError, paramiko.SSHException), x:
366
 
            raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
 
485
        except (IOError, paramiko.SSHException), e:
 
486
            self._translate_io_exception(e, path_from, ': unable copy to: %r' % path_to)
 
487
 
 
488
    def copy_to(self, relpaths, other, pb=None):
 
489
        """Copy a set of entries from self into another Transport.
 
490
 
 
491
        :param relpaths: A list/generator of entries to be copied.
 
492
        """
 
493
        if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
 
494
            # Both from & to are on the same remote filesystem
 
495
            # We can use a remote copy, instead of pulling locally, and pushing
 
496
 
 
497
            total = self._get_total(relpaths)
 
498
            count = 0
 
499
            for path in relpaths:
 
500
                path_from = self._abspath(relpath)
 
501
                path_to = other._abspath(relpath)
 
502
                self._update_pb(pb, 'copy-to', count, total)
 
503
                self._copy_abspaths(path_from, path_to)
 
504
                count += 1
 
505
            return count
 
506
        else:
 
507
            return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
 
508
 
 
509
        # The dummy implementation just does a simple get + put
 
510
        def copy_entry(path):
 
511
            other.put(path, self.get(path))
 
512
 
 
513
        return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
367
514
 
368
515
    def move(self, rel_from, rel_to):
369
516
        """Move the item at rel_from to the location at rel_to"""
371
518
        path_to = self._abspath(rel_to)
372
519
        try:
373
520
            self._sftp.rename(path_from, path_to)
374
 
        except (IOError, paramiko.SSHException), x:
375
 
            raise SFTPTransportError('Unable to move %r to %r' % (path_from, path_to), x)
 
521
        except (IOError, paramiko.SSHException), e:
 
522
            self._translate_io_exception(e, path_from, ': unable to move to: %r' % path_to)
376
523
 
377
524
    def delete(self, relpath):
378
525
        """Delete the item at relpath"""
379
526
        path = self._abspath(relpath)
380
527
        try:
381
528
            self._sftp.remove(path)
382
 
        except (IOError, paramiko.SSHException), x:
383
 
            raise SFTPTransportError('Unable to delete %r' % (path,), x)
 
529
        except (IOError, paramiko.SSHException), e:
 
530
            self._translate_io_exception(e, path, ': unable to delete')
384
531
            
385
532
    def listable(self):
386
533
        """Return True if this store supports listing."""
394
541
        path = self._abspath(relpath)
395
542
        try:
396
543
            return self._sftp.listdir(path)
397
 
        except (IOError, paramiko.SSHException), x:
398
 
            raise SFTPTransportError('Unable to list folder %r' % (path,), x)
 
544
        except (IOError, paramiko.SSHException), e:
 
545
            self._translate_io_exception(e, path, ': failed to list_dir')
399
546
 
400
547
    def stat(self, relpath):
401
548
        """Return the stat information for a file."""
402
549
        path = self._abspath(relpath)
403
550
        try:
404
551
            return self._sftp.stat(path)
405
 
        except (IOError, paramiko.SSHException), x:
406
 
            raise SFTPTransportError('Unable to stat %r' % (path,), x)
 
552
        except (IOError, paramiko.SSHException), e:
 
553
            self._translate_io_exception(e, path, ': unable to stat')
407
554
 
408
555
    def lock_read(self, relpath):
409
556
        """
435
582
    def _unparse_url(self, path=None):
436
583
        if path is None:
437
584
            path = self._path
438
 
        host = self._host
439
 
        username = urllib.quote(self._username)
440
 
        if self._password:
441
 
            username += ':' + urllib.quote(self._password)
442
 
        if self._port != 22:
443
 
            host += ':%d' % self._port
444
 
        return 'sftp://%s@%s/%s' % (username, host, urllib.quote(path))
 
585
        path = urllib.quote(path)
 
586
        if path.startswith('/'):
 
587
            path = '/%2F' + path[1:]
 
588
        else:
 
589
            path = '/' + path
 
590
        netloc = urllib.quote(self._host)
 
591
        if self._username is not None:
 
592
            netloc = '%s@%s' % (urllib.quote(self._username), netloc)
 
593
        if self._port is not None:
 
594
            netloc = '%s:%d' % (netloc, self._port)
 
595
 
 
596
        return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
 
597
 
 
598
    def _split_url(self, url):
 
599
        if isinstance(url, unicode):
 
600
            url = url.encode('utf-8')
 
601
        (scheme, netloc, path, params,
 
602
         query, fragment) = urlparse.urlparse(url, allow_fragments=False)
 
603
        assert scheme == 'sftp'
 
604
        username = password = host = port = None
 
605
        if '@' in netloc:
 
606
            username, host = netloc.split('@', 1)
 
607
            if ':' in username:
 
608
                username, password = username.split(':', 1)
 
609
                password = urllib.unquote(password)
 
610
            username = urllib.unquote(username)
 
611
        else:
 
612
            host = netloc
 
613
 
 
614
        if ':' in host:
 
615
            host, port = host.rsplit(':', 1)
 
616
            try:
 
617
                port = int(port)
 
618
            except ValueError:
 
619
                # TODO: Should this be ConnectionError?
 
620
                raise TransportError('%s: invalid port number' % port)
 
621
        host = urllib.unquote(host)
 
622
 
 
623
        path = urllib.unquote(path)
 
624
 
 
625
        # the initial slash should be removed from the path, and treated
 
626
        # as a homedir relative path (the path begins with a double slash
 
627
        # if it is absolute).
 
628
        # see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
 
629
        if path.startswith('/'):
 
630
            path = path[1:]
 
631
 
 
632
        return (username, password, host, port, path)
445
633
 
446
634
    def _parse_url(self, url):
447
 
        assert url[:7] == 'sftp://'
448
 
        m = self._url_matcher.match(url)
449
 
        if m is None:
450
 
            raise SFTPTransportError('Unable to parse SFTP URL %r' % (url,))
451
 
        self._username, self._password, self._host, self._port, self._path = m.groups()
452
 
        if self._username is None:
453
 
            self._username = getpass.getuser()
454
 
        else:
455
 
            if self._password:
456
 
                # username field is 'user:pass@' in this case, and password is ':pass'
457
 
                username_len = len(self._username) - len(self._password) - 1
458
 
                self._username = urllib.unquote(self._username[:username_len])
459
 
                self._password = urllib.unquote(self._password[1:])
460
 
            else:
461
 
                self._username = urllib.unquote(self._username[:-1])
462
 
        if self._port is None:
463
 
            self._port = 22
464
 
        else:
465
 
            self._port = int(self._port[1:])
466
 
        if (self._path is None) or (self._path == ''):
467
 
            self._path = ''
468
 
        else:
469
 
            # remove leading '/'
470
 
            self._path = urllib.unquote(self._path[1:])
 
635
        (self._username, self._password,
 
636
         self._host, self._port, self._path) = self._split_url(url)
471
637
 
472
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 != 'none':
 
656
            sock = SFTPSubprocess(self._host, self._port, self._username)
 
657
            self._sftp = SFTPClient(sock)
 
658
        else:
 
659
            self._paramiko_connect()
 
660
 
 
661
        _connected_hosts[idx] = self._sftp
 
662
 
 
663
    def _paramiko_connect(self):
473
664
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
474
665
        
475
666
        load_host_keys()
476
 
        
 
667
 
477
668
        try:
478
 
            t = paramiko.Transport((self._host, self._port))
 
669
            t = paramiko.Transport((self._host, self._port or 22))
479
670
            t.start_client()
480
 
        except paramiko.SSHException:
481
 
            raise SFTPTransportError('Unable to reach SSH host %s:%d' % (self._host, self._port))
 
671
        except paramiko.SSHException, e:
 
672
            raise ConnectionError('Unable to reach SSH host %s:%d' %
 
673
                                  (self._host, self._port), e)
482
674
            
483
675
        server_key = t.get_remote_server_key()
484
676
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
500
692
        if server_key != our_server_key:
501
693
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
502
694
            filename2 = os.path.join(config_dir(), 'ssh_host_keys')
503
 
            raise SFTPTransportError('Host keys for %s do not match!  %s != %s' % \
 
695
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
504
696
                (self._host, our_server_key_hex, server_key_hex),
505
697
                ['Try editing %s or %s' % (filename1, filename2)])
506
698
 
507
 
        self._sftp_auth(t, self._username, self._host)
 
699
        self._sftp_auth(t)
508
700
        
509
701
        try:
510
702
            self._sftp = t.open_sftp_client()
511
 
        except paramiko.SSHException:
512
 
            raise BzrError('Unable to find path %s on SFTP server %s' % \
513
 
                (self._path, self._host))
514
 
 
515
 
    def _sftp_auth(self, transport, username, host):
516
 
        agent = paramiko.Agent()
517
 
        for key in agent.get_keys():
518
 
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
519
 
            try:
520
 
                transport.auth_publickey(self._username, key)
521
 
                return
522
 
            except paramiko.SSHException, e:
523
 
                pass
 
703
        except paramiko.SSHException, e:
 
704
            raise ConnectionError('Unable to start sftp client %s:%d' %
 
705
                                  (self._host, self._port), e)
 
706
 
 
707
    def _sftp_auth(self, transport):
 
708
        # paramiko requires a username, but it might be none if nothing was supplied
 
709
        # use the local username, just in case.
 
710
        # We don't override self._username, because if we aren't using paramiko,
 
711
        # the username might be specified in ~/.ssh/config and we don't want to
 
712
        # force it to something else
 
713
        # Also, it would mess up the self.relpath() functionality
 
714
        username = self._username or getpass.getuser()
 
715
 
 
716
        # Paramiko tries to open a socket.AF_UNIX in order to connect
 
717
        # to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
 
718
        # so we get an AttributeError exception. For now, just don't try to
 
719
        # connect to an agent if we are on win32
 
720
        if sys.platform != 'win32':
 
721
            agent = paramiko.Agent()
 
722
            for key in agent.get_keys():
 
723
                mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
 
724
                try:
 
725
                    transport.auth_publickey(username, key)
 
726
                    return
 
727
                except paramiko.SSHException, e:
 
728
                    pass
524
729
        
525
730
        # okay, try finding id_rsa or id_dss?  (posix only)
526
 
        if self._try_pkey_auth(transport, paramiko.RSAKey, 'id_rsa'):
527
 
            return
528
 
        if self._try_pkey_auth(transport, paramiko.DSSKey, 'id_dsa'):
529
 
            return
 
731
        if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
 
732
            return
 
733
        if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
 
734
            return
 
735
 
530
736
 
531
737
        if self._password:
532
738
            try:
533
 
                transport.auth_password(self._username, self._password)
 
739
                transport.auth_password(username, self._password)
534
740
                return
535
741
            except paramiko.SSHException, e:
536
742
                pass
537
743
 
 
744
            # FIXME: Don't keep a password held in memory if you can help it
 
745
            #self._password = None
 
746
 
538
747
        # give up and ask for a password
539
 
        # FIXME: shouldn't be implementing UI this deep into bzrlib
540
 
        enc = sys.stdout.encoding
541
 
        password = getpass.getpass('SSH %s@%s password: ' %
542
 
            (self._username.encode(enc, 'replace'), self._host.encode(enc, 'replace')))
 
748
        password = bzrlib.ui.ui_factory.get_password(
 
749
                prompt='SSH %(user)s@%(host)s password',
 
750
                user=username, host=self._host)
543
751
        try:
544
 
            transport.auth_password(self._username, password)
545
 
        except paramiko.SSHException:
546
 
            raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
547
 
                (self._username, self._host))
 
752
            transport.auth_password(username, password)
 
753
        except paramiko.SSHException, e:
 
754
            raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
 
755
                                  (username, self._host), e)
548
756
 
549
 
    def _try_pkey_auth(self, transport, pkey_class, filename):
 
757
    def _try_pkey_auth(self, transport, pkey_class, username, filename):
550
758
        filename = os.path.expanduser('~/.ssh/' + filename)
551
759
        try:
552
760
            key = pkey_class.from_private_key_file(filename)
553
 
            transport.auth_publickey(self._username, key)
 
761
            transport.auth_publickey(username, key)
554
762
            return True
555
763
        except paramiko.PasswordRequiredException:
556
 
            # FIXME: shouldn't be implementing UI this deep into bzrlib
557
 
            enc = sys.stdout.encoding
558
 
            password = getpass.getpass('SSH %s password: ' % 
559
 
                (os.path.basename(filename).encode(enc, 'replace'),))
 
764
            password = bzrlib.ui.ui_factory.get_password(
 
765
                    prompt='SSH %(filename)s password',
 
766
                    filename=filename)
560
767
            try:
561
768
                key = pkey_class.from_private_key_file(filename, password)
562
 
                transport.auth_publickey(self._username, key)
 
769
                transport.auth_publickey(username, key)
563
770
                return True
564
771
            except paramiko.SSHException:
565
772
                mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
582
789
 
583
790
        :param relpath: The relative path, where the file should be opened
584
791
        """
585
 
        path = self._abspath(relpath)
 
792
        path = self._sftp._adjust_cwd(self._abspath(relpath))
586
793
        attr = SFTPAttributes()
587
794
        mode = (SFTP_FLAG_WRITE | SFTP_FLAG_CREATE 
588
795
                | SFTP_FLAG_TRUNC | SFTP_FLAG_EXCL)
589
796
        try:
590
797
            t, msg = self._sftp._request(CMD_OPEN, path, mode, attr)
591
798
            if t != CMD_HANDLE:
592
 
                raise SFTPTransportError('Expected an SFTP handle')
 
799
                raise TransportError('Expected an SFTP handle')
593
800
            handle = msg.get_string()
594
801
            return SFTPFile(self._sftp, handle, 'w', -1)
595
 
        except IOError, e:
596
 
            self._translate_io_exception(e, relpath)
597
 
        except paramiko.SSHException, x:
598
 
            raise SFTPTransportError('Unable to open file %r' % (path,), x)
 
802
        except (paramiko.SSHException, IOError), e:
 
803
            self._translate_io_exception(e, relpath, ': unable to open',
 
804
                failure_exc=FileExists)
599
805