Skip to content

Commit c936f7e

Browse files
Response Packet Utilities (#903)
* Add response pkt utility * Unused import * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix tests as some content is now by default gzipped based upon min compression config * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove unused * Update necessary tests to use `okResponse` utility * Add option to explicitly disable compression Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 2d83857 commit c936f7e

21 files changed

+294
-283
lines changed

.github/workflows/test-library.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,8 +247,10 @@ jobs:
247247
path: ${{ steps.pip-cache.outputs.dir }}
248248
key: >-
249249
${{ runner.os }}-pip-${{
250-
steps.calc-cache-key-py.outputs.py-hash-key }}-${{
251-
hashFiles('tox.ini') }}
250+
steps.calc-cache-key-py.outputs.py-hash-key
251+
}}-${{
252+
hashFiles('tox.ini')
253+
}}
252254
restore-keys: |
253255
${{ runner.os }}-pip-${{
254256
steps.calc-cache-key-py.outputs.py-hash-key
@@ -370,8 +372,10 @@ jobs:
370372
path: ${{ steps.pip-cache.outputs.dir }}
371373
key: >-
372374
${{ runner.os }}-pip-${{
373-
steps.calc-cache-key-py.outputs.py-hash-key }}-${{
374-
hashFiles('tox.ini') }}
375+
steps.calc-cache-key-py.outputs.py-hash-key
376+
}}-${{
377+
hashFiles('tox.ini')
378+
}}
375379
restore-keys: |
376380
${{ runner.os }}-pip-${{
377381
steps.calc-cache-key-py.outputs.py-hash-key
@@ -701,6 +705,8 @@ jobs:
701705
steps:
702706
- name: Checkout
703707
uses: actions/checkout@v2
708+
with:
709+
ref: ${{ github.event.inputs.release-commitish }}
704710
- name: Download all the dists
705711
uses: actions/download-artifact@v2
706712
with:

examples/https_connect_tunnel.py

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,18 @@
1313
from typing import Any, Optional
1414

1515
from proxy import Proxy
16-
from proxy.common.utils import build_http_response
17-
from proxy.http import httpStatusCodes
16+
17+
from proxy.http.responses import (
18+
PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT,
19+
PROXY_TUNNEL_UNSUPPORTED_SCHEME,
20+
)
21+
1822
from proxy.core.base import BaseTcpTunnelHandler
1923

2024

2125
class HttpsConnectTunnelHandler(BaseTcpTunnelHandler):
2226
"""A https CONNECT tunnel."""
2327

24-
PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(
25-
build_http_response(
26-
httpStatusCodes.OK,
27-
reason=b'Connection established',
28-
),
29-
)
30-
31-
PROXY_TUNNEL_UNSUPPORTED_SCHEME = memoryview(
32-
build_http_response(
33-
httpStatusCodes.BAD_REQUEST,
34-
reason=b'Unsupported protocol scheme',
35-
conn_close=True,
36-
),
37-
)
38-
3928
def __init__(self, *args: Any, **kwargs: Any) -> None:
4029
super().__init__(*args, **kwargs)
4130

@@ -50,9 +39,7 @@ def handle_data(self, data: memoryview) -> Optional[bool]:
5039

5140
# Drop the request if not a CONNECT request
5241
if not self.request.is_https_tunnel:
53-
self.work.queue(
54-
HttpsConnectTunnelHandler.PROXY_TUNNEL_UNSUPPORTED_SCHEME,
55-
)
42+
self.work.queue(PROXY_TUNNEL_UNSUPPORTED_SCHEME)
5643
return True
5744

5845
# CONNECT requests are short and we need not worry about
@@ -63,9 +50,7 @@ def handle_data(self, data: memoryview) -> Optional[bool]:
6350
self.connect_upstream()
6451

6552
# Queue tunnel established response to client
66-
self.work.queue(
67-
HttpsConnectTunnelHandler.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT,
68-
)
53+
self.work.queue(PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT)
6954

7055
return None
7156

proxy/dashboard/dashboard.py

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
from .plugin import ProxyDashboardWebsocketPlugin
1717

18-
from ..common.utils import build_http_response, bytes_
18+
from ..common.utils import bytes_
1919

20-
from ..http import httpStatusCodes
20+
from ..http.responses import permanentRedirectResponse
2121
from ..http.parser import HttpParser
2222
from ..http.websocket import WebsocketFrame
2323
from ..http.server import HttpWebServerPlugin, HttpWebServerBasePlugin, httpProtocolTypes
@@ -69,25 +69,13 @@ def handle_request(self, request: HttpParser) -> None:
6969
self.flags.static_server_dir,
7070
'dashboard', 'proxy.html',
7171
),
72-
self.flags.min_compression_limit,
7372
),
7473
)
7574
elif request.path in (
7675
b'/dashboard',
7776
b'/dashboard/proxy.html',
7877
):
79-
self.client.queue(
80-
memoryview(
81-
build_http_response(
82-
httpStatusCodes.PERMANENT_REDIRECT, reason=b'Permanent Redirect',
83-
headers={
84-
b'Location': b'/dashboard/',
85-
b'Content-Length': b'0',
86-
},
87-
conn_close=True,
88-
),
89-
),
90-
)
78+
self.client.queue(permanentRedirectResponse(b'/dashboard/'))
9179

