Skip to content

Commit ddf90fb

Browse files
Allow --plugins flag to be used multiple times (#725)
* deprecate server_file_or_404 * Optionally compress static content. Currently only if content length higher than 300 * trailing comma * Allow `--plugins` flag to be used multiple times Following are valid invocation: 1) `--plugins A` 2) `--plugins A,B` 3) `--plugins A --plugins B` 4) `--plugins A,B --plugins C` * mypy * Flake8 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * correct type * Add `HttpParser.is_https_tunnel()` utility method * mypy * lint checks * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent e38f1a8 commit ddf90fb

File tree

14 files changed

+174
-121
lines changed

14 files changed

+174
-121
lines changed

README.md

+55-54
Original file line numberDiff line numberDiff line change
@@ -1941,24 +1941,24 @@ for list of tests.
19411941

19421942
```console
19431943
proxy -h
1944-
usage: -m [-h] [--enable-events] [--enable-conn-pool] [--threadless] [--threaded]
1945-
[--num-workers NUM_WORKERS] [--pid-file PID_FILE] [--backlog BACKLOG]
1946-
[--hostname HOSTNAME] [--port PORT] [--num-acceptors NUM_ACCEPTORS]
1947-
[--unix-socket-path UNIX_SOCKET_PATH]
1948-
[--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--key-file KEY_FILE]
1949-
[--timeout TIMEOUT] [--version] [--log-level LOG_LEVEL]
1944+
usage: proxy [-h] [--enable-events] [--enable-conn-pool] [--threadless] [--threaded]
1945+
[--num-workers NUM_WORKERS] [--backlog BACKLOG] [--hostname HOSTNAME]
1946+
[--port PORT] [--unix-socket-path UNIX_SOCKET_PATH]
1947+
[--num-acceptors NUM_ACCEPTORS] [--version] [--log-level LOG_LEVEL]
19501948
[--log-file LOG_FILE] [--log-format LOG_FORMAT]
1951-
[--open-file-limit OPEN_FILE_LIMIT] [--plugins PLUGINS]
1952-
[--enable-dashboard] [--work-klass WORK_KLASS] [--disable-http-proxy]
1953-
[--ca-key-file CA_KEY_FILE] [--ca-cert-dir CA_CERT_DIR]
1954-
[--ca-cert-file CA_CERT_FILE] [--ca-file CA_FILE]
1955-
[--ca-signing-key-file CA_SIGNING_KEY_FILE] [--cert-file CERT_FILE]
1956-
[--disable-headers DISABLE_HEADERS]
1949+
[--open-file-limit OPEN_FILE_LIMIT] [--plugins PLUGINS [PLUGINS ...]]
1950+
[--enable-dashboard] [--work-klass WORK_KLASS] [--pid-file PID_FILE]
1951+
[--client-recvbuf-size CLIENT_RECVBUF_SIZE] [--key-file KEY_FILE]
1952+
[--timeout TIMEOUT] [--disable-http-proxy] [--ca-key-file CA_KEY_FILE]
1953+
[--ca-cert-dir CA_CERT_DIR] [--ca-cert-file CA_CERT_FILE]
1954+
[--ca-file CA_FILE] [--ca-signing-key-file CA_SIGNING_KEY_FILE]
1955+
[--cert-file CERT_FILE] [--disable-headers DISABLE_HEADERS]
19571956
[--server-recvbuf-size SERVER_RECVBUF_SIZE] [--basic-auth BASIC_AUTH]
19581957
[--cache-dir CACHE_DIR]
19591958
[--filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS] [--enable-web-server]
19601959
[--enable-static-server] [--static-server-dir STATIC_SERVER_DIR]
1961-
[--pac-file PAC_FILE] [--pac-file-url-path PAC_FILE_URL_PATH]
1960+
[--min-compression-length MIN_COMPRESSION_LENGTH] [--pac-file PAC_FILE]
1961+
[--pac-file-url-path PAC_FILE_URL_PATH]
19621962
[--filtered-client-ips FILTERED_CLIENT_IPS]
19631963
[--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
19641964
[--cloudflare-dns-mode CLOUDFLARE_DNS_MODE]
@@ -1971,50 +1971,51 @@ options:
19711971
Plugins can be used to subscribe for core events.
19721972
--enable-conn-pool Default: False. (WIP) Enable upstream connection pooling.
19731973
--threadless Default: True. Enabled by default on Python 3.8+ (mac,
1974-
linux). When disabled a new thread is spawned to handle
1975-
each client connection.
1974+
linux). When disabled a new thread is spawned to handle each
1975+
client connection.
19761976
--threaded Default: False. Disabled by default on Python < 3.8 and
1977-
windows. When enabled a new thread is spawned to handle
1978-
each client connection.
1977+
windows. When enabled a new thread is spawned to handle each
1978+
client connection.
19791979
--num-workers NUM_WORKERS
19801980
Defaults to number of CPU cores.
1981-
--pid-file PID_FILE Default: None. Save parent process ID to a file.
1982-
--backlog BACKLOG Default: 100. Maximum number of pending connections to
1983-
proxy server
1981+
--backlog BACKLOG Default: 100. Maximum number of pending connections to proxy
1982+
server
19841983
--hostname HOSTNAME Default: ::1. Server IP address.
19851984
--port PORT Default: 8899. Server port.
1985+
--unix-socket-path UNIX_SOCKET_PATH
1986+
Default: None. Unix socket path to use. When provided --host
1987+
and --port flags are ignored
19861988
--num-acceptors NUM_ACCEPTORS
19871989
Defaults to number of CPU cores.
1988-
--unix-socket-path UNIX_SOCKET_PATH
1989-
Default: None. Unix socket path to use. When provided
1990-
--host and --port flags are ignored
1991-
--client-recvbuf-size CLIENT_RECVBUF_SIZE
1992-
Default: 1 MB. Maximum amount of data received from the
1993-
client in a single recv() operation. Bump this value for
1994-
faster uploads at the expense of increased RAM.
1995-
--key-file KEY_FILE Default: None. Server key file to enable end-to-end TLS
1996-
encryption with clients. If used, must also pass --cert-
1997-
file.
1998-
--timeout TIMEOUT Default: 10.0. Number of seconds after which an inactive
1999-
connection must be dropped. Inactivity is defined by no
2000-
data sent or received by the client.
20011990
--version, -v Prints proxy.py version.
20021991
--log-level LOG_LEVEL
20031992
Valid options: DEBUG, INFO (default), WARNING, ERROR,
20041993
CRITICAL. Both upper and lowercase values are allowed. You
2005-
may also simply use the leading character e.g. --log-level
2006-
d
1994+
may also simply use the leading character e.g. --log-level d
20071995
--log-file LOG_FILE Default: sys.stdout. Log file destination.
20081996
--log-format LOG_FORMAT
20091997
Log format for Python logger.
20101998
--open-file-limit OPEN_FILE_LIMIT
20111999
Default: 1024. Maximum number of files (TCP connections)
20122000
that proxy.py can open concurrently.
2013-
--plugins PLUGINS Comma separated plugins
2001+
--plugins PLUGINS [PLUGINS ...]
2002+
Comma separated plugins. You may use --plugins flag multiple
2003+
times.
20142004
--enable-dashboard Default: False. Enables proxy.py dashboard.
20152005
--work-klass WORK_KLASS
2016-
Default: proxy.http.handler.HttpProtocolHandler. Work klass
2017-
to use for work execution.
2006+
Default: proxy.http.HttpProtocolHandler. Work klass to use
2007+
for work execution.
2008+
--pid-file PID_FILE Default: None. Save "parent" process ID to a file.
2009+
--client-recvbuf-size CLIENT_RECVBUF_SIZE
2010+
Default: 1 MB. Maximum amount of data received from the
2011+
client in a single recv() operation. Bump this value for
2012+
faster uploads at the expense of increased RAM.
2013+
--key-file KEY_FILE Default: None. Server key file to enable end-to-end TLS
2014+
encryption with clients. If used, must also pass --cert-
2015+
file.
2016+
--timeout TIMEOUT Default: 10.0. Number of seconds after which an inactive
2017+
connection must be dropped. Inactivity is defined by no data
2018+
sent or received by the client.
20182019
--disable-http-proxy Default: False. Whether to disable proxy.HttpProxyPlugin.
20192020
--ca-key-file CA_KEY_FILE
20202021
Default: None. CA key to use for signing dynamically
@@ -2026,19 +2027,18 @@ options:
20262027
file and --ca-signing-key-file
20272028
--ca-cert-file CA_CERT_FILE
20282029
Default: None. Signing certificate to use for signing
2029-
dynamically generated HTTPS certificates. If used, must
2030-
also pass --ca-key-file and --ca-signing-key-file
2031-
--ca-file CA_FILE Default: /Users/abhinavsingh/Dev/proxy.py/venv310/lib/pytho
2032-
n3.10/site-packages/certifi/cacert.pem. Provide path to
2030+
dynamically generated HTTPS certificates. If used, must also
2031+
pass --ca-key-file and --ca-signing-key-file
2032+
--ca-file CA_FILE Default: /Users/abhinavsingh/Dev/proxy.py/venv310/lib/python
2033+
3.10/site-packages/certifi/cacert.pem. Provide path to
20332034
custom CA bundle for peer certificate verification
20342035
--ca-signing-key-file CA_SIGNING_KEY_FILE
20352036
Default: None. CA signing key to use for dynamic generation
2036-
of HTTPS certificates. If used, must also pass --ca-key-
2037-
file and --ca-cert-file
2037+
of HTTPS certificates. If used, must also pass --ca-key-file
2038+
and --ca-cert-file
20382039
--cert-file CERT_FILE
20392040
Default: None. Server certificate to enable end-to-end TLS
2040-
encryption with clients. If used, must also pass --key-
2041-
file.
2041+
encryption with clients. If used, must also pass --key-file.
20422042
--disable-headers DISABLE_HEADERS
20432043
Default: None. Comma separated list of headers to remove
20442044
before dispatching client request to upstream server.
@@ -2055,8 +2055,7 @@ options:
20552055
--filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS
20562056
Default: Blocks Facebook. Comma separated list of IPv4 and
20572057
IPv6 addresses.
2058-
--enable-web-server Default: False. Whether to enable
2059-
proxy.HttpWebServerPlugin.
2058+
--enable-web-server Default: False. Whether to enable proxy.HttpWebServerPlugin.
20602059
--enable-static-server
20612060
Default: False. Enable inbuilt static file server.
20622061
Optionally, also use --static-server-dir to serve static
@@ -2065,11 +2064,14 @@ options:
20652064
folder.
20662065
--static-server-dir STATIC_SERVER_DIR
20672066
Default: "public" folder in directory where proxy.py is
2068-
placed. This option is only applicable when static server
2069-
is also enabled. See --enable-static-server.
2067+
placed. This option is only applicable when static server is
2068+
also enabled. See --enable-static-server.
2069+
--min-compression-length MIN_COMPRESSION_LENGTH
2070+
Default: 20 bytes. Sets the minimum length of a response
2071+
that will be compressed (gzipped).
20702072
--pac-file PAC_FILE A file (Proxy Auto Configuration) or string to serve when
2071-
the server receives a direct file request. Using this
2072-
option enables proxy.HttpWebServerPlugin.
2073+
the server receives a direct file request. Using this option
2074+
enables proxy.HttpWebServerPlugin.
20732075
--pac-file-url-path PAC_FILE_URL_PATH
20742076
Default: /. Web server path to serve the PAC file.
20752077
--filtered-client-ips FILTERED_CLIENT_IPS
@@ -2083,8 +2085,7 @@ options:
20832085
protection) or "family" (for malware and adult content
20842086
protection)
20852087

2086-
Proxy.py not working? Report at:
2087-
https://github.com/abhinavsingh/proxy.py/issues/new
2088+
Proxy.py not working? Report at: https://github.com/abhinavsingh/proxy.py/issues/new
20882089
```
20892090

