~bzr-pqm/bzr/bzr.dev

1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>
2
# Copyright (C) 2005, 2006 Canonical Ltd
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
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
17
18
"""Foundation SSH support for SFTP and smart server."""
19
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
20
import errno
1951.1.5 by Andrew Bennetts
Fix some missing imports with a bit of help from pyflakes.
21
import getpass
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
22
import os
23
import socket
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
24
import subprocess
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
25
import sys
26
27
from bzrlib.config import config_dir, ensure_config_dir_exists
28
from bzrlib.errors import (ConnectionError,
29
                           ParamikoNotPresent,
30
                           TransportError,
31
                           )
32
1951.1.5 by Andrew Bennetts
Fix some missing imports with a bit of help from pyflakes.
33
from bzrlib.osutils import pathjoin
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
34
from bzrlib.trace import mutter, warning
1951.1.5 by Andrew Bennetts
Fix some missing imports with a bit of help from pyflakes.
35
import bzrlib.ui
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
36
37
try:
38
    import paramiko
39
except ImportError, e:
40
    raise ParamikoNotPresent(e)
41
else:
42
    from paramiko.sftp_client import SFTPClient
43
44
45
SYSTEM_HOSTKEYS = {}
46
BZR_HOSTKEYS = {}
47
48
1951.1.5 by Andrew Bennetts
Fix some missing imports with a bit of help from pyflakes.
49
_paramiko_version = getattr(paramiko, '__version_info__', (0, 0, 0))
50
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
51
# Paramiko 1.5 tries to open a socket.AF_UNIX in order to connect
52
# to ssh-agent. That attribute doesn't exist on win32 (it does in cygwin)
53
# so we get an AttributeError exception. So we will not try to
54
# connect to an agent if we are on win32 and using Paramiko older than 1.6
55
_use_ssh_agent = (sys.platform != 'win32' or _paramiko_version >= (1, 6, 0))
56
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
57
_ssh_vendors = {}
58
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
59
def register_ssh_vendor(name, vendor):
60
    """Register SSH vendor."""
61
    _ssh_vendors[name] = vendor
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
62
63
    
64
_ssh_vendor = None
65
def _get_ssh_vendor():
66
    """Find out what version of SSH is on the system."""
67
    global _ssh_vendor
68
    if _ssh_vendor is not None:
69
        return _ssh_vendor
70
71
    if 'BZR_SSH' in os.environ:
72
        vendor_name = os.environ['BZR_SSH']
73
        try:
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
74
            _ssh_vendor = _ssh_vendors[vendor_name]
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
75
        except KeyError:
76
            raise UnknownSSH(vendor_name)
77
        return _ssh_vendor
78
79
    try:
80
        p = subprocess.Popen(['ssh', '-V'],
81
                             stdin=subprocess.PIPE,
82
                             stdout=subprocess.PIPE,
83
                             stderr=subprocess.PIPE,
84
                             **os_specific_subprocess_params())
85
        returncode = p.returncode
86
        stdout, stderr = p.communicate()
87
    except OSError:
88
        returncode = -1
89
        stdout = stderr = ''
90
    if 'OpenSSH' in stderr:
91
        mutter('ssh implementation is OpenSSH')
92
        _ssh_vendor = OpenSSHSubprocessVendor()
93
    elif 'SSH Secure Shell' in stderr:
94
        mutter('ssh implementation is SSH Corp.')
95
        _ssh_vendor = SSHCorpSubprocessVendor()
96
97
    if _ssh_vendor is not None:
98
        return _ssh_vendor
99
100
    # XXX: 20051123 jamesh
101
    # A check for putty's plink or lsh would go here.
102
103
    mutter('falling back to paramiko implementation')
104
    _ssh_vendor = ssh.ParamikoVendor()
105
    return _ssh_vendor
106
107
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
108
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
109
def _ignore_sigint():
110
    # TODO: This should possibly ignore SIGHUP as well, but bzr currently
111
    # doesn't handle it itself.
112
    # <https://launchpad.net/products/bzr/+bug/41433/+index>
113
    import signal
114
    signal.signal(signal.SIGINT, signal.SIG_IGN)
115
    
116
117
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
118
class LoopbackSFTP(object):
119
    """Simple wrapper for a socket that pretends to be a paramiko Channel."""
120
121
    def __init__(self, sock):
122
        self.__socket = sock
123
 
124
    def send(self, data):
125
        return self.__socket.send(data)
126
127
    def recv(self, n):
128
        return self.__socket.recv(n)
129
130
    def recv_ready(self):
131
        return True
132
133
    def close(self):
134
        self.__socket.close()
135
136
137
class SSHVendor(object):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
138
    """Abstract base class for SSH vendor implementations."""
