Skip to content

Commit 8a28910

Browse files
[TlsInterception] GHA integration tests (#981)
* Add TLS interception integration tests * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fixture to gen certificate once for the `test_integration` module * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Use https endpoints in tls interception tests * Fix modify post data integration test * Only start 3 acceptor & 3 workers during integration run * disable chunk response Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 41ba504 commit 8a28910

File tree

5 files changed

+435
-9
lines changed

5 files changed

+435
-9
lines changed

tests/integration/test_integration.py

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,70 @@
1111
Test the simplest proxy use scenario for smoke.
1212
"""
1313
import time
14+
import pytest
1415
import tempfile
15-
from typing import Any, Generator
16+
1617
from pathlib import Path
18+
from typing import Any, Generator
1719
from subprocess import Popen, check_output
1820

19-
import pytest
20-
2121
from proxy.common.constants import IS_WINDOWS
2222

2323

24+
TLS_INTERCEPTION_FLAGS = ' '.join((
25+
'--ca-cert-file', 'ca-cert.pem',
26+
'--ca-key-file', 'ca-key.pem',
27+
'--ca-signing-key', 'ca-signing-key.pem',
28+
))
29+
30+
PROXY_PY_FLAGS_INTEGRATION = (
31+
('--threadless'),
32+
('--threadless --local-executor 0'),
33+
('--threaded'),
34+
)
35+
36+
PROXY_PY_FLAGS_TLS_INTERCEPTION = (
37+
('--threadless ' + TLS_INTERCEPTION_FLAGS),
38+
('--threadless --local-executor 0 ' + TLS_INTERCEPTION_FLAGS),
39+
('--threaded ' + TLS_INTERCEPTION_FLAGS),
40+
)
41+
42+
PROXY_PY_FLAGS_MODIFY_CHUNK_RESPONSE_PLUGIN = (
43+
(
44+
'--threadless --plugin proxy.plugin.ModifyChunkResponsePlugin ' +
45+
TLS_INTERCEPTION_FLAGS
46+
),
47+
(
48+
'--threadless --local-executor 0 --plugin proxy.plugin.ModifyChunkResponsePlugin ' +
49+
TLS_INTERCEPTION_FLAGS
50+
),
51+
(
52+
'--threaded --plugin proxy.plugin.ModifyChunkResponsePlugin ' +
53+
TLS_INTERCEPTION_FLAGS
54+
),
55+
)
56+
57+
PROXY_PY_FLAGS_MODIFY_POST_DATA_PLUGIN = (
58+
(
59+
'--threadless --plugin proxy.plugin.ModifyPostDataPlugin ' +
60+
TLS_INTERCEPTION_FLAGS
61+
),
62+
(
63+
'--threadless --local-executor 0 --plugin proxy.plugin.ModifyPostDataPlugin ' +
64+
TLS_INTERCEPTION_FLAGS
65+
),
66+
(
67+
'--threaded --plugin proxy.plugin.ModifyPostDataPlugin ' +
68+
TLS_INTERCEPTION_FLAGS
69+
),
70+
)
71+
72+
73+
@pytest.fixture(scope='session', autouse=True) # type: ignore[misc]
74+
def _gen_ca_certificates() -> None:
75+
check_output(['make', 'ca-certificates'])
76+
77+
2478
# FIXME: Ignore is necessary for as long as pytest hasn't figured out
2579
# FIXME: typing for their fixtures.
2680
# Refs:
@@ -42,6 +96,8 @@ def proxy_py_subprocess(request: Any) -> Generator[int, None, None]:
4296
'--port', '0',
4397
'--port-file', str(port_file),
4498
'--enable-web-server',
99+
'--num-acceptors', '3',
100+
'--num-workers', '3',
45101
) + tuple(request.param.split())
46102
proxy_proc = Popen(proxy_cmd)
47103
# Needed because port file might not be available immediately
@@ -62,11 +118,7 @@ def proxy_py_subprocess(request: Any) -> Generator[int, None, None]:
62118
@pytest.mark.smoke # type: ignore[misc]
63119
@pytest.mark.parametrize(
64120
'proxy_py_subprocess',
65-
(
66-
('--threadless'),
67-
('--threadless --local-executor 0'),
68-
('--threaded'),
69-
),
121+
PROXY_PY_FLAGS_INTEGRATION,
70122
indirect=True,
71123
) # type: ignore[misc]
72124
@pytest.mark.skipif(
@@ -78,3 +130,53 @@ def test_integration(proxy_py_subprocess: int) -> None:
78130
this_test_module = Path(__file__)
79131
shell_script_test = this_test_module.with_suffix('.sh')
80132
check_output([str(shell_script_test), str(proxy_py_subprocess)])
133+
134+
135+
@pytest.mark.smoke # type: ignore[misc]
136+
@pytest.mark.parametrize(
137+
'proxy_py_subprocess',
138+
PROXY_PY_FLAGS_TLS_INTERCEPTION,
139+
indirect=True,
140+
) # type: ignore[misc]
141+
@pytest.mark.skipif(
142+
IS_WINDOWS,
143+
reason='OSError: [WinError 193] %1 is not a valid Win32 application',
144+
) # type: ignore[misc]
145+
def test_integration_with_interception_flags(proxy_py_subprocess: int) -> None:
146+
"""An acceptance test for TLS interception using ``curl`` through proxy.py."""
147+
shell_script_test = Path(__file__).parent / 'test_interception.sh'
148+
check_output([str(shell_script_test), str(proxy_py_subprocess)])
149+
150+
151+
# @pytest.mark.smoke # type: ignore[misc]
152+
# @pytest.mark.parametrize(
153+
# 'proxy_py_subprocess',
154+
# PROXY_PY_FLAGS_MODIFY_CHUNK_RESPONSE_PLUGIN,
155+
# indirect=True,
156+
# ) # type: ignore[misc]
157+
# @pytest.mark.skipif(
158+
# IS_WINDOWS,
159+
# reason='OSError: [WinError 193] %1 is not a valid Win32 application',
160+
# ) # type: ignore[misc]
161+
# def test_modify_chunk_response_integration(proxy_py_subprocess: int) -> None:
162+
# """An acceptance test for :py:class:`~proxy.plugin.ModifyChunkResponsePlugin`
163+
# interception using ``curl`` through proxy.py."""
164+
# shell_script_test = Path(__file__).parent / 'test_modify_chunk_response.sh'
165+
# check_output([str(shell_script_test), str(proxy_py_subprocess)])
166+
167+
168+
@pytest.mark.smoke # type: ignore[misc]
169+
@pytest.mark.parametrize(
170+
'proxy_py_subprocess',
171+
PROXY_PY_FLAGS_MODIFY_POST_DATA_PLUGIN,
172+
indirect=True,
173+
) # type: ignore[misc]
174+
@pytest.mark.skipif(
175+
IS_WINDOWS,
176+
reason='OSError: [WinError 193] %1 is not a valid Win32 application',
177+
) # type: ignore[misc]
178+
def test_modify_post_response_integration(proxy_py_subprocess: int) -> None:
179+
"""An acceptance test for :py:class:`~proxy.plugin.ModifyPostDataPlugin`
180+
interception using ``curl`` through proxy.py."""
181+
shell_script_test = Path(__file__).parent / 'test_modify_post_data.sh'
182+
check_output([str(shell_script_test), str(proxy_py_subprocess)])

tests/integration/test_integration.sh

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
#!/bin/bash
2-
2+
#
3+
# proxy.py
4+
# ~~~~~~~~
5+
# ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable
6+
# proxy server for Application debugging, testing and development.
7+
#
8+
# :copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
# :license: BSD, see LICENSE for more details.
10+
#
311
# TODO: Option to also shutdown proxy.py after
412
# integration testing is done. At least on
513
# macOS and ubuntu, pkill and kill commands
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/bin/bash
2+
#
3+
# proxy.py
4+
# ~~~~~~~~
5+
# ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable
6+
# proxy server for Application debugging, testing and development.
7+
#
8+
# :copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
# :license: BSD, see LICENSE for more details.
10+
#
11+
# TODO: Option to also shutdown proxy.py after
12+
# integration testing is done. At least on
13+
# macOS and ubuntu, pkill and kill commands
14+
# will do the job.
15+
#
16+
# For github action, we simply bank upon GitHub
17+
# to clean up any background process including
18+
# proxy.py
19+
20+
PROXY_PY_PORT=$1
21+
if [[ -z "$PROXY_PY_PORT" ]]; then
22+
echo "PROXY_PY_PORT required as argument."
23+
exit 1
24+
fi
25+
26+
PROXY_URL="127.0.0.1:$PROXY_PY_PORT"
27+
28+
# Wait for server to come up
29+
WAIT_FOR_PROXY="lsof -i TCP:$PROXY_PY_PORT | wc -l | tr -d ' '"
30+
while true; do
31+
if [[ $WAIT_FOR_PORT == 0 ]]; then
32+
echo "Waiting for proxy..."
33+
sleep 1
34+
else
35+
break
36+
fi
37+
done
38+
39+
# Wait for http proxy and web server to start
40+
while true; do
41+
curl -v \
42+
--max-time 1 \
43+
--connect-timeout 1 \
44+
-x $PROXY_URL \
45+
--cacert ca-cert.pem \
46+
http://$PROXY_URL/ 2>/dev/null
47+
if [[ $? == 0 ]]; then
48+
break
49+
fi
50+
echo "Waiting for web server to start accepting requests..."
51+
sleep 1
52+
done
53+
54+
verify_response() {
55+
if [ "$1" == "" ];
56+
then
57+
echo "Empty response";
58+
return 1;
59+
else
60+
if [ "$1" == "$2" ];
61+
then
62+
echo "Ok";
63+
return 0;
64+
else
65+
echo "Invalid response: '$1', expected: '$2'";
66+
return 1;
67+
fi
68+
fi;
69+
}
70+
71+
# Check if proxy was started with integration
72+
# testing web server plugin. If detected, use
73+
# internal web server for integration testing.
74+
75+
# If integration testing plugin is not found,
76+
# detect if we have internet access. If we do,
77+
# then use httpbin.org for integration testing.
78+
read -r -d '' ROBOTS_RESPONSE << EOM
79+
User-agent: *
80+
Disallow: /deny
81+
EOM
82+
83+
echo "[Test HTTP Request via Proxy]"
84+
CMD="curl -v -x $PROXY_URL --cacert ca-cert.pem http://httpbin.org/robots.txt"
85+
RESPONSE=$($CMD 2> /dev/null)
86+
verify_response "$RESPONSE" "$ROBOTS_RESPONSE"
87+
VERIFIED1=$?
88+
89+
echo "[Test HTTPS Request via Proxy]"
90+
CMD="curl -v -x $PROXY_URL --cacert ca-cert.pem https://httpbin.org/robots.txt"
91+
RESPONSE=$($CMD 2> /dev/null)
92+
verify_response "$RESPONSE" "$ROBOTS_RESPONSE"
93+
VERIFIED2=$?
94+
95+
echo "[Test Internal Web Server via Proxy]"
96+
curl -v \
97+
-x $PROXY_URL \
98+
--cacert ca-cert.pem \
99+
http://$PROXY_URL/
100+
VERIFIED3=$?
101+
102+
SHASUM=sha256sum
103+
if [ "$(uname)" = "Darwin" ];
104+
then
105+
SHASUM="shasum -a 256"
106+
fi
107+
108+
echo "[Test Download File Hash Verifies 1]"
109+
touch downloaded.hash
110+
echo "3d1921aab49d3464a712c1c1397b6babf8b461a9873268480aa8064da99441bc -" > downloaded.hash
111+
curl -vL \
112+
-o downloaded.whl \
113+
-x $PROXY_URL \
114+
--cacert ca-cert.pem \
115+
https://files.pythonhosted.org/packages/88/78/e642316313b1cd6396e4b85471a316e003eff968f29773e95ea191ea1d08/proxy.py-2.4.0rc4-py3-none-any.whl#sha256=3d1921aab49d3464a712c1c1397b6babf8b461a9873268480aa8064da99441bc
116+
cat downloaded.whl | $SHASUM -c downloaded.hash
117+
VERIFIED4=$?
118+
rm downloaded.whl downloaded.hash
119+
120+
echo "[Test Download File Hash Verifies 2]"
121+
touch downloaded.hash
122+
echo "077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8 -" > downloaded.hash
123+
curl -vL \
124+
-o downloaded.whl \
125+
-x $PROXY_URL \
126+
--cacert ca-cert.pem \
127+
https://files.pythonhosted.org/packages/20/9a/e5d9ec41927401e41aea8af6d16e78b5e612bca4699d417f646a9610a076/Jinja2-3.0.3-py3-none-any.whl#sha256=077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8
128+
cat downloaded.whl | $SHASUM -c downloaded.hash
129+
VERIFIED5=$?
130+
rm downloaded.whl downloaded.hash
131+
132+
EXIT_CODE=$(( $VERIFIED1 || $VERIFIED2 || $VERIFIED3 || $VERIFIED4 || $VERIFIED5 ))
133+
exit $EXIT_CODE
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
#!/bin/bash
2+
#
3+
# proxy.py
4+
# ~~~~~~~~
5+
# ⚡⚡⚡ Fast, Lightweight, Programmable, TLS interception capable
6+
# proxy server for Application debugging, testing and development.
7+
#
8+
# :copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
# :license: BSD, see LICENSE for more details.
10+
#
11+
# TODO: Option to also shutdown proxy.py after
12+
# integration testing is done. At least on
13+
# macOS and ubuntu, pkill and kill commands
14+
# will do the job.
15+
#
16+
# For github action, we simply bank upon GitHub
17+
# to clean up any background process including
18+
# proxy.py
19+
20+
PROXY_PY_PORT=$1
21+
if [[ -z "$PROXY_PY_PORT" ]]; then
22+
echo "PROXY_PY_PORT required as argument."
23+
exit 1
24+
fi
25+
26+
PROXY_URL="127.0.0.1:$PROXY_PY_PORT"
27+
28+
# Wait for server to come up
29+
WAIT_FOR_PROXY="lsof -i TCP:$PROXY_PY_PORT | wc -l | tr -d ' '"
30+
while true; do
31+
if [[ $WAIT_FOR_PORT == 0 ]]; then
32+
echo "Waiting for proxy..."
33+
sleep 1
34+
else
35+
break
36+
fi
37+
done
38+
39+
# Wait for http proxy and web server to start
40+
while true; do
41+
curl -v \
42+
--max-time 1 \
43+
--connect-timeout 1 \
44+
-x $PROXY_URL \
45+
--cacert ca-cert.pem \
46+
http://$PROXY_URL/ 2>/dev/null
47+
if [[ $? == 0 ]]; then
48+
break
49+
fi
50+
echo "Waiting for web server to start accepting requests..."
51+
sleep 1
52+
done
53+
54+
verify_response() {
55+
if [ "$1" == "" ];
56+
then
57+
echo "Empty response";
58+
return 1;
59+
else
60+
if [ "$1" == "$2" ];
61+
then
62+
echo "Ok";
63+
return 0;
64+
else
65+
echo "Invalid response: '$1', expected: '$2'";
66+
return 1;
67+
fi
68+
fi;
69+
}
70+
71+
read -r -d '' MODIFIED_CHUNK_RESPONSE << EOM
72+
modify
73+
chunk
74+
response
75+
plugin
76+
EOM
77+
78+
echo "[Test ModifyChunkResponsePlugin]"
79+
CMD="curl -v -x $PROXY_URL --cacert ca-cert.pem https://httpbin.org/stream/5"
80+
RESPONSE=$($CMD 2> /dev/null)
81+
verify_response "$RESPONSE" "$MODIFIED_CHUNK_RESPONSE"
82+
VERIFIED1=$?
83+
84+
EXIT_CODE=$(( $VERIFIED1 ))
85+
exit $EXIT_CODE

0 commit comments

Comments
 (0)