docs: try to explain proxy.layer in a better way

This commit is contained in:
Maximilian Hils 2021-03-16 10:26:21 +01:00
parent 10ee19c138
commit f236ee42ed
2 changed files with 45 additions and 10 deletions

View File

@ -23,6 +23,9 @@ class Command:
blocking: Union[bool, "mitmproxy.proxy.layer.Layer"] = False blocking: Union[bool, "mitmproxy.proxy.layer.Layer"] = False
""" """
Determines if the command blocks until it has been completed. Determines if the command blocks until it has been completed.
For practical purposes, this attribute should be thought of as a boolean value,
layers may swap out `True` with a reference to themselves to signal to outer layers
that they do not need to block as well.
Example: Example:

View File

@ -33,21 +33,32 @@ class Layer:
Layers interface with their child layer(s) by calling .handle_event(event), Layers interface with their child layer(s) by calling .handle_event(event),
which returns a list (more precisely: a generator) of commands. which returns a list (more precisely: a generator) of commands.
Most layers only implement ._handle_event, which is called by the default implementation of .handle_event. Most layers do not implement .directly, but instead implement ._handle_event, which
The default implementation allows layers to emulate blocking code: is called by the default implementation of .handle_event.
The default implementation of .handle_event allows layers to emulate blocking code:
When ._handle_event yields a command that has its blocking attribute set to True, .handle_event pauses When ._handle_event yields a command that has its blocking attribute set to True, .handle_event pauses
the execution of ._handle_event and waits until it is called with the corresponding CommandCompleted event. All the execution of ._handle_event and waits until it is called with the corresponding CommandCompleted event.
events encountered in the meantime are buffered and replayed after execution is resumed. All events encountered in the meantime are buffered and replayed after execution is resumed.
The result is code that looks like blocking code, but is not blocking: The result is code that looks like blocking code, but is not blocking:
def _handle_event(self, event): def _handle_event(self, event):
err = yield OpenConnection(server) # execution continues here after a connection has been established. err = yield OpenConnection(server) # execution continues here after a connection has been established.
Technically this is very similar to how coroutines are implemented.
""" """
__last_debug_message: ClassVar[str] = "" __last_debug_message: ClassVar[str] = ""
context: Context context: Context
_paused: Optional[Paused] _paused: Optional[Paused]
"""
If execution is currently paused, this attribute stores the paused coroutine
and the command for which we are expecting a reply.
"""
_paused_event_queue: Deque[events.Event] _paused_event_queue: Deque[events.Event]
"""
All events that have occurred since execution was paused.
These will be replayed to ._child_layer once we resume.
"""
debug: Optional[str] = None debug: Optional[str] = None
""" """
Enable debug logging by assigning a prefix string for log messages. Enable debug logging by assigning a prefix string for log messages.
@ -75,6 +86,7 @@ class Layer:
return f"{type(self).__name__}({state})" return f"{type(self).__name__}({state})"
def __debug(self, message): def __debug(self, message):
"""yield a Log command indicating what message is passing through this layer."""
if len(message) > 512: if len(message) > 512:
message = message[:512] + "" message = message[:512] + ""
if Layer.__last_debug_message == message: if Layer.__last_debug_message == message:
@ -126,6 +138,9 @@ class Layer:
# inlined copy of __process to reduce call stack. # inlined copy of __process to reduce call stack.
# <✂✂✂> # <✂✂✂>
try: try:
# Run ._handle_event to the next yield statement.
# If you are not familiar with generators and their .send() method,
# https://stackoverflow.com/a/12638313/934719 has a good explanation.
command = command_generator.send(send) command = command_generator.send(send)
except StopIteration: except StopIteration:
return return
@ -135,7 +150,12 @@ class Layer:
if not isinstance(command, commands.Log): if not isinstance(command, commands.Log):
yield self.__debug(f"<< {command}") yield self.__debug(f"<< {command}")
if command.blocking is True: if command.blocking is True:
command.blocking = self # assign to our layer so that higher layers don't block. # We only want this layer to block, the outer layers should not block.
# For example, take an HTTP/2 connection: If we intercept one particular request,
# we don't want all other requests in the connection to be blocked a well.
# We signal to outer layers that this command is already handled by assigning our layer to
# `.blocking` here (upper layers explicitly check for `is True`).
command.blocking = self
self._paused = Paused( self._paused = Paused(
command, command,
command_generator, command_generator,
@ -152,11 +172,14 @@ class Layer:
def __process(self, command_generator: CommandGenerator, send=None): def __process(self, command_generator: CommandGenerator, send=None):
""" """
yield all commands from a generator. Yield commands from a generator.
if a command is blocking, the layer is paused and this function returns before If a command is blocking, execution is paused and this function returns without
processing any other commands. processing any further commands.
""" """
try: try:
# Run ._handle_event to the next yield statement.
# If you are not familiar with generators and their .send() method,
# https://stackoverflow.com/a/12638313/934719 has a good explanation.
command = command_generator.send(send) command = command_generator.send(send)
except StopIteration: except StopIteration:
return return
@ -166,7 +189,12 @@ class Layer:
if not isinstance(command, commands.Log): if not isinstance(command, commands.Log):
yield self.__debug(f"<< {command}") yield self.__debug(f"<< {command}")
if command.blocking is True: if command.blocking is True:
command.blocking = self # assign to our layer so that higher layers don't block. # We only want this layer to block, the outer layers should not block.
# For example, take an HTTP/2 connection: If we intercept one particular request,
# we don't want all other requests in the connection to be blocked a well.
# We signal to outer layers that this command is already handled by assigning our layer to
# `.blocking` here (upper layers explicitly check for `is True`).
command.blocking = self
self._paused = Paused( self._paused = Paused(
command, command,
command_generator, command_generator,
@ -181,7 +209,11 @@ class Layer:
return return
def __continue(self, event: events.CommandCompleted): def __continue(self, event: events.CommandCompleted):
"""continue processing events after being paused""" """
Continue processing events after being paused.
The tricky part here is that events in the event queue may trigger commands which again pause the execution,
so we may not be able to process the entire queue.
"""
assert self._paused is not None assert self._paused is not None
command_generator = self._paused.generator command_generator = self._paused.generator
self._paused = None self._paused = None