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
288
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('/')
289
username, password, host, port, path = self._split_url(abspath)
291
if (username != self._username):
292
error.append('username mismatch')
293
if (host != self._host):
294
error.append('host mismatch')
295
if (port != self._port):
296
error.append('port mismatch')
297
if (not path.startswith(self._path)):
298
error.append('path mismatch')
300
raise NonRelativePath('path %r is not under base URL %r: %s'
301
% (abspath, self.base, ', '.join(error)))
303
return path[pl:].lstrip('/')
204
305
def has(self, relpath):
290
385
file_existed = True
292
387
file_existed = False
294
self._sftp.rename(tmp_abspath, final_path)
296
self._translate_io_exception(e, relpath)
297
except paramiko.SSHException, x:
298
raise SFTPTransportError('Unable to rename into file %r'
301
self._sftp.unlink(tmp_safety)
391
self._sftp.rename(tmp_abspath, final_path)
393
self._translate_io_exception(e, relpath)
394
except paramiko.SSHException, x:
395
raise SFTPTransportError('Unable to rename into file %r' % (path,), x)
401
self._sftp.unlink(tmp_safety)
403
self._sftp.rename(tmp_safety, final_path)
303
405
def iter_files_recursive(self):
304
406
"""Walk the relative paths of all files in this transport."""
351
453
"""Copy the item at rel_from to the location at rel_to"""
352
454
path_from = self._abspath(rel_from)
353
455
path_to = self._abspath(rel_to)
456
self._copy_abspaths(path_from, path_to)
458
def _copy_abspaths(self, path_from, path_to):
459
"""Copy files given an absolute path
461
:param path_from: Path on remote server to read
462
:param path_to: Path on remote server to write
465
TODO: Should the destination location be atomically created?
466
This has not been specified
467
TODO: This should use some sort of remote copy, rather than
468
pulling the data locally, and then writing it remotely
355
471
fin = self._sftp.file(path_from, 'rb')
365
481
except (IOError, paramiko.SSHException), x:
366
482
raise SFTPTransportError('Unable to copy %r to %r' % (path_from, path_to), x)
484
def copy_to(self, relpaths, other, pb=None):
485
"""Copy a set of entries from self into another Transport.
487
:param relpaths: A list/generator of entries to be copied.
489
if isinstance(other, SFTPTransport) and other._sftp is self._sftp:
490
# Both from & to are on the same remote filesystem
491
# We can use a remote copy, instead of pulling locally, and pushing
493
total = self._get_total(relpaths)
495
for path in relpaths:
496
path_from = self._abspath(relpath)
497
path_to = other._abspath(relpath)
498
self._update_pb(pb, 'copy-to', count, total)
499
self._copy_abspaths(path_from, path_to)
503
return super(SFTPTransport, self).copy_to(relpaths, other, pb=pb)
505
# The dummy implementation just does a simple get + put
506
def copy_entry(path):
507
other.put(path, self.get(path))
509
return self._iterate_over(relpaths, copy_entry, pb, 'copy_to', expand=False)
368
511
def move(self, rel_from, rel_to):
369
512
"""Move the item at rel_from to the location at rel_to"""
370
513
path_from = self._abspath(rel_from)
435
578
def _unparse_url(self, path=None):
437
580
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))
581
path = urllib.quote(path)
582
if path.startswith('/'):
583
path = '/%2F' + path[1:]
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)
592
return urlparse.urlunparse(('sftp', netloc, path, '', '', ''))
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
602
username, host = netloc.split('@', 1)
604
username, password = username.split(':', 1)
605
password = urllib.unquote(password)
606
username = urllib.unquote(username)
611
host, port = host.rsplit(':', 1)
615
raise SFTPTransportError('%s: invalid port number' % port)
616
host = urllib.unquote(host)
618
path = urllib.unquote(path)
620
# the initial slash should be removed from the path, and treated
621
# as a homedir relative path (the path begins with a double slash
622
# if it is absolute).
623
# see draft-ietf-secsh-scp-sftp-ssh-uri-03.txt
624
if path.startswith('/'):
627
return (username, password, host, port, path)
446
629
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:])
630
(self._username, self._password,
631
self._host, self._port, self._path) = self._split_url(url)
472
633
def _sftp_connect(self):
634
"""Connect to the remote sftp server.
635
After this, self._sftp should have a valid connection (or
636
we raise an SFTPTransportError 'could not connect').
638
TODO: Raise a more reasonable ConnectionFailed exception
640
global _connected_hosts
642
idx = (self._host, self._port, self._username)
644
self._sftp = _connected_hosts[idx]
649
vendor = _get_ssh_vendor()
651
sock = SFTPSubprocess(self._host, self._port, self._username)
652
self._sftp = SFTPClient(sock)
654
self._paramiko_connect()
656
_connected_hosts[idx] = self._sftp
658
def _paramiko_connect(self):
473
659
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
478
664
t = paramiko.Transport((self._host, self._port))
512
698
raise BzrError('Unable to find path %s on SFTP server %s' % \
513
699
(self._path, self._host))
515
def _sftp_auth(self, transport, username, host):
701
def _sftp_auth(self, transport):
702
# paramiko requires a username, but it might be none if nothing was supplied
703
# use the local username, just in case.
704
# We don't override self._username, because if we aren't using paramiko,
705
# the username might be specified in ~/.ssh/config and we don't want to
706
# force it to something else
707
# Also, it would mess up the self.relpath() functionality
708
username = self._username or getpass.getuser()
516
710
agent = paramiko.Agent()
517
711
for key in agent.get_keys():
518
712
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
520
transport.auth_publickey(self._username, key)
714
transport.auth_publickey(username, key)
522
716
except paramiko.SSHException, e:
525
719
# 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'):
720
if self._try_pkey_auth(transport, paramiko.RSAKey, username, 'id_rsa'):
722
if self._try_pkey_auth(transport, paramiko.DSSKey, username, 'id_dsa'):
531
726
if self._password:
533
transport.auth_password(self._username, self._password)
728
transport.auth_password(username, self._password)
535
730
except paramiko.SSHException, e:
733
# FIXME: Don't keep a password held in memory if you can help it
734
#self._password = None
538
736
# 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')))
737
password = ui_factory.get_password(prompt='SSH %(user)s@%(host)s password',
738
user=username, host=self._host)
544
transport.auth_password(self._username, password)
740
transport.auth_password(username, password)
545
741
except paramiko.SSHException:
546
742
raise SFTPTransportError('Unable to authenticate to SSH host as %s@%s' % \
547
(self._username, self._host))
743
(username, self._host))
549
def _try_pkey_auth(self, transport, pkey_class, filename):
745
def _try_pkey_auth(self, transport, pkey_class, username, filename):
550
746
filename = os.path.expanduser('~/.ssh/' + filename)
552
748
key = pkey_class.from_private_key_file(filename)
553
transport.auth_publickey(self._username, key)
749
transport.auth_publickey(username, key)
555
751
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'),))
752
password = ui_factory.get_password(prompt='SSH %(filename)s password',
561
755
key = pkey_class.from_private_key_file(filename, password)
562
transport.auth_publickey(self._username, key)
756
transport.auth_publickey(username, key)
564
758
except paramiko.SSHException:
565
759
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))