/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/transport/ssh.py

  • Committer: Jelmer Vernooij
  • Date: 2017-06-10 01:35:53 UTC
  • mto: (6670.4.8 move-bzr)
  • mto: This revision was merged to the branch mainline in revision 6681.
  • Revision ID: jelmer@jelmer.uk-20170610013553-560y7mn3su4pp763
Fix remaining tests.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2006-2010 Robey Pointer <robey@lag.net>
 
1
# Copyright (C) 2006-2011 Robey Pointer <robey@lag.net>
2
2
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
17
17
 
18
18
"""Foundation SSH support for SFTP and smart server."""
19
19
 
 
20
from __future__ import absolute_import
 
21
 
20
22
import errno
21
23
import getpass
22
24
import logging
24
26
import socket
25
27
import subprocess
26
28
import sys
 
29
from binascii import hexlify
27
30
 
28
 
from bzrlib import (
 
31
from .. import (
29
32
    config,
30
33
    errors,
31
34
    osutils,
35
38
 
36
39
try:
37
40
    import paramiko
38
 
except ImportError, e:
 
41
except ImportError as e:
39
42
    # If we have an ssh subprocess, we don't strictly need paramiko for all ssh
40
43
    # access
41
44
    paramiko = None
44
47
 
45
48
 
46
49
SYSTEM_HOSTKEYS = {}
47
 
BZR_HOSTKEYS = {}
 
50
BRZ_HOSTKEYS = {}
48
51
 
49
52
 
50
53
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
60
63
    """Manager for manage SSH vendors."""
61
64
 
62
65
    # Note, although at first sign the class interface seems similar to
63
 
    # bzrlib.registry.Registry it is not possible/convenient to directly use
 
66
    # breezy.registry.Registry it is not possible/convenient to directly use
64
67
    # the Registry because the class just has "get()" interface instead of the
65
68
    # Registry's "get(key)".
66
69
 
82
85
        self._cached_ssh_vendor = None
83
86
 
84
87
    def _get_vendor_by_environment(self, environment=None):
85
 
        """Return the vendor or None based on BZR_SSH environment variable.
 
88
        """Return the vendor or None based on BRZ_SSH environment variable.
86
89
 
87
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
90
        :raises UnknownSSH: if the BRZ_SSH environment variable contains
88
91
                            unknown vendor name
89
92
        """
90
93
        if environment is None:
91
94
            environment = os.environ
92
 
        if 'BZR_SSH' in environment:
93
 
            vendor_name = environment['BZR_SSH']
 
95
        if 'BRZ_SSH' in environment:
 
96
            vendor_name = environment['BRZ_SSH']
94
97
            try:
95
98
                vendor = self._ssh_vendors[vendor_name]
96
99
            except KeyError:
126
129
        elif 'SSH Secure Shell' in version:
127
130
            trace.mutter('ssh implementation is SSH Corp.')
128
131
            vendor = SSHCorpSubprocessVendor()
 
132
        elif 'lsh' in version:
 
133
            trace.mutter('ssh implementation is GNU lsh.')
 
134
            vendor = LSHSubprocessVendor()
129
135
        # As plink user prompts are not handled currently, don't auto-detect
130
136
        # it by inspection below, but keep this vendor detection for if a path
131
 
        # is given in BZR_SSH. See https://bugs.launchpad.net/bugs/414743
 
137
        # is given in BRZ_SSH. See https://bugs.launchpad.net/bugs/414743
132
138
        elif 'plink' in version and progname == 'plink':
133
139
            # Checking if "plink" was the executed argument as Windows
134
 
            # sometimes reports 'ssh -V' incorrectly with 'plink' in it's
 
140
            # sometimes reports 'ssh -V' incorrectly with 'plink' in its
135
141
            # version.  See https://bugs.launchpad.net/bzr/+bug/107155
136
142
            trace.mutter("ssh implementation is Putty's plink.")
137
143
            vendor = PLinkSubprocessVendor()
152
158
        """Find out what version of SSH is on the system.
153
159
 
154
160
        :raises SSHVendorNotFound: if no any SSH vendor is found
155
 
        :raises UnknownSSH: if the BZR_SSH environment variable contains
 
161
        :raises UnknownSSH: if the BRZ_SSH environment variable contains
156
162
                            unknown vendor name
157
163
        """
158
164
        if self._cached_ssh_vendor is None:
199
205
    def recv(self, n):
200
206
        try:
201
207
            return self.__socket.recv(n)
202
 
        except socket.error, e:
 
208
        except socket.error as e:
203
209
            if e.args[0] in (errno.EPIPE, errno.ECONNRESET, errno.ECONNABORTED,
204
210
                             errno.EBADF):
205
211
                # Connection has closed.  Paramiko expects an empty string in
239
245
    def connect_ssh(self, username, password, host, port, command):
240
246
        """Make an SSH connection.
241
247
 
242
 
        :returns: something with a `close` method, and a `get_filelike_channels`
243
 
            method that returns a pair of (read, write) filelike objects.
 
248
        :returns: an SSHConnection.
244
249
        """
245
250
        raise NotImplementedError(self.connect_ssh)
246
251
 
262
267
        sock = socket.socket()
263
268
        try:
264
269
            sock.connect((host, port))
265
 
        except socket.error, e:
 
270
        except socket.error as e:
266
271
            self._raise_connection_error(host, port=port, orig_error=e)
267
272
        return SFTPClient(SocketAsChannelAdapter(sock))
268
273
 
269
274
register_ssh_vendor('loopback', LoopbackVendor())
270
275
 
271
276
 
272
 
class _ParamikoSSHConnection(object):
273
 
    def __init__(self, channel):
274
 
        self.channel = channel
275
 
 
276
 
    def get_filelike_channels(self):
277
 
        return self.channel.makefile('rb'), self.channel.makefile('wb')
278
 
 
279
 
    def close(self):
280
 
        return self.channel.close()
281
 
 
282
 
 
283
277
class ParamikoVendor(SSHVendor):
284
278
    """Vendor that uses paramiko."""
285
279
 
 
280
    def _hexify(self, s):
 
281
        return hexlify(s).upper()
 
282
 
286
283
    def _connect(self, username, password, host, port):
287
 
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
284
        global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
288
285
 
289
286
        load_host_keys()
290
287
 
292
289
            t = paramiko.Transport((host, port or 22))
293
290
            t.set_log_channel('bzr.paramiko')
294
291
            t.start_client()
295
 
        except (paramiko.SSHException, socket.error), e:
 
292
        except (paramiko.SSHException, socket.error) as e:
296
293
            self._raise_connection_error(host, port=port, orig_error=e)
297
294
 
298
295
        server_key = t.get_remote_server_key()
299
 
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
 
296
        server_key_hex = self._hexify(server_key.get_fingerprint())
300
297
        keytype = server_key.get_name()
301
298
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
302
299
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
303
 
            our_server_key_hex = paramiko.util.hexify(
304
 
                our_server_key.get_fingerprint())
305
 
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
306
 
            our_server_key = BZR_HOSTKEYS[host][keytype]
307
 
            our_server_key_hex = paramiko.util.hexify(
308
 
                our_server_key.get_fingerprint())
 
300
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
 
301
        elif host in BRZ_HOSTKEYS and keytype in BRZ_HOSTKEYS[host]:
 
302
            our_server_key = BRZ_HOSTKEYS[host][keytype]
 
303
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
309
304
        else:
310
305
            trace.warning('Adding %s host key for %s: %s'
311
306
                          % (keytype, host, server_key_hex))
312
 
            add = getattr(BZR_HOSTKEYS, 'add', None)
 
307
            add = getattr(BRZ_HOSTKEYS, 'add', None)
313
308
            if add is not None: # paramiko >= 1.X.X
314
 
                BZR_HOSTKEYS.add(host, keytype, server_key)
 
309
                BRZ_HOSTKEYS.add(host, keytype, server_key)
315
310
            else:
316
 
                BZR_HOSTKEYS.setdefault(host, {})[keytype] = server_key
 
311
                BRZ_HOSTKEYS.setdefault(host, {})[keytype] = server_key
317
312
            our_server_key = server_key
318
 
            our_server_key_hex = paramiko.util.hexify(
319
 
                our_server_key.get_fingerprint())
 
313
            our_server_key_hex = self._hexify(our_server_key.get_fingerprint())
320
314
            save_host_keys()
321
315
        if server_key != our_server_key:
322
316
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
333
327
        t = self._connect(username, password, host, port)
334
328
        try:
335
329
            return t.open_sftp_client()
336
 
        except paramiko.SSHException, e:
 
330
        except paramiko.SSHException as e:
337
331
            self._raise_connection_error(host, port=port, orig_error=e,
338
332
                                         msg='Unable to start sftp client')
339
333
 
344
338
            cmdline = ' '.join(command)
345
339
            channel.exec_command(cmdline)
346
340
            return _ParamikoSSHConnection(channel)
347
 
        except paramiko.SSHException, e:
 
341
        except paramiko.SSHException as e:
348
342
            self._raise_connection_error(host, port=port, orig_error=e,
349
343
                                         msg='Unable to invoke remote bzr')
350
344
 
 
345
_ssh_connection_errors = (EOFError, OSError, IOError, socket.error)
351
346
if paramiko is not None:
352
347
    vendor = ParamikoVendor()
353
348
    register_ssh_vendor('paramiko', vendor)
354
349
    register_ssh_vendor('none', vendor)
355
350
    register_default_ssh_vendor(vendor)
356
 
    _sftp_connection_errors = (EOFError, paramiko.SSHException)
 
351
    _ssh_connection_errors += (paramiko.SSHException,)
357
352
    del vendor
358
 
else:
359
 
    _sftp_connection_errors = (EOFError,)
360
353
 
361
354
 
362
355
class SubprocessVendor(SSHVendor):
363
356
    """Abstract base class for vendors that use pipes to a subprocess."""
364
357
 
 
358
    # In general stderr should be inherited from the parent process so prompts
 
359
    # are visible on the terminal. This can be overriden to another file for
 
360
    # tests, but beware of using PIPE which may hang due to not being read.
 
361
    _stderr_target = None
 
362
 
365
363
    def _connect(self, argv):
366
 
        proc = subprocess.Popen(argv,
367
 
                                stdin=subprocess.PIPE,
368
 
                                stdout=subprocess.PIPE,
 
364
        # Attempt to make a socketpair to use as stdin/stdout for the SSH
 
365
        # subprocess.  We prefer sockets to pipes because they support
 
366
        # non-blocking short reads, allowing us to optimistically read 64k (or
 
367
        # whatever) chunks.
 
368
        try:
 
369
            my_sock, subproc_sock = socket.socketpair()
 
370
            osutils.set_fd_cloexec(my_sock)
 
371
        except (AttributeError, socket.error):
 
372
            # This platform doesn't support socketpair(), so just use ordinary
 
373
            # pipes instead.
 
374
            stdin = stdout = subprocess.PIPE
 
375
            my_sock, subproc_sock = None, None
 
376
        else:
 
377
            stdin = stdout = subproc_sock
 
378
        proc = subprocess.Popen(argv, stdin=stdin, stdout=stdout,
 
379
                                stderr=self._stderr_target,
369
380
                                **os_specific_subprocess_params())
370
 
        return SSHSubprocess(proc)
 
381
        if subproc_sock is not None:
 
382
            subproc_sock.close()
 
383
        return SSHSubprocessConnection(proc, sock=my_sock)
371
384
 
372
385
    def connect_sftp(self, username, password, host, port):
373
386
        try:
375
388
                                                  subsystem='sftp')
376
389
            sock = self._connect(argv)
377
390
            return SFTPClient(SocketAsChannelAdapter(sock))
378
 
        except _sftp_connection_errors, e:
379
 
            self._raise_connection_error(host, port=port, orig_error=e)
380
 
        except (OSError, IOError), e:
381
 
            # If the machine is fast enough, ssh can actually exit
382
 
            # before we try and send it the sftp request, which
383
 
            # raises a Broken Pipe
384
 
            if e.errno not in (errno.EPIPE,):
385
 
                raise
 
391
        except _ssh_connection_errors as e:
386
392
            self._raise_connection_error(host, port=port, orig_error=e)
387
393
 
388
394
    def connect_ssh(self, username, password, host, port, command):
390
396
            argv = self._get_vendor_specific_argv(username, host, port,
391
397
                                                  command=command)
392
398
            return self._connect(argv)
393
 
        except (EOFError), e:
394
 
            self._raise_connection_error(host, port=port, orig_error=e)
395
 
        except (OSError, IOError), e:
396
 
            # If the machine is fast enough, ssh can actually exit
397
 
            # before we try and send it the sftp request, which
398
 
            # raises a Broken Pipe
399
 
            if e.errno not in (errno.EPIPE,):
400
 
                raise
 
399
        except _ssh_connection_errors as e:
401
400
            self._raise_connection_error(host, port=port, orig_error=e)
402
401
 
403
402
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
418
417
                                  command=None):
419
418
        args = [self.executable_path,
420
419
                '-oForwardX11=no', '-oForwardAgent=no',
421
 
                '-oClearAllForwardings=yes', '-oProtocol=2',
 
420
                '-oClearAllForwardings=yes',
422
421
                '-oNoHostAuthenticationForLocalhost=yes']
423
422
        if port is not None:
424
423
            args.extend(['-p', str(port)])
454
453
register_ssh_vendor('sshcorp', SSHCorpSubprocessVendor())
455
454
 
456
455
 
 
456
class LSHSubprocessVendor(SubprocessVendor):
 
457
    """SSH vendor that uses the 'lsh' executable from GNU"""
 
458
 
 
459
    executable_path = 'lsh'
 
460
 
 
461
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
 
462
                                  command=None):
 
463
        args = [self.executable_path]
 
464
        if port is not None:
 
465
            args.extend(['-p', str(port)])
 
466
        if username is not None:
 
467
            args.extend(['-l', username])
 
468
        if subsystem is not None:
 
469
            args.extend(['--subsystem', subsystem, host])
 
470
        else:
 
471
            args.extend([host] + command)
 
472
        return args
 
473
 
 
474
register_ssh_vendor('lsh', LSHSubprocessVendor())
 
475
 
 
476
 
457
477
class PLinkSubprocessVendor(SubprocessVendor):
458
478
    """SSH vendor that uses the 'plink' executable from Putty."""
459
479
 
486
506
        agent = paramiko.Agent()
487
507
        for key in agent.get_keys():
488
508
            trace.mutter('Trying SSH agent key %s'
489
 
                         % paramiko.util.hexify(key.get_fingerprint()))
 
509
                         % self._hexify(key.get_fingerprint()))
490
510
            try:
491
511
                paramiko_transport.auth_publickey(username, key)
492
512
                return
493
 
            except paramiko.SSHException, e:
 
513
            except paramiko.SSHException as e:
494
514
                pass
495
515
 
496
516
    # okay, try finding id_rsa or id_dss?  (posix only)
512
532
            paramiko_transport.auth_none(username)
513
533
        finally:
514
534
            paramiko_transport.logger.setLevel(old_level)
515
 
    except paramiko.BadAuthenticationType, e:
 
535
    except paramiko.BadAuthenticationType as e:
516
536
        # Supported methods are in the exception
517
537
        supported_auth_types = e.allowed_types
518
 
    except paramiko.SSHException, e:
 
538
    except paramiko.SSHException as e:
519
539
        # Don't know what happened, but just ignore it
520
540
        pass
521
541
    # We treat 'keyboard-interactive' and 'password' auth methods identically,
536
556
        try:
537
557
            paramiko_transport.auth_password(username, password)
538
558
            return
539
 
        except paramiko.SSHException, e:
 
559
        except paramiko.SSHException as e:
540
560
            pass
541
561
 
542
562
    # give up and ask for a password
545
565
    if password is not None:
546
566
        try:
547
567
            paramiko_transport.auth_password(username, password)
548
 
        except paramiko.SSHException, e:
 
568
        except paramiko.SSHException as e:
549
569
            raise errors.ConnectionError(
550
570
                'Unable to authenticate to SSH host as'
551
571
                '\n  %s@%s\n' % (username, host), e)
562
582
        return True
563
583
    except paramiko.PasswordRequiredException:
564
584
        password = ui.ui_factory.get_password(
565
 
            prompt='SSH %(filename)s password', filename=filename)
 
585
            prompt=u'SSH %(filename)s password',
 
586
            filename=filename.decode(osutils._fs_enc))
566
587
        try:
567
588
            key = pkey_class.from_private_key_file(filename, password)
568
589
            paramiko_transport.auth_publickey(username, key)
583
604
    Load system host keys (probably doesn't work on windows) and any
584
605
    "discovered" keys from previous sessions.
585
606
    """
586
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
607
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
587
608
    try:
588
609
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(
589
610
            os.path.expanduser('~/.ssh/known_hosts'))
590
 
    except IOError, e:
 
611
    except IOError as e:
591
612
        trace.mutter('failed to load system host keys: ' + str(e))
592
 
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
 
613
    brz_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
593
614
    try:
594
 
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
595
 
    except IOError, e:
596
 
        trace.mutter('failed to load bzr host keys: ' + str(e))
 
615
        BRZ_HOSTKEYS = paramiko.util.load_host_keys(brz_hostkey_path)
 
616
    except IOError as e:
 
617
        trace.mutter('failed to load brz host keys: ' + str(e))
597
618
        save_host_keys()
598
619
 
599
620
 
601
622
    """
602
623
    Save "discovered" host keys in $(config)/ssh_host_keys/.
603
624
    """
604
 
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
 
625
    global SYSTEM_HOSTKEYS, BRZ_HOSTKEYS
605
626
    bzr_hostkey_path = osutils.pathjoin(config.config_dir(), 'ssh_host_keys')
606
627
    config.ensure_config_dir_exists()
607
628
 
608
629
    try:
609
630
        f = open(bzr_hostkey_path, 'w')
610
631
        f.write('# SSH host keys collected by bzr\n')
611
 
        for hostname, keys in BZR_HOSTKEYS.iteritems():
612
 
            for keytype, key in keys.iteritems():
 
632
        for hostname, keys in BRZ_HOSTKEYS.items():
 
633
            for keytype, key in keys.items():
613
634
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
614
635
        f.close()
615
 
    except IOError, e:
 
636
    except IOError as e:
616
637
        trace.mutter('failed to save bzr host keys: ' + str(e))
617
638
 
618
639
 
644
665
import weakref
645
666
_subproc_weakrefs = set()
646
667
 
647
 
def _close_ssh_proc(proc):
648
 
    for func in [proc.stdin.close, proc.stdout.close, proc.wait]:
 
668
def _close_ssh_proc(proc, sock):
 
669
    """Carefully close stdin/stdout and reap the SSH process.
 
670
 
 
671
    If the pipes are already closed and/or the process has already been
 
672
    wait()ed on, that's ok, and no error is raised.  The goal is to do our best
 
673
    to clean up (whether or not a clean up was already tried).
 
674
    """
 
675
    funcs = []
 
676
    for closeable in (proc.stdin, proc.stdout, sock):
 
677
        # We expect that either proc (a subprocess.Popen) will have stdin and
 
678
        # stdout streams to close, or that we will have been passed a socket to
 
679
        # close, with the option not in use being None.
 
680
        if closeable is not None:
 
681
            funcs.append(closeable.close)
 
682
    funcs.append(proc.wait)
 
683
    for func in funcs:
649
684
        try:
650
685
            func()
651
686
        except OSError:
652
 
            pass
653
 
 
654
 
 
655
 
class SSHSubprocess(object):
656
 
    """A socket-like object that talks to an ssh subprocess via pipes."""
657
 
 
658
 
    def __init__(self, proc):
 
687
            # It's ok for the pipe to already be closed, or the process to
 
688
            # already be finished.
 
689
            continue
 
690
 
 
691
 
 
692
class SSHConnection(object):
 
693
    """Abstract base class for SSH connections."""
 
694
 
 
695
    def get_sock_or_pipes(self):
 
696
        """Returns a (kind, io_object) pair.
 
697
 
 
698
        If kind == 'socket', then io_object is a socket.
 
699
 
 
700
        If kind == 'pipes', then io_object is a pair of file-like objects
 
701
        (read_from, write_to).
 
702
        """
 
703
        raise NotImplementedError(self.get_sock_or_pipes)
 
704
 
 
705
    def close(self):
 
706
        raise NotImplementedError(self.close)
 
707
 
 
708
 
 
709
class SSHSubprocessConnection(SSHConnection):
 
710
    """A connection to an ssh subprocess via pipes or a socket.
 
711
 
 
712
    This class is also socket-like enough to be used with
 
713
    SocketAsChannelAdapter (it has 'send' and 'recv' methods).
 
714
    """
 
715
 
 
716
    def __init__(self, proc, sock=None):
 
717
        """Constructor.
 
718
 
 
719
        :param proc: a subprocess.Popen
 
720
        :param sock: if proc.stdin/out is a socket from a socketpair, then sock
 
721
            should breezy's half of that socketpair.  If not passed, proc's
 
722
            stdin/out is assumed to be ordinary pipes.
 
723
        """
659
724
        self.proc = proc
 
725
        self._sock = sock
660
726
        # Add a weakref to proc that will attempt to do the same as self.close
661
727
        # to avoid leaving processes lingering indefinitely.
662
728
        def terminate(ref):
663
729
            _subproc_weakrefs.remove(ref)
664
 
            _close_ssh_proc(proc)
 
730
            _close_ssh_proc(proc, sock)
665
731
        _subproc_weakrefs.add(weakref.ref(self, terminate))
666
732
 
667
733
    def send(self, data):
668
 
        return os.write(self.proc.stdin.fileno(), data)
 
734
        if self._sock is not None:
 
735
            return self._sock.send(data)
 
736
        else:
 
737
            return os.write(self.proc.stdin.fileno(), data)
669
738
 
670
739
    def recv(self, count):
671
 
        return os.read(self.proc.stdout.fileno(), count)
672
 
 
673
 
    def close(self):
674
 
        _close_ssh_proc(self.proc)
675
 
 
676
 
    def get_filelike_channels(self):
677
 
        return (self.proc.stdout, self.proc.stdin)
 
740
        if self._sock is not None:
 
741
            return self._sock.recv(count)
 
742
        else:
 
743
            return os.read(self.proc.stdout.fileno(), count)
 
744
 
 
745
    def close(self):
 
746
        _close_ssh_proc(self.proc, self._sock)
 
747
 
 
748
    def get_sock_or_pipes(self):
 
749
        if self._sock is not None:
 
750
            return 'socket', self._sock
 
751
        else:
 
752
            return 'pipes', (self.proc.stdout, self.proc.stdin)
 
753
 
 
754
 
 
755
class _ParamikoSSHConnection(SSHConnection):
 
756
    """An SSH connection via paramiko."""
 
757
 
 
758
    def __init__(self, channel):
 
759
        self.channel = channel
 
760
 
 
761
    def get_sock_or_pipes(self):
 
762
        return ('socket', self.channel)
 
763
 
 
764
    def close(self):
 
765
        return self.channel.close()
 
766
 
678
767