Codebase list sshtunnel / 1b12400
Import upstream version 0.4.0 Debian Janitor 2 years ago
31 changed file(s) with 1665 addition(s) and 806 deletion(s). Raw diff Collapse all Expand all
0 Copyright (c) 2014-2016 Pahaz Blinov
0 Copyright (c) 2014-2019 Pahaz White
11
22 Permission is hereby granted, free of charge, to any person obtaining a copy
33 of this software and associated documentation files (the "Software"), to deal
00 # Include the data files recursive-include data *
1
2 # If using Python 2.6 or less, then have to include package data, even though
3 # it's already declared in setup.py
4 # include sample/*.dat
51
62 include LICENSE
73 include *.rst
106 include docs/*.rst
117 include docs/*.txt
128 include tests/*
9 include e2e_tests/*
10 include e2e_tests/ssh-server-config/*
11 exclude .github/*
12 exclude .circleci/*
13 exclude *.pyc
14 exclude __pycache__
15 exclude Pipfile*
0 Metadata-Version: 1.1
0 Metadata-Version: 2.1
11 Name: sshtunnel
2 Version: 0.1.4
2 Version: 0.4.0
33 Summary: Pure python SSH tunnels
44 Home-page: https://github.com/pahaz/sshtunnel
5 Author: Pahaz Blinov
6 Author-email: pahaz.blinov@gmail.com
5 Author: Pahaz White
6 Author-email: pahaz.white@gmail.com
77 License: MIT
8 Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip
9 Description-Content-Type: UNKNOWN
8 Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.4.0.zip
109 Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
1110
1211 |pyversions| |license|
1312
14 **Author**: `Pahaz Blinov`_
13 **Author**: `Pahaz`_
1514
1615 **Repo**: https://github.com/pahaz/sshtunnel/
1716
18 Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on
17 Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on
1918 Windows.
2019
2120 See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py
111110 ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind
112111 port.
113112
114 .. code-block:: py
113 .. code-block:: python
115114
116115 from sshtunnel import SSHTunnelForwarder
117116
118117 server = SSHTunnelForwarder(
119 'pahaz.urfuclub.ru',
118 'alfa.8iq.dev',
120119 ssh_username="pahaz",
121120 ssh_password="secret",
122121 remote_bind_address=('127.0.0.1', 8080)
136135 assuming password protected pkey authentication, remote server's SSH service is
137136 listening on port 443 and that port is open in the firewall (**Fig2**):
138137
139 .. code-block:: py
138 .. code-block:: python
140139
141140 import paramiko
142 from sshtunnel import SSHTunnelForwarder
143
144 with SSHTunnelForwarder(
141 import sshtunnel
142
143 with sshtunnel.open_tunnel(
145144 (REMOTE_SERVER_IP, 443),
146145 ssh_username="",
147146 ssh_pkey="/var/ssh/rsa_key",
163162
164163 Example of a port forwarding for the Vagrant MySQL local port:
165164
166 .. code-block:: py
167
168 from sshtunnel import SSHTunnelForwarder
165 .. code-block:: python
166
167 from sshtunnel import open_tunnel
169168 from time import sleep
170169
171 with SSHTunnelForwarder(
170 with open_tunnel(
172171 ('localhost', 2222),
173172 ssh_username="vagrant",
174173 ssh_password="vagrant",
188187
189188 (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost
190189
190 Example 4
191 ---------
192
193 Opening an SSH session jumping over two tunnels. SSH transport and tunnels
194 will be daemonised, which will not wait for the connections to stop at close
195 time.
196
197 .. code-block:: python
198
199 import sshtunnel
200 from paramiko import SSHClient
201
202
203 with sshtunnel.open_tunnel(
204 ssh_address_or_host=('GW1_ip', 20022),
205 remote_bind_address=('GW2_ip', 22),
206 ) as tunnel1:
207 print('Connection to tunnel1 (GW1_ip:GW1_port) OK...')
208 with sshtunnel.open_tunnel(
209 ssh_address_or_host=('localhost', tunnel1.local_bind_port),
210 remote_bind_address=('target_ip', 22),
211 ssh_username='GW2_user',
212 ssh_password='GW2_pwd',
213 ) as tunnel2:
214 print('Connection to tunnel2 (GW2_ip:GW2_port) OK...')
215 with SSHClient() as ssh:
216 ssh.connect('localhost',
217 port=tunnel2.local_bind_port,
218 username='target_user',
219 password='target_pwd',
220 )
221 ssh.exec_command(...)
222
223
191224 CLI usage
192225 =========
193226
197230 usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R
198231 IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]]
199232 [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v]
200 [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]]
233 [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n]
234 [-d [FOLDER [FOLDER ...]]]
201235 ssh_address
202236
203237 Pure python ssh tunnel utils
204 Version 0.1.4
238 Version 0.4.0
205239
206240 positional arguments:
207241 ssh_address SSH server IP address (GW for SSH tunnels)
245279 -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]]
246280 List of directories where SSH pkeys (in the format `id_*`) may be found
247281
248 .. _Pahaz Blinov: https://github.com/pahaz
282 .. _Pahaz: https://github.com/pahaz
249283 .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel
250284 .. _paramiko: http://www.paramiko.org/
251285 .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg
284318 - `Dan Harbin`_
285319 - `Ignacio Peluffo`_
286320 - `Niels Zeilemaker`_
321 - `Georgy Rylov`_
322 - `Eddie Chiang`_
323 - `kkrasovskii`_
287324
288325 CHANGELOG
289326 =========
327
328 - v.0.4.0 (`Pahaz`_)
329 + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_)
330 + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_)
331 + Add docker based end to end hangs tests (`#219`_)
332
333 - v.0.3.2 (`Pahaz`_, `JM Fernández`_)
334 + Fix host key directory detection
335 + Unify default ssh config folder to `~/.ssh`
336
337 - v.0.3.1 (`Pahaz`_)
338 + Increase open connection timeout to 10 secods
339
340 - v.0.3.0 (`Pahaz`_)
341 + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible)
342 + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible)
343 + Set transport keepalive to 5 second by default (disabled for version < 0.3.0)
344 + Set default transport timeout to 0.1
345 + Deprecate and remove `block_on_close` option
346 + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_)
347
348 - v.0.2.2 (`Pahaz`_)
349 + Add `.stop(force=True)` for force close active connections (`#201`_)
350
351 - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_)
352 + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_)
353
354 - v.0.2.0 (`Georgy Rylov`_)
355 + Support IPv6 without proxy command. Use built-in paramiko create socket logic. The logic tries to use ipv6 socket family first, then ipv4 socket family.
356
357 - v.0.1.5 (`JM Fernández`_)
358 + Introduce `block_on_close` attribute
290359
291360 - v.0.1.4 (`Niels Zeilemaker`_)
292361 + Allow loading pkeys from `~/.ssh`
304373 - v.0.1.0 (`JM Fernández`_)
305374 + Add `tunnel_bindings` property
306375 + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...)
307 (`Pahaz Blinov`_, `JM Fernández`_)
376 (`Pahaz`_, `JM Fernández`_)
308377 + Add TRACE logging level (`JM Fernández`_)
309378 + Code and tests refactoring (`JM Fernández`_)
310379 + Drop python3.2 support
330399 + Add coverage (`JM Fernández`_)
331400 + Refactoring (`JM Fernández`_)
332401
333 - v.0.0.6 (`Pahaz Blinov`_)
334 + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_)
335
336 - v.0.0.5 (`Pahaz Blinov`_)
402 - v.0.0.6 (`Pahaz`_)
403 + add ``-S`` CLI options for ssh private key password support (`Pahaz`_)
404
405 - v.0.0.5 (`Pahaz`_)
337406 + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_)
338407 + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_)
339408 + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_)
340409 + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_)
341 + add tests (`Pahaz Blinov`_)
342 + add CI integration (`Pahaz Blinov`_)
343 + normal packaging (`Pahaz Blinov`_)
344 + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior]
345 + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior]
346
347 - v.0.0.4.4 (`Pahaz Blinov`_)
348 + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_)
349
350 - v.0.0.4.3 (`Pahaz Blinov`_)
351 + fix default port issue `#19`_ (`Pahaz Blinov`_)
352
353 - v.0.0.4.2 (`Pahaz Blinov`_)
410 + add tests (`Pahaz`_)
411 + add CI integration (`Pahaz`_)
412 + normal packaging (`Pahaz`_)
413 + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior]
414 + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior]
415
416 - v.0.0.4.4 (`Pahaz`_)
417 + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_)
418
419 - v.0.0.4.3 (`Pahaz`_)
420 + fix default port issue `#19`_ (`Pahaz`_)
421
422 - v.0.0.4.2 (`Pahaz`_)
354423 + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_)
355424
356 - v.0.0.4.1 (`Pahaz Blinov`_)
357 + fix CLI issues `#13`_ (`Pahaz Blinov`_)
358
359 - v.0.0.4 (`Pahaz Blinov`_)
360 + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible*
361 + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
362 + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
425 - v.0.0.4.1 (`Pahaz`_)
426 + fix CLI issues `#13`_ (`Pahaz`_)
427
428 - v.0.0.4 (`Pahaz`_)
429 + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible*
430 + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
431 + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
363432 + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible*
364433 + add CLI interface (`JM Fernández`_)
365434 + support opening several tunnels at once (`JM Fernández`_)
366 + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_)
367 + improve logging (`JM Fernández`_, `Pahaz Blinov`_)
368 + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_)
435 + improve stability and readability (`JM Fernández`_, `Pahaz`_)
436 + improve logging (`JM Fernández`_, `Pahaz`_)
437 + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_)
369438 + add ``ssh_config_file`` argument support (`JM Fernández`_)
370 + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_)
371
372 - v.0.0.3 (`Pahaz Blinov`_)
439 + add Python 3 support (`JM Fernández`_, `Pahaz`_)
440
441 - v.0.0.3 (`Pahaz`_)
373442 + add ``threaded`` option (`Cameron Maske`_)
374443 + fix exception error message, correctly printing destination address (`Gustavo Machado`_)
375 + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_)
376
377 - v.0.0.1 (`Pahaz Blinov`_)
378 + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_)
379 + ``open`` function (`Pahaz Blinov`_)
380
381
444 + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_)
445
446 - v.0.0.1 (`Pahaz`_)
447 + ``SSHTunnelForwarder`` class (`Pahaz`_)
448 + ``open`` function (`Pahaz`_)
449
450
451 .. _Pahaz: https://github.com/pahaz
382452 .. _Cameron Maske: https://github.com/cameronmaske
383453 .. _Gustavo Machado: https://github.com/gdmachado
384454 .. _Colin Jermain: https://github.com/cjermain
390460 .. _Dan Harbin: https://github.com/RasterBurn
391461 .. _Ignacio Peluffo: https://github.com/ipeluffo
392462 .. _Niels Zeilemaker: https://github.com/NielsZeilemaker
463 .. _Georgy Rylov: https://github.com/g0djan
464 .. _Eddie Chiang: https://github.com/eddie-chiang
465 .. _kkrasovskii: https://github.com/kkrasovskii
393466 .. _#13: https://github.com/pahaz/sshtunnel/issues/13
394467 .. _#16: https://github.com/pahaz/sshtunnel/issues/16
395468 .. _#19: https://github.com/pahaz/sshtunnel/issues/19
403476 .. _#41: https://github.com/pahaz/sshtunnel/issues/41
404477 .. _#43: https://github.com/pahaz/sshtunnel/issues/43
405478 .. _#46: https://github.com/pahaz/sshtunnel/issues/46
479 .. _#170: https://github.com/pahaz/sshtunnel/issues/170
480 .. _#201: https://github.com/pahaz/sshtunnel/issues/201
481 .. _#162: https://github.com/pahaz/sshtunnel/issues/162
482 .. _#173: https://github.com/pahaz/sshtunnel/issues/173
483 .. _#201: https://github.com/pahaz/sshtunnel/issues/201
484 .. _#211: https://github.com/pahaz/sshtunnel/issues/211
485 .. _#219: https://github.com/pahaz/sshtunnel/issues/219
406486 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127
407487
408488 Keywords: ssh tunnel paramiko proxy tcp-forward
419499 Classifier: Programming Language :: Python :: 3.4
420500 Classifier: Programming Language :: Python :: 3.5
421501 Classifier: Programming Language :: Python :: 3.6
502 Classifier: Programming Language :: Python :: 3.7
503 Classifier: Programming Language :: Python :: 3.8
504 Description-Content-Type: text/x-rst
505 Provides-Extra: build_sphinx
506 Provides-Extra: dev
507 Provides-Extra: test
11
22 |pyversions| |license|
33
4 **Author**: `Pahaz Blinov`_
4 **Author**: `Pahaz`_
55
66 **Repo**: https://github.com/pahaz/sshtunnel/
77
8 Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on
8 Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on
99 Windows.
1010
1111 See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py
101101 ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind
102102 port.
103103
104 .. code-block:: py
104 .. code-block:: python
105105
106106 from sshtunnel import SSHTunnelForwarder
107107
108108 server = SSHTunnelForwarder(
109 'pahaz.urfuclub.ru',
109 'alfa.8iq.dev',
110110 ssh_username="pahaz",
111111 ssh_password="secret",
112112 remote_bind_address=('127.0.0.1', 8080)
126126 assuming password protected pkey authentication, remote server's SSH service is
127127 listening on port 443 and that port is open in the firewall (**Fig2**):
128128
129 .. code-block:: py
129 .. code-block:: python
130130
131131 import paramiko
132 from sshtunnel import SSHTunnelForwarder
133
134 with SSHTunnelForwarder(
132 import sshtunnel
133
134 with sshtunnel.open_tunnel(
135135 (REMOTE_SERVER_IP, 443),
136136 ssh_username="",
137137 ssh_pkey="/var/ssh/rsa_key",
153153
154154 Example of a port forwarding for the Vagrant MySQL local port:
155155
156 .. code-block:: py
157
158 from sshtunnel import SSHTunnelForwarder
156 .. code-block:: python
157
158 from sshtunnel import open_tunnel
159159 from time import sleep
160160
161 with SSHTunnelForwarder(
161 with open_tunnel(
162162 ('localhost', 2222),
163163 ssh_username="vagrant",
164164 ssh_password="vagrant",
178178
179179 (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost
180180
181 Example 4
182 ---------
183
184 Opening an SSH session jumping over two tunnels. SSH transport and tunnels
185 will be daemonised, which will not wait for the connections to stop at close
186 time.
187
188 .. code-block:: python
189
190 import sshtunnel
191 from paramiko import SSHClient
192
193
194 with sshtunnel.open_tunnel(
195 ssh_address_or_host=('GW1_ip', 20022),
196 remote_bind_address=('GW2_ip', 22),
197 ) as tunnel1:
198 print('Connection to tunnel1 (GW1_ip:GW1_port) OK...')
199 with sshtunnel.open_tunnel(
200 ssh_address_or_host=('localhost', tunnel1.local_bind_port),
201 remote_bind_address=('target_ip', 22),
202 ssh_username='GW2_user',
203 ssh_password='GW2_pwd',
204 ) as tunnel2:
205 print('Connection to tunnel2 (GW2_ip:GW2_port) OK...')
206 with SSHClient() as ssh:
207 ssh.connect('localhost',
208 port=tunnel2.local_bind_port,
209 username='target_user',
210 password='target_pwd',
211 )
212 ssh.exec_command(...)
213
214
181215 CLI usage
182216 =========
183217
187221 usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R
188222 IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]]
189223 [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v]
190 [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]]
224 [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n]
225 [-d [FOLDER [FOLDER ...]]]
191226 ssh_address
192227
193228 Pure python ssh tunnel utils
194 Version 0.1.4
229 Version 0.4.0
195230
196231 positional arguments:
197232 ssh_address SSH server IP address (GW for SSH tunnels)
235270 -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]]
236271 List of directories where SSH pkeys (in the format `id_*`) may be found
237272
238 .. _Pahaz Blinov: https://github.com/pahaz
273 .. _Pahaz: https://github.com/pahaz
239274 .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel
240275 .. _paramiko: http://www.paramiko.org/
241276 .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg
1111 - `Dan Harbin`_
1212 - `Ignacio Peluffo`_
1313 - `Niels Zeilemaker`_
14 - `Georgy Rylov`_
15 - `Eddie Chiang`_
16 - `kkrasovskii`_
1417
1518 CHANGELOG
1619 =========
20
21 - v.0.4.0 (`Pahaz`_)
22 + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_)
23 + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_)
24 + Add docker based end to end hangs tests (`#219`_)
25
26 - v.0.3.2 (`Pahaz`_, `JM Fernández`_)
27 + Fix host key directory detection
28 + Unify default ssh config folder to `~/.ssh`
29
30 - v.0.3.1 (`Pahaz`_)
31 + Increase open connection timeout to 10 secods
32
33 - v.0.3.0 (`Pahaz`_)
34 + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible)
35 + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible)
36 + Set transport keepalive to 5 second by default (disabled for version < 0.3.0)
37 + Set default transport timeout to 0.1
38 + Deprecate and remove `block_on_close` option
39 + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_)
40
41 - v.0.2.2 (`Pahaz`_)
42 + Add `.stop(force=True)` for force close active connections (`#201`_)
43
44 - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_)
45 + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_)
46
47 - v.0.2.0 (`Georgy Rylov`_)
48 + Support IPv6 without proxy command. Use built-in paramiko create socket logic. The logic tries to use ipv6 socket family first, then ipv4 socket family.
49
50 - v.0.1.5 (`JM Fernández`_)
51 + Introduce `block_on_close` attribute
1752
1853 - v.0.1.4 (`Niels Zeilemaker`_)
1954 + Allow loading pkeys from `~/.ssh`
3166 - v.0.1.0 (`JM Fernández`_)
3267 + Add `tunnel_bindings` property
3368 + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...)
34 (`Pahaz Blinov`_, `JM Fernández`_)
69 (`Pahaz`_, `JM Fernández`_)
3570 + Add TRACE logging level (`JM Fernández`_)
3671 + Code and tests refactoring (`JM Fernández`_)
3772 + Drop python3.2 support
5792 + Add coverage (`JM Fernández`_)
5893 + Refactoring (`JM Fernández`_)
5994
60 - v.0.0.6 (`Pahaz Blinov`_)
61 + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_)
95 - v.0.0.6 (`Pahaz`_)
96 + add ``-S`` CLI options for ssh private key password support (`Pahaz`_)
6297
63 - v.0.0.5 (`Pahaz Blinov`_)
98 - v.0.0.5 (`Pahaz`_)
6499 + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_)
65100 + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_)
66101 + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_)
67102 + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_)
68 + add tests (`Pahaz Blinov`_)
69 + add CI integration (`Pahaz Blinov`_)
70 + normal packaging (`Pahaz Blinov`_)
71 + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior]
72 + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior]
103 + add tests (`Pahaz`_)
104 + add CI integration (`Pahaz`_)
105 + normal packaging (`Pahaz`_)
106 + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior]
107 + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior]
73108
74 - v.0.0.4.4 (`Pahaz Blinov`_)
75 + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_)
109 - v.0.0.4.4 (`Pahaz`_)
110 + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_)
76111
77 - v.0.0.4.3 (`Pahaz Blinov`_)
78 + fix default port issue `#19`_ (`Pahaz Blinov`_)
112 - v.0.0.4.3 (`Pahaz`_)
113 + fix default port issue `#19`_ (`Pahaz`_)
79114
80 - v.0.0.4.2 (`Pahaz Blinov`_)
115 - v.0.0.4.2 (`Pahaz`_)
81116 + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_)
82117
83 - v.0.0.4.1 (`Pahaz Blinov`_)
84 + fix CLI issues `#13`_ (`Pahaz Blinov`_)
118 - v.0.0.4.1 (`Pahaz`_)
119 + fix CLI issues `#13`_ (`Pahaz`_)
85120
86 - v.0.0.4 (`Pahaz Blinov`_)
87 + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible*
88 + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
89 + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
121 - v.0.0.4 (`Pahaz`_)
122 + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible*
123 + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
124 + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
90125 + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible*
91126 + add CLI interface (`JM Fernández`_)
92127 + support opening several tunnels at once (`JM Fernández`_)
93 + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_)
94 + improve logging (`JM Fernández`_, `Pahaz Blinov`_)
95 + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_)
128 + improve stability and readability (`JM Fernández`_, `Pahaz`_)
129 + improve logging (`JM Fernández`_, `Pahaz`_)
130 + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_)
96131 + add ``ssh_config_file`` argument support (`JM Fernández`_)
97 + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_)
132 + add Python 3 support (`JM Fernández`_, `Pahaz`_)
98133
99 - v.0.0.3 (`Pahaz Blinov`_)
134 - v.0.0.3 (`Pahaz`_)
100135 + add ``threaded`` option (`Cameron Maske`_)
101136 + fix exception error message, correctly printing destination address (`Gustavo Machado`_)
102 + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_)
137 + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_)
103138
104 - v.0.0.1 (`Pahaz Blinov`_)
105 + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_)
106 + ``open`` function (`Pahaz Blinov`_)
139 - v.0.0.1 (`Pahaz`_)
140 + ``SSHTunnelForwarder`` class (`Pahaz`_)
141 + ``open`` function (`Pahaz`_)
107142
108143
144 .. _Pahaz: https://github.com/pahaz
109145 .. _Cameron Maske: https://github.com/cameronmaske
110146 .. _Gustavo Machado: https://github.com/gdmachado
111147 .. _Colin Jermain: https://github.com/cjermain
117153 .. _Dan Harbin: https://github.com/RasterBurn
118154 .. _Ignacio Peluffo: https://github.com/ipeluffo
119155 .. _Niels Zeilemaker: https://github.com/NielsZeilemaker
156 .. _Georgy Rylov: https://github.com/g0djan
157 .. _Eddie Chiang: https://github.com/eddie-chiang
158 .. _kkrasovskii: https://github.com/kkrasovskii
120159 .. _#13: https://github.com/pahaz/sshtunnel/issues/13
121160 .. _#16: https://github.com/pahaz/sshtunnel/issues/16
122161 .. _#19: https://github.com/pahaz/sshtunnel/issues/19
130169 .. _#41: https://github.com/pahaz/sshtunnel/issues/41
131170 .. _#43: https://github.com/pahaz/sshtunnel/issues/43
132171 .. _#46: https://github.com/pahaz/sshtunnel/issues/46
172 .. _#170: https://github.com/pahaz/sshtunnel/issues/170
173 .. _#201: https://github.com/pahaz/sshtunnel/issues/201
174 .. _#162: https://github.com/pahaz/sshtunnel/issues/162
175 .. _#173: https://github.com/pahaz/sshtunnel/issues/173
176 .. _#201: https://github.com/pahaz/sshtunnel/issues/201
177 .. _#211: https://github.com/pahaz/sshtunnel/issues/211
178 .. _#219: https://github.com/pahaz/sshtunnel/issues/219
133179 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127
1414
1515 import sys
1616 import os
17
18 import sshtunnel
1719
1820 # Patch to disable warning on non-local image
1921 import sphinx.environment
6062
6163 # General information about the project.
6264 project = 'sshtunnel'
63 copyright = '2014-2016, Pahaz Blinov and contributors'
64 author = 'Pahaz Blinov'
65 copyright = '2014-2020, Pahaz White and contributors'
66 author = 'Pahaz White'
6567
6668 # The version info for the project you're documenting, acts as replacement for
6769 # |version| and |release|, also used in various other places throughout the
6870 # built documents.
6971 #
7072 # The short X.Y version.
71 version = '0.0.8'
73 version = sshtunnel.__version__
7274 # The full version, including alpha/beta/rc tags.
73 release = '0.0.8'
75 release = sshtunnel.__version__
7476
7577 # The language for content autogenerated by Sphinx. Refer to documentation
7678 # for a list of supported languages.
66
77 API
88 ===
9
10 .. toctree::
11 :maxdepth: 3
129
1310 .. automodule:: sshtunnel
1411 :members:
2017 License
2118 =======
2219
23 .. include:: ../LICENSE
20 .. include:: ../LICENSE
+0
-3
docs/requirements-docs.txt less more
0 docutils==0.12
1 sphinx==1.3.5
2 sphinxcontrib-napoleon==0.5.0
0 docutils
1 sphinx
2 sphinxcontrib-napoleon
0 ---
1 version: "2.1"
2 services:
3 ssh:
4 image: ghcr.io/linuxserver/openssh-server
5 container_name: openssh-server
6 hostname: openssh-server
7 environment:
8 - PUID=1000
9 - PGID=1000
10 - TZ=Europe/London
11 - PUBLIC_KEY_FILE=/config/ssh_host_keys/ssh_host_rsa_key.pub
12 - SUDO_ACCESS=false
13 - PASSWORD_ACCESS=false
14 - USER_NAME=linuxserver
15 volumes:
16 - ./ssh-server-config:/config/ssh_host_keys:ro
17 ports:
18 - "127.0.0.1:2223:2222"
19 networks:
20 - inner
21
22 postgresdb:
23 image: postgres:13.0
24 environment:
25 POSTGRES_USER: postgres
26 POSTGRES_PASSWORD: postgres
27 POSTGRES_DB: main
28 networks:
29 inner:
30 ipv4_address: 10.5.0.5
31
32 mysqldb:
33 image: mysql:8
34 environment:
35 MYSQL_DATABASE: main
36 MYSQL_USER: mysql
37 MYSQL_PASSWORD: mysql
38 MYSQL_ROOT_PASSWORD: mysqlroot
39 networks:
40 inner:
41 ipv4_address: 10.5.0.6
42
43 mongodb:
44 image: mongo:3.6
45 environment:
46 MONGO_INITDB_ROOT_USERNAME: mongo
47 MONGO_INITDB_ROOT_PASSWORD: mongo
48 MONGO_INITDB_DATABASE: main
49 networks:
50 inner:
51 ipv4_address: 10.5.0.7
52
53 networks:
54 inner:
55 driver: bridge
56 ipam:
57 config:
58 - subnet: 10.5.0.0/16
59 gateway: 10.5.0.1
0 import select
1 import traceback
2 import sys
3 import os
4 import time
5 from sshtunnel import SSHTunnelForwarder
6 import sshtunnel
7 import logging
8 import threading
9 import paramiko
10
11 sshtunnel.DEFAULT_LOGLEVEL = 1
12 logging.basicConfig(
13 format='%(asctime)s| %(levelname)-4.3s|%(threadName)10.9s/%(lineno)04d@%(module)-10.9s| %(message)s', level=1)
14
15 SSH_SERVER_ADDRESS = ('127.0.0.1', 2223)
16 SSH_SERVER_USERNAME = 'linuxserver'
17 SSH_PKEY = os.path.join(os.path.dirname(__file__), 'ssh-server-config', 'ssh_host_rsa_key')
18 SSH_SERVER_REMOTE_SIDE_ADDRESS_PG = ('10.5.0.5', 5432)
19 SSH_SERVER_REMOTE_SIDE_ADDRESS_MYSQL = ('10.5.0.6', 3306)
20 SSH_SERVER_REMOTE_SIDE_ADDRESS_MONGO = ('10.5.0.7', 27017)
21
22 PG_DATABASE_NAME = 'main'
23 PG_USERNAME = 'postgres'
24 PG_PASSWORD = 'postgres'
25 PG_QUERY = 'select version()'
26 PG_EXPECT = eval(
27 """('PostgreSQL 13.0 (Debian 13.0-1.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit',)""")
28
29 MYSQL_DATABASE_NAME = 'main'
30 MYSQL_USERNAME = 'mysql'
31 MYSQL_PASSWORD = 'mysql'
32 MYSQL_QUERY = 'select version()'
33 MYSQL_EXPECT = (('8.0.22',),)
34
35 MONGO_DATABASE_NAME = 'main'
36 MONGO_USERNAME = 'mongo'
37 MONGO_PASSWORD = 'mongo'
38 MONGO_QUERY = lambda client, db: client.server_info()
39 MONGO_EXPECT = eval(
40 """{'version': '3.6.21', 'gitVersion': '1cd2db51dce4b16f4bc97a75056269df0dc0bddb', 'modules': [], 'allocator': 'tcmalloc', 'javascriptEngine': 'mozjs', 'sysInfo': 'deprecated', 'versionArray': [3, 6, 21, 0], 'openssl': {'running': 'OpenSSL 1.0.2g 1 Mar 2016', 'compiled': 'OpenSSL 1.0.2g 1 Mar 2016'}, 'buildEnvironment': {'distmod': 'ubuntu1604', 'distarch': 'x86_64', 'cc': '/opt/mongodbtoolchain/v2/bin/gcc: gcc (GCC) 5.4.0', 'ccflags': '-fno-omit-frame-pointer -fno-strict-aliasing -ggdb -pthread -Wall -Wsign-compare -Wno-unknown-pragmas -Winvalid-pch -Werror -O2 -Wno-unused-local-typedefs -Wno-unused-function -Wno-deprecated-declarations -Wno-unused-but-set-variable -Wno-missing-braces -fstack-protector-strong -fno-builtin-memcmp', 'cxx': '/opt/mongodbtoolchain/v2/bin/g++: g++ (GCC) 5.4.0', 'cxxflags': '-Woverloaded-virtual -Wno-maybe-uninitialized -std=c++14', 'linkflags': '-pthread -Wl,-z,now -rdynamic -Wl,--fatal-warnings -fstack-protector-strong -fuse-ld=gold -Wl,--build-id -Wl,--hash-style=gnu -Wl,-z,noexecstack -Wl,--warn-execstack -Wl,-z,relro', 'target_arch': 'x86_64', 'target_os': 'linux'}, 'bits': 64, 'debug': False, 'maxBsonObjectSize': 16777216, 'storageEngines': ['devnull', 'ephemeralForTest', 'mmapv1', 'wiredTiger'], 'ok': 1.0}""")
41
42
43 def run_postgres_query(port, query=PG_QUERY):
44 import psycopg2
45
46 ASYNC_OK = 1
47 ASYNC_READ_TIMEOUT = 2
48 ASYNC_WRITE_TIMEOUT = 3
49 ASYNC_TIMEOUT = 0.2
50
51 def wait(conn):
52 while 1:
53 state = conn.poll()
54 if state == psycopg2.extensions.POLL_OK:
55 break
56 elif state == psycopg2.extensions.POLL_WRITE:
57 select.select([], [conn.fileno()], [])
58 elif state == psycopg2.extensions.POLL_READ:
59 select.select([conn.fileno()], [], [])
60 else:
61 raise psycopg2.OperationalError(
62 "poll() returned %s from _wait function" % state)
63
64 def wait_timeout(conn):
65 while 1:
66 state = conn.poll()
67 if state == psycopg2.extensions.POLL_OK:
68 return ASYNC_OK
69 elif state == psycopg2.extensions.POLL_WRITE:
70 # Wait for the given time and then check the return status
71 # If three empty lists are returned then the time-out is
72 # reached.
73 timeout_status = select.select(
74 [], [conn.fileno()], [], ASYNC_TIMEOUT
75 )
76 if timeout_status == ([], [], []):
77 return ASYNC_WRITE_TIMEOUT
78 elif state == psycopg2.extensions.POLL_READ:
79 # Wait for the given time and then check the return status
80 # If three empty lists are returned then the time-out is
81 # reached.
82 timeout_status = select.select(
83 [conn.fileno()], [], [], ASYNC_TIMEOUT
84 )
85 if timeout_status == ([], [], []):
86 return ASYNC_READ_TIMEOUT
87 else:
88 raise psycopg2.OperationalError(
89 "poll() returned %s from _wait_timeout function" % state
90 )
91
92 pg_conn = psycopg2.connect(
93 host='127.0.0.1',
94 hostaddr='127.0.0.1',
95 port=port,
96 database=PG_DATABASE_NAME,
97 user=PG_USERNAME,
98 password=PG_PASSWORD,
99 sslmode='disable',
100 async_=1
101 )
102 wait(pg_conn)
103 cur = pg_conn.cursor()
104 cur.execute(query)
105 res = wait_timeout(cur.connection)
106 while res != ASYNC_OK:
107 res = wait_timeout(cur.connection)
108 return cur.fetchone()
109
110
111 def run_mysql_query(port, query=MYSQL_QUERY):
112 import pymysql
113 conn = pymysql.connect(
114 host='127.0.0.1',
115 port=port,
116 user=MYSQL_USERNAME,
117 password=MYSQL_PASSWORD,
118 database=MYSQL_DATABASE_NAME,
119 connect_timeout=5,
120 read_timeout=5)
121 cursor = conn.cursor()
122 cursor.execute(query)
123 return cursor.fetchall()
124
125
126 def run_mongo_query(port, query=MONGO_QUERY):
127 import pymongo
128 client = pymongo.MongoClient('127.0.0.1', port)
129 db = client[MONGO_DATABASE_NAME]
130 return query(client, db)
131
132
133 def create_tunnel():
134 logging.info('Creating SSHTunnelForwarder... (sshtunnel v%s, paramiko v%s)',
135 sshtunnel.__version__, paramiko.__version__)
136 tunnel = SSHTunnelForwarder(
137 SSH_SERVER_ADDRESS,
138 ssh_username=SSH_SERVER_USERNAME,
139 ssh_pkey=SSH_PKEY,
140 remote_bind_addresses=[
141 SSH_SERVER_REMOTE_SIDE_ADDRESS_PG, SSH_SERVER_REMOTE_SIDE_ADDRESS_MYSQL,
142 SSH_SERVER_REMOTE_SIDE_ADDRESS_MONGO,
143 ],
144 )
145 return tunnel
146
147
148 def start(tunnel):
149 try:
150 logging.info('Trying to start ssh tunnel...')
151 tunnel.start()
152 except Exception as e:
153 logging.exception('Tunnel start exception: %r', e)
154 raise
155
156
157 def run_db_queries(tunnel):
158 result1, result2, result3 = None, None, None
159
160 try:
161 logging.info('Trying to run PG query...')
162 result1 = run_postgres_query(tunnel.local_bind_ports[0])
163 logging.info('PG query: %r', result1)
164 except Exception as e:
165 logging.exception('PG query exception: %r', e)
166 raise
167
168 try:
169 logging.info('Trying to run MYSQL query...')
170 result2 = run_mysql_query(tunnel.local_bind_ports[1])
171 logging.info('MYSQL query: %r', result2)
172 except Exception as e:
173 logging.exception('MYSQL query exception: %r', e)
174 raise
175
176 try:
177 logging.info('Trying to run MONGO query...')
178 result3 = run_mongo_query(tunnel.local_bind_ports[2])
179 logging.info('MONGO query: %r', result3)
180 except Exception as e:
181 logging.exception('MONGO query exception: %r', e)
182 raise
183
184 return result1, result2, result3
185
186
187 def wait_and_check_or_restart_if_required(tunnel, i=1):
188 logging.warning('Sleeping for %s second...', i)
189 while i:
190 time.sleep(1)
191 if i % 10 == 0:
192 logging.info('Running tunnel.check_tunnels... (i=%s)', i)
193 tunnel.check_tunnels()
194 logging.info('Check result: %r (i=%s)', tunnel.tunnel_is_up, i)
195 if not tunnel.is_active:
196 logging.warning('Tunnel is DOWN! restarting ...')
197 tunnel.restart()
198 i -= 1
199
200
201 def stop(tunnel, force=True):
202 try:
203 logging.info('Trying to stop resources...')
204 tunnel.stop(force=force)
205 except Exception as e:
206 logging.exception('Tunnel stop exception: %r', e)
207 raise
208
209
210 def show_threading_state_if_required():
211 current_threads = list(threading.enumerate())
212 if len(current_threads) > 1:
213 logging.warning('[1] THREAD INFO')
214 logging.info('Threads: %r', current_threads)
215 logging.info('Threads.daemon: %r', [x.daemon for x in current_threads])
216
217 if len(current_threads) > 1:
218 logging.warning('[2] STACK INFO')
219 code = ["\n\n*** STACKTRACE - START ***\n"]
220 for threadId, stack in sys._current_frames().items():
221 code.append("\n# ThreadID: %s" % threadId)
222 for filename, lineno, name, line in traceback.extract_stack(stack):
223 code.append('File: "%s", line %d, in %s' % (filename, lineno, name))
224 if line:
225 code.append(" %s" % (line.strip()))
226 code.append("\n*** STACKTRACE - END ***\n\n")
227 logging.info('\n'.join(code))
228
229
230 if __name__ == '__main__':
231 logging.warning('RUN')
232 tunnel = create_tunnel()
233 start(tunnel)
234 res = run_db_queries(tunnel)
235 stop(tunnel)
236 wait_and_check_or_restart_if_required(tunnel)
237 show_threading_state_if_required()
238 logging.warning('EOF')
239
240 assert res == (PG_EXPECT, MYSQL_EXPECT, MONGO_EXPECT)
0 import logging
1 import sshtunnel
2 import os
3
4
5 if __name__ == '__main__':
6 path = os.path.join(os.path.dirname(__file__), 'run_docker_e2e_db_tests.py')
7 with open(path) as f:
8 exec(f.read())
9 logging.warning('RUN')
10 tunnel = create_tunnel()
11 start(tunnel)
12 logging.warning('EOF')
0 -----BEGIN OPENSSH PRIVATE KEY-----
1 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
2 NzAAAAgQDSZjQVKBCj57wXTZTFusc/Amp5wet2ugo/Mh+86+v2WDbluFztNZXTA3EtX8p6
3 zZtoLZJ/+VCtLqZD7MjJIt4/bPhOjyXOlbtIwL7w80drTxMFBOvuBQkD+TqIzaONwzsN5b
4 GcQNACpyz4C2eSUP4KOmOrKXovFI6pMQ22lbqrrQAAABUAv4qw6qJkET1T4J8o0RgzoxNI
5 TFkAAACAQQ5w7+2rPlC/GP9ScUCQZTicgzAYlTNOCvIcO4pRj7E1NwNMuafl6xNRjrIYBp
6 OqMhDLIBx15Yob0J/6PpE65oeQ8Lq8QboZxO8bio0FGt4qE6mXB4vJq2oOwQkWHzH64x9l
7 fmFQNe8KRpd0G/daXBgeF+FEqV2vVsjsjKXxwncAAACAFRvMwvnkzX/c2MaWx78+HJEjjf
8 ATYt2acoLAH2YRwnhavQyEScNQDiZnBbIr2J21ccvGvFyZT2dtcz83pwFDa9o7Y41EWQG7
9 ifRPYrj9aHd3TyxeiSGSZlna9ekcfXbIF7+aRHSyEie/YIYUGm73jCW+TDcXK1nQHu7tGL
10 1KkBQAAAHox++oGsfvqBoAAAAHc3NoLWRzcwAAAIEA0mY0FSgQo+e8F02UxbrHPwJqecHr
11 droKPzIfvOvr9lg25bhc7TWV0wNxLV/Kes2baC2Sf/lQrS6mQ+zIySLeP2z4To8lzpW7SM
12 C+8PNHa08TBQTr7gUJA/k6iM2jjcM7DeWxnEDQAqcs+AtnklD+Cjpjqyl6LxSOqTENtpW6
13 q60AAAAVAL+KsOqiZBE9U+CfKNEYM6MTSExZAAAAgEEOcO/tqz5Qvxj/UnFAkGU4nIMwGJ
14 UzTgryHDuKUY+xNTcDTLmn5esTUY6yGAaTqjIQyyAcdeWKG9Cf+j6ROuaHkPC6vEG6GcTv
15 G4qNBRreKhOplweLyatqDsEJFh8x+uMfZX5hUDXvCkaXdBv3WlwYHhfhRKldr1bI7Iyl8c
16 J3AAAAgBUbzML55M1/3NjGlse/PhyRI43wE2LdmnKCwB9mEcJ4Wr0MhEnDUA4mZwWyK9id
17 tXHLxrxcmU9nbXM/N6cBQ2vaO2ONRFkBu4n0T2K4/Wh3d08sXokhkmZZ2vXpHH12yBe/mk
18 R0shInv2CGFBpu94wlvkw3FytZ0B7u7Ri9SpAUAAAAFAZscEj14jPPE+Znbk4FflEe6t2r
19 AAAAE3Jvb3RAb3BlbnNzaC1zZXJ2ZXI=
20 -----END OPENSSH PRIVATE KEY-----
0 ssh-dss AAAAB3NzaC1kc3MAAACBANJmNBUoEKPnvBdNlMW6xz8CannB63a6Cj8yH7zr6/ZYNuW4XO01ldMDcS1fynrNm2gtkn/5UK0upkPsyMki3j9s+E6PJc6Vu0jAvvDzR2tPEwUE6+4FCQP5OojNo43DOw3lsZxA0AKnLPgLZ5JQ/go6Y6spei8UjqkxDbaVuqutAAAAFQC/irDqomQRPVPgnyjRGDOjE0hMWQAAAIBBDnDv7as+UL8Y/1JxQJBlOJyDMBiVM04K8hw7ilGPsTU3A0y5p+XrE1GOshgGk6oyEMsgHHXlihvQn/o+kTrmh5DwurxBuhnE7xuKjQUa3ioTqZcHi8mrag7BCRYfMfrjH2V+YVA17wpGl3Qb91pcGB4X4USpXa9WyOyMpfHCdwAAAIAVG8zC+eTNf9zYxpbHvz4ckSON8BNi3ZpygsAfZhHCeFq9DIRJw1AOJmcFsivYnbVxy8a8XJlPZ21zPzenAUNr2jtjjURZAbuJ9E9iuP1od3dPLF6JIZJmWdr16Rx9dsgXv5pEdLISJ79ghhQabveMJb5MNxcrWdAe7u0YvUqQFA== root@openssh-server
0 -----BEGIN OPENSSH PRIVATE KEY-----
1 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
2 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQS9T8ajwjHV1Xl705ShFqry77rS7wrh
3 lCN0a4Hf33yapuCBKCRDr+/Y7gISoiER3rez56TQCvIFuKUEgCUsTMSWAAAAsFXWKa1V1i
4 mtAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL1PxqPCMdXVeXvT
5 lKEWqvLvutLvCuGUI3Rrgd/ffJqm4IEoJEOv79juAhKiIRHet7PnpNAK8gW4pQSAJSxMxJ
6 YAAAAhAOe2L6cmoGh4gZ++o+GiqMQ2WQ3RUfle/gc0G1nhLPhWAAAAE3Jvb3RAb3BlbnNz
7 aC1zZXJ2ZXIBAgME
8 -----END OPENSSH PRIVATE KEY-----
0 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL1PxqPCMdXVeXvTlKEWqvLvutLvCuGUI3Rrgd/ffJqm4IEoJEOv79juAhKiIRHet7PnpNAK8gW4pQSAJSxMxJY= root@openssh-server
0 -----BEGIN OPENSSH PRIVATE KEY-----
1 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
2 QyNTUxOQAAACCjD/qc+T3Cc/k2pbjOUFW0OobeLnoWUAoaBhTQchI26wAAAJgismUyIrJl
3 MgAAAAtzc2gtZWQyNTUxOQAAACCjD/qc+T3Cc/k2pbjOUFW0OobeLnoWUAoaBhTQchI26w
4 AAAECV2axXFduGtP3RS1f97smocwVLphmETzpWdwi89jWrJaMP+pz5PcJz+TaluM5QVbQ6
5 ht4uehZQChoGFNByEjbrAAAAE3Jvb3RAb3BlbnNzaC1zZXJ2ZXIBAg==
6 -----END OPENSSH PRIVATE KEY-----
0 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKMP+pz5PcJz+TaluM5QVbQ6ht4uehZQChoGFNByEjbr root@openssh-server
0 -----BEGIN OPENSSH PRIVATE KEY-----
1 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
2 NhAAAAAwEAAQAAAYEAvIGU0pRpThhIcaSPrg2+v7cXl+QcG0icb45hfD44yrCoXkpJp7nh
3 Hv0ObZL2Y1cG7eeayYF4AqD3kwQ7W89GN6YO9b/mkJgawk0/YLUyojTS9dbcTbdkfPzyUa
4 vTMDjly+PIjfiWOEnUgPf1y3xONLkJU0ILyTmgTzSIMNdKngtdCGfytBCuNiPKU8hEdEVt
5 82ebqgtLoSYn9cUcVVz6LewzUh8+YtoPb8Z/BIVEzU37HiE9MOYIBXjo1AEJSnOCkjwlVl
6 PzLhcXKTPht0iwv/KnZNNg0LDmnU/z0n+nPq/EMflum8jRYbgp0C5hksPdc8e0eEKd9gak
7 t7B0ta3Mjt5b8HPQdBGZI/QFufEnSOxfJmoK4Bvjy/oUwE0hGU6po5g+4T2j6Bqqm2I+yV
8 EbkP/UiuD/kEiT0C3yCV547gIDjN2ME9tGJDkd023BFvqn3stFVVZ5WsisRKGc+lvTfqeA
9 JyKFaVt5a23y68ztjEMVrMLksRuEF8gG5kV7EGyjAAAFiCzGBRksxgUZAAAAB3NzaC1yc2
10 EAAAGBALyBlNKUaU4YSHGkj64Nvr+3F5fkHBtInG+OYXw+OMqwqF5KSae54R79Dm2S9mNX
11 Bu3nmsmBeAKg95MEO1vPRjemDvW/5pCYGsJNP2C1MqI00vXW3E23ZHz88lGr0zA45cvjyI
12 34ljhJ1ID39ct8TjS5CVNCC8k5oE80iDDXSp4LXQhn8rQQrjYjylPIRHRFbfNnm6oLS6Em
13 J/XFHFVc+i3sM1IfPmLaD2/GfwSFRM1N+x4hPTDmCAV46NQBCUpzgpI8JVZT8y4XFykz4b
14 dIsL/yp2TTYNCw5p1P89J/pz6vxDH5bpvI0WG4KdAuYZLD3XPHtHhCnfYGpLewdLWtzI7e
15 W/Bz0HQRmSP0BbnxJ0jsXyZqCuAb48v6FMBNIRlOqaOYPuE9o+gaqptiPslRG5D/1Irg/5
16 BIk9At8gleeO4CA4zdjBPbRiQ5HdNtwRb6p97LRVVWeVrIrEShnPpb036ngCcihWlbeWtt
17 8uvM7YxDFazC5LEbhBfIBuZFexBsowAAAAMBAAEAAAGAflHjdb2oV4HkQetBsSRa18QM1m
18 cxAoOE+SiTYRudGQ6KtSzY8MGZ/xca7QiXfXhbF1+llTTiQ/i0Dtu+H0blyfLIgZwIGIsl
19 G2GCf/7MoG//kmhaFuY3O56Rj3MyQVVPgHLy+VhE6hFniske+C4jhicc/aL7nOu15n3Qad
20 JLmV8KB9EIjevDoloXgk9ot/WyuXKLmMaa9rFIA+UDmJyGtfFbbsOrHbj8sS11/oSD14RT
21 LBygEb2EUI52j2LmY/LEvUL+59oCuJ6Y/h+pMdFeuHJzGjrVb573KnGwejzY24HHzzebrC
22 Q+9NyVCTyizPHNu9w52/GPEZQFQBi7o9cDMd3ITZEPIaIvDHsUwPXaHUBHy/XHQTs8pDqk
23 zCMcAs5zdzao2I0LQ+ZFYyvl1rue82ITjDISX1WK6nFYLBVXugi0rLGEdH6P+Psfl3uCIf
24 aW7c12/BpZz2Pql5AuO1wsu4rmz2th68vaC/0IDqWekIbW9qihFbqnhfAxRsIURjpBAAAA
25 wDhIQPsj9T9Vud3Z/TZjiAKCPbg3zi082u1GMMxXnNQtKO3J35wU7VUcAxAzosWr+emMqS
26 U0qW+a5RXr3sqUOqH85b5+Xw0yv2sTr2pL0ALFW7Tq1mesCc3K0So3Yo30pWRIOxYM9ihm
27 E4ci/3mN5kcKWwvLLomFPRU9u0XtIGKnF/cNByTuz9fceR6Pi6mQXZawv+OOMiBeu0gbyp
28 F1uVe8PCshzCrWTE3UjRpQxy9gizvSbGZyGQi1Lm42JXKG3wAAAMEA4r4CLM1xsyxBBMld
29 rxiTqy6bfrZjKkT5MPjBjp+57i5kW9NVqGCnIy/m98pLTuKjTCDmUuWQXS+oqhHw5vq/wj
30 RvQYqkJDz1UGmC1lD2qyqERjOiWa8/iy4dXSLeHCT70+/xR2dBb0z8cT++yZEqLdEZSnHG
31 yRaZMHot1OohVDqJS8nEbxOzgPGdopRMiX6ws/p5/k9YAGkHx0hszA8cn/Tk2/mdS5lugw
32 Y7mdXzfcKvxkgoFrG7XowqRVrozcvDAAAAwQDU1ITasquNLaQhKNqiHx/N7bvKVO33icAx
33 NdShqJEWx/g9idvQ25sA1Ubc1a+Ot5Lgfrs2OBKe+LgSmPAZOjv4ShqBHtsSh3am8/K1xR
34 gQKgojLL4FhtgxtwoZrVvovZHGV3g2A28BRGbKIGVGPsOszJALU7jlLlcTHlB7SCQBI8FQ
35 vTi2UEsfTmA22NnuVPITeqbmAQQXkSZcZbpbvdc0vQzp/3iOb/OCrIMET3HqVEMyQVsVs6
36 xa9026AMTGLaEAAAATcm9vdEBvcGVuc3NoLXNlcnZlcg==
37 -----END OPENSSH PRIVATE KEY-----
0 ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC8gZTSlGlOGEhxpI+uDb6/txeX5BwbSJxvjmF8PjjKsKheSkmnueEe/Q5tkvZjVwbt55rJgXgCoPeTBDtbz0Y3pg71v+aQmBrCTT9gtTKiNNL11txNt2R8/PJRq9MwOOXL48iN+JY4SdSA9/XLfE40uQlTQgvJOaBPNIgw10qeC10IZ/K0EK42I8pTyER0RW3zZ5uqC0uhJif1xRxVXPot7DNSHz5i2g9vxn8EhUTNTfseIT0w5ggFeOjUAQlKc4KSPCVWU/MuFxcpM+G3SLC/8qdk02DQsOadT/PSf6c+r8Qx+W6byNFhuCnQLmGSw91zx7R4Qp32BqS3sHS1rcyO3lvwc9B0EZkj9AW58SdI7F8magrgG+PL+hTATSEZTqmjmD7hPaPoGqqbYj7JURuQ/9SK4P+QSJPQLfIJXnjuAgOM3YwT20YkOR3TbcEW+qfey0VVVnlayKxEoZz6W9N+p4AnIoVpW3lrbfLrzO2MQxWswuSxG4QXyAbmRXsQbKM= root@openssh-server
0 # $OpenBSD: sshd_config,v 1.103 2018/04/09 20:41:22 tj Exp $
1
2 # This is the sshd server system-wide configuration file. See
3 # sshd_config(5) for more information.
4
5 # This sshd was compiled with PATH=/bin:/usr/bin:/sbin:/usr/sbin
6
7 # The strategy used for options in the default sshd_config shipped with
8 # OpenSSH is to specify options with their default value where
9 # possible, but leave them commented. Uncommented options override the
10 # default value.
11
12 #Port 22
13 #AddressFamily any
14 #ListenAddress 0.0.0.0
15 #ListenAddress ::
16
17 #HostKey /etc/ssh/ssh_host_rsa_key
18 #HostKey /etc/ssh/ssh_host_ecdsa_key
19 #HostKey /etc/ssh/ssh_host_ed25519_key
20
21 # Ciphers and keying
22 #RekeyLimit default none
23
24 # Logging
25 #SyslogFacility AUTH
26 #LogLevel INFO
27
28 # Authentication:
29
30 #LoginGraceTime 2m
31 #PermitRootLogin prohibit-password
32 #StrictModes yes
33 #MaxAuthTries 6
34 #MaxSessions 10
35
36 #PubkeyAuthentication yes
37
38 # The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2
39 # but this is overridden so installations will only check .ssh/authorized_keys
40 AuthorizedKeysFile .ssh/authorized_keys
41
42 #AuthorizedPrincipalsFile none
43
44 #AuthorizedKeysCommand none
45 #AuthorizedKeysCommandUser nobody
46
47 # For this to work you will also need host keys in /etc/ssh/ssh_known_hosts
48 #HostbasedAuthentication no
49 # Change to yes if you don't trust ~/.ssh/known_hosts for
50 # HostbasedAuthentication
51 #IgnoreUserKnownHosts no
52 # Don't read the user's ~/.rhosts and ~/.shosts files
53 #IgnoreRhosts yes
54
55 # To disable tunneled clear text passwords, change to no here!
56 PasswordAuthentication no
57 #PermitEmptyPasswords no
58
59 # Change to no to disable s/key passwords
60 #ChallengeResponseAuthentication yes
61
62 # Kerberos options
63 #KerberosAuthentication no
64 #KerberosOrLocalPasswd yes
65 #KerberosTicketCleanup yes
66 #KerberosGetAFSToken no
67
68 # GSSAPI options
69 #GSSAPIAuthentication no
70 #GSSAPICleanupCredentials yes
71
72 # Set this to 'yes' to enable PAM authentication, account processing,
73 # and session processing. If this is enabled, PAM authentication will
74 # be allowed through the ChallengeResponseAuthentication and
75 # PasswordAuthentication. Depending on your PAM configuration,
76 # PAM authentication via ChallengeResponseAuthentication may bypass
77 # the setting of "PermitRootLogin without-password".
78 # If you just want the PAM account and session checks to run without
79 # PAM authentication, then enable this but set PasswordAuthentication
80 # and ChallengeResponseAuthentication to 'no'.
81 #UsePAM no
82
83 #AllowAgentForwarding yes
84 # Feel free to re-enable these if your use case requires them.
85 AllowTcpForwarding yes
86 GatewayPorts no
87 X11Forwarding no
88 #X11DisplayOffset 10
89 #X11UseLocalhost yes
90 #PermitTTY yes
91 #PrintMotd yes
92 #PrintLastLog yes
93 #TCPKeepAlive yes
94 #PermitUserEnvironment no
95 #Compression delayed
96 #ClientAliveInterval 0
97 #ClientAliveCountMax 3
98 #UseDNS no
99 PidFile /config/sshd.pid
100 #MaxStartups 10:30:100
101 #PermitTunnel no
102 #ChrootDirectory none
103 #VersionAddendum none
104
105 # no default banner path
106 #Banner none
107
108 # override default of no subsystems
109 Subsystem sftp /usr/lib/ssh/sftp-server -u 022
110
111 # Example of overriding settings on a per-user basis
112 #Match User anoncvs
113 # X11Forwarding no
114 # AllowTcpForwarding no
115 # PermitTTY no
116 # ForceCommand cvs server
0 [build-system]
1 requires = ["setuptools", "wheel"]
2 build-backend = "setuptools.build_meta:__legacy__"
55 """
66
77 import re
8 import sys
98 from os import path
109 from codecs import open # To use a consistent encoding
11
1210 from setuptools import setup # Always prefer setuptools over distutils
13 from setuptools.command.test import test as TestCommand
1411
1512 here = path.abspath(path.dirname(__file__))
1613 name = 'sshtunnel'
3128 version = eval(re.search("__version__[ ]*=[ ]*([^\r\n]+)", data).group(1))
3229
3330
34 class Tox(TestCommand):
35 """ Integration with tox """
36
37 def finalize_options(self):
38 TestCommand.finalize_options(self)
39 self.test_args = ['--recreate', '-v']
40 self.test_suite = True
41
42 def run_tests(self):
43 # import here, otherwise eggs aren't loaded
44 import tox
45 errcode = tox.cmdline(self.test_args)
46 sys.exit(errcode)
47
48
4931 setup(
5032 name=name,
5133
5638
5739 description=description,
5840 long_description='\n'.join((long_description, documentation, changelog)),
41 long_description_content_type='text/x-rst',
5942
6043 # The project's main homepage.
6144 url=url,
6245 download_url=ppa + version + '.zip', # noqa
6346
6447 # Author details
65 author='Pahaz Blinov',
66 author_email='pahaz.blinov@gmail.com',
48 author='Pahaz White',
49 author_email='pahaz.white@gmail.com',
6750
6851 # Choose your license
6952 license='MIT',
9174 'Programming Language :: Python :: 3.4',
9275 'Programming Language :: Python :: 3.5',
9376 'Programming Language :: Python :: 3.6',
77 'Programming Language :: Python :: 3.7',
78 'Programming Language :: Python :: 3.8',
9479 ],
9580
9681 platforms=['unix', 'macos', 'windows'],
11196 # requirements files see:
11297 # https://packaging.python.org/en/latest/requirements.html
11398 install_requires=[
114 'paramiko>=1.15.2',
99 'paramiko>=2.7.2',
115100 ],
116101
117102 # List additional groups of dependencies here (e.g. development
118103 # dependencies). You can install these using the following syntax,
119104 # for example:
120105 # $ pip install -e .[dev,test]
106 tests_require=[
107 'tox>=1.8.1',
108 ],
121109 extras_require={
122110 'dev': ['check-manifest'],
123111 'test': [
145133 ]
146134 },
147135
148 # Integrate tox with setuptools
149 cmdclass={'test': Tox},
150136 )
0 Metadata-Version: 1.1
0 Metadata-Version: 2.1
11 Name: sshtunnel
2 Version: 0.1.4
2 Version: 0.4.0
33 Summary: Pure python SSH tunnels
44 Home-page: https://github.com/pahaz/sshtunnel
5 Author: Pahaz Blinov
6 Author-email: pahaz.blinov@gmail.com
5 Author: Pahaz White
6 Author-email: pahaz.white@gmail.com
77 License: MIT
8 Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip
9 Description-Content-Type: UNKNOWN
8 Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.4.0.zip
109 Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version|
1110
1211 |pyversions| |license|
1312
14 **Author**: `Pahaz Blinov`_
13 **Author**: `Pahaz`_
1514
1615 **Repo**: https://github.com/pahaz/sshtunnel/
1716
18 Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on
17 Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on
1918 Windows.
2019
2120 See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py
111110 ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind
112111 port.
113112
114 .. code-block:: py
113 .. code-block:: python
115114
116115 from sshtunnel import SSHTunnelForwarder
117116
118117 server = SSHTunnelForwarder(
119 'pahaz.urfuclub.ru',
118 'alfa.8iq.dev',
120119 ssh_username="pahaz",
121120 ssh_password="secret",
122121 remote_bind_address=('127.0.0.1', 8080)
136135 assuming password protected pkey authentication, remote server's SSH service is
137136 listening on port 443 and that port is open in the firewall (**Fig2**):
138137
139 .. code-block:: py
138 .. code-block:: python
140139
141140 import paramiko
142 from sshtunnel import SSHTunnelForwarder
143
144 with SSHTunnelForwarder(
141 import sshtunnel
142
143 with sshtunnel.open_tunnel(
145144 (REMOTE_SERVER_IP, 443),
146145 ssh_username="",
147146 ssh_pkey="/var/ssh/rsa_key",
163162
164163 Example of a port forwarding for the Vagrant MySQL local port:
165164
166 .. code-block:: py
167
168 from sshtunnel import SSHTunnelForwarder
165 .. code-block:: python
166
167 from sshtunnel import open_tunnel
169168 from time import sleep
170169
171 with SSHTunnelForwarder(
170 with open_tunnel(
172171 ('localhost', 2222),
173172 ssh_username="vagrant",
174173 ssh_password="vagrant",
188187
189188 (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost
190189
190 Example 4
191 ---------
192
193 Opening an SSH session jumping over two tunnels. SSH transport and tunnels
194 will be daemonised, which will not wait for the connections to stop at close
195 time.
196
197 .. code-block:: python
198
199 import sshtunnel
200 from paramiko import SSHClient
201
202
203 with sshtunnel.open_tunnel(
204 ssh_address_or_host=('GW1_ip', 20022),
205 remote_bind_address=('GW2_ip', 22),
206 ) as tunnel1:
207 print('Connection to tunnel1 (GW1_ip:GW1_port) OK...')
208 with sshtunnel.open_tunnel(
209 ssh_address_or_host=('localhost', tunnel1.local_bind_port),
210 remote_bind_address=('target_ip', 22),
211 ssh_username='GW2_user',
212 ssh_password='GW2_pwd',
213 ) as tunnel2:
214 print('Connection to tunnel2 (GW2_ip:GW2_port) OK...')
215 with SSHClient() as ssh:
216 ssh.connect('localhost',
217 port=tunnel2.local_bind_port,
218 username='target_user',
219 password='target_pwd',
220 )
221 ssh.exec_command(...)
222
223
191224 CLI usage
192225 =========
193226
197230 usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R
198231 IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]]
199232 [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v]
200 [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] [-d [FOLDER [FOLDER ...]]]
233 [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n]
234 [-d [FOLDER [FOLDER ...]]]
201235 ssh_address
202236
203237 Pure python ssh tunnel utils
204 Version 0.1.4
238 Version 0.4.0
205239
206240 positional arguments:
207241 ssh_address SSH server IP address (GW for SSH tunnels)
245279 -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]]
246280 List of directories where SSH pkeys (in the format `id_*`) may be found
247281
248 .. _Pahaz Blinov: https://github.com/pahaz
282 .. _Pahaz: https://github.com/pahaz
249283 .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel
250284 .. _paramiko: http://www.paramiko.org/
251285 .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg
284318 - `Dan Harbin`_
285319 - `Ignacio Peluffo`_
286320 - `Niels Zeilemaker`_
321 - `Georgy Rylov`_
322 - `Eddie Chiang`_
323 - `kkrasovskii`_
287324
288325 CHANGELOG
289326 =========
327
328 - v.0.4.0 (`Pahaz`_)
329 + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_)
330 + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_)
331 + Add docker based end to end hangs tests (`#219`_)
332
333 - v.0.3.2 (`Pahaz`_, `JM Fernández`_)
334 + Fix host key directory detection
335 + Unify default ssh config folder to `~/.ssh`
336
337 - v.0.3.1 (`Pahaz`_)
338 + Increase open connection timeout to 10 secods
339
340 - v.0.3.0 (`Pahaz`_)
341 + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible)
342 + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible)
343 + Set transport keepalive to 5 second by default (disabled for version < 0.3.0)
344 + Set default transport timeout to 0.1
345 + Deprecate and remove `block_on_close` option
346 + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_)
347
348 - v.0.2.2 (`Pahaz`_)
349 + Add `.stop(force=True)` for force close active connections (`#201`_)
350
351 - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_)
352 + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_)
353
354 - v.0.2.0 (`Georgy Rylov`_)
355 + Support IPv6 without proxy command. Use built-in paramiko create socket logic. The logic tries to use ipv6 socket family first, then ipv4 socket family.
356
357 - v.0.1.5 (`JM Fernández`_)
358 + Introduce `block_on_close` attribute
290359
291360 - v.0.1.4 (`Niels Zeilemaker`_)
292361 + Allow loading pkeys from `~/.ssh`
304373 - v.0.1.0 (`JM Fernández`_)
305374 + Add `tunnel_bindings` property
306375 + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...)
307 (`Pahaz Blinov`_, `JM Fernández`_)
376 (`Pahaz`_, `JM Fernández`_)
308377 + Add TRACE logging level (`JM Fernández`_)
309378 + Code and tests refactoring (`JM Fernández`_)
310379 + Drop python3.2 support
330399 + Add coverage (`JM Fernández`_)
331400 + Refactoring (`JM Fernández`_)
332401
333 - v.0.0.6 (`Pahaz Blinov`_)
334 + add ``-S`` CLI options for ssh private key password support (`Pahaz Blinov`_)
335
336 - v.0.0.5 (`Pahaz Blinov`_)
402 - v.0.0.6 (`Pahaz`_)
403 + add ``-S`` CLI options for ssh private key password support (`Pahaz`_)
404
405 - v.0.0.5 (`Pahaz`_)
337406 + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_)
338407 + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_)
339408 + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_)
340409 + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_)
341 + add tests (`Pahaz Blinov`_)
342 + add CI integration (`Pahaz Blinov`_)
343 + normal packaging (`Pahaz Blinov`_)
344 + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz Blinov`_) [changed default behavior]
345 + use daemon mode = False in all threads by default; detail_ (`Pahaz Blinov`_) [changed default behavior]
346
347 - v.0.0.4.4 (`Pahaz Blinov`_)
348 + fix issue `#24`_ - hide ssh password in logs (`Pahaz Blinov`_)
349
350 - v.0.0.4.3 (`Pahaz Blinov`_)
351 + fix default port issue `#19`_ (`Pahaz Blinov`_)
352
353 - v.0.0.4.2 (`Pahaz Blinov`_)
410 + add tests (`Pahaz`_)
411 + add CI integration (`Pahaz`_)
412 + normal packaging (`Pahaz`_)
413 + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior]
414 + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior]
415
416 - v.0.0.4.4 (`Pahaz`_)
417 + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_)
418
419 - v.0.0.4.3 (`Pahaz`_)
420 + fix default port issue `#19`_ (`Pahaz`_)
421
422 - v.0.0.4.2 (`Pahaz`_)
354423 + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_)
355424
356 - v.0.0.4.1 (`Pahaz Blinov`_)
357 + fix CLI issues `#13`_ (`Pahaz Blinov`_)
358
359 - v.0.0.4 (`Pahaz Blinov`_)
360 + daemon mode by default for all threads (`JM Fernández`_, `Pahaz Blinov`_) - *incompatible*
361 + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
362 + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz Blinov`_, `JM Fernández`_) - *incompatible*
425 - v.0.0.4.1 (`Pahaz`_)
426 + fix CLI issues `#13`_ (`Pahaz`_)
427
428 - v.0.0.4 (`Pahaz`_)
429 + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible*
430 + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
431 + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible*
363432 + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible*
364433 + add CLI interface (`JM Fernández`_)
365434 + support opening several tunnels at once (`JM Fernández`_)
366 + improve stability and readability (`JM Fernández`_, `Pahaz Blinov`_)
367 + improve logging (`JM Fernández`_, `Pahaz Blinov`_)
368 + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz Blinov`_)
435 + improve stability and readability (`JM Fernández`_, `Pahaz`_)
436 + improve logging (`JM Fernández`_, `Pahaz`_)
437 + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_)
369438 + add ``ssh_config_file`` argument support (`JM Fernández`_)
370 + add Python 3 support (`JM Fernández`_, `Pahaz Blinov`_)
371
372 - v.0.0.3 (`Pahaz Blinov`_)
439 + add Python 3 support (`JM Fernández`_, `Pahaz`_)
440
441 - v.0.0.3 (`Pahaz`_)
373442 + add ``threaded`` option (`Cameron Maske`_)
374443 + fix exception error message, correctly printing destination address (`Gustavo Machado`_)
375 + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz Blinov`_)
376
377 - v.0.0.1 (`Pahaz Blinov`_)
378 + ``SSHTunnelForwarder`` class (`Pahaz Blinov`_)
379 + ``open`` function (`Pahaz Blinov`_)
380
381
444 + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_)
445
446 - v.0.0.1 (`Pahaz`_)
447 + ``SSHTunnelForwarder`` class (`Pahaz`_)
448 + ``open`` function (`Pahaz`_)
449
450
451 .. _Pahaz: https://github.com/pahaz
382452 .. _Cameron Maske: https://github.com/cameronmaske
383453 .. _Gustavo Machado: https://github.com/gdmachado
384454 .. _Colin Jermain: https://github.com/cjermain
390460 .. _Dan Harbin: https://github.com/RasterBurn
391461 .. _Ignacio Peluffo: https://github.com/ipeluffo
392462 .. _Niels Zeilemaker: https://github.com/NielsZeilemaker
463 .. _Georgy Rylov: https://github.com/g0djan
464 .. _Eddie Chiang: https://github.com/eddie-chiang
465 .. _kkrasovskii: https://github.com/kkrasovskii
393466 .. _#13: https://github.com/pahaz/sshtunnel/issues/13
394467 .. _#16: https://github.com/pahaz/sshtunnel/issues/16
395468 .. _#19: https://github.com/pahaz/sshtunnel/issues/19
403476 .. _#41: https://github.com/pahaz/sshtunnel/issues/41
404477 .. _#43: https://github.com/pahaz/sshtunnel/issues/43
405478 .. _#46: https://github.com/pahaz/sshtunnel/issues/46
479 .. _#170: https://github.com/pahaz/sshtunnel/issues/170
480 .. _#201: https://github.com/pahaz/sshtunnel/issues/201
481 .. _#162: https://github.com/pahaz/sshtunnel/issues/162
482 .. _#173: https://github.com/pahaz/sshtunnel/issues/173
483 .. _#201: https://github.com/pahaz/sshtunnel/issues/201
484 .. _#211: https://github.com/pahaz/sshtunnel/issues/211
485 .. _#219: https://github.com/pahaz/sshtunnel/issues/219
406486 .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127
407487
408488 Keywords: ssh tunnel paramiko proxy tcp-forward
419499 Classifier: Programming Language :: Python :: 3.4
420500 Classifier: Programming Language :: Python :: 3.5
421501 Classifier: Programming Language :: Python :: 3.6
502 Classifier: Programming Language :: Python :: 3.7
503 Classifier: Programming Language :: Python :: 3.8
504 Description-Content-Type: text/x-rst
505 Provides-Extra: build_sphinx
506 Provides-Extra: dev
507 Provides-Extra: test
33 Troubleshoot.rst
44 changelog.rst
55 docs.rst
6 pyproject.toml
67 setup.cfg
78 setup.py
89 sshtunnel.py
910 docs/Makefile
1011 docs/conf.py
1112 docs/index.rst
12 docs/requirements-docs.txt
13 docs/requirements.txt
14 e2e_tests/docker-compose.yaml
15 e2e_tests/run_docker_e2e_db_tests.py
16 e2e_tests/run_docker_e2e_hangs_tests.py
17 e2e_tests/ssh-server-config/ssh_host_dsa_key
18 e2e_tests/ssh-server-config/ssh_host_dsa_key.pub
19 e2e_tests/ssh-server-config/ssh_host_ecdsa_key
20 e2e_tests/ssh-server-config/ssh_host_ecdsa_key.pub
21 e2e_tests/ssh-server-config/ssh_host_ed25519_key
22 e2e_tests/ssh-server-config/ssh_host_ed25519_key.pub
23 e2e_tests/ssh-server-config/ssh_host_rsa_key
24 e2e_tests/ssh-server-config/ssh_host_rsa_key.pub
25 e2e_tests/ssh-server-config/sshd_config
1326 sshtunnel.egg-info/PKG-INFO
1427 sshtunnel.egg-info/SOURCES.txt
1528 sshtunnel.egg-info/dependency_links.txt
1730 sshtunnel.egg-info/requires.txt
1831 sshtunnel.egg-info/top_level.txt
1932 tests/__init__.py
20 tests/__init__.pyc
33 tests/requirements-syntax.txt
34 tests/requirements.txt
2135 tests/test_forwarder.py
2236 tests/testconfig
2337 tests/testrsa.key
0 paramiko>=1.15.2
0 paramiko>=2.7.2
11
22 [build_sphinx]
33 sphinx
2828 import SocketServer as socketserver
2929 string_types = basestring, # noqa
3030 input_ = raw_input # noqa
31 else:
31 else: # pragma: no cover
3232 import queue
3333 import socketserver
3434 string_types = str
3535 input_ = input
3636
3737
38 __version__ = '0.1.4'
38 __version__ = '0.4.0'
3939 __author__ = 'pahaz'
4040
4141
42 DEFAULT_LOGLEVEL = logging.ERROR #: default level if no logger passed (ERROR)
43 TUNNEL_TIMEOUT = 1.0 #: Timeout (seconds) for tunnel connection
44 DAEMON = False
45 TRACE_LEVEL = 1
42 #: Timeout (seconds) for transport socket (``socket.settimeout``)
43 SSH_TIMEOUT = 0.1 # ``None`` may cause a block of transport thread
44 #: Timeout (seconds) for tunnel connection (open_channel timeout)
45 TUNNEL_TIMEOUT = 10.0
46
47 _DAEMON = True #: Use daemon threads in connections
4648 _CONNECTION_COUNTER = 1
4749 _LOCK = threading.Lock()
48 #: Timeout (seconds) for the connection to the SSH gateway, ``None`` to disable
49 SSH_TIMEOUT = None
50 DEPRECATIONS = {
50 _DEPRECATIONS = {
5151 'ssh_address': 'ssh_address_or_host',
5252 'ssh_host': 'ssh_address_or_host',
5353 'ssh_private_key': 'ssh_pkey',
5454 'raise_exception_if_any_forwarder_have_a_problem': 'mute_exceptions'
5555 }
5656
57 # logging
58 DEFAULT_LOGLEVEL = logging.ERROR #: default level if no logger passed (ERROR)
59 TRACE_LEVEL = 1
5760 logging.addLevelName(TRACE_LEVEL, 'TRACE')
58
59 if os.name == 'posix':
60 DEFAULT_SSH_DIRECTORY = '~/.ssh'
61 UnixStreamServer = socketserver.UnixStreamServer
62 else:
63 DEFAULT_SSH_DIRECTORY = '~/ssh'
64 UnixStreamServer = socketserver.TCPServer
61 DEFAULT_SSH_DIRECTORY = '~/.ssh'
62
63 _StreamServer = socketserver.UnixStreamServer if os.name == 'posix' \
64 else socketserver.TCPServer
6565
6666 #: Path of optional ssh configuration file
67 DEFAULT_SSH_DIRECTORY = '~/.ssh'
6768 SSH_CONFIG_FILE = os.path.join(DEFAULT_SSH_DIRECTORY, 'config')
6869
6970 ########################
189190 :class:`logging.Logger`
190191 """
191192 logger = logger or logging.getLogger(
192 '{0}.SSHTunnelForwarder'.format(__name__)
193 'sshtunnel.SSHTunnelForwarder'
193194 )
194195 if not any(isinstance(x, logging.Handler) for x in logger.handlers):
195196 logger.setLevel(loglevel or DEFAULT_LOGLEVEL)
306307 if self.request in rqst:
307308 data = self.request.recv(1024)
308309 if not data:
310 self.logger.log(
311 TRACE_LEVEL,
312 '>>> OUT {0} recv empty data >>>'.format(self.info)
313 )
309314 break
310 self.logger.log(TRACE_LEVEL,
311 '>>> OUT {0} send to {1}: {2} >>>'.format(
312 self.info,
313 self.remote_address,
314 hexlify(data)
315 ))
316 chan.send(data)
315 self.logger.log(
316 TRACE_LEVEL,
317 '>>> OUT {0} send to {1}: {2} >>>'.format(
318 self.info,
319 self.remote_address,
320 hexlify(data)
321 )
322 )
323 chan.sendall(data)
317324 if chan in rqst: # else
318325 if not chan.recv_ready():
326 self.logger.log(
327 TRACE_LEVEL,
328 '<<< IN {0} recv is not ready <<<'.format(self.info)
329 )
319330 break
320331 data = chan.recv(1024)
321332 self.logger.log(
322333 TRACE_LEVEL,
323334 '<<< IN {0} recv: {1} <<<'.format(self.info, hexlify(data))
324335 )
325 self.request.send(data)
336 self.request.sendall(data)
326337
327338 def handle(self):
328339 uid = get_connection_id()
338349 src_addr=src_address,
339350 timeout=TUNNEL_TIMEOUT
340351 )
341 except paramiko.SSHException:
342 chan = None
343 if chan is None:
344 msg = '{0} to {1} was rejected by the SSH server'.format(
345 self.info,
346 self.remote_address
347 )
348 self.logger.log(TRACE_LEVEL, msg)
349 raise HandlerSSHTunnelForwarderError(msg)
352 except Exception as e: # pragma: no cover
353 msg_tupe = 'ssh ' if isinstance(e, paramiko.SSHException) else ''
354 exc_msg = 'open new channel {0}error: {1}'.format(msg_tupe, e)
355 log_msg = '{0} {1}'.format(self.info, exc_msg)
356 self.logger.log(TRACE_LEVEL, log_msg)
357 raise HandlerSSHTunnelForwarderError(exc_msg)
350358
351359 self.logger.log(TRACE_LEVEL, '{0} connected'.format(self.info))
352360 try:
375383
376384 def __init__(self, *args, **kwargs):
377385 self.logger = create_logger(kwargs.pop('logger', None))
378 self.tunnel_ok = queue.Queue()
386 self.tunnel_ok = queue.Queue(1)
379387 socketserver.TCPServer.__init__(self, *args, **kwargs)
380388
381389 def handle_error(self, request, client_address):
382390 (exc_class, exc, tb) = sys.exc_info()
383 self.logger.error('Could not establish connection from {0} to remote '
384 'side of the tunnel'.format(request.getsockname()))
385 self.tunnel_ok.put(False)
391 local_side = request.getsockname()
392 remote_side = self.remote_address
393 self.logger.error('Could not establish connection from local {0} '
394 'to remote {1} side of the tunnel: {2}'
395 .format(local_side, remote_side, exc))
396 try:
397 self.tunnel_ok.put(False, block=False, timeout=0.1)
398 except queue.Full:
399 # wait untill tunnel_ok.get is called
400 pass
401 except exc:
402 self.logger.error('unexpected internal error: {0}'.format(exc))
386403
387404 @property
388405 def local_address(self):
414431 Allow concurrent connections to each tunnel
415432 """
416433 # If True, cleanly stop threads created by ThreadingMixIn when quitting
417 daemon_threads = DAEMON
418
419
420 class _UnixStreamForwardServer(UnixStreamServer):
421 """
422 Serve over UNIX domain sockets (does not work on Windows)
434 # This value is overrides by SSHTunnelForwarder.daemon_forward_servers
435 daemon_threads = _DAEMON
436
437
438 class _StreamForwardServer(_StreamServer):
439 """
440 Serve over domain sockets (does not work on Windows)
423441 """
424442
425443 def __init__(self, *args, **kwargs):
426444 self.logger = create_logger(kwargs.pop('logger', None))
427 self.tunnel_ok = queue.Queue()
428 UnixStreamServer.__init__(self, *args, **kwargs)
445 self.tunnel_ok = queue.Queue(1)
446 _StreamServer.__init__(self, *args, **kwargs)
429447
430448 @property
431449 def local_address(self):
452470 return self.RequestHandlerClass.remote_address[1]
453471
454472
455 class _ThreadingUnixStreamForwardServer(socketserver.ThreadingMixIn,
456 _UnixStreamForwardServer):
473 class _ThreadingStreamForwardServer(socketserver.ThreadingMixIn,
474 _StreamForwardServer):
457475 """
458476 Allow concurrent connections to each tunnel
459477 """
460478 # If True, cleanly stop threads created by ThreadingMixIn when quitting
461 daemon_threads = DAEMON
479 # This value is overrides by SSHTunnelForwarder.daemon_forward_servers
480 daemon_threads = _DAEMON
462481
463482
464483 class SSHTunnelForwarder(object):
613632
614633 host_pkey_directories (list):
615634 Look for pkeys in folders on this list, for example ['~/.ssh'].
616 An empty list disables this feature
617
618 Default: ``None``
635
636 Default: ``None`` (disabled)
619637
620638 .. versionadded:: 0.1.4
621639
648666 Interval in seconds defining the period in which, if no data
649667 was sent over the connection, a *'keepalive'* packet will be
650668 sent (and ignored by the remote host). This can be useful to
651 keep connections alive over a NAT
652
653 Default: 0.0 (no keepalive packets are sent)
669 keep connections alive over a NAT. You can set to 0.0 for
670 disable keepalive.
671
672 Default: 5.0 (no keepalive packets are sent)
654673
655674 .. versionadded:: 0.0.7
656675
719738
720739 """
721740 skip_tunnel_checkup = True
722 daemon_forward_servers = DAEMON #: flag tunnel threads in daemon mode
723 daemon_transport = DAEMON #: flag SSH transport thread in daemon mode
741 # This option affects the `ForwardServer` and all his threads
742 daemon_forward_servers = _DAEMON #: flag tunnel threads in daemon mode
743 # This option affect only `Transport` thread
744 daemon_transport = _DAEMON #: flag SSH transport thread in daemon mode
724745
725746 def local_is_up(self, target):
726747 """
746767 'target can be a valid UNIX domain socket.')
747768 return False
748769
749 if self.skip_tunnel_checkup: # force tunnel check at this point
770 self.check_tunnels()
771 return self.tunnel_is_up.get(target, True)
772
773 def check_tunnels(self):
774 """
775 Check that if all tunnels are established and populates
776 :attr:`.tunnel_is_up`
777 """
778 skip_tunnel_checkup = self.skip_tunnel_checkup
779 try:
780 # force tunnel check at this point
750781 self.skip_tunnel_checkup = False
751 self.check_tunnels()
752 self.skip_tunnel_checkup = True # roll it back
753 return self.tunnel_is_up.get(target, True)
754
755 def _make_ssh_forward_handler_class(self, remote_address_):
756 """
757 Make SSH Handler class
758 """
759 class Handler(_ForwardHandler):
760 remote_address = remote_address_
761 ssh_transport = self._transport
762 logger = self.logger
763 return Handler
764
765 def _make_ssh_forward_server_class(self, remote_address_):
766 return _ThreadingForwardServer if self._threaded else _ForwardServer
767
768 def _make_unix_ssh_forward_server_class(self, remote_address_):
769 return _ThreadingUnixStreamForwardServer if \
770 self._threaded else _UnixStreamForwardServer
771
772 def _make_ssh_forward_server(self, remote_address, local_bind_address):
773 """
774 Make SSH forward proxy Server class
775 """
776 _Handler = self._make_ssh_forward_handler_class(remote_address)
777 try:
778 if isinstance(local_bind_address, string_types):
779 forward_maker_class = self._make_unix_ssh_forward_server_class
780 else:
781 forward_maker_class = self._make_ssh_forward_server_class
782 _Server = forward_maker_class(remote_address)
783 ssh_forward_server = _Server(
784 local_bind_address,
785 _Handler,
786 logger=self.logger,
787 )
788
789 if ssh_forward_server:
790 ssh_forward_server.daemon_threads = self.daemon_forward_servers
791 self._server_list.append(ssh_forward_server)
792 self.tunnel_is_up[ssh_forward_server.server_address] = False
793 else:
794 self._raise(
795 BaseSSHTunnelForwarderError,
796 'Problem setting up ssh {0} <> {1} forwarder. You can '
797 'suppress this exception by using the `mute_exceptions`'
798 'argument'.format(address_to_str(local_bind_address),
799 address_to_str(remote_address))
800 )
801 except IOError:
802 self._raise(
803 BaseSSHTunnelForwarderError,
804 "Couldn't open tunnel {0} <> {1} might be in use or "
805 "destination not reachable".format(
806 address_to_str(local_bind_address),
807 address_to_str(remote_address)
808 )
809 )
810
811 def __init__(
812 self,
813 ssh_address_or_host=None,
814 ssh_config_file=SSH_CONFIG_FILE,
815 ssh_host_key=None,
816 ssh_password=None,
817 ssh_pkey=None,
818 ssh_private_key_password=None,
819 ssh_proxy=None,
820 ssh_proxy_enabled=True,
821 ssh_username=None,
822 local_bind_address=None,
823 local_bind_addresses=None,
824 logger=None,
825 mute_exceptions=False,
826 remote_bind_address=None,
827 remote_bind_addresses=None,
828 set_keepalive=0.0,
829 threaded=True, # old version False
830 compression=None,
831 allow_agent=True, # look for keys from an SSH agent
832 host_pkey_directories=None, # look for keys in ~/.ssh
833 *args,
834 **kwargs # for backwards compatibility
835 ):
836 self.logger = logger or create_logger()
837
838 # Ensure paramiko.transport has a console handler
839 _check_paramiko_handlers(logger=logger)
840
841 self.ssh_host_key = ssh_host_key
842 self.set_keepalive = set_keepalive
843 self._server_list = [] # reset server list
844 self.tunnel_is_up = {} # handle tunnel status
845 self._threaded = threaded
846 self.is_alive = False
847 # Check if deprecated arguments ssh_address or ssh_host were used
848 for deprecated_argument in ['ssh_address', 'ssh_host']:
849 ssh_address_or_host = self._process_deprecated(ssh_address_or_host,
850 deprecated_argument,
851 kwargs)
852 # other deprecated arguments
853 ssh_pkey = self._process_deprecated(ssh_pkey,
854 'ssh_private_key',
855 kwargs)
856
857 self._raise_fwd_exc = self._process_deprecated(
858 None,
859 'raise_exception_if_any_forwarder_have_a_problem',
860 kwargs) or not mute_exceptions
861
862 if isinstance(ssh_address_or_host, tuple):
863 check_address(ssh_address_or_host)
864 (ssh_host, ssh_port) = ssh_address_or_host
865 else:
866 ssh_host = ssh_address_or_host
867 ssh_port = kwargs.pop('ssh_port', None)
868
869 if kwargs:
870 raise ValueError('Unknown arguments: {0}'.format(kwargs))
871
872 # remote binds
873 self._remote_binds = self._get_binds(remote_bind_address,
874 remote_bind_addresses,
875 is_remote=True)
876 # local binds
877 self._local_binds = self._get_binds(local_bind_address,
878 local_bind_addresses)
879 self._local_binds = self._consolidate_binds(self._local_binds,
880 self._remote_binds)
881
882 (self.ssh_host,
883 self.ssh_username,
884 ssh_pkey, # still needs to go through _consolidate_auth
885 self.ssh_port,
886 self.ssh_proxy,
887 self.compression) = self._read_ssh_config(
888 ssh_host,
889 ssh_config_file,
890 ssh_username,
891 ssh_pkey,
892 ssh_port,
893 ssh_proxy if ssh_proxy_enabled else None,
894 compression,
895 self.logger
896 )
897
898 (self.ssh_password, self.ssh_pkeys) = self._consolidate_auth(
899 ssh_password=ssh_password,
900 ssh_pkey=ssh_pkey,
901 ssh_pkey_password=ssh_private_key_password,
902 allow_agent=allow_agent,
903 host_pkey_directories=host_pkey_directories,
904 logger=self.logger
905 )
906
907 check_host(self.ssh_host)
908 check_port(self.ssh_port)
909
910 self.logger.info("Connecting to gateway: {0}:{1} as user '{2}'"
911 .format(self.ssh_host,
912 self.ssh_port,
913 self.ssh_username))
914
915 self.logger.debug('Concurrent connections allowed: {0}'
916 .format(self._threaded))
917
918 @staticmethod
919 def _read_ssh_config(ssh_host,
920 ssh_config_file,
921 ssh_username=None,
922 ssh_pkey=None,
923 ssh_port=None,
924 ssh_proxy=None,
925 compression=None,
926 logger=None):
927 """
928 Read ssh_config_file and tries to look for user (ssh_username),
929 identityfile (ssh_pkey), port (ssh_port) and proxycommand
930 (ssh_proxy) entries for ssh_host
931 """
932 ssh_config = paramiko.SSHConfig()
933 if not ssh_config_file: # handle case where it's an empty string
934 ssh_config_file = None
935
936 # Try to read SSH_CONFIG_FILE
937 try:
938 # open the ssh config file
939 with open(os.path.expanduser(ssh_config_file), 'r') as f:
940 ssh_config.parse(f)
941 # looks for information for the destination system
942 hostname_info = ssh_config.lookup(ssh_host)
943 # gather settings for user, port and identity file
944 # last resort: use the 'login name' of the user
945 ssh_username = (
946 ssh_username or
947 hostname_info.get('user')
948 )
949 ssh_pkey = (
950 ssh_pkey or
951 hostname_info.get('identityfile', [None])[0]
952 )
953 ssh_host = hostname_info.get('hostname')
954 ssh_port = ssh_port or hostname_info.get('port')
955
956 proxycommand = hostname_info.get('proxycommand')
957 ssh_proxy = ssh_proxy or (paramiko.ProxyCommand(proxycommand) if
958 proxycommand else None)
959 if compression is None:
960 compression = hostname_info.get('compression', '')
961 compression = True if compression.upper() == 'YES' else False
962 except IOError:
963 if logger:
964 logger.warning(
965 'Could not read SSH configuration file: {0}'
966 .format(ssh_config_file)
967 )
968 except (AttributeError, TypeError): # ssh_config_file is None
969 if logger:
970 logger.info('Skipping loading of ssh configuration file')
782 for _srv in self._server_list:
783 self._check_tunnel(_srv)
971784 finally:
972 return (ssh_host,
973 ssh_username or getpass.getuser(),
974 ssh_pkey,
975 int(ssh_port) if ssh_port else 22, # fallback value
976 ssh_proxy,
977 compression)
978
979 @staticmethod
980 def get_agent_keys(logger=None):
981 """ Load public keys from any available SSH agent
982
983 Arguments:
984 logger (Optional[logging.Logger])
985
986 Return:
987 list
988 """
989 paramiko_agent = paramiko.Agent()
990 agent_keys = paramiko_agent.get_keys()
991 if logger:
992 logger.info('{0} keys loaded from agent'.format(len(agent_keys)))
993 return list(agent_keys)
994
995 @staticmethod
996 def get_keys(logger=None, host_pkey_directories=None, allow_agent=False):
997 """
998 Load public keys from any available SSH agent or local
999 .ssh directory.
1000
1001 Arguments:
1002 logger (Optional[logging.Logger])
1003
1004 host_pkey_directories (Optional[list[str]]):
1005 List of local directories where host SSH pkeys in the format
1006 "id_*" are searched. For example, ['~/.ssh']
1007
1008 .. versionadded:: 0.1.0
1009
1010 allow_agent (Optional[boolean]):
1011 Whether or not load keys from agent
1012
1013 Default: False
1014
1015 Return:
1016 list
1017 """
1018 keys = SSHTunnelForwarder.get_agent_keys(logger=logger) \
1019 if allow_agent else []
1020
1021 if host_pkey_directories is not None:
1022 paramiko_key_types = {'rsa': paramiko.RSAKey,
1023 'dsa': paramiko.DSSKey,
1024 'ecdsa': paramiko.ECDSAKey,
1025 'ed25519': paramiko.Ed25519Key}
1026 for directory in host_pkey_directories or [DEFAULT_SSH_DIRECTORY]:
1027 for keytype in paramiko_key_types.keys():
1028 ssh_pkey_expanded = os.path.expanduser(
1029 os.path.join(directory, 'id_{}'.format(keytype))
1030 )
1031 if os.path.isfile(ssh_pkey_expanded):
1032 ssh_pkey = SSHTunnelForwarder.read_private_key_file(
1033 pkey_file=ssh_pkey_expanded,
1034 logger=logger,
1035 key_type=paramiko_key_types[keytype]
1036 )
1037 if ssh_pkey:
1038 keys.append(ssh_pkey)
1039 if logger:
1040 logger.info('{0} keys loaded from host directory'.format(
1041 len(keys))
1042 )
1043
1044 return keys
1045
1046 @staticmethod
1047 def _consolidate_binds(local_binds, remote_binds):
1048 """
1049 Fill local_binds with defaults when no value/s were specified,
1050 leaving paramiko to decide in which local port the tunnel will be open
1051 """
1052 count = len(remote_binds) - len(local_binds)
1053 if count < 0:
1054 raise ValueError('Too many local bind addresses '
1055 '(local_bind_addresses > remote_bind_addresses)')
1056 local_binds.extend([('0.0.0.0', 0) for x in range(count)])
1057 return local_binds
1058
1059 @staticmethod
1060 def _consolidate_auth(ssh_password=None,
1061 ssh_pkey=None,
1062 ssh_pkey_password=None,
1063 allow_agent=True,
1064 host_pkey_directories=None,
1065 logger=None):
1066 """
1067 Get sure authentication information is in place.
1068 ``ssh_pkey`` may be of classes:
1069 - ``str`` - in this case it represents a private key file; public
1070 key will be obtained from it
1071 - ``paramiko.Pkey`` - it will be transparently added to loaded keys
1072
1073 """
1074 ssh_loaded_pkeys = SSHTunnelForwarder.get_keys(
1075 logger=logger,
1076 host_pkey_directories=host_pkey_directories,
1077 allow_agent=allow_agent
1078 )
1079
1080 if isinstance(ssh_pkey, string_types):
1081 ssh_pkey_expanded = os.path.expanduser(ssh_pkey)
1082 if os.path.exists(ssh_pkey_expanded):
1083 ssh_pkey = SSHTunnelForwarder.read_private_key_file(
1084 pkey_file=ssh_pkey_expanded,
1085 pkey_password=ssh_pkey_password or ssh_password,
1086 logger=logger
1087 )
1088 elif logger:
1089 logger.warning('Private key file not found: {0}'
1090 .format(ssh_pkey))
1091 if isinstance(ssh_pkey, paramiko.pkey.PKey):
1092 ssh_loaded_pkeys.append(ssh_pkey)
1093
1094 if not ssh_password and not ssh_loaded_pkeys:
1095 raise ValueError('No password or public key available!')
1096 return (ssh_password, ssh_loaded_pkeys)
1097
1098 def _raise(self, exception=BaseSSHTunnelForwarderError, reason=None):
1099 if self._raise_fwd_exc:
1100 raise exception(reason)
1101 else:
1102 self.logger.error(repr(exception(reason)))
1103
1104 def _get_transport(self):
1105 """ Return the SSH transport to the remote gateway """
1106 if self.ssh_proxy:
1107 if isinstance(self.ssh_proxy, paramiko.proxy.ProxyCommand):
1108 proxy_repr = repr(self.ssh_proxy.cmd[1])
1109 else:
1110 proxy_repr = repr(self.ssh_proxy)
1111 self.logger.debug('Connecting via proxy: {0}'.format(proxy_repr))
1112 _socket = self.ssh_proxy
1113 else:
1114 _socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
1115 if isinstance(_socket, socket.socket):
1116 _socket.settimeout(SSH_TIMEOUT)
1117 _socket.connect((self.ssh_host, self.ssh_port))
1118 transport = paramiko.Transport(_socket)
1119 transport.set_keepalive(self.set_keepalive)
1120 transport.use_compression(compress=self.compression)
1121 transport.daemon = self.daemon_transport
1122
1123 return transport
1124
1125 def _create_tunnels(self):
1126 """
1127 Create SSH tunnels on top of a transport to the remote gateway
1128 """
1129 if not self.is_active:
1130 try:
1131 self._connect_to_gateway()
1132 except socket.gaierror: # raised by paramiko.Transport
1133 msg = 'Could not resolve IP address for {0}, aborting!' \
1134 .format(self.ssh_host)
1135 self.logger.error(msg)
1136 return
1137 except (paramiko.SSHException, socket.error) as e:
1138 template = 'Could not connect to gateway {0}:{1} : {2}'
1139 msg = template.format(self.ssh_host, self.ssh_port, e.args[0])
1140 self.logger.error(msg)
1141 return
1142 for (rem, loc) in zip(self._remote_binds, self._local_binds):
1143 try:
1144 self._make_ssh_forward_server(rem, loc)
1145 except BaseSSHTunnelForwarderError as e:
1146 msg = 'Problem setting SSH Forwarder up: {0}'.format(e.value)
1147 self.logger.error(msg)
1148
1149 @staticmethod
1150 def _get_binds(bind_address, bind_addresses, is_remote=False):
1151 addr_kind = 'remote' if is_remote else 'local'
1152
1153 if not bind_address and not bind_addresses:
1154 if is_remote:
1155 raise ValueError("No {0} bind addresses specified. Use "
1156 "'{0}_bind_address' or '{0}_bind_addresses'"
1157 " argument".format(addr_kind))
1158 else:
1159 return []
1160 elif bind_address and bind_addresses:
1161 raise ValueError("You can't use both '{0}_bind_address' and "
1162 "'{0}_bind_addresses' arguments. Use one of "
1163 "them.".format(addr_kind))
1164 if bind_address:
1165 bind_addresses = [bind_address]
1166 if not is_remote:
1167 # Add random port if missing in local bind
1168 for (i, local_bind) in enumerate(bind_addresses):
1169 if isinstance(local_bind, tuple) and len(local_bind) == 1:
1170 bind_addresses[i] = (local_bind[0], 0)
1171 check_addresses(bind_addresses, is_remote)
1172 return bind_addresses
1173
1174 @staticmethod
1175 def _process_deprecated(attrib, deprecated_attrib, kwargs):
1176 """
1177 Processes optional deprecate arguments
1178 """
1179 if deprecated_attrib not in DEPRECATIONS:
1180 raise ValueError('{0} not included in deprecations list'
1181 .format(deprecated_attrib))
1182 if deprecated_attrib in kwargs:
1183 warnings.warn("'{0}' is DEPRECATED use '{1}' instead"
1184 .format(deprecated_attrib,
1185 DEPRECATIONS[deprecated_attrib]),
1186 DeprecationWarning)
1187 if attrib:
1188 raise ValueError("You can't use both '{0}' and '{1}'. "
1189 "Please only use one of them"
1190 .format(deprecated_attrib,
1191 DEPRECATIONS[deprecated_attrib]))
1192 else:
1193 return kwargs.pop(deprecated_attrib)
1194 return attrib
1195
1196 @staticmethod
1197 def read_private_key_file(pkey_file,
1198 pkey_password=None,
1199 key_type=None,
1200 logger=None):
1201 """
1202 Get SSH Public key from a private key file, given an optional password
1203
1204 Arguments:
1205 pkey_file (str):
1206 File containing a private key (RSA, DSS or ECDSA)
1207 Keyword Arguments:
1208 pkey_password (Optional[str]):
1209 Password to decrypt the private key
1210 logger (Optional[logging.Logger])
1211 Return:
1212 paramiko.Pkey
1213 """
1214 ssh_pkey = None
1215 for pkey_class in (key_type,) if key_type else (
1216 paramiko.RSAKey,
1217 paramiko.DSSKey,
1218 paramiko.ECDSAKey,
1219 paramiko.Ed25519Key
1220 ):
1221 try:
1222 ssh_pkey = pkey_class.from_private_key_file(
1223 pkey_file,
1224 password=pkey_password
1225 )
1226 if logger:
1227 logger.debug('Private key file ({0}, {1}) successfully '
1228 'loaded'.format(pkey_file, pkey_class))
1229 break
1230 except paramiko.PasswordRequiredException:
1231 if logger:
1232 logger.error('Password is required for key {0}'
1233 .format(pkey_file))
1234 break
1235 except paramiko.SSHException:
1236 if logger:
1237 logger.debug('Private key file ({0}) could not be loaded '
1238 'as type {1} or bad password'
1239 .format(pkey_file, pkey_class))
1240 return ssh_pkey
785 self.skip_tunnel_checkup = skip_tunnel_checkup # roll it back
1241786
1242787 def _check_tunnel(self, _srv):
1243788 """ Check if tunnel is already established """
1275820 finally:
1276821 s.close()
1277822
1278 def check_tunnels(self):
1279 """
1280 Check that if all tunnels are established and populates
1281 :attr:`.tunnel_is_up`
1282 """
1283 for _srv in self._server_list:
1284 self._check_tunnel(_srv)
823 def _make_ssh_forward_handler_class(self, remote_address_):
824 """
825 Make SSH Handler class
826 """
827 class Handler(_ForwardHandler):
828 remote_address = remote_address_
829 ssh_transport = self._transport
830 logger = self.logger
831 return Handler
832
833 def _make_ssh_forward_server_class(self, remote_address_):
834 return _ThreadingForwardServer if self._threaded else _ForwardServer
835
836 def _make_stream_ssh_forward_server_class(self, remote_address_):
837 return _ThreadingStreamForwardServer if self._threaded \
838 else _StreamForwardServer
839
840 def _make_ssh_forward_server(self, remote_address, local_bind_address):
841 """
842 Make SSH forward proxy Server class
843 """
844 _Handler = self._make_ssh_forward_handler_class(remote_address)
845 try:
846 forward_maker_class = self._make_stream_ssh_forward_server_class \
847 if isinstance(local_bind_address, string_types) \
848 else self._make_ssh_forward_server_class
849 _Server = forward_maker_class(remote_address)
850 ssh_forward_server = _Server(
851 local_bind_address,
852 _Handler,
853 logger=self.logger,
854 )
855
856 if ssh_forward_server:
857 ssh_forward_server.daemon_threads = self.daemon_forward_servers
858 self._server_list.append(ssh_forward_server)
859 self.tunnel_is_up[ssh_forward_server.server_address] = False
860 else:
861 self._raise(
862 BaseSSHTunnelForwarderError,
863 'Problem setting up ssh {0} <> {1} forwarder. You can '
864 'suppress this exception by using the `mute_exceptions`'
865 'argument'.format(address_to_str(local_bind_address),
866 address_to_str(remote_address))
867 )
868 except IOError:
869 self._raise(
870 BaseSSHTunnelForwarderError,
871 "Couldn't open tunnel {0} <> {1} might be in use or "
872 "destination not reachable".format(
873 address_to_str(local_bind_address),
874 address_to_str(remote_address)
875 )
876 )
877
878 def __init__(
879 self,
880 ssh_address_or_host=None,
881 ssh_config_file=SSH_CONFIG_FILE,
882 ssh_host_key=None,
883 ssh_password=None,
884 ssh_pkey=None,
885 ssh_private_key_password=None,
886 ssh_proxy=None,
887 ssh_proxy_enabled=True,
888 ssh_username=None,
889 local_bind_address=None,
890 local_bind_addresses=None,
891 logger=None,
892 mute_exceptions=False,
893 remote_bind_address=None,
894 remote_bind_addresses=None,
895 set_keepalive=5.0,
896 threaded=True, # old version False
897 compression=None,
898 allow_agent=True, # look for keys from an SSH agent
899 host_pkey_directories=None, # look for keys in ~/.ssh
900 *args,
901 **kwargs # for backwards compatibility
902 ):
903 self.logger = logger or create_logger()
904
905 # Ensure paramiko.transport has a console handler
906 _check_paramiko_handlers(logger=logger)
907
908 self.ssh_host_key = ssh_host_key
909 self.set_keepalive = set_keepalive
910 self._server_list = [] # reset server list
911 self.tunnel_is_up = {} # handle tunnel status
912 self._threaded = threaded
913 self.is_alive = False
914 # Check if deprecated arguments ssh_address or ssh_host were used
915 for deprecated_argument in ['ssh_address', 'ssh_host']:
916 ssh_address_or_host = self._process_deprecated(ssh_address_or_host,
917 deprecated_argument,
918 kwargs)
919 # other deprecated arguments
920 ssh_pkey = self._process_deprecated(ssh_pkey,
921 'ssh_private_key',
922 kwargs)
923
924 self._raise_fwd_exc = self._process_deprecated(
925 None,
926 'raise_exception_if_any_forwarder_have_a_problem',
927 kwargs) or not mute_exceptions
928
929 if isinstance(ssh_address_or_host, tuple):
930 check_address(ssh_address_or_host)
931 (ssh_host, ssh_port) = ssh_address_or_host
932 else:
933 ssh_host = ssh_address_or_host
934 ssh_port = kwargs.pop('ssh_port', None)
935
936 if kwargs:
937 raise ValueError('Unknown arguments: {0}'.format(kwargs))
938
939 # remote binds
940 self._remote_binds = self._get_binds(remote_bind_address,
941 remote_bind_addresses,
942 is_remote=True)
943 # local binds
944 self._local_binds = self._get_binds(local_bind_address,
945 local_bind_addresses)
946 self._local_binds = self._consolidate_binds(self._local_binds,
947 self._remote_binds)
948
949 (self.ssh_host,
950 self.ssh_username,
951 ssh_pkey, # still needs to go through _consolidate_auth
952 self.ssh_port,
953 self.ssh_proxy,
954 self.compression) = self._read_ssh_config(
955 ssh_host,
956 ssh_config_file,
957 ssh_username,
958 ssh_pkey,
959 ssh_port,
960 ssh_proxy if ssh_proxy_enabled else None,
961 compression,
962 self.logger
963 )
964
965 (self.ssh_password, self.ssh_pkeys) = self._consolidate_auth(
966 ssh_password=ssh_password,
967 ssh_pkey=ssh_pkey,
968 ssh_pkey_password=ssh_private_key_password,
969 allow_agent=allow_agent,
970 host_pkey_directories=host_pkey_directories,
971 logger=self.logger
972 )
973
974 check_host(self.ssh_host)
975 check_port(self.ssh_port)
976
977 self.logger.info("Connecting to gateway: {0}:{1} as user '{2}'"
978 .format(self.ssh_host,
979 self.ssh_port,
980 self.ssh_username))
981
982 self.logger.debug('Concurrent connections allowed: {0}'
983 .format(self._threaded))
984
985 @staticmethod
986 def _read_ssh_config(ssh_host,
987 ssh_config_file,
988 ssh_username=None,
989 ssh_pkey=None,
990 ssh_port=None,
991 ssh_proxy=None,
992 compression=None,
993 logger=None):
994 """
995 Read ssh_config_file and tries to look for user (ssh_username),
996 identityfile (ssh_pkey), port (ssh_port) and proxycommand
997 (ssh_proxy) entries for ssh_host
998 """
999 ssh_config = paramiko.SSHConfig()
1000 if not ssh_config_file: # handle case where it's an empty string
1001 ssh_config_file = None
1002
1003 # Try to read SSH_CONFIG_FILE
1004 try:
1005 # open the ssh config file
1006 with open(os.path.expanduser(ssh_config_file), 'r') as f:
1007 ssh_config.parse(f)
1008 # looks for information for the destination system
1009 hostname_info = ssh_config.lookup(ssh_host)
1010 # gather settings for user, port and identity file
1011 # last resort: use the 'login name' of the user
1012 ssh_username = (
1013 ssh_username or
1014 hostname_info.get('user')
1015 )
1016 ssh_pkey = (
1017 ssh_pkey or
1018 hostname_info.get('identityfile', [None])[0]
1019 )
1020 ssh_host = hostname_info.get('hostname')
1021 ssh_port = ssh_port or hostname_info.get('port')
1022
1023 proxycommand = hostname_info.get('proxycommand')
1024 ssh_proxy = ssh_proxy or (paramiko.ProxyCommand(proxycommand) if
1025 proxycommand else None)
1026 if compression is None:
1027 compression = hostname_info.get('compression', '')
1028 compression = True if compression.upper() == 'YES' else False
1029 except IOError:
1030 if logger:
1031 logger.warning(
1032 'Could not read SSH configuration file: {0}'
1033 .format(ssh_config_file)
1034 )
1035 except (AttributeError, TypeError): # ssh_config_file is None
1036 if logger:
1037 logger.info('Skipping loading of ssh configuration file')
1038 finally:
1039 return (ssh_host,
1040 ssh_username or getpass.getuser(),
1041 ssh_pkey,
1042 int(ssh_port) if ssh_port else 22, # fallback value
1043 ssh_proxy,
1044 compression)
1045
1046 @staticmethod
1047 def get_agent_keys(logger=None):
1048 """ Load public keys from any available SSH agent
1049
1050 Arguments:
1051 logger (Optional[logging.Logger])
1052
1053 Return:
1054 list
1055 """
1056 paramiko_agent = paramiko.Agent()
1057 agent_keys = paramiko_agent.get_keys()
1058 if logger:
1059 logger.info('{0} keys loaded from agent'.format(len(agent_keys)))
1060 return list(agent_keys)
1061
1062 @staticmethod
1063 def get_keys(logger=None, host_pkey_directories=None, allow_agent=False):
1064 """
1065 Load public keys from any available SSH agent or local
1066 .ssh directory.
1067
1068 Arguments:
1069 logger (Optional[logging.Logger])
1070
1071 host_pkey_directories (Optional[list[str]]):
1072 List of local directories where host SSH pkeys in the format
1073 "id_*" are searched. For example, ['~/.ssh']
1074
1075 .. versionadded:: 0.1.0
1076
1077 allow_agent (Optional[boolean]):
1078 Whether or not load keys from agent
1079
1080 Default: False
1081
1082 Return:
1083 list
1084 """
1085 keys = SSHTunnelForwarder.get_agent_keys(logger=logger) \
1086 if allow_agent else []
1087
1088 if host_pkey_directories is None:
1089 host_pkey_directories = [DEFAULT_SSH_DIRECTORY]
1090
1091 paramiko_key_types = {'rsa': paramiko.RSAKey,
1092 'dsa': paramiko.DSSKey,
1093 'ecdsa': paramiko.ECDSAKey}
1094 if hasattr(paramiko, 'Ed25519Key'):
1095 # NOQA: new in paramiko>=2.2: http://docs.paramiko.org/en/stable/api/keys.html#module-paramiko.ed25519key
1096 paramiko_key_types['ed25519'] = paramiko.Ed25519Key
1097 for directory in host_pkey_directories:
1098 for keytype in paramiko_key_types.keys():
1099 ssh_pkey_expanded = os.path.expanduser(
1100 os.path.join(directory, 'id_{}'.format(keytype))
1101 )
1102 try:
1103 if os.path.isfile(ssh_pkey_expanded):
1104 ssh_pkey = SSHTunnelForwarder.read_private_key_file(
1105 pkey_file=ssh_pkey_expanded,
1106 logger=logger,
1107 key_type=paramiko_key_types[keytype]
1108 )
1109 if ssh_pkey:
1110 keys.append(ssh_pkey)
1111 except OSError as exc:
1112 if logger:
1113 logger.warning('Private key file {0} check error: {1}'
1114 .format(ssh_pkey_expanded, exc))
1115 if logger:
1116 logger.info('{0} key(s) loaded'.format(len(keys)))
1117 return keys
1118
1119 @staticmethod
1120 def _consolidate_binds(local_binds, remote_binds):
1121 """
1122 Fill local_binds with defaults when no value/s were specified,
1123 leaving paramiko to decide in which local port the tunnel will be open
1124 """
1125 count = len(remote_binds) - len(local_binds)
1126 if count < 0:
1127 raise ValueError('Too many local bind addresses '
1128 '(local_bind_addresses > remote_bind_addresses)')
1129 local_binds.extend([('0.0.0.0', 0) for x in range(count)])
1130 return local_binds
1131
1132 @staticmethod
1133 def _consolidate_auth(ssh_password=None,
1134 ssh_pkey=None,
1135 ssh_pkey_password=None,
1136 allow_agent=True,
1137 host_pkey_directories=None,
1138 logger=None):
1139 """
1140 Get sure authentication information is in place.
1141 ``ssh_pkey`` may be of classes:
1142 - ``str`` - in this case it represents a private key file; public
1143 key will be obtained from it
1144 - ``paramiko.Pkey`` - it will be transparently added to loaded keys
1145
1146 """
1147 ssh_loaded_pkeys = SSHTunnelForwarder.get_keys(
1148 logger=logger,
1149 host_pkey_directories=host_pkey_directories,
1150 allow_agent=allow_agent
1151 )
1152
1153 if isinstance(ssh_pkey, string_types):
1154 ssh_pkey_expanded = os.path.expanduser(ssh_pkey)
1155 if os.path.exists(ssh_pkey_expanded):
1156 ssh_pkey = SSHTunnelForwarder.read_private_key_file(
1157 pkey_file=ssh_pkey_expanded,
1158 pkey_password=ssh_pkey_password or ssh_password,
1159 logger=logger
1160 )
1161 elif logger:
1162 logger.warning('Private key file not found: {0}'
1163 .format(ssh_pkey))
1164 if isinstance(ssh_pkey, paramiko.pkey.PKey):
1165 ssh_loaded_pkeys.insert(0, ssh_pkey)
1166
1167 if not ssh_password and not ssh_loaded_pkeys:
1168 raise ValueError('No password or public key available!')
1169 return (ssh_password, ssh_loaded_pkeys)
1170
1171 def _raise(self, exception=BaseSSHTunnelForwarderError, reason=None):
1172 if self._raise_fwd_exc:
1173 raise exception(reason)
1174 else:
1175 self.logger.error(repr(exception(reason)))
1176
1177 def _get_transport(self):
1178 """ Return the SSH transport to the remote gateway """
1179 if self.ssh_proxy:
1180 if isinstance(self.ssh_proxy, paramiko.proxy.ProxyCommand):
1181 proxy_repr = repr(self.ssh_proxy.cmd[1])
1182 else:
1183 proxy_repr = repr(self.ssh_proxy)
1184 self.logger.debug('Connecting via proxy: {0}'.format(proxy_repr))
1185 _socket = self.ssh_proxy
1186 else:
1187 _socket = (self.ssh_host, self.ssh_port)
1188 if isinstance(_socket, socket.socket):
1189 _socket.settimeout(SSH_TIMEOUT)
1190 _socket.connect((self.ssh_host, self.ssh_port))
1191 transport = paramiko.Transport(_socket)
1192 sock = transport.sock
1193 if isinstance(sock, socket.socket):
1194 sock.settimeout(SSH_TIMEOUT)
1195 transport.set_keepalive(self.set_keepalive)
1196 transport.use_compression(compress=self.compression)
1197 transport.daemon = self.daemon_transport
1198 # try to solve https://github.com/paramiko/paramiko/issues/1181
1199 # transport.banner_timeout = 200
1200 if isinstance(sock, socket.socket):
1201 sock_timeout = sock.gettimeout()
1202 sock_info = repr((sock.family, sock.type, sock.proto))
1203 self.logger.debug('Transport socket info: {0}, timeout={1}'
1204 .format(sock_info, sock_timeout))
1205 return transport
1206
1207 def _create_tunnels(self):
1208 """
1209 Create SSH tunnels on top of a transport to the remote gateway
1210 """
1211 if not self.is_active:
1212 try:
1213 self._connect_to_gateway()
1214 except socket.gaierror: # raised by paramiko.Transport
1215 msg = 'Could not resolve IP address for {0}, aborting!' \
1216 .format(self.ssh_host)
1217 self.logger.error(msg)
1218 return
1219 except (paramiko.SSHException, socket.error) as e:
1220 template = 'Could not connect to gateway {0}:{1} : {2}'
1221 msg = template.format(self.ssh_host, self.ssh_port, e.args[0])
1222 self.logger.error(msg)
1223 return
1224 for (rem, loc) in zip(self._remote_binds, self._local_binds):
1225 try:
1226 self._make_ssh_forward_server(rem, loc)
1227 except BaseSSHTunnelForwarderError as e:
1228 msg = 'Problem setting SSH Forwarder up: {0}'.format(e.value)
1229 self.logger.error(msg)
1230
1231 @staticmethod
1232 def _get_binds(bind_address, bind_addresses, is_remote=False):
1233 addr_kind = 'remote' if is_remote else 'local'
1234
1235 if not bind_address and not bind_addresses:
1236 if is_remote:
1237 raise ValueError("No {0} bind addresses specified. Use "
1238 "'{0}_bind_address' or '{0}_bind_addresses'"
1239 " argument".format(addr_kind))
1240 else:
1241 return []
1242 elif bind_address and bind_addresses:
1243 raise ValueError("You can't use both '{0}_bind_address' and "
1244 "'{0}_bind_addresses' arguments. Use one of "
1245 "them.".format(addr_kind))
1246 if bind_address:
1247 bind_addresses = [bind_address]
1248 if not is_remote:
1249 # Add random port if missing in local bind
1250 for (i, local_bind) in enumerate(bind_addresses):
1251 if isinstance(local_bind, tuple) and len(local_bind) == 1:
1252 bind_addresses[i] = (local_bind[0], 0)
1253 check_addresses(bind_addresses, is_remote)
1254 return bind_addresses
1255
1256 @staticmethod
1257 def _process_deprecated(attrib, deprecated_attrib, kwargs):
1258 """
1259 Processes optional deprecate arguments
1260 """
1261 if deprecated_attrib not in _DEPRECATIONS:
1262 raise ValueError('{0} not included in deprecations list'
1263 .format(deprecated_attrib))
1264 if deprecated_attrib in kwargs:
1265 warnings.warn("'{0}' is DEPRECATED use '{1}' instead"
1266 .format(deprecated_attrib,
1267 _DEPRECATIONS[deprecated_attrib]),
1268 DeprecationWarning)
1269 if attrib:
1270 raise ValueError("You can't use both '{0}' and '{1}'. "
1271 "Please only use one of them"
1272 .format(deprecated_attrib,
1273 _DEPRECATIONS[deprecated_attrib]))
1274 else:
1275 return kwargs.pop(deprecated_attrib)
1276 return attrib
1277
1278 @staticmethod
1279 def read_private_key_file(pkey_file,
1280 pkey_password=None,
1281 key_type=None,
1282 logger=None):
1283 """
1284 Get SSH Public key from a private key file, given an optional password
1285
1286 Arguments:
1287 pkey_file (str):
1288 File containing a private key (RSA, DSS or ECDSA)
1289 Keyword Arguments:
1290 pkey_password (Optional[str]):
1291 Password to decrypt the private key
1292 logger (Optional[logging.Logger])
1293 Return:
1294 paramiko.Pkey
1295 """
1296 ssh_pkey = None
1297 key_types = (paramiko.RSAKey, paramiko.DSSKey, paramiko.ECDSAKey)
1298 if hasattr(paramiko, 'Ed25519Key'):
1299 # NOQA: new in paramiko>=2.2: http://docs.paramiko.org/en/stable/api/keys.html#module-paramiko.ed25519key
1300 key_types += (paramiko.Ed25519Key, )
1301 for pkey_class in (key_type,) if key_type else key_types:
1302 try:
1303 ssh_pkey = pkey_class.from_private_key_file(
1304 pkey_file,
1305 password=pkey_password
1306 )
1307 if logger:
1308 logger.debug('Private key file ({0}, {1}) successfully '
1309 'loaded'.format(pkey_file, pkey_class))
1310 break
1311 except paramiko.PasswordRequiredException:
1312 if logger:
1313 logger.error('Password is required for key {0}'
1314 .format(pkey_file))
1315 break
1316 except paramiko.SSHException:
1317 if logger:
1318 logger.debug('Private key file ({0}) could not be loaded '
1319 'as type {1} or bad password'
1320 .format(pkey_file, pkey_class))
1321 return ssh_pkey
12851322
12861323 def start(self):
12871324 """ Start the SSH tunnels """
13061343 self._raise(HandlerSSHTunnelForwarderError,
13071344 'An error occurred while opening tunnels.')
13081345
1309 def stop(self):
1310 """
1311 Shut the tunnel down.
1346 def stop(self, force=False):
1347 """
1348 Shut the tunnel down. By default we are always waiting until closing
1349 all connections. You can use `force=True` to force close connections
1350
1351 Keyword Arguments:
1352 force (bool):
1353 Force close current connections
1354
1355 Default: False
1356
1357 .. versionadded:: 0.2.2
13121358
13131359 .. note:: This **had** to be handled with care before ``0.1.0``:
13141360
13291375 (address_to_str(k.local_address) for k in self._server_list)
13301376 ) or 'None'
13311377 self.logger.debug('Listening tunnels: ' + opened_address_text)
1332 self._stop_transport()
1378 self._stop_transport(force=force)
13331379 self._server_list = [] # reset server list
13341380 self.tunnel_is_up = {} # reset tunnel status
13351381
13941440 address_to_str(_srv.remote_address))
13951441 )
13961442
1397 def _stop_transport(self):
1443 def _stop_transport(self, force=False):
13981444 """ Close the underlying transport when nothing more is needed """
13991445 try:
14001446 self._check_is_started()
14011447 except (BaseSSHTunnelForwarderError,
14021448 HandlerSSHTunnelForwarderError) as e:
14031449 self.logger.warning(e)
1450 if force and self.is_active:
1451 # don't wait connections
1452 self.logger.info('Closing ssh transport')
1453 self._transport.close()
1454 self._transport.stop_thread()
14041455 for _srv in self._server_list:
1405 tunnel = _srv.local_address
1406 if self.tunnel_is_up[tunnel]:
1407 self.logger.info('Shutting down tunnel {0}'.format(tunnel))
1408 _srv.shutdown()
1456 status = 'up' if self.tunnel_is_up[_srv.local_address] else 'down'
1457 self.logger.info('Shutting down tunnel: {0} <> {1} ({2})'.format(
1458 address_to_str(_srv.local_address),
1459 address_to_str(_srv.remote_address),
1460 status
1461 ))
1462 _srv.shutdown()
14091463 _srv.server_close()
14101464 # clean up the UNIX domain socket if we're using one
1411 if isinstance(_srv, _UnixStreamForwardServer):
1465 if isinstance(_srv, _StreamForwardServer):
14121466 try:
14131467 os.unlink(_srv.local_address)
14141468 except Exception as e:
14151469 self.logger.error('Unable to unlink socket {0}: {1}'
1416 .format(self.local_address, repr(e)))
1470 .format(_srv.local_address, repr(e)))
14171471 self.is_alive = False
14181472 if self.is_active:
1473 self.logger.info('Closing ssh transport')
14191474 self._transport.close()
14201475 self._transport.stop_thread()
14211476 self.logger.debug('Transport is closed')
15321587 self.ssh_proxy.cmd[1] if self.ssh_proxy else 'no',
15331588 self.ssh_username,
15341589 credentials,
1535 self.ssh_host_key if self.ssh_host_key else'not checked',
1590 self.ssh_host_key if self.ssh_host_key else 'not checked',
15361591 '' if self.is_alive else 'not ',
15371592 'disabled' if not self.set_keepalive else
15381593 'every {0} sec'.format(self.set_keepalive),
15551610 self.__exit__()
15561611
15571612 def __exit__(self, *args):
1558 self._stop_transport()
1613 self.stop(force=True)
1614
1615 def __del__(self):
1616 if self.is_active or self.is_alive:
1617 self.logger.warning(
1618 "It looks like you didn't call the .stop() before "
1619 "the SSHTunnelForwarder obj was collected by "
1620 "the garbage collector! Running .stop(force=True)")
1621 self.stop(force=True)
15591622
15601623
15611624 def open_tunnel(*args, **kwargs):
16141677 kwargs
16151678 )
16161679
1617 ssh_port = kwargs.pop('ssh_port', None)
1680 ssh_port = kwargs.pop('ssh_port', 22)
16181681 skip_tunnel_checkup = kwargs.pop('skip_tunnel_checkup', True)
1682 block_on_close = kwargs.pop('block_on_close', None)
1683 if block_on_close:
1684 warnings.warn("'block_on_close' is DEPRECATED. You should use either"
1685 " .stop() or .stop(force=True), depends on what you do"
1686 " with the active connections. This option has no"
1687 " affect since 0.3.0",
1688 DeprecationWarning)
16191689 if not args:
16201690 if isinstance(ssh_address_or_host, tuple):
16211691 args = (ssh_address_or_host, )
18071877 return vars(parser.parse_args(args))
18081878
18091879
1810 def _cli_main(args=None):
1880 def _cli_main(args=None, **extras):
18111881 """ Pass input arguments to open_tunnel
18121882
18131883 Mandatory: ssh_address, -R (remote bind address list)
18391909 logging.DEBUG,
18401910 TRACE_LEVEL]
18411911 arguments.setdefault('debug_level', levels[verbosity])
1912 # do this while supporting py27/py34 instead of merging dicts
1913 for (extra, value) in extras.items():
1914 arguments.setdefault(extra, value)
18421915 with open_tunnel(**arguments) as tunnel:
18431916 if tunnel.is_alive:
18441917 input_('''
tests/__init__.pyc less more
Binary diff not shown
0 bashtest
1 check-manifest
2 docutils
3 flake8
4 mccabe
5 pygments
6 readme
7 twine
0 coveralls
1 mock
2 pytest
3 pytest-cov
4 pytest-xdist
5 twine
6 # readme-renderer (required by twine) 25.0 has removed support for Python 3.4
7 readme-renderer>21.0,<25.0; python_version == '3.4'
8 # try to solve CI problem
9 importlib-metadata==1.7.0; python_version == '3.5'
10 importlib-metadata==1.1.3; python_version == '3.4'
7373 ssh_config_file=None,
7474 allow_agent=False,
7575 skip_tunnel_checkup=True,
76 host_pkey_directories=[],
7677 )
7778
7879 # CONSTANTS
168169 return paramiko.AUTH_SUCCESSFUL if _ok else paramiko.AUTH_FAILED
169170
170171 def check_channel_request(self, kind, chanid):
171 self.log.debug('NullServer.check_channel_request()'
172 .format(kind, chanid))
172 self.log.debug('NullServer.check_channel_request()')
173173 return paramiko.OPEN_SUCCEEDED
174174
175175 def check_channel_exec_request(self, channel, command):
176 self.log.debug('NullServer.check_channel_exec_request()'
177 .format(channel, command))
176 self.log.debug('NullServer.check_channel_exec_request()')
178177 return True
179178
180179 def check_port_forward_request(self, address, port):
181 self.log.debug('NullServer.check_port_forward_request()'
182 .format(address, port))
180 self.log.debug('NullServer.check_port_forward_request()')
183181 return True
184182
185183 def check_global_request(self, kind, msg):
186 self.log.debug('NullServer.check_port_forward_request()'
187 .format(kind, msg))
184 self.log.debug('NullServer.check_port_forward_request()')
188185 return True
189186
190187 def check_channel_direct_tcpip_request(self, chanid, origin, destination):
412409 self.log.info('<<< forward-server received STOP signal')
413410 except socket.error:
414411 self.log.critical('{0} sending RST'.format(info))
415 except Exception as e:
416 # we reach this point usually when schan is None (paramiko bug?)
417 self.log.critical(repr(e))
412 # except Exception as e:
413 # # we reach this point usually when schan is None (paramiko bug?)
414 # self.log.critical(repr(e))
418415 finally:
419416 if schan:
420417 self.log.debug('{0} closing connection...'.format(info))
423420 self.log.debug('{0} connection closed.'.format(info))
424421
425422 def randomize_eport(self):
426 return self.eport + random.randint(1, 999)
423 return random.randint(49152, 65535)
427424
428425 def test_echo_server(self):
429426 with self._test_server(
490487 remote_bind_address=(self.eaddr, self.eport),
491488 logger=self.log,
492489 ssh_config_file=None,
493 allow_agent=False
490 allow_agent=False,
491 host_pkey_directories=[],
494492 )
495493 self.assertEqual(server.ssh_host, self.saddr)
496494 self.assertEqual(server.ssh_port, self.sport)
692690 open_tunnel(**_kwargs)
693691 logged_message = "'{0}' is DEPRECATED use '{1}' instead"\
694692 .format(deprecated_arg,
695 sshtunnel.DEPRECATIONS[deprecated_arg])
693 sshtunnel._DEPRECATIONS[deprecated_arg])
696694 self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
697695 self.assertEqual(logged_message, str(w[-1].message))
698696
712710 open_tunnel(**_kwargs)
713711 logged_message = "'{0}' is DEPRECATED use '{1}' instead"\
714712 .format(deprecated_arg,
715 sshtunnel.DEPRECATIONS[deprecated_arg])
713 sshtunnel._DEPRECATIONS[deprecated_arg])
716714 self.assertTrue(issubclass(w[-1].category, DeprecationWarning))
717715 self.assertEqual(logged_message, str(w[-1].message))
718716
960958 ) as server:
961959 self.assertIsInstance(server.local_bind_addresses, list)
962960 self.assertListEqual(server.local_bind_addresses,
963 [l for l in zip([self.saddr] * 2,
964 server.local_bind_ports)])
961 list(zip([self.saddr] * 2,
962 server.local_bind_ports)))
965963 with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError):
966964 self.log.info(server.local_bind_address)
967965
10081006 '-R', '{0}:{1}'.format(self.eaddr,
10091007 self.eport),
10101008 '-c', '',
1011 '-n'])
1009 '-n'],
1010 host_pkey_directories=[])
10121011 self.stop_echo_and_ssh_server()
10131012
10141013 @unittest.skipIf(sys.version_info < (2, 7),
10831082 s.close
10841083 log = 'send to {0}'.format((self.eaddr, self.eport))
10851084
1086 self.assertTrue(any(log in l for l in
1085 self.assertTrue(any(log in msg for msg in
10871086 self.sshtunnel_log_messages['trace']))
10881087 # set loglevel back to the original value
10891088 logger = sshtunnel.create_logger(logger=self.log,
11241123 remote_bind_address=(self.eaddr, self.eport),
11251124 logger=self.log,
11261125 ssh_config_file=None,
1127 allow_agent=False
1126 allow_agent=False,
1127 host_pkey_directories=[],
11281128 )
11291129 try:
11301130 tunnel.daemon_forward_servers = case
11561156 local_bind_address=('', self.randomize_eport()),
11571157 logger=self.log
11581158 ) as server:
1159
1160 keys = server.get_keys(allow_agent=True)
1159 keys = server.get_keys(logger=self.log)
11611160 self.assertIsInstance(keys, list)
1162 self.assertTrue(any('keys loaded from agent' in l) for l in
1163 self.sshtunnel_log_messages['info'])
1161 self.assertFalse(any('keys loaded from agent' in msg for msg in
1162 self.sshtunnel_log_messages['info']))
11641163
11651164 with self._test_server(
11661165 (self.saddr, self.sport),
11701169 local_bind_address=('', self.randomize_eport()),
11711170 logger=self.log
11721171 ) as server:
1173 keys = server.get_keys()
1172 keys = server.get_keys(logger=self.log, allow_agent=True)
11741173 self.assertIsInstance(keys, list)
1175 self.assertFalse(any('keys loaded from agent' in l for l in
1176 self.sshtunnel_log_messages['info']))
1174 self.assertTrue(any('keys loaded from agent' in msg for msg in
1175 self.sshtunnel_log_messages['info']))
11771176
11781177 tmp_dir = tempfile.mkdtemp()
11791178 shutil.copy(get_test_data_path(PKEY_FILE),
11851184 )
11861185 self.assertIsInstance(keys, list)
11871186 self.assertTrue(
1188 any('1 keys loaded from host directory' in l
1189 for l in self.sshtunnel_log_messages['info'])
1187 any('1 key(s) loaded' in msg
1188 for msg in self.sshtunnel_log_messages['info'])
11901189 )
11911190 shutil.rmtree(tmp_dir)
11921191
13861385 'item',
13871386 kwargs.copy())
13881387
1389 def check_address(self):
1388 def test_check_address(self):
13901389 """ Test that an exception is raised with incorrect bind addresses """
13911390 address_list = [('10.0.0.1', 10000),
13921391 ('10.0.0.1', 10001)]
13931392 if os.name == 'posix': # UNIX sockets supported by the platform
13941393 address_list.append('/tmp/unix-socket')
1394 # UNIX sockets not supported on remote addresses
1395 with self.assertRaises(AssertionError):
1396 sshtunnel.check_addresses(address_list, is_remote=True)
13951397 self.assertIsNone(sshtunnel.check_addresses(address_list))
1396 # UNIX sockets not supported on remote addresses
1397 with self.assertRaises(AssertionError):
1398 sshtunnel.check_addresses(address_list, is_remote=True)
13991398 with self.assertRaises(ValueError):
14001399 sshtunnel.check_address('this is not valid')
14011400 with self.assertRaises(ValueError):