139
    
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
140
    def connect_sftp(self, username, password, host, port):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
141
        """Make an SSH connection, and return an SFTPClient.
142
        
143
        :param username: an ascii string
144
        :param password: an ascii string
145
        :param host: a host name as an ascii string
146
        :param port: a port number
147
        :type port: int
148
149
        :raises: ConnectionError if it cannot connect.
150
151
        :rtype: paramiko.sftp_client.SFTPClient
152
        """
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
153
        raise NotImplementedError(self.connect_sftp)
154
155
    def connect_ssh(self, username, password, host, port, command):
1951.1.12 by Andrew Bennetts
Cosmetic tweaks.
156
        """Make an SSH connection, and return a pipe-like object.
157
        
158
        (This is currently unused, it's just here to indicate future directions
159
        for this code.)
160
        """
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
161
        raise NotImplementedError(self.connect_ssh)
162
        
163
164
class LoopbackVendor(SSHVendor):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
165
    """SSH "vendor" that connects over a plain TCP socket, not SSH."""
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
166
    
167
    def connect_sftp(self, username, password, host, port):
168
        sock = socket.socket()
169
        try:
170
            sock.connect((host, port))
171
        except socket.error, e:
172
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
173
                                  % (host, port, e))
174
        return SFTPClient(LoopbackSFTP(sock))
175
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
176
register_ssh_vendor('loopback', LoopbackVendor())
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
177
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
178
179
class ParamikoVendor(SSHVendor):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
180
    """Vendor that uses paramiko."""
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
181
182
    def connect_sftp(self, username, password, host, port):
183
        global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
184
        
185
        load_host_keys()
186
187
        try:
188
            t = paramiko.Transport((host, port or 22))
189
            t.set_log_channel('bzr.paramiko')
190
            t.start_client()
191
        except (paramiko.SSHException, socket.error), e:
192
            raise ConnectionError('Unable to reach SSH host %s:%s: %s' 
193
                                  % (host, port, e))
194
            
195
        server_key = t.get_remote_server_key()
196
        server_key_hex = paramiko.util.hexify(server_key.get_fingerprint())
197
        keytype = server_key.get_name()
1711.9.10 by John Arbash Meinel
Update transport/ssh.py to remove has_key usage
198
        if host in SYSTEM_HOSTKEYS and keytype in SYSTEM_HOSTKEYS[host]:
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
199
            our_server_key = SYSTEM_HOSTKEYS[host][keytype]
200
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
1711.9.10 by John Arbash Meinel
Update transport/ssh.py to remove has_key usage
201
        elif host in BZR_HOSTKEYS and keytype in BZR_HOSTKEYS[host]:
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
202
            our_server_key = BZR_HOSTKEYS[host][keytype]
203
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
204
        else:
205
            warning('Adding %s host key for %s: %s' % (keytype, host, server_key_hex))
1711.9.10 by John Arbash Meinel
Update transport/ssh.py to remove has_key usage
206
            if host not in BZR_HOSTKEYS:
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
207
                BZR_HOSTKEYS[host] = {}
208
            BZR_HOSTKEYS[host][keytype] = server_key
209
            our_server_key = server_key
210
            our_server_key_hex = paramiko.util.hexify(our_server_key.get_fingerprint())
211
            save_host_keys()
212
        if server_key != our_server_key:
213
            filename1 = os.path.expanduser('~/.ssh/known_hosts')
214
            filename2 = pathjoin(config_dir(), 'ssh_host_keys')
215
            raise TransportError('Host keys for %s do not match!  %s != %s' % \
216
                (host, our_server_key_hex, server_key_hex),
217
                ['Try editing %s or %s' % (filename1, filename2)])
218
219
        _paramiko_auth(username, password, host, t)
220
        
221
        try:
222
            sftp = t.open_sftp_client()
223
        except paramiko.SSHException, e:
224
            raise ConnectionError('Unable to start sftp client %s:%d' %
225
                                  (host, port), e)
226
        return sftp
227
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
228
register_ssh_vendor('paramiko', ParamikoVendor())
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
229
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
230
231
class SubprocessVendor(SSHVendor):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
232
    """Abstract base class for vendors that use pipes to a subprocess."""
233
    
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
234
    def connect_sftp(self, username, password, host, port):
235
        try:
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
236
            argv = self._get_vendor_specific_argv(username, host, port,
237
                                                  subsystem='sftp')
238
            proc = subprocess.Popen(argv,
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
239
                                    stdin=subprocess.PIPE,
240
                                    stdout=subprocess.PIPE,
241
                                    **os_specific_subprocess_params())
242
            sock = SSHSubprocess(proc)
