Merge pull request #4294 from mhils/sans-io-adjustments

Add a switch for sans-io proxy core
This commit is contained in:
Maximilian Hils 2020-11-17 21:58:27 +01:00 committed by GitHub
commit 4351262c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 119 additions and 75 deletions

View File

@ -1,18 +1,19 @@
import itertools import itertools
import shutil
import sys import sys
from typing import Optional, TextIO
import click import click
import shutil
import typing # noqa
from mitmproxy import contentviews from mitmproxy import contentviews
from mitmproxy import ctx from mitmproxy import ctx
from mitmproxy import flow
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import flowfilter from mitmproxy import flowfilter
from mitmproxy import http
from mitmproxy.net import http as net_http
from mitmproxy.utils import human from mitmproxy.utils import human
from mitmproxy.utils import strutils from mitmproxy.utils import strutils
from mitmproxy.net import http
def indent(n: int, text: str) -> str: def indent(n: int, text: str) -> str:
@ -29,9 +30,9 @@ def colorful(line, styles):
class Dumper: class Dumper:
def __init__(self, outfile=sys.stdout, errfile=sys.stderr): def __init__(self, outfile=sys.stdout, errfile=sys.stderr):
self.filter: flowfilter.TFilter = None self.filter: Optional[flowfilter.TFilter] = None
self.outfp: typing.io.TextIO = outfile self.outfp: TextIO = outfile
self.errfp: typing.io.TextIO = errfile self.errfp: TextIO = errfile
def load(self, loader): def load(self, loader):
loader.add_option( loader.add_option(
@ -47,10 +48,10 @@ class Dumper:
loader.add_option( loader.add_option(
"dumper_default_contentview", str, "auto", "dumper_default_contentview", str, "auto",
"The default content view mode.", "The default content view mode.",
choices = [i.name.lower() for i in contentviews.views] choices=[i.name.lower() for i in contentviews.views]
) )
loader.add_option( loader.add_option(
"dumper_filter", typing.Optional[str], None, "dumper_filter", Optional[str], None,
"Limit which flows are dumped." "Limit which flows are dumped."
) )
@ -65,19 +66,19 @@ class Dumper:
else: else:
self.filter = None self.filter = None
def echo(self, text, ident=None, **style): def echo(self, text: str, ident=None, **style):
if ident: if ident:
text = indent(ident, text) text = indent(ident, text)
click.secho(text, file=self.outfp, **style) click.secho(text, file=self.outfp, **style)
if self.outfp: if self.outfp:
self.outfp.flush() self.outfp.flush()
def echo_error(self, text, **style): def echo_error(self, text: str, **style):
click.secho(text, file=self.errfp, **style) click.secho(text, file=self.errfp, **style)
if self.errfp: if self.errfp:
self.errfp.flush() self.errfp.flush()
def _echo_headers(self, headers): def _echo_headers(self, headers: net_http.Headers):
for k, v in headers.fields: for k, v in headers.fields:
k = strutils.bytes_to_escaped_str(k) k = strutils.bytes_to_escaped_str(k)
v = strutils.bytes_to_escaped_str(v) v = strutils.bytes_to_escaped_str(v)
@ -87,13 +88,13 @@ class Dumper:
) )
self.echo(out, ident=4) self.echo(out, ident=4)
def _echo_trailers(self, trailers): def _echo_trailers(self, trailers: Optional[net_http.Headers]):
if not trailers or not isinstance(trailers, http.Headers): if not trailers:
return return
self.echo(click.style("--- HTTP Trailers", fg="magenta"), ident=4) self.echo(click.style("--- HTTP Trailers", fg="magenta"), ident=4)
self._echo_headers(trailers) self._echo_headers(trailers)
def _echo_message(self, message, flow): def _echo_message(self, message, flow: flow.Flow):
_, lines, error = contentviews.get_message_content_view( _, lines, error = contentviews.get_message_content_view(
ctx.options.dumper_default_contentview, ctx.options.dumper_default_contentview,
message, message,
@ -127,7 +128,7 @@ class Dumper:
if ctx.options.flow_detail >= 2: if ctx.options.flow_detail >= 2:
self.echo("") self.echo("")
def _echo_request_line(self, flow): def _echo_request_line(self, flow: http.HTTPFlow) -> None:
if flow.client_conn: if flow.client_conn:
client = click.style( client = click.style(
strutils.escape_control_characters( strutils.escape_control_characters(
@ -154,46 +155,54 @@ class Dumper:
url = flow.request.pretty_url url = flow.request.pretty_url
else: else:
url = flow.request.url url = flow.request.url
terminalWidthLimit = max(shutil.get_terminal_size()[0] - 25, 50)
if ctx.options.flow_detail < 1 and len(url) > terminalWidthLimit: if ctx.options.flow_detail <= 1:
url = url[:terminalWidthLimit] + "" # We need to truncate before applying styles, so we just focus on the URL.
terminal_width_limit = max(shutil.get_terminal_size()[0] - 25, 50)
if len(url) > terminal_width_limit:
url = url[:terminal_width_limit] + ""
url = click.style(strutils.escape_control_characters(url), bold=True) url = click.style(strutils.escape_control_characters(url), bold=True)
http_version = "" http_version = ""
if flow.request.http_version not in ("HTTP/1.1", "HTTP/1.0"): if (
# We hide "normal" HTTP 1. flow.request.http_version not in ("HTTP/1.1", "HTTP/1.0")
or flow.request.http_version != getattr(flow.response, "http_version", "HTTP/1.1")
):
# Hide version for h1 <-> h1 connections.
http_version = " " + flow.request.http_version http_version = " " + flow.request.http_version
line = "{client}: {method} {url}{http_version}".format( self.echo(f"{client}: {method} {url}{http_version}")
client=client,
method=method,
url=url,
http_version=http_version
)
self.echo(line)
def _echo_response_line(self, flow): def _echo_response_line(self, flow: http.HTTPFlow) -> None:
if flow.is_replay == "response": if flow.is_replay == "response":
replay = click.style("[replay] ", fg="yellow", bold=True) replay_str = "[replay]"
replay = click.style(replay_str, fg="yellow", bold=True)
else: else:
replay_str = ""
replay = "" replay = ""
code = flow.response.status_code assert flow.response
code_int = flow.response.status_code
code_color = None code_color = None
if 200 <= code < 300: if 200 <= code_int < 300:
code_color = "green" code_color = "green"
elif 300 <= code < 400: elif 300 <= code_int < 400:
code_color = "magenta" code_color = "magenta"
elif 400 <= code < 600: elif 400 <= code_int < 600:
code_color = "red" code_color = "red"
code = click.style( code = click.style(
str(code), str(code_int),
fg=code_color, fg=code_color,
bold=True, bold=True,
blink=(code == 418) blink=(code_int == 418),
) )
if not flow.response.is_http2:
reason = flow.response.reason
else:
reason = net_http.status_codes.RESPONSES.get(flow.response.status_code, "")
reason = click.style( reason = click.style(
strutils.escape_control_characters(flow.response.reason), strutils.escape_control_characters(reason),
fg=code_color, fg=code_color,
bold=True bold=True
) )
@ -204,23 +213,25 @@ class Dumper:
size = human.pretty_size(len(flow.response.raw_content)) size = human.pretty_size(len(flow.response.raw_content))
size = click.style(size, bold=True) size = click.style(size, bold=True)
http_version = ""
if (
flow.response.http_version not in ("HTTP/1.1", "HTTP/1.0")
or flow.request.http_version != flow.response.http_version
):
# Hide version for h1 <-> h1 connections.
http_version = f"{flow.response.http_version} "
arrows = click.style(" <<", bold=True) arrows = click.style(" <<", bold=True)
if ctx.options.flow_detail == 1: if ctx.options.flow_detail == 1:
# This aligns the HTTP response code with the HTTP request method: # This aligns the HTTP response code with the HTTP request method:
# 127.0.0.1:59519: GET http://example.com/ # 127.0.0.1:59519: GET http://example.com/
# << 304 Not Modified 0b # << 304 Not Modified 0b
arrows = " " * (len(human.format_address(flow.client_conn.address)) - 2) + arrows pad = max(0, len(human.format_address(flow.client_conn.address)) - (2 + len(http_version) + len(replay_str)))
arrows = " " * pad + arrows
line = "{replay}{arrows} {code} {reason} {size}".format( self.echo(f"{replay}{arrows} {http_version}{code} {reason} {size}")
replay=replay,
arrows=arrows,
code=code,
reason=reason,
size=size
)
self.echo(line)
def echo_flow(self, f): def echo_flow(self, f: http.HTTPFlow) -> None:
if f.request: if f.request:
self._echo_request_line(f) self._echo_request_line(f)
if ctx.options.flow_detail >= 2: if ctx.options.flow_detail >= 2:
@ -286,12 +297,13 @@ class Dumper:
f.close_reason)) f.close_reason))
def tcp_error(self, f): def tcp_error(self, f):
self.echo_error( if self.match(f):
"Error in TCP connection to {}: {}".format( self.echo_error(
human.format_address(f.server_conn.address), f.error "Error in TCP connection to {}: {}".format(
), human.format_address(f.server_conn.address), f.error
fg="red" ),
) fg="red"
)
def tcp_message(self, f): def tcp_message(self, f):
if self.match(f): if self.match(f):

View File

@ -9,7 +9,7 @@ from mitmproxy.utils import human
class TermStatus: class TermStatus:
def running(self): def running(self):
if ctx.options.server: if ctx.master.server.bound:
ctx.log.info( ctx.log.info(
"Proxy server listening at http://{}".format( "Proxy server listening at http://{}".format(
human.format_address(ctx.master.server.address) human.format_address(ctx.master.server.address)

View File

@ -2,10 +2,11 @@ import time
import typing # noqa import typing # noqa
import uuid import uuid
from mitmproxy import connections from mitmproxy import controller
from mitmproxy import controller, exceptions # noqa from mitmproxy import exceptions
from mitmproxy import stateobject from mitmproxy import stateobject
from mitmproxy import version from mitmproxy import version
from mitmproxy.utils import compat
class Error(stateobject.StateObject): class Error(stateobject.StateObject):
@ -63,8 +64,8 @@ class Flow(stateobject.StateObject):
def __init__( def __init__(
self, self,
type: str, type: str,
client_conn: connections.ClientConnection, client_conn: compat.Client,
server_conn: connections.ServerConnection, server_conn: compat.Server,
live: bool=None live: bool=None
) -> None: ) -> None:
self.type = type self.type = type
@ -84,8 +85,8 @@ class Flow(stateobject.StateObject):
_stateobject_attributes = dict( _stateobject_attributes = dict(
id=str, id=str,
error=Error, error=Error,
client_conn=connections.ClientConnection, client_conn=compat.Client,
server_conn=connections.ServerConnection, server_conn=compat.Server,
type=str, type=str,
intercepted=bool, intercepted=bool,
is_replay=str, is_replay=str,

View File

@ -1,10 +1,10 @@
import html import html
import time import time
from typing import Optional, Tuple from typing import Optional, Tuple
from mitmproxy import connections
from mitmproxy import flow from mitmproxy import flow
from mitmproxy import version from mitmproxy import version
from mitmproxy.net import http from mitmproxy.net import http
from mitmproxy.utils import compat
HTTPRequest = http.Request HTTPRequest = http.Request
HTTPResponse = http.Response HTTPResponse = http.Response
@ -15,16 +15,16 @@ class HTTPFlow(flow.Flow):
An HTTPFlow is a collection of objects representing a single HTTP An HTTPFlow is a collection of objects representing a single HTTP
transaction. transaction.
""" """
request: HTTPRequest request: http.Request
response: Optional[HTTPResponse] = None response: Optional[http.Response] = None
error: Optional[flow.Error] = None error: Optional[flow.Error] = None
""" """
Note that it's possible for a Flow to have both a response and an error Note that it's possible for a Flow to have both a response and an error
object. This might happen, for instance, when a response was received object. This might happen, for instance, when a response was received
from the server, but there was an error sending it back to the client. from the server, but there was an error sending it back to the client.
""" """
server_conn: connections.ServerConnection server_conn: compat.Server
client_conn: connections.ClientConnection client_conn: compat.Client
intercepted: bool = False intercepted: bool = False
""" Is this flow currently being intercepted? """ """ Is this flow currently being intercepted? """
mode: str mode: str
@ -37,8 +37,8 @@ class HTTPFlow(flow.Flow):
_stateobject_attributes = flow.Flow._stateobject_attributes.copy() _stateobject_attributes = flow.Flow._stateobject_attributes.copy()
# mypy doesn't support update with kwargs # mypy doesn't support update with kwargs
_stateobject_attributes.update(dict( _stateobject_attributes.update(dict(
request=HTTPRequest, request=http.Request,
response=HTTPResponse, response=http.Response,
mode=str mode=str
)) ))
@ -67,7 +67,7 @@ def make_error_response(
status_code: int, status_code: int,
message: str = "", message: str = "",
headers: Optional[http.Headers] = None, headers: Optional[http.Headers] = None,
) -> HTTPResponse: ) -> http.Response:
body: bytes = """ body: bytes = """
<html> <html>
<head> <head>
@ -92,11 +92,11 @@ def make_error_response(
Content_Type="text/html" Content_Type="text/html"
) )
return HTTPResponse.make(status_code, body, headers) return http.Response.make(status_code, body, headers)
def make_connect_request(address: Tuple[str, int]) -> HTTPRequest: def make_connect_request(address: Tuple[str, int]) -> http.Request:
return HTTPRequest( return http.Request(
host=address[0], host=address[0],
port=address[1], port=address[1],
method=b"CONNECT", method=b"CONNECT",
@ -115,7 +115,7 @@ def make_connect_request(address: Tuple[str, int]) -> HTTPRequest:
def make_connect_response(http_version): def make_connect_response(http_version):
# Do not send any response headers as it breaks proxying non-80 ports on # Do not send any response headers as it breaks proxying non-80 ports on
# Android emulators using the -http-proxy option. # Android emulators using the -http-proxy option.
return HTTPResponse( return http.Response(
http_version, http_version,
200, 200,
b"Connection established", b"Connection established",
@ -128,4 +128,4 @@ def make_connect_response(http_version):
def make_expect_continue_response(): def make_expect_continue_response():
return HTTPResponse.make(100) return http.Response.make(100)

View File

@ -15,7 +15,7 @@ from mitmproxy import exceptions, master
from mitmproxy import options from mitmproxy import options
from mitmproxy import optmanager from mitmproxy import optmanager
from mitmproxy import proxy from mitmproxy import proxy
from mitmproxy.utils import debug, arg_check from mitmproxy.utils import compat, debug, arg_check
def assert_utf8_env(): def assert_utf8_env():
@ -92,7 +92,7 @@ def run(
) )
pconf = process_options(parser, opts, args) pconf = process_options(parser, opts, args)
server: typing.Any = None server: typing.Any = None
if pconf.options.server: if pconf.options.server and not compat.new_proxy_core: # new core initializes itself as an addon
try: try:
server = proxy.server.ProxyServer(pconf) server = proxy.server.ProxyServer(pconf)
except exceptions.ServerException as v: except exceptions.ServerException as v:

13
mitmproxy/utils/compat.py Normal file
View File

@ -0,0 +1,13 @@
new_proxy_core = False
"""If true, use mitmproxy's new sans-io proxy core."""
if new_proxy_core: # pragma: no cover
from mitmproxy.proxy2 import context
Client = context.Client
Server = context.Server
else: # pragma: no cover
from mitmproxy import connections
Client = connections.ClientConnection
Server = connections.ServerConnection

View File

@ -236,3 +236,14 @@ def test_websocket():
f = tflow.twebsocketflow(client_conn=True, err=True) f = tflow.twebsocketflow(client_conn=True, err=True)
d.websocket_error(f) d.websocket_error(f)
assert "Error in WebSocket" in sio_err.getvalue() assert "Error in WebSocket" in sio_err.getvalue()
def test_http2():
sio = io.StringIO()
sio_err = io.StringIO()
d = dumper.Dumper(sio, sio_err)
with taddons.context(d):
f = tflow.tflow(resp=True)
f.response.http_version = b"HTTP/2.0"
d.response(f)
assert "HTTP/2.0 200 OK" in sio.getvalue()

View File

@ -10,6 +10,7 @@ async def test_configure():
ts = termstatus.TermStatus() ts = termstatus.TermStatus()
with taddons.context() as ctx: with taddons.context() as ctx:
ctx.master.server = proxy.DummyServer() ctx.master.server = proxy.DummyServer()
ctx.master.server.bound = True
ctx.configure(ts, server=False) ctx.configure(ts, server=False)
ts.running() ts.running()
ctx.configure(ts, server=True) ctx.configure(ts, server=True)

View File

@ -0,0 +1,6 @@
from mitmproxy.utils import compat
def test_simple():
assert compat.Server
assert compat.Client