20902091
# Changelog

examples/https_connect_tunnel.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from proxy.common.utils import build_http_response
1717
from proxy.http.codes import httpStatusCodes
1818
from proxy.http.parser import httpParserStates
19-
from proxy.http.methods import httpMethods
2019
from proxy.core.base import BaseTcpTunnelHandler
2120

2221

@@ -51,7 +50,7 @@ def handle_data(self, data: memoryview) -> Optional[bool]:
5150
self.request.parse(data)
5251

5352
# Drop the request if not a CONNECT request
54-
if self.request.method != httpMethods.CONNECT:
53+
if not self.request.is_https_tunnel():
5554
self.work.queue(
5655
HttpsConnectTunnelHandler.PROXY_TUNNEL_UNSUPPORTED_SCHEME,
5756
)

proxy/common/constants.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import sysconfig
1717
import ipaddress
1818

19-
from typing import List
19+
from typing import Any, List
2020

2121
from .version import __version__
2222

@@ -93,10 +93,11 @@ def _env_threadless_compliant() -> bool:
9393
DEFAULT_PAC_FILE = None
9494
DEFAULT_PAC_FILE_URL_PATH = b'/'
9595
DEFAULT_PID_FILE = None
96-
DEFAULT_PLUGINS = ''
96+
DEFAULT_PLUGINS: List[Any] = []
9797
DEFAULT_PORT = 8899
9898
DEFAULT_SERVER_RECVBUF_SIZE = DEFAULT_BUFFER_SIZE
9999
DEFAULT_STATIC_SERVER_DIR = os.path.join(PROXY_PY_DIR, "public")
100+
DEFAULT_MIN_COMPRESSION_LIMIT = 20 # In bytes
100101
DEFAULT_THREADLESS = _env_threadless_compliant()
101102
DEFAULT_TIMEOUT = 10.0
102103
DEFAULT_VERSION = False

