Merge branch 'master' into no-dev-scripts

This commit is contained in:
Maximilian Hils 2021-01-21 11:05:05 +01:00 committed by GitHub
commit 0a4e2c9b59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 393 additions and 189 deletions

View File

@ -41,6 +41,8 @@ If you depend on these features, please raise your voice in
* mitmproxy's command line interface now supports Windows (@mhils) * mitmproxy's command line interface now supports Windows (@mhils)
* The `clientconnect`, `clientdisconnect`, `serverconnect`, `serverdisconnect`, and `log` * The `clientconnect`, `clientdisconnect`, `serverconnect`, `serverdisconnect`, and `log`
events have been replaced with new events, see addon documentation for details (@mhils) events have been replaced with new events, see addon documentation for details (@mhils)
* Contentviews now implement `render_priority` instead of `should_render`, allowing more specialization (@mhils)
* Automatic JSON view mode when `+json` suffix in content type (@kam800)
* Use pyca/cryptography to generate certificates, not pyOpenSSL (@mhils) * Use pyca/cryptography to generate certificates, not pyOpenSSL (@mhils)
* Remove the legacy protocol stack (@Kriechi) * Remove the legacy protocol stack (@Kriechi)
* Remove all deprecated pathod and pathoc tools and modules (@Kriechi) * Remove all deprecated pathod and pathoc tools and modules (@Kriechi)

View File

@ -5,16 +5,32 @@ This example shows how one can add a custom contentview to mitmproxy,
which is used to pretty-print HTTP bodies for example. which is used to pretty-print HTTP bodies for example.
The content view API is explained in the mitmproxy.contentviews module. The content view API is explained in the mitmproxy.contentviews module.
""" """
from mitmproxy import contentviews from typing import Optional
from mitmproxy import contentviews, flow
from mitmproxy.net import http
class ViewSwapCase(contentviews.View): class ViewSwapCase(contentviews.View):
name = "swapcase" name = "swapcase"
content_types = ["text/plain"]
def __call__(self, data, **metadata) -> contentviews.TViewResult: def __call__(self, data, **metadata) -> contentviews.TViewResult:
return "case-swapped text", contentviews.format_text(data.swapcase()) return "case-swapped text", contentviews.format_text(data.swapcase())
def render_priority(
self,
data: bytes,
*,
content_type: Optional[str] = None,
flow: Optional[flow.Flow] = None,
http_message: Optional[http.Message] = None,
**unknown_metadata,
) -> float:
if content_type == "text/plain":
return 1
else:
return 0
view = ViewSwapCase() view = ViewSwapCase()

View File

@ -1,19 +1,20 @@
import itertools import itertools
import shutil import shutil
import sys import sys
from typing import Optional, TextIO from typing import Optional, TextIO, Union
import click import click
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 import http
from mitmproxy.net import http as net_http from mitmproxy.net import http as net_http
from mitmproxy.tcp import TCPFlow, TCPMessage
from mitmproxy.utils import human from mitmproxy.utils import human
from mitmproxy.utils import strutils from mitmproxy.utils import strutils
from mitmproxy.websocket import WebSocketFlow, WebSocketMessage
def indent(n: int, text: str) -> str: def indent(n: int, text: str) -> str:
@ -94,7 +95,11 @@ class Dumper:
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: flow.Flow): def _echo_message(
self,
message: Union[net_http.Message, TCPMessage, WebSocketMessage],
flow: Union[http.HTTPFlow, TCPFlow, WebSocketFlow]
):
_, 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,
@ -165,8 +170,8 @@ class Dumper:
http_version = "" http_version = ""
if ( if (
flow.request.http_version not in ("HTTP/1.1", "HTTP/1.0") not (flow.request.is_http10 or flow.request.is_http11)
or flow.request.http_version != getattr(flow.response, "http_version", "HTTP/1.1") or flow.request.http_version != getattr(flow.response, "http_version", "HTTP/1.1")
): ):
# Hide version for h1 <-> h1 connections. # Hide version for h1 <-> h1 connections.
http_version = " " + flow.request.http_version http_version = " " + flow.request.http_version
@ -215,8 +220,8 @@ class Dumper:
http_version = "" http_version = ""
if ( if (
flow.response.http_version not in ("HTTP/1.1", "HTTP/1.0") not (flow.response.is_http10 or flow.response.is_http11)
or flow.request.http_version != flow.response.http_version or flow.request.http_version != flow.response.http_version
): ):
# Hide version for h1 <-> h1 connections. # Hide version for h1 <-> h1 connections.
http_version = f"{flow.response.http_version} " http_version = f"{flow.response.http_version} "
@ -226,7 +231,8 @@ class Dumper:
# 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
pad = max(0, len(human.format_address(flow.client_conn.peername)) - (2 + len(http_version) + len(replay_str))) pad = max(0,
len(human.format_address(flow.client_conn.peername)) - (2 + len(http_version) + len(replay_str)))
arrows = " " * pad + arrows arrows = " " * pad + arrows
self.echo(f"{replay}{arrows} {http_version}{code} {reason} {size}") self.echo(f"{replay}{arrows} {http_version}{code} {reason} {size}")

View File

