Skip to content

Commit 81aa82b

Browse files
SSH handler/listener plugins (#1398)
* SSH handler/listener plugins * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Readme updated * Fix listener tests * pyclassrole * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Trigger rebuild * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Trigger build * pre-commit default language version 3.10 * Language version --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 67706ac commit 81aa82b

File tree

8 files changed

+140
-91
lines changed

8 files changed

+140
-91
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ repos:
144144
rev: 3.9.2
145145
hooks:
146146
- id: flake8
147-
language_version: python3
147+
language_version: python3.10
148148
additional_dependencies:
149149
- flake8-2020 >= 1.6.0
150150
- flake8-docstrings >= 1.5.0

README.md

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2341,25 +2341,25 @@ To run standalone benchmark for `proxy.py`, use the following command from repo
23412341

23422342
```console
23432343
proxy -h
2344-
usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
2345-
[--tunnel-username TUNNEL_USERNAME]
2344+
usage: -m [-h] [--threadless] [--threaded] [--num-workers NUM_WORKERS]
2345+
[--enable-events] [--local-executor LOCAL_EXECUTOR]
2346+
[--backlog BACKLOG] [--hostname HOSTNAME]
2347+
[--hostnames HOSTNAMES [HOSTNAMES ...]] [--port PORT]
2348+
[--ports PORTS [PORTS ...]] [--port-file PORT_FILE]
2349+
[--unix-socket-path UNIX_SOCKET_PATH]
2350+
[--num-acceptors NUM_ACCEPTORS] [--tunnel-hostname TUNNEL_HOSTNAME]
2351+
[--tunnel-port TUNNEL_PORT] [--tunnel-username TUNNEL_USERNAME]
23462352
[--tunnel-ssh-key TUNNEL_SSH_KEY]
23472353
[--tunnel-ssh-key-passphrase TUNNEL_SSH_KEY_PASSPHRASE]
2348-
[--tunnel-remote-port TUNNEL_REMOTE_PORT] [--threadless]
2349-
[--threaded] [--num-workers NUM_WORKERS] [--enable-events]
2350-
[--local-executor LOCAL_EXECUTOR] [--backlog BACKLOG]
2351-
[--hostname HOSTNAME] [--hostnames HOSTNAMES [HOSTNAMES ...]]
2352-
[--port PORT] [--ports PORTS [PORTS ...]] [--port-file PORT_FILE]
2353-
[--unix-socket-path UNIX_SOCKET_PATH]
2354-
[--num-acceptors NUM_ACCEPTORS] [--version] [--log-level LOG_LEVEL]
2355-
[--log-file LOG_FILE] [--log-format LOG_FORMAT]
2356-
[--open-file-limit OPEN_FILE_LIMIT]
2354+
[--tunnel-remote-port TUNNEL_REMOTE_PORT] [--version]
2355+
[--log-level LOG_LEVEL] [--log-file LOG_FILE]
2356+
[--log-format LOG_FORMAT] [--open-file-limit OPEN_FILE_LIMIT]
23572357
[--plugins PLUGINS [PLUGINS ...]] [--enable-dashboard]
23582358
[--basic-auth BASIC_AUTH] [--enable-ssh-tunnel]
23592359
[--work-klass WORK_KLASS] [--pid-file PID_FILE] [--openssl OPENSSL]
2360-
[--data-dir DATA_DIR] [--enable-proxy-protocol] [--enable-conn-pool]
2361-
[--key-file KEY_FILE] [--cert-file CERT_FILE]
2362-
[--client-recvbuf-size CLIENT_RECVBUF_SIZE]
2360+
[--data-dir DATA_DIR] [--ssh-listener-klass SSH_LISTENER_KLASS]
2361+
[--enable-proxy-protocol] [--enable-conn-pool] [--key-file KEY_FILE]
2362+
[--cert-file CERT_FILE] [--client-recvbuf-size CLIENT_RECVBUF_SIZE]
23632363
[--server-recvbuf-size SERVER_RECVBUF_SIZE]
23642364
[--max-sendbuf-size MAX_SENDBUF_SIZE] [--timeout TIMEOUT]
23652365
[--disable-http-proxy] [--disable-headers DISABLE_HEADERS]
@@ -2379,25 +2379,10 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
23792379
[--filtered-client-ips FILTERED_CLIENT_IPS]
23802380
[--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
23812381

2382-
proxy.py v2.4.4rc6.dev85+g9335918b
2382+
proxy.py v2.4.4rc6.dev164+g73497f30
23832383

23842384
options:
23852385
-h, --help show this help message and exit
2386-
--tunnel-hostname TUNNEL_HOSTNAME
2387-
Default: None. Remote hostname or IP address to which
2388-
SSH tunnel will be established.
2389-
--tunnel-port TUNNEL_PORT
2390-
Default: 22. SSH port of the remote host.
2391-
--tunnel-username TUNNEL_USERNAME
2392-
Default: None. Username to use for establishing SSH
2393-
tunnel.
2394-
--tunnel-ssh-key TUNNEL_SSH_KEY
2395-
Default: None. Private key path in pem format
2396-
--tunnel-ssh-key-passphrase TUNNEL_SSH_KEY_PASSPHRASE
2397-
Default: None. Private key passphrase
2398-
--tunnel-remote-port TUNNEL_REMOTE_PORT
2399-
Default: 8899. Remote port which will be forwarded
2400-
locally for proxy.
24012386
--threadless Default: True. Enabled by default on Python 3.8+ (mac,
24022387
linux). When disabled a new thread is spawned to
24032388
handle each client connection.
@@ -2434,6 +2419,21 @@ options:
24342419
--host and --port flags are ignored
24352420
--num-acceptors NUM_ACCEPTORS
24362421
Defaults to number of CPU cores.
2422+
--tunnel-hostname TUNNEL_HOSTNAME
2423+
Default: None. Remote hostname or IP address to which
2424+
SSH tunnel will be established.
2425+
--tunnel-port TUNNEL_PORT
2426+
Default: 22. SSH port of the remote host.
2427+
--tunnel-username TUNNEL_USERNAME
2428+
Default: None. Username to use for establishing SSH
2429+
tunnel.
2430+
--tunnel-ssh-key TUNNEL_SSH_KEY
2431+
Default: None. Private key path in pem format
2432+
--tunnel-ssh-key-passphrase TUNNEL_SSH_KEY_PASSPHRASE
2433+
Default: None. Private key passphrase
2434+
--tunnel-remote-port TUNNEL_REMOTE_PORT
2435+
Default: 8899. Remote port which will be forwarded
2436+
locally for proxy.
24372437
--version, -v Prints proxy.py version.
24382438
--log-level LOG_LEVEL
24392439
Valid options: DEBUG, INFO (default), WARNING, ERROR,
@@ -2461,6 +2461,9 @@ options:
24612461
--openssl OPENSSL Default: openssl. Path to openssl binary. By default,
24622462
assumption is that openssl is in your PATH.
24632463
--data-dir DATA_DIR Default: ~/.proxypy. Path to proxypy data directory.
2464+
--ssh-listener-klass SSH_LISTENER_KLASS
2465+
Default: proxy.core.ssh.listener.SshTunnelListener. An
2466+
implementation of BaseSshTunnelListener
24642467
--enable-proxy-protocol
24652468
Default: False. If used, will enable proxy protocol.
24662469
Only version 1 is currently supported.

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@
324324
(_py_class_role, 're.Pattern'),
325325
(_py_class_role, 'proxy.core.base.tcp_server.T'),
326326
(_py_class_role, 'proxy.common.types.RePattern'),
327+
(_py_class_role, 'BaseSshTunnelHandler'),
327328
(_py_obj_role, 'proxy.core.work.threadless.T'),
328329
(_py_obj_role, 'proxy.core.work.work.T'),
329330
(_py_obj_role, 'proxy.core.base.tcp_server.T'),

proxy/core/ssh/listener.py

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,20 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
import sys
12+
import socket
1113
import logging
1214
import argparse
13-
from typing import TYPE_CHECKING, Any, Set, Callable, Optional
15+
from typing import TYPE_CHECKING, Any, Set, Optional, cast
1416

1517

1618
try:
17-
from paramiko import SSHClient, AutoAddPolicy
18-
from paramiko.transport import Transport
19-
if TYPE_CHECKING: # pragma: no cover
20-
from paramiko.channel import Channel
21-
19+
if TYPE_CHECKING: # pragma: no cover
2220
from ...common.types import HostPort
2321
except ImportError: # pragma: no cover
2422
pass
2523

24+
from .base import BaseSshTunnelHandler, BaseSshTunnelListener
2625
from ...common.flag import flags
2726

2827

@@ -72,18 +71,27 @@
7271
)
7372

7473

75-
class SshTunnelListener:
74+
class SshTunnelListener(BaseSshTunnelListener):
7675
"""Connects over SSH and forwards a remote port to local host.
7776
7877
Incoming connections are delegated to provided callback."""
7978

8079
def __init__(
81-
self,
82-
flags: argparse.Namespace,
83-
on_connection_callback: Callable[['Channel', 'HostPort', 'HostPort'], None],
80+
self,
81+
flags: argparse.Namespace,
82+
handler: BaseSshTunnelHandler,
83+
*args: Any,
84+
**kwargs: Any,
8485
) -> None:
86+
paramiko_logger = logging.getLogger('paramiko')
87+
paramiko_logger.setLevel(logging.WARNING)
88+
89+
# pylint: disable=import-outside-toplevel
90+
from paramiko import SSHClient
91+
from paramiko.transport import Transport
92+
8593
self.flags = flags
86-
self.on_connection_callback = on_connection_callback
94+
self.handler = handler
8795
self.ssh: Optional[SSHClient] = None
8896
self.transport: Optional[Transport] = None
8997
self.forwarded: Set['HostPort'] = set()
@@ -92,24 +100,20 @@ def start_port_forward(self, remote_addr: 'HostPort') -> None:
92100
assert self.transport is not None
93101
self.transport.request_port_forward(
94102
*remote_addr,
95-
handler=self.on_connection_callback,
103+
handler=self.handler.on_connection,
96104
)
97105
self.forwarded.add(remote_addr)
98-
logger.info('%s:%d forwarding successful...' % remote_addr)
106+
logger.debug('%s:%d forwarding successful...' % remote_addr)
99107

100108
def stop_port_forward(self, remote_addr: 'HostPort') -> None:
101109
assert self.transport is not None
102110
self.transport.cancel_port_forward(*remote_addr)
103111
self.forwarded.remove(remote_addr)
104112

105-
def __enter__(self) -> 'SshTunnelListener':
106-
self.setup()
107-
return self
108-
109-
def __exit__(self, *args: Any) -> None:
110-
self.shutdown()
111-
112113
def setup(self) -> None:
114+
# pylint: disable=import-outside-toplevel
115+
from paramiko import SSHClient, AutoAddPolicy
116+
113117
self.ssh = SSHClient()
114118
self.ssh.load_system_host_keys()
115119
self.ssh.set_missing_host_key_policy(AutoAddPolicy())
@@ -119,14 +123,30 @@ def setup(self) -> None:
119123
username=self.flags.tunnel_username,
120124
key_filename=self.flags.tunnel_ssh_key,
121125
passphrase=self.flags.tunnel_ssh_key_passphrase,
126+
compress=True,
127+
timeout=10,
128+
auth_timeout=7,
122129
)
123-
logger.info(
124-
'SSH connection established to %s:%d...' % (
130+
logger.debug(
131+
'SSH connection established to %s:%d...'
132+
% (
125133
self.flags.tunnel_hostname,
126134
self.flags.tunnel_port,
127135
),
128136
)
129137
self.transport = self.ssh.get_transport()
138+
assert self.transport
139+
sock = cast(socket.socket, self.transport.sock) # type: ignore[redundant-cast]
140+
# Enable TCP keep-alive
141+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
142+
# Keep-alive interval (in seconds)
143+
if sys.platform != 'darwin':
144+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 30)
145+
# Keep-alive probe interval (in seconds)
146+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 5)
147+
# Number of keep-alive probes before timeout
148+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
149+
self.start_port_forward(('', self.flags.tunnel_remote_port))
130150

131151
def shutdown(self) -> None:
132152
for remote_addr in list(self.forwarded):
@@ -136,3 +156,10 @@ def shutdown(self) -> None:
136156
self.transport.close()
137157
if self.ssh is not None:
138158
self.ssh.close()
159+
self.handler.shutdown()
160+
161+
def is_alive(self) -> bool:
162+
return self.transport.is_alive() if self.transport else False
163+
164+
def is_active(self) -> bool:
165+
return self.transport.is_active() if self.transport else False

proxy/http/proxy/server.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -726,9 +726,9 @@ def generate_upstream_certificate(
726726
):
727727
raise HttpProtocolException(
728728
f'For certificate generation all the following flags are mandatory: '
729-
f'--ca-cert-file:{ self.flags.ca_cert_file }, '
730-
f'--ca-key-file:{ self.flags.ca_key_file }, '
731-
f'--ca-signing-key-file:{ self.flags.ca_signing_key_file }',
729+
f'--ca-cert-file:{ self.flags.ca_cert_file}, '
730+
f'--ca-key-file:{ self.flags.ca_key_file}, '
731+
f'--ca-signing-key-file:{ self.flags.ca_signing_key_file}',
732732
)
733733
cert_file_path = HttpProxyPlugin.generated_cert_file_path(
734734
self.flags.ca_cert_dir, text_(self.request.host),

proxy/http/websocket/frame.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ def build(self) -> bytes:
128128
)
129129
else:
130130
raise ValueError(
131-
f'Invalid payload_length { self.payload_length },'
132-
f'maximum allowed { 1 << 64 }',
131+
f'Invalid payload_length { self.payload_length},'
132+
f'maximum allowed { 1 << 64}',
133133
)
134134
if self.masked and self.data:
135135
mask = secrets.token_bytes(4) if self.mask is None else self.mask

0 commit comments

Comments
 (0)