9280
def on_websocket_open(self) -> None:
9381
logger.info('app ws opened')

proxy/http/exception/proxy_auth_failed.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717

1818
from .base import HttpProtocolException
1919

20-
from ..codes import httpStatusCodes
21-
22-
from ...common.constants import PROXY_AGENT_HEADER_VALUE, PROXY_AGENT_HEADER_KEY
23-
from ...common.utils import build_http_response
20+
from ..responses import PROXY_AUTH_FAILED_RESPONSE_PKT
2421

2522
if TYPE_CHECKING:
2623
from ..parser import HttpParser
@@ -30,21 +27,8 @@ class ProxyAuthenticationFailed(HttpProtocolException):
3027
"""Exception raised when HTTP Proxy auth is enabled and
3128
incoming request doesn't present necessary credentials."""
3229

33-
RESPONSE_PKT = memoryview(
34-
build_http_response(
35-
httpStatusCodes.PROXY_AUTH_REQUIRED,
36-
reason=b'Proxy Authentication Required',
37-
headers={
38-
PROXY_AGENT_HEADER_KEY: PROXY_AGENT_HEADER_VALUE,
39-
b'Proxy-Authenticate': b'Basic',
40-
},
41-
body=b'Proxy Authentication Required',
42-
conn_close=True,
43-
),
44-
)
45-
4630
def __init__(self, **kwargs: Any) -> None:
4731
super().__init__(self.__class__.__name__, **kwargs)
4832

4933
def response(self, _request: 'HttpParser') -> memoryview:
50-
return self.RESPONSE_PKT
34+
return PROXY_AUTH_FAILED_RESPONSE_PKT

proxy/http/exception/proxy_conn_failed.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@
1616

1717
from .base import HttpProtocolException
1818

19-
from ..codes import httpStatusCodes
20-
21-
from ...common.constants import PROXY_AGENT_HEADER_VALUE, PROXY_AGENT_HEADER_KEY
22-
from ...common.utils import build_http_response
19+
from ..responses import BAD_GATEWAY_RESPONSE_PKT
2320

2421
if TYPE_CHECKING:
2522
from ..parser import HttpParser
@@ -28,23 +25,11 @@
2825
class ProxyConnectionFailed(HttpProtocolException):
2926
"""Exception raised when ``HttpProxyPlugin`` is unable to establish connection to upstream server."""
3027

31-
RESPONSE_PKT = memoryview(
32-
build_http_response(
33-
httpStatusCodes.BAD_GATEWAY,
34-
reason=b'Bad Gateway',
35-
headers={
36-
PROXY_AGENT_HEADER_KEY: PROXY_AGENT_HEADER_VALUE,
37-
},
38-
body=b'Bad Gateway',
39-
conn_close=True,
40-
),
41-
)
42-
4328
def __init__(self, host: str, port: int, reason: str, **kwargs: Any):
4429
self.host: str = host
4530
self.port: int = port
4631
self.reason: str = reason
4732
super().__init__('%s %s' % (self.__class__.__name__, reason), **kwargs)
4833

4934
def response(self, _request: 'HttpParser') -> memoryview:
50-
return self.RESPONSE_PKT
35+
return BAD_GATEWAY_RESPONSE_PKT

proxy/http/proxy/server.py

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@
2828

2929
from ..headers import httpHeaders
3030
from ..methods import httpMethods
31-
from ..codes import httpStatusCodes
3231
from ..plugin import HttpProtocolHandlerPlugin
3332
from ..exception import HttpProtocolException, ProxyConnectionFailed
3433
from ..parser import HttpParser, httpParserStates, httpParserTypes
34+
from ..responses import PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT
3535

3636
from ...common.types import Readables, Writables
3737
from ...common.constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_FILE
@@ -40,7 +40,7 @@
4040
from ...common.constants import PROXY_AGENT_HEADER_VALUE, DEFAULT_DISABLE_HEADERS
4141
from ...common.constants import DEFAULT_HTTP_ACCESS_LOG_FORMAT, DEFAULT_HTTPS_ACCESS_LOG_FORMAT
4242
from ...common.constants import DEFAULT_DISABLE_HTTP_PROXY, PLUGIN_PROXY_AUTH
43-
from ...common.utils import build_http_response, text_
43+
from ...common.utils import text_
4444
from ...common.pki import gen_public_key, gen_csr, sign_csr
4545

4646
from ...core.event import eventNames
@@ -136,13 +136,6 @@
136136
class HttpProxyPlugin(HttpProtocolHandlerPlugin):
137137
"""HttpProtocolHandler plugin which implements HttpProxy specifications."""
138138

