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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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
114
113
stdout = stderr = ''
115
114
return stdout + stderr
117
def _get_vendor_by_version_string(self, version):
116
def _get_vendor_by_version_string(self, version, progname):
118
117
"""Return the vendor or None based on output from the subprocess.
120
119
:param version: The output of 'ssh -V' like command.
120
:param args: Command line that was run.
123
123
if 'OpenSSH' in version:
124
mutter('ssh implementation is OpenSSH')
124
trace.mutter('ssh implementation is OpenSSH')
125
125
vendor = OpenSSHSubprocessVendor()
126
126
elif 'SSH Secure Shell' in version:
127
mutter('ssh implementation is SSH Corp.')
127
trace.mutter('ssh implementation is SSH Corp.')
128
128
vendor = SSHCorpSubprocessVendor()
129
elif 'plink' in version:
130
mutter("ssh implementation is Putty's plink.")
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.")
131
140
vendor = PLinkSubprocessVendor()
134
143
def _get_vendor_by_inspection(self):
135
144
"""Return the vendor or None by checking for known SSH implementations."""
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:
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])
143
154
def get_vendor(self, environment=None):
144
155
"""Find out what version of SSH is on the system.
165
176
register_ssh_vendor = _ssh_vendor_manager.register_vendor
168
def _ignore_sigint():
179
def _ignore_signals():
169
180
# TODO: This should possibly ignore SIGHUP as well, but bzr currently
170
181
# doesn't handle it itself.
171
182
# <https://launchpad.net/products/bzr/+bug/41433/+index>
173
184
signal.signal(signal.SIGINT, signal.SIG_IGN)
176
class LoopbackSFTP(object):
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):
177
191
"""Simple wrapper for a socket that pretends to be a paramiko Channel."""
179
193
def __init__(self, sock):
180
194
self.__socket = sock
197
return "bzr SocketAsChannelAdapter"
182
199
def send(self, data):
183
200
return self.__socket.send(data)
185
202
def recv(self, n):
186
return self.__socket.recv(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.
188
213
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
225
253
This just unifies all the locations that try to raise ConnectionError,
226
254
so that they format things properly.
228
raise SocketConnectionError(host=host, port=port, msg=msg,
229
orig_error=orig_error)
256
raise errors.SocketConnectionError(host=host, port=port, msg=msg,
257
orig_error=orig_error)
232
260
class LoopbackVendor(SSHVendor):
233
261
"""SSH "vendor" that connects over a plain TCP socket, not SSH."""
235
263
def connect_sftp(self, username, password, host, port):
236
264
sock = socket.socket()
238
266
sock.connect((host, port))
239
267
except socket.error, e:
240
268
self._raise_connection_error(host, port=port, orig_error=e)
241
return SFTPClient(LoopbackSFTP(sock))
269
return SFTPClient(SocketAsChannelAdapter(sock))
243
271
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()
257
274
class ParamikoVendor(SSHVendor):
258
275
"""Vendor that uses paramiko."""
260
277
def _connect(self, username, password, host, port):
261
278
global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
269
286
except (paramiko.SSHException, socket.error), e:
270
287
self._raise_connection_error(host, port=port, orig_error=e)
272
289
server_key = t.get_remote_server_key()
273
290
server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
274
291
keytype = server_key.get_name()
275
292
if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
276
293
our_server_key = SYSTEM_HOSTKEYS[host][keytype]
277
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
294
our_server_key_hex = paramiko.util.hexify(
295
our_server_key.get_fingerprint())
278
296
elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
279
297
our_server_key = BZR_HOSTKEYS[host][keytype]
280
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
298
our_server_key_hex = paramiko.util.hexify(
299
our_server_key.get_fingerprint())
282
warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
301
trace.warning('Adding %s host key for %s: %s'
302
% (keytype, host, server_key_hex))
283
303
add = getattr(BZR_HOSTKEYS, 'add', None)
284
304
if add is not None: # paramiko >= 1.X.X
285
305
BZR_HOSTKEYS.add(host, keytype, server_key)
287
307
BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
288
308
our_server_key = server_key
289
our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
309
our_server_key_hex = paramiko.util.hexify(
310
our_server_key.get_fingerprint())
291
312
if server_key != our_server_key:
292
313
filename1 = os.path.expanduser('~/.ssh/known_hosts')
293
filename2 = pathjoin(config_dir(), 'ssh_host_keys')
294
raise TransportError('Host keys for %s do not match! %s != %s' % \
314
filename2 = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
315
raise errors.TransportError(
316
'Host keys for %s do not match! %s != %s' %
295
317
(host, our_server_key_hex, server_key_hex),
296
318
['Try editing %s or %s' % (filename1, filename2)])
298
_paramiko_auth(username, password, host, t)
320
_paramiko_auth(username, password, host, port, t)
301
323
def connect_sftp(self, username, password, host, port):
302
324
t = self._connect(username, password, host, port)
317
339
self._raise_connection_error(host, port=port, orig_error=e,
318
340
msg='Unable to invoke remote bzr')
342
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
320
343
if paramiko is not None:
321
344
vendor = ParamikoVendor()
322
345
register_ssh_vendor('paramiko', vendor)
323
346
register_ssh_vendor('none', vendor)
324
347
register_default_ssh_vendor(vendor)
348
_ssh_connection_errors += (paramiko.SSHException,)
328
352
class SubprocessVendor(SSHVendor):
329
353
"""Abstract base class for vendors that use pipes to a subprocess."""
355
# In general stderr should be inherited from the parent process so prompts
356
# are visible on the terminal. This can be overriden to another file for
357
# tests, but beware of using PIPE which may hang due to not being read.
358
_stderr_target = None
331
360
def _connect(self, argv):
332
proc = subprocess.Popen(argv,
333
stdin=subprocess.PIPE,
334
stdout=subprocess.PIPE,
361
# Attempt to make a socketpair to use as stdin/stdout for the SSH
362
# subprocess. We prefer sockets to pipes because they support
363
# non-blocking short reads, allowing us to optimistically read 64k (or
366
my_sock, subproc_sock = socket.socketpair()
367
osutils.set_fd_cloexec(my_sock)
368
except (AttributeError, socket.error):
369
# This platform doesn't support socketpair(), so just use ordinary
371
stdin = stdout = subprocess.PIPE
372
my_sock, subproc_sock = None, None
374
stdin = stdout = subproc_sock
375
proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
376
stderr=self._stderr_target,
335
377
**os_specific_subprocess_params())
336
return SSHSubprocess(proc)
378
if subproc_sock is not None:
380
return SSHSubprocessConnection(proc, sock=my_sock)
338
382
def connect_sftp(self, username, password, host, port):
340
384
argv = self._get_vendor_specific_argv(username, host, port,
341
385
subsystem='sftp')
342
386
sock = self._connect(argv)
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,):
387
return SFTPClient(SocketAsChannelAdapter(sock))
388
except _ssh_connection_errors, e:
352
389
self._raise_connection_error(host, port=port, orig_error=e)
354
391
def connect_ssh(self, username, password, host, port, command):
356
393
argv = self._get_vendor_specific_argv(username, host, port,
358
395
return self._connect(argv)
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,):
396
except _ssh_connection_errors, e:
367
397
self._raise_connection_error(host, port=port, orig_error=e)
369
399
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
371
401
"""Returns the argument list to run the subprocess with.
373
403
Exactly one of 'subsystem' and 'command' must be specified.
375
405
raise NotImplementedError(self._get_vendor_specific_argv)
378
408
class OpenSSHSubprocessVendor(SubprocessVendor):
379
409
"""SSH vendor that uses the 'ssh' executable from OpenSSH."""
411
executable_path = 'ssh'
381
413
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
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')
415
args = [self.executable_path,
389
416
'-oForwardX11=no', '-oForwardAgent=no',
390
'-oClearAllForwardings=yes', '-oProtocol=2',
417
'-oClearAllForwardings=yes',
391
418
'-oNoHostAuthenticationForLocalhost=yes']
392
419
if port is not None:
393
420
args.extend(['-p', str(port)])
423
447
args.extend([host] + command)
426
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
450
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
453
class LSHSubprocessVendor(SubprocessVendor):
454
"""SSH vendor that uses the 'lsh' executable from GNU"""
456
executable_path = 'lsh'
458
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
460
args = [self.executable_path]
462
args.extend(['-p', str(port)])
463
if username is not None:
464
args.extend(['-l', username])
465
if subsystem is not None:
466
args.extend(['--subsystem', subsystem, host])
468
args.extend([host] + command)
471
register_ssh_vendor('lsh', LSHSubprocessVendor())
429
474
class PLinkSubprocessVendor(SubprocessVendor):
430
475
"""SSH vendor that uses the 'plink' executable from Putty."""
477
executable_path = 'plink'
432
479
def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
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']
481
args = [self.executable_path, '-x', '-a', '-ssh', '-2', '-batch']
440
482
if port is not None:
441
483
args.extend(['-P', str(port)])
442
484
if username is not None:
450
492
register_ssh_vendor('plink', PLinkSubprocessVendor())
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()
495
def _paramiko_auth(username, password, host, port, paramiko_transport):
496
auth = config.AuthenticationConfig()
497
# paramiko requires a username, but it might be none if nothing was
498
# supplied. If so, use the local username.
500
username = auth.get_user('ssh', host, port=port,
501
default=getpass.getuser())
462
502
if _use_ssh_agent:
463
503
agent = paramiko.Agent()
464
504
for key in agent.get_keys():
465
mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
505
trace.mutter('Trying SSH agent key %s'
506
% paramiko.util.hexify(key.get_fingerprint()))
467
508
paramiko_transport.auth_publickey(username, key)
469
510
except paramiko.SSHException, e:
472
513
# okay, try finding id_rsa or id_dss? (posix only)
473
514
if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
475
516
if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
519
# If we have gotten this far, we are about to try for passwords, do an
520
# auth_none check to see if it is even supported.
521
supported_auth_types = []
523
# Note that with paramiko <1.7.5 this logs an INFO message:
524
# Authentication type (none) not permitted.
525
# So we explicitly disable the logging level for this action
526
old_level = paramiko_transport.logger.level
527
paramiko_transport.logger.setLevel(logging.WARNING)
529
paramiko_transport.auth_none(username)
531
paramiko_transport.logger.setLevel(old_level)
532
except paramiko.BadAuthenticationType, e:
533
# Supported methods are in the exception
534
supported_auth_types = e.allowed_types
535
except paramiko.SSHException, e:
536
# Don't know what happened, but just ignore it
538
# We treat 'keyboard-interactive' and 'password' auth methods identically,
539
# because Paramiko's auth_password method will automatically try
540
# 'keyboard-interactive' auth (using the password as the response) if
541
# 'password' auth is not available. Apparently some Debian and Gentoo
542
# OpenSSH servers require this.
543
# XXX: It's possible for a server to require keyboard-interactive auth that
544
# requires something other than a single password, but we currently don't
546
if ('password' not in supported_auth_types and
547
'keyboard-interactive' not in supported_auth_types):
548
raise errors.ConnectionError('Unable to authenticate to SSH host as'
549
'\n %s@%s\nsupported auth types: %s'
550
% (username, host, supported_auth_types))
480
554
paramiko_transport.auth_password(username, password)
485
559
# give up and ask for a password
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' %
560
password = auth.get_password('ssh', host, username, port=port)
561
# get_password can still return None, which means we should not prompt
562
if password is not None:
564
paramiko_transport.auth_password(username, password)
565
except paramiko.SSHException, e:
566
raise errors.ConnectionError(
567
'Unable to authenticate to SSH host as'
568
'\n %s@%s\n' % (username, host), e)
570
raise errors.ConnectionError('Unable to authenticate to SSH host as'
571
' %s@%s' % (username, host))
496
574
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
500
578
paramiko_transport.auth_publickey(username, key)
502
580
except paramiko.PasswordRequiredException:
503
password = bzrlib.ui.ui_factory.get_password(
504
prompt='SSH %(filename)s password',
581
password = ui.ui_factory.get_password(
582
prompt=u'SSH %(filename)s password',
583
filename=filename.decode(osutils._fs_enc))
507
585
key = pkey_class.from_private_key_file(filename, password)
508
586
paramiko_transport.auth_publickey(username, key)
510
588
except paramiko.SSHException:
511
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
589
trace.mutter('SSH authentication via %s key failed.'
590
% (os.path.basename(filename),))
512
591
except paramiko.SSHException:
513
mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
592
trace.mutter('SSH authentication via %s key failed.'
593
% (os.path.basename(filename),))
568
649
# this causes it to be seen only by bzr and not by ssh. Python will
569
650
# generate a KeyboardInterrupt in bzr, and we will then have a chance
570
651
# to release locks or do other cleanup over ssh before the connection
572
653
# <https://launchpad.net/products/bzr/+bug/5987>
574
655
# Running it in a separate process group is not good because then it
575
656
# can't get non-echoed input of a password or passphrase.
576
657
# <https://launchpad.net/products/bzr/+bug/40508>
577
return {'preexec_fn': _ignore_sigint,
658
return {'preexec_fn': _ignore_signals,
578
659
'close_fds': True,
582
class SSHSubprocess(object):
583
"""A socket-like object that talks to an ssh subprocess via pipes."""
585
def __init__(self, proc):
663
_subproc_weakrefs = set()
665
def _close_ssh_proc(proc, sock):
666
"""Carefully close stdin/stdout and reap the SSH process.
668
If the pipes are already closed and/or the process has already been
669
wait()ed on, that's ok, and no error is raised. The goal is to do our best
670
to clean up (whether or not a clean up was already tried).
673
for closeable in (proc.stdin, proc.stdout, sock):
674
# We expect that either proc (a subprocess.Popen) will have stdin and
675
# stdout streams to close, or that we will have been passed a socket to
676
# close, with the option not in use being None.
677
if closeable is not None:
678
funcs.append(closeable.close)
679
funcs.append(proc.wait)
684
# It's ok for the pipe to already be closed, or the process to
685
# already be finished.
689
class SSHConnection(object):
690
"""Abstract base class for SSH connections."""
692
def get_sock_or_pipes(self):
693
"""Returns a (kind, io_object) pair.
695
If kind == 'socket', then io_object is a socket.
697
If kind == 'pipes', then io_object is a pair of file-like objects
698
(read_from, write_to).
700
raise NotImplementedError(self.get_sock_or_pipes)
703
raise NotImplementedError(self.close)
706
class SSHSubprocessConnection(SSHConnection):
707
"""A connection to an ssh subprocess via pipes or a socket.
709
This class is also socket-like enough to be used with
710
SocketAsChannelAdapter (it has 'send' and 'recv' methods).
713
def __init__(self, proc, sock=None):
716
:param proc: a subprocess.Popen
717
:param sock: if proc.stdin/out is a socket from a socketpair, then sock
718
should bzrlib's half of that socketpair. If not passed, proc's
719
stdin/out is assumed to be ordinary pipes.
723
# Add a weakref to proc that will attempt to do the same as self.close
724
# to avoid leaving processes lingering indefinitely.
726
_subproc_weakrefs.remove(ref)
727
_close_ssh_proc(proc, sock)
728
_subproc_weakrefs.add(weakref.ref(self, terminate))
588
730
def send(self, 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
731
if self._sock is not None:
732
return self._sock.send(data)
734
return os.write(self.proc.stdin.fileno(), data)
598
736
def recv(self, count):
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)
737
if self._sock is not None:
738
return self._sock.recv(count)
740
return os.read(self.proc.stdout.fileno(), count)
743
_close_ssh_proc(self.proc, self._sock)
745
def get_sock_or_pipes(self):
746
if self._sock is not None:
747
return 'socket', self._sock
749
return 'pipes', (self.proc.stdout, self.proc.stdin)
752
class _ParamikoSSHConnection(SSHConnection):
753
"""An SSH connection via paramiko."""
755
def __init__(self, channel):
756
self.channel = channel
758
def get_sock_or_pipes(self):
759
return ('socket', self.channel)
762
return self.channel.close()