Skip to content

Commit 0ffa7ca

Browse files
[TlsInterception] Fix broken ChunkedResponsePlugin for Python < 3.10 (#986)
* Add TLS interception integration tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix CI * unused * Well 3.9 just worked locally * Dispatching empty byte to client results in OSError for Python < 3.10 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Uncomment old integration tests Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2714d3d commit 0ffa7ca

File tree

12 files changed

+72
-51
lines changed

12 files changed

+72
-51
lines changed

Makefile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ HTTPS_CERT_FILE_PATH := https-cert.pem
1212
HTTPS_CSR_FILE_PATH := https-csr.pem
1313
HTTPS_SIGNED_CERT_FILE_PATH := https-signed-cert.pem
1414

15-
CA_KEY_FILE_PATH := ca-key.pem
16-
CA_CERT_FILE_PATH := ca-cert.pem
17-
CA_SIGNING_KEY_FILE_PATH := ca-signing-key.pem
15+
CA_CERT_SUFFIX :=
16+
CA_KEY_FILE_PATH := ca-key$(CA_CERT_SUFFIX).pem
17+
CA_CERT_FILE_PATH := ca-cert$(CA_CERT_SUFFIX).pem
18+
CA_SIGNING_KEY_FILE_PATH := ca-signing-key$(CA_CERT_SUFFIX).pem
1819

1920
# Dummy invalid hardcoded value
2021
PROXYPY_PKG_PATH := dist/proxy.py.whl

proxy/common/flag.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,14 @@ def initialize(
242242
),
243243
)
244244
args.disable_headers = disabled_headers if disabled_headers is not None else DEFAULT_DISABLE_HEADERS
245+
245246
args.certfile = cast(
246247
Optional[str], opts.get(
247248
'cert_file', args.cert_file,
248249
),
249250
)
250251
args.keyfile = cast(Optional[str], opts.get('key_file', args.key_file))
252+
251253
args.ca_key_file = cast(
252254
Optional[str], opts.get(
253255
'ca_key_file', args.ca_key_file,
@@ -272,6 +274,7 @@ def initialize(
272274
args.ca_file,
273275
),
274276
)
277+
275278
args.hostname = cast(
276279
IpAddress,
277280
opts.get('hostname', ipaddress.ip_address(args.hostname)),

proxy/core/connection/client.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ def wrap(self, keyfile: str, certfile: str) -> None:
4646
self._conn = ssl.wrap_socket(
4747
self.connection,
4848
server_side=True,
49-
# ca_certs=self.flags.ca_cert_file,
5049
certfile=certfile,
5150
keyfile=keyfile,
5251
ssl_version=ssl.PROTOCOL_TLS,

proxy/http/proxy/plugin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,14 @@ def handle_client_request(
131131
# No longer abstract since 2.4.0
132132
#
133133
# @abstractmethod
134-
def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
134+
def handle_upstream_chunk(self, chunk: memoryview) -> Optional[memoryview]:
135135
"""Handler called right after receiving raw response from upstream server.
136136
137137
For HTTPS connections, chunk will be encrypted unless
138-
TLS interception is also enabled."""
138+
TLS interception is also enabled.
139+
140+
Return None if you don't want to sent this chunk to the client.
141+
"""
139142
return chunk # pragma: no cover
140143

141144
# No longer abstract since 2.4.0

proxy/http/proxy/server.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -280,24 +280,29 @@ async def read_from_descriptors(self, r: Readables) -> bool:
280280

281281
for plugin in self.plugins.values():
282282
raw = plugin.handle_upstream_chunk(raw)
283+
if raw is None:
284+
break
283285

284286
# parse incoming response packet
285287
# only for non-https requests and when
286288
# tls interception is enabled
287-
if not self.request.is_https_tunnel \
288-
or self.tls_interception_enabled:
289-
if self.response.is_complete:
290-
self.handle_pipeline_response(raw)
289+
if raw is not None:
290+
if (
291+
not self.request.is_https_tunnel
292+
or self.tls_interception_enabled
293+
):
294+
if self.response.is_complete:
295+
self.handle_pipeline_response(raw)
296+
else:
297+
# TODO(abhinavsingh): Remove .tobytes after parser is
298+
# memoryview compliant
299+
chunk = raw.tobytes()
300+
self.response.parse(chunk)
301+
self.emit_response_events(len(chunk))
291302
else:
292-
# TODO(abhinavsingh): Remove .tobytes after parser is
293-
# memoryview compliant
294-
chunk = raw.tobytes()
295-
self.response.parse(chunk)
296-
self.emit_response_events(len(chunk))
297-
else:
298-
self.response.total_size += len(raw)
299-
# queue raw data for client
300-
self.client.queue(raw)
303+
self.response.total_size += len(raw)
304+
# queue raw data for client
305+
self.client.queue(raw)
301306
return False
302307

303308
def on_client_connection_close(self) -> None:

proxy/plugin/cache/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def handle_client_request(
5454
assert self.store
5555
return self.store.cache_request(request)
5656

57-
def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
57+
def handle_upstream_chunk(self, chunk: memoryview) -> Optional[memoryview]:
5858
assert self.store
5959
return self.store.cache_response_chunk(chunk)
6060

proxy/plugin/man_in_the_middle.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11+
from typing import Optional
12+
1113
from ..http.responses import okResponse
1214
from ..http.proxy import HttpProxyBasePlugin
1315

1416

1517
class ManInTheMiddlePlugin(HttpProxyBasePlugin):
1618
"""Modifies upstream server responses."""
1719

18-
def handle_upstream_chunk(self, _chunk: memoryview) -> memoryview:
20+
def handle_upstream_chunk(self, _chunk: memoryview) -> Optional[memoryview]:
1921
return okResponse(content=b'Hello from man in the middle')

proxy/plugin/modify_chunk_response.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
:copyright: (c) 2013-present by Abhinav Singh and contributors.
99
:license: BSD, see LICENSE for more details.
1010
"""
11-
from typing import Any
11+
from typing import Any, Optional
1212

1313
from ..http.parser import HttpParser, httpParserTypes
1414
from ..http.proxy import HttpProxyBasePlugin
@@ -29,7 +29,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
2929
# Create a new http protocol parser for response payloads
3030
self.response = HttpParser(httpParserTypes.RESPONSE_PARSER)
3131

32-
def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
32+
def handle_upstream_chunk(self, chunk: memoryview) -> Optional[memoryview]:
3333
# Parse the response.
3434
# Note that these chunks also include headers
3535
self.response.parse(chunk.tobytes())
@@ -40,4 +40,4 @@ def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
4040
if self.response.body_expected:
4141
self.response.body = b'\n'.join(self.DEFAULT_CHUNKS) + b'\n'
4242
self.client.queue(memoryview(self.response.build_response()))
43-
return memoryview(b'')
43+
return None

proxy/plugin/proxy_pool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def handle_client_data(self, raw: memoryview) -> Optional[memoryview]:
182182
self.upstream.queue(raw)
183183
return raw
184184

185-
def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
185+
def handle_upstream_chunk(self, chunk: memoryview) -> Optional[memoryview]:
186186
"""Will never be called since we didn't establish an upstream connection."""
187187
if not self.upstream:
188188
return chunk

tests/integration/test_integration.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
1111
Test the simplest proxy use scenario for smoke.
1212
"""
13-
import sys
1413
import time
1514
import pytest
1615
import tempfile
16+
import subprocess
1717

1818
from pathlib import Path
1919
from typing import Any, Generator
@@ -22,11 +22,13 @@
2222
from proxy.common.constants import IS_WINDOWS
2323

2424

25-
TLS_INTERCEPTION_FLAGS = ' '.join((
26-
'--ca-cert-file', 'ca-cert.pem',
27-
'--ca-key-file', 'ca-key.pem',
28-
'--ca-signing-key', 'ca-signing-key.pem',
29-
))
25+
def _tls_interception_flags(ca_cert_suffix: str = '') -> str:
26+
return ' '.join((
27+
'--ca-cert-file', 'ca-cert%s.pem' % ca_cert_suffix,
28+
'--ca-key-file', 'ca-key%s.pem' % ca_cert_suffix,
29+
'--ca-signing-key', 'ca-signing-key%s.pem' % ca_cert_suffix,
30+
))
31+
3032

3133
PROXY_PY_FLAGS_INTEGRATION = (
3234
('--threadless'),
@@ -35,45 +37,55 @@
3537
)
3638

3739
PROXY_PY_FLAGS_TLS_INTERCEPTION = (
38-
('--threadless ' + TLS_INTERCEPTION_FLAGS),
39-
('--threadless --local-executor 0 ' + TLS_INTERCEPTION_FLAGS),
40-
('--threaded ' + TLS_INTERCEPTION_FLAGS),
40+
('--threadless ' + _tls_interception_flags()),
41+
('--threadless --local-executor 0 ' + _tls_interception_flags()),
42+
('--threaded ' + _tls_interception_flags()),
4143
)
4244

4345
PROXY_PY_FLAGS_MODIFY_CHUNK_RESPONSE_PLUGIN = (
4446
(
4547
'--threadless --plugin proxy.plugin.ModifyChunkResponsePlugin ' +
46-
TLS_INTERCEPTION_FLAGS
48+
_tls_interception_flags('-chunk')
4749
),
4850
(
4951
'--threadless --local-executor 0 --plugin proxy.plugin.ModifyChunkResponsePlugin ' +
50-
TLS_INTERCEPTION_FLAGS
52+
_tls_interception_flags('-chunk')
5153
),
5254
(
5355
'--threaded --plugin proxy.plugin.ModifyChunkResponsePlugin ' +
54-
TLS_INTERCEPTION_FLAGS
56+
_tls_interception_flags('-chunk')
5557
),
5658
)
5759

5860
PROXY_PY_FLAGS_MODIFY_POST_DATA_PLUGIN = (
5961
(
6062
'--threadless --plugin proxy.plugin.ModifyPostDataPlugin ' +
61-
TLS_INTERCEPTION_FLAGS
63+
_tls_interception_flags('-post')
6264
),
6365
(
6466
'--threadless --local-executor 0 --plugin proxy.plugin.ModifyPostDataPlugin ' +
65-
TLS_INTERCEPTION_FLAGS
67+
_tls_interception_flags('-post')
6668
),
6769
(
6870
'--threaded --plugin proxy.plugin.ModifyPostDataPlugin ' +
69-
TLS_INTERCEPTION_FLAGS
71+
_tls_interception_flags('-post')
7072
),
7173
)
7274

7375

7476
@pytest.fixture(scope='session', autouse=True) # type: ignore[misc]
75-
def _gen_ca_certificates() -> None:
76-
check_output(['make', 'ca-certificates'])
77+
def _gen_ca_certificates(request: Any) -> None:
78+
check_output([
79+
'make', 'ca-certificates',
80+
])
81+
check_output([
82+
'make', 'ca-certificates',
83+
'-e', 'CA_CERT_SUFFIX=-chunk',
84+
])
85+
check_output([
86+
'make', 'ca-certificates',
87+
'-e', 'CA_CERT_SUFFIX=-post',
88+
])
7789

7890

7991
# FIXME: Ignore is necessary for as long as pytest hasn't figured out
@@ -104,7 +116,7 @@ def proxy_py_subprocess(request: Any) -> Generator[int, None, None]:
104116
'--ca-cert-dir', str(ca_cert_dir),
105117
'--log-level', 'd',
106118
) + tuple(request.param.split())
107-
proxy_proc = Popen(proxy_cmd)
119+
proxy_proc = Popen(proxy_cmd, stderr=subprocess.STDOUT)
108120
# Needed because port file might not be available immediately
109121
while not port_file.exists():
110122
time.sleep(1)
@@ -163,10 +175,6 @@ def test_integration_with_interception_flags(proxy_py_subprocess: int) -> None:
163175
IS_WINDOWS,
164176
reason='OSError: [WinError 193] %1 is not a valid Win32 application',
165177
) # type: ignore[misc]
166-
@pytest.mark.skipif(
167-
sys.version_info < (3, 10),
168-
reason='For version < 3.10, GHA integration run into OSError when flushing to clients',
169-
) # type: ignore[misc]
170178
def test_modify_chunk_response_integration(proxy_py_subprocess: int) -> None:
171179
"""An acceptance test for :py:class:`~proxy.plugin.ModifyChunkResponsePlugin`
172180
interception using ``curl`` through proxy.py."""

0 commit comments

Comments
 (0)