@ -3,21 +3,19 @@ Mitmproxy Content Views
======================= =======================
mitmproxy includes a set of content views which can be used to mitmproxy includes a set of content views which can be used to
format/decode/highlight data. While they are currently used for HTTP message format/decode/highlight data. While they are mostly used for HTTP message
bodies only, the may be used in other contexts in the future, e.g. to decode bodies, the may be used in other contexts, e.g. to decode WebSocket messages.
protobuf messages sent as WebSocket frames.
Thus, the View API is very minimalistic. The only arguments are `data` and Thus, the View API is very minimalistic. The only arguments are `data` and
`**metadata`, where `data` is the actual content (as bytes). The contents on `**metadata`, where `data` is the actual content (as bytes). The contents on
metadata depend on the protocol in use. For HTTP, the message headers and metadata depend on the protocol in use. Known attributes can be found in
message trailers are passed as the ``headers`` and ``trailers`` keyword `base.View`.
argument. For HTTP requests, the query parameters are passed as the ``query``
keyword argument.
""" """
import traceback import traceback
from typing import Dict, Optional # noqa from typing import List, Union
from typing import List # noqa from typing import Optional
from mitmproxy import flow
from mitmproxy.net import http from mitmproxy.net import http
from mitmproxy.utils import strutils from mitmproxy.utils import strutils
from . import ( from . import (
@ -25,9 +23,11 @@ from . import (
urlencoded, multipart, image, query, protobuf, msgpack urlencoded, multipart, image, query, protobuf, msgpack
) )
from .base import View, KEY_MAX, format_text, format_dict, TViewResult from .base import View, KEY_MAX, format_text, format_dict, TViewResult
from ..http import HTTPFlow
from ..tcp import TCPMessage, TCPFlow
from ..websocket import WebSocketMessage, WebSocketFlow
views: List[View] = [] views: List[View] = []
content_types_map: Dict[str, List[View]] = {}
def get(name: str) -> Optional[View]: def get(name: str) -> Optional[View]:
@ -45,19 +45,8 @@ def add(view: View) -> None:
views.append(view) views.append(view)
for ct in view.content_types:
l = content_types_map.setdefault(ct, [])
l.append(view)
def remove(view: View) -> None: def remove(view: View) -> None:
for ct in view.content_types:
l = content_types_map.setdefault(ct, [])
l.remove(view)
if not len(l):
del content_types_map[ct]
views.remove(view) views.remove(view)
@ -75,16 +64,24 @@ def safe_to_print(lines, encoding="utf8"):
yield clean_line yield clean_line
def get_message_content_view(viewname, message, flow): def get_message_content_view(
viewname: str,
message: Union[http.Message, TCPMessage, WebSocketMessage],
flow: Union[HTTPFlow, TCPFlow, WebSocketFlow],
):
""" """
Like get_content_view, but also handles message encoding. Like get_content_view, but also handles message encoding.
""" """
viewmode = get(viewname) viewmode = get(viewname)
if not viewmode: if not viewmode:
viewmode = get("auto") viewmode = get("auto")
assert viewmode
content: Optional[bytes]
try: try:
content = message.content content = message.content # type: ignore
except ValueError: except ValueError:
assert isinstance(message, http.Message)
content = message.raw_content content = message.raw_content
enc = "[cannot decode]" enc = "[cannot decode]"
else: else:
@ -93,22 +90,24 @@ def get_message_content_view(viewname, message, flow):
message.headers.get("content-encoding") message.headers.get("content-encoding")
) )
else: else:
enc = None enc = ""
if content is None: if content is None:
return "", iter([[("error", "content missing")]]), None return "", iter([[("error", "content missing")]]), None
metadata = {} content_type = None
if isinstance(message, http.Request): http_message = None
metadata["query"] = message.query
if isinstance(message, http.Message): if isinstance(message, http.Message):
metadata["headers"] = message.headers http_message = message
metadata["trailers"] = message.trailers if ctype := message.headers.get("content-type"):
metadata["message"] = message if ct := http.parse_content_type(ctype):
metadata["flow"] = flow content_type = f"{ct[0]}/{ct[1]}"
description, lines, error = get_content_view( description, lines, error = get_content_view(
viewmode, content, **metadata viewmode, content,
content_type=content_type,
flow=flow,
http_message=http_message,
) )
if enc: if enc:
@ -117,7 +116,11 @@ def get_message_content_view(viewname, message, flow):
return description, lines, error return description, lines, error
def get_tcp_content_view(viewname: str, data: bytes): def get_tcp_content_view(
viewname: str,
data: bytes,
flow: TCPFlow,
):
viewmode = get(viewname) viewmode = get(viewname)
if not viewmode: if not viewmode:
viewmode = get("auto") viewmode = get("auto")
@ -125,12 +128,19 @@ def get_tcp_content_view(viewname: str, data: bytes):
# https://github.com/mitmproxy/mitmproxy/pull/3970#issuecomment-623024447 # https://github.com/mitmproxy/mitmproxy/pull/3970#issuecomment-623024447
assert viewmode assert viewmode
description, lines, error = get_content_view(viewmode, data) description, lines, error = get_content_view(viewmode, data, flow=flow)
return description, lines, error return description, lines, error
def get_content_view(viewmode: View, data: bytes, **metadata): def get_content_view(
viewmode: View,
data: bytes,
*,
content_type: Optional[str] = None,
flow: Optional[flow.Flow] = None,
http_message: Optional[http.Message] = None,
):
""" """
Args: Args:
viewmode: the view to use. viewmode: the view to use.
@ -143,9 +153,11 @@ def get_content_view(viewmode: View, data: bytes, **metadata):
In contrast to calling the views directly, text is always safe-to-print unicode. In contrast to calling the views directly, text is always safe-to-print unicode.
""" """
try: try:
ret = viewmode(data, **metadata) ret = viewmode(data, content_type=content_type, flow=flow, http_message=http_message)
if ret is None: if ret is None:
ret = "Couldn't parse: falling back to Raw", get("Raw")(data, **metadata)[1] ret = "Couldn't parse: falling back to Raw", get("Raw")(
data, content_type=content_type, flow=flow, http_message=http_message
)[1]
desc, content = ret desc, content = ret
error = None error = None
# Third-party viewers can fail in unexpected ways... # Third-party viewers can fail in unexpected ways...
@ -153,11 +165,8 @@ def get_content_view(viewmode: View, data: bytes, **metadata):
desc = "Couldn't parse: falling back to Raw" desc = "Couldn't parse: falling back to Raw"
raw = get("Raw") raw = get("Raw")
assert raw assert raw
content = raw(data, **metadata)[1] content = raw(data, content_type=content_type, flow=flow, http_message=http_message)[1]
error = "{} Content viewer failed: \n{}".format( error = f"{getattr(viewmode, 'name')} content viewer failed: \n{traceback.format_exc()}"
getattr(viewmode, "name"),
traceback.format_exc()
)
return desc, safe_to_print(content), error return desc, safe_to_print(content), error

View File

