diff --git a/CHANGELOG.md b/CHANGELOG.md index 0120d19ac..db6cb5ffb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ If you depend on these features, please raise your voice in * New Proxy Core based on sans-io pattern (@mhils) * mitmproxy's command line interface now supports Windows (@mhils) +* The `clientconnect`, `clientdisconnect`, `serverconnect`, `serverdisconnect`, and `log` + events have been replaced with new events, see addon documentation for details (@mhils) * Use pyca/cryptography to generate certificates, not pyOpenSSL (@mhils) * Remove the legacy protocol stack (@Kriechi) * Remove all deprecated pathod and pathoc tools and modules (@Kriechi) diff --git a/docs/scripts/events.py b/docs/scripts/events.py new file mode 100644 index 000000000..d7c26ca81 --- /dev/null +++ b/docs/scripts/events.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +import inspect +import textwrap +from typing import List, Type + +import mitmproxy.addons.next_layer # noqa +from mitmproxy import events, log, addonmanager +from mitmproxy.proxy import server_hooks, layer +from mitmproxy.proxy.layers import http, tcp, tls, websocket + +known = set() + + +def category(name: str, hooks: List[Type[events.MitmproxyEvent]]) -> None: + print(f"### {name} Events") + print("```python") + + all_params = [ + list(inspect.signature(hook.__init__).parameters.values())[1:] + for hook in hooks + ] + + # slightly overengineered, but this was fun to write. ¯\_(ツ)_/¯ + imports = set() + types = set() + for params in all_params: + for param in params: + try: + mod = inspect.getmodule(param.annotation).__name__ + if mod == "typing": + # this is ugly, but can be removed once we are on Python 3.9+ only + imports.add(inspect.getmodule(param.annotation.__args__[0]).__name__) + types.add(param.annotation._name) + else: + imports.add(mod) + except AttributeError: + raise ValueError(f"Missing type annotation: {params}") + imports.discard("builtins") + if types: + print(f"from typing import {', '.join(sorted(types))}") + print("from mitmproxy import ctx") + for imp in sorted(imports): + print(f"import {imp}") + print() + + first = True + for hook, params in zip(hooks, all_params): + if first: + first = False + else: + print() + assert hook.name not in known + known.add(hook.name) + doc = inspect.getdoc(hook) + print(f"def {hook.name}({', '.join(str(p) for p in params)}):") + print(textwrap.indent(f'"""\n{doc}\n"""', " ")) + if params: + print(f' ctx.log(f"{hook.name}: {" ".join("{" + p.name + "=}" for p in params)}")') + else: + print(f' ctx.log("{hook.name}")') + print("```") + + +category( + "Lifecycle", + [ + addonmanager.LoadEvent, + events.RunningEvent, + events.ConfigureEvent, + events.DoneEvent, + ] +) + +category( + "Connection", + [ + server_hooks.ClientConnectedHook, + server_hooks.ClientDisconnectedHook, + server_hooks.ServerConnectHook, + server_hooks.ServerConnectedHook, + server_hooks.ServerDisconnectedHook, + ] +) + +category( + "HTTP", + [ + http.HttpRequestHeadersHook, + http.HttpRequestHook, + http.HttpResponseHeadersHook, + http.HttpResponseHook, + http.HttpErrorHook, + http.HttpConnectHook, + ] +) + +category( + "TCP", + [ + tcp.TcpStartHook, + tcp.TcpMessageHook, + tcp.TcpEndHook, + tcp.TcpErrorHook, + ] +) + +category( + "TLS", + [ + tls.TlsClienthelloHook, + tls.TlsStartHook, + ] +) + +category( + "WebSocket", + [ + websocket.WebsocketStartHook, + websocket.WebsocketMessageHook, + websocket.WebsocketEndHook, + websocket.WebsocketErrorHook, + ] +) + +category( + "Advanced Lifecycle", + [ + layer.NextLayerHook, + events.UpdateEvent, + log.AddLogEvent, + ] +) + +not_documented = set(events.all_events.keys()) - known +if not_documented: + raise RuntimeError(f"Not documented: {not_documented}") + +# print("") +# for i in flowfilter.help: +# print("" % i) +# print("
%s%s
") diff --git a/docs/src/content/addons-commands.md b/docs/src/content/addons-commands.md index 7fd7db4f6..83ddb8c3e 100644 --- a/docs/src/content/addons-commands.md +++ b/docs/src/content/addons-commands.md @@ -62,7 +62,7 @@ and adds a header to every request. The really interesting aspect of this example is how users specify flows. Because mitmproxy can inspect the type signature, it can expand a text flow selector into a sequence of flows for us transparently. This means that the user has the full flexibility of [flow -filters]({{< relref addons-options >}}) available. Let's try it out. +filters]({{< relref concepts-filters >}}) available. Let's try it out. Start by loading the addon into mitmproxy and sending some traffic through so we have flows to work with: diff --git a/docs/src/content/addons-events.md b/docs/src/content/addons-events.md index b0c982cb1..33f1c18cc 100644 --- a/docs/src/content/addons-events.md +++ b/docs/src/content/addons-events.md @@ -18,21 +18,7 @@ header with a count of the number of responses seen: ## Supported Events -Below is an addon class that implements stubs for all events. We've added -annotations to illustrate the argument types for the various events. +Below we list events supported by mitmproxy. We've added +annotations to illustrate the argument types. -### Generic Events - -{{< example src="examples/addons/events.py" lang="py" >}} - -### HTTP Events - -{{< example src="examples/addons/events-http-specific.py" lang="py" >}} - -### WebSocket Events - -{{< example src="examples/addons/events-websocket-specific.py" lang="py" >}} - -### TCP Events - -{{< example src="examples/addons/events-tcp-specific.py" lang="py" >}} +{{< readfile file="/generated/events.html" markdown="true" >}} diff --git a/examples/addons/events-http-specific.py b/examples/addons/events-http-specific.py deleted file mode 100644 index 8e101fce0..000000000 --- a/examples/addons/events-http-specific.py +++ /dev/null @@ -1,42 +0,0 @@ -"""HTTP-specific events.""" -import mitmproxy.http - - -class Events: - def http_connect(self, flow: mitmproxy.http.HTTPFlow): - """ - An HTTP CONNECT request was received. Setting a non 2xx response on - the flow will return the response to the client abort the - connection. CONNECT requests and responses do not generate the usual - HTTP handler events. CONNECT requests are only valid in regular and - upstream proxy modes. - """ - - def requestheaders(self, flow: mitmproxy.http.HTTPFlow): - """ - HTTP request headers were successfully read. At this point, the body - is empty. - """ - - def request(self, flow: mitmproxy.http.HTTPFlow): - """ - The full HTTP request has been read. - """ - - def responseheaders(self, flow: mitmproxy.http.HTTPFlow): - """ - HTTP response headers were successfully read. At this point, the body - is empty. - """ - - def response(self, flow: mitmproxy.http.HTTPFlow): - """ - The full HTTP response has been read. - """ - - def error(self, flow: mitmproxy.http.HTTPFlow): - """ - An HTTP error has occurred, e.g. invalid server responses, or - interrupted connections. This is distinct from a valid server HTTP - error response, which is simply a response with an HTTP error code. - """ diff --git a/examples/addons/events-tcp-specific.py b/examples/addons/events-tcp-specific.py deleted file mode 100644 index f5a577b85..000000000 --- a/examples/addons/events-tcp-specific.py +++ /dev/null @@ -1,25 +0,0 @@ -"""TCP-specific events.""" -import mitmproxy.tcp - - -class Events: - def tcp_start(self, flow: mitmproxy.tcp.TCPFlow): - """ - A TCP connection has started. - """ - - def tcp_message(self, flow: mitmproxy.tcp.TCPFlow): - """ - A TCP connection has received a message. The most recent message - will be flow.messages[-1]. The message is user-modifiable. - """ - - def tcp_error(self, flow: mitmproxy.tcp.TCPFlow): - """ - A TCP error has occurred. - """ - - def tcp_end(self, flow: mitmproxy.tcp.TCPFlow): - """ - A TCP connection has ended. - """ diff --git a/examples/addons/events-websocket-specific.py b/examples/addons/events-websocket-specific.py deleted file mode 100644 index 1a499cd97..000000000 --- a/examples/addons/events-websocket-specific.py +++ /dev/null @@ -1,37 +0,0 @@ -"""WebSocket-specific events.""" -import mitmproxy.http -import mitmproxy.websocket - - -class Events: - # WebSocket lifecycle - def websocket_handshake(self, flow: mitmproxy.http.HTTPFlow): - """ - Called when a client wants to establish a WebSocket connection. The - WebSocket-specific headers can be manipulated to alter the - handshake. The flow object is guaranteed to have a non-None request - attribute. - """ - - def websocket_start(self, flow: mitmproxy.websocket.WebSocketFlow): - """ - A WebSocket connection has commenced. - """ - - def websocket_message(self, flow: mitmproxy.websocket.WebSocketFlow): - """ - Called when a WebSocket message is received from the client or - server. The most recent message will be flow.messages[-1]. The - message is user-modifiable. Currently there are two types of - messages, corresponding to the BINARY and TEXT frame types. - """ - - def websocket_error(self, flow: mitmproxy.websocket.WebSocketFlow): - """ - A WebSocket connection has had an error. - """ - - def websocket_end(self, flow: mitmproxy.websocket.WebSocketFlow): - """ - A WebSocket connection has ended. - """ diff --git a/examples/addons/events.py b/examples/addons/events.py deleted file mode 100644 index 0e7526a87..000000000 --- a/examples/addons/events.py +++ /dev/null @@ -1,83 +0,0 @@ -"""Generic event hooks.""" -import typing - -import mitmproxy.addonmanager -import mitmproxy.connections -import mitmproxy.log -import mitmproxy.proxy.protocol - - -class Events: - # Network lifecycle - def clientconnect(self, layer: mitmproxy.proxy.protocol.Layer): - """ - A client has connected to mitmproxy. Note that a connection can - correspond to multiple HTTP requests. - """ - - def clientdisconnect(self, layer: mitmproxy.proxy.protocol.Layer): - """ - A client has disconnected from mitmproxy. - """ - - def serverconnect(self, conn: mitmproxy.connections.ServerConnection): - """ - Mitmproxy has connected to a server. Note that a connection can - correspond to multiple requests. - """ - - def serverdisconnect(self, conn: mitmproxy.connections.ServerConnection): - """ - Mitmproxy has disconnected from a server. - """ - - def next_layer(self, layer: mitmproxy.proxy.protocol.Layer): - """ - Network layers are being switched. You may change which layer will - be used by returning a new layer object from this event. - """ - - # General lifecycle - def configure(self, updated: typing.Set[str]): - """ - Called when configuration changes. The updated argument is a - set-like object containing the keys of all changed options. This - event is called during startup with all options in the updated set. - """ - - def done(self): - """ - Called when the addon shuts down, either by being removed from - the mitmproxy instance, or when mitmproxy itself shuts down. On - shutdown, this event is called after the event loop is - terminated, guaranteeing that it will be the final event an addon - sees. Note that log handlers are shut down at this point, so - calls to log functions will produce no output. - """ - - def load(self, entry: mitmproxy.addonmanager.Loader): - """ - Called when an addon is first loaded. This event receives a Loader - object, which contains methods for adding options and commands. This - method is where the addon configures itself. - """ - - def log(self, entry: mitmproxy.log.LogEntry): - """ - Called whenever a new log entry is created through the mitmproxy - context. Be careful not to log from this event, which will cause an - infinite loop! - """ - - def running(self): - """ - Called when the proxy is completely up and running. At this point, - you can expect the proxy to be bound to a port, and all addons to be - loaded. - """ - - def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]): - """ - Update is called when one or more flow objects have been modified, - usually from a different addon. - """