14
14
# You should have received a copy of the GNU General Public License
15
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
18
"""Foundation SSH support for SFTP and smart server."""
27
from bzrlib.config import config_dir, ensure_config_dir_exists
28
from bzrlib.errors import (ConnectionError,
30
SocketConnectionError,
36
from bzrlib.osutils import pathjoin
37
from bzrlib.trace import mutter, warning
113
114
stdout = stderr = ''
114
115
return stdout + stderr
116
def _get_vendor_by_version_string(self, version, progname):
117
def _get_vendor_by_version_string(self, version):
117
118
"""Return the vendor or None based on output from the subprocess.
119
120
:param version: The output of 'ssh -V' like command.
120
:param args: Command line that was run.
123
123
if 'OpenSSH' in version:
124
trace.mutter('ssh implementation is OpenSSH')
124
mutter('ssh implementation is OpenSSH')
125
125
vendor = OpenSSHSubprocessVendor()
126
126
elif 'SSH Secure Shell' in version:
127
trace.mutter('ssh implementation is SSH Corp.')
127
mutter('ssh implementation is SSH Corp.')
128
128
vendor = SSHCorpSubprocessVendor()
129
elif 'lsh' in version:
130
trace.mutter('ssh implementation is GNU lsh.')
131
vendor = LSHSubprocessVendor()
132
# As plink user prompts are not handled currently, don't auto-detect
133
# it by inspection below, but keep this vendor detection for if a path
134
# is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
135
elif 'plink' in version and progname == 'plink':
136
# Checking if "plink" was the executed argument as Windows
137
# sometimes reports 'ssh -V' incorrectly with 'plink' in its
138
# version. See https://bugs.launchpad.net/bzr/+bug/107155
139
trace.mutter("ssh implementation is Putty's plink.")
129
elif 'plink' in version:
130
mutter("ssh implementation is Putty's plink.")
140
131
vendor = PLinkSubprocessVendor()
143
134
def _get_vendor_by_inspection(self):
144
135
"""Return the vendor or None by checking for known SSH implementations."""
145
version = self._get_ssh_version_string(['ssh', '-V'])
146
return self._get_vendor_by_version_string(version, "ssh")
148
def _get_vendor_from_path(self, path):
149
"""Return the vendor or None using the program at the given path"""
150
version = self._get_ssh_version_string([path, '-V'])
151
return self._get_vendor_by_version_string(version,
152
os.path.splitext(os.path.basename(path))[0])
136
for args in [['ssh', '-V'], ['plink', '-V']]:
137
version = self._get_ssh_version_string(args)
138
vendor = self._get_vendor_by_version_string(version)
139
if vendor is not None:
154
143
def get_vendor(self, environment=None):
155
144
"""Find out what version of SSH is on the system.
176
165
register_ssh_vendor = _ssh_vendor_manager.register_vendor
179
def _ignore_signals():
168
def _ignore_sigint():
180
169
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
181
170
# doesn't handle it itself.
182
171
# <https://launchpad.net/products/bzr/+bug/41433/+index>
184
173
signal.signal(signal.SIGINT, signal.SIG_IGN)
185
# GZ 2010-02-19: Perhaps make this check if breakin is installed instead
186
if signal.getsignal(signal.SIGQUIT) != signal.SIG_DFL:
187
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
190
class SocketAsChannelAdapter(object):
176
class LoopbackSFTP(object):
191
177
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
193
179
def __init__(self, sock):
194
180
self.__socket = sock
197
return "bzr SocketAsChannelAdapter"
199
182
def send(self, data):
200
183
return self.__socket.send(data)
202
185
def recv(self, n):
204
return self.__socket.recv(n)
205
except socket.error, e:
206
if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
208
# Connection has closed. Paramiko expects an empty string in
209
# this case, not an exception.
186
return self.__socket.recv(n)
213
188
def recv_ready(self):
214
# TODO: jam 20051215 this function is necessary to support the
215
# pipelined() function. In reality, it probably should use
216
# poll() or select() to actually return if there is data
217
# available, otherwise we probably don't get any benefit
253
225
This just unifies all the locations that try to raise ConnectionError,
254
226
so that they format things properly.
256
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
257
orig_error=orig_error)
228
raise SocketConnectionError(host=host, port=port, msg=msg,
229
orig_error=orig_error)
260
232
class LoopbackVendor(SSHVendor):
261
233
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
263
235
def connect_sftp(self, username, password, host, port):
264
236
sock = socket.socket()
266
238
sock.connect((host, port))
267
239
except socket.error, e:
268
240
self._raise_connection_error(host, port=port, orig_error=e)
269
return SFTPClient(SocketAsChannelAdapter(sock))
241
return SFTPClient(LoopbackSFTP(sock))
271
243
register_ssh_vendor('loopback', LoopbackVendor())
246
class _ParamikoSSHConnection(object):
247
def __init__(self, channel):
248
self.channel = channel
250
def get_filelike_channels(self):
251
return self.channel.makefile('rb'), self.channel.makefile('wb')
254
return self.channel.close()
274
257
class ParamikoVendor(SSHVendor):
275
258
"""Vendor that uses paramiko."""
277
260
def _connect(self, username, password, host, port):
278
261
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
286
269
except (paramiko.SSHException, socket.error), e:
287
270
self._raise_connection_error(host, port=port, orig_error=e)
289
272
server_key = t.get_remote_server_key()
290
273
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
291
274
keytype = server_key.get_name()
292
275
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
293
276
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
294
our_server_key_hex = paramiko.util.hexify(
295
our_server_key.get_fingerprint())
277
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
296
278
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
297
279
our_server_key = BZR_HOSTKEYS[host][keytype]
298
our_server_key_hex = paramiko.util.hexify(
299
our_server_key.get_fingerprint())
280
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
301
trace.warning('Adding %s host key for %s: %s'
302
% (keytype, host, server_key_hex))
282
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
303
283
add = getattr(BZR_HOSTKEYS, 'add', None)
304
284
if add is not None: # paramiko >= 1.X.X
305
285
BZR_HOSTKEYS.add(host, keytype, server_key)
307
287
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
308
288
our_server_key = server_key
309
our_server_key_hex = paramiko.util.hexify(
310
our_server_key.get_fingerprint())
289
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
312
291
if server_key != our_server_key:
313
292
filename1 = os.path.expanduser('~/.ssh/known_hosts')
314
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
315
raise errors.TransportError(
316
'Host keys for %s do not match! %s != %s' %
293
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
294
raise TransportError('Host keys for %s do not match! %s != %s' % \
317
295
(host, our_server_key_hex, server_key_hex),
318
296
['Try editing %s or %s' % (filename1, filename2)])
320
_paramiko_auth(username, password, host, port, t)
298
_paramiko_auth(username, password, host, t)
323
301
def connect_sftp(self, username, password, host, port):
324
302
t = self._connect(username, password, host, port)
339
317
self._raise_connection_error(host, port=port, orig_error=e,
340
318
msg='Unable to invoke remote bzr')
342
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
343
320
if paramiko is not None:
344
321
vendor = ParamikoVendor()
345
322
register_ssh_vendor('paramiko', vendor)
346
323
register_ssh_vendor('none', vendor)
347
324
register_default_ssh_vendor(vendor)
348
_ssh_connection_errors += (paramiko.SSHException,)
352
328
class SubprocessVendor(SSHVendor):
353
329
"""Abstract base class for vendors that use pipes to a subprocess."""
355
331
def _connect(self, argv):
356
# Attempt to make a socketpair to use as stdin/stdout for the SSH
357
# subprocess. We prefer sockets to pipes because they support
358
# non-blocking short reads, allowing us to optimistically read 64k (or
361
my_sock, subproc_sock = socket.socketpair()
362
osutils.set_fd_cloexec(my_sock)
363
except (AttributeError, socket.error):
364
# This platform doesn't support socketpair(), so just use ordinary
366
stdin = stdout = subprocess.PIPE
367
my_sock, subproc_sock = None, None
369
stdin = stdout = subproc_sock
370
proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
332
proc = subprocess.Popen(argv,
333
stdin=subprocess.PIPE,
334
stdout=subprocess.PIPE,
371
335
**os_specific_subprocess_params())
372
if subproc_sock is not None:
374
return SSHSubprocessConnection(proc, sock=my_sock)
336
return SSHSubprocess(proc)
376
338
def connect_sftp(self, username, password, host, port):
378
340
argv = self._get_vendor_specific_argv(username, host, port,
379
341
subsystem='sftp')
380
342
sock = self._connect(argv)
381
return SFTPClient(SocketAsChannelAdapter(sock))
382
except _ssh_connection_errors, e:
343
return SFTPClient(sock)
344
except (EOFError, paramiko.SSHException), e:
345
self._raise_connection_error(host, port=port, orig_error=e)
346
except (OSError, IOError), e:
347
# If the machine is fast enough, ssh can actually exit
348
# before we try and send it the sftp request, which
349
# raises a Broken Pipe
350
if e.errno not in (errno.EPIPE,):
383
352
self._raise_connection_error(host, port=port, orig_error=e)
385
354
def connect_ssh(self, username, password, host, port, command):
387
356
argv = self._get_vendor_specific_argv(username, host, port,
389
358
return self._connect(argv)
390
except _ssh_connection_errors, e:
359
except (EOFError), e:
360
self._raise_connection_error(host, port=port, orig_error=e)
361
except (OSError, IOError), e:
362
# If the machine is fast enough, ssh can actually exit
363
# before we try and send it the sftp request, which
364
# raises a Broken Pipe
365
if e.errno not in (errno.EPIPE,):
391
367
self._raise_connection_error(host, port=port, orig_error=e)
393
369
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
395
371
"""Returns the argument list to run the subprocess with.
397
373
Exactly one of 'subsystem' and 'command' must be specified.
399
375
raise NotImplementedError(self._get_vendor_specific_argv)
402
378
class OpenSSHSubprocessVendor(SubprocessVendor):
403
379
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
405
executable_path = 'ssh'
407
381
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
409
args = [self.executable_path,
383
assert subsystem is not None or command is not None, (
384
'Must specify a command or subsystem')
385
if subsystem is not None:
386
assert command is None, (
387
'subsystem and command are mutually exclusive')
410
389
'-oForwardX11=no', '-oForwardAgent=no',
411
'-oClearAllForwardings=yes',
390
'-oClearAllForwardings=yes', '-oProtocol=2',
412
391
'-oNoHostAuthenticationForLocalhost=yes']
413
392
if port is not None:
414
393
args.extend(['-p', str(port)])
441
423
args.extend([host] + command)
444
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
447
class LSHSubprocessVendor(SubprocessVendor):
448
"""SSH vendor that uses the 'lsh' executable from GNU"""
450
executable_path = 'lsh'
452
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
454
args = [self.executable_path]
456
args.extend(['-p', str(port)])
457
if username is not None:
458
args.extend(['-l', username])
459
if subsystem is not None:
460
args.extend(['--subsystem', subsystem, host])
462
args.extend([host] + command)
465
register_ssh_vendor('lsh', LSHSubprocessVendor())
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
468
429
class PLinkSubprocessVendor(SubprocessVendor):
469
430
"""SSH vendor that uses the 'plink' executable from Putty."""
471
executable_path = 'plink'
473
432
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
475
args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
434
assert subsystem is not None or command is not None, (
435
'Must specify a command or subsystem')
436
if subsystem is not None:
437
assert command is None, (
438
'subsystem and command are mutually exclusive')
439
args = ['plink', '-x', '-a', '-ssh', '-2']
476
440
if port is not None:
477
441
args.extend(['-P', str(port)])
478
442
if username is not None:
486
450
register_ssh_vendor('plink', PLinkSubprocessVendor())
489
def _paramiko_auth(username, password, host, port, paramiko_transport):
490
auth = config.AuthenticationConfig()
491
# paramiko requires a username, but it might be none if nothing was
492
# supplied. If so, use the local username.
494
username = auth.get_user('ssh', host, port=port,
495
default=getpass.getuser())
453
def _paramiko_auth(username, password, host, paramiko_transport):
454
# paramiko requires a username, but it might be none if nothing was supplied
455
# use the local username, just in case.
456
# We don't override username, because if we aren't using paramiko,
457
# the username might be specified in ~/.ssh/config and we don't want to
458
# force it to something else
459
# Also, it would mess up the self.relpath() functionality
460
username = username or getpass.getuser()
496
462
if _use_ssh_agent:
497
463
agent = paramiko.Agent()
498
464
for key in agent.get_keys():
499
trace.mutter('Trying SSH agent key %s'
500
% paramiko.util.hexify(key.get_fingerprint()))
465
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
502
467
paramiko_transport.auth_publickey(username, key)
504
469
except paramiko.SSHException, e:
507
472
# okay, try finding id_rsa or id_dss? (posix only)
508
473
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
510
475
if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
513
# If we have gotten this far, we are about to try for passwords, do an
514
# auth_none check to see if it is even supported.
515
supported_auth_types = []
517
# Note that with paramiko <1.7.5 this logs an INFO message:
518
# Authentication type (none) not permitted.
519
# So we explicitly disable the logging level for this action
520
old_level = paramiko_transport.logger.level
521
paramiko_transport.logger.setLevel(logging.WARNING)
523
paramiko_transport.auth_none(username)
525
paramiko_transport.logger.setLevel(old_level)
526
except paramiko.BadAuthenticationType, e:
527
# Supported methods are in the exception
528
supported_auth_types = e.allowed_types
529
except paramiko.SSHException, e:
530
# Don't know what happened, but just ignore it
532
# We treat 'keyboard-interactive' and 'password' auth methods identically,
533
# because Paramiko's auth_password method will automatically try
534
# 'keyboard-interactive' auth (using the password as the response) if
535
# 'password' auth is not available. Apparently some Debian and Gentoo
536
# OpenSSH servers require this.
537
# XXX: It's possible for a server to require keyboard-interactive auth that
538
# requires something other than a single password, but we currently don't
540
if ('password' not in supported_auth_types and
541
'keyboard-interactive' not in supported_auth_types):
542
raise errors.ConnectionError('Unable to authenticate to SSH host as'
543
'\n %s@%s\nsupported auth types: %s'
544
% (username, host, supported_auth_types))
548
480
paramiko_transport.auth_password(username, password)
553
485
# give up and ask for a password
554
password = auth.get_password('ssh', host, username, port=port)
555
# get_password can still return None, which means we should not prompt
556
if password is not None:
558
paramiko_transport.auth_password(username, password)
559
except paramiko.SSHException, e:
560
raise errors.ConnectionError(
561
'Unable to authenticate to SSH host as'
562
'\n %s@%s\n' % (username, host), e)
564
raise errors.ConnectionError('Unable to authenticate to SSH host as'
565
' %s@%s' % (username, host))
486
password = bzrlib.ui.ui_factory.get_password(
487
prompt='SSH %(user)s@%(host)s password',
488
user=username, host=host)
490
paramiko_transport.auth_password(username, password)
491
except paramiko.SSHException, e:
492
raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
568
496
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
572
500
paramiko_transport.auth_publickey(username, key)
574
502
except paramiko.PasswordRequiredException:
575
password = ui.ui_factory.get_password(
576
prompt=u'SSH %(filename)s password',
577
filename=filename.decode(osutils._fs_enc))
503
password = bzrlib.ui.ui_factory.get_password(
504
prompt='SSH %(filename)s password',
579
507
key = pkey_class.from_private_key_file(filename, password)
580
508
paramiko_transport.auth_publickey(username, key)
582
510
except paramiko.SSHException:
583
trace.mutter('SSH authentication via %s key failed.'
584
% (os.path.basename(filename),))
511
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
585
512
except paramiko.SSHException:
586
trace.mutter('SSH authentication via %s key failed.'
587
% (os.path.basename(filename),))
513
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
643
568
# this causes it to be seen only by bzr and not by ssh. Python will
644
569
# generate a KeyboardInterrupt in bzr, and we will then have a chance
645
570
# to release locks or do other cleanup over ssh before the connection
647
572
# <https://launchpad.net/products/bzr/+bug/5987>
649
574
# Running it in a separate process group is not good because then it
650
575
# can't get non-echoed input of a password or passphrase.
651
576
# <https://launchpad.net/products/bzr/+bug/40508>
652
return {'preexec_fn': _ignore_signals,
577
return {'preexec_fn': _ignore_sigint,
653
578
'close_fds': True,
657
_subproc_weakrefs = set()
659
def _close_ssh_proc(proc, sock):
660
"""Carefully close stdin/stdout and reap the SSH process.
662
If the pipes are already closed and/or the process has already been
663
wait()ed on, that's ok, and no error is raised. The goal is to do our best
664
to clean up (whether or not a clean up was already tried).
667
for closeable in (proc.stdin, proc.stdout, sock):
668
# We expect that either proc (a subprocess.Popen) will have stdin and
669
# stdout streams to close, or that we will have been passed a socket to
670
# close, with the option not in use being None.
671
if closeable is not None:
672
funcs.append(closeable.close)
673
funcs.append(proc.wait)
678
# It's ok for the pipe to already be closed, or the process to
679
# already be finished.
683
class SSHConnection(object):
684
"""Abstract base class for SSH connections."""
686
def get_sock_or_pipes(self):
687
"""Returns a (kind, io_object) pair.
689
If kind == 'socket', then io_object is a socket.
691
If kind == 'pipes', then io_object is a pair of file-like objects
692
(read_from, write_to).
694
raise NotImplementedError(self.get_sock_or_pipes)
697
raise NotImplementedError(self.close)
700
class SSHSubprocessConnection(SSHConnection):
701
"""A connection to an ssh subprocess via pipes or a socket.
703
This class is also socket-like enough to be used with
704
SocketAsChannelAdapter (it has 'send' and 'recv' methods).
707
def __init__(self, proc, sock=None):
710
:param proc: a subprocess.Popen
711
:param sock: if proc.stdin/out is a socket from a socketpair, then sock
712
should bzrlib's half of that socketpair. If not passed, proc's
713
stdin/out is assumed to be ordinary pipes.
582
class SSHSubprocess(object):
583
"""A socket-like object that talks to an ssh subprocess via pipes."""
585
def __init__(self, proc):
717
# Add a weakref to proc that will attempt to do the same as self.close
718
# to avoid leaving processes lingering indefinitely.
720
_subproc_weakrefs.remove(ref)
721
_close_ssh_proc(proc, sock)
722
_subproc_weakrefs.add(weakref.ref(self, terminate))
724
588
def send(self, data):
725
if self._sock is not None:
726
return self._sock.send(data)
728
return os.write(self.proc.stdin.fileno(), data)
589
return os.write(self.proc.stdin.fileno(), data)
591
def recv_ready(self):
592
# TODO: jam 20051215 this function is necessary to support the
593
# pipelined() function. In reality, it probably should use
594
# poll() or select() to actually return if there is data
595
# available, otherwise we probably don't get any benefit
730
598
def recv(self, count):
731
if self._sock is not None:
732
return self._sock.recv(count)
734
return os.read(self.proc.stdout.fileno(), count)
737
_close_ssh_proc(self.proc, self._sock)
739
def get_sock_or_pipes(self):
740
if self._sock is not None:
741
return 'socket', self._sock
743
return 'pipes', (self.proc.stdout, self.proc.stdin)
746
class _ParamikoSSHConnection(SSHConnection):
747
"""An SSH connection via paramiko."""
749
def __init__(self, channel):
750
self.channel = channel
752
def get_sock_or_pipes(self):
753
return ('socket', self.channel)
756
return self.channel.close()
599
return os.read(self.proc.stdout.fileno(), count)
602
self.proc.stdin.close()
603
self.proc.stdout.close()
606
def get_filelike_channels(self):
607
return (self.proc.stdout, self.proc.stdin)