Skip to content

Commit ea227b1

Browse files
authored
Document FilterByClientIpPlugin & ModifyChunkResponsePlugin (#387)
1 parent 167afbf commit ea227b1

File tree

4 files changed

+119
-2
lines changed

4 files changed

+119
-2
lines changed

README.md

+53
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Table of Contents
5757
* [Cache Responses Plugin](#cacheresponsesplugin)
5858
* [Man-In-The-Middle Plugin](#maninthemiddleplugin)
5959
* [Proxy Pool Plugin](#proxypoolplugin)
60+
* [FilterByClientIpPlugin](#filterbyclientipplugin)
61+
* [ModifyChunkResponsePlugin](#modifychunkresponseplugin)
6062
* [HTTP Web Server Plugins](#http-web-server-plugins)
6163
* [Reverse Proxy](#reverse-proxy)
6264
* [Web Server Route](#web-server-route)
@@ -669,6 +671,57 @@ Make a curl request via `8899` proxy:
669671
Verify that `8899` proxy forwards requests to upstream proxies
670672
by checking respective logs.
671673
674+
### FilterByClientIpPlugin
675+
676+
Reject traffic from specific IP addresses. By default this
677+
plugin blocks traffic from `127.0.0.1` and `::1`.
678+
679+
Start `proxy.py` as:
680+
681+
```bash
682+
❯ proxy \
683+
--plugins proxy.plugin.FilterByClientIpPlugin
684+
```
685+
686+
Send a request using `curl -v -x localhost:8899 http://google.com`:
687+
688+
```bash
689+
... [redacted] ...
690+
> Proxy-Connection: Keep-Alive
691+
>
692+
< HTTP/1.1 418 I'm a tea pot
693+
< Connection: close
694+
<
695+
* Closing connection 0
696+
```
697+
698+
Modify plugin to your taste e.g. Allow specific IP addresses only.
699+
700+
### ModifyChunkResponsePlugin
701+
702+
This plugin demonstrate how to modify chunked encoded responses. In able to do so, this plugin uses `proxy.py` core to parse the chunked encoded response. Then we reconstruct the response using custom hardcoded chunks, ignoring original chunks received from upstream server.
703+
704+
Start `proxy.py` as:
705+
706+
```bash
707+
❯ proxy \
708+
--plugins proxy.plugin.ModifyChunkResponsePlugin
709+
```
710+
711+
Verify using `curl -v -x localhost:8899 http://httpbin.org/stream/5`:
712+
713+
```bash
714+
... [redacted] ...
715+
modify
716+
chunk
717+
response
718+
plugin
719+
* Connection #0 to host localhost left intact
720+
* Closing connection 0
721+
```
722+
723+
Modify `ModifyChunkResponsePlugin` to your taste. Example, instead of sending hardcoded chunks, parse and modify the original `JSON` chunks received from the upstream server.
724+
672725
## HTTP Web Server Plugins
673726
674727
### Reverse Proxy

proxy/http/parser.py

+13-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .chunk_parser import ChunkParser, chunkParserStates
1616

1717
from ..common.constants import DEFAULT_DISABLE_HEADERS, COLON, CRLF, WHITESPACE, HTTP_1_1, DEFAULT_HTTP_PORT
18-
from ..common.utils import build_http_request, find_http_line, text_
18+
from ..common.utils import build_http_request, build_http_response, find_http_line, text_
1919

2020

2121
HttpParserStates = NamedTuple('HttpParserStates', [
@@ -237,7 +237,8 @@ def build_path(self) -> bytes:
237237
return url
238238

239239
def build(self, disable_headers: Optional[List[bytes]] = None) -> bytes:
240-
assert self.method and self.version and self.path
240+
"""Rebuild the request object."""
241+
assert self.method and self.version and self.path and self.type == httpParserTypes.REQUEST_PARSER
241242
if disable_headers is None:
242243
disable_headers = DEFAULT_DISABLE_HEADERS
243244
body: Optional[bytes] = ChunkParser.to_chunks(self.body) \
@@ -250,6 +251,16 @@ def build(self, disable_headers: Optional[List[bytes]] = None) -> bytes:
250251
body=body
251252
)
252253

254+
def build_response(self) -> bytes:
255+
"""Rebuild the response object."""
256+
assert self.code and self.version and self.body and self.type == httpParserTypes.RESPONSE_PARSER
257+
return build_http_response(
258+
status_code=int(self.code),
259+
protocol_version=self.version,
260+
reason=self.reason,
261+
headers={} if not self.headers else {self.headers[k][0]: self.headers[k][1] for k in self.headers},
262+
body=self.body if not self.is_chunked_encoded() else ChunkParser.to_chunks(self.body))
263+
253264
def has_upstream_server(self) -> bool:
254265
"""Host field SHOULD be None for incoming local WebServer requests."""
255266
return True if self.host is not None else False

proxy/plugin/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from .reverse_proxy import ReverseProxyPlugin
2020
from .proxy_pool import ProxyPoolPlugin
2121
from .filter_by_client_ip import FilterByClientIpPlugin
22+
from .modify_chunk_response_plugin import ModifyChunkResponsePlugin
2223

2324
__all__ = [
2425
'CacheResponsesPlugin',
@@ -33,4 +34,5 @@
3334
'ReverseProxyPlugin',
3435
'ProxyPoolPlugin',
3536
'FilterByClientIpPlugin',
37+
'ModifyChunkResponsePlugin',
3638
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
from typing import Optional, Any
12+
13+
from ..http.parser import HttpParser, httpParserTypes, httpParserStates
14+
from ..http.proxy import HttpProxyBasePlugin
15+
16+
17+
class ModifyChunkResponsePlugin(HttpProxyBasePlugin):
18+
"""Accumulate & modify chunk responses as received from upstream."""
19+
20+
DEFAULT_CHUNKS = [
21+
b'modify',
22+
b'chunk',
23+
b'response',
24+
b'plugin',
25+
]
26+
27+
def __init__(self, *args: Any, **kwargs: Any) -> None:
28+
super().__init__(*args, **kwargs)
29+
# Create a new http protocol parser for response payloads
30+
self.response = HttpParser(httpParserTypes.RESPONSE_PARSER)
31+
32+
def before_upstream_connection(
33+
self, request: HttpParser) -> Optional[HttpParser]:
34+
return request
35+
36+
def handle_client_request(
37+
self, request: HttpParser) -> Optional[HttpParser]:
38+
return request
39+
40+
def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
41+
# Parse the response.
42+
# Note that these chunks also include headers
43+
self.response.parse(chunk.tobytes())
44+
# If response is complete, modify and dispatch to client
45+
if self.response.state == httpParserStates.COMPLETE:
46+
self.response.body = b'\n'.join(self.DEFAULT_CHUNKS) + b'\n'
47+
self.client.queue(memoryview(self.response.build_response()))
48+
return memoryview(b'')
49+
50+
def on_upstream_connection_close(self) -> None:
51+
pass

0 commit comments

Comments
 (0)