Import upstream version 0.4.0
Debian Janitor
2 years ago
0 | Copyright (c) 2014-2016 Pahaz Blinov | |
0 | Copyright (c) 2014-2019 Pahaz White | |
1 | 1 | |
2 | 2 | Permission is hereby granted, free of charge, to any person obtaining a copy |
3 | 3 | of this software and associated documentation files (the "Software"), to deal |
0 | 0 | # 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 | |
5 | 1 | |
6 | 2 | include LICENSE |
7 | 3 | include *.rst |
10 | 6 | include docs/*.rst |
11 | 7 | include docs/*.txt |
12 | 8 | 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 | |
1 | 1 | Name: sshtunnel |
2 | Version: 0.1.4 | |
2 | Version: 0.4.0 | |
3 | 3 | Summary: Pure python SSH tunnels |
4 | 4 | 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 | |
7 | 7 | License: MIT |
8 | Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip | |
9 | Description-Content-Type: UNKNOWN | |
10 | Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| | |
11 | ||
12 | |pyversions| |license| | |
13 | ||
14 | **Author**: `Pahaz Blinov`_ | |
15 | ||
16 | **Repo**: https://github.com/pahaz/sshtunnel/ | |
17 | ||
18 | Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on | |
19 | Windows. | |
20 | ||
21 | See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py | |
22 | ||
23 | Requirements | |
24 | ------------- | |
25 | ||
26 | * `paramiko`_ | |
27 | ||
28 | Installation | |
29 | ============ | |
30 | ||
31 | `sshtunnel`_ is on PyPI, so simply run: | |
32 | ||
33 | :: | |
34 | ||
35 | pip install sshtunnel | |
36 | ||
37 | or :: | |
38 | ||
39 | easy_install sshtunnel | |
40 | ||
41 | or :: | |
42 | ||
43 | conda install -c conda-forge sshtunnel | |
44 | ||
45 | to have it installed in your environment. | |
46 | ||
47 | For installing from source, clone the | |
48 | `repo <https://github.com/pahaz/sshtunnel>`_ and run:: | |
49 | ||
50 | python setup.py install | |
51 | ||
52 | Testing the package | |
53 | ------------------- | |
54 | ||
55 | In order to run the tests you first need | |
56 | `tox <https://testrun.org/tox/latest/>`_ and run:: | |
57 | ||
58 | python setup.py test | |
59 | ||
60 | Usage scenarios | |
61 | =============== | |
62 | ||
63 | One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the | |
64 | figure below. User may need to connect a port of a remote server (i.e. 8080) | |
65 | where only SSH port (usually port 22) is reachable. :: | |
66 | ||
67 | ---------------------------------------------------------------------- | |
68 | ||
69 | | | |
70 | -------------+ | +----------+ | |
71 | LOCAL | | | REMOTE | :22 SSH | |
72 | CLIENT | <== SSH ========> | SERVER | :8080 web service | |
73 | -------------+ | +----------+ | |
74 | | | |
75 | FIREWALL (only port 22 is open) | |
76 | ||
77 | ---------------------------------------------------------------------- | |
78 | ||
79 | **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. | |
80 | ||
81 | ||
82 | If allowed by the SSH server, it is also possible to reach a private server | |
83 | (from the perspective of ``REMOTE SERVER``) not directly visible from the | |
84 | outside (``LOCAL CLIENT``'s perspective). :: | |
85 | ||
86 | ---------------------------------------------------------------------- | |
87 | ||
88 | | | |
89 | -------------+ | +----------+ +--------- | |
90 | LOCAL | | | REMOTE | | PRIVATE | |
91 | CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER | |
92 | -------------+ | +----------+ +--------- | |
93 | | | |
94 | FIREWALL (only port 443 is open) | |
95 | ||
96 | ---------------------------------------------------------------------- | |
97 | ||
98 | **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. | |
99 | ||
100 | ||
101 | Usage examples | |
102 | ============== | |
103 | ||
104 | API allows either initializing the tunnel and starting it or using a ``with`` | |
105 | context, which will take care of starting **and stopping** the tunnel: | |
106 | ||
107 | Example 1 | |
108 | --------- | |
109 | ||
110 | Code corresponding to **Fig1** above follows, given remote server's address is | |
111 | ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind | |
112 | port. | |
113 | ||
114 | .. code-block:: py | |
115 | ||
116 | from sshtunnel import SSHTunnelForwarder | |
117 | ||
118 | server = SSHTunnelForwarder( | |
119 | 'pahaz.urfuclub.ru', | |
120 | ssh_username="pahaz", | |
121 | ssh_password="secret", | |
122 | remote_bind_address=('127.0.0.1', 8080) | |
123 | ) | |
124 | ||
125 | server.start() | |
126 | ||
127 | print(server.local_bind_port) # show assigned local port | |
128 | # work with `SECRET SERVICE` through `server.local_bind_port`. | |
129 | ||
130 | server.stop() | |
131 | ||
132 | Example 2 | |
133 | --------- | |
134 | ||
135 | Example of a port forwarding to a private server not directly reachable, | |
136 | assuming password protected pkey authentication, remote server's SSH service is | |
137 | listening on port 443 and that port is open in the firewall (**Fig2**): | |
138 | ||
139 | .. code-block:: py | |
140 | ||
141 | import paramiko | |
142 | from sshtunnel import SSHTunnelForwarder | |
143 | ||
144 | with SSHTunnelForwarder( | |
145 | (REMOTE_SERVER_IP, 443), | |
146 | ssh_username="", | |
147 | ssh_pkey="/var/ssh/rsa_key", | |
148 | ssh_private_key_password="secret", | |
149 | remote_bind_address=(PRIVATE_SERVER_IP, 22), | |
150 | local_bind_address=('0.0.0.0', 10022) | |
151 | ) as tunnel: | |
152 | client = paramiko.SSHClient() | |
153 | client.load_system_host_keys() | |
154 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
155 | client.connect('127.0.0.1', 10022) | |
156 | # do some operations with client session | |
157 | client.close() | |
158 | ||
159 | print('FINISH!') | |
160 | ||
161 | Example 3 | |
162 | --------- | |
163 | ||
164 | Example of a port forwarding for the Vagrant MySQL local port: | |
165 | ||
166 | .. code-block:: py | |
167 | ||
168 | from sshtunnel import SSHTunnelForwarder | |
169 | from time import sleep | |
170 | ||
171 | with SSHTunnelForwarder( | |
172 | ('localhost', 2222), | |
173 | ssh_username="vagrant", | |
174 | ssh_password="vagrant", | |
175 | remote_bind_address=('127.0.0.1', 3306) | |
176 | ) as server: | |
177 | ||
178 | print(server.local_bind_port) | |
179 | while True: | |
180 | # press Ctrl-C for stopping | |
181 | sleep(1) | |
182 | ||
183 | print('FINISH!') | |
184 | ||
185 | Or simply using the CLI: | |
186 | ||
187 | .. code-block:: console | |
188 | ||
189 | (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost | |
190 | ||
191 | CLI usage | |
192 | ========= | |
193 | ||
194 | :: | |
195 | ||
196 | $ sshtunnel --help | |
197 | usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R | |
198 | IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] | |
199 | [-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 ...]]] | |
201 | ssh_address | |
202 | ||
203 | Pure python ssh tunnel utils | |
204 | Version 0.1.4 | |
205 | ||
206 | positional arguments: | |
207 | ssh_address SSH server IP address (GW for SSH tunnels) | |
208 | set with "-- ssh_address" if immediately after -R or -L | |
209 | ||
210 | optional arguments: | |
211 | -h, --help show this help message and exit | |
212 | -U SSH_USERNAME, --username SSH_USERNAME | |
213 | SSH server account username | |
214 | -p SSH_PORT, --server_port SSH_PORT | |
215 | SSH server TCP port (default: 22) | |
216 | -P SSH_PASSWORD, --password SSH_PASSWORD | |
217 | SSH server account password | |
218 | -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] | |
219 | Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
220 | Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT | |
221 | If port is omitted, defaults to 22. | |
222 | Example: -R 10.10.10.10: 10.10.10.10:5900 | |
223 | -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] | |
224 | Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
225 | Elements may also be valid UNIX socket domains: | |
226 | /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock | |
227 | Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. | |
228 | By default it will listen in all interfaces (0.0.0.0) and choose a random port. | |
229 | Example: -L :40000 | |
230 | -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY | |
231 | Gateway's host key | |
232 | -K KEY_FILE, --private_key_file KEY_FILE | |
233 | RSA/DSS/ECDSA private key file | |
234 | -S KEY_PASSWORD, --private_key_password KEY_PASSWORD | |
235 | RSA/DSS/ECDSA private key password | |
236 | -t, --threaded Allow concurrent connections to each tunnel | |
237 | -v, --verbose Increase output verbosity (default: ERROR) | |
238 | -V, --version Show version number and quit | |
239 | -x IP:PORT, --proxy IP:PORT | |
240 | IP and port of SSH proxy to destination | |
241 | -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE | |
242 | SSH configuration file, defaults to ~/.ssh/config | |
243 | -z, --compress Request server for compression over SSH transport | |
244 | -n, --noagent Disable looking for keys from an SSH agent | |
245 | -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] | |
246 | List of directories where SSH pkeys (in the format `id_*`) may be found | |
247 | ||
248 | .. _Pahaz Blinov: https://github.com/pahaz | |
249 | .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel | |
250 | .. _paramiko: http://www.paramiko.org/ | |
251 | .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg | |
252 | :target: https://circleci.com/gh/pahaz/sshtunnel | |
253 | .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail | |
254 | :target: https://ci.appveyor.com/project/pahaz/sshtunnel | |
255 | .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest | |
256 | :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest | |
257 | :alt: Documentation Status | |
258 | .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master | |
259 | :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master | |
260 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg | |
261 | .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg | |
262 | :target: `sshtunnel`_ | |
263 | .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg | |
264 | :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE | |
265 | ||
266 | Online documentation | |
267 | ==================== | |
268 | ||
269 | Documentation may be found at `readthedocs`_. | |
270 | ||
271 | .. _readthedocs: https://sshtunnel.readthedocs.org/ | |
272 | ||
273 | CONTRIBUTORS | |
274 | ============ | |
275 | ||
276 | - `Cameron Maske`_ | |
277 | - `Gustavo Machado`_ | |
278 | - `Colin Jermain`_ | |
279 | - `JM Fernández`_ - (big thanks!) | |
280 | - `Lewis Thompson`_ | |
281 | - `Erik Rogers`_ | |
282 | - `Mart Sõmermaa`_ | |
283 | - `Chronial`_ | |
284 | - `Dan Harbin`_ | |
285 | - `Ignacio Peluffo`_ | |
286 | - `Niels Zeilemaker`_ | |
287 | ||
288 | CHANGELOG | |
289 | ========= | |
290 | ||
291 | - v.0.1.4 (`Niels Zeilemaker`_) | |
292 | + Allow loading pkeys from `~/.ssh` | |
293 | ||
294 | - v.0.1.3 (`Ignacio Peluffo`_ and others) | |
295 | + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` | |
296 | + Several bugfixes | |
297 | ||
298 | - v.0.1.2 (`JM Fernández`_) | |
299 | + Fix #77 | |
300 | ||
301 | - v.0.1.1 (`JM Fernández`_) | |
302 | + Fix #72 | |
303 | ||
304 | - v.0.1.0 (`JM Fernández`_) | |
305 | + Add `tunnel_bindings` property | |
306 | + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) | |
307 | (`Pahaz Blinov`_, `JM Fernández`_) | |
308 | + Add TRACE logging level (`JM Fernández`_) | |
309 | + Code and tests refactoring (`JM Fernández`_) | |
310 | + Drop python3.2 support | |
311 | ||
312 | - v.0.0.8 (`JM Fernández`_) | |
313 | + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) | |
314 | + Simplify API (`JM Fernández`_) | |
315 | + Add sphinx-based documentation (`JM Fernández`_) | |
316 | + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) | |
317 | + Add ``compression`` (`JM Fernández`_) | |
318 | + Add ``__str__`` method (`JM Fernández`_) | |
319 | + Add test functions (`JM Fernández`_) | |
320 | + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) | |
321 | + Fix gateway IP unresolvable exception catching (`JM Fernández`_) | |
322 | + Minor fixes (`JM Fernández`_) | |
323 | + Add AppVeyor support (`JM Fernández`_) | |
324 | ||
325 | - v.0.0.7 (`JM Fernández`_) | |
326 | + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) | |
327 | + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) | |
328 | + Allow sending a pkey directly (`#43`_) (`Chronial`_) | |
329 | + Add ``-V`` CLI option to show current version (`JM Fernández`_) | |
330 | + Add coverage (`JM Fernández`_) | |
331 | + Refactoring (`JM Fernández`_) | |
332 | ||
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`_) | |
337 | + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) | |
338 | + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) | |
339 | + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) | |
340 | + 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`_) | |
354 | + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) | |
355 | ||
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* | |
363 | + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* | |
364 | + add CLI interface (`JM Fernández`_) | |
365 | + 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`_) | |
369 | + 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`_) | |
373 | + add ``threaded`` option (`Cameron Maske`_) | |
374 | + 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 | ||
382 | .. _Cameron Maske: https://github.com/cameronmaske | |
383 | .. _Gustavo Machado: https://github.com/gdmachado | |
384 | .. _Colin Jermain: https://github.com/cjermain | |
385 | .. _JM Fernández: https://github.com/fernandezcuesta | |
386 | .. _Lewis Thompson: https://github.com/lewisthompson | |
387 | .. _Erik Rogers: https://github.com/ewrogers | |
388 | .. _Mart Sõmermaa: https://github.com/mrts | |
389 | .. _Chronial: https://github.com/Chronial | |
390 | .. _Dan Harbin: https://github.com/RasterBurn | |
391 | .. _Ignacio Peluffo: https://github.com/ipeluffo | |
392 | .. _Niels Zeilemaker: https://github.com/NielsZeilemaker | |
393 | .. _#13: https://github.com/pahaz/sshtunnel/issues/13 | |
394 | .. _#16: https://github.com/pahaz/sshtunnel/issues/16 | |
395 | .. _#19: https://github.com/pahaz/sshtunnel/issues/19 | |
396 | .. _#21: https://github.com/pahaz/sshtunnel/issues/21 | |
397 | .. _#24: https://github.com/pahaz/sshtunnel/issues/24 | |
398 | .. _#29: https://github.com/pahaz/sshtunnel/issues/29 | |
399 | .. _#31: https://github.com/pahaz/sshtunnel/issues/31 | |
400 | .. _#33: https://github.com/pahaz/sshtunnel/issues/33 | |
401 | .. _#34: https://github.com/pahaz/sshtunnel/issues/34 | |
402 | .. _#36: https://github.com/pahaz/sshtunnel/issues/36 | |
403 | .. _#41: https://github.com/pahaz/sshtunnel/issues/41 | |
404 | .. _#43: https://github.com/pahaz/sshtunnel/issues/43 | |
405 | .. _#46: https://github.com/pahaz/sshtunnel/issues/46 | |
406 | .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 | |
407 | ||
8 | Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.4.0.zip | |
408 | 9 | Keywords: ssh tunnel paramiko proxy tcp-forward |
409 | 10 | Platform: unix |
410 | 11 | Platform: macos |
419 | 20 | Classifier: Programming Language :: Python :: 3.4 |
420 | 21 | Classifier: Programming Language :: Python :: 3.5 |
421 | 22 | Classifier: Programming Language :: Python :: 3.6 |
23 | Classifier: Programming Language :: Python :: 3.7 | |
24 | Classifier: Programming Language :: Python :: 3.8 | |
25 | Description-Content-Type: text/x-rst | |
26 | Provides-Extra: build_sphinx | |
27 | Provides-Extra: dev | |
28 | Provides-Extra: test | |
29 | License-File: LICENSE | |
30 | ||
31 | |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| | |
32 | ||
33 | |pyversions| |license| | |
34 | ||
35 | **Author**: `Pahaz`_ | |
36 | ||
37 | **Repo**: https://github.com/pahaz/sshtunnel/ | |
38 | ||
39 | Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on | |
40 | Windows. | |
41 | ||
42 | See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py | |
43 | ||
44 | Requirements | |
45 | ------------- | |
46 | ||
47 | * `paramiko`_ | |
48 | ||
49 | Installation | |
50 | ============ | |
51 | ||
52 | `sshtunnel`_ is on PyPI, so simply run: | |
53 | ||
54 | :: | |
55 | ||
56 | pip install sshtunnel | |
57 | ||
58 | or :: | |
59 | ||
60 | easy_install sshtunnel | |
61 | ||
62 | or :: | |
63 | ||
64 | conda install -c conda-forge sshtunnel | |
65 | ||
66 | to have it installed in your environment. | |
67 | ||
68 | For installing from source, clone the | |
69 | `repo <https://github.com/pahaz/sshtunnel>`_ and run:: | |
70 | ||
71 | python setup.py install | |
72 | ||
73 | Testing the package | |
74 | ------------------- | |
75 | ||
76 | In order to run the tests you first need | |
77 | `tox <https://testrun.org/tox/latest/>`_ and run:: | |
78 | ||
79 | python setup.py test | |
80 | ||
81 | Usage scenarios | |
82 | =============== | |
83 | ||
84 | One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the | |
85 | figure below. User may need to connect a port of a remote server (i.e. 8080) | |
86 | where only SSH port (usually port 22) is reachable. :: | |
87 | ||
88 | ---------------------------------------------------------------------- | |
89 | ||
90 | | | |
91 | -------------+ | +----------+ | |
92 | LOCAL | | | REMOTE | :22 SSH | |
93 | CLIENT | <== SSH ========> | SERVER | :8080 web service | |
94 | -------------+ | +----------+ | |
95 | | | |
96 | FIREWALL (only port 22 is open) | |
97 | ||
98 | ---------------------------------------------------------------------- | |
99 | ||
100 | **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. | |
101 | ||
102 | ||
103 | If allowed by the SSH server, it is also possible to reach a private server | |
104 | (from the perspective of ``REMOTE SERVER``) not directly visible from the | |
105 | outside (``LOCAL CLIENT``'s perspective). :: | |
106 | ||
107 | ---------------------------------------------------------------------- | |
108 | ||
109 | | | |
110 | -------------+ | +----------+ +--------- | |
111 | LOCAL | | | REMOTE | | PRIVATE | |
112 | CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER | |
113 | -------------+ | +----------+ +--------- | |
114 | | | |
115 | FIREWALL (only port 443 is open) | |
116 | ||
117 | ---------------------------------------------------------------------- | |
118 | ||
119 | **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. | |
120 | ||
121 | ||
122 | Usage examples | |
123 | ============== | |
124 | ||
125 | API allows either initializing the tunnel and starting it or using a ``with`` | |
126 | context, which will take care of starting **and stopping** the tunnel: | |
127 | ||
128 | Example 1 | |
129 | --------- | |
130 | ||
131 | Code corresponding to **Fig1** above follows, given remote server's address is | |
132 | ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind | |
133 | port. | |
134 | ||
135 | .. code-block:: python | |
136 | ||
137 | from sshtunnel import SSHTunnelForwarder | |
138 | ||
139 | server = SSHTunnelForwarder( | |
140 | 'alfa.8iq.dev', | |
141 | ssh_username="pahaz", | |
142 | ssh_password="secret", | |
143 | remote_bind_address=('127.0.0.1', 8080) | |
144 | ) | |
145 | ||
146 | server.start() | |
147 | ||
148 | print(server.local_bind_port) # show assigned local port | |
149 | # work with `SECRET SERVICE` through `server.local_bind_port`. | |
150 | ||
151 | server.stop() | |
152 | ||
153 | Example 2 | |
154 | --------- | |
155 | ||
156 | Example of a port forwarding to a private server not directly reachable, | |
157 | assuming password protected pkey authentication, remote server's SSH service is | |
158 | listening on port 443 and that port is open in the firewall (**Fig2**): | |
159 | ||
160 | .. code-block:: python | |
161 | ||
162 | import paramiko | |
163 | import sshtunnel | |
164 | ||
165 | with sshtunnel.open_tunnel( | |
166 | (REMOTE_SERVER_IP, 443), | |
167 | ssh_username="", | |
168 | ssh_pkey="/var/ssh/rsa_key", | |
169 | ssh_private_key_password="secret", | |
170 | remote_bind_address=(PRIVATE_SERVER_IP, 22), | |
171 | local_bind_address=('0.0.0.0', 10022) | |
172 | ) as tunnel: | |
173 | client = paramiko.SSHClient() | |
174 | client.load_system_host_keys() | |
175 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
176 | client.connect('127.0.0.1', 10022) | |
177 | # do some operations with client session | |
178 | client.close() | |
179 | ||
180 | print('FINISH!') | |
181 | ||
182 | Example 3 | |
183 | --------- | |
184 | ||
185 | Example of a port forwarding for the Vagrant MySQL local port: | |
186 | ||
187 | .. code-block:: python | |
188 | ||
189 | from sshtunnel import open_tunnel | |
190 | from time import sleep | |
191 | ||
192 | with open_tunnel( | |
193 | ('localhost', 2222), | |
194 | ssh_username="vagrant", | |
195 | ssh_password="vagrant", | |
196 | remote_bind_address=('127.0.0.1', 3306) | |
197 | ) as server: | |
198 | ||
199 | print(server.local_bind_port) | |
200 | while True: | |
201 | # press Ctrl-C for stopping | |
202 | sleep(1) | |
203 | ||
204 | print('FINISH!') | |
205 | ||
206 | Or simply using the CLI: | |
207 | ||
208 | .. code-block:: console | |
209 | ||
210 | (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost | |
211 | ||
212 | Example 4 | |
213 | --------- | |
214 | ||
215 | Opening an SSH session jumping over two tunnels. SSH transport and tunnels | |
216 | will be daemonised, which will not wait for the connections to stop at close | |
217 | time. | |
218 | ||
219 | .. code-block:: python | |
220 | ||
221 | import sshtunnel | |
222 | from paramiko import SSHClient | |
223 | ||
224 | ||
225 | with sshtunnel.open_tunnel( | |
226 | ssh_address_or_host=('GW1_ip', 20022), | |
227 | remote_bind_address=('GW2_ip', 22), | |
228 | ) as tunnel1: | |
229 | print('Connection to tunnel1 (GW1_ip:GW1_port) OK...') | |
230 | with sshtunnel.open_tunnel( | |
231 | ssh_address_or_host=('localhost', tunnel1.local_bind_port), | |
232 | remote_bind_address=('target_ip', 22), | |
233 | ssh_username='GW2_user', | |
234 | ssh_password='GW2_pwd', | |
235 | ) as tunnel2: | |
236 | print('Connection to tunnel2 (GW2_ip:GW2_port) OK...') | |
237 | with SSHClient() as ssh: | |
238 | ssh.connect('localhost', | |
239 | port=tunnel2.local_bind_port, | |
240 | username='target_user', | |
241 | password='target_pwd', | |
242 | ) | |
243 | ssh.exec_command(...) | |
244 | ||
245 | ||
246 | CLI usage | |
247 | ========= | |
248 | ||
249 | :: | |
250 | ||
251 | $ sshtunnel --help | |
252 | usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R | |
253 | IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] | |
254 | [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v] | |
255 | [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] | |
256 | [-d [FOLDER [FOLDER ...]]] | |
257 | ssh_address | |
258 | ||
259 | Pure python ssh tunnel utils | |
260 | Version 0.4.0 | |
261 | ||
262 | positional arguments: | |
263 | ssh_address SSH server IP address (GW for SSH tunnels) | |
264 | set with "-- ssh_address" if immediately after -R or -L | |
265 | ||
266 | optional arguments: | |
267 | -h, --help show this help message and exit | |
268 | -U SSH_USERNAME, --username SSH_USERNAME | |
269 | SSH server account username | |
270 | -p SSH_PORT, --server_port SSH_PORT | |
271 | SSH server TCP port (default: 22) | |
272 | -P SSH_PASSWORD, --password SSH_PASSWORD | |
273 | SSH server account password | |
274 | -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] | |
275 | Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
276 | Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT | |
277 | If port is omitted, defaults to 22. | |
278 | Example: -R 10.10.10.10: 10.10.10.10:5900 | |
279 | -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] | |
280 | Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
281 | Elements may also be valid UNIX socket domains: | |
282 | /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock | |
283 | Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. | |
284 | By default it will listen in all interfaces (0.0.0.0) and choose a random port. | |
285 | Example: -L :40000 | |
286 | -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY | |
287 | Gateway's host key | |
288 | -K KEY_FILE, --private_key_file KEY_FILE | |
289 | RSA/DSS/ECDSA private key file | |
290 | -S KEY_PASSWORD, --private_key_password KEY_PASSWORD | |
291 | RSA/DSS/ECDSA private key password | |
292 | -t, --threaded Allow concurrent connections to each tunnel | |
293 | -v, --verbose Increase output verbosity (default: ERROR) | |
294 | -V, --version Show version number and quit | |
295 | -x IP:PORT, --proxy IP:PORT | |
296 | IP and port of SSH proxy to destination | |
297 | -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE | |
298 | SSH configuration file, defaults to ~/.ssh/config | |
299 | -z, --compress Request server for compression over SSH transport | |
300 | -n, --noagent Disable looking for keys from an SSH agent | |
301 | -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] | |
302 | List of directories where SSH pkeys (in the format `id_*`) may be found | |
303 | ||
304 | .. _Pahaz: https://github.com/pahaz | |
305 | .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel | |
306 | .. _paramiko: http://www.paramiko.org/ | |
307 | .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg | |
308 | :target: https://circleci.com/gh/pahaz/sshtunnel | |
309 | .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail | |
310 | :target: https://ci.appveyor.com/project/pahaz/sshtunnel | |
311 | .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest | |
312 | :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest | |
313 | :alt: Documentation Status | |
314 | .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master | |
315 | :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master | |
316 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg | |
317 | .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg | |
318 | :target: `sshtunnel`_ | |
319 | .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg | |
320 | :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE | |
321 | ||
322 | Online documentation | |
323 | ==================== | |
324 | ||
325 | Documentation may be found at `readthedocs`_. | |
326 | ||
327 | .. _readthedocs: https://sshtunnel.readthedocs.org/ | |
328 | ||
329 | CONTRIBUTORS | |
330 | ============ | |
331 | ||
332 | - `Cameron Maske`_ | |
333 | - `Gustavo Machado`_ | |
334 | - `Colin Jermain`_ | |
335 | - `JM Fernández`_ - (big thanks!) | |
336 | - `Lewis Thompson`_ | |
337 | - `Erik Rogers`_ | |
338 | - `Mart Sõmermaa`_ | |
339 | - `Chronial`_ | |
340 | - `Dan Harbin`_ | |
341 | - `Ignacio Peluffo`_ | |
342 | - `Niels Zeilemaker`_ | |
343 | - `Georgy Rylov`_ | |
344 | - `Eddie Chiang`_ | |
345 | - `kkrasovskii`_ | |
346 | ||
347 | CHANGELOG | |
348 | ========= | |
349 | ||
350 | - v.0.4.0 (`Pahaz`_) | |
351 | + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_) | |
352 | + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_) | |
353 | + Add docker based end to end hangs tests (`#219`_) | |
354 | ||
355 | - v.0.3.2 (`Pahaz`_, `JM Fernández`_) | |
356 | + Fix host key directory detection | |
357 | + Unify default ssh config folder to `~/.ssh` | |
358 | ||
359 | - v.0.3.1 (`Pahaz`_) | |
360 | + Increase open connection timeout to 10 secods | |
361 | ||
362 | - v.0.3.0 (`Pahaz`_) | |
363 | + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible) | |
364 | + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible) | |
365 | + Set transport keepalive to 5 second by default (disabled for version < 0.3.0) | |
366 | + Set default transport timeout to 0.1 | |
367 | + Deprecate and remove `block_on_close` option | |
368 | + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_) | |
369 | ||
370 | - v.0.2.2 (`Pahaz`_) | |
371 | + Add `.stop(force=True)` for force close active connections (`#201`_) | |
372 | ||
373 | - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_) | |
374 | + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_) | |
375 | ||
376 | - v.0.2.0 (`Georgy Rylov`_) | |
377 | + 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. | |
378 | ||
379 | - v.0.1.5 (`JM Fernández`_) | |
380 | + Introduce `block_on_close` attribute | |
381 | ||
382 | - v.0.1.4 (`Niels Zeilemaker`_) | |
383 | + Allow loading pkeys from `~/.ssh` | |
384 | ||
385 | - v.0.1.3 (`Ignacio Peluffo`_ and others) | |
386 | + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` | |
387 | + Several bugfixes | |
388 | ||
389 | - v.0.1.2 (`JM Fernández`_) | |
390 | + Fix #77 | |
391 | ||
392 | - v.0.1.1 (`JM Fernández`_) | |
393 | + Fix #72 | |
394 | ||
395 | - v.0.1.0 (`JM Fernández`_) | |
396 | + Add `tunnel_bindings` property | |
397 | + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) | |
398 | (`Pahaz`_, `JM Fernández`_) | |
399 | + Add TRACE logging level (`JM Fernández`_) | |
400 | + Code and tests refactoring (`JM Fernández`_) | |
401 | + Drop python3.2 support | |
402 | ||
403 | - v.0.0.8 (`JM Fernández`_) | |
404 | + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) | |
405 | + Simplify API (`JM Fernández`_) | |
406 | + Add sphinx-based documentation (`JM Fernández`_) | |
407 | + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) | |
408 | + Add ``compression`` (`JM Fernández`_) | |
409 | + Add ``__str__`` method (`JM Fernández`_) | |
410 | + Add test functions (`JM Fernández`_) | |
411 | + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) | |
412 | + Fix gateway IP unresolvable exception catching (`JM Fernández`_) | |
413 | + Minor fixes (`JM Fernández`_) | |
414 | + Add AppVeyor support (`JM Fernández`_) | |
415 | ||
416 | - v.0.0.7 (`JM Fernández`_) | |
417 | + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) | |
418 | + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) | |
419 | + Allow sending a pkey directly (`#43`_) (`Chronial`_) | |
420 | + Add ``-V`` CLI option to show current version (`JM Fernández`_) | |
421 | + Add coverage (`JM Fernández`_) | |
422 | + Refactoring (`JM Fernández`_) | |
423 | ||
424 | - v.0.0.6 (`Pahaz`_) | |
425 | + add ``-S`` CLI options for ssh private key password support (`Pahaz`_) | |
426 | ||
427 | - v.0.0.5 (`Pahaz`_) | |
428 | + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) | |
429 | + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) | |
430 | + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) | |
431 | + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_) | |
432 | + add tests (`Pahaz`_) | |
433 | + add CI integration (`Pahaz`_) | |
434 | + normal packaging (`Pahaz`_) | |
435 | + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior] | |
436 | + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior] | |
437 | ||
438 | - v.0.0.4.4 (`Pahaz`_) | |
439 | + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_) | |
440 | ||
441 | - v.0.0.4.3 (`Pahaz`_) | |
442 | + fix default port issue `#19`_ (`Pahaz`_) | |
443 | ||
444 | - v.0.0.4.2 (`Pahaz`_) | |
445 | + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) | |
446 | ||
447 | - v.0.0.4.1 (`Pahaz`_) | |
448 | + fix CLI issues `#13`_ (`Pahaz`_) | |
449 | ||
450 | - v.0.0.4 (`Pahaz`_) | |
451 | + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible* | |
452 | + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible* | |
453 | + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible* | |
454 | + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* | |
455 | + add CLI interface (`JM Fernández`_) | |
456 | + support opening several tunnels at once (`JM Fernández`_) | |
457 | + improve stability and readability (`JM Fernández`_, `Pahaz`_) | |
458 | + improve logging (`JM Fernández`_, `Pahaz`_) | |
459 | + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_) | |
460 | + add ``ssh_config_file`` argument support (`JM Fernández`_) | |
461 | + add Python 3 support (`JM Fernández`_, `Pahaz`_) | |
462 | ||
463 | - v.0.0.3 (`Pahaz`_) | |
464 | + add ``threaded`` option (`Cameron Maske`_) | |
465 | + fix exception error message, correctly printing destination address (`Gustavo Machado`_) | |
466 | + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_) | |
467 | ||
468 | - v.0.0.1 (`Pahaz`_) | |
469 | + ``SSHTunnelForwarder`` class (`Pahaz`_) | |
470 | + ``open`` function (`Pahaz`_) | |
471 | ||
472 | ||
473 | .. _Pahaz: https://github.com/pahaz | |
474 | .. _Cameron Maske: https://github.com/cameronmaske | |
475 | .. _Gustavo Machado: https://github.com/gdmachado | |
476 | .. _Colin Jermain: https://github.com/cjermain | |
477 | .. _JM Fernández: https://github.com/fernandezcuesta | |
478 | .. _Lewis Thompson: https://github.com/lewisthompson | |
479 | .. _Erik Rogers: https://github.com/ewrogers | |
480 | .. _Mart Sõmermaa: https://github.com/mrts | |
481 | .. _Chronial: https://github.com/Chronial | |
482 | .. _Dan Harbin: https://github.com/RasterBurn | |
483 | .. _Ignacio Peluffo: https://github.com/ipeluffo | |
484 | .. _Niels Zeilemaker: https://github.com/NielsZeilemaker | |
485 | .. _Georgy Rylov: https://github.com/g0djan | |
486 | .. _Eddie Chiang: https://github.com/eddie-chiang | |
487 | .. _kkrasovskii: https://github.com/kkrasovskii | |
488 | .. _#13: https://github.com/pahaz/sshtunnel/issues/13 | |
489 | .. _#16: https://github.com/pahaz/sshtunnel/issues/16 | |
490 | .. _#19: https://github.com/pahaz/sshtunnel/issues/19 | |
491 | .. _#21: https://github.com/pahaz/sshtunnel/issues/21 | |
492 | .. _#24: https://github.com/pahaz/sshtunnel/issues/24 | |
493 | .. _#29: https://github.com/pahaz/sshtunnel/issues/29 | |
494 | .. _#31: https://github.com/pahaz/sshtunnel/issues/31 | |
495 | .. _#33: https://github.com/pahaz/sshtunnel/issues/33 | |
496 | .. _#34: https://github.com/pahaz/sshtunnel/issues/34 | |
497 | .. _#36: https://github.com/pahaz/sshtunnel/issues/36 | |
498 | .. _#41: https://github.com/pahaz/sshtunnel/issues/41 | |
499 | .. _#43: https://github.com/pahaz/sshtunnel/issues/43 | |
500 | .. _#46: https://github.com/pahaz/sshtunnel/issues/46 | |
501 | .. _#170: https://github.com/pahaz/sshtunnel/issues/170 | |
502 | .. _#201: https://github.com/pahaz/sshtunnel/issues/201 | |
503 | .. _#162: https://github.com/pahaz/sshtunnel/issues/162 | |
504 | .. _#173: https://github.com/pahaz/sshtunnel/issues/173 | |
505 | .. _#201: https://github.com/pahaz/sshtunnel/issues/201 | |
506 | .. _#211: https://github.com/pahaz/sshtunnel/issues/211 | |
507 | .. _#219: https://github.com/pahaz/sshtunnel/issues/219 | |
508 | .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 | |
509 | ||
510 |
1 | 1 | |
2 | 2 | |pyversions| |license| |
3 | 3 | |
4 | **Author**: `Pahaz Blinov`_ | |
4 | **Author**: `Pahaz`_ | |
5 | 5 | |
6 | 6 | **Repo**: https://github.com/pahaz/sshtunnel/ |
7 | 7 | |
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 | |
9 | 9 | Windows. |
10 | 10 | |
11 | 11 | See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py |
101 | 101 | ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind |
102 | 102 | port. |
103 | 103 | |
104 | .. code-block:: py | |
104 | .. code-block:: python | |
105 | 105 | |
106 | 106 | from sshtunnel import SSHTunnelForwarder |
107 | 107 | |
108 | 108 | server = SSHTunnelForwarder( |
109 | 'pahaz.urfuclub.ru', | |
109 | 'alfa.8iq.dev', | |
110 | 110 | ssh_username="pahaz", |
111 | 111 | ssh_password="secret", |
112 | 112 | remote_bind_address=('127.0.0.1', 8080) |
126 | 126 | assuming password protected pkey authentication, remote server's SSH service is |
127 | 127 | listening on port 443 and that port is open in the firewall (**Fig2**): |
128 | 128 | |
129 | .. code-block:: py | |
129 | .. code-block:: python | |
130 | 130 | |
131 | 131 | import paramiko |
132 | from sshtunnel import SSHTunnelForwarder | |
133 | ||
134 | with SSHTunnelForwarder( | |
132 | import sshtunnel | |
133 | ||
134 | with sshtunnel.open_tunnel( | |
135 | 135 | (REMOTE_SERVER_IP, 443), |
136 | 136 | ssh_username="", |
137 | 137 | ssh_pkey="/var/ssh/rsa_key", |
153 | 153 | |
154 | 154 | Example of a port forwarding for the Vagrant MySQL local port: |
155 | 155 | |
156 | .. code-block:: py | |
157 | ||
158 | from sshtunnel import SSHTunnelForwarder | |
156 | .. code-block:: python | |
157 | ||
158 | from sshtunnel import open_tunnel | |
159 | 159 | from time import sleep |
160 | 160 | |
161 | with SSHTunnelForwarder( | |
161 | with open_tunnel( | |
162 | 162 | ('localhost', 2222), |
163 | 163 | ssh_username="vagrant", |
164 | 164 | ssh_password="vagrant", |
178 | 178 | |
179 | 179 | (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost |
180 | 180 | |
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 | ||
181 | 215 | CLI usage |
182 | 216 | ========= |
183 | 217 | |
187 | 221 | usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R |
188 | 222 | IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] |
189 | 223 | [-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 ...]]] | |
191 | 226 | ssh_address |
192 | 227 | |
193 | 228 | Pure python ssh tunnel utils |
194 | Version 0.1.4 | |
229 | Version 0.4.0 | |
195 | 230 | |
196 | 231 | positional arguments: |
197 | 232 | ssh_address SSH server IP address (GW for SSH tunnels) |
235 | 270 | -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] |
236 | 271 | List of directories where SSH pkeys (in the format `id_*`) may be found |
237 | 272 | |
238 | .. _Pahaz Blinov: https://github.com/pahaz | |
273 | .. _Pahaz: https://github.com/pahaz | |
239 | 274 | .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel |
240 | 275 | .. _paramiko: http://www.paramiko.org/ |
241 | 276 | .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg |
11 | 11 | - `Dan Harbin`_ |
12 | 12 | - `Ignacio Peluffo`_ |
13 | 13 | - `Niels Zeilemaker`_ |
14 | - `Georgy Rylov`_ | |
15 | - `Eddie Chiang`_ | |
16 | - `kkrasovskii`_ | |
14 | 17 | |
15 | 18 | CHANGELOG |
16 | 19 | ========= |
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 | |
17 | 52 | |
18 | 53 | - v.0.1.4 (`Niels Zeilemaker`_) |
19 | 54 | + Allow loading pkeys from `~/.ssh` |
31 | 66 | - v.0.1.0 (`JM Fernández`_) |
32 | 67 | + Add `tunnel_bindings` property |
33 | 68 | + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) |
34 | (`Pahaz Blinov`_, `JM Fernández`_) | |
69 | (`Pahaz`_, `JM Fernández`_) | |
35 | 70 | + Add TRACE logging level (`JM Fernández`_) |
36 | 71 | + Code and tests refactoring (`JM Fernández`_) |
37 | 72 | + Drop python3.2 support |
57 | 92 | + Add coverage (`JM Fernández`_) |
58 | 93 | + Refactoring (`JM Fernández`_) |
59 | 94 | |
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`_) | |
62 | 97 | |
63 | - v.0.0.5 (`Pahaz Blinov`_) | |
98 | - v.0.0.5 (`Pahaz`_) | |
64 | 99 | + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) |
65 | 100 | + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) |
66 | 101 | + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) |
67 | 102 | + 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] | |
73 | 108 | |
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`_) | |
76 | 111 | |
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`_) | |
79 | 114 | |
80 | - v.0.0.4.2 (`Pahaz Blinov`_) | |
115 | - v.0.0.4.2 (`Pahaz`_) | |
81 | 116 | + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) |
82 | 117 | |
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`_) | |
85 | 120 | |
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* | |
90 | 125 | + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* |
91 | 126 | + add CLI interface (`JM Fernández`_) |
92 | 127 | + 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`_) | |
96 | 131 | + 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`_) | |
98 | 133 | |
99 | - v.0.0.3 (`Pahaz Blinov`_) | |
134 | - v.0.0.3 (`Pahaz`_) | |
100 | 135 | + add ``threaded`` option (`Cameron Maske`_) |
101 | 136 | + 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`_) | |
103 | 138 | |
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`_) | |
107 | 142 | |
108 | 143 | |
144 | .. _Pahaz: https://github.com/pahaz | |
109 | 145 | .. _Cameron Maske: https://github.com/cameronmaske |
110 | 146 | .. _Gustavo Machado: https://github.com/gdmachado |
111 | 147 | .. _Colin Jermain: https://github.com/cjermain |
117 | 153 | .. _Dan Harbin: https://github.com/RasterBurn |
118 | 154 | .. _Ignacio Peluffo: https://github.com/ipeluffo |
119 | 155 | .. _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 | |
120 | 159 | .. _#13: https://github.com/pahaz/sshtunnel/issues/13 |
121 | 160 | .. _#16: https://github.com/pahaz/sshtunnel/issues/16 |
122 | 161 | .. _#19: https://github.com/pahaz/sshtunnel/issues/19 |
130 | 169 | .. _#41: https://github.com/pahaz/sshtunnel/issues/41 |
131 | 170 | .. _#43: https://github.com/pahaz/sshtunnel/issues/43 |
132 | 171 | .. _#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 | |
133 | 179 | .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 |
14 | 14 | |
15 | 15 | import sys |
16 | 16 | import os |
17 | ||
18 | import sshtunnel | |
17 | 19 | |
18 | 20 | # Patch to disable warning on non-local image |
19 | 21 | import sphinx.environment |
60 | 62 | |
61 | 63 | # General information about the project. |
62 | 64 | 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' | |
65 | 67 | |
66 | 68 | # The version info for the project you're documenting, acts as replacement for |
67 | 69 | # |version| and |release|, also used in various other places throughout the |
68 | 70 | # built documents. |
69 | 71 | # |
70 | 72 | # The short X.Y version. |
71 | version = '0.0.8' | |
73 | version = sshtunnel.__version__ | |
72 | 74 | # The full version, including alpha/beta/rc tags. |
73 | release = '0.0.8' | |
75 | release = sshtunnel.__version__ | |
74 | 76 | |
75 | 77 | # The language for content autogenerated by Sphinx. Refer to documentation |
76 | 78 | # for a list of supported languages. |
6 | 6 | |
7 | 7 | API |
8 | 8 | === |
9 | ||
10 | .. toctree:: | |
11 | :maxdepth: 3 | |
12 | 9 | |
13 | 10 | .. automodule:: sshtunnel |
14 | 11 | :members: |
20 | 17 | License |
21 | 18 | ======= |
22 | 19 | |
23 | .. include:: ../LICENSE⏎ | |
20 | .. include:: ../LICENSE |
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__"⏎ |
5 | 5 | """ |
6 | 6 | |
7 | 7 | import re |
8 | import sys | |
9 | 8 | from os import path |
10 | 9 | from codecs import open # To use a consistent encoding |
11 | ||
12 | 10 | from setuptools import setup # Always prefer setuptools over distutils |
13 | from setuptools.command.test import test as TestCommand | |
14 | 11 | |
15 | 12 | here = path.abspath(path.dirname(__file__)) |
16 | 13 | name = 'sshtunnel' |
31 | 28 | version = eval(re.search("__version__[ ]*=[ ]*([^\r\n]+)", data).group(1)) |
32 | 29 | |
33 | 30 | |
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 | ||
49 | 31 | setup( |
50 | 32 | name=name, |
51 | 33 | |
56 | 38 | |
57 | 39 | description=description, |
58 | 40 | long_description='\n'.join((long_description, documentation, changelog)), |
41 | long_description_content_type='text/x-rst', | |
59 | 42 | |
60 | 43 | # The project's main homepage. |
61 | 44 | url=url, |
62 | 45 | download_url=ppa + version + '.zip', # noqa |
63 | 46 | |
64 | 47 | # Author details |
65 | author='Pahaz Blinov', | |
66 | author_email='pahaz.blinov@gmail.com', | |
48 | author='Pahaz White', | |
49 | author_email='pahaz.white@gmail.com', | |
67 | 50 | |
68 | 51 | # Choose your license |
69 | 52 | license='MIT', |
91 | 74 | 'Programming Language :: Python :: 3.4', |
92 | 75 | 'Programming Language :: Python :: 3.5', |
93 | 76 | 'Programming Language :: Python :: 3.6', |
77 | 'Programming Language :: Python :: 3.7', | |
78 | 'Programming Language :: Python :: 3.8', | |
94 | 79 | ], |
95 | 80 | |
96 | 81 | platforms=['unix', 'macos', 'windows'], |
111 | 96 | # requirements files see: |
112 | 97 | # https://packaging.python.org/en/latest/requirements.html |
113 | 98 | install_requires=[ |
114 | 'paramiko>=1.15.2', | |
99 | 'paramiko>=2.7.2', | |
115 | 100 | ], |
116 | 101 | |
117 | 102 | # List additional groups of dependencies here (e.g. development |
118 | 103 | # dependencies). You can install these using the following syntax, |
119 | 104 | # for example: |
120 | 105 | # $ pip install -e .[dev,test] |
106 | tests_require=[ | |
107 | 'tox>=1.8.1', | |
108 | ], | |
121 | 109 | extras_require={ |
122 | 110 | 'dev': ['check-manifest'], |
123 | 111 | 'test': [ |
145 | 133 | ] |
146 | 134 | }, |
147 | 135 | |
148 | # Integrate tox with setuptools | |
149 | cmdclass={'test': Tox}, | |
150 | 136 | ) |
0 | Metadata-Version: 1.1 | |
0 | Metadata-Version: 2.1 | |
1 | 1 | Name: sshtunnel |
2 | Version: 0.1.4 | |
2 | Version: 0.4.0 | |
3 | 3 | Summary: Pure python SSH tunnels |
4 | 4 | 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 | |
7 | 7 | License: MIT |
8 | Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.1.4.zip | |
9 | Description-Content-Type: UNKNOWN | |
10 | Description: |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| | |
11 | ||
12 | |pyversions| |license| | |
13 | ||
14 | **Author**: `Pahaz Blinov`_ | |
15 | ||
16 | **Repo**: https://github.com/pahaz/sshtunnel/ | |
17 | ||
18 | Inspired by https://github.com/jmagnusson/bgtunnel, but it doesn't work on | |
19 | Windows. | |
20 | ||
21 | See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py | |
22 | ||
23 | Requirements | |
24 | ------------- | |
25 | ||
26 | * `paramiko`_ | |
27 | ||
28 | Installation | |
29 | ============ | |
30 | ||
31 | `sshtunnel`_ is on PyPI, so simply run: | |
32 | ||
33 | :: | |
34 | ||
35 | pip install sshtunnel | |
36 | ||
37 | or :: | |
38 | ||
39 | easy_install sshtunnel | |
40 | ||
41 | or :: | |
42 | ||
43 | conda install -c conda-forge sshtunnel | |
44 | ||
45 | to have it installed in your environment. | |
46 | ||
47 | For installing from source, clone the | |
48 | `repo <https://github.com/pahaz/sshtunnel>`_ and run:: | |
49 | ||
50 | python setup.py install | |
51 | ||
52 | Testing the package | |
53 | ------------------- | |
54 | ||
55 | In order to run the tests you first need | |
56 | `tox <https://testrun.org/tox/latest/>`_ and run:: | |
57 | ||
58 | python setup.py test | |
59 | ||
60 | Usage scenarios | |
61 | =============== | |
62 | ||
63 | One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the | |
64 | figure below. User may need to connect a port of a remote server (i.e. 8080) | |
65 | where only SSH port (usually port 22) is reachable. :: | |
66 | ||
67 | ---------------------------------------------------------------------- | |
68 | ||
69 | | | |
70 | -------------+ | +----------+ | |
71 | LOCAL | | | REMOTE | :22 SSH | |
72 | CLIENT | <== SSH ========> | SERVER | :8080 web service | |
73 | -------------+ | +----------+ | |
74 | | | |
75 | FIREWALL (only port 22 is open) | |
76 | ||
77 | ---------------------------------------------------------------------- | |
78 | ||
79 | **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. | |
80 | ||
81 | ||
82 | If allowed by the SSH server, it is also possible to reach a private server | |
83 | (from the perspective of ``REMOTE SERVER``) not directly visible from the | |
84 | outside (``LOCAL CLIENT``'s perspective). :: | |
85 | ||
86 | ---------------------------------------------------------------------- | |
87 | ||
88 | | | |
89 | -------------+ | +----------+ +--------- | |
90 | LOCAL | | | REMOTE | | PRIVATE | |
91 | CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER | |
92 | -------------+ | +----------+ +--------- | |
93 | | | |
94 | FIREWALL (only port 443 is open) | |
95 | ||
96 | ---------------------------------------------------------------------- | |
97 | ||
98 | **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. | |
99 | ||
100 | ||
101 | Usage examples | |
102 | ============== | |
103 | ||
104 | API allows either initializing the tunnel and starting it or using a ``with`` | |
105 | context, which will take care of starting **and stopping** the tunnel: | |
106 | ||
107 | Example 1 | |
108 | --------- | |
109 | ||
110 | Code corresponding to **Fig1** above follows, given remote server's address is | |
111 | ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind | |
112 | port. | |
113 | ||
114 | .. code-block:: py | |
115 | ||
116 | from sshtunnel import SSHTunnelForwarder | |
117 | ||
118 | server = SSHTunnelForwarder( | |
119 | 'pahaz.urfuclub.ru', | |
120 | ssh_username="pahaz", | |
121 | ssh_password="secret", | |
122 | remote_bind_address=('127.0.0.1', 8080) | |
123 | ) | |
124 | ||
125 | server.start() | |
126 | ||
127 | print(server.local_bind_port) # show assigned local port | |
128 | # work with `SECRET SERVICE` through `server.local_bind_port`. | |
129 | ||
130 | server.stop() | |
131 | ||
132 | Example 2 | |
133 | --------- | |
134 | ||
135 | Example of a port forwarding to a private server not directly reachable, | |
136 | assuming password protected pkey authentication, remote server's SSH service is | |
137 | listening on port 443 and that port is open in the firewall (**Fig2**): | |
138 | ||
139 | .. code-block:: py | |
140 | ||
141 | import paramiko | |
142 | from sshtunnel import SSHTunnelForwarder | |
143 | ||
144 | with SSHTunnelForwarder( | |
145 | (REMOTE_SERVER_IP, 443), | |
146 | ssh_username="", | |
147 | ssh_pkey="/var/ssh/rsa_key", | |
148 | ssh_private_key_password="secret", | |
149 | remote_bind_address=(PRIVATE_SERVER_IP, 22), | |
150 | local_bind_address=('0.0.0.0', 10022) | |
151 | ) as tunnel: | |
152 | client = paramiko.SSHClient() | |
153 | client.load_system_host_keys() | |
154 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
155 | client.connect('127.0.0.1', 10022) | |
156 | # do some operations with client session | |
157 | client.close() | |
158 | ||
159 | print('FINISH!') | |
160 | ||
161 | Example 3 | |
162 | --------- | |
163 | ||
164 | Example of a port forwarding for the Vagrant MySQL local port: | |
165 | ||
166 | .. code-block:: py | |
167 | ||
168 | from sshtunnel import SSHTunnelForwarder | |
169 | from time import sleep | |
170 | ||
171 | with SSHTunnelForwarder( | |
172 | ('localhost', 2222), | |
173 | ssh_username="vagrant", | |
174 | ssh_password="vagrant", | |
175 | remote_bind_address=('127.0.0.1', 3306) | |
176 | ) as server: | |
177 | ||
178 | print(server.local_bind_port) | |
179 | while True: | |
180 | # press Ctrl-C for stopping | |
181 | sleep(1) | |
182 | ||
183 | print('FINISH!') | |
184 | ||
185 | Or simply using the CLI: | |
186 | ||
187 | .. code-block:: console | |
188 | ||
189 | (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost | |
190 | ||
191 | CLI usage | |
192 | ========= | |
193 | ||
194 | :: | |
195 | ||
196 | $ sshtunnel --help | |
197 | usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R | |
198 | IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] | |
199 | [-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 ...]]] | |
201 | ssh_address | |
202 | ||
203 | Pure python ssh tunnel utils | |
204 | Version 0.1.4 | |
205 | ||
206 | positional arguments: | |
207 | ssh_address SSH server IP address (GW for SSH tunnels) | |
208 | set with "-- ssh_address" if immediately after -R or -L | |
209 | ||
210 | optional arguments: | |
211 | -h, --help show this help message and exit | |
212 | -U SSH_USERNAME, --username SSH_USERNAME | |
213 | SSH server account username | |
214 | -p SSH_PORT, --server_port SSH_PORT | |
215 | SSH server TCP port (default: 22) | |
216 | -P SSH_PASSWORD, --password SSH_PASSWORD | |
217 | SSH server account password | |
218 | -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] | |
219 | Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
220 | Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT | |
221 | If port is omitted, defaults to 22. | |
222 | Example: -R 10.10.10.10: 10.10.10.10:5900 | |
223 | -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] | |
224 | Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
225 | Elements may also be valid UNIX socket domains: | |
226 | /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock | |
227 | Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. | |
228 | By default it will listen in all interfaces (0.0.0.0) and choose a random port. | |
229 | Example: -L :40000 | |
230 | -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY | |
231 | Gateway's host key | |
232 | -K KEY_FILE, --private_key_file KEY_FILE | |
233 | RSA/DSS/ECDSA private key file | |
234 | -S KEY_PASSWORD, --private_key_password KEY_PASSWORD | |
235 | RSA/DSS/ECDSA private key password | |
236 | -t, --threaded Allow concurrent connections to each tunnel | |
237 | -v, --verbose Increase output verbosity (default: ERROR) | |
238 | -V, --version Show version number and quit | |
239 | -x IP:PORT, --proxy IP:PORT | |
240 | IP and port of SSH proxy to destination | |
241 | -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE | |
242 | SSH configuration file, defaults to ~/.ssh/config | |
243 | -z, --compress Request server for compression over SSH transport | |
244 | -n, --noagent Disable looking for keys from an SSH agent | |
245 | -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] | |
246 | List of directories where SSH pkeys (in the format `id_*`) may be found | |
247 | ||
248 | .. _Pahaz Blinov: https://github.com/pahaz | |
249 | .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel | |
250 | .. _paramiko: http://www.paramiko.org/ | |
251 | .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg | |
252 | :target: https://circleci.com/gh/pahaz/sshtunnel | |
253 | .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail | |
254 | :target: https://ci.appveyor.com/project/pahaz/sshtunnel | |
255 | .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest | |
256 | :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest | |
257 | :alt: Documentation Status | |
258 | .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master | |
259 | :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master | |
260 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg | |
261 | .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg | |
262 | :target: `sshtunnel`_ | |
263 | .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg | |
264 | :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE | |
265 | ||
266 | Online documentation | |
267 | ==================== | |
268 | ||
269 | Documentation may be found at `readthedocs`_. | |
270 | ||
271 | .. _readthedocs: https://sshtunnel.readthedocs.org/ | |
272 | ||
273 | CONTRIBUTORS | |
274 | ============ | |
275 | ||
276 | - `Cameron Maske`_ | |
277 | - `Gustavo Machado`_ | |
278 | - `Colin Jermain`_ | |
279 | - `JM Fernández`_ - (big thanks!) | |
280 | - `Lewis Thompson`_ | |
281 | - `Erik Rogers`_ | |
282 | - `Mart Sõmermaa`_ | |
283 | - `Chronial`_ | |
284 | - `Dan Harbin`_ | |
285 | - `Ignacio Peluffo`_ | |
286 | - `Niels Zeilemaker`_ | |
287 | ||
288 | CHANGELOG | |
289 | ========= | |
290 | ||
291 | - v.0.1.4 (`Niels Zeilemaker`_) | |
292 | + Allow loading pkeys from `~/.ssh` | |
293 | ||
294 | - v.0.1.3 (`Ignacio Peluffo`_ and others) | |
295 | + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` | |
296 | + Several bugfixes | |
297 | ||
298 | - v.0.1.2 (`JM Fernández`_) | |
299 | + Fix #77 | |
300 | ||
301 | - v.0.1.1 (`JM Fernández`_) | |
302 | + Fix #72 | |
303 | ||
304 | - v.0.1.0 (`JM Fernández`_) | |
305 | + Add `tunnel_bindings` property | |
306 | + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) | |
307 | (`Pahaz Blinov`_, `JM Fernández`_) | |
308 | + Add TRACE logging level (`JM Fernández`_) | |
309 | + Code and tests refactoring (`JM Fernández`_) | |
310 | + Drop python3.2 support | |
311 | ||
312 | - v.0.0.8 (`JM Fernández`_) | |
313 | + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) | |
314 | + Simplify API (`JM Fernández`_) | |
315 | + Add sphinx-based documentation (`JM Fernández`_) | |
316 | + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) | |
317 | + Add ``compression`` (`JM Fernández`_) | |
318 | + Add ``__str__`` method (`JM Fernández`_) | |
319 | + Add test functions (`JM Fernández`_) | |
320 | + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) | |
321 | + Fix gateway IP unresolvable exception catching (`JM Fernández`_) | |
322 | + Minor fixes (`JM Fernández`_) | |
323 | + Add AppVeyor support (`JM Fernández`_) | |
324 | ||
325 | - v.0.0.7 (`JM Fernández`_) | |
326 | + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) | |
327 | + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) | |
328 | + Allow sending a pkey directly (`#43`_) (`Chronial`_) | |
329 | + Add ``-V`` CLI option to show current version (`JM Fernández`_) | |
330 | + Add coverage (`JM Fernández`_) | |
331 | + Refactoring (`JM Fernández`_) | |
332 | ||
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`_) | |
337 | + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) | |
338 | + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) | |
339 | + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) | |
340 | + 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`_) | |
354 | + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) | |
355 | ||
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* | |
363 | + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* | |
364 | + add CLI interface (`JM Fernández`_) | |
365 | + 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`_) | |
369 | + 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`_) | |
373 | + add ``threaded`` option (`Cameron Maske`_) | |
374 | + 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 | ||
382 | .. _Cameron Maske: https://github.com/cameronmaske | |
383 | .. _Gustavo Machado: https://github.com/gdmachado | |
384 | .. _Colin Jermain: https://github.com/cjermain | |
385 | .. _JM Fernández: https://github.com/fernandezcuesta | |
386 | .. _Lewis Thompson: https://github.com/lewisthompson | |
387 | .. _Erik Rogers: https://github.com/ewrogers | |
388 | .. _Mart Sõmermaa: https://github.com/mrts | |
389 | .. _Chronial: https://github.com/Chronial | |
390 | .. _Dan Harbin: https://github.com/RasterBurn | |
391 | .. _Ignacio Peluffo: https://github.com/ipeluffo | |
392 | .. _Niels Zeilemaker: https://github.com/NielsZeilemaker | |
393 | .. _#13: https://github.com/pahaz/sshtunnel/issues/13 | |
394 | .. _#16: https://github.com/pahaz/sshtunnel/issues/16 | |
395 | .. _#19: https://github.com/pahaz/sshtunnel/issues/19 | |
396 | .. _#21: https://github.com/pahaz/sshtunnel/issues/21 | |
397 | .. _#24: https://github.com/pahaz/sshtunnel/issues/24 | |
398 | .. _#29: https://github.com/pahaz/sshtunnel/issues/29 | |
399 | .. _#31: https://github.com/pahaz/sshtunnel/issues/31 | |
400 | .. _#33: https://github.com/pahaz/sshtunnel/issues/33 | |
401 | .. _#34: https://github.com/pahaz/sshtunnel/issues/34 | |
402 | .. _#36: https://github.com/pahaz/sshtunnel/issues/36 | |
403 | .. _#41: https://github.com/pahaz/sshtunnel/issues/41 | |
404 | .. _#43: https://github.com/pahaz/sshtunnel/issues/43 | |
405 | .. _#46: https://github.com/pahaz/sshtunnel/issues/46 | |
406 | .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 | |
407 | ||
8 | Download-URL: https://pypi.python.org/packages/source/s/sshtunnel/sshtunnel-0.4.0.zip | |
408 | 9 | Keywords: ssh tunnel paramiko proxy tcp-forward |
409 | 10 | Platform: unix |
410 | 11 | Platform: macos |
419 | 20 | Classifier: Programming Language :: Python :: 3.4 |
420 | 21 | Classifier: Programming Language :: Python :: 3.5 |
421 | 22 | Classifier: Programming Language :: Python :: 3.6 |
23 | Classifier: Programming Language :: Python :: 3.7 | |
24 | Classifier: Programming Language :: Python :: 3.8 | |
25 | Description-Content-Type: text/x-rst | |
26 | Provides-Extra: build_sphinx | |
27 | Provides-Extra: dev | |
28 | Provides-Extra: test | |
29 | License-File: LICENSE | |
30 | ||
31 | |CircleCI| |AppVeyor| |readthedocs| |coveralls| |version| | |
32 | ||
33 | |pyversions| |license| | |
34 | ||
35 | **Author**: `Pahaz`_ | |
36 | ||
37 | **Repo**: https://github.com/pahaz/sshtunnel/ | |
38 | ||
39 | Inspired by https://github.com/jmagnusson/bgtunnel, which doesn't work on | |
40 | Windows. | |
41 | ||
42 | See also: https://github.com/paramiko/paramiko/blob/master/demos/forward.py | |
43 | ||
44 | Requirements | |
45 | ------------- | |
46 | ||
47 | * `paramiko`_ | |
48 | ||
49 | Installation | |
50 | ============ | |
51 | ||
52 | `sshtunnel`_ is on PyPI, so simply run: | |
53 | ||
54 | :: | |
55 | ||
56 | pip install sshtunnel | |
57 | ||
58 | or :: | |
59 | ||
60 | easy_install sshtunnel | |
61 | ||
62 | or :: | |
63 | ||
64 | conda install -c conda-forge sshtunnel | |
65 | ||
66 | to have it installed in your environment. | |
67 | ||
68 | For installing from source, clone the | |
69 | `repo <https://github.com/pahaz/sshtunnel>`_ and run:: | |
70 | ||
71 | python setup.py install | |
72 | ||
73 | Testing the package | |
74 | ------------------- | |
75 | ||
76 | In order to run the tests you first need | |
77 | `tox <https://testrun.org/tox/latest/>`_ and run:: | |
78 | ||
79 | python setup.py test | |
80 | ||
81 | Usage scenarios | |
82 | =============== | |
83 | ||
84 | One of the typical scenarios where ``sshtunnel`` is helpful is depicted in the | |
85 | figure below. User may need to connect a port of a remote server (i.e. 8080) | |
86 | where only SSH port (usually port 22) is reachable. :: | |
87 | ||
88 | ---------------------------------------------------------------------- | |
89 | ||
90 | | | |
91 | -------------+ | +----------+ | |
92 | LOCAL | | | REMOTE | :22 SSH | |
93 | CLIENT | <== SSH ========> | SERVER | :8080 web service | |
94 | -------------+ | +----------+ | |
95 | | | |
96 | FIREWALL (only port 22 is open) | |
97 | ||
98 | ---------------------------------------------------------------------- | |
99 | ||
100 | **Fig1**: How to connect to a service blocked by a firewall through SSH tunnel. | |
101 | ||
102 | ||
103 | If allowed by the SSH server, it is also possible to reach a private server | |
104 | (from the perspective of ``REMOTE SERVER``) not directly visible from the | |
105 | outside (``LOCAL CLIENT``'s perspective). :: | |
106 | ||
107 | ---------------------------------------------------------------------- | |
108 | ||
109 | | | |
110 | -------------+ | +----------+ +--------- | |
111 | LOCAL | | | REMOTE | | PRIVATE | |
112 | CLIENT | <== SSH ========> | SERVER | <== local ==> | SERVER | |
113 | -------------+ | +----------+ +--------- | |
114 | | | |
115 | FIREWALL (only port 443 is open) | |
116 | ||
117 | ---------------------------------------------------------------------- | |
118 | ||
119 | **Fig2**: How to connect to ``PRIVATE SERVER`` through SSH tunnel. | |
120 | ||
121 | ||
122 | Usage examples | |
123 | ============== | |
124 | ||
125 | API allows either initializing the tunnel and starting it or using a ``with`` | |
126 | context, which will take care of starting **and stopping** the tunnel: | |
127 | ||
128 | Example 1 | |
129 | --------- | |
130 | ||
131 | Code corresponding to **Fig1** above follows, given remote server's address is | |
132 | ``pahaz.urfuclub.ru``, password authentication and randomly assigned local bind | |
133 | port. | |
134 | ||
135 | .. code-block:: python | |
136 | ||
137 | from sshtunnel import SSHTunnelForwarder | |
138 | ||
139 | server = SSHTunnelForwarder( | |
140 | 'alfa.8iq.dev', | |
141 | ssh_username="pahaz", | |
142 | ssh_password="secret", | |
143 | remote_bind_address=('127.0.0.1', 8080) | |
144 | ) | |
145 | ||
146 | server.start() | |
147 | ||
148 | print(server.local_bind_port) # show assigned local port | |
149 | # work with `SECRET SERVICE` through `server.local_bind_port`. | |
150 | ||
151 | server.stop() | |
152 | ||
153 | Example 2 | |
154 | --------- | |
155 | ||
156 | Example of a port forwarding to a private server not directly reachable, | |
157 | assuming password protected pkey authentication, remote server's SSH service is | |
158 | listening on port 443 and that port is open in the firewall (**Fig2**): | |
159 | ||
160 | .. code-block:: python | |
161 | ||
162 | import paramiko | |
163 | import sshtunnel | |
164 | ||
165 | with sshtunnel.open_tunnel( | |
166 | (REMOTE_SERVER_IP, 443), | |
167 | ssh_username="", | |
168 | ssh_pkey="/var/ssh/rsa_key", | |
169 | ssh_private_key_password="secret", | |
170 | remote_bind_address=(PRIVATE_SERVER_IP, 22), | |
171 | local_bind_address=('0.0.0.0', 10022) | |
172 | ) as tunnel: | |
173 | client = paramiko.SSHClient() | |
174 | client.load_system_host_keys() | |
175 | client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | |
176 | client.connect('127.0.0.1', 10022) | |
177 | # do some operations with client session | |
178 | client.close() | |
179 | ||
180 | print('FINISH!') | |
181 | ||
182 | Example 3 | |
183 | --------- | |
184 | ||
185 | Example of a port forwarding for the Vagrant MySQL local port: | |
186 | ||
187 | .. code-block:: python | |
188 | ||
189 | from sshtunnel import open_tunnel | |
190 | from time import sleep | |
191 | ||
192 | with open_tunnel( | |
193 | ('localhost', 2222), | |
194 | ssh_username="vagrant", | |
195 | ssh_password="vagrant", | |
196 | remote_bind_address=('127.0.0.1', 3306) | |
197 | ) as server: | |
198 | ||
199 | print(server.local_bind_port) | |
200 | while True: | |
201 | # press Ctrl-C for stopping | |
202 | sleep(1) | |
203 | ||
204 | print('FINISH!') | |
205 | ||
206 | Or simply using the CLI: | |
207 | ||
208 | .. code-block:: console | |
209 | ||
210 | (bash)$ python -m sshtunnel -U vagrant -P vagrant -L :3306 -R 127.0.0.1:3306 -p 2222 localhost | |
211 | ||
212 | Example 4 | |
213 | --------- | |
214 | ||
215 | Opening an SSH session jumping over two tunnels. SSH transport and tunnels | |
216 | will be daemonised, which will not wait for the connections to stop at close | |
217 | time. | |
218 | ||
219 | .. code-block:: python | |
220 | ||
221 | import sshtunnel | |
222 | from paramiko import SSHClient | |
223 | ||
224 | ||
225 | with sshtunnel.open_tunnel( | |
226 | ssh_address_or_host=('GW1_ip', 20022), | |
227 | remote_bind_address=('GW2_ip', 22), | |
228 | ) as tunnel1: | |
229 | print('Connection to tunnel1 (GW1_ip:GW1_port) OK...') | |
230 | with sshtunnel.open_tunnel( | |
231 | ssh_address_or_host=('localhost', tunnel1.local_bind_port), | |
232 | remote_bind_address=('target_ip', 22), | |
233 | ssh_username='GW2_user', | |
234 | ssh_password='GW2_pwd', | |
235 | ) as tunnel2: | |
236 | print('Connection to tunnel2 (GW2_ip:GW2_port) OK...') | |
237 | with SSHClient() as ssh: | |
238 | ssh.connect('localhost', | |
239 | port=tunnel2.local_bind_port, | |
240 | username='target_user', | |
241 | password='target_pwd', | |
242 | ) | |
243 | ssh.exec_command(...) | |
244 | ||
245 | ||
246 | CLI usage | |
247 | ========= | |
248 | ||
249 | :: | |
250 | ||
251 | $ sshtunnel --help | |
252 | usage: sshtunnel [-h] [-U SSH_USERNAME] [-p SSH_PORT] [-P SSH_PASSWORD] -R | |
253 | IP:PORT [IP:PORT ...] [-L [IP:PORT [IP:PORT ...]]] | |
254 | [-k SSH_HOST_KEY] [-K KEY_FILE] [-S KEY_PASSWORD] [-t] [-v] | |
255 | [-V] [-x IP:PORT] [-c SSH_CONFIG_FILE] [-z] [-n] | |
256 | [-d [FOLDER [FOLDER ...]]] | |
257 | ssh_address | |
258 | ||
259 | Pure python ssh tunnel utils | |
260 | Version 0.4.0 | |
261 | ||
262 | positional arguments: | |
263 | ssh_address SSH server IP address (GW for SSH tunnels) | |
264 | set with "-- ssh_address" if immediately after -R or -L | |
265 | ||
266 | optional arguments: | |
267 | -h, --help show this help message and exit | |
268 | -U SSH_USERNAME, --username SSH_USERNAME | |
269 | SSH server account username | |
270 | -p SSH_PORT, --server_port SSH_PORT | |
271 | SSH server TCP port (default: 22) | |
272 | -P SSH_PASSWORD, --password SSH_PASSWORD | |
273 | SSH server account password | |
274 | -R IP:PORT [IP:PORT ...], --remote_bind_address IP:PORT [IP:PORT ...] | |
275 | Remote bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
276 | Equivalent to ssh -Lxxxx:IP_ADDRESS:PORT | |
277 | If port is omitted, defaults to 22. | |
278 | Example: -R 10.10.10.10: 10.10.10.10:5900 | |
279 | -L [IP:PORT [IP:PORT ...]], --local_bind_address [IP:PORT [IP:PORT ...]] | |
280 | Local bind address sequence: ip_1:port_1 ip_2:port_2 ... ip_n:port_n | |
281 | Elements may also be valid UNIX socket domains: | |
282 | /tmp/foo.sock /tmp/bar.sock ... /tmp/baz.sock | |
283 | Equivalent to ssh -LPORT:xxxxxxxxx:xxxx, being the local IP address optional. | |
284 | By default it will listen in all interfaces (0.0.0.0) and choose a random port. | |
285 | Example: -L :40000 | |
286 | -k SSH_HOST_KEY, --ssh_host_key SSH_HOST_KEY | |
287 | Gateway's host key | |
288 | -K KEY_FILE, --private_key_file KEY_FILE | |
289 | RSA/DSS/ECDSA private key file | |
290 | -S KEY_PASSWORD, --private_key_password KEY_PASSWORD | |
291 | RSA/DSS/ECDSA private key password | |
292 | -t, --threaded Allow concurrent connections to each tunnel | |
293 | -v, --verbose Increase output verbosity (default: ERROR) | |
294 | -V, --version Show version number and quit | |
295 | -x IP:PORT, --proxy IP:PORT | |
296 | IP and port of SSH proxy to destination | |
297 | -c SSH_CONFIG_FILE, --config SSH_CONFIG_FILE | |
298 | SSH configuration file, defaults to ~/.ssh/config | |
299 | -z, --compress Request server for compression over SSH transport | |
300 | -n, --noagent Disable looking for keys from an SSH agent | |
301 | -d [FOLDER [FOLDER ...]], --host_pkey_directories [FOLDER [FOLDER ...]] | |
302 | List of directories where SSH pkeys (in the format `id_*`) may be found | |
303 | ||
304 | .. _Pahaz: https://github.com/pahaz | |
305 | .. _sshtunnel: https://pypi.python.org/pypi/sshtunnel | |
306 | .. _paramiko: http://www.paramiko.org/ | |
307 | .. |CircleCI| image:: https://circleci.com/gh/pahaz/sshtunnel.svg?style=svg | |
308 | :target: https://circleci.com/gh/pahaz/sshtunnel | |
309 | .. |AppVeyor| image:: https://ci.appveyor.com/api/projects/status/oxg1vx2ycmnw3xr9?svg=true&passingText=Windows%20-%20OK&failingText=Windows%20-%20Fail | |
310 | :target: https://ci.appveyor.com/project/pahaz/sshtunnel | |
311 | .. |readthedocs| image:: https://readthedocs.org/projects/sshtunnel/badge/?version=latest | |
312 | :target: http://sshtunnel.readthedocs.io/en/latest/?badge=latest | |
313 | :alt: Documentation Status | |
314 | .. |coveralls| image:: https://coveralls.io/repos/github/pahaz/sshtunnel/badge.svg?branch=master | |
315 | :target: https://coveralls.io/github/pahaz/sshtunnel?branch=master | |
316 | .. |pyversions| image:: https://img.shields.io/pypi/pyversions/sshtunnel.svg | |
317 | .. |version| image:: https://img.shields.io/pypi/v/sshtunnel.svg | |
318 | :target: `sshtunnel`_ | |
319 | .. |license| image:: https://img.shields.io/pypi/l/sshtunnel.svg | |
320 | :target: https://github.com/pahaz/sshtunnel/blob/master/LICENSE | |
321 | ||
322 | Online documentation | |
323 | ==================== | |
324 | ||
325 | Documentation may be found at `readthedocs`_. | |
326 | ||
327 | .. _readthedocs: https://sshtunnel.readthedocs.org/ | |
328 | ||
329 | CONTRIBUTORS | |
330 | ============ | |
331 | ||
332 | - `Cameron Maske`_ | |
333 | - `Gustavo Machado`_ | |
334 | - `Colin Jermain`_ | |
335 | - `JM Fernández`_ - (big thanks!) | |
336 | - `Lewis Thompson`_ | |
337 | - `Erik Rogers`_ | |
338 | - `Mart Sõmermaa`_ | |
339 | - `Chronial`_ | |
340 | - `Dan Harbin`_ | |
341 | - `Ignacio Peluffo`_ | |
342 | - `Niels Zeilemaker`_ | |
343 | - `Georgy Rylov`_ | |
344 | - `Eddie Chiang`_ | |
345 | - `kkrasovskii`_ | |
346 | ||
347 | CHANGELOG | |
348 | ========= | |
349 | ||
350 | - v.0.4.0 (`Pahaz`_) | |
351 | + Change the daemon mod flag for all tunnel threads (is not fully backward compatible) to prevent unexpected hangs (`#219`_) | |
352 | + Add docker based end to end functinal tests for Mongo/Postgres/MySQL (`#219`_) | |
353 | + Add docker based end to end hangs tests (`#219`_) | |
354 | ||
355 | - v.0.3.2 (`Pahaz`_, `JM Fernández`_) | |
356 | + Fix host key directory detection | |
357 | + Unify default ssh config folder to `~/.ssh` | |
358 | ||
359 | - v.0.3.1 (`Pahaz`_) | |
360 | + Increase open connection timeout to 10 secods | |
361 | ||
362 | - v.0.3.0 (`Pahaz`_) | |
363 | + Change default with context behavior to use `.stop(force=True)` on exit (is not fully backward compatible) | |
364 | + Remove useless `daemon_forward_servers = True` hack for hangs prevention (is not fully backward compatible) | |
365 | + Set transport keepalive to 5 second by default (disabled for version < 0.3.0) | |
366 | + Set default transport timeout to 0.1 | |
367 | + Deprecate and remove `block_on_close` option | |
368 | + Fix "deadlocks" / "tunneling hangs" (`#173`_, `#201`_, `#162`_, `#211`_) | |
369 | ||
370 | - v.0.2.2 (`Pahaz`_) | |
371 | + Add `.stop(force=True)` for force close active connections (`#201`_) | |
372 | ||
373 | - v.0.2.1 (`Pahaz`_, `Eddie Chiang`_ and `kkrasovskii`_) | |
374 | + Fixes bug with orphan thread for a tunnel that is DOWN (`#170`_) | |
375 | ||
376 | - v.0.2.0 (`Georgy Rylov`_) | |
377 | + 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. | |
378 | ||
379 | - v.0.1.5 (`JM Fernández`_) | |
380 | + Introduce `block_on_close` attribute | |
381 | ||
382 | - v.0.1.4 (`Niels Zeilemaker`_) | |
383 | + Allow loading pkeys from `~/.ssh` | |
384 | ||
385 | - v.0.1.3 (`Ignacio Peluffo`_ and others) | |
386 | + ``pkey_file`` parameter updated to accept relative paths to user folder using ``~`` | |
387 | + Several bugfixes | |
388 | ||
389 | - v.0.1.2 (`JM Fernández`_) | |
390 | + Fix #77 | |
391 | ||
392 | - v.0.1.1 (`JM Fernández`_) | |
393 | + Fix #72 | |
394 | ||
395 | - v.0.1.0 (`JM Fernández`_) | |
396 | + Add `tunnel_bindings` property | |
397 | + Several bugfixes (#49, #56, #57, #59, #60, #62, #64, #66, ...) | |
398 | (`Pahaz`_, `JM Fernández`_) | |
399 | + Add TRACE logging level (`JM Fernández`_) | |
400 | + Code and tests refactoring (`JM Fernández`_) | |
401 | + Drop python3.2 support | |
402 | ||
403 | - v.0.0.8 (`JM Fernández`_) | |
404 | + Merge `#31`_: Support Unix domain socket (local) forwarding (`Dan Harbin`_) | |
405 | + Simplify API (`JM Fernández`_) | |
406 | + Add sphinx-based documentation (`JM Fernández`_) | |
407 | + Add ``allow_agent`` (fixes `#36`_, `#46`_) (`JM Fernández`_) | |
408 | + Add ``compression`` (`JM Fernández`_) | |
409 | + Add ``__str__`` method (`JM Fernández`_) | |
410 | + Add test functions (`JM Fernández`_) | |
411 | + Fix default username when not provided and ssh_config file is skipped (`JM Fernández`_) | |
412 | + Fix gateway IP unresolvable exception catching (`JM Fernández`_) | |
413 | + Minor fixes (`JM Fernández`_) | |
414 | + Add AppVeyor support (`JM Fernández`_) | |
415 | ||
416 | - v.0.0.7 (`JM Fernández`_) | |
417 | + Tunnels can now be stopped and started safely (`#41`_) (`JM Fernández`_) | |
418 | + Add timeout to SSH gateway and keep-alive messages (`#29`_) (`JM Fernández`_) | |
419 | + Allow sending a pkey directly (`#43`_) (`Chronial`_) | |
420 | + Add ``-V`` CLI option to show current version (`JM Fernández`_) | |
421 | + Add coverage (`JM Fernández`_) | |
422 | + Refactoring (`JM Fernández`_) | |
423 | ||
424 | - v.0.0.6 (`Pahaz`_) | |
425 | + add ``-S`` CLI options for ssh private key password support (`Pahaz`_) | |
426 | ||
427 | - v.0.0.5 (`Pahaz`_) | |
428 | + add ``ssh_proxy`` argument, as well as ``ssh_config(5)`` ``ProxyCommand`` support (`Lewis Thompson`_) | |
429 | + add some python 2.6 compatibility fixes (`Mart Sõmermaa`_) | |
430 | + ``paramiko.transport`` inherits handlers of loggers passed to ``SSHTunnelForwarder`` (`JM Fernández`_) | |
431 | + fix `#34`_, `#33`_, code style and docs (`JM Fernández`_) | |
432 | + add tests (`Pahaz`_) | |
433 | + add CI integration (`Pahaz`_) | |
434 | + normal packaging (`Pahaz`_) | |
435 | + disable check distenation socket connection by ``SSHTunnelForwarder.local_is_up`` (`Pahaz`_) [changed default behavior] | |
436 | + use daemon mode = False in all threads by default; detail_ (`Pahaz`_) [changed default behavior] | |
437 | ||
438 | - v.0.0.4.4 (`Pahaz`_) | |
439 | + fix issue `#24`_ - hide ssh password in logs (`Pahaz`_) | |
440 | ||
441 | - v.0.0.4.3 (`Pahaz`_) | |
442 | + fix default port issue `#19`_ (`Pahaz`_) | |
443 | ||
444 | - v.0.0.4.2 (`Pahaz`_) | |
445 | + fix Thread.daemon mode for Python < 3.3 `#16`_, `#21`_ (`Lewis Thompson`_, `Erik Rogers`_) | |
446 | ||
447 | - v.0.0.4.1 (`Pahaz`_) | |
448 | + fix CLI issues `#13`_ (`Pahaz`_) | |
449 | ||
450 | - v.0.0.4 (`Pahaz`_) | |
451 | + daemon mode by default for all threads (`JM Fernández`_, `Pahaz`_) - *incompatible* | |
452 | + move ``make_ssh_forward_server`` to ``SSHTunnelForwarder.make_ssh_forward_server`` (`Pahaz`_, `JM Fernández`_) - *incompatible* | |
453 | + move ``make_ssh_forward_handler`` to ``SSHTunnelForwarder.make_ssh_forward_handler_class`` (`Pahaz`_, `JM Fernández`_) - *incompatible* | |
454 | + rename ``open`` to ``open_tunnel`` (`JM Fernández`_) - *incompatible* | |
455 | + add CLI interface (`JM Fernández`_) | |
456 | + support opening several tunnels at once (`JM Fernández`_) | |
457 | + improve stability and readability (`JM Fernández`_, `Pahaz`_) | |
458 | + improve logging (`JM Fernández`_, `Pahaz`_) | |
459 | + add ``raise_exception_if_any_forwarder_have_a_problem`` argument for opening several tunnels at once (`Pahaz`_) | |
460 | + add ``ssh_config_file`` argument support (`JM Fernández`_) | |
461 | + add Python 3 support (`JM Fernández`_, `Pahaz`_) | |
462 | ||
463 | - v.0.0.3 (`Pahaz`_) | |
464 | + add ``threaded`` option (`Cameron Maske`_) | |
465 | + fix exception error message, correctly printing destination address (`Gustavo Machado`_) | |
466 | + fix ``pip install`` failure (`Colin Jermain`_, `Pahaz`_) | |
467 | ||
468 | - v.0.0.1 (`Pahaz`_) | |
469 | + ``SSHTunnelForwarder`` class (`Pahaz`_) | |
470 | + ``open`` function (`Pahaz`_) | |
471 | ||
472 | ||
473 | .. _Pahaz: https://github.com/pahaz | |
474 | .. _Cameron Maske: https://github.com/cameronmaske | |
475 | .. _Gustavo Machado: https://github.com/gdmachado | |
476 | .. _Colin Jermain: https://github.com/cjermain | |
477 | .. _JM Fernández: https://github.com/fernandezcuesta | |
478 | .. _Lewis Thompson: https://github.com/lewisthompson | |
479 | .. _Erik Rogers: https://github.com/ewrogers | |
480 | .. _Mart Sõmermaa: https://github.com/mrts | |
481 | .. _Chronial: https://github.com/Chronial | |
482 | .. _Dan Harbin: https://github.com/RasterBurn | |
483 | .. _Ignacio Peluffo: https://github.com/ipeluffo | |
484 | .. _Niels Zeilemaker: https://github.com/NielsZeilemaker | |
485 | .. _Georgy Rylov: https://github.com/g0djan | |
486 | .. _Eddie Chiang: https://github.com/eddie-chiang | |
487 | .. _kkrasovskii: https://github.com/kkrasovskii | |
488 | .. _#13: https://github.com/pahaz/sshtunnel/issues/13 | |
489 | .. _#16: https://github.com/pahaz/sshtunnel/issues/16 | |
490 | .. _#19: https://github.com/pahaz/sshtunnel/issues/19 | |
491 | .. _#21: https://github.com/pahaz/sshtunnel/issues/21 | |
492 | .. _#24: https://github.com/pahaz/sshtunnel/issues/24 | |
493 | .. _#29: https://github.com/pahaz/sshtunnel/issues/29 | |
494 | .. _#31: https://github.com/pahaz/sshtunnel/issues/31 | |
495 | .. _#33: https://github.com/pahaz/sshtunnel/issues/33 | |
496 | .. _#34: https://github.com/pahaz/sshtunnel/issues/34 | |
497 | .. _#36: https://github.com/pahaz/sshtunnel/issues/36 | |
498 | .. _#41: https://github.com/pahaz/sshtunnel/issues/41 | |
499 | .. _#43: https://github.com/pahaz/sshtunnel/issues/43 | |
500 | .. _#46: https://github.com/pahaz/sshtunnel/issues/46 | |
501 | .. _#170: https://github.com/pahaz/sshtunnel/issues/170 | |
502 | .. _#201: https://github.com/pahaz/sshtunnel/issues/201 | |
503 | .. _#162: https://github.com/pahaz/sshtunnel/issues/162 | |
504 | .. _#173: https://github.com/pahaz/sshtunnel/issues/173 | |
505 | .. _#201: https://github.com/pahaz/sshtunnel/issues/201 | |
506 | .. _#211: https://github.com/pahaz/sshtunnel/issues/211 | |
507 | .. _#219: https://github.com/pahaz/sshtunnel/issues/219 | |
508 | .. _detail: https://github.com/pahaz/sshtunnel/commit/64af238b799b0e0057c4f9b386cda247e0006da9#diff-76bc1662a114401c2954deb92b740081R127 | |
509 | ||
510 |
3 | 3 | Troubleshoot.rst |
4 | 4 | changelog.rst |
5 | 5 | docs.rst |
6 | pyproject.toml | |
6 | 7 | setup.cfg |
7 | 8 | setup.py |
8 | 9 | sshtunnel.py |
9 | 10 | docs/Makefile |
10 | 11 | docs/conf.py |
11 | 12 | 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 | |
13 | 26 | sshtunnel.egg-info/PKG-INFO |
14 | 27 | sshtunnel.egg-info/SOURCES.txt |
15 | 28 | sshtunnel.egg-info/dependency_links.txt |
17 | 30 | sshtunnel.egg-info/requires.txt |
18 | 31 | sshtunnel.egg-info/top_level.txt |
19 | 32 | tests/__init__.py |
20 | tests/__init__.pyc | |
33 | tests/requirements-syntax.txt | |
34 | tests/requirements.txt | |
21 | 35 | tests/test_forwarder.py |
22 | 36 | tests/testconfig |
23 | 37 | tests/testrsa.key |
28 | 28 | import SocketServer as socketserver |
29 | 29 | string_types = basestring, # noqa |
30 | 30 | input_ = raw_input # noqa |
31 | else: | |
31 | else: # pragma: no cover | |
32 | 32 | import queue |
33 | 33 | import socketserver |
34 | 34 | string_types = str |
35 | 35 | input_ = input |
36 | 36 | |
37 | 37 | |
38 | __version__ = '0.1.4' | |
38 | __version__ = '0.4.0' | |
39 | 39 | __author__ = 'pahaz' |
40 | 40 | |
41 | 41 | |
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 | |
46 | 48 | _CONNECTION_COUNTER = 1 |
47 | 49 | _LOCK = threading.Lock() |
48 | #: Timeout (seconds) for the connection to the SSH gateway, ``None`` to disable | |
49 | SSH_TIMEOUT = None | |
50 | DEPRECATIONS = { | |
50 | _DEPRECATIONS = { | |
51 | 51 | 'ssh_address': 'ssh_address_or_host', |
52 | 52 | 'ssh_host': 'ssh_address_or_host', |
53 | 53 | 'ssh_private_key': 'ssh_pkey', |
54 | 54 | 'raise_exception_if_any_forwarder_have_a_problem': 'mute_exceptions' |
55 | 55 | } |
56 | 56 | |
57 | # logging | |
58 | DEFAULT_LOGLEVEL = logging.ERROR #: default level if no logger passed (ERROR) | |
59 | TRACE_LEVEL = 1 | |
57 | 60 | 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 | |
65 | 65 | |
66 | 66 | #: Path of optional ssh configuration file |
67 | DEFAULT_SSH_DIRECTORY = '~/.ssh' | |
67 | 68 | SSH_CONFIG_FILE = os.path.join(DEFAULT_SSH_DIRECTORY, 'config') |
68 | 69 | |
69 | 70 | ######################## |
189 | 190 | :class:`logging.Logger` |
190 | 191 | """ |
191 | 192 | logger = logger or logging.getLogger( |
192 | '{0}.SSHTunnelForwarder'.format(__name__) | |
193 | 'sshtunnel.SSHTunnelForwarder' | |
193 | 194 | ) |
194 | 195 | if not any(isinstance(x, logging.Handler) for x in logger.handlers): |
195 | 196 | logger.setLevel(loglevel or DEFAULT_LOGLEVEL) |
306 | 307 | if self.request in rqst: |
307 | 308 | data = self.request.recv(1024) |
308 | 309 | if not data: |
310 | self.logger.log( | |
311 | TRACE_LEVEL, | |
312 | '>>> OUT {0} recv empty data >>>'.format(self.info) | |
313 | ) | |
309 | 314 | 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) | |
317 | 324 | if chan in rqst: # else |
318 | 325 | if not chan.recv_ready(): |
326 | self.logger.log( | |
327 | TRACE_LEVEL, | |
328 | '<<< IN {0} recv is not ready <<<'.format(self.info) | |
329 | ) | |
319 | 330 | break |
320 | 331 | data = chan.recv(1024) |
321 | 332 | self.logger.log( |
322 | 333 | TRACE_LEVEL, |
323 | 334 | '<<< IN {0} recv: {1} <<<'.format(self.info, hexlify(data)) |
324 | 335 | ) |
325 | self.request.send(data) | |
336 | self.request.sendall(data) | |
326 | 337 | |
327 | 338 | def handle(self): |
328 | 339 | uid = get_connection_id() |
338 | 349 | src_addr=src_address, |
339 | 350 | timeout=TUNNEL_TIMEOUT |
340 | 351 | ) |
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) | |
350 | 358 | |
351 | 359 | self.logger.log(TRACE_LEVEL, '{0} connected'.format(self.info)) |
352 | 360 | try: |
375 | 383 | |
376 | 384 | def __init__(self, *args, **kwargs): |
377 | 385 | self.logger = create_logger(kwargs.pop('logger', None)) |
378 | self.tunnel_ok = queue.Queue() | |
386 | self.tunnel_ok = queue.Queue(1) | |
379 | 387 | socketserver.TCPServer.__init__(self, *args, **kwargs) |
380 | 388 | |
381 | 389 | def handle_error(self, request, client_address): |
382 | 390 | (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)) | |
386 | 403 | |
387 | 404 | @property |
388 | 405 | def local_address(self): |
414 | 431 | Allow concurrent connections to each tunnel |
415 | 432 | """ |
416 | 433 | # 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) | |
423 | 441 | """ |
424 | 442 | |
425 | 443 | def __init__(self, *args, **kwargs): |
426 | 444 | 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) | |
429 | 447 | |
430 | 448 | @property |
431 | 449 | def local_address(self): |
452 | 470 | return self.RequestHandlerClass.remote_address[1] |
453 | 471 | |
454 | 472 | |
455 | class _ThreadingUnixStreamForwardServer(socketserver.ThreadingMixIn, | |
456 | _UnixStreamForwardServer): | |
473 | class _ThreadingStreamForwardServer(socketserver.ThreadingMixIn, | |
474 | _StreamForwardServer): | |
457 | 475 | """ |
458 | 476 | Allow concurrent connections to each tunnel |
459 | 477 | """ |
460 | 478 | # 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 | |
462 | 481 | |
463 | 482 | |
464 | 483 | class SSHTunnelForwarder(object): |
613 | 632 | |
614 | 633 | host_pkey_directories (list): |
615 | 634 | 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) | |
619 | 637 | |
620 | 638 | .. versionadded:: 0.1.4 |
621 | 639 | |
648 | 666 | Interval in seconds defining the period in which, if no data |
649 | 667 | was sent over the connection, a *'keepalive'* packet will be |
650 | 668 | 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) | |
654 | 673 | |
655 | 674 | .. versionadded:: 0.0.7 |
656 | 675 | |
719 | 738 | |
720 | 739 | """ |
721 | 740 | 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 | |
724 | 745 | |
725 | 746 | def local_is_up(self, target): |
726 | 747 | """ |
746 | 767 | 'target can be a valid UNIX domain socket.') |
747 | 768 | return False |
748 | 769 | |
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 | |
750 | 781 | 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) | |
971 | 784 | 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 | |
1241 | 786 | |
1242 | 787 | def _check_tunnel(self, _srv): |
1243 | 788 | """ Check if tunnel is already established """ |
1275 | 820 | finally: |
1276 | 821 | s.close() |
1277 | 822 | |
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 | |
1285 | 1322 | |
1286 | 1323 | def start(self): |
1287 | 1324 | """ Start the SSH tunnels """ |
1306 | 1343 | self._raise(HandlerSSHTunnelForwarderError, |
1307 | 1344 | 'An error occurred while opening tunnels.') |
1308 | 1345 | |
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 | |
1312 | 1358 | |
1313 | 1359 | .. note:: This **had** to be handled with care before ``0.1.0``: |
1314 | 1360 | |
1329 | 1375 | (address_to_str(k.local_address) for k in self._server_list) |
1330 | 1376 | ) or 'None' |
1331 | 1377 | self.logger.debug('Listening tunnels: ' + opened_address_text) |
1332 | self._stop_transport() | |
1378 | self._stop_transport(force=force) | |
1333 | 1379 | self._server_list = [] # reset server list |
1334 | 1380 | self.tunnel_is_up = {} # reset tunnel status |
1335 | 1381 | |
1394 | 1440 | address_to_str(_srv.remote_address)) |
1395 | 1441 | ) |
1396 | 1442 | |
1397 | def _stop_transport(self): | |
1443 | def _stop_transport(self, force=False): | |
1398 | 1444 | """ Close the underlying transport when nothing more is needed """ |
1399 | 1445 | try: |
1400 | 1446 | self._check_is_started() |
1401 | 1447 | except (BaseSSHTunnelForwarderError, |
1402 | 1448 | HandlerSSHTunnelForwarderError) as e: |
1403 | 1449 | 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() | |
1404 | 1455 | 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() | |
1409 | 1463 | _srv.server_close() |
1410 | 1464 | # clean up the UNIX domain socket if we're using one |
1411 | if isinstance(_srv, _UnixStreamForwardServer): | |
1465 | if isinstance(_srv, _StreamForwardServer): | |
1412 | 1466 | try: |
1413 | 1467 | os.unlink(_srv.local_address) |
1414 | 1468 | except Exception as e: |
1415 | 1469 | self.logger.error('Unable to unlink socket {0}: {1}' |
1416 | .format(self.local_address, repr(e))) | |
1470 | .format(_srv.local_address, repr(e))) | |
1417 | 1471 | self.is_alive = False |
1418 | 1472 | if self.is_active: |
1473 | self.logger.info('Closing ssh transport') | |
1419 | 1474 | self._transport.close() |
1420 | 1475 | self._transport.stop_thread() |
1421 | 1476 | self.logger.debug('Transport is closed') |
1532 | 1587 | self.ssh_proxy.cmd[1] if self.ssh_proxy else 'no', |
1533 | 1588 | self.ssh_username, |
1534 | 1589 | 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', | |
1536 | 1591 | '' if self.is_alive else 'not ', |
1537 | 1592 | 'disabled' if not self.set_keepalive else |
1538 | 1593 | 'every {0} sec'.format(self.set_keepalive), |
1555 | 1610 | self.__exit__() |
1556 | 1611 | |
1557 | 1612 | 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) | |
1559 | 1622 | |
1560 | 1623 | |
1561 | 1624 | def open_tunnel(*args, **kwargs): |
1614 | 1677 | kwargs |
1615 | 1678 | ) |
1616 | 1679 | |
1617 | ssh_port = kwargs.pop('ssh_port', None) | |
1680 | ssh_port = kwargs.pop('ssh_port', 22) | |
1618 | 1681 | 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) | |
1619 | 1689 | if not args: |
1620 | 1690 | if isinstance(ssh_address_or_host, tuple): |
1621 | 1691 | args = (ssh_address_or_host, ) |
1807 | 1877 | return vars(parser.parse_args(args)) |
1808 | 1878 | |
1809 | 1879 | |
1810 | def _cli_main(args=None): | |
1880 | def _cli_main(args=None, **extras): | |
1811 | 1881 | """ Pass input arguments to open_tunnel |
1812 | 1882 | |
1813 | 1883 | Mandatory: ssh_address, -R (remote bind address list) |
1839 | 1909 | logging.DEBUG, |
1840 | 1910 | TRACE_LEVEL] |
1841 | 1911 | 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) | |
1842 | 1915 | with open_tunnel(**arguments) as tunnel: |
1843 | 1916 | if tunnel.is_alive: |
1844 | 1917 | input_(''' |
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' |
73 | 73 | ssh_config_file=None, |
74 | 74 | allow_agent=False, |
75 | 75 | skip_tunnel_checkup=True, |
76 | host_pkey_directories=[], | |
76 | 77 | ) |
77 | 78 | |
78 | 79 | # CONSTANTS |
168 | 169 | return paramiko.AUTH_SUCCESSFUL if _ok else paramiko.AUTH_FAILED |
169 | 170 | |
170 | 171 | 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()') | |
173 | 173 | return paramiko.OPEN_SUCCEEDED |
174 | 174 | |
175 | 175 | 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()') | |
178 | 177 | return True |
179 | 178 | |
180 | 179 | 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()') | |
183 | 181 | return True |
184 | 182 | |
185 | 183 | 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()') | |
188 | 185 | return True |
189 | 186 | |
190 | 187 | def check_channel_direct_tcpip_request(self, chanid, origin, destination): |
412 | 409 | self.log.info('<<< forward-server received STOP signal') |
413 | 410 | except socket.error: |
414 | 411 | 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)) | |
418 | 415 | finally: |
419 | 416 | if schan: |
420 | 417 | self.log.debug('{0} closing connection...'.format(info)) |
423 | 420 | self.log.debug('{0} connection closed.'.format(info)) |
424 | 421 | |
425 | 422 | def randomize_eport(self): |
426 | return self.eport + random.randint(1, 999) | |
423 | return random.randint(49152, 65535) | |
427 | 424 | |
428 | 425 | def test_echo_server(self): |
429 | 426 | with self._test_server( |
490 | 487 | remote_bind_address=(self.eaddr, self.eport), |
491 | 488 | logger=self.log, |
492 | 489 | ssh_config_file=None, |
493 | allow_agent=False | |
490 | allow_agent=False, | |
491 | host_pkey_directories=[], | |
494 | 492 | ) |
495 | 493 | self.assertEqual(server.ssh_host, self.saddr) |
496 | 494 | self.assertEqual(server.ssh_port, self.sport) |
692 | 690 | open_tunnel(**_kwargs) |
693 | 691 | logged_message = "'{0}' is DEPRECATED use '{1}' instead"\ |
694 | 692 | .format(deprecated_arg, |
695 | sshtunnel.DEPRECATIONS[deprecated_arg]) | |
693 | sshtunnel._DEPRECATIONS[deprecated_arg]) | |
696 | 694 | self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) |
697 | 695 | self.assertEqual(logged_message, str(w[-1].message)) |
698 | 696 | |
712 | 710 | open_tunnel(**_kwargs) |
713 | 711 | logged_message = "'{0}' is DEPRECATED use '{1}' instead"\ |
714 | 712 | .format(deprecated_arg, |
715 | sshtunnel.DEPRECATIONS[deprecated_arg]) | |
713 | sshtunnel._DEPRECATIONS[deprecated_arg]) | |
716 | 714 | self.assertTrue(issubclass(w[-1].category, DeprecationWarning)) |
717 | 715 | self.assertEqual(logged_message, str(w[-1].message)) |
718 | 716 | |
960 | 958 | ) as server: |
961 | 959 | self.assertIsInstance(server.local_bind_addresses, list) |
962 | 960 | 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))) | |
965 | 963 | with self.assertRaises(sshtunnel.BaseSSHTunnelForwarderError): |
966 | 964 | self.log.info(server.local_bind_address) |
967 | 965 | |
1008 | 1006 | '-R', '{0}:{1}'.format(self.eaddr, |
1009 | 1007 | self.eport), |
1010 | 1008 | '-c', '', |
1011 | '-n']) | |
1009 | '-n'], | |
1010 | host_pkey_directories=[]) | |
1012 | 1011 | self.stop_echo_and_ssh_server() |
1013 | 1012 | |
1014 | 1013 | @unittest.skipIf(sys.version_info < (2, 7), |
1083 | 1082 | s.close |
1084 | 1083 | log = 'send to {0}'.format((self.eaddr, self.eport)) |
1085 | 1084 | |
1086 | self.assertTrue(any(log in l for l in | |
1085 | self.assertTrue(any(log in msg for msg in | |
1087 | 1086 | self.sshtunnel_log_messages['trace'])) |
1088 | 1087 | # set loglevel back to the original value |
1089 | 1088 | logger = sshtunnel.create_logger(logger=self.log, |
1124 | 1123 | remote_bind_address=(self.eaddr, self.eport), |
1125 | 1124 | logger=self.log, |
1126 | 1125 | ssh_config_file=None, |
1127 | allow_agent=False | |
1126 | allow_agent=False, | |
1127 | host_pkey_directories=[], | |
1128 | 1128 | ) |
1129 | 1129 | try: |
1130 | 1130 | tunnel.daemon_forward_servers = case |
1156 | 1156 | local_bind_address=('', self.randomize_eport()), |
1157 | 1157 | logger=self.log |
1158 | 1158 | ) as server: |
1159 | ||
1160 | keys = server.get_keys(allow_agent=True) | |
1159 | keys = server.get_keys(logger=self.log) | |
1161 | 1160 | 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'])) | |
1164 | 1163 | |
1165 | 1164 | with self._test_server( |
1166 | 1165 | (self.saddr, self.sport), |
1170 | 1169 | local_bind_address=('', self.randomize_eport()), |
1171 | 1170 | logger=self.log |
1172 | 1171 | ) as server: |
1173 | keys = server.get_keys() | |
1172 | keys = server.get_keys(logger=self.log, allow_agent=True) | |
1174 | 1173 | 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'])) | |
1177 | 1176 | |
1178 | 1177 | tmp_dir = tempfile.mkdtemp() |
1179 | 1178 | shutil.copy(get_test_data_path(PKEY_FILE), |
1185 | 1184 | ) |
1186 | 1185 | self.assertIsInstance(keys, list) |
1187 | 1186 | 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']) | |
1190 | 1189 | ) |
1191 | 1190 | shutil.rmtree(tmp_dir) |
1192 | 1191 | |
1386 | 1385 | 'item', |
1387 | 1386 | kwargs.copy()) |
1388 | 1387 | |
1389 | def check_address(self): | |
1388 | def test_check_address(self): | |
1390 | 1389 | """ Test that an exception is raised with incorrect bind addresses """ |
1391 | 1390 | address_list = [('10.0.0.1', 10000), |
1392 | 1391 | ('10.0.0.1', 10001)] |
1393 | 1392 | if os.name == 'posix': # UNIX sockets supported by the platform |
1394 | 1393 | 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) | |
1395 | 1397 | 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) | |
1399 | 1398 | with self.assertRaises(ValueError): |
1400 | 1399 | sshtunnel.check_address('this is not valid') |
1401 | 1400 | with self.assertRaises(ValueError): |