/paramiko/transport.py
Python | 1243 lines | 1196 code | 14 blank | 33 comment | 19 complexity | c46ffbd7e677ac07630651095602327c MD5 | raw file
- # Copyright (C) 2003-2007 Robey Pointer <robeypointer@gmail.com>
- #
- # This file is part of paramiko.
- #
- # Paramiko is free software; you can redistribute it and/or modify it under the
- # terms of the GNU Lesser General Public License as published by the Free
- # Software Foundation; either version 2.1 of the License, or (at your option)
- # any later version.
- #
- # Paramiko is distrubuted in the hope that it will be useful, but WITHOUT ANY
- # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- # A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
- # details.
- #
- # You should have received a copy of the GNU Lesser General Public License
- # along with Paramiko; if not, write to the Free Software Foundation, Inc.,
- # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
- """
- L{Transport} handles the core SSH2 protocol.
- """
- import os
- import socket
- import string
- import struct
- import sys
- import threading
- import time
- import weakref
- from paramiko import util
- from paramiko.auth_handler import AuthHandler
- from paramiko.channel import Channel
- from paramiko.common import *
- from paramiko.compress import ZlibCompressor, ZlibDecompressor
- from paramiko.dsskey import DSSKey
- from paramiko.kex_gex import KexGex
- from paramiko.kex_group1 import KexGroup1
- from paramiko.message import Message
- from paramiko.packet import Packetizer, NeedRekeyException
- from paramiko.primes import ModulusPack
- from paramiko.rsakey import RSAKey
- from paramiko.server import ServerInterface
- from paramiko.sftp_client import SFTPClient
- from paramiko.ssh_exception import SSHException, BadAuthenticationType, ChannelException
- from Crypto import Random
- from Crypto.Cipher import Blowfish, AES, DES3, ARC4
- from Crypto.Hash import SHA, MD5
- try:
- from Crypto.Util import Counter
- except ImportError:
- from paramiko.util import Counter
- # for thread cleanup
- _active_threads = []
- def _join_lingering_threads():
- for thr in _active_threads:
- thr.stop_thread()
- import atexit
- atexit.register(_join_lingering_threads)
- class SecurityOptions (object):
- """
- Simple object containing the security preferences of an ssh transport.
- These are tuples of acceptable ciphers, digests, key types, and key
- exchange algorithms, listed in order of preference.
- Changing the contents and/or order of these fields affects the underlying
- L{Transport} (but only if you change them before starting the session).
- If you try to add an algorithm that paramiko doesn't recognize,
- C{ValueError} will be raised. If you try to assign something besides a
- tuple to one of the fields, C{TypeError} will be raised.
- """
- __slots__ = [ 'ciphers', 'digests', 'key_types', 'kex', 'compression', '_transport' ]
- def __init__(self, transport):
- self._transport = transport
- def __repr__(self):
- """
- Returns a string representation of this object, for debugging.
- @rtype: str
- """
- return '<paramiko.SecurityOptions for %s>' % repr(self._transport)
- def _get_ciphers(self):
- return self._transport._preferred_ciphers
- def _get_digests(self):
- return self._transport._preferred_macs
- def _get_key_types(self):
- return self._transport._preferred_keys
- def _get_kex(self):
- return self._transport._preferred_kex
- def _get_compression(self):
- return self._transport._preferred_compression
- def _set(self, name, orig, x):
- if type(x) is list:
- x = tuple(x)
- if type(x) is not tuple:
- raise TypeError('expected tuple or list')
- possible = getattr(self._transport, orig).keys()
- forbidden = filter(lambda n: n not in possible, x)
- if len(forbidden) > 0:
- raise ValueError('unknown cipher')
- setattr(self._transport, name, x)
- def _set_ciphers(self, x):
- self._set('_preferred_ciphers', '_cipher_info', x)
- def _set_digests(self, x):
- self._set('_preferred_macs', '_mac_info', x)
- def _set_key_types(self, x):
- self._set('_preferred_keys', '_key_info', x)
- def _set_kex(self, x):
- self._set('_preferred_kex', '_kex_info', x)
- def _set_compression(self, x):
- self._set('_preferred_compression', '_compression_info', x)
- ciphers = property(_get_ciphers, _set_ciphers, None,
- "Symmetric encryption ciphers")
- digests = property(_get_digests, _set_digests, None,
- "Digest (one-way hash) algorithms")
- key_types = property(_get_key_types, _set_key_types, None,
- "Public-key algorithms")
- kex = property(_get_kex, _set_kex, None, "Key exchange algorithms")
- compression = property(_get_compression, _set_compression, None,
- "Compression algorithms")
- class ChannelMap (object):
- def __init__(self):
- # (id -> Channel)
- self._map = weakref.WeakValueDictionary()
- self._lock = threading.Lock()
- def put(self, chanid, chan):
- self._lock.acquire()
- try:
- self._map[chanid] = chan
- finally:
- self._lock.release()
- def get(self, chanid):
- self._lock.acquire()
- try:
- return self._map.get(chanid, None)
- finally:
- self._lock.release()
- def delete(self, chanid):
- self._lock.acquire()
- try:
- try:
- del self._map[chanid]
- except KeyError:
- pass
- finally:
- self._lock.release()
- def values(self):
- self._lock.acquire()
- try:
- return self._map.values()
- finally:
- self._lock.release()
- def __len__(self):
- self._lock.acquire()
- try:
- return len(self._map)
- finally:
- self._lock.release()
- class Transport (threading.Thread):
- """
- An SSH Transport attaches to a stream (usually a socket), negotiates an
- encrypted session, authenticates, and then creates stream tunnels, called
- L{Channel}s, across the session. Multiple channels can be multiplexed
- across a single session (and often are, in the case of port forwardings).
- """
- _PROTO_ID = '2.0'
- _CLIENT_ID = 'paramiko_1.7.7.1'
- _preferred_ciphers = ( 'aes128-ctr', 'aes256-ctr', 'aes128-cbc', 'blowfish-cbc', 'aes256-cbc', '3des-cbc',
- 'arcfour128', 'arcfour256' )
- _preferred_macs = ( 'hmac-sha1', 'hmac-md5', 'hmac-sha1-96', 'hmac-md5-96' )
- _preferred_keys = ( 'ssh-rsa', 'ssh-dss' )
- _preferred_kex = ( 'diffie-hellman-group1-sha1', 'diffie-hellman-group-exchange-sha1' )
- _preferred_compression = ( 'none', )
- _cipher_info = {
- 'aes128-ctr': { 'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 16 },
- 'aes256-ctr': { 'class': AES, 'mode': AES.MODE_CTR, 'block-size': 16, 'key-size': 32 },
- 'blowfish-cbc': { 'class': Blowfish, 'mode': Blowfish.MODE_CBC, 'block-size': 8, 'key-size': 16 },
- 'aes128-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 16 },
- 'aes256-cbc': { 'class': AES, 'mode': AES.MODE_CBC, 'block-size': 16, 'key-size': 32 },
- '3des-cbc': { 'class': DES3, 'mode': DES3.MODE_CBC, 'block-size': 8, 'key-size': 24 },
- 'arcfour128': { 'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 16 },
- 'arcfour256': { 'class': ARC4, 'mode': None, 'block-size': 8, 'key-size': 32 },
- }
- _mac_info = {
- 'hmac-sha1': { 'class': SHA, 'size': 20 },
- 'hmac-sha1-96': { 'class': SHA, 'size': 12 },
- 'hmac-md5': { 'class': MD5, 'size': 16 },
- 'hmac-md5-96': { 'class': MD5, 'size': 12 },
- }
- _key_info = {
- 'ssh-rsa': RSAKey,
- 'ssh-dss': DSSKey,
- }
- _kex_info = {
- 'diffie-hellman-group1-sha1': KexGroup1,
- 'diffie-hellman-group-exchange-sha1': KexGex,
- }
- _compression_info = {
- # zlib@openssh.com is just zlib, but only turned on after a successful
- # authentication. openssh servers may only offer this type because
- # they've had troubles with security holes in zlib in the past.
- 'zlib@openssh.com': ( ZlibCompressor, ZlibDecompressor ),
- 'zlib': ( ZlibCompressor, ZlibDecompressor ),
- 'none': ( None, None ),
- }
- _modulus_pack = None
- def __init__(self, sock):
- """
- Create a new SSH session over an existing socket, or socket-like
- object. This only creates the Transport object; it doesn't begin the
- SSH session yet. Use L{connect} or L{start_client} to begin a client
- session, or L{start_server} to begin a server session.
- If the object is not actually a socket, it must have the following
- methods:
- - C{send(str)}: Writes from 1 to C{len(str)} bytes, and
- returns an int representing the number of bytes written. Returns
- 0 or raises C{EOFError} if the stream has been closed.
- - C{recv(int)}: Reads from 1 to C{int} bytes and returns them as a
- string. Returns 0 or raises C{EOFError} if the stream has been
- closed.
- - C{close()}: Closes the socket.
- - C{settimeout(n)}: Sets a (float) timeout on I/O operations.
- For ease of use, you may also pass in an address (as a tuple) or a host
- string as the C{sock} argument. (A host string is a hostname with an
- optional port (separated by C{":"}) which will be converted into a
- tuple of C{(hostname, port)}.) A socket will be connected to this
- address and used for communication. Exceptions from the C{socket} call
- may be thrown in this case.
- @param sock: a socket or socket-like object to create the session over.
- @type sock: socket
- """
- if isinstance(sock, (str, unicode)):
- # convert "host:port" into (host, port)
- hl = sock.split(':', 1)
- if len(hl) == 1:
- sock = (hl[0], 22)
- else:
- sock = (hl[0], int(hl[1]))
- if type(sock) is tuple:
- # connect to the given (host, port)
- hostname, port = sock
- reason = 'No suitable address family'
- for (family, socktype, proto, canonname, sockaddr) in socket.getaddrinfo(hostname, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
- if socktype == socket.SOCK_STREAM:
- af = family
- addr = sockaddr
- sock = socket.socket(af, socket.SOCK_STREAM)
- try:
- sock.connect((hostname, port))
- except socket.error, e:
- reason = str(e)
- else:
- break
- else:
- raise SSHException(
- 'Unable to connect to %s: %s' % (hostname, reason))
- # okay, normal socket-ish flow here...
- threading.Thread.__init__(self)
- self.setDaemon(True)
- self.rng = rng
- self.sock = sock
- # Python < 2.3 doesn't have the settimeout method - RogerB
- try:
- # we set the timeout so we can check self.active periodically to
- # see if we should bail. socket.timeout exception is never
- # propagated.
- self.sock.settimeout(0.1)
- except AttributeError:
- pass
- # negotiated crypto parameters
- self.packetizer = Packetizer(sock)
- self.local_version = 'SSH-' + self._PROTO_ID + '-' + self._CLIENT_ID
- self.remote_version = ''
- self.local_cipher = self.remote_cipher = ''
- self.local_kex_init = self.remote_kex_init = None
- self.local_mac = self.remote_mac = None
- self.local_compression = self.remote_compression = None
- self.session_id = None
- self.host_key_type = None
- self.host_key = None
- # state used during negotiation
- self.kex_engine = None
- self.H = None
- self.K = None
- self.active = False
- self.initial_kex_done = False
- self.in_kex = False
- self.authenticated = False
- self._expected_packet = tuple()
- self.lock = threading.Lock() # synchronization (always higher level than write_lock)
- # tracking open channels
- self._channels = ChannelMap()
- self.channel_events = { } # (id -> Event)
- self.channels_seen = { } # (id -> True)
- self._channel_counter = 1
- self.window_size = 65536
- self.max_packet_size = 34816
- self._x11_handler = None
- self._tcp_handler = None
- self.saved_exception = None
- self.clear_to_send = threading.Event()
- self.clear_to_send_lock = threading.Lock()
- self.clear_to_send_timeout = 30.0
- self.log_name = 'paramiko.transport'
- self.logger = util.get_logger(self.log_name)
- self.packetizer.set_log(self.logger)
- self.auth_handler = None
- self.global_response = None # response Message from an arbitrary global request
- self.completion_event = None # user-defined event callbacks
- self.banner_timeout = 15 # how long (seconds) to wait for the SSH banner
- # server mode:
- self.server_mode = False
- self.server_object = None
- self.server_key_dict = { }
- self.server_accepts = [ ]
- self.server_accept_cv = threading.Condition(self.lock)
- self.subsystem_table = { }
- def __repr__(self):
- """
- Returns a string representation of this object, for debugging.
- @rtype: str
- """
- out = '<paramiko.Transport at %s' % hex(long(id(self)) & 0xffffffffL)
- if not self.active:
- out += ' (unconnected)'
- else:
- if self.local_cipher != '':
- out += ' (cipher %s, %d bits)' % (self.local_cipher,
- self._cipher_info[self.local_cipher]['key-size'] * 8)
- if self.is_authenticated():
- out += ' (active; %d open channel(s))' % len(self._channels)
- elif self.initial_kex_done:
- out += ' (connected; awaiting auth)'
- else:
- out += ' (connecting)'
- out += '>'
- return out
- def atfork(self):
- """
- Terminate this Transport without closing the session. On posix
- systems, if a Transport is open during process forking, both parent
- and child will share the underlying socket, but only one process can
- use the connection (without corrupting the session). Use this method
- to clean up a Transport object without disrupting the other process.
- @since: 1.5.3
- """
- self.sock.close()
- self.close()
- def get_security_options(self):
- """
- Return a L{SecurityOptions} object which can be used to tweak the
- encryption algorithms this transport will permit, and the order of
- preference for them.
- @return: an object that can be used to change the preferred algorithms
- for encryption, digest (hash), public key, and key exchange.
- @rtype: L{SecurityOptions}
- """
- return SecurityOptions(self)
- def start_client(self, event=None):
- """
- Negotiate a new SSH2 session as a client. This is the first step after
- creating a new L{Transport}. A separate thread is created for protocol
- negotiation.
- If an event is passed in, this method returns immediately. When
- negotiation is done (successful or not), the given C{Event} will
- be triggered. On failure, L{is_active} will return C{False}.
- (Since 1.4) If C{event} is C{None}, this method will not return until
- negotation is done. On success, the method returns normally.
- Otherwise an SSHException is raised.
- After a successful negotiation, you will usually want to authenticate,
- calling L{auth_password <Transport.auth_password>} or
- L{auth_publickey <Transport.auth_publickey>}.
- @note: L{connect} is a simpler method for connecting as a client.
- @note: After calling this method (or L{start_server} or L{connect}),
- you should no longer directly read from or write to the original
- socket object.
- @param event: an event to trigger when negotiation is complete
- (optional)
- @type event: threading.Event
- @raise SSHException: if negotiation fails (and no C{event} was passed
- in)
- """
- self.active = True
- if event is not None:
- # async, return immediately and let the app poll for completion
- self.completion_event = event
- self.start()
- return
- # synchronous, wait for a result
- self.completion_event = event = threading.Event()
- self.start()
- Random.atfork()
- while True:
- event.wait(0.1)
- if not self.active:
- e = self.get_exception()
- if e is not None:
- raise e
- raise SSHException('Negotiation failed.')
- if event.isSet():
- break
- def start_server(self, event=None, server=None):
- """
- Negotiate a new SSH2 session as a server. This is the first step after
- creating a new L{Transport} and setting up your server host key(s). A
- separate thread is created for protocol negotiation.
- If an event is passed in, this method returns immediately. When
- negotiation is done (successful or not), the given C{Event} will
- be triggered. On failure, L{is_active} will return C{False}.
- (Since 1.4) If C{event} is C{None}, this method will not return until
- negotation is done. On success, the method returns normally.
- Otherwise an SSHException is raised.
- After a successful negotiation, the client will need to authenticate.
- Override the methods
- L{get_allowed_auths <ServerInterface.get_allowed_auths>},
- L{check_auth_none <ServerInterface.check_auth_none>},
- L{check_auth_password <ServerInterface.check_auth_password>}, and
- L{check_auth_publickey <ServerInterface.check_auth_publickey>} in the
- given C{server} object to control the authentication process.
- After a successful authentication, the client should request to open
- a channel. Override
- L{check_channel_request <ServerInterface.check_channel_request>} in the
- given C{server} object to allow channels to be opened.
- @note: After calling this method (or L{start_client} or L{connect}),
- you should no longer directly read from or write to the original
- socket object.
- @param event: an event to trigger when negotiation is complete.
- @type event: threading.Event
- @param server: an object used to perform authentication and create
- L{Channel}s.
- @type server: L{server.ServerInterface}
- @raise SSHException: if negotiation fails (and no C{event} was passed
- in)
- """
- if server is None:
- server = ServerInterface()
- self.server_mode = True
- self.server_object = server
- self.active = True
- if event is not None:
- # async, return immediately and let the app poll for completion
- self.completion_event = event
- self.start()
- return
- # synchronous, wait for a result
- self.completion_event = event = threading.Event()
- self.start()
- while True:
- event.wait(0.1)
- if not self.active:
- e = self.get_exception()
- if e is not None:
- raise e
- raise SSHException('Negotiation failed.')
- if event.isSet():
- break
- def add_server_key(self, key):
- """
- Add a host key to the list of keys used for server mode. When behaving
- as a server, the host key is used to sign certain packets during the
- SSH2 negotiation, so that the client can trust that we are who we say
- we are. Because this is used for signing, the key must contain private
- key info, not just the public half. Only one key of each type (RSA or
- DSS) is kept.
- @param key: the host key to add, usually an L{RSAKey <rsakey.RSAKey>} or
- L{DSSKey <dsskey.DSSKey>}.
- @type key: L{PKey <pkey.PKey>}
- """
- self.server_key_dict[key.get_name()] = key
- def get_server_key(self):
- """
- Return the active host key, in server mode. After negotiating with the
- client, this method will return the negotiated host key. If only one
- type of host key was set with L{add_server_key}, that's the only key
- that will ever be returned. But in cases where you have set more than
- one type of host key (for example, an RSA key and a DSS key), the key
- type will be negotiated by the client, and this method will return the
- key of the type agreed on. If the host key has not been negotiated
- yet, C{None} is returned. In client mode, the behavior is undefined.
- @return: host key of the type negotiated by the client, or C{None}.
- @rtype: L{PKey <pkey.PKey>}
- """
- try:
- return self.server_key_dict[self.host_key_type]
- except KeyError:
- pass
- return None
- def load_server_moduli(filename=None):
- """
- I{(optional)}
- Load a file of prime moduli for use in doing group-exchange key
- negotiation in server mode. It's a rather obscure option and can be
- safely ignored.
- In server mode, the remote client may request "group-exchange" key
- negotiation, which asks the server to send a random prime number that
- fits certain criteria. These primes are pretty difficult to compute,
- so they can't be generated on demand. But many systems contain a file
- of suitable primes (usually named something like C{/etc/ssh/moduli}).
- If you call C{load_server_moduli} and it returns C{True}, then this
- file of primes has been loaded and we will support "group-exchange" in
- server mode. Otherwise server mode will just claim that it doesn't
- support that method of key negotiation.
- @param filename: optional path to the moduli file, if you happen to
- know that it's not in a standard location.
- @type filename: str
- @return: True if a moduli file was successfully loaded; False
- otherwise.
- @rtype: bool
- @note: This has no effect when used in client mode.
- """
- Transport._modulus_pack = ModulusPack(rng)
- # places to look for the openssh "moduli" file
- file_list = [ '/etc/ssh/moduli', '/usr/local/etc/moduli' ]
- if filename is not None:
- file_list.insert(0, filename)
- for fn in file_list:
- try:
- Transport._modulus_pack.read_file(fn)
- return True
- except IOError:
- pass
- # none succeeded
- Transport._modulus_pack = None
- return False
- load_server_moduli = staticmethod(load_server_moduli)
- def close(self):
- """
- Close this session, and any open channels that are tied to it.
- """
- if not self.active:
- return
- self.active = False
- self.packetizer.close()
- self.join()
- for chan in self._channels.values():
- chan._unlink()
- def get_remote_server_key(self):
- """
- Return the host key of the server (in client mode).
- @note: Previously this call returned a tuple of (key type, key string).
- You can get the same effect by calling
- L{PKey.get_name <pkey.PKey.get_name>} for the key type, and
- C{str(key)} for the key string.
- @raise SSHException: if no session is currently active.
- @return: public key of the remote server
- @rtype: L{PKey <pkey.PKey>}
- """
- if (not self.active) or (not self.initial_kex_done):
- raise SSHException('No existing session')
- return self.host_key
- def is_active(self):
- """
- Return true if this session is active (open).
- @return: True if the session is still active (open); False if the
- session is closed
- @rtype: bool
- """
- return self.active
- def open_session(self):
- """
- Request a new channel to the server, of type C{"session"}. This
- is just an alias for C{open_channel('session')}.
- @return: a new L{Channel}
- @rtype: L{Channel}
- @raise SSHException: if the request is rejected or the session ends
- prematurely
- """
- return self.open_channel('session')
- def open_x11_channel(self, src_addr=None):
- """
- Request a new channel to the client, of type C{"x11"}. This
- is just an alias for C{open_channel('x11', src_addr=src_addr)}.
- @param src_addr: the source address of the x11 server (port is the
- x11 port, ie. 6010)
- @type src_addr: (str, int)
- @return: a new L{Channel}
- @rtype: L{Channel}
- @raise SSHException: if the request is rejected or the session ends
- prematurely
- """
- return self.open_channel('x11', src_addr=src_addr)
- def open_forwarded_tcpip_channel(self, (src_addr, src_port), (dest_addr, dest_port)):
- """
- Request a new channel back to the client, of type C{"forwarded-tcpip"}.
- This is used after a client has requested port forwarding, for sending
- incoming connections back to the client.
- @param src_addr: originator's address
- @param src_port: originator's port
- @param dest_addr: local (server) connected address
- @param dest_port: local (server) connected port
- """
- return self.open_channel('forwarded-tcpip', (dest_addr, dest_port), (src_addr, src_port))
- def open_channel(self, kind, dest_addr=None, src_addr=None):
- """
- Request a new channel to the server. L{Channel}s are socket-like
- objects used for the actual transfer of data across the session.
- You may only request a channel after negotiating encryption (using
- L{connect} or L{start_client}) and authenticating.
- @param kind: the kind of channel requested (usually C{"session"},
- C{"forwarded-tcpip"}, C{"direct-tcpip"}, or C{"x11"})
- @type kind: str
- @param dest_addr: the destination address of this port forwarding,
- if C{kind} is C{"forwarded-tcpip"} or C{"direct-tcpip"} (ignored
- for other channel types)
- @type dest_addr: (str, int)
- @param src_addr: the source address of this port forwarding, if
- C{kind} is C{"forwarded-tcpip"}, C{"direct-tcpip"}, or C{"x11"}
- @type src_addr: (str, int)
- @return: a new L{Channel} on success
- @rtype: L{Channel}
- @raise SSHException: if the request is rejected or the session ends
- prematurely
- """
- if not self.active:
- raise SSHException('SSH session not active')
- self.lock.acquire()
- try:
- chanid = self._next_channel()
- m = Message()
- m.add_byte(chr(MSG_CHANNEL_OPEN))
- m.add_string(kind)
- m.add_int(chanid)
- m.add_int(self.window_size)
- m.add_int(self.max_packet_size)
- if (kind == 'forwarded-tcpip') or (kind == 'direct-tcpip'):
- m.add_string(dest_addr[0])
- m.add_int(dest_addr[1])
- m.add_string(src_addr[0])
- m.add_int(src_addr[1])
- elif kind == 'x11':
- m.add_string(src_addr[0])
- m.add_int(src_addr[1])
- chan = Channel(chanid)
- self._channels.put(chanid, chan)
- self.channel_events[chanid] = event = threading.Event()
- self.channels_seen[chanid] = True
- chan._set_transport(self)
- chan._set_window(self.window_size, self.max_packet_size)
- finally:
- self.lock.release()
- self._send_user_message(m)
- while True:
- event.wait(0.1);
- if not self.active:
- e = self.get_exception()
- if e is None:
- e = SSHException('Unable to open channel.')
- raise e
- if event.isSet():
- break
- chan = self._channels.get(chanid)
- if chan is not None:
- return chan
- e = self.get_exception()
- if e is None:
- e = SSHException('Unable to open channel.')
- raise e
- def request_port_forward(self, address, port, handler=None):
- """
- Ask the server to forward TCP connections from a listening port on
- the server, across this SSH session.
- If a handler is given, that handler is called from a different thread
- whenever a forwarded connection arrives. The handler parameters are::
- handler(channel, (origin_addr, origin_port), (server_addr, server_port))
- where C{server_addr} and C{server_port} are the address and port that
- the server was listening on.
- If no handler is set, the default behavior is to send new incoming
- forwarded connections into the accept queue, to be picked up via
- L{accept}.
- @param address: the address to bind when forwarding
- @type address: str
- @param port: the port to forward, or 0 to ask the server to allocate
- any port
- @type port: int
- @param handler: optional handler for incoming forwarded connections
- @type handler: function(Channel, (str, int), (str, int))
- @return: the port # allocated by the server
- @rtype: int
- @raise SSHException: if the server refused the TCP forward request
- """
- if not self.active:
- raise SSHException('SSH session not active')
- address = str(address)
- port = int(port)
- response = self.global_request('tcpip-forward', (address, port), wait=True)
- if response is None:
- raise SSHException('TCP forwarding request denied')
- if port == 0:
- port = response.get_int()
- if handler is None:
- def default_handler(channel, (src_addr, src_port), (dest_addr, dest_port)):
- self._queue_incoming_channel(channel)
- handler = default_handler
- self._tcp_handler = handler
- return port
- def cancel_port_forward(self, address, port):
- """
- Ask the server to cancel a previous port-forwarding request. No more
- connections to the given address & port will be forwarded across this
- ssh connection.
- @param address: the address to stop forwarding
- @type address: str
- @param port: the port to stop forwarding
- @type port: int
- """
- if not self.active:
- return
- self._tcp_handler = None
- self.global_request('cancel-tcpip-forward', (address, port), wait=True)
- def open_sftp_client(self):
- """
- Create an SFTP client channel from an open transport. On success,
- an SFTP session will be opened with the remote host, and a new
- SFTPClient object will be returned.
- @return: a new L{SFTPClient} object, referring to an sftp session
- (channel) across this transport
- @rtype: L{SFTPClient}
- """
- return SFTPClient.from_transport(self)
- def send_ignore(self, bytes=None):
- """
- Send a junk packet across the encrypted link. This is sometimes used
- to add "noise" to a connection to confuse would-be attackers. It can
- also be used as a keep-alive for long lived connections traversing
- firewalls.
- @param bytes: the number of random bytes to send in the payload of the
- ignored packet -- defaults to a random number from 10 to 41.
- @type bytes: int
- """
- m = Message()
- m.add_byte(chr(MSG_IGNORE))
- if bytes is None:
- bytes = (ord(rng.read(1)) % 32) + 10
- m.add_bytes(rng.read(bytes))
- self._send_user_message(m)
- def renegotiate_keys(self):
- """
- Force this session to switch to new keys. Normally this is done
- automatically after the session hits a certain number of packets or
- bytes sent or received, but this method gives you the option of forcing
- new keys whenever you want. Negotiating new keys causes a pause in
- traffic both ways as the two sides swap keys and do computations. This
- method returns when the session has switched to new keys.
- @raise SSHException: if the key renegotiation failed (which causes the
- session to end)
- """
- self.completion_event = threading.Event()
- self._send_kex_init()
- while True:
- self.completion_event.wait(0.1)
- if not self.active:
- e = self.get_exception()
- if e is not None:
- raise e
- raise SSHException('Negotiation failed.')
- if self.completion_event.isSet():
- break
- return
- def set_keepalive(self, interval):
- """
- Turn on/off keepalive packets (default is off). If this is set, after
- C{interval} seconds without sending any data over the connection, a
- "keepalive" packet will be sent (and ignored by the remote host). This
- can be useful to keep connections alive over a NAT, for example.
- @param interval: seconds to wait before sending a keepalive packet (or
- 0 to disable keepalives).
- @type interval: int
- """
- self.packetizer.set_keepalive(interval,
- lambda x=weakref.proxy(self): x.global_request('keepalive@lag.net', wait=False))
- def global_request(self, kind, data=None, wait=True):
- """
- Make a global request to the remote host. These are normally
- extensions to the SSH2 protocol.
- @param kind: name of the request.
- @type kind: str
- @param data: an optional tuple containing additional data to attach
- to the request.
- @type data: tuple
- @param wait: C{True} if this method should not return until a response
- is received; C{False} otherwise.
- @type wait: bool
- @return: a L{Message} containing possible additional data if the
- request was successful (or an empty L{Message} if C{wait} was
- C{False}); C{None} if the request was denied.
- @rtype: L{Message}
- """
- if wait:
- self.completion_event = threading.Event()
- m = Message()
- m.add_byte(chr(MSG_GLOBAL_REQUEST))
- m.add_string(kind)
- m.add_boolean(wait)
- if data is not None:
- m.add(*data)
- self._log(DEBUG, 'Sending global request "%s"' % kind)
- self._send_user_message(m)
- if not wait:
- return None
- while True:
- self.completion_event.wait(0.1)
- if not self.active:
- return None
- if self.completion_event.isSet():
- break
- return self.global_response
- def accept(self, timeout=None):
- """
- Return the next channel opened by the client over this transport, in
- server mode. If no channel is opened before the given timeout, C{None}
- is returned.
- @param timeout: seconds to wait for a channel, or C{None} to wait
- forever
- @type timeout: int
- @return: a new Channel opened by the client
- @rtype: L{Channel}
- """
- self.lock.acquire()
- try:
- if len(self.server_accepts) > 0:
- chan = self.server_accepts.pop(0)
- else:
- self.server_accept_cv.wait(timeout)
- if len(self.server_accepts) > 0:
- chan = self.server_accepts.pop(0)
- else:
- # timeout
- chan = None
- finally:
- self.lock.release()
- return chan
- def connect(self, hostkey=None, username='', password=None, pkey=None):
- """
- Negotiate an SSH2 session, and optionally verify the server's host key
- and authenticate using a password or private key. This is a shortcut
- for L{start_client}, L{get_remote_server_key}, and
- L{Transport.auth_password} or L{Transport.auth_publickey}. Use those
- methods if you want more control.
- You can use this method immediately after creating a Transport to
- negotiate encryption with a server. If it fails, an exception will be
- thrown. On success, the method will return cleanly, and an encrypted
- session exists. You may immediately call L{open_channel} or
- L{open_session} to get a L{Channel} object, which is used for data
- transfer.
- @note: If you fail to supply a password or private key, this method may
- succeed, but a subsequent L{open_channel} or L{open_session} call may
- fail because you haven't authenticated yet.
- @param hostkey: the host key expected from the server, or C{None} if
- you don't want to do host key verification.
- @type hostkey: L{PKey<pkey.PKey>}
- @param username: the username to authenticate as.
- @type username: str
- @param password: a password to use for authentication, if you want to
- use password authentication; otherwise C{None}.
- @type password: str
- @param pkey: a private key to use for authentication, if you want to
- use private key authentication; otherwise C{None}.
- @type pkey: L{PKey<pkey.PKey>}
- @raise SSHException: if the SSH2 negotiation fails, the host key
- supplied by the server is incorrect, or authentication fails.
- """
- if hostkey is not None:
- self._preferred_keys = [ hostkey.get_name() ]
- self.start_client()
- # check host key if we were given one
- if (hostkey is not None):
- key = self.get_remote_server_key()
- if (key.get_name() != hostkey.get_name()) or (str(key) != str(hostkey)):
- self._log(DEBUG, 'Bad host key from server')
- self._log(DEBUG, 'Expected: %s: %s' % (hostkey.get_name(), repr(str(hostkey))))
- self._log(DEBUG, 'Got : %s: %s' % (key.get_name(), repr(str(key))))
- raise SSHException('Bad host key from server')
- self._log(DEBUG, 'Host key verified (%s)' % hostkey.get_name())
- if (pkey is not None) or (password is not None):
- if password is not None:
- self._log(DEBUG, 'Attempting password auth...')
- self.auth_password(username, password)
- else:
- self._log(DEBUG, 'Attempting public-key auth...')
- self.auth_publickey(username, pkey)
- return
- def get_exception(self):
- """
- Return any exception that happened during the last server request.
- This can be used to fetch more specific error information after using
- calls like L{start_client}. The exception (if any) is cleared after
- this call.
- @return: an exception, or C{None} if there is no stored exception.
- @rtype: Exception
- @since: 1.1
- """
- self.lock.acquire()
- try:
- e = self.saved_exception
- self.saved_exception = None
- return e
- finally:
- self.lock.release()
- def set_subsystem_handler(self, name, handler, *larg, **kwarg):
- """
- Set the handler class for a subsystem in server mode. If a request
- for this subsystem is made on an open ssh channel later, this handler
- will be constructed and called -- see L{SubsystemHandler} for more
- detailed documentation.
- Any extra parameters (including keyword arguments) are saved and
- passed to the L{SubsystemHandler} constructor later.
- @param name: name of the subsystem.
- @type name: str
- @param handler: subclass of L{SubsystemHandler} that handles this
- subsystem.
- @type handler: class
- """
- try:
- self.lock.acquire()
- self.subsystem_table[name] = (handler, larg, kwarg)
- finally:
- self.lock.release()
- def is_authenticated(self):
- """
- Return true if this session is active and authenticated.
- @return: True if the session is still open and has been authenticated
- successfully; False if authentication failed and/or the session is
- closed.
- @rtype: bool
- """
- return self.active and (self.auth_handler is not None) and self.auth_handler.is_authenticated()
- def get_username(self):
- """
- Return the username this connection is authenticated for. If the
- session is not authenticated (or authentication failed), this method
- returns C{None}.
- @return: username that was authenticated, or C{None}.
- @rtype: string
- """
- if not self.active or (self.auth_handler is None):
- return None
- return self.auth_handler.get_username()
- def auth_none(self, username):
- """
- Try to authenticate to the server using no authentication at all.
- This will almost always fail. It may be useful for determining the
- list of authentication types supported by the server, by catching the
- L{BadAuthenticationType} exception raised.
- @param username: the username to authenticate as
- @type username: string
- @return: list of auth types permissible for the next stage of
- authentication (normally empty)
- @rtype: list
- @raise BadAuthenticationType: if "none" authentication isn't allowed
- by the server for this user
- @raise SSHException: if the authentication failed due to a network
- error
- @since: 1.5
- """
- if (not self.active) or (not self.initial_kex_done):
- raise SSHException('No existing session')
- my_event = threading.Event()
- self.auth_handler = AuthHandler(self)
- self.auth_handler.auth_none(username, my_event)
- return self.auth_handler.wait_for_response(my_event)
- def auth_password(self, username, password, event=None, fallback=True):
- """
- Authenticate to the server using a password. The username and password
- are sent over an encrypted link.
- If an C{event} is passed in, this method will return immediately, and
- the event will be triggered once authentication succeeds or fails. On
- success, L{is_authenticated} will return C{True}. On failure, you may
- use L{get_exception} to get more detailed error information.
- Since 1.1, if no event is passed, this method will block until the
- authentication succeeds or fails. On failure, an exception is raised.
- Otherwise, the method simply returns.
- Since 1.5, if no event is passed and C{fallback} is C{True} (the
- default), if the server doesn't support plain password authentication
- but does support so-called "keyboard-interactive" mode, an attempt
- will be made to authenticate using this interactive mode. If it fails,
- the normal exception will be thrown as if the attempt had never been
- made. This is useful for some recent Gentoo and Debian distributions,
- which turn off plain password authentication in a misguided belief
- that interactive authentication is "more secure". (It's not.)
- If the server requires multi-step authentication (which is very rare),
- this method will return a list of auth types permissible for the next
- step. Otherwise, in the normal case, an empty list is returned.
- @param username: the username to authenticate as
- @type username: str
- @param password: the password to authenticate with
- @type password: str or unicode
- @param event: an event to trigger when the authentication attempt is
- complete (whether it was successful or not)
- @type event: threading.Event
- @param fallback: C{True} if an attempt at an automated "interactive"
- password auth should be made if the server doesn't support normal
- password auth
- @type fallback: bool
- @return: list of auth types permissible for the next stage of
- authentication (normally empty)
- @rtype: list
- @raise BadAuthenticationType: if password authentication isn't
- allowed by the server for this user (and no event was passed in)
- @raise AuthenticationException: if the authentication failed (and no
- event was passed in)
- @raise SSHException: if there was a network error
- """
- if (not self.active) or (not self.initial_kex_done):
- # we should never try to send the password unless we're on a secure link
- raise SSHException('No existing session')
- if event is None:
- my_event = threading.Event()
- else:
- my_event = event
- self.auth_handler = AuthHandler(self)
- self.auth_handler.auth_password(username, password, my_event)
- if event is not None:
- # caller wants to wait for event themselves
- return []
- try:
- return self.auth_handler.wait_for_response(my_event)
- except BadAuthenticationType, x:
- # if password auth isn't allowed, but keyboard-interactive *is*, try to fudge it
- if not fallback or ('keyboard-interactive' not in x.allowed_types):
- raise
- try:
- def handler(title, instructions, fields):
- if len(fields) > 1:
- raise SSHException('Fallback authentication failed.')
- if len(fields) == 0:
- # for some reason, at least on os x, a 2nd request will
- # be made with zero fields requested. maybe it's just
- # to try to fake out automated scripting of the exact
- # type we're doing here. *shrug* :)
- return []
- return [ password ]
- return self.auth_interactive(username, handler)
- except SSHException, ignored:
- # attempt failed; just raise the original exception
- raise x
- return None
- def auth_publickey(self, username, key, event=None):
- """
- Authenticate to the server using a private key. The key is used to
- sign data from the server, so it must include the private part.
- If an C{event} is passed in, this method will return immediately, and
- the event will be triggered once authentication succeeds or fails. On
- success, L{is_authenticated} will return C{True}. On failure, you may
- use L{get_exception} to get more detailed error information.
- Since 1.1, if no event is passed, this method will block until the
- authentication succeeds or fails. On failure, an exception is raised.
- Otherwise, the method simply returns.
- If the server requires multi-step authentication (which is very rare),
- this method will return a list of auth types permissible for the next
- step. Otherwise, in the normal case, an empty list is returned.
- @param username: the username to authenticate as
- @type username: string
- @param key: the private key to authenticate with
- @type key: L{PKey <pkey.PKey>}
- @param event: an event to trigger when the authentication attempt is
- complete (whether it was successful or not)
- @type event: threading.Event
- @return: list of auth types permissible for the next stage of
- authentication (normally empty)
- @rtype: list
- @raise BadAuthenticationType: if public-key authentication isn't
- allowed by the server for this user (and no event was passed in)
- @raise AuthenticationException: if the authentication failed (and no
- event was passed in)
- @raise SSHException: if there was a network error
- """
- if (not self.active) or (not self.initial_kex_done):
- # we should never try to authenticate unless we're on a secure link
- raise SSHException('No existing session')
- if event is None:
- my_event = threading.Event()
- else:
- my_event = event
- self.auth_handler = AuthHandler(self)
- self.auth_handler.auth_publickey(username, key, my_event)
- if event is not None:
- # caller wants to wait for event themselves
- return []
- return self.auth_handler.wait_for_response(my_event)
- def auth_interactive(self, username, handler, submethods=''):
- """
- Authenticate to the server interactively. A handler is used to answer
- arbitrary questions from the server. On many servers, this is just a
- dumb wrapper around PAM.
- This method will block until the authentication succeeds or fails,
- peroidically calling the handler asynchronously to get answers to