Skip to content

Commit 558a430

Browse files
v2.4.0rc9 (#1068)
* [Core] Invoke selector.close on shutdown (#1055) [Core] Invoke `selector.close` on shutdown * [CacheResponsesPlugin] Add ability to cache request packets (#1056) * Start of post encryption tests * Assertion on post encryption callback * Add `--cache-requests` flag * Clean up `on_client_data` API as we no longer have a chain within core http protocol handler * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix flake8 warnings * Fix `inconsistent-return-statements` Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * [Task] A generic payload based work abstraction (#1057) * Refactor into an internal task submodule of work * As context managers * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Add missing license Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * `jupyter` notebook based tutorial (#1059) * `jupyter` notebook based tutorial * Move within `tutorial` directory * Fix spell * Add `as_non_blocking` option during wrap * `as_non_blocking=False` by defaut * `--max-sendbuf-size` flag to speed up large file upload/download (#1060) * Add `--max-sendbuf-size` flag which now defaults to 64Kb * Use `server_recvbuf_size` flag with base tunnel implementation * isort * Add to readme * [Flags] `min_compression_length` consistency (#1061) * `min_compression_length` consistency, it was used as `min_compression_limit` at a few places * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * revert back web server route * Move `serve_static_file` as a staticmethod within web plugin base Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * [Fix] Using `okResponse()` without content hangs the connection (#1062) * It hangs because of no content-length or connection close header * Fix tests * [Jupyter] Add a response notebook (#1064) * Add a response generation jupyter notebook * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Make codespell happy * precommit codespell exclude Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * [Optimize] Avoid using `tobytes` for zero-copies (#1066) * Avoid using `tobytes` where possible * `send` accepts `Union[memoryview, bytes]` now * [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> * [HttpParser] Memory view compliant, Zero copies (#1067) * Remove usage of `tobytes` * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix chunk parser * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Lint fixes Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> * [Jupyter] Request creation notebook (#1065) * Add plugin tests, responses notebook and enhancements to `build_http_packet` * Add js code snip for ws example * Fix tests * ignore all ipynb from codespell * ignore all ipynb from codespell * Fix tests and doc spell Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2 parents 3858f3a + 3fd608e commit 558a430

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2212
-542
lines changed

.pre-commit-config.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,12 @@ repos:
125125
rev: v2.1.0
126126
hooks:
127127
- id: codespell
128-
exclude: >-
129-
^.+\.min\.js$
128+
exclude: >
129+
(?x)^(
130+
^.+\.ipynb$|
131+
tests/http/test_responses\.py|
132+
^.+\.min\.js$
133+
)$
130134
131135
- repo: https://github.com/adrienverge/yamllint.git
132136
rev: v1.26.2

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ lib-clean:
9494
rm -rf .hypothesis
9595
# Doc RST files are cached and can cause issues
9696
# See https://github.com/abhinavsingh/proxy.py/issues/642#issuecomment-1003444578
97-
rm docs/pkg/*.rst
97+
rm -f docs/pkg/*.rst
9898

9999
lib-dep:
100100
pip install --upgrade pip && \
@@ -134,7 +134,7 @@ lib-doc:
134134
python -m tox -e build-docs && \
135135
$(OPEN) .tox/build-docs/docs_out/index.html || true
136136

137-
lib-coverage:
137+
lib-coverage: lib-clean
138138
pytest --cov=proxy --cov=tests --cov-report=html tests/ && \
139139
$(OPEN) htmlcov/index.html || true
140140

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@
250250
- See `--enable-static-server` and `--static-server-dir` flags
251251

252252
- Optimized for large file uploads and downloads
253-
- See `--client-recvbuf-size` and `--server-recvbuf-size` flag
253+
- See `--client-recvbuf-size`, `--server-recvbuf-size`, `--max-sendbuf-size` flags
254254

255255
- `IPv4` and `IPv6` support
256256
- See `--hostname` flag
@@ -710,6 +710,8 @@ Start `proxy.py` as:
710710
--plugins proxy.plugin.CacheResponsesPlugin
711711
```
712712

713+
You may also use the `--cache-requests` flag to enable request packet caching for inspection.
714+
713715
Verify using `curl -v -x localhost:8899 http://httpbin.org/get`:
714716

715717
```console
@@ -2278,13 +2280,14 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
22782280
[--work-klass WORK_KLASS] [--pid-file PID_FILE]
22792281
[--enable-proxy-protocol] [--enable-conn-pool] [--key-file KEY_FILE]
22802282
[--cert-file CERT_FILE] [--client-recvbuf-size CLIENT_RECVBUF_SIZE]
2281-
[--server-recvbuf-size SERVER_RECVBUF_SIZE] [--timeout TIMEOUT]
2283+
[--server-recvbuf-size SERVER_RECVBUF_SIZE]
2284+
[--max-sendbuf-size MAX_SENDBUF_SIZE] [--timeout TIMEOUT]
22822285
[--disable-http-proxy] [--disable-headers DISABLE_HEADERS]
22832286
[--ca-key-file CA_KEY_FILE] [--ca-cert-dir CA_CERT_DIR]
22842287
[--ca-cert-file CA_CERT_FILE] [--ca-file CA_FILE]
22852288
[--ca-signing-key-file CA_SIGNING_KEY_FILE]
22862289
[--auth-plugin AUTH_PLUGIN] [--cache-dir CACHE_DIR]
2287-
[--proxy-pool PROXY_POOL] [--enable-web-server]
2290+
[--cache-requests] [--proxy-pool PROXY_POOL] [--enable-web-server]
22882291
[--enable-static-server] [--static-server-dir STATIC_SERVER_DIR]
22892292
[--min-compression-length MIN_COMPRESSION_LENGTH]
22902293
[--enable-reverse-proxy] [--pac-file PAC_FILE]
@@ -2294,7 +2297,7 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
22942297
[--filtered-client-ips FILTERED_CLIENT_IPS]
22952298
[--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
22962299

2297-
proxy.py v2.4.0rc8.dev17+g59a4335.d20220123
2300+
proxy.py v2.4.0rc9.dev8+gea0253d.d20220126
22982301

22992302
options:
23002303
-h, --help show this help message and exit
@@ -2389,6 +2392,9 @@ options:
23892392
--server-recvbuf-size SERVER_RECVBUF_SIZE
23902393
Default: 128 KB. Maximum amount of data received from
23912394
the server in a single recv() operation.
2395+
--max-sendbuf-size MAX_SENDBUF_SIZE
2396+
Default: 64 KB. Maximum amount of data to dispatch in
2397+
a single send() operation.
23922398
--timeout TIMEOUT Default: 10.0. Number of seconds after which an
23932399
inactive connection must be dropped. Inactivity is
23942400
defined by no data sent or received by the client.
@@ -2425,6 +2431,7 @@ options:
24252431
Default: /Users/abhinavsingh/.proxy/cache. Flag only
24262432
applicable when cache plugin is used with on-disk
24272433
storage.
2434+
--cache-requests Default: False. Whether to also cache request packets.
24282435
--proxy-pool PROXY_POOL
24292436
List of upstream proxies to use in the pool
24302437
--enable-web-server Default: False. Whether to enable

examples/task.py

Lines changed: 37 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,116 +8,64 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11-
import time
11+
import sys
1212
import argparse
13-
import threading
14-
import multiprocessing
15-
from typing import Any
1613

17-
from proxy.core.work import (
18-
Work, ThreadlessPool, BaseLocalExecutor, BaseRemoteExecutor,
19-
)
14+
from proxy.core.work import ThreadlessPool
2015
from proxy.common.flag import FlagParser
21-
from proxy.common.backports import NonBlockingQueue
22-
23-
24-
class Task:
25-
"""This will be our work object."""
26-
27-
def __init__(self, payload: bytes) -> None:
28-
self.payload = payload
29-
print(payload)
30-
31-
32-
class TaskWork(Work[Task]):
33-
"""This will be our handler class, created for each received work."""
34-
35-
@staticmethod
36-
def create(*args: Any) -> Task:
37-
"""Work core doesn't know how to create work objects for us, so
38-
we must provide an implementation of create method here."""
39-
return Task(*args)
40-
41-
42-
class LocalTaskExecutor(BaseLocalExecutor):
43-
"""We'll define a local executor which is capable of receiving
44-
log lines over a non blocking queue."""
45-
46-
def work(self, *args: Any) -> None:
47-
task_id = int(time.time())
48-
uid = '%s-%s' % (self.iid, task_id)
49-
self.works[task_id] = self.create(uid, *args)
50-
51-
52-
class RemoteTaskExecutor(BaseRemoteExecutor):
53-
54-
def work(self, *args: Any) -> None:
55-
task_id = int(time.time())
56-
uid = '%s-%s' % (self.iid, task_id)
57-
self.works[task_id] = self.create(uid, *args)
58-
59-
60-
def start_local(flags: argparse.Namespace) -> None:
61-
work_queue = NonBlockingQueue()
62-
executor = LocalTaskExecutor(iid=1, work_queue=work_queue, flags=flags)
16+
from proxy.core.work.task import (
17+
RemoteTaskExecutor, ThreadedTaskExecutor, SingleProcessTaskExecutor,
18+
)
6319

64-
t = threading.Thread(target=executor.run)
65-
t.daemon = True
66-
t.start()
6720

68-
try:
21+
def start_local_thread(flags: argparse.Namespace) -> None:
22+
with ThreadedTaskExecutor(flags=flags) as thread:
6923
i = 0
7024
while True:
71-
work_queue.put(('%d' % i).encode('utf-8'))
25+
thread.executor.work_queue.put(('%d' % i).encode('utf-8'))
7226
i += 1
73-
except KeyboardInterrupt:
74-
pass
75-
finally:
76-
executor.running.set()
77-
t.join()
7827

7928

80-
def start_remote(flags: argparse.Namespace) -> None:
81-
pipe = multiprocessing.Pipe()
82-
work_queue = pipe[0]
83-
executor = RemoteTaskExecutor(iid=1, work_queue=pipe[1], flags=flags)
29+
def start_remote_process(flags: argparse.Namespace) -> None:
30+
with SingleProcessTaskExecutor(flags=flags) as process:
31+
i = 0
32+
while True:
33+
process.work_queue.send(('%d' % i).encode('utf-8'))
34+
i += 1
8435

85-
p = multiprocessing.Process(target=executor.run)
86-
p.daemon = True
87-
p.start()
8836

89-
try:
37+
def start_remote_pool(flags: argparse.Namespace) -> None:
38+
with ThreadlessPool(flags=flags, executor_klass=RemoteTaskExecutor) as pool:
9039
i = 0
9140
while True:
41+
work_queue = pool.work_queues[i % flags.num_workers]
9242
work_queue.send(('%d' % i).encode('utf-8'))
9343
i += 1
94-
except KeyboardInterrupt:
95-
pass
96-
finally:
97-
executor.running.set()
98-
p.join()
9944

10045

101-
def start_remote_pool(flags: argparse.Namespace) -> None:
102-
with ThreadlessPool(flags=flags, executor_klass=RemoteTaskExecutor) as pool:
103-
try:
104-
i = 0
105-
while True:
106-
work_queue = pool.work_queues[i % flags.num_workers]
107-
work_queue.send(('%d' % i).encode('utf-8'))
108-
i += 1
109-
except KeyboardInterrupt:
110-
pass
46+
def main() -> None:
47+
try:
48+
flags = FlagParser.initialize(
49+
sys.argv[2:] + ['--disable-http-proxy'],
50+
work_klass='proxy.core.work.task.TaskHandler',
51+
)
52+
globals()['start_%s' % sys.argv[1]](flags)
53+
except KeyboardInterrupt:
54+
pass
11155

11256

11357
# TODO: TaskWork, LocalTaskExecutor, RemoteTaskExecutor
11458
# should not be needed, abstract those pieces out in the core
11559
# for stateless tasks.
11660
if __name__ == '__main__':
117-
flags = FlagParser.initialize(
118-
['--disable-http-proxy'],
119-
work_klass=TaskWork,
120-
)
121-
start_remote_pool(flags)
122-
# start_remote(flags)
123-
# start_local(flags)
61+
if len(sys.argv) < 2:
62+
print(
63+
'\n'.join([
64+
'Usage:',
65+
' %s <execution-mode>' % sys.argv[0],
66+
' execution-mode can be one of the following:',
67+
' "remote_pool", "remote_process", "local_thread"',
68+
]),
69+
)
70+
sys.exit(1)
71+
main()

proxy/common/constants.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def _env_threadless_compliant() -> bool:
7474
# Defaults
7575
DEFAULT_BACKLOG = 100
7676
DEFAULT_BASIC_AUTH = None
77+
DEFAULT_MAX_SEND_SIZE = 64 * 1024
7778
DEFAULT_BUFFER_SIZE = 128 * 1024
7879
DEFAULT_CA_CERT_DIR = None
7980
DEFAULT_CA_CERT_FILE = None
@@ -124,14 +125,13 @@ def _env_threadless_compliant() -> bool:
124125
DEFAULT_PORT = 8899
125126
DEFAULT_SERVER_RECVBUF_SIZE = DEFAULT_BUFFER_SIZE
126127
DEFAULT_STATIC_SERVER_DIR = os.path.join(PROXY_PY_DIR, "public")
127-
DEFAULT_MIN_COMPRESSION_LIMIT = 20 # In bytes
128+
DEFAULT_MIN_COMPRESSION_LENGTH = 20 # In bytes
128129
DEFAULT_THREADLESS = _env_threadless_compliant()
129130
DEFAULT_LOCAL_EXECUTOR = True
130131
DEFAULT_TIMEOUT = 10.0
131132
DEFAULT_VERSION = False
132133
DEFAULT_HTTP_PORT = 80
133134
DEFAULT_HTTPS_PORT = 443
134-
DEFAULT_MAX_SEND_SIZE = 16 * 1024
135135
DEFAULT_WORK_KLASS = 'proxy.http.HttpProtocolHandler'
136136
DEFAULT_ENABLE_PROXY_PROTOCOL = False
137137
# 25 milliseconds to keep the loops hot
@@ -148,6 +148,7 @@ def _env_threadless_compliant() -> bool:
148148
DEFAULT_CACHE_DIRECTORY_PATH = os.path.join(
149149
DEFAULT_DATA_DIRECTORY_PATH, 'cache',
150150
)
151+
DEFAULT_CACHE_REQUESTS = False
151152

152153
# Cor plugins enabled by default or via flags
153154
DEFAULT_ABC_PLUGINS = [

proxy/common/flag.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
PLUGIN_REVERSE_PROXY, DEFAULT_NUM_ACCEPTORS, PLUGIN_INSPECT_TRAFFIC,
3131
DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE, DEFAULT_DEVTOOLS_WS_PATH,
3232
PLUGIN_DEVTOOLS_PROTOCOL, PLUGIN_WEBSOCKET_TRANSPORT,
33-
DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_MIN_COMPRESSION_LIMIT,
33+
DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_MIN_COMPRESSION_LENGTH,
3434
)
3535

3636

@@ -335,13 +335,13 @@ def initialize(
335335
args.static_server_dir,
336336
),
337337
)
338-
args.min_compression_limit = cast(
338+
args.min_compression_length = cast(
339339
bool,
340340
opts.get(
341-
'min_compression_limit',
341+
'min_compression_length',
342342
getattr(
343-
args, 'min_compression_limit',
344-
DEFAULT_MIN_COMPRESSION_LIMIT,
343+
args, 'min_compression_length',
344+
DEFAULT_MIN_COMPRESSION_LENGTH,
345345
),
346346
),
347347
)

proxy/common/utils.py

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from .types import HostPort
2727
from .constants import (
2828
CRLF, COLON, HTTP_1_1, IS_WINDOWS, WHITESPACE, DEFAULT_TIMEOUT,
29-
DEFAULT_THREADLESS,
29+
DEFAULT_THREADLESS, PROXY_AGENT_HEADER_VALUE,
3030
)
3131

3232

@@ -84,14 +84,30 @@ def bytes_(s: Any, encoding: str = 'utf-8', errors: str = 'strict') -> Any:
8484
def build_http_request(
8585
method: bytes, url: bytes,
8686
protocol_version: bytes = HTTP_1_1,
87+
content_type: Optional[bytes] = None,
8788
headers: Optional[Dict[bytes, bytes]] = None,
8889
body: Optional[bytes] = None,
8990
conn_close: bool = False,
91+
no_ua: bool = False,
9092
) -> bytes:
9193
"""Build and returns a HTTP request packet."""
94+
headers = headers or {}
95+
if content_type is not None:
96+
headers[b'Content-Type'] = content_type
97+
has_transfer_encoding = False
98+
has_user_agent = False
99+
for k, _ in headers.items():
100+
if k.lower() == b'transfer-encoding':
101+
has_transfer_encoding = True
102+
elif k.lower() == b'user-agent':
103+
has_user_agent = True
104+
if body and not has_transfer_encoding:
105+
headers[b'Content-Length'] = bytes_(len(body))
106+
if not has_user_agent and not no_ua:
107+
headers[b'User-Agent'] = PROXY_AGENT_HEADER_VALUE
92108
return build_http_pkt(
93109
[method, url, protocol_version],
94-
headers or {},
110+
headers,
95111
body,
96112
conn_close,
97113
)
@@ -109,19 +125,14 @@ def build_http_response(
109125
line = [protocol_version, bytes_(status_code)]
110126
if reason:
111127
line.append(reason)
112-
if headers is None:
113-
headers = {}
114-
has_content_length = False
128+
headers = headers or {}
115129
has_transfer_encoding = False
116130
for k, _ in headers.items():
117-
if k.lower() == b'content-length':
118-
has_content_length = True
119131
if k.lower() == b'transfer-encoding':
120132
has_transfer_encoding = True
121-
if body is not None and \
122-
not has_transfer_encoding and \
123-
not has_content_length:
124-
headers[b'Content-Length'] = bytes_(len(body))
133+
break
134+
if not has_transfer_encoding:
135+
headers[b'Content-Length'] = bytes_(len(body)) if body else b'0'
125136
return build_http_pkt(line, headers, body, conn_close)
126137

127138

proxy/core/acceptor/acceptor.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def run(self) -> None:
178178
for fileno in self.socks:
179179
self.socks[fileno].close()
180180
self.socks.clear()
181+
self.selector.close()
181182
logger.debug('Acceptor#%d shutdown', self.idd)
182183

183184
def _recv_and_setup_socks(self) -> None:
@@ -207,7 +208,8 @@ def _start_local(self) -> None:
207208
self._lthread.start()
208209

209210
def _stop_local(self) -> None:
210-
if self._lthread is not None and self._local_work_queue is not None:
211+
if self._lthread is not None and \
212+
self._local_work_queue is not None:
211213
self._local_work_queue.put(False)
212214
self._lthread.join()
213215

0 commit comments

Comments
 (0)