sshtunnel-0.1.4/0000755000175000001440000000000013307725441015227 5ustar fernandezjmusers00000000000000sshtunnel-0.1.4/sshtunnel.egg-info/0000755000175000001440000000000013307725441020744 5ustar fernandezjmusers00000000000000sshtunnel-0.1.4/sshtunnel.egg-info/top_level.txt0000644000175000001440000000001213307725441023467 0ustar fernandezjmusers00000000000000sshtunnel sshtunnel-0.1.4/sshtunnel.egg-info/dependency_links.txt0000644000175000001440000000000113307725441025012 0ustar fernandezjmusers00000000000000 sshtunnel-0.1.4/sshtunnel.egg-info/PKG-INFO0000644000175000001440000004554113307725441022052 0ustar fernandezjmusers00000000000000Metadata-Version: 1.1 Name: sshtunnel Version: 0.1.4 Summary: Pure python SSH tunnels Home-page: https://github.com/pahaz/sshtunnel Author: Pahaz Blinov Author-email: pahaz.blinov@gmail.com License: MIT Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip Description-Content-Type: UNKNOWN Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| |pyversions| |license| **Author**: `Pahaz Blinov`_ **Repo**: https://github.com/pahaz/sshtunnel/ Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on Windows. See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py Requirements ------------- * `paramiko`_ Installation ============ `sshtunnel`_ is on PyPI, so simply run: :: pip install sshtunnel or :: easy_install sshtunnel or :: conda install -c conda-forge sshtunnel to have it installed in your environment. For installing from source, clone the `repo `_ and run:: python setup.py install Testing the package ------------------- In order to run the tests you first need `tox `_ and run:: python setup.py test Usage scenarios =============== One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the figure below. User may need to connect a port of a remote server (i.e. 8080) where only SSH port (usually port 22) is reachable. :: ---------------------------------------------------------------------- | -------------+ | +----------+ LOCAL | | | REMOTE | :22 SSH CLIENT | <== SSH ========> | SERVER | :8080 web service -------------+ | +----------+ | FIREWALL (only port 22 is open) ---------------------------------------------------------------------- **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. If allowed by the SSH server, it is also possible to reach a private server (from the perspective of ``REMOTE SERVER``) not directly visible from the outside (``LOCAL CLIENT``'s perspective). :: ---------------------------------------------------------------------- | -------------+ | +----------+ +--------- LOCAL | | | REMOTE | | PRIVATE CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER -------------+ | +----------+ +--------- | FIREWALL (only port 443 is open) ---------------------------------------------------------------------- **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. Usage examples ============== API allows either initializing the tunnel and starting it or using a ``with`` context, which will take care of starting **and stopping** the tunnel: Example 1 --------- Code corresponding to **Fig1** above follows, given remote server's address is ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind port. .. code-block:: py from sshtunnel import SSHTunnelForwarder server = SSHTunnelForwarder( 'pahaz.urfuclub.ru', ssh_username="pahaz", ssh_password="secret", remote_bind_address=('127.0.0.1', 8080) ) server.start() print(server.local_bind_port) # show assigned local port # work with `SECRET SERVICE` through `server.local_bind_port`. server.stop() Example 2 --------- Example of a port forwarding to a private server not directly reachable, assuming password protected pkey authentication, remote server's SSH service is listening on port 443 and that port is open in the firewall (**Fig2**): .. code-block:: py import paramiko from sshtunnel import SSHTunnelForwarder with SSHTunnelForwarder( (REMOTE_SERVER_IP, 443), ssh_username="", ssh_pkey="/var/ssh/rsa_key", ssh_private_key_password="secret", remote_bind_address=(PRIVATE_SERVER_IP, 22), local_bind_address=('0.0.0.0', 10022) ) as tunnel: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect('127.0.0.1', 10022) # do some operations with client session client.close() print('FINISH!') Example 3 --------- Example of a port forwarding for the Vagrant MySQL local port: .. code-block:: py from sshtunnel import SSHTunnelForwarder from time import sleep with SSHTunnelForwarder( ('localhost', 2222), ssh_username="vagrant", ssh_password="vagrant", remote_bind_address=('127.0.0.1', 3306) ) as server: print(server.local_bind_port) while True: # press Ctrl-C for stopping sleep(1) print('FINISH!') Or simply using the CLI: .. code-block:: console (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost CLI usage ========= :: $ sshtunnel --help usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v] [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]] ssh_address Pure python ssh tunnel utils Version 0.1.4 positional arguments: ssh_address SSH server IP address (GW for SSH tunnels) set with "-- ssh_address" if immediately after -R or -L optional arguments: -h, --help show this help message and exit -U SSH_USERNAME, --username SSH_USERNAME SSH server account username -p SSH_PORT, --server_port SSH_PORT SSH server TCP port (default: 22) -P SSH_PASSWORD, --password SSH_PASSWORD SSH server account password -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT If port is omitted, defaults to 22. Example: -R 10.10.10.10: 10.10.10.10:5900 -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n Elements may also be valid UNIX socket domains: /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. By default it will listen in all interfaces (0.0.0.0) and choose a random port. Example: -L :40000 -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY Gateway's host key -K KEY_FILE, --private_key_file KEY_FILE RSA/DSS/ECDSA private key file -S KEY_PASSWORD, --private_key_password KEY_PASSWORD RSA/DSS/ECDSA private key password -t, --threaded Allow concurrent connections to each tunnel -v, --verbose Increase output verbosity (default: ERROR) -V, --version Show version number and quit -x IP:PORT, --proxy IP:PORT IP and port of SSH proxy to destination -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE SSH configuration file, defaults to ~/.ssh/config -z, --compress Request server for compression over SSH transport -n, --noagent Disable looking for keys from an SSH agent -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] List of directories where SSH pkeys (in the format `id_*`) may be found .. _Pahaz Blinov: https://github.com/pahaz .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel .. _paramiko: http://www.paramiko.org/ .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg :target: https://circleci.com/gh/pahaz/sshtunnel .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail :target: https://ci.appveyor.com/project/pahaz/sshtunnel .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg :target: `sshtunnel`_ .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE Online documentation ==================== Documentation may be found at `readthedocs`_. .. _readthedocs: https://sshtunnel.readthedocs.org/ CONTRIBUTORS ============ - `Cameron Maske`_ - `Gustavo Machado`_ - `Colin Jermain`_ - `JM Fernández`_ - (big thanks!) - `Lewis Thompson`_ - `Erik Rogers`_ - `Mart Sõmermaa`_ - `Chronial`_ - `Dan Harbin`_ - `Ignacio Peluffo`_ - `Niels Zeilemaker`_ CHANGELOG ========= - v.0.1.4 (`Niels Zeilemaker`_) + Allow loading pkeys from `~/.ssh` - v.0.1.3 (`Ignacio Peluffo`_ and others) + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` + Several bugfixes - v.0.1.2 (`JM Fernández`_) + Fix #77 - v.0.1.1 (`JM Fernández`_) + Fix #72 - v.0.1.0 (`JM Fernández`_) + Add `tunnel_bindings` property + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) (`Pahaz Blinov`_, `JM Fernández`_) + Add TRACE logging level (`JM Fernández`_) + Code and tests refactoring (`JM Fernández`_) + Drop python3.2 support - v.0.0.8 (`JM Fernández`_) + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) + Simplify API (`JM Fernández`_) + Add sphinx-based documentation (`JM Fernández`_) + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) + Add ``compression`` (`JM Fernández`_) + Add ``__str__`` method (`JM Fernández`_) + Add test functions (`JM Fernández`_) + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) + Fix gateway IP unresolvable exception catching (`JM Fernández`_) + Minor fixes (`JM Fernández`_) + Add AppVeyor support (`JM Fernández`_) - v.0.0.7 (`JM Fernández`_) + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) + Allow sending a pkey directly (`#43`_) (`Chronial`_) + Add ``-V`` CLI option to show current version (`JM Fernández`_) + Add coverage (`JM Fernández`_) + Refactoring (`JM Fernández`_) - v.0.0.6 (`Pahaz Blinov`_) + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_) - v.0.0.5 (`Pahaz Blinov`_) + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_) + add tests (`Pahaz Blinov`_) + add CI integration (`Pahaz Blinov`_) + normal packaging (`Pahaz Blinov`_) + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior] + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior] - v.0.0.4.4 (`Pahaz Blinov`_) + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_) - v.0.0.4.3 (`Pahaz Blinov`_) + fix default port issue `#19`_ (`Pahaz Blinov`_) - v.0.0.4.2 (`Pahaz Blinov`_) + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) - v.0.0.4.1 (`Pahaz Blinov`_) + fix CLI issues `#13`_ (`Pahaz Blinov`_) - v.0.0.4 (`Pahaz Blinov`_) + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible* + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible* + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible* + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* + add CLI interface (`JM Fernández`_) + support opening several tunnels at once (`JM Fernández`_) + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_) + improve logging (`JM Fernández`_, `Pahaz Blinov`_) + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_) + add ``ssh_config_file`` argument support (`JM Fernández`_) + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_) - v.0.0.3 (`Pahaz Blinov`_) + add ``threaded`` option (`Cameron Maske`_) + fix exception error message, correctly printing destination address (`Gustavo Machado`_) + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_) - v.0.0.1 (`Pahaz Blinov`_) + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_) + ``open`` function (`Pahaz Blinov`_) .. _Cameron Maske: https://github.com/cameronmaske .. _Gustavo Machado: https://github.com/gdmachado .. _Colin Jermain: https://github.com/cjermain .. _JM Fernández: https://github.com/fernandezcuesta .. _Lewis Thompson: https://github.com/lewisthompson .. _Erik Rogers: https://github.com/ewrogers .. _Mart Sõmermaa: https://github.com/mrts .. _Chronial: https://github.com/Chronial .. _Dan Harbin: https://github.com/RasterBurn .. _Ignacio Peluffo: https://github.com/ipeluffo .. _Niels Zeilemaker: https://github.com/NielsZeilemaker .. _#13: https://github.com/pahaz/sshtunnel/issues/13 .. _#16: https://github.com/pahaz/sshtunnel/issues/16 .. _#19: https://github.com/pahaz/sshtunnel/issues/19 .. _#21: https://github.com/pahaz/sshtunnel/issues/21 .. _#24: https://github.com/pahaz/sshtunnel/issues/24 .. _#29: https://github.com/pahaz/sshtunnel/issues/29 .. _#31: https://github.com/pahaz/sshtunnel/issues/31 .. _#33: https://github.com/pahaz/sshtunnel/issues/33 .. _#34: https://github.com/pahaz/sshtunnel/issues/34 .. _#36: https://github.com/pahaz/sshtunnel/issues/36 .. _#41: https://github.com/pahaz/sshtunnel/issues/41 .. _#43: https://github.com/pahaz/sshtunnel/issues/43 .. _#46: https://github.com/pahaz/sshtunnel/issues/46 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 Keywords: ssh tunnel paramiko proxy tcp-forward Platform: unix Platform: macos Platform: windows Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Build Tools Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 sshtunnel-0.1.4/sshtunnel.egg-info/SOURCES.txt0000644000175000001440000000075713307725441022641 0ustar fernandezjmusers00000000000000LICENSE MANIFEST.in README.rst Troubleshoot.rst changelog.rst docs.rst setup.cfg setup.py sshtunnel.py docs/Makefile docs/conf.py docs/index.rst docs/requirements-docs.txt sshtunnel.egg-info/PKG-INFO sshtunnel.egg-info/SOURCES.txt sshtunnel.egg-info/dependency_links.txt sshtunnel.egg-info/entry_points.txt sshtunnel.egg-info/requires.txt sshtunnel.egg-info/top_level.txt tests/__init__.py tests/__init__.pyc tests/test_forwarder.py tests/testconfig tests/testrsa.key tests/testrsa_encrypted.keysshtunnel-0.1.4/sshtunnel.egg-info/entry_points.txt0000644000175000001440000000006313307725441024241 0ustar fernandezjmusers00000000000000[console_scripts] sshtunnel = sshtunnel:_cli_main sshtunnel-0.1.4/sshtunnel.egg-info/requires.txt0000644000175000001440000000015013307725441023340 0ustar fernandezjmusers00000000000000paramiko>=1.15.2 [build_sphinx] sphinx sphinxcontrib-napoleon [dev] check-manifest [test] tox>=1.8.1 sshtunnel-0.1.4/MANIFEST.in0000644000175000001440000000047413307461664016776 0ustar fernandezjmusers00000000000000# Include the data files recursive-include data * # If using Python 2.6 or less, then have to include package data, even though # it's already declared in setup.py # include sample/*.dat include LICENSE include *.rst include docs/conf.py include docs/Makefile include docs/*.rst include docs/*.txt include tests/* sshtunnel-0.1.4/docs.rst0000644000175000001440000000021613307461664016714 0ustar fernandezjmusers00000000000000Online documentation ==================== Documentation may be found at `readthedocs`_. .. _readthedocs: https://sshtunnel.readthedocs.org/ sshtunnel-0.1.4/README.rst0000644000175000001440000002163213307723072016720 0ustar fernandezjmusers00000000000000|CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| |pyversions| |license| **Author**: `Pahaz Blinov`_ **Repo**: https://github.com/pahaz/sshtunnel/ Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on Windows. See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py Requirements ------------- * `paramiko`_ Installation ============ `sshtunnel`_ is on PyPI, so simply run: :: pip install sshtunnel or :: easy_install sshtunnel or :: conda install -c conda-forge sshtunnel to have it installed in your environment. For installing from source, clone the `repo `_ and run:: python setup.py install Testing the package ------------------- In order to run the tests you first need `tox `_ and run:: python setup.py test Usage scenarios =============== One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the figure below. User may need to connect a port of a remote server (i.e. 8080) where only SSH port (usually port 22) is reachable. :: ---------------------------------------------------------------------- | -------------+ | +----------+ LOCAL | | | REMOTE | :22 SSH CLIENT | <== SSH ========> | SERVER | :8080 web service -------------+ | +----------+ | FIREWALL (only port 22 is open) ---------------------------------------------------------------------- **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. If allowed by the SSH server, it is also possible to reach a private server (from the perspective of ``REMOTE SERVER``) not directly visible from the outside (``LOCAL CLIENT``'s perspective). :: ---------------------------------------------------------------------- | -------------+ | +----------+ +--------- LOCAL | | | REMOTE | | PRIVATE CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER -------------+ | +----------+ +--------- | FIREWALL (only port 443 is open) ---------------------------------------------------------------------- **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. Usage examples ============== API allows either initializing the tunnel and starting it or using a ``with`` context, which will take care of starting **and stopping** the tunnel: Example 1 --------- Code corresponding to **Fig1** above follows, given remote server's address is ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind port. .. code-block:: py from sshtunnel import SSHTunnelForwarder server = SSHTunnelForwarder( 'pahaz.urfuclub.ru', ssh_username="pahaz", ssh_password="secret", remote_bind_address=('127.0.0.1', 8080) ) server.start() print(server.local_bind_port) # show assigned local port # work with `SECRET SERVICE` through `server.local_bind_port`. server.stop() Example 2 --------- Example of a port forwarding to a private server not directly reachable, assuming password protected pkey authentication, remote server's SSH service is listening on port 443 and that port is open in the firewall (**Fig2**): .. code-block:: py import paramiko from sshtunnel import SSHTunnelForwarder with SSHTunnelForwarder( (REMOTE_SERVER_IP, 443), ssh_username="", ssh_pkey="/var/ssh/rsa_key", ssh_private_key_password="secret", remote_bind_address=(PRIVATE_SERVER_IP, 22), local_bind_address=('0.0.0.0', 10022) ) as tunnel: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect('127.0.0.1', 10022) # do some operations with client session client.close() print('FINISH!') Example 3 --------- Example of a port forwarding for the Vagrant MySQL local port: .. code-block:: py from sshtunnel import SSHTunnelForwarder from time import sleep with SSHTunnelForwarder( ('localhost', 2222), ssh_username="vagrant", ssh_password="vagrant", remote_bind_address=('127.0.0.1', 3306) ) as server: print(server.local_bind_port) while True: # press Ctrl-C for stopping sleep(1) print('FINISH!') Or simply using the CLI: .. code-block:: console (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost CLI usage ========= :: $ sshtunnel --help usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v] [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]] ssh_address Pure python ssh tunnel utils Version 0.1.4 positional arguments: ssh_address SSH server IP address (GW for SSH tunnels) set with "-- ssh_address" if immediately after -R or -L optional arguments: -h, --help show this help message and exit -U SSH_USERNAME, --username SSH_USERNAME SSH server account username -p SSH_PORT, --server_port SSH_PORT SSH server TCP port (default: 22) -P SSH_PASSWORD, --password SSH_PASSWORD SSH server account password -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT If port is omitted, defaults to 22. Example: -R 10.10.10.10: 10.10.10.10:5900 -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n Elements may also be valid UNIX socket domains: /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. By default it will listen in all interfaces (0.0.0.0) and choose a random port. Example: -L :40000 -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY Gateway's host key -K KEY_FILE, --private_key_file KEY_FILE RSA/DSS/ECDSA private key file -S KEY_PASSWORD, --private_key_password KEY_PASSWORD RSA/DSS/ECDSA private key password -t, --threaded Allow concurrent connections to each tunnel -v, --verbose Increase output verbosity (default: ERROR) -V, --version Show version number and quit -x IP:PORT, --proxy IP:PORT IP and port of SSH proxy to destination -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE SSH configuration file, defaults to ~/.ssh/config -z, --compress Request server for compression over SSH transport -n, --noagent Disable looking for keys from an SSH agent -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] List of directories where SSH pkeys (in the format `id_*`) may be found .. _Pahaz Blinov: https://github.com/pahaz .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel .. _paramiko: http://www.paramiko.org/ .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg :target: https://circleci.com/gh/pahaz/sshtunnel .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail :target: https://ci.appveyor.com/project/pahaz/sshtunnel .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg :target: `sshtunnel`_ .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE sshtunnel-0.1.4/sshtunnel.py0000644000175000001440000017767713307723072017653 0ustar fernandezjmusers00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- """ *sshtunnel* - Initiate SSH tunnels via a remote gateway. ``sshtunnel`` works by opening a port forwarding SSH connection in the background, using threads. The connection(s) are closed when explicitly calling the :meth:`SSHTunnelForwarder.stop` method or using it as a context. """ import os import sys import socket import getpass import logging import argparse import warnings import threading from select import select from binascii import hexlify import paramiko if sys.version_info[0] < 3: # pragma: no cover import Queue as queue import SocketServer as socketserver string_types = basestring, # noqa input_ = raw_input # noqa else: import queue import socketserver string_types = str input_ = input __version__ = '0.1.4' __author__ = 'pahaz' DEFAULT_LOGLEVEL = logging.ERROR #: default level if no logger passed (ERROR) TUNNEL_TIMEOUT = 1.0 #: Timeout (seconds) for tunnel connection DAEMON = False TRACE_LEVEL = 1 _CONNECTION_COUNTER = 1 _LOCK = threading.Lock() #: Timeout (seconds) for the connection to the SSH gateway, ``None`` to disable SSH_TIMEOUT = None DEPRECATIONS = { 'ssh_address': 'ssh_address_or_host', 'ssh_host': 'ssh_address_or_host', 'ssh_private_key': 'ssh_pkey', 'raise_exception_if_any_forwarder_have_a_problem': 'mute_exceptions' } logging.addLevelName(TRACE_LEVEL, 'TRACE') if os.name == 'posix': DEFAULT_SSH_DIRECTORY = '~/.ssh' UnixStreamServer = socketserver.UnixStreamServer else: DEFAULT_SSH_DIRECTORY = '~/ssh' UnixStreamServer = socketserver.TCPServer #: Path of optional ssh configuration file SSH_CONFIG_FILE = os.path.join(DEFAULT_SSH_DIRECTORY, 'config') ######################## # # # Utils # # # ######################## def check_host(host): assert isinstance(host, string_types), 'IP is not a string ({0})'.format( type(host).__name__ ) def check_port(port): assert isinstance(port, int), 'PORT is not a number' assert port >= 0, 'PORT < 0 ({0})'.format(port) def check_address(address): """ Check if the format of the address is correct Arguments: address (tuple): (``str``, ``int``) representing an IP address and port, respectively .. note:: alternatively a local ``address`` can be a ``str`` when working with UNIX domain sockets, if supported by the platform Raises: ValueError: raised when address has an incorrect format Example: >>> check_address(('127.0.0.1', 22)) """ if isinstance(address, tuple): check_host(address[0]) check_port(address[1]) elif isinstance(address, string_types): if os.name != 'posix': raise ValueError('Platform does not support UNIX domain sockets') if not (os.path.exists(address) or os.access(os.path.dirname(address), os.W_OK)): raise ValueError('ADDRESS not a valid socket domain socket ({0})' .format(address)) else: raise ValueError('ADDRESS is not a tuple, string, or character buffer ' '({0})'.format(type(address).__name__)) def check_addresses(address_list, is_remote=False): """ Check if the format of the addresses is correct Arguments: address_list (list[tuple]): Sequence of (``str``, ``int``) pairs, each representing an IP address and port respectively .. note:: when supported by the platform, one or more of the elements in the list can be of type ``str``, representing a valid UNIX domain socket is_remote (boolean): Whether or not the address list Raises: AssertionError: raised when ``address_list`` contains an invalid element ValueError: raised when any address in the list has an incorrect format Example: >>> check_addresses([('127.0.0.1', 22), ('127.0.0.1', 2222)]) """ assert all(isinstance(x, (tuple, string_types)) for x in address_list) if (is_remote and any(isinstance(x, string_types) for x in address_list)): raise AssertionError('UNIX domain sockets not allowed for remote' 'addresses') for address in address_list: check_address(address) def create_logger(logger=None, loglevel=None, capture_warnings=True, add_paramiko_handler=True): """ Attach or create a new logger and add a console handler if not present Arguments: logger (Optional[logging.Logger]): :class:`logging.Logger` instance; a new one is created if this argument is empty loglevel (Optional[str or int]): :class:`logging.Logger`'s level, either as a string (i.e. ``ERROR``) or in numeric format (10 == ``DEBUG``) .. note:: a value of 1 == ``TRACE`` enables Tracing mode capture_warnings (boolean): Enable/disable capturing the events logged by the warnings module into ``logger``'s handlers Default: True .. note:: ignored in python 2.6 add_paramiko_handler (boolean): Whether or not add a console handler for ``paramiko.transport``'s logger if no handler present Default: True Return: :class:`logging.Logger` """ logger = logger or logging.getLogger( '{0}.SSHTunnelForwarder'.format(__name__) ) if not any(isinstance(x, logging.Handler) for x in logger.handlers): logger.setLevel(loglevel or DEFAULT_LOGLEVEL) console_handler = logging.StreamHandler() _add_handler(logger, handler=console_handler, loglevel=loglevel or DEFAULT_LOGLEVEL) if loglevel: # override if loglevel was set logger.setLevel(loglevel) for handler in logger.handlers: handler.setLevel(loglevel) if add_paramiko_handler: _check_paramiko_handlers(logger=logger) if capture_warnings and sys.version_info >= (2, 7): logging.captureWarnings(True) pywarnings = logging.getLogger('py.warnings') pywarnings.handlers.extend(logger.handlers) return logger def _add_handler(logger, handler=None, loglevel=None): """ Add a handler to an existing logging.Logger object """ handler.setLevel(loglevel or DEFAULT_LOGLEVEL) if handler.level <= logging.DEBUG: _fmt = '%(asctime)s| %(levelname)-4.3s|%(threadName)10.9s/' \ '%(lineno)04d@%(module)-10.9s| %(message)s' handler.setFormatter(logging.Formatter(_fmt)) else: handler.setFormatter(logging.Formatter( '%(asctime)s| %(levelname)-8s| %(message)s' )) logger.addHandler(handler) def _check_paramiko_handlers(logger=None): """ Add a console handler for paramiko.transport's logger if not present """ paramiko_logger = logging.getLogger('paramiko.transport') if not paramiko_logger.handlers: if logger: paramiko_logger.handlers = logger.handlers else: console_handler = logging.StreamHandler() console_handler.setFormatter( logging.Formatter('%(asctime)s | %(levelname)-8s| PARAMIKO: ' '%(lineno)03d@%(module)-10s| %(message)s') ) paramiko_logger.addHandler(console_handler) def address_to_str(address): if isinstance(address, tuple): return '{0[0]}:{0[1]}'.format(address) return str(address) def get_connection_id(): global _CONNECTION_COUNTER with _LOCK: uid = _CONNECTION_COUNTER _CONNECTION_COUNTER += 1 return uid def _remove_none_values(dictionary): """ Remove dictionary keys whose value is None """ return list(map(dictionary.pop, [i for i in dictionary if dictionary[i] is None])) ######################## # # # Errors # # # ######################## class BaseSSHTunnelForwarderError(Exception): """ Exception raised by :class:`SSHTunnelForwarder` errors """ def __init__(self, *args, **kwargs): self.value = kwargs.pop('value', args[0] if args else '') def __str__(self): return self.value class HandlerSSHTunnelForwarderError(BaseSSHTunnelForwarderError): """ Exception for Tunnel forwarder errors """ pass ######################## # # # Handlers # # # ######################## class _ForwardHandler(socketserver.BaseRequestHandler): """ Base handler for tunnel connections """ remote_address = None ssh_transport = None logger = None info = None def _redirect(self, chan): while chan.active: rqst, _, _ = select([self.request, chan], [], [], 5) if self.request in rqst: data = self.request.recv(1024) if not data: break self.logger.log(TRACE_LEVEL, '>>> OUT {0} send to {1}: {2} >>>'.format( self.info, self.remote_address, hexlify(data) )) chan.send(data) if chan in rqst: # else if not chan.recv_ready(): break data = chan.recv(1024) self.logger.log( TRACE_LEVEL, '<<< IN {0} recv: {1} <<<'.format(self.info, hexlify(data)) ) self.request.send(data) def handle(self): uid = get_connection_id() self.info = '#{0} <-- {1}'.format(uid, self.client_address or self.server.local_address) src_address = self.request.getpeername() if not isinstance(src_address, tuple): src_address = ('dummy', 12345) try: chan = self.ssh_transport.open_channel( kind='direct-tcpip', dest_addr=self.remote_address, src_addr=src_address, timeout=TUNNEL_TIMEOUT ) except paramiko.SSHException: chan = None if chan is None: msg = '{0} to {1} was rejected by the SSH server'.format( self.info, self.remote_address ) self.logger.log(TRACE_LEVEL, msg) raise HandlerSSHTunnelForwarderError(msg) self.logger.log(TRACE_LEVEL, '{0} connected'.format(self.info)) try: self._redirect(chan) except socket.error: # Sometimes a RST is sent and a socket error is raised, treat this # exception. It was seen that a 3way FIN is processed later on, so # no need to make an ordered close of the connection here or raise # the exception beyond this point... self.logger.log(TRACE_LEVEL, '{0} sending RST'.format(self.info)) except Exception as e: self.logger.log(TRACE_LEVEL, '{0} error: {1}'.format(self.info, repr(e))) finally: chan.close() self.request.close() self.logger.log(TRACE_LEVEL, '{0} connection closed.'.format(self.info)) class _ForwardServer(socketserver.TCPServer): # Not Threading """ Non-threading version of the forward server """ allow_reuse_address = True # faster rebinding def __init__(self, *args, **kwargs): self.logger = create_logger(kwargs.pop('logger', None)) self.tunnel_ok = queue.Queue() socketserver.TCPServer.__init__(self, *args, **kwargs) def handle_error(self, request, client_address): (exc_class, exc, tb) = sys.exc_info() self.logger.error('Could not establish connection from {0} to remote ' 'side of the tunnel'.format(request.getsockname())) self.tunnel_ok.put(False) @property def local_address(self): return self.server_address @property def local_host(self): return self.server_address[0] @property def local_port(self): return self.server_address[1] @property def remote_address(self): return self.RequestHandlerClass.remote_address @property def remote_host(self): return self.RequestHandlerClass.remote_address[0] @property def remote_port(self): return self.RequestHandlerClass.remote_address[1] class _ThreadingForwardServer(socketserver.ThreadingMixIn, _ForwardServer): """ Allow concurrent connections to each tunnel """ # If True, cleanly stop threads created by ThreadingMixIn when quitting daemon_threads = DAEMON class _UnixStreamForwardServer(UnixStreamServer): """ Serve over UNIX domain sockets (does not work on Windows) """ def __init__(self, *args, **kwargs): self.logger = create_logger(kwargs.pop('logger', None)) self.tunnel_ok = queue.Queue() UnixStreamServer.__init__(self, *args, **kwargs) @property def local_address(self): return self.server_address @property def local_host(self): return None @property def local_port(self): return None @property def remote_address(self): return self.RequestHandlerClass.remote_address @property def remote_host(self): return self.RequestHandlerClass.remote_address[0] @property def remote_port(self): return self.RequestHandlerClass.remote_address[1] class _ThreadingUnixStreamForwardServer(socketserver.ThreadingMixIn, _UnixStreamForwardServer): """ Allow concurrent connections to each tunnel """ # If True, cleanly stop threads created by ThreadingMixIn when quitting daemon_threads = DAEMON class SSHTunnelForwarder(object): """ **SSH tunnel class** - Initialize a SSH tunnel to a remote host according to the input arguments - Optionally: + Read an SSH configuration file (typically ``~/.ssh/config``) + Load keys from a running SSH agent (i.e. Pageant, GNOME Keyring) Raises: :class:`.BaseSSHTunnelForwarderError`: raised by SSHTunnelForwarder class methods :class:`.HandlerSSHTunnelForwarderError`: raised by tunnel forwarder threads .. note:: Attributes ``mute_exceptions`` and ``raise_exception_if_any_forwarder_have_a_problem`` (deprecated) may be used to silence most exceptions raised from this class Keyword Arguments: ssh_address_or_host (tuple or str): IP or hostname of ``REMOTE GATEWAY``. It may be a two-element tuple (``str``, ``int``) representing IP and port respectively, or a ``str`` representing the IP address only .. versionadded:: 0.0.4 ssh_config_file (str): SSH configuration file that will be read. If explicitly set to ``None``, parsing of this configuration is omitted Default: :const:`SSH_CONFIG_FILE` .. versionadded:: 0.0.4 ssh_host_key (str): Representation of a line in an OpenSSH-style "known hosts" file. ``REMOTE GATEWAY``'s key fingerprint will be compared to this host key in order to prevent against SSH server spoofing. Important when using passwords in order not to accidentally do a login attempt to a wrong (perhaps an attacker's) machine ssh_username (str): Username to authenticate as in ``REMOTE SERVER`` Default: current local user name ssh_password (str): Text representing the password used to connect to ``REMOTE SERVER`` or for unlocking a private key. .. note:: Avoid coding secret password directly in the code, since this may be visible and make your service vulnerable to attacks ssh_port (int): Optional port number of the SSH service on ``REMOTE GATEWAY``, when `ssh_address_or_host`` is a ``str`` representing the IP part of ``REMOTE GATEWAY``'s address Default: 22 ssh_pkey (str or paramiko.PKey): **Private** key file name (``str``) to obtain the public key from or a **public** key (:class:`paramiko.pkey.PKey`) ssh_private_key_password (str): Password for an encrypted ``ssh_pkey`` .. note:: Avoid coding secret password directly in the code, since this may be visible and make your service vulnerable to attacks ssh_proxy (socket-like object or tuple): Proxy where all SSH traffic will be passed through. It might be for example a :class:`paramiko.proxy.ProxyCommand` instance. See either the :class:`paramiko.transport.Transport`'s sock parameter documentation or ``ProxyCommand`` in ``ssh_config(5)`` for more information. It is also possible to specify the proxy address as a tuple of type (``str``, ``int``) representing proxy's IP and port .. note:: Ignored if ``ssh_proxy_enabled`` is False .. versionadded:: 0.0.5 ssh_proxy_enabled (boolean): Enable/disable SSH proxy. If True and user's ``ssh_config_file`` contains a ``ProxyCommand`` directive that matches the specified ``ssh_address_or_host``, a :class:`paramiko.proxy.ProxyCommand` object will be created where all SSH traffic will be passed through Default: ``True`` .. versionadded:: 0.0.4 local_bind_address (tuple): Local tuple in the format (``str``, ``int``) representing the IP and port of the local side of the tunnel. Both elements in the tuple are optional so both ``('', 8000)`` and ``('10.0.0.1', )`` are valid values Default: ``('0.0.0.0', RANDOM_PORT)`` .. versionchanged:: 0.0.8 Added the ability to use a UNIX domain socket as local bind address local_bind_addresses (list[tuple]): In case more than one tunnel is established at once, a list of tuples (in the same format as ``local_bind_address``) can be specified, such as [(ip1, port_1), (ip_2, port2), ...] Default: ``[local_bind_address]`` .. versionadded:: 0.0.4 remote_bind_address (tuple): Remote tuple in the format (``str``, ``int``) representing the IP and port of the remote side of the tunnel. remote_bind_addresses (list[tuple]): In case more than one tunnel is established at once, a list of tuples (in the same format as ``remote_bind_address``) can be specified, such as [(ip1, port_1), (ip_2, port2), ...] Default: ``[remote_bind_address]`` .. versionadded:: 0.0.4 allow_agent (boolean): Enable/disable load of keys from an SSH agent Default: ``True`` .. versionadded:: 0.0.8 host_pkey_directories (list): Look for pkeys in folders on this list, for example ['~/.ssh']. An empty list disables this feature Default: ``None`` .. versionadded:: 0.1.4 compression (boolean): Turn on/off transport compression. By default compression is disabled since it may negatively affect interactive sessions Default: ``False`` .. versionadded:: 0.0.8 logger (logging.Logger): logging instance for sshtunnel and paramiko Default: :class:`logging.Logger` instance with a single :class:`logging.StreamHandler` handler and :const:`DEFAULT_LOGLEVEL` level .. versionadded:: 0.0.3 mute_exceptions (boolean): Allow silencing :class:`BaseSSHTunnelForwarderError` or :class:`HandlerSSHTunnelForwarderError` exceptions when enabled Default: ``False`` .. versionadded:: 0.0.8 set_keepalive (float): Interval in seconds defining the period in which, if no data was sent 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 Default: 0.0 (no keepalive packets are sent) .. versionadded:: 0.0.7 threaded (boolean): Allow concurrent connections over a single tunnel Default: ``True`` .. versionadded:: 0.0.3 ssh_address (str): Superseded by ``ssh_address_or_host``, tuple of type (str, int) representing the IP and port of ``REMOTE SERVER`` .. deprecated:: 0.0.4 ssh_host (str): Superseded by ``ssh_address_or_host``, tuple of type (str, int) representing the IP and port of ``REMOTE SERVER`` .. deprecated:: 0.0.4 ssh_private_key (str or paramiko.PKey): Superseded by ``ssh_pkey``, which can represent either a **private** key file name (``str``) or a **public** key (:class:`paramiko.pkey.PKey`) .. deprecated:: 0.0.8 raise_exception_if_any_forwarder_have_a_problem (boolean): Allow silencing :class:`BaseSSHTunnelForwarderError` or :class:`HandlerSSHTunnelForwarderError` exceptions when set to False Default: ``True`` .. versionadded:: 0.0.4 .. deprecated:: 0.0.8 (use ``mute_exceptions`` instead) Attributes: tunnel_is_up (dict): Describe whether or not the other side of the tunnel was reported to be up (and we must close it) or not (skip shutting down that tunnel) .. note:: This attribute should not be modified .. note:: When :attr:`.skip_tunnel_checkup` is disabled or the local bind is a UNIX socket, the value will always be ``True`` **Example**:: {('127.0.0.1', 55550): True, # this tunnel is up ('127.0.0.1', 55551): False} # this one isn't where 55550 and 55551 are the local bind ports skip_tunnel_checkup (boolean): Disable tunnel checkup (default for backwards compatibility). .. versionadded:: 0.1.0 """ skip_tunnel_checkup = True daemon_forward_servers = DAEMON #: flag tunnel threads in daemon mode daemon_transport = DAEMON #: flag SSH transport thread in daemon mode def local_is_up(self, target): """ Check if a tunnel is up (remote target's host is reachable on TCP target's port) Arguments: target (tuple): tuple of type (``str``, ``int``) indicating the listen IP address and port Return: boolean .. deprecated:: 0.1.0 Replaced by :meth:`.check_tunnels()` and :attr:`.tunnel_is_up` """ try: check_address(target) except ValueError: self.logger.warning('Target must be a tuple (IP, port), where IP ' 'is a string (i.e. "192.168.0.1") and port is ' 'an integer (i.e. 40000). Alternatively ' 'target can be a valid UNIX domain socket.') return False if self.skip_tunnel_checkup: # force tunnel check at this point self.skip_tunnel_checkup = False self.check_tunnels() self.skip_tunnel_checkup = True # roll it back return self.tunnel_is_up.get(target, True) def _make_ssh_forward_handler_class(self, remote_address_): """ Make SSH Handler class """ class Handler(_ForwardHandler): remote_address = remote_address_ ssh_transport = self._transport logger = self.logger return Handler def _make_ssh_forward_server_class(self, remote_address_): return _ThreadingForwardServer if self._threaded else _ForwardServer def _make_unix_ssh_forward_server_class(self, remote_address_): return _ThreadingUnixStreamForwardServer if \ self._threaded else _UnixStreamForwardServer def _make_ssh_forward_server(self, remote_address, local_bind_address): """ Make SSH forward proxy Server class """ _Handler = self._make_ssh_forward_handler_class(remote_address) try: if isinstance(local_bind_address, string_types): forward_maker_class = self._make_unix_ssh_forward_server_class else: forward_maker_class = self._make_ssh_forward_server_class _Server = forward_maker_class(remote_address) ssh_forward_server = _Server( local_bind_address, _Handler, logger=self.logger, ) if ssh_forward_server: ssh_forward_server.daemon_threads = self.daemon_forward_servers self._server_list.append(ssh_forward_server) self.tunnel_is_up[ssh_forward_server.server_address] = False else: self._raise( BaseSSHTunnelForwarderError, 'Problem setting up ssh {0} <> {1} forwarder. You can ' 'suppress this exception by using the `mute_exceptions`' 'argument'.format(address_to_str(local_bind_address), address_to_str(remote_address)) ) except IOError: self._raise( BaseSSHTunnelForwarderError, "Couldn't open tunnel {0} <> {1} might be in use or " "destination not reachable".format( address_to_str(local_bind_address), address_to_str(remote_address) ) ) def __init__( self, ssh_address_or_host=None, ssh_config_file=SSH_CONFIG_FILE, ssh_host_key=None, ssh_password=None, ssh_pkey=None, ssh_private_key_password=None, ssh_proxy=None, ssh_proxy_enabled=True, ssh_username=None, local_bind_address=None, local_bind_addresses=None, logger=None, mute_exceptions=False, remote_bind_address=None, remote_bind_addresses=None, set_keepalive=0.0, threaded=True, # old version False compression=None, allow_agent=True, # look for keys from an SSH agent host_pkey_directories=None, # look for keys in ~/.ssh *args, **kwargs # for backwards compatibility ): self.logger = logger or create_logger() # Ensure paramiko.transport has a console handler _check_paramiko_handlers(logger=logger) self.ssh_host_key = ssh_host_key self.set_keepalive = set_keepalive self._server_list = [] # reset server list self.tunnel_is_up = {} # handle tunnel status self._threaded = threaded self.is_alive = False # Check if deprecated arguments ssh_address or ssh_host were used for deprecated_argument in ['ssh_address', 'ssh_host']: ssh_address_or_host = self._process_deprecated(ssh_address_or_host, deprecated_argument, kwargs) # other deprecated arguments ssh_pkey = self._process_deprecated(ssh_pkey, 'ssh_private_key', kwargs) self._raise_fwd_exc = self._process_deprecated( None, 'raise_exception_if_any_forwarder_have_a_problem', kwargs) or not mute_exceptions if isinstance(ssh_address_or_host, tuple): check_address(ssh_address_or_host) (ssh_host, ssh_port) = ssh_address_or_host else: ssh_host = ssh_address_or_host ssh_port = kwargs.pop('ssh_port', None) if kwargs: raise ValueError('Unknown arguments: {0}'.format(kwargs)) # remote binds self._remote_binds = self._get_binds(remote_bind_address, remote_bind_addresses, is_remote=True) # local binds self._local_binds = self._get_binds(local_bind_address, local_bind_addresses) self._local_binds = self._consolidate_binds(self._local_binds, self._remote_binds) (self.ssh_host, self.ssh_username, ssh_pkey, # still needs to go through _consolidate_auth self.ssh_port, self.ssh_proxy, self.compression) = self._read_ssh_config( ssh_host, ssh_config_file, ssh_username, ssh_pkey, ssh_port, ssh_proxy if ssh_proxy_enabled else None, compression, self.logger ) (self.ssh_password, self.ssh_pkeys) = self._consolidate_auth( ssh_password=ssh_password, ssh_pkey=ssh_pkey, ssh_pkey_password=ssh_private_key_password, allow_agent=allow_agent, host_pkey_directories=host_pkey_directories, logger=self.logger ) check_host(self.ssh_host) check_port(self.ssh_port) self.logger.info("Connecting to gateway: {0}:{1} as user '{2}'" .format(self.ssh_host, self.ssh_port, self.ssh_username)) self.logger.debug('Concurrent connections allowed: {0}' .format(self._threaded)) @staticmethod def _read_ssh_config(ssh_host, ssh_config_file, ssh_username=None, ssh_pkey=None, ssh_port=None, ssh_proxy=None, compression=None, logger=None): """ Read ssh_config_file and tries to look for user (ssh_username), identityfile (ssh_pkey), port (ssh_port) and proxycommand (ssh_proxy) entries for ssh_host """ ssh_config = paramiko.SSHConfig() if not ssh_config_file: # handle case where it's an empty string ssh_config_file = None # Try to read SSH_CONFIG_FILE try: # open the ssh config file with open(os.path.expanduser(ssh_config_file), 'r') as f: ssh_config.parse(f) # looks for information for the destination system hostname_info = ssh_config.lookup(ssh_host) # gather settings for user, port and identity file # last resort: use the 'login name' of the user ssh_username = ( ssh_username or hostname_info.get('user') ) ssh_pkey = ( ssh_pkey or hostname_info.get('identityfile', [None])[0] ) ssh_host = hostname_info.get('hostname') ssh_port = ssh_port or hostname_info.get('port') proxycommand = hostname_info.get('proxycommand') ssh_proxy = ssh_proxy or (paramiko.ProxyCommand(proxycommand) if proxycommand else None) if compression is None: compression = hostname_info.get('compression', '') compression = True if compression.upper() == 'YES' else False except IOError: if logger: logger.warning( 'Could not read SSH configuration file: {0}' .format(ssh_config_file) ) except (AttributeError, TypeError): # ssh_config_file is None if logger: logger.info('Skipping loading of ssh configuration file') finally: return (ssh_host, ssh_username or getpass.getuser(), ssh_pkey, int(ssh_port) if ssh_port else 22, # fallback value ssh_proxy, compression) @staticmethod def get_agent_keys(logger=None): """ Load public keys from any available SSH agent Arguments: logger (Optional[logging.Logger]) Return: list """ paramiko_agent = paramiko.Agent() agent_keys = paramiko_agent.get_keys() if logger: logger.info('{0} keys loaded from agent'.format(len(agent_keys))) return list(agent_keys) @staticmethod def get_keys(logger=None, host_pkey_directories=None, allow_agent=False): """ Load public keys from any available SSH agent or local .ssh directory. Arguments: logger (Optional[logging.Logger]) host_pkey_directories (Optional[list[str]]): List of local directories where host SSH pkeys in the format "id_*" are searched. For example, ['~/.ssh'] .. versionadded:: 0.1.0 allow_agent (Optional[boolean]): Whether or not load keys from agent Default: False Return: list """ keys = SSHTunnelForwarder.get_agent_keys(logger=logger) \ if allow_agent else [] if host_pkey_directories is not None: paramiko_key_types = {'rsa': paramiko.RSAKey, 'dsa': paramiko.DSSKey, 'ecdsa': paramiko.ECDSAKey, 'ed25519': paramiko.Ed25519Key} for directory in host_pkey_directories or [DEFAULT_SSH_DIRECTORY]: for keytype in paramiko_key_types.keys(): ssh_pkey_expanded = os.path.expanduser( os.path.join(directory, 'id_{}'.format(keytype)) ) if os.path.isfile(ssh_pkey_expanded): ssh_pkey = SSHTunnelForwarder.read_private_key_file( pkey_file=ssh_pkey_expanded, logger=logger, key_type=paramiko_key_types[keytype] ) if ssh_pkey: keys.append(ssh_pkey) if logger: logger.info('{0} keys loaded from host directory'.format( len(keys)) ) return keys @staticmethod def _consolidate_binds(local_binds, remote_binds): """ Fill local_binds with defaults when no value/s were specified, leaving paramiko to decide in which local port the tunnel will be open """ count = len(remote_binds) - len(local_binds) if count < 0: raise ValueError('Too many local bind addresses ' '(local_bind_addresses > remote_bind_addresses)') local_binds.extend([('0.0.0.0', 0) for x in range(count)]) return local_binds @staticmethod def _consolidate_auth(ssh_password=None, ssh_pkey=None, ssh_pkey_password=None, allow_agent=True, host_pkey_directories=None, logger=None): """ Get sure authentication information is in place. ``ssh_pkey`` may be of classes: - ``str`` - in this case it represents a private key file; public key will be obtained from it - ``paramiko.Pkey`` - it will be transparently added to loaded keys """ ssh_loaded_pkeys = SSHTunnelForwarder.get_keys( logger=logger, host_pkey_directories=host_pkey_directories, allow_agent=allow_agent ) if isinstance(ssh_pkey, string_types): ssh_pkey_expanded = os.path.expanduser(ssh_pkey) if os.path.exists(ssh_pkey_expanded): ssh_pkey = SSHTunnelForwarder.read_private_key_file( pkey_file=ssh_pkey_expanded, pkey_password=ssh_pkey_password or ssh_password, logger=logger ) elif logger: logger.warning('Private key file not found: {0}' .format(ssh_pkey)) if isinstance(ssh_pkey, paramiko.pkey.PKey): ssh_loaded_pkeys.append(ssh_pkey) if not ssh_password and not ssh_loaded_pkeys: raise ValueError('No password or public key available!') return (ssh_password, ssh_loaded_pkeys) def _raise(self, exception=BaseSSHTunnelForwarderError, reason=None): if self._raise_fwd_exc: raise exception(reason) else: self.logger.error(repr(exception(reason))) def _get_transport(self): """ Return the SSH transport to the remote gateway """ if self.ssh_proxy: if isinstance(self.ssh_proxy, paramiko.proxy.ProxyCommand): proxy_repr = repr(self.ssh_proxy.cmd[1]) else: proxy_repr = repr(self.ssh_proxy) self.logger.debug('Connecting via proxy: {0}'.format(proxy_repr)) _socket = self.ssh_proxy else: _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if isinstance(_socket, socket.socket): _socket.settimeout(SSH_TIMEOUT) _socket.connect((self.ssh_host, self.ssh_port)) transport = paramiko.Transport(_socket) transport.set_keepalive(self.set_keepalive) transport.use_compression(compress=self.compression) transport.daemon = self.daemon_transport return transport def _create_tunnels(self): """ Create SSH tunnels on top of a transport to the remote gateway """ if not self.is_active: try: self._connect_to_gateway() except socket.gaierror: # raised by paramiko.Transport msg = 'Could not resolve IP address for {0}, aborting!' \ .format(self.ssh_host) self.logger.error(msg) return except (paramiko.SSHException, socket.error) as e: template = 'Could not connect to gateway {0}:{1} : {2}' msg = template.format(self.ssh_host, self.ssh_port, e.args[0]) self.logger.error(msg) return for (rem, loc) in zip(self._remote_binds, self._local_binds): try: self._make_ssh_forward_server(rem, loc) except BaseSSHTunnelForwarderError as e: msg = 'Problem setting SSH Forwarder up: {0}'.format(e.value) self.logger.error(msg) @staticmethod def _get_binds(bind_address, bind_addresses, is_remote=False): addr_kind = 'remote' if is_remote else 'local' if not bind_address and not bind_addresses: if is_remote: raise ValueError("No {0} bind addresses specified. Use " "'{0}_bind_address' or '{0}_bind_addresses'" " argument".format(addr_kind)) else: return [] elif bind_address and bind_addresses: raise ValueError("You can't use both '{0}_bind_address' and " "'{0}_bind_addresses' arguments. Use one of " "them.".format(addr_kind)) if bind_address: bind_addresses = [bind_address] if not is_remote: # Add random port if missing in local bind for (i, local_bind) in enumerate(bind_addresses): if isinstance(local_bind, tuple) and len(local_bind) == 1: bind_addresses[i] = (local_bind[0], 0) check_addresses(bind_addresses, is_remote) return bind_addresses @staticmethod def _process_deprecated(attrib, deprecated_attrib, kwargs): """ Processes optional deprecate arguments """ if deprecated_attrib not in DEPRECATIONS: raise ValueError('{0} not included in deprecations list' .format(deprecated_attrib)) if deprecated_attrib in kwargs: warnings.warn("'{0}' is DEPRECATED use '{1}' instead" .format(deprecated_attrib, DEPRECATIONS[deprecated_attrib]), DeprecationWarning) if attrib: raise ValueError("You can't use both '{0}' and '{1}'. " "Please only use one of them" .format(deprecated_attrib, DEPRECATIONS[deprecated_attrib])) else: return kwargs.pop(deprecated_attrib) return attrib @staticmethod def read_private_key_file(pkey_file, pkey_password=None, key_type=None, logger=None): """ Get SSH Public key from a private key file, given an optional password Arguments: pkey_file (str): File containing a private key (RSA, DSS or ECDSA) Keyword Arguments: pkey_password (Optional[str]): Password to decrypt the private key logger (Optional[logging.Logger]) Return: paramiko.Pkey """ ssh_pkey = None for pkey_class in (key_type,) if key_type else ( paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey, paramiko.Ed25519Key ): try: ssh_pkey = pkey_class.from_private_key_file( pkey_file, password=pkey_password ) if logger: logger.debug('Private key file ({0}, {1}) successfully ' 'loaded'.format(pkey_file, pkey_class)) break except paramiko.PasswordRequiredException: if logger: logger.error('Password is required for key {0}' .format(pkey_file)) break except paramiko.SSHException: if logger: logger.debug('Private key file ({0}) could not be loaded ' 'as type {1} or bad password' .format(pkey_file, pkey_class)) return ssh_pkey def _check_tunnel(self, _srv): """ Check if tunnel is already established """ if self.skip_tunnel_checkup: self.tunnel_is_up[_srv.local_address] = True return self.logger.info('Checking tunnel to: {0}'.format(_srv.remote_address)) if isinstance(_srv.local_address, string_types): # UNIX stream s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) else: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(TUNNEL_TIMEOUT) try: # Windows raises WinError 10049 if trying to connect to 0.0.0.0 connect_to = ('127.0.0.1', _srv.local_port) \ if _srv.local_host == '0.0.0.0' else _srv.local_address s.connect(connect_to) self.tunnel_is_up[_srv.local_address] = _srv.tunnel_ok.get( timeout=TUNNEL_TIMEOUT * 1.1 ) self.logger.debug( 'Tunnel to {0} is DOWN'.format(_srv.remote_address) ) except socket.error: self.logger.debug( 'Tunnel to {0} is DOWN'.format(_srv.remote_address) ) self.tunnel_is_up[_srv.local_address] = False except queue.Empty: self.logger.debug( 'Tunnel to {0} is UP'.format(_srv.remote_address) ) self.tunnel_is_up[_srv.local_address] = True finally: s.close() def check_tunnels(self): """ Check that if all tunnels are established and populates :attr:`.tunnel_is_up` """ for _srv in self._server_list: self._check_tunnel(_srv) def start(self): """ Start the SSH tunnels """ if self.is_alive: self.logger.warning('Already started!') return self._create_tunnels() if not self.is_active: self._raise(BaseSSHTunnelForwarderError, reason='Could not establish session to SSH gateway') for _srv in self._server_list: thread = threading.Thread( target=self._serve_forever_wrapper, args=(_srv, ), name='Srv-{0}'.format(address_to_str(_srv.local_port)) ) thread.daemon = self.daemon_forward_servers thread.start() self._check_tunnel(_srv) self.is_alive = any(self.tunnel_is_up.values()) if not self.is_alive: self._raise(HandlerSSHTunnelForwarderError, 'An error occurred while opening tunnels.') def stop(self): """ Shut the tunnel down. .. note:: This **had** to be handled with care before ``0.1.0``: - if a port redirection is opened - the destination is not reachable - we attempt a connection to that tunnel (``SYN`` is sent and acknowledged, then a ``FIN`` packet is sent and never acknowledged... weird) - we try to shutdown: it will not succeed until ``FIN_WAIT_2`` and ``CLOSE_WAIT`` time out. .. note:: Handle these scenarios with :attr:`.tunnel_is_up`: if False, server ``shutdown()`` will be skipped on that tunnel """ self.logger.info('Closing all open connections...') opened_address_text = ', '.join( (address_to_str(k.local_address) for k in self._server_list) ) or 'None' self.logger.debug('Listening tunnels: ' + opened_address_text) self._stop_transport() self._server_list = [] # reset server list self.tunnel_is_up = {} # reset tunnel status def close(self): """ Stop the an active tunnel, alias to :meth:`.stop` """ self.stop() def restart(self): """ Restart connection to the gateway and tunnels """ self.stop() self.start() def _connect_to_gateway(self): """ Open connection to SSH gateway - First try with all keys loaded from an SSH agent (if allowed) - Then with those passed directly or read from ~/.ssh/config - As last resort, try with a provided password """ for key in self.ssh_pkeys: self.logger.debug('Trying to log in with key: {0}' .format(hexlify(key.get_fingerprint()))) try: self._transport = self._get_transport() self._transport.connect(hostkey=self.ssh_host_key, username=self.ssh_username, pkey=key) if self._transport.is_alive: return except paramiko.AuthenticationException: self.logger.debug('Authentication error') self._stop_transport() if self.ssh_password: # avoid conflict using both pass and pkey self.logger.debug('Trying to log in with password: {0}' .format('*' * len(self.ssh_password))) try: self._transport = self._get_transport() self._transport.connect(hostkey=self.ssh_host_key, username=self.ssh_username, password=self.ssh_password) if self._transport.is_alive: return except paramiko.AuthenticationException: self.logger.debug('Authentication error') self._stop_transport() self.logger.error('Could not open connection to gateway') def _serve_forever_wrapper(self, _srv, poll_interval=0.1): """ Wrapper for the server created for a SSH forward """ self.logger.info('Opening tunnel: {0} <> {1}'.format( address_to_str(_srv.local_address), address_to_str(_srv.remote_address)) ) _srv.serve_forever(poll_interval) # blocks until finished self.logger.info('Tunnel: {0} <> {1} released'.format( address_to_str(_srv.local_address), address_to_str(_srv.remote_address)) ) def _stop_transport(self): """ Close the underlying transport when nothing more is needed """ try: self._check_is_started() except (BaseSSHTunnelForwarderError, HandlerSSHTunnelForwarderError) as e: self.logger.warning(e) for _srv in self._server_list: tunnel = _srv.local_address if self.tunnel_is_up[tunnel]: self.logger.info('Shutting down tunnel {0}'.format(tunnel)) _srv.shutdown() _srv.server_close() # clean up the UNIX domain socket if we're using one if isinstance(_srv, _UnixStreamForwardServer): try: os.unlink(_srv.local_address) except Exception as e: self.logger.error('Unable to unlink socket {0}: {1}' .format(self.local_address, repr(e))) self.is_alive = False if self.is_active: self._transport.close() self._transport.stop_thread() self.logger.debug('Transport is closed') @property def local_bind_port(self): # BACKWARDS COMPATIBILITY self._check_is_started() if len(self._server_list) != 1: raise BaseSSHTunnelForwarderError( 'Use .local_bind_ports property for more than one tunnel' ) return self.local_bind_ports[0] @property def local_bind_host(self): # BACKWARDS COMPATIBILITY self._check_is_started() if len(self._server_list) != 1: raise BaseSSHTunnelForwarderError( 'Use .local_bind_hosts property for more than one tunnel' ) return self.local_bind_hosts[0] @property def local_bind_address(self): # BACKWARDS COMPATIBILITY self._check_is_started() if len(self._server_list) != 1: raise BaseSSHTunnelForwarderError( 'Use .local_bind_addresses property for more than one tunnel' ) return self.local_bind_addresses[0] @property def local_bind_ports(self): """ Return a list containing the ports of local side of the TCP tunnels """ self._check_is_started() return [_server.local_port for _server in self._server_list if _server.local_port is not None] @property def local_bind_hosts(self): """ Return a list containing the IP addresses listening for the tunnels """ self._check_is_started() return [_server.local_host for _server in self._server_list if _server.local_host is not None] @property def local_bind_addresses(self): """ Return a list of (IP, port) pairs for the local side of the tunnels """ self._check_is_started() return [_server.local_address for _server in self._server_list] @property def tunnel_bindings(self): """ Return a dictionary containing the active local<>remote tunnel_bindings """ return dict((_server.remote_address, _server.local_address) for _server in self._server_list if self.tunnel_is_up[_server.local_address]) @property def is_active(self): """ Return True if the underlying SSH transport is up """ if ( '_transport' in self.__dict__ and self._transport.is_active() ): return True return False def _check_is_started(self): if not self.is_active: # underlying transport not alive msg = 'Server is not started. Please .start() first!' raise BaseSSHTunnelForwarderError(msg) if not self.is_alive: msg = 'Tunnels are not started. Please .start() first!' raise HandlerSSHTunnelForwarderError(msg) def __str__(self): credentials = { 'password': self.ssh_password, 'pkeys': [(key.get_name(), hexlify(key.get_fingerprint())) for key in self.ssh_pkeys] if any(self.ssh_pkeys) else None } _remove_none_values(credentials) template = os.linesep.join(['{0} object', 'ssh gateway: {1}:{2}', 'proxy: {3}', 'username: {4}', 'authentication: {5}', 'hostkey: {6}', 'status: {7}started', 'keepalive messages: {8}', 'tunnel connection check: {9}', 'concurrent connections: {10}allowed', 'compression: {11}requested', 'logging level: {12}', 'local binds: {13}', 'remote binds: {14}']) return (template.format( self.__class__, self.ssh_host, self.ssh_port, self.ssh_proxy.cmd[1] if self.ssh_proxy else 'no', self.ssh_username, credentials, self.ssh_host_key if self.ssh_host_key else'not checked', '' if self.is_alive else 'not ', 'disabled' if not self.set_keepalive else 'every {0} sec'.format(self.set_keepalive), 'disabled' if self.skip_tunnel_checkup else 'enabled', '' if self._threaded else 'not ', '' if self.compression else 'not ', logging.getLevelName(self.logger.level), self._local_binds, self._remote_binds, )) def __repr__(self): return self.__str__() def __enter__(self): try: self.start() return self except KeyboardInterrupt: self.__exit__() def __exit__(self, *args): self._stop_transport() def open_tunnel(*args, **kwargs): """ Open an SSH Tunnel, wrapper for :class:`SSHTunnelForwarder` Arguments: destination (Optional[tuple]): SSH server's IP address and port in the format (``ssh_address``, ``ssh_port``) Keyword Arguments: debug_level (Optional[int or str]): log level for :class:`logging.Logger` instance, i.e. ``DEBUG`` skip_tunnel_checkup (boolean): Enable/disable the local side check and populate :attr:`~SSHTunnelForwarder.tunnel_is_up` Default: True .. versionadded:: 0.1.0 .. note:: A value of ``debug_level`` set to 1 == ``TRACE`` enables tracing mode .. note:: See :class:`SSHTunnelForwarder` for keyword arguments **Example**:: from sshtunnel import open_tunnel with open_tunnel(SERVER, ssh_username=SSH_USER, ssh_port=22, ssh_password=SSH_PASSWORD, remote_bind_address=(REMOTE_HOST, REMOTE_PORT), local_bind_address=('', LOCAL_PORT)) as server: def do_something(port): pass print("LOCAL PORTS:", server.local_bind_port) do_something(server.local_bind_port) """ # Attach a console handler to the logger or create one if not passed kwargs['logger'] = create_logger(logger=kwargs.get('logger', None), loglevel=kwargs.pop('debug_level', None)) ssh_address_or_host = kwargs.pop('ssh_address_or_host', None) # Check if deprecated arguments ssh_address or ssh_host were used for deprecated_argument in ['ssh_address', 'ssh_host']: ssh_address_or_host = SSHTunnelForwarder._process_deprecated( ssh_address_or_host, deprecated_argument, kwargs ) ssh_port = kwargs.pop('ssh_port', None) skip_tunnel_checkup = kwargs.pop('skip_tunnel_checkup', True) if not args: if isinstance(ssh_address_or_host, tuple): args = (ssh_address_or_host, ) else: args = ((ssh_address_or_host, ssh_port), ) forwarder = SSHTunnelForwarder(*args, **kwargs) forwarder.skip_tunnel_checkup = skip_tunnel_checkup return forwarder def _bindlist(input_str): """ Define type of data expected for remote and local bind address lists Returns a tuple (ip_address, port) whose elements are (str, int) """ try: ip_port = input_str.split(':') if len(ip_port) == 1: _ip = ip_port[0] _port = None else: (_ip, _port) = ip_port if not _ip and not _port: raise AssertionError elif not _port: _port = '22' # default port if not given return _ip, int(_port) except ValueError: raise argparse.ArgumentTypeError( 'Address tuple must be of type IP_ADDRESS:PORT' ) except AssertionError: raise argparse.ArgumentTypeError("Both IP:PORT can't be missing!") def _parse_arguments(args=None): """ Parse arguments directly passed from CLI """ parser = argparse.ArgumentParser( description='Pure python ssh tunnel utils\n' 'Version {0}'.format(__version__), formatter_class=argparse.RawTextHelpFormatter ) parser.add_argument( 'ssh_address', type=str, help='SSH server IP address (GW for SSH tunnels)\n' 'set with "-- ssh_address" if immediately after ' '-R or -L' ) parser.add_argument( '-U', '--username', type=str, dest='ssh_username', help='SSH server account username' ) parser.add_argument( '-p', '--server_port', type=int, dest='ssh_port', default=22, help='SSH server TCP port (default: 22)' ) parser.add_argument( '-P', '--password', type=str, dest='ssh_password', help='SSH server account password' ) parser.add_argument( '-R', '--remote_bind_address', type=_bindlist, nargs='+', default=[], metavar='IP:PORT', required=True, dest='remote_bind_addresses', help='Remote bind address sequence: ' 'ip_1:port_1 ip_2:port_2 ... ip_n:port_n\n' 'Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT\n' 'If port is omitted, defaults to 22.\n' 'Example: -R 10.10.10.10: 10.10.10.10:5900' ) parser.add_argument( '-L', '--local_bind_address', type=_bindlist, nargs='*', dest='local_bind_addresses', metavar='IP:PORT', help='Local bind address sequence: ' 'ip_1:port_1 ip_2:port_2 ... ip_n:port_n\n' 'Elements may also be valid UNIX socket domains: \n' '/tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock\n' 'Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, ' 'being the local IP address optional.\n' 'By default it will listen in all interfaces ' '(0.0.0.0) and choose a random port.\n' 'Example: -L :40000' ) parser.add_argument( '-k', '--ssh_host_key', type=str, help="Gateway's host key" ) parser.add_argument( '-K', '--private_key_file', dest='ssh_private_key', metavar='KEY_FILE', type=str, help='RSA/DSS/ECDSA private key file' ) parser.add_argument( '-S', '--private_key_password', dest='ssh_private_key_password', metavar='KEY_PASSWORD', type=str, help='RSA/DSS/ECDSA private key password' ) parser.add_argument( '-t', '--threaded', action='store_true', help='Allow concurrent connections to each tunnel' ) parser.add_argument( '-v', '--verbose', action='count', default=0, help='Increase output verbosity (default: {0})'.format( logging.getLevelName(DEFAULT_LOGLEVEL) ) ) parser.add_argument( '-V', '--version', action='version', version='%(prog)s {version}'.format(version=__version__), help='Show version number and quit' ) parser.add_argument( '-x', '--proxy', type=_bindlist, dest='ssh_proxy', metavar='IP:PORT', help='IP and port of SSH proxy to destination' ) parser.add_argument( '-c', '--config', type=str, default=SSH_CONFIG_FILE, dest='ssh_config_file', help='SSH configuration file, defaults to {0}'.format(SSH_CONFIG_FILE) ) parser.add_argument( '-z', '--compress', action='store_true', dest='compression', help='Request server for compression over SSH transport' ) parser.add_argument( '-n', '--noagent', action='store_false', dest='allow_agent', help='Disable looking for keys from an SSH agent' ) parser.add_argument( '-d', '--host_pkey_directories', nargs='*', dest='host_pkey_directories', metavar='FOLDER', help='List of directories where SSH pkeys (in the format `id_*`) ' 'may be found' ) return vars(parser.parse_args(args)) def _cli_main(args=None): """ Pass input arguments to open_tunnel Mandatory: ssh_address, -R (remote bind address list) Optional: -U (username) we may gather it from SSH_CONFIG_FILE or current username -p (server_port), defaults to 22 -P (password) -L (local_bind_address), default to 0.0.0.0:22 -k (ssh_host_key) -K (private_key_file), may be gathered from SSH_CONFIG_FILE -S (private_key_password) -t (threaded), allow concurrent connections over tunnels -v (verbose), up to 3 (-vvv) to raise loglevel from ERROR to DEBUG -V (version) -x (proxy), ProxyCommand's IP:PORT, may be gathered from config file -c (ssh_config), ssh configuration file (defaults to SSH_CONFIG_FILE) -z (compress) -n (noagent), disable looking for keys from an Agent -d (host_pkey_directories), look for keys on these folders """ arguments = _parse_arguments(args) # Remove all "None" input values _remove_none_values(arguments) verbosity = min(arguments.pop('verbose'), 4) levels = [logging.ERROR, logging.WARNING, logging.INFO, logging.DEBUG, TRACE_LEVEL] arguments.setdefault('debug_level', levels[verbosity]) with open_tunnel(**arguments) as tunnel: if tunnel.is_alive: input_(''' Press or to stop! ''') if __name__ == '__main__': # pragma: no cover _cli_main() sshtunnel-0.1.4/PKG-INFO0000644000175000001440000004554113307725441016335 0ustar fernandezjmusers00000000000000Metadata-Version: 1.1 Name: sshtunnel Version: 0.1.4 Summary: Pure python SSH tunnels Home-page: https://github.com/pahaz/sshtunnel Author: Pahaz Blinov Author-email: pahaz.blinov@gmail.com License: MIT Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip Description-Content-Type: UNKNOWN Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| |pyversions| |license| **Author**: `Pahaz Blinov`_ **Repo**: https://github.com/pahaz/sshtunnel/ Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on Windows. See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py Requirements ------------- * `paramiko`_ Installation ============ `sshtunnel`_ is on PyPI, so simply run: :: pip install sshtunnel or :: easy_install sshtunnel or :: conda install -c conda-forge sshtunnel to have it installed in your environment. For installing from source, clone the `repo `_ and run:: python setup.py install Testing the package ------------------- In order to run the tests you first need `tox `_ and run:: python setup.py test Usage scenarios =============== One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the figure below. User may need to connect a port of a remote server (i.e. 8080) where only SSH port (usually port 22) is reachable. :: ---------------------------------------------------------------------- | -------------+ | +----------+ LOCAL | | | REMOTE | :22 SSH CLIENT | <== SSH ========> | SERVER | :8080 web service -------------+ | +----------+ | FIREWALL (only port 22 is open) ---------------------------------------------------------------------- **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. If allowed by the SSH server, it is also possible to reach a private server (from the perspective of ``REMOTE SERVER``) not directly visible from the outside (``LOCAL CLIENT``'s perspective). :: ---------------------------------------------------------------------- | -------------+ | +----------+ +--------- LOCAL | | | REMOTE | | PRIVATE CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER -------------+ | +----------+ +--------- | FIREWALL (only port 443 is open) ---------------------------------------------------------------------- **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. Usage examples ============== API allows either initializing the tunnel and starting it or using a ``with`` context, which will take care of starting **and stopping** the tunnel: Example 1 --------- Code corresponding to **Fig1** above follows, given remote server's address is ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind port. .. code-block:: py from sshtunnel import SSHTunnelForwarder server = SSHTunnelForwarder( 'pahaz.urfuclub.ru', ssh_username="pahaz", ssh_password="secret", remote_bind_address=('127.0.0.1', 8080) ) server.start() print(server.local_bind_port) # show assigned local port # work with `SECRET SERVICE` through `server.local_bind_port`. server.stop() Example 2 --------- Example of a port forwarding to a private server not directly reachable, assuming password protected pkey authentication, remote server's SSH service is listening on port 443 and that port is open in the firewall (**Fig2**): .. code-block:: py import paramiko from sshtunnel import SSHTunnelForwarder with SSHTunnelForwarder( (REMOTE_SERVER_IP, 443), ssh_username="", ssh_pkey="/var/ssh/rsa_key", ssh_private_key_password="secret", remote_bind_address=(PRIVATE_SERVER_IP, 22), local_bind_address=('0.0.0.0', 10022) ) as tunnel: client = paramiko.SSHClient() client.load_system_host_keys() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect('127.0.0.1', 10022) # do some operations with client session client.close() print('FINISH!') Example 3 --------- Example of a port forwarding for the Vagrant MySQL local port: .. code-block:: py from sshtunnel import SSHTunnelForwarder from time import sleep with SSHTunnelForwarder( ('localhost', 2222), ssh_username="vagrant", ssh_password="vagrant", remote_bind_address=('127.0.0.1', 3306) ) as server: print(server.local_bind_port) while True: # press Ctrl-C for stopping sleep(1) print('FINISH!') Or simply using the CLI: .. code-block:: console (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost CLI usage ========= :: $ sshtunnel --help usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v] [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]] ssh_address Pure python ssh tunnel utils Version 0.1.4 positional arguments: ssh_address SSH server IP address (GW for SSH tunnels) set with "-- ssh_address" if immediately after -R or -L optional arguments: -h, --help show this help message and exit -U SSH_USERNAME, --username SSH_USERNAME SSH server account username -p SSH_PORT, --server_port SSH_PORT SSH server TCP port (default: 22) -P SSH_PASSWORD, --password SSH_PASSWORD SSH server account password -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT If port is omitted, defaults to 22. Example: -R 10.10.10.10: 10.10.10.10:5900 -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n Elements may also be valid UNIX socket domains: /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. By default it will listen in all interfaces (0.0.0.0) and choose a random port. Example: -L :40000 -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY Gateway's host key -K KEY_FILE, --private_key_file KEY_FILE RSA/DSS/ECDSA private key file -S KEY_PASSWORD, --private_key_password KEY_PASSWORD RSA/DSS/ECDSA private key password -t, --threaded Allow concurrent connections to each tunnel -v, --verbose Increase output verbosity (default: ERROR) -V, --version Show version number and quit -x IP:PORT, --proxy IP:PORT IP and port of SSH proxy to destination -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE SSH configuration file, defaults to ~/.ssh/config -z, --compress Request server for compression over SSH transport -n, --noagent Disable looking for keys from an SSH agent -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] List of directories where SSH pkeys (in the format `id_*`) may be found .. _Pahaz Blinov: https://github.com/pahaz .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel .. _paramiko: http://www.paramiko.org/ .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg :target: https://circleci.com/gh/pahaz/sshtunnel .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail :target: https://ci.appveyor.com/project/pahaz/sshtunnel .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg :target: `sshtunnel`_ .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE Online documentation ==================== Documentation may be found at `readthedocs`_. .. _readthedocs: https://sshtunnel.readthedocs.org/ CONTRIBUTORS ============ - `Cameron Maske`_ - `Gustavo Machado`_ - `Colin Jermain`_ - `JM Fernández`_ - (big thanks!) - `Lewis Thompson`_ - `Erik Rogers`_ - `Mart Sõmermaa`_ - `Chronial`_ - `Dan Harbin`_ - `Ignacio Peluffo`_ - `Niels Zeilemaker`_ CHANGELOG ========= - v.0.1.4 (`Niels Zeilemaker`_) + Allow loading pkeys from `~/.ssh` - v.0.1.3 (`Ignacio Peluffo`_ and others) + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` + Several bugfixes - v.0.1.2 (`JM Fernández`_) + Fix #77 - v.0.1.1 (`JM Fernández`_) + Fix #72 - v.0.1.0 (`JM Fernández`_) + Add `tunnel_bindings` property + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) (`Pahaz Blinov`_, `JM Fernández`_) + Add TRACE logging level (`JM Fernández`_) + Code and tests refactoring (`JM Fernández`_) + Drop python3.2 support - v.0.0.8 (`JM Fernández`_) + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) + Simplify API (`JM Fernández`_) + Add sphinx-based documentation (`JM Fernández`_) + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) + Add ``compression`` (`JM Fernández`_) + Add ``__str__`` method (`JM Fernández`_) + Add test functions (`JM Fernández`_) + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) + Fix gateway IP unresolvable exception catching (`JM Fernández`_) + Minor fixes (`JM Fernández`_) + Add AppVeyor support (`JM Fernández`_) - v.0.0.7 (`JM Fernández`_) + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) + Allow sending a pkey directly (`#43`_) (`Chronial`_) + Add ``-V`` CLI option to show current version (`JM Fernández`_) + Add coverage (`JM Fernández`_) + Refactoring (`JM Fernández`_) - v.0.0.6 (`Pahaz Blinov`_) + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_) - v.0.0.5 (`Pahaz Blinov`_) + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_) + add tests (`Pahaz Blinov`_) + add CI integration (`Pahaz Blinov`_) + normal packaging (`Pahaz Blinov`_) + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior] + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior] - v.0.0.4.4 (`Pahaz Blinov`_) + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_) - v.0.0.4.3 (`Pahaz Blinov`_) + fix default port issue `#19`_ (`Pahaz Blinov`_) - v.0.0.4.2 (`Pahaz Blinov`_) + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) - v.0.0.4.1 (`Pahaz Blinov`_) + fix CLI issues `#13`_ (`Pahaz Blinov`_) - v.0.0.4 (`Pahaz Blinov`_) + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible* + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible* + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible* + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* + add CLI interface (`JM Fernández`_) + support opening several tunnels at once (`JM Fernández`_) + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_) + improve logging (`JM Fernández`_, `Pahaz Blinov`_) + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_) + add ``ssh_config_file`` argument support (`JM Fernández`_) + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_) - v.0.0.3 (`Pahaz Blinov`_) + add ``threaded`` option (`Cameron Maske`_) + fix exception error message, correctly printing destination address (`Gustavo Machado`_) + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_) - v.0.0.1 (`Pahaz Blinov`_) + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_) + ``open`` function (`Pahaz Blinov`_) .. _Cameron Maske: https://github.com/cameronmaske .. _Gustavo Machado: https://github.com/gdmachado .. _Colin Jermain: https://github.com/cjermain .. _JM Fernández: https://github.com/fernandezcuesta .. _Lewis Thompson: https://github.com/lewisthompson .. _Erik Rogers: https://github.com/ewrogers .. _Mart Sõmermaa: https://github.com/mrts .. _Chronial: https://github.com/Chronial .. _Dan Harbin: https://github.com/RasterBurn .. _Ignacio Peluffo: https://github.com/ipeluffo .. _Niels Zeilemaker: https://github.com/NielsZeilemaker .. _#13: https://github.com/pahaz/sshtunnel/issues/13 .. _#16: https://github.com/pahaz/sshtunnel/issues/16 .. _#19: https://github.com/pahaz/sshtunnel/issues/19 .. _#21: https://github.com/pahaz/sshtunnel/issues/21 .. _#24: https://github.com/pahaz/sshtunnel/issues/24 .. _#29: https://github.com/pahaz/sshtunnel/issues/29 .. _#31: https://github.com/pahaz/sshtunnel/issues/31 .. _#33: https://github.com/pahaz/sshtunnel/issues/33 .. _#34: https://github.com/pahaz/sshtunnel/issues/34 .. _#36: https://github.com/pahaz/sshtunnel/issues/36 .. _#41: https://github.com/pahaz/sshtunnel/issues/41 .. _#43: https://github.com/pahaz/sshtunnel/issues/43 .. _#46: https://github.com/pahaz/sshtunnel/issues/46 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 Keywords: ssh tunnel paramiko proxy tcp-forward Platform: unix Platform: macos Platform: windows Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Topic :: Software Development :: Build Tools Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 sshtunnel-0.1.4/changelog.rst0000644000175000001440000001346113307723432017713 0ustar fernandezjmusers00000000000000CONTRIBUTORS ============ - `Cameron Maske`_ - `Gustavo Machado`_ - `Colin Jermain`_ - `JM Fernández`_ - (big thanks!) - `Lewis Thompson`_ - `Erik Rogers`_ - `Mart Sõmermaa`_ - `Chronial`_ - `Dan Harbin`_ - `Ignacio Peluffo`_ - `Niels Zeilemaker`_ CHANGELOG ========= - v.0.1.4 (`Niels Zeilemaker`_) + Allow loading pkeys from `~/.ssh` - v.0.1.3 (`Ignacio Peluffo`_ and others) + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` + Several bugfixes - v.0.1.2 (`JM Fernández`_) + Fix #77 - v.0.1.1 (`JM Fernández`_) + Fix #72 - v.0.1.0 (`JM Fernández`_) + Add `tunnel_bindings` property + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) (`Pahaz Blinov`_, `JM Fernández`_) + Add TRACE logging level (`JM Fernández`_) + Code and tests refactoring (`JM Fernández`_) + Drop python3.2 support - v.0.0.8 (`JM Fernández`_) + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) + Simplify API (`JM Fernández`_) + Add sphinx-based documentation (`JM Fernández`_) + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) + Add ``compression`` (`JM Fernández`_) + Add ``__str__`` method (`JM Fernández`_) + Add test functions (`JM Fernández`_) + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) + Fix gateway IP unresolvable exception catching (`JM Fernández`_) + Minor fixes (`JM Fernández`_) + Add AppVeyor support (`JM Fernández`_) - v.0.0.7 (`JM Fernández`_) + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) + Allow sending a pkey directly (`#43`_) (`Chronial`_) + Add ``-V`` CLI option to show current version (`JM Fernández`_) + Add coverage (`JM Fernández`_) + Refactoring (`JM Fernández`_) - v.0.0.6 (`Pahaz Blinov`_) + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_) - v.0.0.5 (`Pahaz Blinov`_) + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_) + add tests (`Pahaz Blinov`_) + add CI integration (`Pahaz Blinov`_) + normal packaging (`Pahaz Blinov`_) + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior] + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior] - v.0.0.4.4 (`Pahaz Blinov`_) + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_) - v.0.0.4.3 (`Pahaz Blinov`_) + fix default port issue `#19`_ (`Pahaz Blinov`_) - v.0.0.4.2 (`Pahaz Blinov`_) + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) - v.0.0.4.1 (`Pahaz Blinov`_) + fix CLI issues `#13`_ (`Pahaz Blinov`_) - v.0.0.4 (`Pahaz Blinov`_) + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible* + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible* + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible* + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* + add CLI interface (`JM Fernández`_) + support opening several tunnels at once (`JM Fernández`_) + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_) + improve logging (`JM Fernández`_, `Pahaz Blinov`_) + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_) + add ``ssh_config_file`` argument support (`JM Fernández`_) + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_) - v.0.0.3 (`Pahaz Blinov`_) + add ``threaded`` option (`Cameron Maske`_) + fix exception error message, correctly printing destination address (`Gustavo Machado`_) + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_) - v.0.0.1 (`Pahaz Blinov`_) + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_) + ``open`` function (`Pahaz Blinov`_) .. _Cameron Maske: https://github.com/cameronmaske .. _Gustavo Machado: https://github.com/gdmachado .. _Colin Jermain: https://github.com/cjermain .. _JM Fernández: https://github.com/fernandezcuesta .. _Lewis Thompson: https://github.com/lewisthompson .. _Erik Rogers: https://github.com/ewrogers .. _Mart Sõmermaa: https://github.com/mrts .. _Chronial: https://github.com/Chronial .. _Dan Harbin: https://github.com/RasterBurn .. _Ignacio Peluffo: https://github.com/ipeluffo .. _Niels Zeilemaker: https://github.com/NielsZeilemaker .. _#13: https://github.com/pahaz/sshtunnel/issues/13 .. _#16: https://github.com/pahaz/sshtunnel/issues/16 .. _#19: https://github.com/pahaz/sshtunnel/issues/19 .. _#21: https://github.com/pahaz/sshtunnel/issues/21 .. _#24: https://github.com/pahaz/sshtunnel/issues/24 .. _#29: https://github.com/pahaz/sshtunnel/issues/29 .. _#31: https://github.com/pahaz/sshtunnel/issues/31 .. _#33: https://github.com/pahaz/sshtunnel/issues/33 .. _#34: https://github.com/pahaz/sshtunnel/issues/34 .. _#36: https://github.com/pahaz/sshtunnel/issues/36 .. _#41: https://github.com/pahaz/sshtunnel/issues/41 .. _#43: https://github.com/pahaz/sshtunnel/issues/43 .. _#46: https://github.com/pahaz/sshtunnel/issues/46 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 sshtunnel-0.1.4/docs/0000755000175000001440000000000013307725441016157 5ustar fernandezjmusers00000000000000sshtunnel-0.1.4/docs/conf.py0000644000175000001440000002305613307461664017470 0ustar fernandezjmusers00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # sshtunnel documentation build configuration file, created by # sphinx-quickstart on Mon Feb 22 11:01:56 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os # Patch to disable warning on non-local image import sphinx.environment from docutils.utils import get_source_line def _warn_node(self, msg, node): if not msg.startswith('nonlocal image URI found:'): self._warnfunc(msg, '%s:%s' % get_source_line(node)) sphinx.environment.BuildEnvironment.warn_node = _warn_node # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinxcontrib.napoleon', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'sshtunnel' copyright = '2014-2016, Pahaz Blinov and contributors' author = 'Pahaz Blinov' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.0.8' # The full version, including alpha/beta/rc tags. release = '0.0.8' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'sshtunneldoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'sshtunnel.tex', 'sshtunnel Documentation', 'Pahaz Blinov', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'sshtunnel', 'sshtunnel Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'sshtunnel', 'sshtunnel Documentation', author, 'sshtunnel', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False intersphinx_mapping = { 'paramiko': ('http://docs.paramiko.org/en/latest', None), 'python': ('https://docs.python.org/', None), } sshtunnel-0.1.4/docs/requirements-docs.txt0000644000175000001440000000007313307461664022375 0ustar fernandezjmusers00000000000000docutils==0.12 sphinx==1.3.5 sphinxcontrib-napoleon==0.5.0 sshtunnel-0.1.4/docs/index.rst0000644000175000001440000000050313307461664020022 0ustar fernandezjmusers00000000000000.. sshtunnel documentation master file Welcome to sshtunnel's documentation! ===================================== .. include:: ../README.rst API === .. toctree:: :maxdepth: 3 .. automodule:: sshtunnel :members: :member-order: bysource .. include:: ../changelog.rst License ======= .. include:: ../LICENSEsshtunnel-0.1.4/docs/Makefile0000644000175000001440000001676413307461664017641 0ustar fernandezjmusers00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sshtunnel.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sshtunnel.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/sshtunnel" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sshtunnel" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." sshtunnel-0.1.4/tests/0000755000175000001440000000000013307725441016371 5ustar fernandezjmusers00000000000000sshtunnel-0.1.4/tests/__init__.py0000644000175000001440000000032013307461664020501 0ustar fernandezjmusers00000000000000# the inclusion of the tests module is not meant to offer best practices for # testing in general, but rather to support the `find_packages` example in # setup.py that excludes installing the "tests" package sshtunnel-0.1.4/tests/testconfig0000644000175000001440000000023413307461664020464 0ustar fernandezjmusers00000000000000Host * User test Compression yes IdentityFile testrsa.key Host test ProxyCommand ssh -q -W %h:%p sshproxy Host other Port 222 Hostname 10.0.0.1 sshtunnel-0.1.4/tests/testrsa.key0000644000175000001440000000156313307461664020601 0ustar fernandezjmusers00000000000000-----BEGIN RSA PRIVATE KEY----- MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/ d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0 EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA 4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7 -----END RSA PRIVATE KEY----- sshtunnel-0.1.4/tests/test_forwarder.py0000644000175000001440000015742413307723072022010 0ustar fernandezjmusers00000000000000from __future__ import with_statement import os import sys import random import select import socket import getpass import logging import argparse import warnings import threading from os import path, linesep from functools import partial from contextlib import contextmanager import mock import paramiko import sshtunnel import shutil import tempfile if sys.version_info[0] == 2: from cStringIO import StringIO if sys.version_info < (2, 7): import unittest2 as unittest else: import unittest else: import unittest from io import StringIO # UTILS def get_random_string(length=12): """ >>> r = get_random_string(1) >>> r in asciis True >>> r = get_random_string(2) >>> [r[0] in asciis, r[1] in asciis] [True, True] """ ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz' ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' digits = '0123456789' asciis = ascii_lowercase + ascii_uppercase + digits return ''.join([random.choice(asciis) for _ in range(length)]) def get_test_data_path(x): return path.join(HERE, x) @contextmanager def capture_stdout_stderr(): (old_out, old_err) = (sys.stdout, sys.stderr) try: out = [StringIO(), StringIO()] (sys.stdout, sys.stderr) = out yield out finally: (sys.stdout, sys.stderr) = (old_out, old_err) out[0] = out[0].getvalue() out[1] = out[1].getvalue() # Ensure that ``ssh_config_file is None`` during tests, exceptions are not # raised and pkey loading from an SSH agent is disabled open_tunnel = partial( sshtunnel.open_tunnel, mute_exceptions=False, ssh_config_file=None, allow_agent=False, skip_tunnel_checkup=True, ) # CONSTANTS SSH_USERNAME = get_random_string() SSH_PASSWORD = get_random_string() SSH_DSS = b'\x44\x78\xf0\xb9\xa2\x3c\xc5\x18\x20\x09\xff\x75\x5b\xc1\xd2\x6c' SSH_RSA = b'\x60\x73\x38\x44\xcb\x51\x86\x65\x7f\xde\xda\xa2\x2b\x5a\x57\xd5' ECDSA = b'\x25\x19\xeb\x55\xe6\xa1\x47\xff\x4f\x38\xd2\x75\x6f\xa5\xd5\x60' FINGERPRINTS = { 'ssh-dss': SSH_DSS, 'ssh-rsa': SSH_RSA, 'ecdsa-sha2-nistp256': ECDSA, } DAEMON_THREADS = False HERE = path.abspath(path.dirname(__file__)) THREADS_TIMEOUT = 5.0 PKEY_FILE = 'testrsa.key' ENCRYPTED_PKEY_FILE = 'testrsa_encrypted.key' TEST_CONFIG_FILE = 'testconfig' TEST_UNIX_SOCKET = get_test_data_path('test_socket') sshtunnel.TRACE = True sshtunnel.SSH_TIMEOUT = 1.0 # TESTS class MockLoggingHandler(logging.Handler, object): """Mock logging handler to check for expected logs. Messages are available from an instance's `messages` dict, in order, indexed by a lowercase log level string (e.g., 'debug', 'info', etc.). """ def __init__(self, *args, **kwargs): self.messages = {'debug': [], 'info': [], 'warning': [], 'error': [], 'critical': [], 'trace': []} super(MockLoggingHandler, self).__init__(*args, **kwargs) def emit(self, record): "Store a message from ``record`` in the instance's ``messages`` dict." self.acquire() try: self.messages[record.levelname.lower()].append(record.getMessage()) finally: self.release() def reset(self): self.acquire() try: for message_list in self.messages: self.messages[message_list] = [] finally: self.release() class NullServer(paramiko.ServerInterface): def __init__(self, *args, **kwargs): # Allow tests to enable/disable specific key types self.__allowed_keys = kwargs.pop('allowed_keys', []) self.log = kwargs.pop('log', sshtunnel.create_logger(loglevel='DEBUG')) super(NullServer, self).__init__(*args, **kwargs) def check_channel_forward_agent_request(self, channel): self.log.debug('NullServer.check_channel_forward_agent_request() {0}' .format(channel)) return False def get_allowed_auths(self, username): allowed_auths = 'publickey{0}'.format( ',password' if username == SSH_USERNAME else '' ) self.log.debug('NullServer >> allowed auths for {0}: {1}' .format(username, allowed_auths)) return allowed_auths def check_auth_password(self, username, password): _ok = (username == SSH_USERNAME and password == SSH_PASSWORD) self.log.debug('NullServer >> password for {0} {1}OK' .format(username, '' if _ok else 'NOT-')) return paramiko.AUTH_SUCCESSFUL if _ok else paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): try: expected = FINGERPRINTS[key.get_name()] _ok = (key.get_name() in self.__allowed_keys and key.get_fingerprint() == expected) except KeyError: _ok = False self.log.debug('NullServer >> pkey authentication for {0} {1}OK' .format(username, '' if _ok else 'NOT-')) return paramiko.AUTH_SUCCESSFUL if _ok else paramiko.AUTH_FAILED def check_channel_request(self, kind, chanid): self.log.debug('NullServer.check_channel_request()' .format(kind, chanid)) return paramiko.OPEN_SUCCEEDED def check_channel_exec_request(self, channel, command): self.log.debug('NullServer.check_channel_exec_request()' .format(channel, command)) return True def check_port_forward_request(self, address, port): self.log.debug('NullServer.check_port_forward_request()' .format(address, port)) return True def check_global_request(self, kind, msg): self.log.debug('NullServer.check_port_forward_request()' .format(kind, msg)) return True def check_channel_direct_tcpip_request(self, chanid, origin, destination): self.log.debug('NullServer.check_channel_direct_tcpip_request' '(chanid={0}) {1} -> {2}' .format(chanid, origin, destination)) return paramiko.OPEN_SUCCEEDED class SSHClientTest(unittest.TestCase): def make_socket(self): s = socket.socket() s.bind(('localhost', 0)) s.listen(5) addr, port = s.getsockname() return s, addr, port @classmethod def setUpClass(cls): super(SSHClientTest, cls).setUpClass() socket.setdefaulttimeout(sshtunnel.SSH_TIMEOUT) cls.log = logging.getLogger(sshtunnel.__name__) cls.log = sshtunnel.create_logger(logger=cls.log, loglevel='DEBUG') cls._sshtunnel_log_handler = MockLoggingHandler(level='DEBUG') cls.log.addHandler(cls._sshtunnel_log_handler) cls.sshtunnel_log_messages = cls._sshtunnel_log_handler.messages # set verbose format for logging _fmt = '%(asctime)s| %(levelname)-4.3s|%(threadName)10.9s/' \ '%(lineno)04d@%(module)-10.9s| %(message)s' for handler in cls.log.handlers: handler.setFormatter(logging.Formatter(_fmt)) def setUp(self): super(SSHClientTest, self).setUp() self.log.debug('*' * 80) self.log.info('setUp for: {0}()'.format(self._testMethodName.upper())) self.ssockl, self.saddr, self.sport = self.make_socket() self.esockl, self.eaddr, self.eport = self.make_socket() self.log.info("Socket for ssh-server: {0}:{1}" .format(self.saddr, self.sport)) self.log.info("Socket for echo-server: {0}:{1}" .format(self.eaddr, self.eport)) self.ssh_event = threading.Event() self.running_threads = [] self.threads = {} self.is_server_working = False self._sshtunnel_log_handler.reset() def tearDown(self): self.log.info('tearDown for: {0}()' .format(self._testMethodName.upper())) self.stop_echo_and_ssh_server() for thread in self.running_threads: x = self.threads[thread] self.log.info('thread {0} ({1})' .format(thread, 'alive' if x.is_alive() else 'defunct')) while self.running_threads: for thread in self.running_threads: x = self.threads[thread] self.wait_for_thread(self.threads[thread], who='tearDown') if not x.is_alive(): self.log.info('thread {0} now stopped'.format(thread)) for attr in ['server', 'tc', 'ts', 'socks', 'ssockl', 'esockl']: if hasattr(self, attr): self.log.info('tearDown() {0}'.format(attr)) getattr(self, attr).close() def wait_for_thread(self, thread, timeout=THREADS_TIMEOUT, who=None): if thread.is_alive(): self.log.debug('{0}waiting for {1} to end...' .format('{0} '.format(who) if who else '', thread.name)) thread.join(timeout) def start_echo_and_ssh_server(self): self.is_server_working = True self.start_echo_server() t = threading.Thread(target=self._run_ssh_server, name='ssh-server') t.daemon = DAEMON_THREADS self.running_threads.append(t.name) self.threads[t.name] = t t.start() def stop_echo_and_ssh_server(self): self.log.info('Sending STOP signal') self.is_server_working = False def _check_server_auth(self): # Check if authentication to server was successfulZ self.ssh_event.wait(sshtunnel.SSH_TIMEOUT) # wait for transport self.assertTrue(self.ssh_event.is_set()) self.assertTrue(self.ts.is_active()) self.assertEqual(self.ts.get_username(), SSH_USERNAME) self.assertTrue(self.ts.is_authenticated()) @contextmanager def _test_server(self, *args, **kwargs): self.start_echo_and_ssh_server() server = open_tunnel(*args, **kwargs) server.start() self._check_server_auth() yield server server._stop_transport() def start_echo_server(self): t = threading.Thread(target=self._run_echo_server, name='echo-server') t.daemon = DAEMON_THREADS self.running_threads.append(t.name) self.threads[t.name] = t t.start() def _run_ssh_server(self): self.log.info('ssh-server Start') try: self.socks, addr = self.ssockl.accept() except socket.timeout: self.log.error('ssh-server connection timed out!') self.running_threads.remove('ssh-server') return self.ts = paramiko.Transport(self.socks) host_key = paramiko.RSAKey.from_private_key_file( get_test_data_path(PKEY_FILE) ) self.ts.add_server_key(host_key) server = NullServer(allowed_keys=FINGERPRINTS.keys(), log=self.log) t = threading.Thread(target=self._do_forwarding, name='forward-server') t.daemon = DAEMON_THREADS self.running_threads.append(t.name) self.threads[t.name] = t t.start() self.ts.start_server(self.ssh_event, server) self.wait_for_thread(t, timeout=None, who='ssh-server') self.log.info('ssh-server shutting down') self.running_threads.remove('ssh-server') def _run_echo_server(self, timeout=sshtunnel.SSH_TIMEOUT): self.log.info('echo-server Started') self.ssh_event.wait(timeout) # wait for transport socks = [self.esockl] try: while self.is_server_working: inputready, _, _ = select.select(socks, [], [], timeout) for s in inputready: if s == self.esockl: # handle the server socket try: client, address = self.esockl.accept() self.log.info('echo-server accept() {0}' .format(address)) except OSError: self.log.info('echo-server accept() OSError') break socks.append(client) else: # handle all other sockets try: data = s.recv(1000) self.log.info('echo-server echoing {0}' .format(data)) s.send(data) except OSError: self.log.warning('echo-server OSError') continue finally: s.close() socks.remove(s) self.log.info('<<< echo-server received STOP signal') except Exception as e: self.log.info('echo-server got Exception: {0}'.format(repr(e))) finally: self.is_server_working = False if 'forward-server' in self.threads: t = self.threads['forward-server'] self.wait_for_thread(t, timeout=None, who='echo-server') self.running_threads.remove('forward-server') for s in socks: s.close() self.log.info('echo-server shutting down') self.running_threads.remove('echo-server') def _do_forwarding(self, timeout=sshtunnel.SSH_TIMEOUT): self.log.debug('forward-server Start') self.ssh_event.wait(THREADS_TIMEOUT) # wait for SSH server's transport try: schan = self.ts.accept(timeout=timeout) info = "forward-server schan <> echo" self.log.info(info + " accept()") echo = socket.create_connection( (self.eaddr, self.eport) ) while self.is_server_working: rqst, _, _ = select.select([schan, echo], [], [], timeout) if schan in rqst: data = schan.recv(1024) self.log.debug('{0} -->: {1}'.format(info, repr(data))) echo.send(data) if len(data) == 0: break if echo in rqst: data = echo.recv(1024) self.log.debug('{0} <--: {1}'.format(info, repr(data))) schan.send(data) if len(data) == 0: break self.log.info('<<< forward-server received STOP signal') except socket.error: self.log.critical('{0} sending RST'.format(info)) except Exception as e: # we reach this point usually when schan is None (paramiko bug?) self.log.critical(repr(e)) finally: if schan: self.log.debug('{0} closing connection...'.format(info)) schan.close() echo.close() self.log.debug('{0} connection closed.'.format(info)) def randomize_eport(self): return self.eport + random.randint(1, 999) def test_echo_server(self): with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ) as server: message = get_random_string().encode() local_bind_addr = ('127.0.0.1', server.local_bind_port) self.log.info('_test_server(): try connect!') s = socket.create_connection(local_bind_addr) self.log.info('_test_server(): connected from {0}! try send!' .format(s.getsockname())) s.send(message) self.log.info('_test_server(): sent!') z = (s.recv(1000)) self.assertEqual(z, message) s.close() def test_connect_by_username_password(self): """ Test connecting using username/password as authentication """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ): pass # no exceptions are raised def test_connect_by_rsa_key_file(self): """ Test connecting using a RSA key file """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_pkey=get_test_data_path(PKEY_FILE), remote_bind_address=(self.eaddr, self.eport), logger=self.log, ): pass # no exceptions are raised def test_connect_by_paramiko_key(self): """ Test connecting when ssh_private_key is a paramiko.RSAKey """ ssh_key = paramiko.RSAKey.from_private_key_file( get_test_data_path(PKEY_FILE) ) with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_pkey=ssh_key, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ): pass def test_open_tunnel(self): """ Test wrapper method mainly used from CLI """ server = sshtunnel.open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ssh_config_file=None, allow_agent=False ) self.assertEqual(server.ssh_host, self.saddr) self.assertEqual(server.ssh_port, self.sport) self.assertEqual(server.ssh_username, SSH_USERNAME) self.assertEqual(server.ssh_password, SSH_PASSWORD) self.assertEqual(server.logger, self.log) self.start_echo_and_ssh_server() server.start() self._check_server_auth() server.stop() def test_sshaddress_and_sshaddressorhost_mutually_exclusive(self): """ Test that deprecate argument ssh_address cannot be used together with ssh_address_or_host """ with self.assertRaises(ValueError): open_tunnel( ssh_address_or_host=(self.saddr, self.sport), ssh_address=(self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ) def test_sshhost_and_sshaddressorhost_mutually_exclusive(self): """ Test that deprecate argument ssh_host cannot be used together with ssh_address_or_host """ with self.assertRaises(ValueError): open_tunnel( ssh_address_or_host=(self.saddr, self.sport), ssh_host=(self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ) def test_sshaddressorhost_may_not_be_a_tuple(self): """ Test that when ssh_address_or_host contains just the address part (and not the port), we'll look at the contents of ssh_port (if any) """ server = open_tunnel( self.saddr, ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ) self.assertEqual(server.ssh_port, 22) def test_unknown_argument_raises_exception(self): """Test that an exception is raised when setting an invalid argument""" with self.assertRaises(ValueError): open_tunnel( self.saddr, ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), i_do_not_exist=0 ) def test_more_local_than_remote_bind_sizes_raises_exception(self): """ Test that when the number of local_bind_addresses exceed number of remote_bind_addresses, an exception is raised """ with self.assertRaises(ValueError): open_tunnel( self.saddr, ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_addresses=[('127.0.0.1', self.eport), ('127.0.0.1', self.randomize_eport())] ) def test_localbindaddress_and_localbindaddresses_mutually_exclusive(self): """ Test that arguments local_bind_address and local_bind_addresses cannot be used together """ with self.assertRaises(ValueError): open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=('127.0.0.1', self.eport), local_bind_addresses=[('127.0.0.1', self.eport), ('127.0.0.1', self.randomize_eport())] ) def test_localbindaddress_host_is_optional(self): """ Test that the host part of the local_bind_address tuple may be omitted and instead all the local interfaces (0.0.0.0) will be listening """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=('', self.randomize_eport()) ) as server: self.assertEqual(server.local_bind_host, '0.0.0.0') def test_localbindaddress_port_is_optional(self): """ Test that the port part of the local_bind_address tuple may be omitted and instead a random port will be chosen """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=('127.0.0.1', ) ) as server: self.assertIsInstance(server.local_bind_port, int) def test_remotebindaddress_and_remotebindaddresses_are_exclusive(self): """ Test that arguments remote_bind_address and remote_bind_addresses cannot be used together """ with self.assertRaises(ValueError): open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), remote_bind_addresses=[(self.eaddr, self.eport), (self.eaddr, self.randomize_eport())] ) def test_no_remote_bind_address_raises_exception(self): """ When no remote_bind_address or remote_bind_addresses are specified, a ValueError exception should be raised """ with self.assertRaises(ValueError): open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_reading_from_a_bad_sshconfigfile_does_not_raise_error(self): """ Test that when a bad ssh_config file is found, a warning is shown but no exception is raised """ ssh_config_file = 'not_existing_file' open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=('127.0.0.1', self.randomize_eport()), logger=self.log, ssh_config_file=ssh_config_file ) logged_message = 'Could not read SSH configuration file: {0}'.format( ssh_config_file ) self.assertIn(logged_message, self.sshtunnel_log_messages['warning']) def test_not_setting_password_or_pkey_raises_error(self): """ Test that when a no authentication method is specified, an exception is raised """ with self.assertRaises(ValueError): open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, remote_bind_address=(self.eaddr, self.eport), ssh_config_file=None ) @unittest.skipIf(os.name == 'nt', reason='Need to fix test on Windows') def test_deprecate_warnings_are_shown(self): """Test that when using deprecate arguments a warning is logged""" warnings.simplefilter('always') # don't ignore DeprecationWarnings with warnings.catch_warnings(record=True) as w: for deprecated_arg in ['ssh_address', 'ssh_host']: _kwargs = { deprecated_arg: (self.saddr, self.sport), 'ssh_username': SSH_USERNAME, 'ssh_password': SSH_PASSWORD, 'remote_bind_address': (self.eaddr, self.eport), } open_tunnel(**_kwargs) logged_message = "'{0}' is DEPRECATED use '{1}' instead"\ .format(deprecated_arg, sshtunnel.DEPRECATIONS[deprecated_arg]) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) self.assertEqual(logged_message, str(w[-1].message)) # other deprecated arguments with warnings.catch_warnings(record=True) as w: for deprecated_arg in [ 'raise_exception_if_any_forwarder_have_a_problem', 'ssh_private_key' ]: _kwargs = { 'ssh_address_or_host': (self.saddr, self.sport), 'ssh_username': SSH_USERNAME, 'ssh_password': SSH_PASSWORD, 'remote_bind_address': (self.eaddr, self.eport), deprecated_arg: (self.saddr, self.sport), } open_tunnel(**_kwargs) logged_message = "'{0}' is DEPRECATED use '{1}' instead"\ .format(deprecated_arg, sshtunnel.DEPRECATIONS[deprecated_arg]) self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) self.assertEqual(logged_message, str(w[-1].message)) warnings.simplefilter('default') def test_gateway_unreachable_raises_exception(self): """ BaseSSHTunnelForwarderError is raised when not able to reach the ssh gateway """ with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): with open_tunnel( (self.saddr, self.randomize_eport()), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ssh_config_file=None, ): pass @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_gateway_ip_unresolvable_raises_exception(self): """ BaseSSHTunnelForwarderError is raised when not able to resolve the ssh gateway IP address """ with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): with open_tunnel( (SSH_USERNAME, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ssh_config_file=None, ): pass self.assertIn( 'Could not resolve IP address for {0}, aborting!'.format( SSH_USERNAME ), self.sshtunnel_log_messages['error'] ) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_running_start_twice_logs_warning(self): """Test that when running start() twice a warning is shown""" with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport) ) as server: self.assertNotIn('Already started!', self.sshtunnel_log_messages['warning']) server.logger.error(server.is_active) server.logger.error(server.is_alive) server.start() # 2nd start should prompt the warning self.assertIn('Already started!', self.sshtunnel_log_messages['warning']) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_stop_before_start_logs_warning(self): """ Test that running .stop() on an already stopped server logs a warning """ server = open_tunnel( '10.10.10.10', ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=('10.0.0.1', 8080), mute_exceptions=True, logger=self.log, ) server.stop() self.assertIn('Server is not started. Please .start() first!', self.sshtunnel_log_messages['warning']) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_wrong_auth_to_gateway_logs_error(self): """ Test that when connecting to the ssh gateway with wrong credentials, an error is logged """ with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD[::-1], remote_bind_address=(self.eaddr, self.randomize_eport()), logger=self.log, ): pass self.assertIn('Could not open connection to gateway', self.sshtunnel_log_messages['error']) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_missing_pkey_file_logs_warning(self): """ Test that when the private key file is missing, a warning is logged """ bad_pkey = 'this_file_does_not_exist' with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, ssh_pkey=bad_pkey, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ): self.assertIn('Private key file not found: {0}'.format(bad_pkey), self.sshtunnel_log_messages['warning']) def test_connect_via_proxy(self): """ Test connecting using a ProxyCommand """ proxycmd = paramiko.proxy.ProxyCommand('ssh proxy -W {0}:{1}' .format(self.saddr, self.sport)) server = open_tunnel( self.saddr, ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ssh_proxy=proxycmd, ssh_proxy_enabled=True, logger=self.log, ) self.assertEqual(server.ssh_proxy.cmd[1], 'proxy') @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_can_skip_loading_sshconfig(self): """ Test that we can skip loading the ~/.ssh/config file """ server = open_tunnel( (self.saddr, self.sport), ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), ssh_config_file=None, logger=self.log, ) self.assertEqual(server.ssh_username, getpass.getuser()) self.assertIn('Skipping loading of ssh configuration file', self.sshtunnel_log_messages['info']) def test_local_bind_port(self): """ Test local_bind_port property """ s = socket.socket() s.bind(('localhost', 0)) addr, port = s.getsockname() s.close() with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, local_bind_address=(addr, port), remote_bind_address=(self.eaddr, self.eport), logger=self.log, ) as server: self.assertIsInstance(server.local_bind_port, int) self.assertEqual(server.local_bind_port, port) def test_local_bind_host(self): """ Test local_bind_host property """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, local_bind_address=(self.saddr, 0), remote_bind_address=(self.eaddr, self.eport), logger=self.log, ) as server: self.assertIsInstance(server.local_bind_host, str) self.assertEqual(server.local_bind_host, self.saddr) def test_local_bind_address(self): """ Test local_bind_address property """ s = socket.socket() s.bind(('localhost', 0)) addr, port = s.getsockname() s.close() with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, local_bind_address=(addr, port), remote_bind_address=(self.eaddr, self.eport), logger=self.log, ) as server: self.assertIsInstance(server.local_bind_address, tuple) self.assertTupleEqual(server.local_bind_address, (addr, port)) def test_local_bind_ports(self): """ Test local_bind_ports property """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_addresses=[(self.eaddr, self.eport), (self.saddr, self.sport)], logger=self.log, ) as server: self.assertIsInstance(server.local_bind_ports, list) with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): self.log.info(server.local_bind_port) # Single bind should still produce a 1 element list with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ) as server: self.assertIsInstance(server.local_bind_ports, list) def test_local_bind_hosts(self): """ Test local_bind_hosts property """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, local_bind_addresses=[(self.saddr, 0)] * 2, remote_bind_addresses=[(self.eaddr, self.eport), (self.saddr, self.sport)], logger=self.log, ) as server: self.assertIsInstance(server.local_bind_hosts, list) self.assertListEqual(server.local_bind_hosts, [self.saddr] * 2) with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): self.log.info(server.local_bind_host) def test_local_bind_addresses(self): """ Test local_bind_addresses property """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, local_bind_addresses=[(self.saddr, 0)] * 2, remote_bind_addresses=[(self.eaddr, self.eport), (self.saddr, self.sport)], logger=self.log, ) as server: self.assertIsInstance(server.local_bind_addresses, list) self.assertListEqual(server.local_bind_addresses, [l for l in zip([self.saddr] * 2, server.local_bind_ports)]) with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): self.log.info(server.local_bind_address) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_check_tunnels(self): """ Test method checking if tunnels are up """ remote_address = (self.eaddr, self.eport) with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=remote_address, logger=self.log, skip_tunnel_checkup=False, ) as server: self.assertIn('Tunnel to {0} is UP'.format(remote_address), self.sshtunnel_log_messages['debug']) server.check_tunnels() self.assertIn('Tunnel to {0} is DOWN'.format(remote_address), self.sshtunnel_log_messages['debug']) # Calling local_is_up() should also return the same server.skip_tunnel_checkup = True server.local_is_up((self.saddr, self.sport)) self.assertIn('Tunnel to {0} is DOWN'.format(remote_address), self.sshtunnel_log_messages['debug']) self.assertFalse(server.local_is_up("not a valid address")) self.assertIn('Target must be a tuple (IP, port), where IP ' 'is a string (i.e. "192.168.0.1") and port is ' 'an integer (i.e. 40000). Alternatively ' 'target can be a valid UNIX domain socket.', self.sshtunnel_log_messages['warning']) @mock.patch('sshtunnel.input_', return_value=linesep) def test_cli_main_exits_when_pressing_enter(self, input): """ Test that _cli_main() function quits when Enter is pressed """ self.start_echo_and_ssh_server() sshtunnel._cli_main(args=[self.saddr, '-U', SSH_USERNAME, '-P', SSH_PASSWORD, '-p', str(self.sport), '-R', '{0}:{1}'.format(self.eaddr, self.eport), '-c', '', '-n']) self.stop_echo_and_ssh_server() @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_read_private_key_file(self): """ Test that an encrypted private key can be opened """ encr_pkey = get_test_data_path(ENCRYPTED_PKEY_FILE) pkey = sshtunnel.SSHTunnelForwarder.read_private_key_file( encr_pkey, pkey_password='sshtunnel', logger=self.log ) _pkey = paramiko.RSAKey.from_private_key_file( get_test_data_path(PKEY_FILE) ) self.assertEqual(pkey, _pkey) # Using a wrong password returns None self.assertIsNone(sshtunnel.SSHTunnelForwarder.read_private_key_file( encr_pkey, pkey_password='bad password', logger=self.log )) self.assertIn("Private key file ({0}) could not be loaded as type " "{1} or bad password" .format(encr_pkey, type(_pkey)), self.sshtunnel_log_messages['debug']) # Using no password on an encrypted key returns None self.assertIsNone(sshtunnel.SSHTunnelForwarder.read_private_key_file( encr_pkey, logger=self.log )) self.assertIn('Password is required for key {0}'.format(encr_pkey), self.sshtunnel_log_messages['error']) @unittest.skipIf(os.name != 'posix', reason="UNIX sockets not supported on this platform") def test_unix_domains(self): """ Test use of UNIX domain sockets in local binds """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=TEST_UNIX_SOCKET, logger=self.log, ) as server: self.assertEqual(server.local_bind_address, TEST_UNIX_SOCKET) @unittest.skipIf(sys.version_info < (2, 7), reason="Cannot intercept logging messages in py26") def test_tracing_logging(self): """ Test that Tracing mode may be enabled for more fine-grained logs """ logger = sshtunnel.create_logger(logger=self.log, loglevel='TRACE') with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), logger=logger, ) as server: server.logger = sshtunnel.create_logger(logger=server.logger, loglevel='TRACE') message = get_random_string(100).encode() # Windows raises WinError 10049 if trying to connect to 0.0.0.0 s = socket.create_connection(('127.0.0.1', server.local_bind_port)) s.send(message) s.recv(100) s.close log = 'send to {0}'.format((self.eaddr, self.eport)) self.assertTrue(any(log in l for l in self.sshtunnel_log_messages['trace'])) # set loglevel back to the original value logger = sshtunnel.create_logger(logger=self.log, loglevel='DEBUG') def test_tunnel_bindings_contain_active_tunnels(self): """ Test that `tunnel_bindings` property returns only the active tunnels """ remote_ports = [self.randomize_eport(), self.randomize_eport()] local_ports = [self.randomize_eport(), self.randomize_eport()] with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_addresses=[(self.eaddr, remote_ports[0]), (self.eaddr, remote_ports[1])], local_bind_addresses=[('127.0.0.1', local_ports[0]), ('127.0.0.1', local_ports[1])], skip_tunnel_checkup=False, ) as server: self.assertListEqual(server.local_bind_ports, local_ports) self.assertTupleEqual( server.tunnel_bindings[(self.eaddr, remote_ports[0])], ('127.0.0.1', local_ports[0]) ) self.assertTupleEqual( server.tunnel_bindings[(self.eaddr, remote_ports[1])], ('127.0.0.1', local_ports[1]) ) def check_make_ssh_forward_server_sets_daemon(self, case): self.start_echo_and_ssh_server() tunnel = sshtunnel.open_tunnel( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), logger=self.log, ssh_config_file=None, allow_agent=False ) try: tunnel.daemon_forward_servers = case tunnel.start() for server in tunnel._server_list: self.assertEqual(server.daemon_threads, case) finally: tunnel.stop() def test_make_ssh_forward_server_sets_daemon_true(self): """ Test `make_ssh_forward_server` respects `daemon_forward_servers=True` """ self.check_make_ssh_forward_server_sets_daemon(True) def test_make_ssh_forward_server_sets_daemon_false(self): """ Test `make_ssh_forward_server` respects `daemon_forward_servers=False` """ self.check_make_ssh_forward_server_sets_daemon(False) def test_get_keys(self): """ Test loading keys from the paramiko Agent """ with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=('', self.randomize_eport()), logger=self.log ) as server: keys = server.get_keys(allow_agent=True) self.assertIsInstance(keys, list) self.assertTrue(any('keys loaded from agent' in l) for l in self.sshtunnel_log_messages['info']) with self._test_server( (self.saddr, self.sport), ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=(self.eaddr, self.eport), local_bind_address=('', self.randomize_eport()), logger=self.log ) as server: keys = server.get_keys() self.assertIsInstance(keys, list) self.assertFalse(any('keys loaded from agent' in l for l in self.sshtunnel_log_messages['info'])) tmp_dir = tempfile.mkdtemp() shutil.copy(get_test_data_path(PKEY_FILE), os.path.join(tmp_dir, 'id_rsa')) keys = sshtunnel.SSHTunnelForwarder.get_keys( self.log, host_pkey_directories=[tmp_dir, ] ) self.assertIsInstance(keys, list) self.assertTrue( any('1 keys loaded from host directory' in l for l in self.sshtunnel_log_messages['info']) ) shutil.rmtree(tmp_dir) class AuxiliaryTest(unittest.TestCase): """ Set of tests that do not need the mock SSH server or logger """ def test_parse_arguments_short(self): """ Test CLI argument parsing with short parameter names """ args = ['10.10.10.10', # ssh_address '-U={0}'.format(getpass.getuser()), # GW username '-p=22', # GW SSH port '-P={0}'.format(SSH_PASSWORD), # GW password '-R', '10.0.0.1:8080', '10.0.0.2:8080', # remote bind list '-L', ':8081', ':8082', # local bind list '-k={0}'.format(SSH_DSS), # hostkey '-K={0}'.format(__file__), # pkey file '-S={0}'.format(SSH_PASSWORD), # pkey password '-t', # concurrent connections (threaded) '-vvv', # triple verbosity '-x=10.0.0.2:', # proxy address '-c=ssh_config', # ssh configuration file '-z', # request compression '-n', # disable SSH agent key lookup ] parser = sshtunnel._parse_arguments(args) self._test_parser(parser) with capture_stdout_stderr(): # silence stderr # First argument is mandatory with self.assertRaises(SystemExit): parser = sshtunnel._parse_arguments(args[1:]) # -R argument is mandatory with self.assertRaises(SystemExit): parser = sshtunnel._parse_arguments(args[:4] + args[5:]) def test_parse_arguments_long(self): """ Test CLI argument parsing with long parameter names """ parser = sshtunnel._parse_arguments( ['10.10.10.10', # ssh_address '--username={0}'.format(getpass.getuser()), # GW username '--server_port=22', # GW SSH port '--password={0}'.format(SSH_PASSWORD), # GW password '--remote_bind_address', '10.0.0.1:8080', '10.0.0.2:8080', '--local_bind_address', ':8081', ':8082', # local bind list '--ssh_host_key={0}'.format(SSH_DSS), # hostkey '--private_key_file={0}'.format(__file__), # pkey file '--private_key_password={0}'.format(SSH_PASSWORD), '--threaded', # concurrent connections (threaded) '--verbose', '--verbose', '--verbose', # triple verbosity '--proxy', '10.0.0.2:22', # proxy address '--config', 'ssh_config', # ssh configuration file '--compress', # request compression '--noagent', # disable SSH agent key lookup ] ) self._test_parser(parser) def _test_parser(self, parser): self.assertEqual(parser['ssh_address'], '10.10.10.10') self.assertEqual(parser['ssh_username'], getpass.getuser()) self.assertEqual(parser['ssh_port'], 22) self.assertEqual(parser['ssh_password'], SSH_PASSWORD) self.assertListEqual(parser['remote_bind_addresses'], [('10.0.0.1', 8080), ('10.0.0.2', 8080)]) self.assertListEqual(parser['local_bind_addresses'], [('', 8081), ('', 8082)]) self.assertEqual(parser['ssh_host_key'], str(SSH_DSS)) self.assertEqual(parser['ssh_private_key'], __file__) self.assertEqual(parser['ssh_private_key_password'], SSH_PASSWORD) self.assertTrue(parser['threaded']) self.assertEqual(parser['verbose'], 3) self.assertEqual(parser['ssh_proxy'], ('10.0.0.2', 22)) self.assertEqual(parser['ssh_config_file'], 'ssh_config') self.assertTrue(parser['compression']) self.assertFalse(parser['allow_agent']) def test_bindlist(self): """ Test that _bindlist enforces IP:PORT format for local and remote binds """ self.assertTupleEqual(sshtunnel._bindlist('10.0.0.1:8080'), ('10.0.0.1', 8080)) # Missing port in tuple is filled with port 22 self.assertTupleEqual(sshtunnel._bindlist('10.0.0.1:'), ('10.0.0.1', 22)) self.assertTupleEqual(sshtunnel._bindlist('10.0.0.1'), ('10.0.0.1', 22)) with self.assertRaises(argparse.ArgumentTypeError): sshtunnel._bindlist('10022:10.0.0.1:22') with self.assertRaises(argparse.ArgumentTypeError): sshtunnel._bindlist(':') def test_raise_fwd_ext(self): """ Test that we can silence the exceptions on sshtunnel creation """ server = open_tunnel( '10.10.10.10', ssh_username=SSH_USERNAME, ssh_password=SSH_PASSWORD, remote_bind_address=('10.0.0.1', 8080), mute_exceptions=True, ) # This should not raise an exception server._raise(sshtunnel.BaseSSHTunnelForwarderError, 'test') server._raise_fwd_exc = True # now exceptions are not silenced with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): server._raise(sshtunnel.BaseSSHTunnelForwarderError, 'test') def test_show_running_version(self): """ Test that _cli_main() function quits when Enter is pressed """ with capture_stdout_stderr() as (out, err): with self.assertRaises(SystemExit): sshtunnel._cli_main(args=['-V']) if sys.version_info < (3, 4): version = err.getvalue().split()[-1] else: version = out.getvalue().split()[-1] self.assertEqual(version, sshtunnel.__version__) def test_remove_none_values(self): """ Test removing keys from a dict where values are None """ test_dict = {'key1': 1, 'key2': None, 'key3': 3, 'key4': 0} sshtunnel._remove_none_values(test_dict) self.assertDictEqual(test_dict, {'key1': 1, 'key3': 3, 'key4': 0}) def test_read_ssh_config(self): """ Test that we can gather host information from a config file """ (ssh_hostname, ssh_username, ssh_private_key, ssh_port, ssh_proxy, compression) = sshtunnel.SSHTunnelForwarder._read_ssh_config( 'test', get_test_data_path(TEST_CONFIG_FILE), ) self.assertEqual(ssh_hostname, 'test') self.assertEqual(ssh_username, 'test') self.assertEqual(PKEY_FILE, ssh_private_key) self.assertEqual(ssh_port, 22) # fallback value self.assertListEqual(ssh_proxy.cmd[-2:], ['test:22', 'sshproxy']) self.assertTrue(compression) # passed parameters are not overriden by config (ssh_hostname, ssh_username, ssh_private_key, ssh_port, ssh_proxy, compression) = sshtunnel.SSHTunnelForwarder._read_ssh_config( 'other', get_test_data_path(TEST_CONFIG_FILE), compression=False ) self.assertEqual(ssh_hostname, '10.0.0.1') self.assertEqual(ssh_port, 222) self.assertFalse(compression) def test_str(self): server = open_tunnel( 'test', ssh_private_key=get_test_data_path(PKEY_FILE), remote_bind_address=('10.0.0.1', 8080), ) _str = str(server).split(linesep) self.assertEqual(repr(server), str(server)) self.assertIn('ssh gateway: test:22', _str) self.assertIn('proxy: no', _str) self.assertIn('username: {0}'.format(getpass.getuser()), _str) self.assertIn('status: not started', _str) def test_process_deprecations(self): """ Test processing deprecated API attributes """ kwargs = {'ssh_host': '10.0.0.1', 'ssh_address': '10.0.0.1', 'ssh_private_key': 'testrsa.key', 'raise_exception_if_any_forwarder_have_a_problem': True} for item in kwargs: self.assertEqual(kwargs[item], sshtunnel.SSHTunnelForwarder._process_deprecated( None, item, kwargs.copy() )) # use both deprecated and not None new attribute should raise exception for item in kwargs: with self.assertRaises(ValueError): sshtunnel.SSHTunnelForwarder._process_deprecated('some value', item, kwargs.copy()) # deprecated attribute not in deprecation list should raise exception with self.assertRaises(ValueError): sshtunnel.SSHTunnelForwarder._process_deprecated('some value', 'item', kwargs.copy()) def check_address(self): """ Test that an exception is raised with incorrect bind addresses """ address_list = [('10.0.0.1', 10000), ('10.0.0.1', 10001)] if os.name == 'posix': # UNIX sockets supported by the platform address_list.append('/tmp/unix-socket') self.assertIsNone(sshtunnel.check_addresses(address_list)) # UNIX sockets not supported on remote addresses with self.assertRaises(AssertionError): sshtunnel.check_addresses(address_list, is_remote=True) with self.assertRaises(ValueError): sshtunnel.check_address('this is not valid') with self.assertRaises(ValueError): sshtunnel.check_address(-1) # that's not valid either sshtunnel-0.1.4/tests/testrsa_encrypted.key0000644000175000001440000000170613307461664022655 0ustar fernandezjmusers00000000000000-----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-128-CBC,6FBF59B34F7A4C1BC7566EAB42D21A9B wk9i86OJEt1ayS1Vl08bED2n7aPK10DX/wtIjEixDeUWTPofy9HiEeWVsDxKacTp hLR5bU5p0rzSQ/BXwADAzAaFvqkzOJ2eAz8zY9PC1hE52vniW6Z5jtTvdSyysaEP g26Ut88EfASajqPmZJtVoViL+epv4X+sbhl8Ssh/3jLZSH+Ay3Sz2tgPXsPMbgOY /GH73O1AolFMbm9EwxRP1RlzCnHrM2+6cVOknw5t12biSpOtVZyFf+rKPf14+r6T /+TyBSGY3fh0LH5w8ro+s6VIuEdhJSY+CXbQ6G43vv6uTMIIZ0cQoFu2XZf7TgHm fqfnC9Ttzy6bvpuA+WjnYkEibxW4T7TJsLjpIiesFaWn6NbhVyLrv2j9Zs+80VkS 2ue9zBGVtJOXQaRkafi7r/e3eDp8twZrfujWg5cA6RU2qF3/IzC4m5P66aRd2nwT njgY1mrNSn+6ZLnnIV4vJ6I8RB3kctIA06a9pOWMtrKrnayLKSfpntIoYczjgHsN rDTFgHg84u+1GWRNYaBLWaEbDPeewtc2Zi7pZQz8xGpK97NYvaok171bXg8nGfsy Qj67/AcRSNH9l5NX1jxlj5RF7UILaS1xfNNU85w/L2vlt5zIGcTvHf54azPQjGNO RE5d5ePea21DgX+jkxucvA9jmhiXKnvBBUg8BfQuaQQ/f9Voktk+ZSfHYabf89y3 D+sWsl708JyuQr6hwDEb7qwv3A/cb867WFrXkptj8OBfgIAyQilTaLDjj3XuNHMC jbr6rqbn55NP8TVdz9O1MfoeQsJxDYcCa7l3n2i6gnU= -----END RSA PRIVATE KEY----- sshtunnel-0.1.4/tests/__init__.pyc0000644000175000001440000000023313307507234020641 0ustar fernandezjmusers00000000000000ó ´c[c@sdS(N((((sD/home/fernandezjm/.virtualenvs/sshtunnel/sshtunnel/tests/__init__.pytssshtunnel-0.1.4/LICENSE0000644000175000001440000000204613307461664016242 0ustar fernandezjmusers00000000000000Copyright (c) 2014-2016 Pahaz Blinov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sshtunnel-0.1.4/setup.py0000644000175000001440000001143213307461664016746 0ustar fernandezjmusers00000000000000"""A setuptools based setup module. See: https://packaging.python.org/en/latest/distributing.html https://github.com/pypa/sampleproject """ import re import sys from os import path from codecs import open # To use a consistent encoding from setuptools import setup # Always prefer setuptools over distutils from setuptools.command.test import test as TestCommand here = path.abspath(path.dirname(__file__)) name = 'sshtunnel' description = 'Pure python SSH tunnels' url = 'https://github.com/pahaz/sshtunnel' ppa = 'https://pypi.python.org/packages/source/s/{0}/{0}-'.format(name) # Get the long description from the README file with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() with open(path.join(here, 'docs.rst'), encoding='utf-8') as f: documentation = f.read() with open(path.join(here, 'changelog.rst'), encoding='utf-8') as f: changelog = f.read() with open(path.join(here, name + '.py'), encoding='utf-8') as f: data = f.read() version = eval(re.search("__version__[ ]*=[ ]*([^\r\n]+)", data).group(1)) class Tox(TestCommand): """ Integration with tox """ def finalize_options(self): TestCommand.finalize_options(self) self.test_args = ['--recreate', '-v'] self.test_suite = True def run_tests(self): # import here, otherwise eggs aren't loaded import tox errcode = tox.cmdline(self.test_args) sys.exit(errcode) setup( name=name, # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html version=version, description=description, long_description='\n'.join((long_description, documentation, changelog)), # The project's main homepage. url=url, download_url=ppa + version + '.zip', # noqa # Author details author='Pahaz Blinov', author_email='pahaz.blinov@gmail.com', # Choose your license license='MIT', # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are # 3 - Alpha # 4 - Beta # 5 - Production/Stable 'Development Status :: 3 - Alpha', # Indicate who your project is intended for 'Intended Audience :: Developers', 'Topic :: Software Development :: Build Tools', # Pick your license as you wish (should match "license" above) 'License :: OSI Approved :: MIT License', # Specify the Python versions you support here. In particular, ensure # that you indicate whether you support Python 2, Python 3 or both. 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', ], platforms=['unix', 'macos', 'windows'], # What does your project relate to? keywords='ssh tunnel paramiko proxy tcp-forward', # You can just specify the packages manually here if your project is # simple. Or you can use find_packages(). # packages=find_packages(exclude=['contrib', 'docs', 'tests']), # Alternatively, if you want to distribute just a my_module.py, uncomment # this: py_modules=["sshtunnel"], # List run-time dependencies here. These will be installed by pip when # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html install_requires=[ 'paramiko>=1.15.2', ], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, # for example: # $ pip install -e .[dev,test] extras_require={ 'dev': ['check-manifest'], 'test': [ 'tox>=1.8.1', ], 'build_sphinx': [ 'sphinx', 'sphinxcontrib-napoleon', ], }, # If there are data files included in your packages that need to be # installed, specify them here. If using Python 2.6 or less, then these # have to be included in MANIFEST.in as well. package_data={ 'tests': ['testrsa.key'], }, # To provide executable scripts, use entry points in preference to the # "scripts" keyword. Entry points provide cross-platform support and allow # pip to create the appropriate form of executable for the target platform. entry_points={ 'console_scripts': [ 'sshtunnel=sshtunnel:_cli_main', ] }, # Integrate tox with setuptools cmdclass={'test': Tox}, ) sshtunnel-0.1.4/Troubleshoot.rst0000644000175000001440000000375613307461664020471 0ustar fernandezjmusers00000000000000Troubleshooting guidelines ========================== In case of problems using ``sshtunnel`` and prior to logging an issue, please consider following the next steps to debug where your problem may come from. - Check if you're running the latest version (`PYPI`_ package may not be updated) - Double-check connectivity to SSH gateway/bastion host using `paramiko`_ An example of an SSH connectivity test using `paramiko`_ authenticating with username and password follows:: import paramiko client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(IP_ADDRESS_OR_HOSTNAME, username=USERNAME, password=PASSWORD, allow_agent=False, look_for_keys=False timeout=5.0) While troubleshooting, implicitly set the local bind address and enable verbose logging as follows:: import sshtunnel sshtunnel.SSH_TIMEOUT = sshtunnel.TUNNEL_TIMEOUT = 5.0 server = sshtunnel.open_tunnel( IP_ADDRESS_OR_HOSTNAME, ssh_username=USERNAME, ssh_password=PASSWORD, remote_bind_address=(REMOTE_BIND_IP, REMOTE_BIND_PORT), local_bind_address=('127.0.0.1', LOCAL_BIND_PORT), debug_level='TRACE', ) server.start() print(server.local_bind_port) # show assigned local port server.stop() Check if you've permission to listen at ``LOCAL_BIND_PORT``:: import socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('0.0.0.0', LOCAL_BIND_PORT)) s.listen(1) s.close() Additional notes ---------------- - In general, try to use wrapper (``open_tunnel()``) - The context manager (``with`` statement) handles opening and closing of tunnels and underlying SSH transports - Deprecated parameters/arguments may be deleted in future releases, thus it's recommended not to use them unless necessary .. _PYPI: https://pypi.python.org/pypi/sshtunnel .. _paramiko: http://www.paramiko.org/ sshtunnel-0.1.4/setup.cfg0000644000175000001440000000027413307725441017053 0ustar fernandezjmusers00000000000000[bdist_wheel] universal = 1 [check-manifest] ignore = .travis.yml circle.yml tox.ini [build_sphinx] source-dir = docs/ build-dir = docs/_build [egg_info] tag_build = tag_date = 0