restructure examples

- restructure examples (fix #4031)
 - remove example dependencies from setup.py,
   we do not need special dependencies for our supported addons.
 - unify how we generate docs from code
 - improve example docs
This commit is contained in:
Maximilian Hils 2020-06-23 01:53:39 +02:00
parent 67372d26ae
commit 08895e9ba6
78 changed files with 176 additions and 253 deletions

1
docs/.gitignore vendored
View File

@ -3,4 +3,3 @@ src/public/
node_modules/
public/
src/resources/_gen/
src/content/addons-examples.md

View File

@ -15,9 +15,5 @@ for script in scripts/* ; do
"${script}" > "${output}"
done
output="src/content/addons-examples.md"
echo "Generating examples content page into ${output} ..."
./render_examples.py > "${output}"
cd src
hugo

View File

@ -1,3 +1,7 @@
scripts/*.py {
prep: build.sh
}
{
daemon: cd src; hugo server -D
}

View File

@ -1,50 +0,0 @@
#!/usr/bin/env python3
import os
import textwrap
from pathlib import Path
print("""
---
title: "Examples"
menu:
addons:
weight: 6
---
# Examples of Addons and Scripts
The most recent set of examples is also available [on our GitHub project](https://github.com/mitmproxy/mitmproxy/tree/master/examples).
""")
base = os.path.dirname(os.path.realpath(__file__))
examples_path = os.path.join(base, 'src/examples/')
pathlist = Path(examples_path).glob('**/*.py')
examples = [os.path.relpath(str(p), examples_path) for p in sorted(pathlist)]
examples = [p for p in examples if not os.path.basename(p) == '__init__.py']
examples = [p for p in examples if not os.path.basename(p).startswith('test_')]
current_dir = None
current_level = 2
for ex in examples:
if os.path.dirname(ex) != current_dir:
current_dir = os.path.dirname(ex)
sanitized = current_dir.replace('/', '').replace('.', '')
print(" * [Examples: {}]({{{{< relref \"addons-examples#{}\">}}}})".format(current_dir, sanitized))
sanitized = ex.replace('/', '').replace('.', '')
print(" * [{}]({{{{< relref \"addons-examples#example-{}\">}}}})".format(os.path.basename(ex), sanitized))
current_dir = None
current_level = 2
for ex in examples:
if os.path.dirname(ex) != current_dir:
current_dir = os.path.dirname(ex)
print("#" * current_level, current_dir)
print(textwrap.dedent("""
{} Example: {}
{{{{< example src="{}" lang="py" >}}}}
""".format("#" * (current_level + 1), ex, "examples/" + ex)))

48
docs/scripts/examples.py Executable file
View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
import re
from pathlib import Path
here = Path(__file__).absolute().parent
example_dir = here / ".." / "src" / "examples" / "addons"
examples = example_dir.glob('*.py')
overview = []
listings = []
for example in examples:
code = example.read_text()
slug = str(example.with_suffix("").relative_to(example_dir))
slug = re.sub(r"[^a-zA-Z]", "-", slug)
match = re.search(r'''
^
(?:[#][^\n]*\n)? # there might be a shebang
"""
\s*
(.+?)
\s*
(?:\n\n|""") # stop on empty line or end of comment
''', code, re.VERBOSE)
if match:
comment = "" + match.group(1)
else:
comment = ""
overview.append(
f" * [{example.name}](#{slug}){comment}"
)
listings.append(f"""
<h2 id="{slug}">Example: {example.name}</h2>
```python
{code}
```
""")
print("\n".join(overview))
print("""
### Community Examples
Additional examples contributed by the mitmproxy community can be found
[on GitHub](https://github.com/mitmproxy/mitmproxy/tree/master/examples/contrib).
""")
print("\n".join(listings))

View File

@ -13,7 +13,7 @@ receive `Flow` objects as arguments - by modifying these objects, addons can
change traffic on the fly. For instance, here is an addon that adds a response
header with a count of the number of responses seen:
{{< example src="examples/addons/addheader.py" lang="py" >}}
{{< example src="examples/addons/http-add-header.py" lang="py" >}}
## Supported Events

View File

@ -0,0 +1,11 @@
---
title: "Example Addons"
menu:
addons:
weight: 6
---
# Example Addons
{{< readfile file="/generated/examples.html" markdown="true" >}}

View File

@ -13,13 +13,13 @@ module as a whole to be treated as an addon object. This lets us place event
handler functions in the module scope. For instance, here is a complete script
that adds a header to every request.
{{< example src="examples/addons/scripting-headers.py" lang="py" >}}
{{< example src="examples/addons/scripting-minimal-example.py" lang="py" >}}
Here's another example that intercepts requests to a particular URL and sends
an arbitrary response instead:
{{< example src="examples/simple/send_reply_from_proxy.py" lang="py" >}}
{{< example src="examples/addons/http-reply-from-proxy.py" lang="py" >}}
All events around the HTTP protocol [can be found here]({{< relref "addons-events#http-events">}}).
@ -31,7 +31,7 @@ scripting.
The WebSocket protocol initially looks like a regular HTTP request, before the client and server agree to upgrade the connection to WebSocket. All scripting events for initial HTTP handshake, and also the dedicated WebSocket events [can be found here]({{< relref "addons-events#websocket-events">}}).
{{< example src="examples/simple/websocket_messages.py" lang="py" >}}
{{< example src="examples/addons/websocket-simple.py" lang="py" >}}
For WebSocket-related objects please look at the [websocket][] module to find
all attributes that you can use when scripting.
@ -43,7 +43,7 @@ all attributes that you can use when scripting.
All events around the TCP protocol [can be found here]({{< relref "addons-events#tcp-events">}}).
{{< example src="examples/complex/tcp_message.py" lang="py" >}}
{{< example src="examples/addons/tcp-simple.py" lang="py" >}}
For WebSocket-related objects please look at the [tcp][] module to find
all attributes that you can use when scripting.

View File

@ -189,7 +189,7 @@ You can also use a script to customise exactly which requests or responses are
streamed. Requests/Responses that should be tagged for streaming by setting
their ``.stream`` attribute to ``True``:
{{< example src="examples/complex/stream.py" lang="py" >}}
{{< example src="examples/addons/http-stream-simple.py" lang="py" >}}
### Websockets

View File

@ -1,15 +1,9 @@
# Mitmproxy Scripting API
# Mitmproxy Examples
Mitmproxy has a powerful scripting API that allows you to control almost any aspect of traffic being
proxied. In fact, much of mitmproxys own core functionality is implemented using the exact same API
exposed to scripters (see [mitmproxy/addons](../mitmproxy/addons)).
(see [mitmproxy/addons](../mitmproxy/addons)).
This directory contains some examples of the scripting API. We recommend to start with the
ones in [simple/](./simple).
| :warning: | If you are browsing this on GitHub, make sure to select the git tag matching your mitmproxy version. |
|------------|------------------------------------------------------------------------------------------------------|
Some inline scripts may require additional dependencies, which can be installed using
`pip install mitmproxy[examples]`.

View File

@ -1,3 +1,8 @@
"""
Basic skeleton of a mitmproxy addon.
Run as follows: mitmproxy -s anatomy.py
"""
from mitmproxy import ctx

View File

@ -1,3 +1,4 @@
"""Handle flows as command arguments."""
import typing
from mitmproxy import command
@ -6,9 +7,6 @@ from mitmproxy import flow
class MyAddon:
def __init__(self):
self.num = 0
@command.command("myaddon.addheader")
def addheader(self, flows: typing.Sequence[flow.Flow]) -> None:
for f in flows:

View File

@ -1,3 +1,4 @@
"""Handle file paths as command arguments."""
import typing
from mitmproxy import command
@ -7,9 +8,6 @@ from mitmproxy import types
class MyAddon:
def __init__(self):
self.num = 0
@command.command("myaddon.histogram")
def histogram(
self,

View File

@ -1,3 +1,4 @@
"""Add a custom command to mitmproxy's command prompt."""
from mitmproxy import command
from mitmproxy import ctx
@ -9,7 +10,7 @@ class MyAddon:
@command.command("myaddon.inc")
def inc(self) -> None:
self.num += 1
ctx.log.info("num = %s" % self.num)
ctx.log.info(f"num = {self.num}")
addons = [

View File

@ -1,5 +1,8 @@
"""
This example shows how one can add a custom contentview to mitmproxy.
Add a custom message body pretty-printer for use inside mitmproxy.
This example shows how one can add a custom contentview to mitmproxy,
which is used to pretty-print HTTP bodies for example.
The content view API is explained in the mitmproxy.contentviews module.
"""
from mitmproxy import contentviews

View File

@ -1,3 +1,4 @@
"""Take incoming HTTP requests and replay them with modified parameters."""
from mitmproxy import ctx

View File

@ -1,8 +1,8 @@
"""HTTP-specific events."""
import mitmproxy.http
class Events:
# HTTP lifecycle
def http_connect(self, flow: mitmproxy.http.HTTPFlow):
"""
An HTTP CONNECT request was received. Setting a non 2xx response on

View File

@ -1,8 +1,8 @@
"""TCP-specific events."""
import mitmproxy.tcp
class Events:
# TCP lifecycle
def tcp_start(self, flow: mitmproxy.tcp.TCPFlow):
"""
A TCP connection has started.

View File

@ -1,3 +1,4 @@
"""WebSocket-specific events."""
import mitmproxy.http
import mitmproxy.websocket

View File

@ -1,11 +1,9 @@
"""Generic event hooks."""
import typing
import mitmproxy.addonmanager
import mitmproxy.connections
import mitmproxy.http
import mitmproxy.log
import mitmproxy.tcp
import mitmproxy.websocket
import mitmproxy.proxy.protocol

View File

@ -1,5 +1,5 @@
"""
This script demonstrates how to use mitmproxy's filter pattern in scripts.
Use mitmproxy's filter pattern in scripts.
"""
from mitmproxy import flowfilter
from mitmproxy import ctx, http

View File

@ -1,3 +1,5 @@
"""Add an HTTP header to each response."""
class AddHeader:
def __init__(self):

View File

@ -1,3 +1,4 @@
"""Modify an HTTP form submission."""
from mitmproxy import http

View File

@ -1,3 +1,4 @@
"""Modify HTTP query parameters."""
from mitmproxy import http

View File

@ -1,6 +1,4 @@
"""
This example shows two ways to redirect flows to another server.
"""
"""Redirect HTTP requests to another server."""
from mitmproxy import http

View File

@ -1,14 +1,8 @@
"""
This example shows how to send a reply from the proxy immediately
without sending any data to the remote server.
"""
"""Send a reply from the proxy without sending any data to the remote server."""
from mitmproxy import http
def request(flow: http.HTTPFlow) -> None:
# pretty_url takes the "Host" header of the request into account, which
# is useful in transparent mode where we usually only have the IP otherwise.
if flow.request.pretty_url == "http://example.com/path":
flow.response = http.HTTPResponse.make(
200, # (optional) status code

View File

@ -1,7 +1,8 @@
"""
This inline script modifies a streamed response.
If you do not need streaming, see the modify_response_body example.
Be aware that content replacement isn't trivial:
Modify a streamed response.
Generally speaking, we recommend *not* to stream messages you need to modify.
Modifying streamed responses is tricky and brittle:
- If the transfer encoding isn't chunked, you cannot simply change the content length.
- If you want to replace all occurrences of "foobar", make sure to catch the cases
where one chunk ends with [...]foo" and the next starts with "bar[...].

View File

@ -1,3 +1,11 @@
"""
Select which responses should be streamed.
Enable response streaming for all HTTP flows.
This is equivalent to passing `--set stream_large_bodies=1` to mitmproxy.
"""
def responseheaders(flow):
"""
Enables streaming for all responses.

View File

@ -1,5 +1,7 @@
"""
This script reflects all content passing through the proxy.
Mirror all web pages.
Useful if you are living down under.
"""
from mitmproxy import http

View File

@ -1,13 +1,12 @@
#!/usr/bin/env python
#
# Simple script showing how to read a mitmproxy dump file
#
"""
Read a mitmproxy dump file.
"""
from mitmproxy import io
from mitmproxy.exceptions import FlowReadException
import pprint
import sys
with open(sys.argv[1], "rb") as logfile:
freader = io.FlowReader(logfile)
pp = pprint.PrettyPrinter(indent=4)

View File

@ -1,4 +1,6 @@
"""
Generate a mitmproxy dump file.
This script demonstrates how to generate a mitmproxy dump file,
as it would also be generated by passing `-w` to mitmproxy.
In contrast to `-w`, this gives you full control over which

View File

@ -1,3 +1,4 @@
"""Post messages to mitmproxy's event log."""
from mitmproxy import ctx

View File

@ -1,3 +1,9 @@
"""
Make events hooks non-blocking.
When event hooks are decorated with @concurrent, they will be run in their own thread, freeing the main event loop.
Please note that this generally opens the door to race conditions and decreases performance if not required.
"""
import time
from mitmproxy.script import concurrent

View File

@ -1,3 +1,4 @@
"""React to configuration changes."""
import typing
from mitmproxy import ctx

View File

@ -1,3 +1,10 @@
"""
Add a new mitmproxy option.
Usage:
mitmproxy -s options-simple.py --set addheader true
"""
from mitmproxy import ctx

View File

@ -0,0 +1,24 @@
"""
Process individual messages from a TCP connection.
This script replaces full occurences of "foo" with "bar" and prints various details for each message.
Please note that TCP is stream-based and *not* message-based. mitmproxy splits stream contents into "messages"
as they are received by socket.recv(). This is pretty arbitrary and should not be relied on.
However, it is sometimes good enough as a quick hack.
Example Invocation:
mitmdump --rawtcp --tcp-hosts ".*" -s examples/tcp-simple.py
"""
from mitmproxy.utils import strutils
from mitmproxy import ctx
from mitmproxy import tcp
def tcp_message(flow: tcp.TCPFlow):
message = flow.messages[-1]
message.content = message.content.replace(b"foo", b"bar")
ctx.log.info(
f"tcp_message[from_client={message.from_client}), content={strutils.bytes_to_escaped_str(message.content)}]"
)

View File

@ -1,4 +1,6 @@
"""
Inject a WebSocket message into a running connection.
This example shows how to inject a WebSocket message to the client.
Every new WebSocket connection will trigger a new asyncio task that
periodically injects a new message to the client.
@ -8,12 +10,11 @@ import mitmproxy.websocket
class InjectWebSocketMessage:
async def inject(self, flow: mitmproxy.websocket.WebSocketFlow):
i = 0
while not flow.ended and not flow.error:
await asyncio.sleep(5)
flow.inject_message(flow.client_conn, 'This is the #{} injected message!'.format(i))
flow.inject_message(flow.client_conn, f'This is the #{i} injected message!')
i += 1
def websocket_start(self, flow):

View File

@ -1,3 +1,4 @@
"""Process individual messages from a WebSocket connection."""
import re
from mitmproxy import ctx

View File

@ -1,4 +1,6 @@
"""
Host a WSGI app in mitmproxy.
This example shows how to graft a WSGI app onto mitmproxy. In this
instance, we're using the Flask framework (http://flask.pocoo.org/) to expose
a single simplest-possible page.

View File

@ -1,19 +0,0 @@
## Complex Examples
| Filename | Description |
|:-------------------------|:----------------------------------------------------------------------------------------------|
| block_dns_over_https.py | Use mitmproxy to block DNS over HTTPS (DoH) queries |
| change_upstream_proxy.py | Dynamically change the upstream proxy. |
| dns_spoofing.py | Use mitmproxy in a DNS spoofing scenario. |
| dup_and_replay.py | Duplicates each request, changes it, and then replays the modified request. |
| full_transparency_shim.c | Setuid wrapper that can be used to run mitmproxy in full transparency mode, as a normal user. |
| har_dump.py | Dump flows as HAR files. |
| mitmproxywrapper.py | Bracket mitmproxy run with proxy enable/disable on OS X |
| nonblocking.py | Demonstrate parallel processing with a blocking script |
| remote_debug.py | This script enables remote debugging of the mitmproxy _UI_ with PyCharm. |
| sslstrip.py | sslstrip-like functionality implemented with mitmproxy |
| stream.py | Enable streaming for all responses. |
| stream_modify.py | Modify a streamed response body. |
| tcp_message.py | Modify a raw TCP connection |
| tls_passthrough.py | Use conditional TLS interception based on a user-defined strategy. |
| xss_scanner.py | Scan all visited webpages. |

View File

@ -1,27 +0,0 @@
"""
tcp_message Inline Script Hook API Demonstration
------------------------------------------------
* modifies packets containing "foo" to "bar"
* prints various details for each packet.
example cmdline invocation:
mitmdump --rawtcp --tcp-host ".*" -s examples/complex/tcp_message.py
"""
from mitmproxy.utils import strutils
from mitmproxy import ctx
from mitmproxy import tcp
def tcp_message(flow: tcp.TCPFlow):
message = flow.messages[-1]
old_content = message.content
message.content = old_content.replace(b"foo", b"bar")
ctx.log.info(
"[tcp_message{}] from {} to {}:\n{}".format(
" (modified)" if message.content != old_content else "",
"client" if message.from_client else "server",
"server" if message.from_client else "client",
strutils.bytes_to_escaped_str(message.content))
)

View File

@ -0,0 +1,4 @@
# Community-Contributed Examples
Examples in this directory are contributed by the mitmproxy community.
We do _not_ maintain them, but we welcome PRs that add/fix/modernize/clean up examples.

View File

@ -1,5 +1,5 @@
"""
This script enables remote debugging of the mitmproxy *UI* with PyCharm.
This script enables remote debugging of the mitmproxy console *UI* with PyCharm.
For general debugging purposes, it is easier to just debug mitmdump within PyCharm.
Usage:

View File

View File

@ -1,19 +0,0 @@
## Simple Examples
| Filename | Description |
| :----------------------------- | :--------------------------------------------------------------------------- |
| add_header.py | Simple script that just adds a header to every request. |
| custom_contentview.py | Add a custom content view to the mitmproxy UI. |
| custom_option.py | Add arguments to a script. |
| filter_flows.py | This script demonstrates how to use mitmproxy's filter pattern in scripts. |
| io_read_dumpfile.py | Read a dumpfile generated by mitmproxy. |
| io_write_dumpfile.py | Only write selected flows into a mitmproxy dumpfile. |
| link_expander.py | Discover relative links in HTML traffic and replace them with absolute paths |
| log_events.py | Use mitmproxy's logging API. |
| modify_body_inject_iframe.py | Inject configurable iframe into pages. |
| modify_form.py | Modify HTTP form submissions. |
| modify_querystring.py | Modify HTTP query strings. |
| redirect_requests.py | Redirect a request to a different server. |
| send_reply_from_proxy.py | Send a HTTP response directly from the proxy. |
| internet_in_mirror.py | Turn all images upside down. |
| wsgi_flask_app.py | Embed a WSGI app into mitmproxy. |

View File

@ -1,5 +0,0 @@
from mitmproxy import http
def response(flow: http.HTTPFlow) -> None:
flow.response.headers["newheader"] = "foo"

View File

@ -1,9 +0,0 @@
from mitmproxy import http
class AddHeader:
def response(self, flow: http.HTTPFlow) -> None:
flow.response.headers["newheader"] = "foo"
addons = [AddHeader()]

View File

@ -1,21 +0,0 @@
"""
This example shows how addons can register custom options
that can be configured at startup or during execution
from the options dialog within mitmproxy.
Example:
$ mitmproxy --set custom=true
$ mitmproxy --set custom # shorthand for boolean options
"""
from mitmproxy import ctx
def load(l):
ctx.log.info("Registering option 'custom'")
l.add_option("custom", bool, False, "A custom option")
def configure(updated):
if "custom" in updated:
ctx.log.info("custom option value: %s" % ctx.options.custom)

View File

@ -1 +1 @@
-e .[dev,examples]
-e .[dev]

View File

@ -102,9 +102,6 @@ setup(
"pytest>=5.1.3,<6",
"requests>=2.9.1,<3",
"tox>=3.5,<3.15",
],
'examples': [
"beautifulsoup4>=4.4.1,<4.9"
]
}
)

View File

@ -10,35 +10,21 @@ from ..mitmproxy import tservers
class TestScripts(tservers.MasterTest):
def test_add_header(self, tdata):
with taddons.context() as tctx:
a = tctx.script(tdata.path("../examples/simple/add_header.py"))
f = tflow.tflow(resp=tutils.tresp())
a.response(f)
assert f.response.headers["newheader"] == "foo"
a = tctx.script(tdata.path("../examples/addons/scripting-minimal-example.py"))
f = tflow.tflow()
a.request(f)
assert f.request.headers["myheader"] == "value"
def test_custom_contentviews(self, tdata):
with taddons.context() as tctx:
tctx.script(tdata.path("../examples/simple/custom_contentview.py"))
tctx.script(tdata.path("../examples/addons/contentview.py"))
swapcase = contentviews.get("swapcase")
_, fmt = swapcase(b"<html>Test!</html>")
assert any(b'tEST!' in val[0][1] for val in fmt)
def test_iframe_injector(self, tdata):
with taddons.context() as tctx:
sc = tctx.script(tdata.path("../examples/simple/modify_body_inject_iframe.py"))
tctx.configure(
sc,
iframe = "http://example.org/evil_iframe"
)
f = tflow.tflow(
resp=tutils.tresp(content=b"<html><body>mitmproxy</body></html>")
)
tctx.master.addons.invoke_addon(sc, "response", f)
content = f.response.content
assert b'iframe' in content and b'evil_iframe' in content
def test_modify_form(self, tdata):
with taddons.context() as tctx:
sc = tctx.script(tdata.path("../examples/simple/modify_form.py"))
sc = tctx.script(tdata.path("../examples/addons/http-modify-form.py"))
form_header = Headers(content_type="application/x-www-form-urlencoded")
f = tflow.tflow(req=tutils.treq(headers=form_header))
@ -52,7 +38,7 @@ class TestScripts(tservers.MasterTest):
def test_modify_querystring(self, tdata):
with taddons.context() as tctx:
sc = tctx.script(tdata.path("../examples/simple/modify_querystring.py"))
sc = tctx.script(tdata.path("../examples/addons/http-modify-query-string.py"))
f = tflow.tflow(req=tutils.treq(path="/search?q=term"))
sc.request(f)
@ -64,36 +50,14 @@ class TestScripts(tservers.MasterTest):
def test_redirect_requests(self, tdata):
with taddons.context() as tctx:
sc = tctx.script(tdata.path("../examples/simple/redirect_requests.py"))
sc = tctx.script(tdata.path("../examples/addons/http-redirect-requests.py"))
f = tflow.tflow(req=tutils.treq(host="example.org"))
sc.request(f)
assert f.request.host == "mitmproxy.org"
def test_send_reply_from_proxy(self, tdata):
with taddons.context() as tctx:
sc = tctx.script(tdata.path("../examples/simple/send_reply_from_proxy.py"))
sc = tctx.script(tdata.path("../examples/addons/http-reply-from-proxy.py"))
f = tflow.tflow(req=tutils.treq(host="example.com", port=80))
sc.request(f)
assert f.response.content == b"Hello World"
def test_dns_spoofing(self, tdata):
with taddons.context() as tctx:
sc = tctx.script(tdata.path("../examples/complex/dns_spoofing.py"))
original_host = "example.com"
host_header = Headers(host=original_host)
f = tflow.tflow(req=tutils.treq(headers=host_header, port=80))
tctx.master.addons.invoke_addon(sc, "requestheaders", f)
# Rewrite by reverse proxy mode
f.request.scheme = "https"
f.request.port = 443
tctx.master.addons.invoke_addon(sc, "request", f)
assert f.request.scheme == "http"
assert f.request.port == 80
assert f.request.headers["Host"] == original_host