proxy/common/flag.py

+20-9
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121

2222
from .plugins import Plugins
2323
from .types import IpAddress
24-
from .utils import text_, bytes_, is_py2, set_open_file_limit
24+
from .utils import bytes_, is_py2, set_open_file_limit
2525
from .constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_NUM_ACCEPTORS, DEFAULT_NUM_WORKERS
2626
from .constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE
27-
from .constants import PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL
27+
from .constants import PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL, DEFAULT_MIN_COMPRESSION_LIMIT
2828
from .constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE
2929
from .constants import PLUGIN_WEB_SERVER, PLUGIN_PROXY_AUTH
3030
from .logger import Logger
@@ -110,6 +110,9 @@ def initialize(
110110
# proxy.py currently cannot serve over HTTPS and also perform TLS interception
111111
# at the same time. Check if user is trying to enable both feature
112112
# at the same time.
113+
#
114+
# TODO: Use parser.add_mutually_exclusive_group()
115+
# and remove this logic from here.
113116
if (args.cert_file and args.key_file) and \
114117
(args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file):
115118
print(
@@ -140,18 +143,16 @@ def initialize(
140143
bytes_(p)
141144
for p in FlagParser.get_default_plugins(args)
142145
]
143-
extra_plugins = [
144-
p if isinstance(p, type) else bytes_(p)
145-
for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
146-
if not (isinstance(p, str) and len(p) == 0)
147-
]
148-
plugins = Plugins.load(default_plugins + extra_plugins)
146+
plugins = Plugins.load(
147+
default_plugins + Plugins.resolve_plugin_flag(
148+
args.plugins, opts.get('plugins', None),
149+
),
150+
)
149151

150152
# https://github.com/python/mypy/issues/5865
151153
#
152154
# def option(t: object, key: str, default: Any) -> Any:
153155
# return cast(t, opts.get(key, default))
154-
155156
args.work_klass = work_klass
156157
args.plugins = plugins
157158
args.auth_code = cast(
@@ -284,6 +285,16 @@ def initialize(
284285
args.enable_static_server,
285286
),
286287
)
288+
args.min_compression_limit = cast(
289+
bool,
290+
opts.get(
291+
'min_compression_limit',
292+
getattr(
293+
args, 'min_compression_limit',
294+
DEFAULT_MIN_COMPRESSION_LIMIT,
295+
),
296+
),
297+
)
287298
args.devtools_ws_path = cast(
288299
bytes,
289300
opts.get(

proxy/common/plugins.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,38 @@
1212
import abc
1313
import logging
1414
import inspect
15+
import itertools
1516
import importlib
1617

1718
from typing import Any, List, Dict, Optional, Union
1819

1920
from .utils import bytes_, text_
20-
from .constants import DOT, DEFAULT_ABC_PLUGINS
21+
from .constants import DOT, DEFAULT_ABC_PLUGINS, COMMA
2122

2223
logger = logging.getLogger(__name__)
2324

2425

2526
class Plugins:
2627
"""Common utilities for plugin discovery."""
2728

29+
@staticmethod
30+
def resolve_plugin_flag(flag_plugins: Any, opt_plugins: Optional[Any] = None) -> List[Union[bytes, type]]:
31+
if isinstance(flag_plugins, list):
32+
requested_plugins = list(
33+
itertools.chain.from_iterable([
34+
p.split(text_(COMMA)) for p in list(
35+
itertools.chain.from_iterable(flag_plugins),
36+
)
37+
]),
38+
)
39+
else:
40+
requested_plugins = flag_plugins.split(text_(COMMA))
41+
return [
42+
p if isinstance(p, type) else bytes_(p)
43+
for p in (opt_plugins if opt_plugins is not None else requested_plugins)
44+
if not (isinstance(p, str) and len(p) == 0)
45+
]
46+
2847
@staticmethod
2948
def discover(input_args: List[str]) -> None:
3049
"""Search for plugin and plugins flag in command line arguments,

proxy/core/connection/connection.py

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def connection(self) -> Union[ssl.SSLSocket, socket.socket]:
5656

5757
def send(self, data: bytes) -> int:
5858
"""Users must handle BrokenPipeError exceptions"""
59+
# logger.info(data)
5960
return self.connection.send(data)
6061

6162
def recv(

proxy/dashboard/dashboard.py

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ def handle_request(self, request: HttpParser) -> None:
6767
self.flags.static_server_dir,
6868
'dashboard', 'proxy.html',
6969
),
70+
self.flags.min_compression_limit,
7071
),
7172
)
7273
elif request.path in (

proxy/http/parser.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -138,11 +138,14 @@ def set_url(self, url: bytes) -> None:
138138
# For CONNECT requests, request line contains
139139
# upstream_host:upstream_port which is not complaint
140140
# with urlsplit, which expects a fully qualified url.
141-
if self.method == httpMethods.CONNECT:
141+
if self.is_https_tunnel():
142142
url = b'https://' + url
143143
self._url = urlparse.urlsplit(url)
144144
self._set_line_attributes()
145145

146+
def is_https_tunnel(self) -> bool:
147+
return self.method == httpMethods.CONNECT
148+
146149
def is_chunked_encoded(self) -> bool:
147150
return b'transfer-encoding' in self.headers and \
148151
self.headers[b'transfer-encoding'][1].lower() == b'chunked'
@@ -184,7 +187,7 @@ def build(self, disable_headers: Optional[List[bytes]] = None, for_proxy: bool =
184187
COLON +
185188
str(self.port).encode() +
186189
self.path
187-
) if self.method != httpMethods.CONNECT else (self.host + COLON + str(self.port).encode())
190+
) if not self.is_https_tunnel() else (self.host + COLON + str(self.port).encode())
188191
return build_http_request(
189192
self.method, path, self.version,
190193
headers={} if not self.headers else {
@@ -305,7 +308,7 @@ def _get_body_or_chunks(self) -> Optional[bytes]:
305308

306309
def _set_line_attributes(self) -> None:
307310
if self.type == httpParserTypes.REQUEST_PARSER:
308-
if self.method == httpMethods.CONNECT and self._url:
311+
if self.is_https_tunnel() and self._url:
309312
self.host = self._url.hostname
310313
self.port = 443 if self._url.port is None else self._url.port
311314
elif self._url:

0 commit comments

Comments
 (0)