243
            return SFTPClient(sock)
244
        except (EOFError, paramiko.SSHException), e:
245
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
246
                                  % (host, port, e))
247
        except (OSError, IOError), e:
248
            # If the machine is fast enough, ssh can actually exit
249
            # before we try and send it the sftp request, which
250
            # raises a Broken Pipe
251
            if e.errno not in (errno.EPIPE,):
252
                raise
253
            raise ConnectionError('Unable to connect to SSH host %s:%s: %s'
254
                                  % (host, port, e))
255
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
256
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
257
                                  command=None):
258
        """Returns the argument list to run the subprocess with.
259
        
260
        Exactly one of 'subsystem' and 'command' must be specified.
261
        """
262
        raise NotImplementedError(self._get_vendor_specific_argv)
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
263
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
264
register_ssh_vendor('none', ParamikoVendor())
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
265
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
266
267
class OpenSSHSubprocessVendor(SubprocessVendor):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
268
    """SSH vendor that uses the 'ssh' executable from OpenSSH."""
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
269
    
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
270
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
271
                                  command=None):
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
272
        assert subsystem is not None or command is not None, (
273
            'Must specify a command or subsystem')
274
        if subsystem is not None:
275
            assert command is None, (
276
                'subsystem and command are mutually exclusive')
277
        args = ['ssh',
278
                '-oForwardX11=no', '-oForwardAgent=no',
279
                '-oClearAllForwardings=yes', '-oProtocol=2',
280
                '-oNoHostAuthenticationForLocalhost=yes']
281
        if port is not None:
282
            args.extend(['-p', str(port)])
283
        if username is not None:
284
            args.extend(['-l', username])
285
        if subsystem is not None:
286
            args.extend(['-s', host, subsystem])
287
        else:
288
            args.extend([host] + command)
289
        return args
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
290
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
291
register_ssh_vendor('openssh', OpenSSHSubprocessVendor())
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
292
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
293
294
class SSHCorpSubprocessVendor(SubprocessVendor):
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
295
    """SSH vendor that uses the 'ssh' executable from SSH Corporation."""
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
296
1951.1.9 by Andrew Bennetts
Add docstrings and tweak method names in ssh.py
297
    def _get_vendor_specific_argv(self, username, host, port, subsystem=None,
298
                                  command=None):
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
299
        assert subsystem is not None or command is not None, (
300
            'Must specify a command or subsystem')
301
        if subsystem is not None:
302
            assert command is None, (
303
                'subsystem and command are mutually exclusive')
304
        args = ['ssh', '-x']
305
        if port is not None:
306
            args.extend(['-p', str(port)])
307
        if username is not None:
308
            args.extend(['-l', username])
309
        if subsystem is not None:
310
            args.extend(['-s', subsystem, host])
311
        else:
312
            args.extend([host] + command)
313
        return args
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
314
    
1951.1.11 by Andrew Bennetts
Change register_ssh_vendor to take an instance rather than a class.
315
register_ssh_vendor('ssh', SSHCorpSubprocessVendor())
1951.1.10 by Andrew Bennetts
Move register_ssh_vendor, _ssh_vendor and _get_ssh_vendor into ssh.py
316
1951.1.4 by Andrew Bennetts
Start moving SSH connection code into bzrlib/transport/ssh.py
317
318
def _paramiko_auth(username, password, host, paramiko_transport):
319
    # paramiko requires a username, but it might be none if nothing was supplied
320
    # use the local username, just in case.
321
    # We don't override username, because if we aren't using paramiko,
322
    # the username might be specified in ~/.ssh/config and we don't want to
323
    # force it to something else
324
    # Also, it would mess up the self.relpath() functionality
325
    username = username or getpass.getuser()
326
327
    if _use_ssh_agent:
328
        agent = paramiko.Agent()
329
        for key in agent.get_keys():
330
            mutter('Trying SSH agent key %s' % paramiko.util.hexify(key.get_fingerprint()))
331
            try:
332
                paramiko_transport.auth_publickey(username, key)
333
                return
334
            except paramiko.SSHException, e:
335
                pass
336
    
337
    # okay, try finding id_rsa or id_dss?  (posix only)
338
    if _try_pkey_auth(paramiko_transport, paramiko.RSAKey, username, 'id_rsa'):
339
        return
340
    if _try_pkey_auth(paramiko_transport, paramiko.DSSKey, username, 'id_dsa'):
341
        return
342
343
    if password:
344
        try:
345
            paramiko_transport.auth_password(username, password)
346
            return
347
        except paramiko.SSHException, e:
348
            pass
349
350
    # give up and ask for a password