@ -1,6 +1,4 @@
from mitmproxy import contentviews from mitmproxy import contentviews
from mitmproxy.net import http
from mitmproxy.utils import strutils
from . import base from . import base
@ -8,21 +6,15 @@ class ViewAuto(base.View):
name = "Auto" name = "Auto"
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
headers = metadata.get("headers", {}) # TODO: The auto view has little justification now that views implement render_priority,
ctype = headers.get("content-type") # but we keep it around for now to not touch more parts.
if data and ctype: priority, view = max(
ct = http.parse_content_type(ctype) if ctype else None (v.render_priority(data, **metadata), v)
ct = "{}/{}".format(ct[0], ct[1]) for v in contentviews.views
if ct in contentviews.content_types_map: )
return contentviews.content_types_map[ct][0](data, **metadata) if priority == 0 and not data:
elif strutils.is_xml(data):
return contentviews.get("XML/HTML")(data, **metadata)
elif ct.startswith("image/"):
return contentviews.get("Image")(data, **metadata)
if metadata.get("query"):
return contentviews.get("Query")(data, **metadata)
if data and strutils.is_mostly_bin(data):
return contentviews.get("Hex")(data)
if not data:
return "No content", [] return "No content", []
return contentviews.get("Raw")(data) return view(data, **metadata)
def render_priority(self, data: bytes, **metadata) -> float:
return -1 # don't recurse.

View File

