/paramiko/transport.py
Python | 1251 lines | 1186 code | 24 blank | 41 comment | 19 complexity | 0afad42c49f38c59218cf7bb80094dc6 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 distributed 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.
- """
- Core protocol implementation
- """
- from __future__ import print_function
- import os
- import socket
- import sys
- import threading
- import time
- import weakref
- from hashlib import md5, sha1, sha256, sha512
- import paramiko
- from paramiko import util
- from paramiko.auth_handler import AuthHandler
- from paramiko.ssh_gss import GSSAuth
- from paramiko.channel import Channel
- from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \
- cMSG_GLOBAL_REQUEST, DEBUG, MSG_KEXINIT, MSG_IGNORE, MSG_DISCONNECT, \
- MSG_DEBUG, ERROR, WARNING, cMSG_UNIMPLEMENTED, INFO, cMSG_KEXINIT, \
- cMSG_NEWKEYS, MSG_NEWKEYS, cMSG_REQUEST_SUCCESS, cMSG_REQUEST_FAILURE, \
- CONNECTION_FAILED_CODE, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED, \
- OPEN_SUCCEEDED, cMSG_CHANNEL_OPEN_FAILURE, cMSG_CHANNEL_OPEN_SUCCESS, \
- MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE, \
- MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, MSG_CHANNEL_OPEN, \
- MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \
- MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \
- MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_WINDOW_SIZE, MIN_PACKET_SIZE, \
- MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE
- from paramiko.compress import ZlibCompressor, ZlibDecompressor
- from paramiko.dsskey import DSSKey
- from paramiko.kex_gex import KexGex, KexGexSHA256
- from paramiko.kex_group1 import KexGroup1
- from paramiko.kex_group14 import KexGroup14
- from paramiko.kex_gss import KexGSSGex, KexGSSGroup1, KexGSSGroup14, NullHostKey
- from paramiko.message import Message
- from paramiko.packet import Packetizer, NeedRekeyException
- from paramiko.primes import ModulusPack
- from paramiko.py3compat import string_types, long, byte_ord, b
- from paramiko.rsakey import RSAKey
- from paramiko.ecdsakey import ECDSAKey
- from paramiko.server import ServerInterface
- from paramiko.sftp_client import SFTPClient
- from paramiko.ssh_exception import (SSHException, BadAuthenticationType,
- ChannelException, ProxyCommandFailure)
- from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
- from Crypto.Cipher import Blowfish, AES, DES3, ARC4
- 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 Transport (threading.Thread, ClosingContextManager):
- """
- An SSH Transport attaches to a stream (usually a socket), negotiates an
- encrypted session, authenticates, and then creates stream tunnels, called
- `channels <.Channel>`, across the session. Multiple channels can be
- multiplexed across a single session (and often are, in the case of port
- forwardings).
- Instances of this class may be used as context managers.
- """
- _PROTO_ID = '2.0'
- _CLIENT_ID = 'paramiko_%s' % paramiko.__version__
- # These tuples of algorithm identifiers are in preference order; do not
- # reorder without reason!
- _preferred_ciphers = (
- 'aes128-ctr',
- 'aes192-ctr',
- 'aes256-ctr',
- 'aes128-cbc',
- 'blowfish-cbc',
- 'aes192-cbc',
- 'aes256-cbc',
- '3des-cbc',
- 'arcfour128',
- 'arcfour256',
- )
- _preferred_macs = (
- 'hmac-sha2-256',
- 'hmac-sha2-512',
- 'hmac-md5',
- 'hmac-sha1-96',
- 'hmac-md5-96',
- 'hmac-sha1',
- )
- _preferred_keys = (
- 'ssh-rsa',
- 'ssh-dss',
- 'ecdsa-sha2-nistp256',
- )
- _preferred_kex = (
- 'diffie-hellman-group1-sha1',
- 'diffie-hellman-group14-sha1',
- 'diffie-hellman-group-exchange-sha1',
- 'diffie-hellman-group-exchange-sha256',
- )
- _preferred_compression = ('none',)
- _cipher_info = {
- 'aes128-ctr': {
- 'class': AES,
- 'mode': AES.MODE_CTR,
- 'block-size': 16,
- 'key-size': 16
- },
- 'aes192-ctr': {
- 'class': AES,
- 'mode': AES.MODE_CTR,
- 'block-size': 16,
- 'key-size': 24
- },
- '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
- },
- 'aes192-cbc': {
- 'class': AES,
- 'mode': AES.MODE_CBC,
- 'block-size': 16,
- 'key-size': 24
- },
- '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': sha1, 'size': 20},
- 'hmac-sha1-96': {'class': sha1, 'size': 12},
- 'hmac-sha2-256': {'class': sha256, 'size': 32},
- 'hmac-sha2-512': {'class': sha512, 'size': 64},
- 'hmac-md5': {'class': md5, 'size': 16},
- 'hmac-md5-96': {'class': md5, 'size': 12},
- }
- _key_info = {
- 'ssh-rsa': RSAKey,
- 'ssh-dss': DSSKey,
- 'ecdsa-sha2-nistp256': ECDSAKey,
- }
- _kex_info = {
- 'diffie-hellman-group1-sha1': KexGroup1,
- 'diffie-hellman-group14-sha1': KexGroup14,
- 'diffie-hellman-group-exchange-sha1': KexGex,
- 'diffie-hellman-group-exchange-sha256': KexGexSHA256,
- 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup1,
- 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGroup14,
- 'gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==': KexGSSGex
- }
- _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,
- default_window_size=DEFAULT_WINDOW_SIZE,
- default_max_packet_size=DEFAULT_MAX_PACKET_SIZE,
- gss_kex=False,
- gss_deleg_creds=True):
- """
- 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 `connect` or `start_client` to begin a client
- session, or `start_server` to begin a server session.
- If the object is not actually a socket, it must have the following
- methods:
- - ``send(str)``: Writes from 1 to ``len(str)`` bytes, and returns an
- int representing the number of bytes written. Returns
- 0 or raises ``EOFError`` if the stream has been closed.
- - ``recv(int)``: Reads from 1 to ``int`` bytes and returns them as a
- string. Returns 0 or raises ``EOFError`` if the stream has been
- closed.
- - ``close()``: Closes the socket.
- - ``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 ``sock`` argument. (A host string is a hostname with an
- optional port (separated by ``":"``) which will be converted into a
- tuple of ``(hostname, port)``.) A socket will be connected to this
- address and used for communication. Exceptions from the ``socket``
- call may be thrown in this case.
- .. note::
- Modifying the the window and packet sizes might have adverse
- effects on your channels created from this transport. The default
- values are the same as in the OpenSSH code base and have been
- battle tested.
- :param socket sock:
- a socket or socket-like object to create the session over.
- :param int default_window_size:
- sets the default window size on the transport. (defaults to
- 2097152)
- :param int default_max_packet_size:
- sets the default max packet size on the transport. (defaults to
- 32768)
- .. versionchanged:: 1.15
- Added the ``default_window_size`` and ``default_max_packet_size``
- arguments.
- """
- self.active = False
- if isinstance(sock, string_types):
- # 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:
- retry_on_signal(lambda: sock.connect((hostname, port)))
- except socket.error as 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.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
- # GSS-API / SSPI Key Exchange
- self.use_gss_kex = gss_kex
- # This will be set to True if GSS-API Key Exchange was performed
- self.gss_kex_used = False
- self.kexgss_ctxt = None
- self.gss_host = None
- if self.use_gss_kex:
- self.kexgss_ctxt = GSSAuth("gssapi-keyex", gss_deleg_creds)
- self._preferred_kex = ('gss-gex-sha1-toWM5Slw5Ew8Mqkay+al2g==',
- 'gss-group14-sha1-toWM5Slw5Ew8Mqkay+al2g==',
- 'gss-group1-sha1-toWM5Slw5Ew8Mqkay+al2g==',
- 'diffie-hellman-group-exchange-sha1',
- 'diffie-hellman-group14-sha1',
- 'diffie-hellman-group1-sha1')
- # state used during negotiation
- self.kex_engine = None
- self.H = None
- self.K = None
- 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 = 0
- self.default_max_packet_size = default_max_packet_size
- self.default_window_size = default_window_size
- self._forward_agent_handler = None
- 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
- self.handshake_timeout = 15 # how long (seconds) to wait for the handshake to finish after SSH banner sent.
- # 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.
- """
- out = '<paramiko.Transport at %s' % hex(long(id(self)) & xffffffff)
- 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.
- .. versionadded:: 1.5.3
- """
- self.sock.close()
- self.close()
- def get_security_options(self):
- """
- Return a `.SecurityOptions` object which can be used to tweak the
- encryption algorithms this transport will permit (for encryption,
- digest/hash operations, public keys, and key exchanges) and the order
- of preference for them.
- """
- return SecurityOptions(self)
- def set_gss_host(self, gss_host):
- """
- Setter for C{gss_host} if GSS-API Key Exchange is performed.
- :param str gss_host: The targets name in the kerberos database
- Default: The name of the host to connect to
- :rtype: Void
- """
- # We need the FQDN to get this working with SSPI
- self.gss_host = socket.getfqdn(gss_host)
- def start_client(self, event=None):
- """
- Negotiate a new SSH2 session as a client. This is the first step after
- creating a new `.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 ``Event`` will
- be triggered. On failure, `is_active` will return ``False``.
- (Since 1.4) If ``event`` is ``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 `auth_password <Transport.auth_password>` or
- `auth_publickey <Transport.auth_publickey>`.
- .. note:: `connect` is a simpler method for connecting as a client.
- .. note::
- After calling this method (or `start_server` or `connect`), you
- should no longer directly read from or write to the original socket
- object.
- :param .threading.Event event:
- an event to trigger when negotiation is complete (optional)
- :raises SSHException: if negotiation fails (and no ``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()
- 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.is_set():
- 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 `.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 ``Event`` will
- be triggered. On failure, `is_active` will return ``False``.
- (Since 1.4) If ``event`` is ``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 `get_allowed_auths
- <.ServerInterface.get_allowed_auths>`, `check_auth_none
- <.ServerInterface.check_auth_none>`, `check_auth_password
- <.ServerInterface.check_auth_password>`, and `check_auth_publickey
- <.ServerInterface.check_auth_publickey>` in the given ``server`` object
- to control the authentication process.
- After a successful authentication, the client should request to open a
- channel. Override `check_channel_request
- <.ServerInterface.check_channel_request>` in the given ``server``
- object to allow channels to be opened.
- .. note::
- After calling this method (or `start_client` or `connect`), you
- should no longer directly read from or write to the original socket
- object.
- :param .threading.Event event:
- an event to trigger when negotiation is complete.
- :param .ServerInterface server:
- an object used to perform authentication and create `channels
- <.Channel>`
- :raises SSHException: if negotiation fails (and no ``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.is_set():
- 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 .PKey key:
- the host key to add, usually an `.RSAKey` or `.DSSKey`.
- """
- 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 `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, ``None`` is returned. In client mode, the behavior is undefined.
- :return:
- host key (`.PKey`) of the type negotiated by the client, or
- ``None``.
- """
- try:
- return self.server_key_dict[self.host_key_type]
- except KeyError:
- pass
- return None
- @staticmethod
- def load_server_moduli(filename=None):
- """
- (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 ``/etc/ssh/moduli``).
- If you call `load_server_moduli` and it returns ``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 str filename:
- optional path to the moduli file, if you happen to know that it's
- not in a standard location.
- :return:
- True if a moduli file was successfully loaded; False otherwise.
- .. note:: This has no effect when used in client mode.
- """
- Transport._modulus_pack = ModulusPack()
- # 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
- def close(self):
- """
- Close this session, and any open channels that are tied to it.
- """
- if not self.active:
- return
- self.stop_thread()
- for chan in list(self._channels.values()):
- chan._unlink()
- self.sock.close()
- 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 `.PKey.get_name`
- for the key type, and ``str(key)`` for the key string.
- :raises SSHException: if no session is currently active.
- :return: public key (`.PKey`) of the remote server
- """
- 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
- """
- return self.active
- def open_session(self, window_size=None, max_packet_size=None, timeout=None):
- """
- Request a new channel to the server, of type ``"session"``. This is
- just an alias for calling `open_channel` with an argument of
- ``"session"``.
- .. note:: Modifying the the window and packet sizes might have adverse
- effects on the session created. The default values are the same
- as in the OpenSSH code base and have been battle tested.
- :param int window_size:
- optional window size for this session.
- :param int max_packet_size:
- optional max packet size for this session.
- :return: a new `.Channel`
- :raises SSHException: if the request is rejected or the session ends
- prematurely
- .. versionchanged:: 1.15
- Added the ``window_size`` and ``max_packet_size`` arguments.
- """
- return self.open_channel('session',
- window_size=window_size,
- max_packet_size=max_packet_size,
- timeout=timeout)
- def open_x11_channel(self, src_addr=None):
- """
- Request a new channel to the client, of type ``"x11"``. This
- is just an alias for ``open_channel('x11', src_addr=src_addr)``.
- :param tuple src_addr:
- the source address (``(str, int)``) of the x11 server (port is the
- x11 port, ie. 6010)
- :return: a new `.Channel`
- :raises SSHException: if the request is rejected or the session ends
- prematurely
- """
- return self.open_channel('x11', src_addr=src_addr)
- def open_forward_agent_channel(self):
- """
- Request a new channel to the client, of type
- ``"auth-agent@openssh.com"``.
- This is just an alias for ``open_channel('auth-agent@openssh.com')``.
- :return: a new `.Channel`
- :raises SSHException:
- if the request is rejected or the session ends prematurely
- """
- return self.open_channel('auth-agent@openssh.com')
- def open_forwarded_tcpip_channel(self, src_addr, dest_addr):
- """
- Request a new channel back to the client, of type ``"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 dest_addr: local (server) connected address
- """
- return self.open_channel('forwarded-tcpip', dest_addr, src_addr)
- def open_channel(self,
- kind,
- dest_addr=None,
- src_addr=None,
- window_size=None,
- max_packet_size=None,
- timeout=None):
- """
- Request a new channel to the server. `Channels <.Channel>` are
- socket-like objects used for the actual transfer of data across the
- session. You may only request a channel after negotiating encryption
- (using `connect` or `start_client`) and authenticating.
- .. note:: Modifying the the window and packet sizes might have adverse
- effects on the channel created. The default values are the same
- as in the OpenSSH code base and have been battle tested.
- :param str kind:
- the kind of channel requested (usually ``"session"``,
- ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``)
- :param tuple dest_addr:
- the destination address (address + port tuple) of this port
- forwarding, if ``kind`` is ``"forwarded-tcpip"`` or
- ``"direct-tcpip"`` (ignored for other channel types)
- :param src_addr: the source address of this port forwarding, if
- ``kind`` is ``"forwarded-tcpip"``, ``"direct-tcpip"``, or ``"x11"``
- :param int window_size:
- optional window size for this session.
- :param int max_packet_size:
- optional max packet size for this session.
- :param float timeout:
- optional timeout opening a channel, default 3600s (1h)
- :return: a new `.Channel` on success
- :raises SSHException: if the request is rejected, the session ends
- prematurely or there is a timeout openning a channel
- .. versionchanged:: 1.15
- Added the ``window_size`` and ``max_packet_size`` arguments.
- """
- if not self.active:
- raise SSHException('SSH session not active')
- timeout = 3600 if timeout is None else timeout
- self.lock.acquire()
- try:
- window_size = self._sanitize_window_size(window_size)
- max_packet_size = self._sanitize_packet_size(max_packet_size)
- chanid = self._next_channel()
- m = Message()
- m.add_byte(cMSG_CHANNEL_OPEN)
- m.add_string(kind)
- m.add_int(chanid)
- m.add_int(window_size)
- m.add_int(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(window_size, max_packet_size)
- finally:
- self.lock.release()
- self._send_user_message(m)
- start_ts = time.time()
- 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.is_set():
- break
- elif start_ts + timeout < time.time():
- raise SSHException('Timeout openning channel.')
- 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 ``server_addr`` and ``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
- `accept`.
- :param str address: the address to bind when forwarding
- :param int port:
- the port to forward, or 0 to ask the server to allocate any port
- :param callable handler:
- optional handler for incoming forwarded connections, of the form
- ``func(Channel, (str, int), (str, int))``.
- :return: the port number (`int`) allocated by the server
- :raises SSHException: if the server refused the TCP forward request
- """
- if not self.active:
- raise SSHException('SSH session not active')
- 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, dest_addr_port):
- #src_addr, src_port = src_addr_port
- #dest_addr, dest_port = dest_addr_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 str address: the address to stop forwarding
- :param int port: the port to stop forwarding
- """
- 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 `.SFTPClient` referring to an sftp session (channel) across
- this transport
- """
- return SFTPClient.from_transport(self)
- def send_ignore(self, byte_count=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 int byte_count:
- the number of random bytes to send in the payload of the ignored
- packet -- defaults to a random number from 10 to 41.
- """
- m = Message()
- m.add_byte(cMSG_IGNORE)
- if byte_count is None:
- byte_count = (byte_ord(os.urandom(1)) % 32) + 10
- m.add_bytes(os.urandom(byte_count))
- 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.
- :raises 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.is_set():
- break
- return
- def set_keepalive(self, interval):
- """
- Turn on/off keepalive packets (default is off). If this is set, after
- ``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 int interval:
- seconds to wait before sending a keepalive packet (or
- 0 to disable keepalives).
- """
- 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 str kind: name of the request.
- :param tuple data:
- an optional tuple containing additional data to attach to the
- request.
- :param bool wait:
- ``True`` if this method should not return until a response is
- received; ``False`` otherwise.
- :return:
- a `.Message` containing possible additional data if the request was
- successful (or an empty `.Message` if ``wait`` was ``False``);
- ``None`` if the request was denied.
- """
- if wait:
- self.completion_event = threading.Event()
- m = Message()
- m.add_byte(cMSG_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.is_set():
- 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, ``None``
- is returned.
- :param int timeout:
- seconds to wait for a channel, or ``None`` to wait forever
- :return: a new `.Channel` opened by the client
- """
- 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,
- gss_host=None, gss_auth=False, gss_kex=False, gss_deleg_creds=True):
- """
- 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 `start_client`, `get_remote_server_key`, and
- `Transport.auth_password` or `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 `open_channel` or
- `open_session` to get a `.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 `open_channel` or `open_session` call may
- fail because you haven't authenticated yet.
- :param .PKey hostkey:
- the host key expected from the server, or ``None`` if you don't
- want to do host key verification.
- :param str username: the username to authenticate as.
- :param str password:
- a password to use for authentication, if you want to use password
- authentication; otherwise ``None``.
- :param .PKey pkey:
- a private key to use for authentication, if you want to use private
- key authentication; otherwise ``None``.
- :param str gss_host:
- The target's name in the kerberos database. Default: hostname
- :param bool gss_auth:
- ``True`` if you want to use GSS-API authentication.
- :param bool gss_kex:
- Perform GSS-API Key Exchange and user authentication.
- :param bool gss_deleg_creds:
- Whether to delegate GSS-API client credentials.
- :raises 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 GSS-API Key Exchange was performed, we are not required to check
- # the host key.
- if (hostkey is not None) and not gss_kex:
- key = self.get_remote_server_key()
- if (key.get_name() != hostkey.get_name()) or (key.asbytes() != hostkey.asbytes()):
- self._log(DEBUG, 'Bad host key from server')
- self._log(DEBUG, 'Expected: %s: %s' % (hostkey.get_name(), repr(hostkey.asbytes())))
- self._log(DEBUG, 'Got : %s: %s' % (key.get_name(), repr(key.asbytes())))
- 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) or gss_auth or gss_kex:
- if gss_auth:
- self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-with-mic)')
- self.auth_gssapi_with_mic(username, gss_host, gss_deleg_creds)
- elif gss_kex:
- self._log(DEBUG, 'Attempting GSS-API auth... (gssapi-keyex)')
- self.auth_gssapi_keyex(username)
- elif pkey is not None:
- self._log(DEBUG, 'Attempting public-key auth...')
- self.auth_publickey(username, pkey)
- else:
- self._log(DEBUG, 'Attempting password auth...')
- self.auth_password(username, password)
- 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 `start_client`. The exception (if any) is cleared after
- this call.
- :return:
- an exception, or ``None`` if there is no stored exception.
- .. versionadded:: 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 `.SubsystemHandler` for more
- detailed documentation.
- Any extra parameters (including keyword arguments) are saved and
- passed to the `.SubsystemHandler` constructor later.
- :param str name: name of the subsystem.
- :param class handler:
- subclass of `.SubsystemHandler` that handles this subsystem.
- """
- 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.
- """
- 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 ``None``.
- :return: username that was authenticated (a `str`), or ``None``.
- """
- if not self.active or (self.auth_handler is None):
- return None
- return self.auth_handler.get_username()
- def get_banner(self):
- """
- Return the banner supplied by the server upon connect. If no banner is
- supplied, this method returns ``None``.
- :returns: server supplied banner (`str`), or ``None``.
- .. versionadded:: 1.13
- """
- if not self.active or (self.auth_handler is None):
- return None
- return self.auth_handler.banner
- 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
- `.BadAuthenticationType` exception raised.
- :param str username: the username to authenticate as
- :return:
- `list` of auth types permissible for the next stage of
- authentication (normally empty)
- :raises BadAuthenticationType: if "none" authentication isn't allowed
- by the server for this user
- :raises SSHException: if the authentication failed due to a network
- error
- .. versionadded:: 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 ``event`` is passed in, this method will return immediately, and
- the event will be triggered once authentication succeeds or fails. On
- success, `is_authenticated` will return ``True``. On failure, you may
- use `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 ``fallback`` is ``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 str username: the username to authenticate as
- :param basestring password: the password to authenticate with
- :param .threading.Event event:
- an event to trigger when the authentication attempt is complete
- (whether it was successful or not)
- :param bool fallback:
- ``True`` if an attempt at an automated "interactive" password auth
- should be made if the server doesn't support normal password auth
- :return:
- `list` of auth types permissible for the next stage of
- authentication (normally empty)
- :raises BadAuthenticationType: if password authentication isn't
- allowed by the server for this user (and no event was passed in)
- :raises AuthenticationException: if the authentication failed (and no
- event was passed in)
- :raises 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 ses