351
    password = bzrlib.ui.ui_factory.get_password(
352
            prompt='SSH %(user)s@%(host)s password',
353
            user=username, host=host)
354
    try:
355
        paramiko_transport.auth_password(username, password)
356
    except paramiko.SSHException, e:
357
        raise ConnectionError('Unable to authenticate to SSH host as %s@%s' %
358
                              (username, host), e)
359
360
361
def _try_pkey_auth(paramiko_transport, pkey_class, username, filename):
362
    filename = os.path.expanduser('~/.ssh/' + filename)
363
    try:
364
        key = pkey_class.from_private_key_file(filename)
365
        paramiko_transport.auth_publickey(username, key)
366
        return True
367
    except paramiko.PasswordRequiredException:
368
        password = bzrlib.ui.ui_factory.get_password(
369
                prompt='SSH %(filename)s password',
370
                filename=filename)
371
        try:
372
            key = pkey_class.from_private_key_file(filename, password)
373
            paramiko_transport.auth_publickey(username, key)
374
            return True
375
        except paramiko.SSHException:
376
            mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
377
    except paramiko.SSHException:
378
        mutter('SSH authentication via %s key failed.' % (os.path.basename(filename),))
379
    except IOError:
380
        pass
381
    return False
382
383
384
def load_host_keys():
385
    """
386
    Load system host keys (probably doesn't work on windows) and any
387
    "discovered" keys from previous sessions.
388
    """
389
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
390
    try:
391
        SYSTEM_HOSTKEYS = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))
392
    except Exception, e:
393
        mutter('failed to load system host keys: ' + str(e))
394
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
395
    try:
396
        BZR_HOSTKEYS = paramiko.util.load_host_keys(bzr_hostkey_path)
397
    except Exception, e:
398
        mutter('failed to load bzr host keys: ' + str(e))
399
        save_host_keys()
400
401
402
def save_host_keys():
403
    """
404
    Save "discovered" host keys in $(config)/ssh_host_keys/.
405
    """
406
    global SYSTEM_HOSTKEYS, BZR_HOSTKEYS
407
    bzr_hostkey_path = pathjoin(config_dir(), 'ssh_host_keys')
408
    ensure_config_dir_exists()
409
410
    try:
411
        f = open(bzr_hostkey_path, 'w')
412
        f.write('# SSH host keys collected by bzr\n')
413
        for hostname, keys in BZR_HOSTKEYS.iteritems():
414
            for keytype, key in keys.iteritems():
415
                f.write('%s %s %s\n' % (hostname, keytype, key.get_base64()))
416
        f.close()
417
    except IOError, e:
418
        mutter('failed to save bzr host keys: ' + str(e))
419
420
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
421
def os_specific_subprocess_params():
422
    """Get O/S specific subprocess parameters."""
423
    if sys.platform == 'win32':
424
        # setting the process group and closing fds is not supported on 
425
        # win32
426
        return {}
427
    else:
428
        # We close fds other than the pipes as the child process does not need 
429
        # them to be open.
430
        #
431
        # We also set the child process to ignore SIGINT.  Normally the signal
432
        # would be sent to every process in the foreground process group, but
433
        # this causes it to be seen only by bzr and not by ssh.  Python will
434
        # generate a KeyboardInterrupt in bzr, and we will then have a chance
435
        # to release locks or do other cleanup over ssh before the connection
436
        # goes away.  
437
        # <https://launchpad.net/products/bzr/+bug/5987>
438
        #
439
        # Running it in a separate process group is not good because then it
440
        # can't get non-echoed input of a password or passphrase.
441
        # <https://launchpad.net/products/bzr/+bug/40508>
442
        return {'preexec_fn': _ignore_sigint,
443
                'close_fds': True,
444
                }
445
1951.1.12 by Andrew Bennetts
Cosmetic tweaks.
446
1951.1.7 by Andrew Bennetts
Move more generic SSH code from sftp.py into ssh.py, and start unifying the connection establishing logic.
447
class SSHSubprocess(object):
448
    """A socket-like object that talks to an ssh subprocess via pipes."""
449
450
    def __init__(self, proc):
451
        self.proc = proc
452
453
    def send(self, data):
454
        return os.write(self.proc.stdin.fileno(), data)
455
456
    def recv_ready(self):
457
        # TODO: jam 20051215 this function is necessary to support the
458
        # pipelined() function. In reality, it probably should use
459
        # poll() or select() to actually return if there is data
460
        # available, otherwise we probably don't get any benefit
461
        return True
462
463
    def recv(self, count):
464
        return os.read(self.proc.stdout.fileno(), count)
465
466
    def close(self):
467
        self.proc.stdin.close()
468
        self.proc.stdout.close()
469
        self.proc.wait()
470
471