@ -1,5 +1,9 @@
# Default view cutoff *in lines* # Default view cutoff *in lines*
import typing import typing
from abc import ABC, abstractmethod
from mitmproxy import flow
from mitmproxy.net import http
KEY_MAX = 30 KEY_MAX = 30
@ -8,37 +12,62 @@ TViewLine = typing.List[typing.Tuple[str, TTextType]]
TViewResult = typing.Tuple[str, typing.Iterator[TViewLine]] TViewResult = typing.Tuple[str, typing.Iterator[TViewLine]]
class View: class View(ABC):
name: typing.ClassVar[str] name: typing.ClassVar[str]
content_types: typing.ClassVar[typing.List[str]] = []
def __call__(self, data: bytes, **metadata) -> TViewResult: @abstractmethod
def __call__(
self,
data: bytes,
*,
content_type: typing.Optional[str] = None,
flow: typing.Optional[flow.Flow] = None,
http_message: typing.Optional[http.Message] = None,
**unknown_metadata,
) -> TViewResult:
""" """
Transform raw data into human-readable output. Transform raw data into human-readable output.
Args: Returns a (description, content generator) tuple.
data: the data to decode/format. The content generator yields lists of (style, text) tuples, where each list represents
metadata: optional keyword-only arguments for metadata. Implementations must not a single line. ``text`` is a unfiltered string which may need to be escaped,
rely on a given argument being present. depending on the used output. For example, it may contain terminal control sequences
or unfiltered HTML.
Returns: Except for `data`, implementations must not rely on any given argument to be present.
A (description, content generator) tuple. To ensure compatibility with future mitmproxy versions, unknown keyword arguments should be ignored.
The content generator yields lists of (style, text) tuples, where each list represents The content generator must not yield tuples of tuples, because urwid cannot process that.
a single line. ``text`` is a unfiltered byte string which may need to be escaped, You have to yield a *list* of tuples per line.
depending on the used output.
Caveats:
The content generator must not yield tuples of tuples,
because urwid cannot process that. You have to yield a *list* of tuples per line.
""" """
raise NotImplementedError() # pragma: no cover raise NotImplementedError() # pragma: no cover
def render_priority(
self,
data: bytes,
*,
content_type: typing.Optional[str] = None,
flow: typing.Optional[flow.Flow] = None,
http_message: typing.Optional[http.Message] = None,
**unknown_metadata,
) -> float:
"""
Return the priority of this view for rendering `data`.
If no particular view is chosen by the user, the view with the highest priority is selected.
Except for `data`, implementations must not rely on any given argument to be present.
To ensure compatibility with future mitmproxy versions, unknown keyword arguments should be ignored.
"""
return 0
def __lt__(self, other):
assert isinstance(other, View)
return self.name.__lt__(other.name)
def format_pairs( def format_pairs(
items: typing.Iterable[typing.Tuple[TTextType, TTextType]] items: typing.Iterable[typing.Tuple[TTextType, TTextType]]
) -> typing.Iterator[TViewLine]: ) -> typing.Iterator[TViewLine]:
""" """
Helper function that accepts a list of (k,v) pairs into a list of Helper function that accepts a list of (k,v) pairs into a list of
[ [
@ -67,7 +96,7 @@ def format_pairs(
def format_dict( def format_dict(
d: typing.Mapping[TTextType, TTextType] d: typing.Mapping[TTextType, TTextType]
) -> typing.Iterator[TViewLine]: ) -> typing.Iterator[TViewLine]:
""" """
Helper function that transforms the given dictionary into a list of Helper function that transforms the given dictionary into a list of

View File

@ -1,5 +1,6 @@
import re import re
import time import time
from typing import Optional
from mitmproxy.contentviews import base from mitmproxy.contentviews import base
from mitmproxy.utils import strutils from mitmproxy.utils import strutils
@ -50,15 +51,15 @@ def beautify(data: str, indent: str = " "):
class ViewCSS(base.View): class ViewCSS(base.View):
name = "CSS" name = "CSS"
content_types = [
"text/css"
]
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
data = data.decode("utf8", "surrogateescape") data = data.decode("utf8", "surrogateescape")
beautified = beautify(data) beautified = beautify(data)
return "CSS", base.format_text(beautified) return "CSS", base.format_text(beautified)
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(content_type == "text/css")
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
with open("../tools/web/static/vendor.css") as f: with open("../tools/web/static/vendor.css") as f:

View File

@ -16,3 +16,6 @@ class ViewHex(base.View):
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
return "Hex", self._format(data) return "Hex", self._format(data)
def render_priority(self, data: bytes, **metadata) -> float:
return 0.2 * strutils.is_mostly_bin(data)

View File

@ -1,4 +1,5 @@
import imghdr import imghdr
from typing import Optional
from mitmproxy.contentviews import base from mitmproxy.contentviews import base
from mitmproxy.coretypes import multidict from mitmproxy.coretypes import multidict
@ -16,16 +17,6 @@ imghdr.tests.append(test_ico)
class ViewImage(base.View): class ViewImage(base.View):
name = "Image" name = "Image"
# there is also a fallback in the auto view for image/*.
content_types = [
"image/png",
"image/jpeg",
"image/gif",
"image/vnd.microsoft.icon",
"image/x-icon",
"image/webp",
]
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
image_type = imghdr.what('', h=data) image_type = imghdr.what('', h=data)
if image_type == 'png': if image_type == 'png':
@ -45,3 +36,10 @@ class ViewImage(base.View):
else: else:
view_name = "Unknown Image" view_name = "Unknown Image"
return view_name, base.format_dict(multidict.MultiDict(image_metadata)) return view_name, base.format_dict(multidict.MultiDict(image_metadata))
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(bool(
content_type
and content_type.startswith("image/")
and content_type != "image/svg+xml"
))

View File

@ -1,5 +1,6 @@
import io import io
import re import re
from typing import Optional
from mitmproxy.utils import strutils from mitmproxy.utils import strutils
from mitmproxy.contentviews import base from mitmproxy.contentviews import base
@ -46,13 +47,16 @@ def beautify(data):
class ViewJavaScript(base.View): class ViewJavaScript(base.View):
name = "JavaScript" name = "JavaScript"
content_types = [ __content_types = (
"application/x-javascript", "application/x-javascript",
"application/javascript", "application/javascript",
"text/javascript" "text/javascript"
] )
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
data = data.decode("utf-8", "replace") data = data.decode("utf-8", "replace")
res = beautify(data) res = beautify(data)
return "JavaScript", base.format_text(res) return "JavaScript", base.format_text(res)
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(content_type in self.__content_types)

View File

@ -38,13 +38,18 @@ def format_json(data: typing.Any) -> typing.Iterator[base.TViewLine]:
class ViewJSON(base.View): class ViewJSON(base.View):
name = "JSON" name = "JSON"
content_types = [
"application/json",
"application/json-rpc",
"application/vnd.api+json"
]
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
data = parse_json(data) data = parse_json(data)
if data is not PARSE_ERROR: if data is not PARSE_ERROR:
return "JSON", format_json(data) return "JSON", format_json(data)
def render_priority(self, data: bytes, *, content_type: typing.Optional[str] = None, **metadata) -> float:
if content_type in (
"application/json",
"application/json-rpc",
):
return 1
if content_type and content_type.startswith("application/") and content_type.endswith("+json"):
return 1
return 0

View File

@ -39,12 +39,15 @@ def format_msgpack(data):
class ViewMsgPack(base.View): class ViewMsgPack(base.View):
name = "MsgPack" name = "MsgPack"
content_types = [ __content_types = (
"application/msgpack", "application/msgpack",
"application/x-msgpack", "application/x-msgpack",
] )
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
data = parse_msgpack(data) data = parse_msgpack(data)
if data is not PARSE_ERROR: if data is not PARSE_ERROR:
return "MsgPack", format_msgpack(data) return "MsgPack", format_msgpack(data)
def render_priority(self, data: bytes, *, content_type: typing.Optional[str] = None, **metadata) -> float:
return float(content_type in self.__content_types)

View File

@ -1,19 +1,24 @@
from mitmproxy.net import http from typing import Optional
from mitmproxy.coretypes import multidict from mitmproxy.coretypes import multidict
from mitmproxy.net import http
from . import base from . import base
class ViewMultipart(base.View): class ViewMultipart(base.View):
name = "Multipart Form" name = "Multipart Form"
content_types = ["multipart/form-data"]
@staticmethod @staticmethod
def _format(v): def _format(v):
yield [("highlight", "Form data:\n")] yield [("highlight", "Form data:\n")]
yield from base.format_dict(multidict.MultiDict(v)) yield from base.format_dict(multidict.MultiDict(v))
def __call__(self, data, **metadata): def __call__(self, data: bytes, content_type: Optional[str] = None, **metadata):
headers = metadata.get("headers", {}) if content_type is None:
v = http.multipart.decode(headers, data) return
v = http.multipart.decode(content_type, data)
if v: if v:
return "Multipart form", self._format(v) return "Multipart form", self._format(v)
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(content_type == "multipart/form-data")

View File

@ -1,4 +1,5 @@
import io import io
from typing import Optional
from kaitaistruct import KaitaiStream from kaitaistruct import KaitaiStream
from . import base from . import base
@ -66,7 +67,7 @@ class ViewProtobuf(base.View):
""" """
name = "Protocol Buffer" name = "Protocol Buffer"
content_types = [ __content_types = [
"application/x-protobuf", "application/x-protobuf",
"application/x-protobuffer", "application/x-protobuffer",
] ]
@ -77,3 +78,6 @@ class ViewProtobuf(base.View):
raise ValueError("Failed to parse input.") raise ValueError("Failed to parse input.")
return "Protobuf", base.format_text(decoded) return "Protobuf", base.format_text(decoded)
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(content_type in self.__content_types)

View File

@ -1,14 +1,18 @@
from typing import List # noqa from typing import Optional
from . import base from . import base
from ..net import http
class ViewQuery(base.View): class ViewQuery(base.View):
name = "Query" name = "Query"
def __call__(self, data, **metadata): def __call__(self, data: bytes, http_message: Optional[http.Message] = None, **metadata):
query = metadata.get("query") query = getattr(http_message, "query", None)
if query: if query:
return "Query", base.format_pairs(query.items(multi=True)) return "Query", base.format_pairs(query.items(multi=True))
else: else:
return "Query", base.format_text("") return "Query", base.format_text("")
def render_priority(self, data: bytes, *, http_message: Optional[http.Message] = None, **metadata) -> float:
return 0.3 * float(bool(getattr(http_message, "query", False)))

View File

@ -9,3 +9,6 @@ class ViewRaw(base.View):
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
return "Raw", base.format_text(strutils.bytes_to_escaped_str(data, True)) return "Raw", base.format_text(strutils.bytes_to_escaped_str(data, True))
def render_priority(self, data: bytes, **metadata) -> float:
return 0.1 * float(bool(data))

View File

@ -1,10 +1,11 @@
from typing import Optional
from mitmproxy.net.http import url from mitmproxy.net.http import url
from . import base from . import base
class ViewURLEncoded(base.View): class ViewURLEncoded(base.View):
name = "URL-encoded" name = "URL-encoded"
content_types = ["application/x-www-form-urlencoded"]
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
try: try:
@ -13,3 +14,6 @@ class ViewURLEncoded(base.View):
return None return None
d = url.decode(data) d = url.decode(data)
return "URLEncoded form", base.format_pairs(d) return "URLEncoded form", base.format_pairs(d)
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(content_type == "application/x-www-form-urlencoded")

View File

@ -1,13 +1,15 @@
from typing import Optional
from mitmproxy.contrib.wbxml import ASCommandResponse from mitmproxy.contrib.wbxml import ASCommandResponse
from . import base from . import base
class ViewWBXML(base.View): class ViewWBXML(base.View):
name = "WBXML" name = "WBXML"
content_types = [ __content_types = (
"application/vnd.wap.wbxml", "application/vnd.wap.wbxml",
"application/vnd.ms-sync.wbxml" "application/vnd.ms-sync.wbxml"
] )
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
try: try:
@ -17,3 +19,6 @@ class ViewWBXML(base.View):
return "WBXML", base.format_text(parsedContent) return "WBXML", base.format_text(parsedContent)
except: except:
return None return None
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
return float(content_type in self.__content_types)

View File

@ -4,7 +4,7 @@ import textwrap
from typing import Iterable, Optional from typing import Iterable, Optional
from mitmproxy.contentviews import base from mitmproxy.contentviews import base
from mitmproxy.utils import sliding_window from mitmproxy.utils import sliding_window, strutils
""" """
A custom XML/HTML prettifier. Compared to other prettifiers, its main features are: A custom XML/HTML prettifier. Compared to other prettifiers, its main features are:
@ -214,7 +214,7 @@ def format_xml(tokens: Iterable[Token]) -> str:
class ViewXmlHtml(base.View): class ViewXmlHtml(base.View):
name = "XML/HTML" name = "XML/HTML"
content_types = ["text/xml", "text/html"] __content_types = ("text/xml", "text/html")
def __call__(self, data, **metadata): def __call__(self, data, **metadata):
# TODO: # TODO:
@ -233,3 +233,10 @@ class ViewXmlHtml(base.View):
else: else:
t = "XML" t = "XML"
return t, pretty return t, pretty
def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float:
if content_type in self.__content_types:
return 1
elif strutils.is_xml(data):
return 0.4
return float(content_type in self.__content_types)

View File

@ -1,4 +1,5 @@
import re import re
import warnings
from dataclasses import dataclass, is_dataclass, fields from dataclasses import dataclass, is_dataclass, fields
from typing import ClassVar, Any, Dict, Type, Set, List, TYPE_CHECKING, Sequence from typing import ClassVar, Any, Dict, Type, Set, List, TYPE_CHECKING, Sequence
@ -32,7 +33,7 @@ class Hook:
cls.name = re.sub('(?!^)([A-Z]+)', r'_\1', name).lower() cls.name = re.sub('(?!^)([A-Z]+)', r'_\1', name).lower()
if cls.name in all_hooks: if cls.name in all_hooks:
other = all_hooks[cls.name] other = all_hooks[cls.name]
raise RuntimeError(f"Two conflicting event classes for {cls.name}: {cls} and {other}") warnings.warn(f"Two conflicting event classes for {cls.name}: {cls} and {other}", RuntimeWarning)
if cls.name == "": if cls.name == "":
return # don't register Hook class. return # don't register Hook class.
all_hooks[cls.name] = cls all_hooks[cls.name] = cls

View File

@ -65,9 +65,9 @@ class HTTPFlow(flow.Flow):
def make_error_response( def make_error_response(
status_code: int, status_code: int,
message: str = "", message: str = "",
headers: Optional[http.Headers] = None, headers: Optional[http.Headers] = None,
) -> http.Response: ) -> http.Response:
body: bytes = """ body: bytes = """
<html> <html>

View File

@ -69,7 +69,7 @@ class Master:
print(exc, file=sys.stderr) print(exc, file=sys.stderr)
print("mitmproxy has crashed!", file=sys.stderr) print("mitmproxy has crashed!", file=sys.stderr)
print("Please lodge a bug report at:", file=sys.stderr) print("Please lodge a bug report at:", file=sys.stderr)
print("\thttps://github.com/mitmproxy/mitmproxy", file=sys.stderr) print("\thttps://github.com/mitmproxy/mitmproxy/issues", file=sys.stderr)
self.addons.trigger(hooks.DoneHook()) self.addons.trigger(hooks.DoneHook())

View File

@ -1,11 +1,12 @@
import re
import mimetypes import mimetypes
import re
from typing import Tuple, List, Optional
from urllib.parse import quote from urllib.parse import quote
from mitmproxy.net.http import headers from mitmproxy.net.http import headers
def encode(head, l): def encode(head, l):
k = head.get("content-type") k = head.get("content-type")
if k: if k:
k = headers.parse_content_type(k) k = headers.parse_content_type(k)
@ -38,17 +39,16 @@ def encode(head, l):
return temp return temp
def decode(hdrs, content): def decode(content_type: Optional[str], content: bytes) -> List[Tuple[bytes, bytes]]:
""" """
Takes a multipart boundary encoded string and returns list of (key, value) tuples. Takes a multipart boundary encoded string and returns list of (key, value) tuples.
""" """
v = hdrs.get("content-type") if content_type:
if v: ct = headers.parse_content_type(content_type)
v = headers.parse_content_type(v) if not ct:
if not v:
return [] return []
try: try:
boundary = v[2]["boundary"].encode("ascii") boundary = ct[2]["boundary"].encode("ascii")
except (KeyError, UnicodeError): except (KeyError, UnicodeError):
return [] return []

View File

@ -449,7 +449,7 @@ class Request(message.Message):
is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower() is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower()
if is_valid_content_type: if is_valid_content_type:
try: try:
return multipart.decode(self.headers, self.content) return multipart.decode(self.headers.get("content-type"), self.content)
except ValueError: except ValueError:
pass pass
return () return ()

View File

@ -4,6 +4,7 @@ from abc import ABCMeta
from enum import Flag from enum import Flag
from typing import List, Literal, Optional, Sequence, Tuple, Union, TYPE_CHECKING from typing import List, Literal, Optional, Sequence, Tuple, Union, TYPE_CHECKING
import mitmproxy
from mitmproxy import certs from mitmproxy import certs
from mitmproxy.coretypes import serializable from mitmproxy.coretypes import serializable
from mitmproxy.net import server_spec from mitmproxy.net import server_spec

View File

@ -5,6 +5,7 @@ The counterpart to events are commands.
""" """
import socket import socket
import typing import typing
import warnings
from dataclasses import dataclass, is_dataclass from dataclasses import dataclass, is_dataclass
from mitmproxy.proxy import commands from mitmproxy.proxy import commands
@ -78,7 +79,7 @@ class CommandCompleted(Event):
raise RuntimeError(f"{command_cls} needs a properly annotated command attribute.") raise RuntimeError(f"{command_cls} needs a properly annotated command attribute.")
if command_cls in command_reply_subclasses: if command_cls in command_reply_subclasses:
other = command_reply_subclasses[command_cls] other = command_reply_subclasses[command_cls]
raise RuntimeError(f"Two conflicting subclasses for {command_cls}: {cls} and {other}") warnings.warn(f"Two conflicting subclasses for {command_cls}: {cls} and {other}", RuntimeWarning)
command_reply_subclasses[command_cls] = cls command_reply_subclasses[command_cls] = cls
def __repr__(self): def __repr__(self):

View File

@ -222,7 +222,7 @@ class NextLayer(Layer):
# 3. Some layers may however still have a reference to the old .handle_event. # 3. Some layers may however still have a reference to the old .handle_event.
# ._handle is just an optimization to reduce the callstack in these cases. # ._handle is just an optimization to reduce the callstack in these cases.
self.handle_event = self.layer.handle_event self.handle_event = self.layer.handle_event
self._handle_event = self.layer._handle_event self._handle_event = self.layer.handle_event
self._handle = self.layer.handle_event self._handle = self.layer.handle_event
# Utility methods for whoever decides what the next layer is going to be. # Utility methods for whoever decides what the next layer is going to be.

View File

@ -152,7 +152,7 @@ class FlowDetails(tabs.Tabs):
from_client = flow.messages[0].from_client from_client = flow.messages[0].from_client
for m in messages: for m in messages:
_, lines, _ = contentviews.get_tcp_content_view(viewmode, m) _, lines, _ = contentviews.get_tcp_content_view(viewmode, m, flow)
for line in lines: for line in lines:
if from_client: if from_client:

View File

@ -5,7 +5,7 @@ import shutil
import time import time
import typing import typing
from mitmproxy import contentviews from mitmproxy import contentviews, http
from mitmproxy import ctx from mitmproxy import ctx
from mitmproxy import flowfilter from mitmproxy import flowfilter
from mitmproxy import io, flow from mitmproxy import io, flow
@ -49,6 +49,7 @@ def save_flows(path: pathlib.Path, flows: typing.Iterable[flow.Flow]) -> None:
def save_flows_content(path: pathlib.Path, flows: typing.Iterable[flow.Flow]) -> None: def save_flows_content(path: pathlib.Path, flows: typing.Iterable[flow.Flow]) -> None:
for f in flows: for f in flows:
assert isinstance(f, http.HTTPFlow)
for m in ('request', 'response'): for m in ('request', 'response'):
message = getattr(f, m) message = getattr(f, m)
message_path = path / "flows" / f.id / m message_path = path / "flows" / f.id / m

View File

@ -156,7 +156,7 @@ def check():
# Check for underscores in the options. Options always follow '--'. # Check for underscores in the options. Options always follow '--'.
for argument in args: for argument in args:
underscoreParam = re.search('[-]{2}((.*?_)(.*?(\s|$)))+', argument) underscoreParam = re.search(r'[-]{2}((.*?_)(.*?(\s|$)))+', argument)
if underscoreParam is not None: if underscoreParam is not None:
print("{} uses underscores, please use hyphens {}".format( print("{} uses underscores, please use hyphens {}".format(
argument, argument,

View File

@ -133,7 +133,11 @@ def is_mostly_bin(s: bytes) -> bool:
def is_xml(s: bytes) -> bool: def is_xml(s: bytes) -> bool:
return s.strip().startswith(b"<") for char in s:
if char in (9, 10, 32): # is space?
continue
return char == 60 # is a "<"?
return False
def clean_hanging_newline(t): def clean_hanging_newline(t):

View File

@ -97,7 +97,7 @@ setup(
"pydivert>=2.0.3,<2.2", "pydivert>=2.0.3,<2.2",
], ],
'dev': [ 'dev': [
"hypothesis>=5.8,<6", "hypothesis>=5.8,<6.1",
"parver>=0.1,<2.0", "parver>=0.1,<2.0",
"pytest-asyncio>=0.10.0,<0.14,!=0.14", "pytest-asyncio>=0.10.0,<0.14,!=0.14",
"pytest-cov>=2.7.1,<3", "pytest-cov>=2.7.1,<3",

View File

@ -15,3 +15,15 @@ def test_view_image(tdata):
assert img.split(".")[-1].upper() in viewname assert img.split(".")[-1].upper() in viewname
assert v(b"flibble") == ('Unknown Image', [[('header', 'Image Format: '), ('text', 'unknown')]]) assert v(b"flibble") == ('Unknown Image', [[('header', 'Image Format: '), ('text', 'unknown')]])
def test_render_priority():
v = image.ViewImage()
assert v.render_priority(b"", content_type="image/png")
assert v.render_priority(b"", content_type="image/jpeg")
assert v.render_priority(b"", content_type="image/gif")
assert v.render_priority(b"", content_type="image/vnd.microsoft.icon")
assert v.render_priority(b"", content_type="image/x-icon")
assert v.render_priority(b"", content_type="image/webp")
assert v.render_priority(b"", content_type="image/future-unknown-format-42")
assert not v.render_priority(b"", content_type="image/svg+xml")

View File

@ -3,14 +3,18 @@ from unittest import mock
import pytest import pytest
from mitmproxy import contentviews from mitmproxy import contentviews
from mitmproxy.net.http import Headers
from mitmproxy.test import tflow from mitmproxy.test import tflow
from mitmproxy.test import tutils from mitmproxy.test import tutils
class TestContentView(contentviews.View): class TestContentView(contentviews.View):
name = "test" name = "test"
content_types = ["test/123"]
def __call__(self, *args, **kwargs):
pass
def should_render(self, content_type):
return content_type == "test/123"
def test_add_remove(): def test_add_remove():
@ -38,7 +42,7 @@ def test_get_content_view():
desc, lines, err = contentviews.get_content_view( desc, lines, err = contentviews.get_content_view(
contentviews.get("Auto"), contentviews.get("Auto"),
b"[1, 2, 3]", b"[1, 2, 3]",
headers=Headers(content_type="application/json") content_type="application/json",
) )
assert desc == "JSON" assert desc == "JSON"
assert list(lines) assert list(lines)

View File

@ -1,6 +1,5 @@
from mitmproxy.contentviews import auto from mitmproxy.contentviews import auto
from mitmproxy.net import http from mitmproxy.test import tflow
from mitmproxy.coretypes import multidict
from . import full_eval from . import full_eval
@ -8,37 +7,42 @@ def test_view_auto():
v = full_eval(auto.ViewAuto()) v = full_eval(auto.ViewAuto())
f = v( f = v(
b"foo", b"foo",
headers=http.Headers()
) )
assert f[0] == "Raw" assert f[0] == "Raw"
f = v( f = v(
b"<html></html>", b"<html></html>",
headers=http.Headers(content_type="text/html") content_type="text/html",
) )
assert f[0] == "HTML" assert f[0] == "HTML"
f = v( f = v(
b"foo", b"foo",
headers=http.Headers(content_type="text/flibble") content_type="text/flibble",
) )
assert f[0] == "Raw" assert f[0] == "Raw"
f = v( f = v(
b"<xml></xml>", b"<xml></xml>",
headers=http.Headers(content_type="text/flibble") content_type="text/flibble",
) )
assert f[0].startswith("XML") assert f[0].startswith("XML")
f = v( f = v(
b"<svg></svg>", b"<svg></svg>",
headers=http.Headers(content_type="image/svg+xml") content_type="image/svg+xml",
) )
assert f[0].startswith("XML") assert f[0].startswith("XML")
f = v(
b"{}",
content_type="application/acme+json",
)
assert f[0].startswith("JSON")
f = v( f = v(
b"verybinary", b"verybinary",
headers=http.Headers(content_type="image/new-magic-image-format") content_type="image/new-magic-image-format",
) )
assert f[0] == "Unknown Image" assert f[0] == "Unknown Image"
@ -47,13 +51,14 @@ def test_view_auto():
f = v( f = v(
b"", b"",
headers=http.Headers()
) )
assert f[0] == "No content" assert f[0] == "No content"
flow = tflow.tflow()
flow.request.query = [("foo", "bar")]
f = v( f = v(
b"", b"",
headers=http.Headers(), flow=flow,
query=multidict.MultiDict([("foo", "bar")]), http_message=flow.request,
) )
assert f[0] == "Query" assert f[0] == "Query"

View File

@ -37,3 +37,9 @@ def test_simple():
assert v(b"console.log('not really css')") == ( assert v(b"console.log('not really css')") == (
'CSS', [[('text', "console.log('not really css')")]] 'CSS', [[('text', "console.log('not really css')")]]
) )
def test_render_priority():
v = css.ViewCSS()
assert v.render_priority(b"", content_type="text/css")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -5,3 +5,10 @@ from . import full_eval
def test_view_hex(): def test_view_hex():
v = full_eval(hex.ViewHex()) v = full_eval(hex.ViewHex())
assert v(b"foo") assert v(b"foo")
def test_render_priority():
v = hex.ViewHex()
assert not v.render_priority(b"ascii")
assert v.render_priority(b"\xFF")
assert not v.render_priority(b"")

View File

@ -27,3 +27,11 @@ def test_format_xml(filename, tdata):
expected = f.read() expected = f.read()
js = javascript.beautify(input) js = javascript.beautify(input)
assert js == expected assert js == expected
def test_render_priority():
v = javascript.ViewJavaScript()
assert v.render_priority(b"", content_type="application/x-javascript")
assert v.render_priority(b"", content_type="application/javascript")
assert v.render_priority(b"", content_type="text/javascript")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -41,3 +41,12 @@ def test_view_json():
def test_view_json_doesnt_crash(data): def test_view_json_doesnt_crash(data):
v = full_eval(json.ViewJSON()) v = full_eval(json.ViewJSON())
v(data) v(data)
def test_render_priority():
v = json.ViewJSON()
assert v.render_priority(b"", content_type="application/json")
assert v.render_priority(b"", content_type="application/json-rpc")
assert v.render_priority(b"", content_type="application/vnd.api+json")
assert v.render_priority(b"", content_type="application/acme+json")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -44,3 +44,10 @@ def test_view_msgpack():
def test_view_msgpack_doesnt_crash(data): def test_view_msgpack_doesnt_crash(data):
v = full_eval(msgpack.ViewMsgPack()) v = full_eval(msgpack.ViewMsgPack())
v(data) v(data)
def test_render_priority():
v = msgpack.ViewMsgPack()
assert v.render_priority(b"", content_type="application/msgpack")
assert v.render_priority(b"", content_type="application/x-msgpack")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -1,5 +1,4 @@
from mitmproxy.contentviews import multipart from mitmproxy.contentviews import multipart
from mitmproxy.net import http
from . import full_eval from . import full_eval
@ -12,14 +11,16 @@ Content-Disposition: form-data; name="submit-name"
Larry Larry
--AaB03x --AaB03x
""".strip() """.strip()
h = http.Headers(content_type="multipart/form-data; boundary=AaB03x") assert view(v, content_type="multipart/form-data; boundary=AaB03x")
assert view(v, headers=h)
h = http.Headers() assert not view(v)
assert not view(v, headers=h)
h = http.Headers(content_type="multipart/form-data") assert not view(v, content_type="multipart/form-data")
assert not view(v, headers=h)
h = http.Headers(content_type="unparseable") assert not view(v, content_type="unparseable")
assert not view(v, headers=h)
def test_render_priority():
v = multipart.ViewMultipart()
assert v.render_priority(b"", content_type="multipart/form-data")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -28,3 +28,10 @@ def test_format_pbuf(filename, tdata):
expected = f.read() expected = f.read()
assert protobuf.format_pbuf(input) == expected assert protobuf.format_pbuf(input) == expected
def test_render_priority():
v = protobuf.ViewProtobuf()
assert v.render_priority(b"", content_type="application/x-protobuf")
assert v.render_priority(b"", content_type="application/x-protobuffer")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -1,13 +1,23 @@
from mitmproxy.contentviews import query from mitmproxy.contentviews import query
from mitmproxy.coretypes import multidict from mitmproxy.test import tutils
from . import full_eval from . import full_eval
def test_view_query(): def test_view_query():
d = "" d = ""
v = full_eval(query.ViewQuery()) v = full_eval(query.ViewQuery())
f = v(d, query=multidict.MultiDict([("foo", "bar"), ("foo", "baz")])) req = tutils.treq()
req.query = [("foo", "bar"), ("foo", "baz")]
f = v(d, http_message=req)
assert f[0] == "Query" assert f[0] == "Query"
assert f[1] == [[("header", "foo: "), ("text", "bar")], [("header", "foo: "), ("text", "baz")]] assert f[1] == [[("header", "foo: "), ("text", "bar")], [("header", "foo: "), ("text", "baz")]]
assert v(d) == ("Query", []) assert v(d) == ("Query", [])
def test_render_priority():
view = query.ViewQuery()
req = tutils.treq()
req.query = [("foo", "bar"), ("foo", "baz")]
assert view.render_priority(b"", http_message=req)
assert not view.render_priority(b"")

View File

@ -5,3 +5,9 @@ from . import full_eval
def test_view_raw(): def test_view_raw():
v = full_eval(raw.ViewRaw()) v = full_eval(raw.ViewRaw())
assert v(b"foo") assert v(b"foo")
def test_render_priority():
v = raw.ViewRaw()
assert v.render_priority(b"anything")
assert not v.render_priority(b"")

View File

@ -13,3 +13,9 @@ def test_view_urlencoded():
assert v(d) assert v(d)
assert not v(b"\xFF\x00") assert not v(b"\xFF\x00")
def test_render_priority():
v = urlencoded.ViewURLEncoded()
assert v.render_priority(b"", content_type="application/x-www-form-urlencoded")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -18,3 +18,10 @@ def test_wbxml(tdata):
p = wbxml.ASCommandResponse.ASCommandResponse(input) p = wbxml.ASCommandResponse.ASCommandResponse(input)
assert p.xmlString == expected assert p.xmlString == expected
def test_render_priority():
v = wbxml.ViewWBXML()
assert v.render_priority(b"", content_type="application/vnd.wap.wbxml")
assert v.render_priority(b"", content_type="application/vnd.ms-sync.wbxml")
assert not v.render_priority(b"", content_type="text/plain")

View File

@ -34,3 +34,12 @@ def test_format_xml(filename, tdata):
expected = f.read() expected = f.read()
tokens = xml_html.tokenize(input) tokens = xml_html.tokenize(input)
assert xml_html.format_xml(tokens) == expected assert xml_html.format_xml(tokens) == expected
def test_render_priority():
v = xml_html.ViewXmlHtml()
assert v.render_priority(b"", content_type="text/xml")
assert v.render_priority(b"", content_type="text/xml")
assert v.render_priority(b"", content_type="text/html")
assert not v.render_priority(b"", content_type="text/plain")
assert v.render_priority(b"<html/>")

View File

@ -1,13 +1,11 @@
import pytest
from mitmproxy.net.http import Headers from mitmproxy.net.http import Headers
from mitmproxy.net.http import multipart from mitmproxy.net.http import multipart
import pytest
def test_decode(): def test_decode():
boundary = 'somefancyboundary' boundary = 'somefancyboundary'
headers = Headers(
content_type='multipart/form-data; boundary=' + boundary
)
content = ( content = (
"--{0}\n" "--{0}\n"
"Content-Disposition: form-data; name=\"field1\"\n\n" "Content-Disposition: form-data; name=\"field1\"\n\n"
@ -17,24 +15,17 @@ def test_decode():
"value2\n" "value2\n"
"--{0}--".format(boundary).encode() "--{0}--".format(boundary).encode()
) )
form = multipart.decode(f'multipart/form-data; boundary={boundary}', content)
form = multipart.decode(headers, content)
assert len(form) == 2 assert len(form) == 2
assert form[0] == (b"field1", b"value1") assert form[0] == (b"field1", b"value1")
assert form[1] == (b"field2", b"value2") assert form[1] == (b"field2", b"value2")
boundary = 'boundary茅莽' boundary = 'boundary茅莽'
headers = Headers( result = multipart.decode(f'multipart/form-data; boundary={boundary}', content)
content_type='multipart/form-data; boundary=' + boundary
)
result = multipart.decode(headers, content)
assert result == [] assert result == []
headers = Headers( assert multipart.decode("", content) == []
content_type=''
)
assert multipart.decode(headers, content) == []
def test_encode(): def test_encode():

View File

@ -31,6 +31,6 @@ def test_command_completed():
class FooCompleted1(events.CommandCompleted): class FooCompleted1(events.CommandCompleted):
command: FooCommand command: FooCommand
with pytest.raises(RuntimeError, match="conflicting subclasses"): with pytest.warns(RuntimeWarning, match="conflicting subclasses"):
class FooCompleted2(events.CommandCompleted): class FooCompleted2(events.CommandCompleted):
command: FooCommand command: FooCommand

View File

@ -24,7 +24,7 @@ def test_hook():
assert e.args() == [b"foo"] assert e.args() == [b"foo"]
assert FooHook in hooks.all_hooks.values() assert FooHook in hooks.all_hooks.values()
with pytest.raises(RuntimeError, match="Two conflicting event classes"): with pytest.warns(RuntimeWarning, match="Two conflicting event classes"):
@dataclass @dataclass
class FooHook2(hooks.Hook): class FooHook2(hooks.Hook):
name = "foo" name = "foo"

View File

@ -83,6 +83,7 @@ def test_is_mostly_bin():
def test_is_xml(): def test_is_xml():
assert not strutils.is_xml(b"")
assert not strutils.is_xml(b"foo") assert not strutils.is_xml(b"foo")
assert strutils.is_xml(b"<foo") assert strutils.is_xml(b"<foo")
assert strutils.is_xml(b" \n<foo") assert strutils.is_xml(b" \n<foo")

View File

@ -40,7 +40,7 @@ commands =
passenv = CI_* GITHUB_* AWS_* TWINE_* DOCKER_* passenv = CI_* GITHUB_* AWS_* TWINE_* DOCKER_*
deps = deps =
-e .[dev] -e .[dev]
pyinstaller==4.1 pyinstaller==4.2
twine==3.3.0 twine==3.3.0
awscli awscli
commands = commands =