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
54
if 'sftp' not in urlparse.uses_netloc: urlparse.uses_netloc.append('sftp')
58
if sys.platform == 'win32':
59
# close_fds not supported on win32
63
def _get_ssh_vendor():
64
"""Find out what version of SSH is on the system."""
66
if _ssh_vendor is not None:
72
p = subprocess.Popen(['ssh', '-V'],
74
stdin=subprocess.PIPE,
75
stdout=subprocess.PIPE,
76
stderr=subprocess.PIPE)
77
returncode = p.returncode
78
stdout, stderr = p.communicate()
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.')
89
if _ssh_vendor != 'none':
92
# XXX: 20051123 jamesh
93
# A check for putty's plink or lsh would go here.
95
mutter('falling back to paramiko implementation')
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':
106
'-oForwardX11=no', '-oForwardAgent=no',
107
'-oClearAllForwardings=yes', '-oProtocol=2',
108
'-oNoHostAuthenticationForLocalhost=yes']
110
args.extend(['-p', str(port)])
112
args.extend(['-l', user])
113
args.extend(['-s', hostname, 'sftp'])
114
elif vendor == 'ssh':
117
args.extend(['-p', str(port)])
119
args.extend(['-l', user])
120
args.extend(['-s', 'sftp', hostname])
122
self.proc = subprocess.Popen(args, close_fds=_close_fds,
123
stdin=subprocess.PIPE,
124
stdout=subprocess.PIPE)
126
def send(self, data):
127
return os.write(self.proc.stdin.fileno(), data)
129
def recv(self, count):
130
return os.read(self.proc.stdout.fileno(), count)
133
self.proc.stdin.close()
134
self.proc.stdout.close()
50
138
SYSTEM_HOSTKEYS = {}
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()
53
147
def load_host_keys():
55
149
Load system host keys (probably doesn't work on windows) and any
194
284
def relpath(self, abspath):
195
# FIXME: this is identical to HttpTransport -- share it
196
m = self._url_matcher.match(abspath)
198
if not path.startswith(self._path):
199
raise NonRelativePath('path %r is not under base URL %r'
200
% (abspath, self.base))
202
return abspath[pl:].lstrip('/')
285
username, password, host, port, path = self._split_url(abspath)
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')
296
extra = ': ' + ', '.join(error)
297
raise PathNotChild(abspath, self.base, extra=extra)
299
return path[pl:].lstrip('/')
204
301
def has(self, relpath):
318
412
path = self._abspath(relpath)
319
413
self._sftp.mkdir(path)
321
self._translate_io_exception(e, relpath)
322
except (IOError, paramiko.SSHException), x:
323
raise SFTPTransportError('Unable to mkdir %r' % (path,), x)
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)
418
def _translate_io_exception(self, e, path, more_info='', failure_exc=NoSuchFile):
419
"""Translate a paramiko or IOError into a friendlier exception.
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
428
This sometimes means FileExists, but it also sometimes
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)
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)
338
444
def append(self, relpath, f):
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')
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)
462
def _copy_abspaths(self, path_from, path_to):
463
"""Copy files given an absolute path
465
:param path_from: Path on remote server to read
466
:param path_to: Path on remote server to write
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
355
475
fin = self._sftp.file(path_from, 'rb')
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)
488
def copy_to(self, relpaths, other, pb=None):
489
"""Copy a set of entries from self into another Transport.
491
:param relpaths: A list/generator of entries to be copied.
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
497
total = self._get_total(relpaths)
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)
507
return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
509
# The dummy implementation just does a simple get + put
510
def copy_entry(path):
511
other.put(path, self.get(path))
513
return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
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)
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)
377
524
def delete(self, relpath):
378
525
"""Delete the item at relpath"""
379
526
path = self._abspath(relpath)
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')
385
532
def listable(self):
386
533
"""Return True if this store supports listing."""
394
541
path = self._abspath(relpath)
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')
400
547
def stat(self, relpath):
401
548
"""Return the stat information for a file."""
402
549
path = self._abspath(relpath)
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')
408
555
def lock_read(self, relpath):
435
582
def _unparse_url(self, path=None):
437
584
path = self._path
439
username = urllib.quote(self._username)
441
username += ':' + urllib.quote(self._password)
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:]
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)
596
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
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
606
username, host = netloc.split('@', 1)
608
username, password = username.split(':', 1)
609
password = urllib.unquote(password)
610
username = urllib.unquote(username)
615
host, port = host.rsplit(':', 1)
619
# TODO: Should this be ConnectionError?
620
raise TransportError('%s: invalid port number' % port)
621
host = urllib.unquote(host)
623
path = urllib.unquote(path)
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('/'):
632
return (username, password, host, port, path)
446
634
def _parse_url(self, url):
447
assert url[:7] == 'sftp://'
448
m = self._url_matcher.match(url)
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()
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:])
461
self._username = urllib.unquote(self._username[:-1])
462
if self._port is None:
465
self._port = int(self._port[1:])
466
if (self._path is None) or (self._path == ''):
470
self._path = urllib.unquote(self._path[1:])
635
(self._username, self._password,
636
self._host, self._port, self._path) = self._split_url(url)
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').
643
TODO: Raise a more reasonable ConnectionFailed exception
645
global _connected_hosts
647
idx = (self._host, self._port, self._username)
649
self._sftp = _connected_hosts[idx]
654
vendor = _get_ssh_vendor()
656
sock = SFTPSubprocess(self._host, self._port, self._username)
657
self._sftp = SFTPClient(sock)
659
self._paramiko_connect()
661
_connected_hosts[idx] = self._sftp
663
def _paramiko_connect(self):
473
664
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
478
t = paramiko.Transport((self._host, self._port))
669
t = paramiko.Transport((self._host, self._port or 22))
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)
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)])
507
self._sftp_auth(t, self._username, self._host)
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))
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()))
520
transport.auth_publickey(self._username, key)
522
except paramiko.SSHException, e:
703
except paramiko.SSHException, e:
704
raise ConnectionError('Unable to start sftp client %s:%d' %
705
(self._host, self._port), e)
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()
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()))
725
transport.auth_publickey(username, key)
727
except paramiko.SSHException, e:
525
730
# okay, try finding id_rsa or id_dss? (posix only)
526
if self._try_pkey_auth(transport, paramiko.RSAKey, 'id_rsa'):
528
if self._try_pkey_auth(transport, paramiko.DSSKey, 'id_dsa'):
731
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
733
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
531
737
if self._password:
533
transport.auth_password(self._username, self._password)
739
transport.auth_password(username, self._password)
535
741
except paramiko.SSHException, e:
744
# FIXME: Don't keep a password held in memory if you can help it
745
#self._password = None
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)
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)
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)
552
760
key = pkey_class.from_private_key_file(filename)
553
transport.auth_publickey(self._username, key)
761
transport.auth_publickey(username, key)
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',
561
768
key = pkey_class.from_private_key_file(filename, password)
562
transport.auth_publickey(self._username, key)
769
transport.auth_publickey(username, key)
564
771
except paramiko.SSHException:
565
772
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
583
790
:param relpath: The relative path, where the file should be opened
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)
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)
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)