139-
PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(
140-
build_http_response(
141-
httpStatusCodes.OK,
142-
reason=b'Connection established',
143-
),
144-
)
145-
146139
# Used to synchronization during certificate generation and
147140
# connection pool operations.
148141
lock = threading.Lock()
@@ -546,9 +539,7 @@ def on_request_complete(self) -> Union[socket.socket, bool]:
546539
# Optionally, setup interceptor if TLS interception is enabled.
547540
if self.upstream:
548541
if self.request.is_https_tunnel:
549-
self.client.queue(
550-
HttpProxyPlugin.PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT,
551-
)
542+
self.client.queue(PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT)
552543
if self.tls_interception_enabled():
553544
return self.intercept()
554545
# If an upstream server connection was established for http request,

proxy/http/responses.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
proxy.py
4+
~~~~~~~~
5+
⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
6+
Network monitoring, controls & Application development, testing, debugging.
7+
8+
:copyright: (c) 2013-present by Abhinav Singh and contributors.
9+
:license: BSD, see LICENSE for more details.
10+
"""
11+
import gzip
12+
from typing import Any, Dict, Optional
13+
14+
from ..common.flag import flags
15+
from ..common.utils import build_http_response
16+
from ..common.constants import PROXY_AGENT_HEADER_VALUE, PROXY_AGENT_HEADER_KEY
17+
18+
from .codes import httpStatusCodes
19+
20+
21+
PROXY_TUNNEL_ESTABLISHED_RESPONSE_PKT = memoryview(
22+
build_http_response(
23+
httpStatusCodes.OK,
24+
reason=b'Connection established',
25+
),
26+
)
27+
28+
PROXY_TUNNEL_UNSUPPORTED_SCHEME = memoryview(
29+
build_http_response(
30+
httpStatusCodes.BAD_REQUEST,
31+
reason=b'Unsupported protocol scheme',
32+
conn_close=True,
33+
),
34+
)
35+
36+
PROXY_AUTH_FAILED_RESPONSE_PKT = memoryview(
37+
build_http_response(
38+
httpStatusCodes.PROXY_AUTH_REQUIRED,
39+
reason=b'Proxy Authentication Required',
40+
headers={
41+
PROXY_AGENT_HEADER_KEY: PROXY_AGENT_HEADER_VALUE,
42+
b'Proxy-Authenticate': b'Basic',
43+
},
44+
body=b'Proxy Authentication Required',
45+
conn_close=True,
46+
),
47+
)
48+
49+
NOT_FOUND_RESPONSE_PKT = memoryview(
50+
build_http_response(
51+
httpStatusCodes.NOT_FOUND,
52+
reason=b'NOT FOUND',
53+
headers={
54+
b'Server': PROXY_AGENT_HEADER_VALUE,
55+
b'Content-Length': b'0',
56+
},
57+
conn_close=True,
58+
),
59+
)
60+
61+
NOT_IMPLEMENTED_RESPONSE_PKT = memoryview(
62+
build_http_response(
63+
httpStatusCodes.NOT_IMPLEMENTED,
64+
reason=b'NOT IMPLEMENTED',
65+
headers={
66+
b'Server': PROXY_AGENT_HEADER_VALUE,
67+
b'Content-Length': b'0',
68+
},
69+
conn_close=True,
70+
),
71+
)
72+
73+
BAD_GATEWAY_RESPONSE_PKT = memoryview(
74+
build_http_response(
75+
httpStatusCodes.BAD_GATEWAY,
76+
reason=b'Bad Gateway',
77+
headers={
78+
PROXY_AGENT_HEADER_KEY: PROXY_AGENT_HEADER_VALUE,
79+
},
80+
body=b'Bad Gateway',
81+
conn_close=True,
82+
),
83+
)
84+
85+
86+
def okResponse(
87+
content: Optional[bytes] = None,
88+
headers: Optional[Dict[bytes, bytes]] = None,
89+
compress: bool = True,
90+
**kwargs: Any,
91+
) -> memoryview:
92+
do_compress: bool = False
93+
if compress and flags.args and content and len(content) > flags.args.min_compression_limit:
94+
do_compress = True
95+
if not headers:
96+
headers = {}
97+
headers.update({
98+
b'Content-Encoding': b'gzip',
99+
})
100+
return memoryview(
101+
build_http_response(
102+
200,
103+
reason=b'OK',
104+
headers=headers,
105+
body=gzip.compress(content)
106+
if do_compress and content
107+
else content,
108+
**kwargs,
109+
),
110+
)
111+
112+
113+
def permanentRedirectResponse(location: bytes) -> memoryview:
114+
return memoryview(
115+
build_http_response(
116+
httpStatusCodes.PERMANENT_REDIRECT,
117+
reason=b'Permanent Redirect',
118+
headers={
119+
b'Location': location,
120+
b'Content-Length': b'0',
121+
},
122+
conn_close=True,
123+
),
124+
)
125+
126+
127+
def seeOthersResponse(location: bytes) -> memoryview:
128+
return memoryview(
129+
build_http_response(
130+
httpStatusCodes.SEE_OTHER,
131+
reason=b'See Other',
132+
headers={
133+
b'Location': location,
134+
b'Content-Length': b'0',
135+
},
136+
conn_close=True,
137+
),
138+
)

0 commit comments

Comments
 (0)