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("%s | %s |
" % i)
+# print("
")
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.
- """