Merge branch 'master' into fix-command-bar-issue-3259

This commit is contained in:
Henrique M. D 2019-11-15 13:59:57 -05:00 committed by GitHub
commit 021a141521
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 458 additions and 270 deletions

View File

@ -0,0 +1,86 @@
---
title: "Install System CA on Android"
menu:
howto:
weight: 4
---
# Install System CA Certificate on Android Emulator
[Since Android 7, apps ignore user certificates](https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html), unless they are configured to use them.
As most applications do not explicitly opt in to use user certificates, we need to place our mitmproxy CA certificate in the system certificate store,
in order to avid having to patch each application, which we want to monitor.
Please note, that apps can decide to ignore the system certificate store and maintain their own CA certificates. In this case you have to patch the application.
## 1. Prerequisites
- Emulator from Android SDK with proxy settings pointing to mitmproxy
- Mitmproxy CA certificate
- Usually located in `~/.mitmproxy/mitmproxy-ca-cert.cer`
- If the folder is empty or does not exist, run `mitmproxy` in order to generate the certificates
## 2. Rename certificate
Enter your certificate folder
{{< highlight bash >}}
cd ~/.mitmproxy/
{{< / highlight >}}
- CA Certificates in Android are stored by the name of their hash, with a '0' as extension
- Now generate the hash of your certificate
{{< highlight bash >}}
openssl x509 -inform PEM -subject_hash_old -in mitmproxy-ca-cert.cer | head -1
{{< / highlight >}}
Lets assume, the output is `c8450d0d`
We can now copy `mitmproxy-ca-cert.cer` to `c8450d0d.0` and our system certificate is ready to use
{{< highlight bash >}}
cp mitmproxy-ca-cert.cer c8450d0d.0
{{< / highlight >}}
## 3. Insert certificate into system certificate store
Note, that Android 9 (API LEVEL 28) was used to test the following steps and that the `emulator` executable is located in the Android SDK
- Start your android emulator.
- Get a list of your AVDs with `emulator -list-avds`
- Make sure to use the `-writable-system` option. Otherwise it will not be possible to write to `/system`
- Keep in mind, that the **emulator will load a clean system image when starting without `-writable-system` option**.
- This means you always have to start the emulator with `-writable-system` option in order to use your certificate
{{< highlight bash >}}
emulator -avd <avd_name_here> -writable-system
{{< / highlight >}}
- Restart adb as root
{{< highlight bash >}}
adb root
{{< / highlight >}}
- Get write access to `/system` on the device
- In earlier versions (API LEVEL < 28) of Android you have to use `adb shell "mount -o rw,remount /system"`
{{< highlight bash >}}
adb shell "mount -o rw,remount /"
{{< / highlight >}}
- Push your certificate to the system certificate store and set file permissions
{{< highlight bash >}}
adb push c8450d0d.0 /system/etc/security/cacerts
adb shell "chmod 664 /system/etc/security/cacerts/c8450d0d.0"
{{< / highlight >}}
## 4. Reboot device and enjoy decrypted TLS traffic
- Reboot your device.
- You CA certificate should now be system trusted
{{< highlight bash >}}
adb reboot
{{< / highlight >}}
**Remember**: You **always** have to start the emulator using the `-writable-system` option in order to use your certificate

View File

@ -86,7 +86,7 @@ def get_cookies(flow: http.HTTPFlow) -> Cookies:
return {name: value for name, value in flow.request.cookies.fields} return {name: value for name, value in flow.request.cookies.fields}
def find_unclaimed_URLs(body: str, requestUrl: bytes) -> None: def find_unclaimed_URLs(body, requestUrl):
""" Look for unclaimed URLs in script tags and log them if found""" """ Look for unclaimed URLs in script tags and log them if found"""
def getValue(attrs: List[Tuple[str, str]], attrName: str) -> Optional[str]: def getValue(attrs: List[Tuple[str, str]], attrName: str) -> Optional[str]:
for name, value in attrs: for name, value in attrs:
@ -111,7 +111,7 @@ def find_unclaimed_URLs(body: str, requestUrl: bytes) -> None:
try: try:
socket.gethostbyname(domain) socket.gethostbyname(domain)
except socket.gaierror: except socket.gaierror:
ctx.log.error("XSS found in %s due to unclaimed URL \"%s\"." % (requestUrl, url)) ctx.log.error(f"XSS found in {requestUrl} due to unclaimed URL \"{url}\".")
def test_end_of_URL_injection(original_body: str, request_URL: str, cookies: Cookies) -> VulnData: def test_end_of_URL_injection(original_body: str, request_URL: str, cookies: Cookies) -> VulnData:

View File

@ -126,20 +126,18 @@ class Cut:
format is UTF-8 encoded CSV. If there is exactly one row and one format is UTF-8 encoded CSV. If there is exactly one row and one
column, the data is written to file as-is, with raw bytes preserved. column, the data is written to file as-is, with raw bytes preserved.
""" """
v: typing.Union[str, bytes]
fp = io.StringIO(newline="") fp = io.StringIO(newline="")
if len(cuts) == 1 and len(flows) == 1: if len(cuts) == 1 and len(flows) == 1:
v = extract(cuts[0], flows[0]) v = extract(cuts[0], flows[0])
if isinstance(v, bytes): fp.write(strutils.always_str(v)) # type: ignore
fp.write(strutils.always_str(v))
else:
fp.write(v)
ctx.log.alert("Clipped single cut.") ctx.log.alert("Clipped single cut.")
else: else:
writer = csv.writer(fp) writer = csv.writer(fp)
for f in flows: for f in flows:
vals = [extract(c, f) for c in cuts] vals = [extract(c, f) for c in cuts]
writer.writerow( writer.writerow(
[strutils.always_str(v) or "" for v in vals] # type: ignore [strutils.always_str(v) for v in vals]
) )
ctx.log.alert("Clipped %s cuts as CSV." % len(cuts)) ctx.log.alert("Clipped %s cuts as CSV." % len(cuts))
try: try:

View File

@ -14,7 +14,7 @@ class EventStore:
self.sig_refresh = blinker.Signal() self.sig_refresh = blinker.Signal()
@property @property
def size(self) -> int: def size(self) -> typing.Optional[int]:
return self.data.maxlen return self.data.maxlen
def log(self, entry: LogEntry) -> None: def log(self, entry: LogEntry) -> None:

View File

@ -16,7 +16,7 @@ from mitmproxy import ctx
import mitmproxy.types as mtypes import mitmproxy.types as mtypes
def load_script(path: str) -> types.ModuleType: def load_script(path: str) -> typing.Optional[types.ModuleType]:
fullname = "__mitmproxy_script__.{}".format( fullname = "__mitmproxy_script__.{}".format(
os.path.splitext(os.path.basename(path))[0] os.path.splitext(os.path.basename(path))[0]
) )

View File

@ -68,6 +68,13 @@ class ServerPlayback:
to replay. to replay.
""" """
) )
loader.add_option(
"server_replay_ignore_port", bool, False,
"""
Ignore request's destination port while searching for a saved flow
to replay.
"""
)
@command.command("replay.server") @command.command("replay.server")
def load_flows(self, flows: typing.Sequence[flow.Flow]) -> None: def load_flows(self, flows: typing.Sequence[flow.Flow]) -> None:
@ -110,7 +117,7 @@ class ServerPlayback:
_, _, path, _, query, _ = urllib.parse.urlparse(r.url) _, _, path, _, query, _ = urllib.parse.urlparse(r.url)
queriesArray = urllib.parse.parse_qsl(query, keep_blank_values=True) queriesArray = urllib.parse.parse_qsl(query, keep_blank_values=True)
key: typing.List[typing.Any] = [str(r.port), str(r.scheme), str(r.method), str(path)] key: typing.List[typing.Any] = [str(r.scheme), str(r.method), str(path)]
if not ctx.options.server_replay_ignore_content: if not ctx.options.server_replay_ignore_content:
if ctx.options.server_replay_ignore_payload_params and r.multipart_form: if ctx.options.server_replay_ignore_payload_params and r.multipart_form:
key.extend( key.extend(
@ -128,7 +135,9 @@ class ServerPlayback:
key.append(str(r.raw_content)) key.append(str(r.raw_content))
if not ctx.options.server_replay_ignore_host: if not ctx.options.server_replay_ignore_host:
key.append(r.host) key.append(r.pretty_host)
if not ctx.options.server_replay_ignore_port:
key.append(r.port)
filtered = [] filtered = []
ignore_params = ctx.options.server_replay_ignore_params or [] ignore_params = ctx.options.server_replay_ignore_params or []

View File

@ -215,8 +215,8 @@ class Session:
def __init__(self): def __init__(self):
self.db_store: SessionDB = None self.db_store: SessionDB = None
self._hot_store: collections.OrderedDict = collections.OrderedDict() self._hot_store: collections.OrderedDict = collections.OrderedDict()
self._order_store: typing.Dict[str, typing.Dict[str, typing.Union[int, float, str]]] = {} self._order_store: typing.Dict[str, typing.Dict[str, typing.Union[int, float, str, None]]] = {}
self._view: typing.List[typing.Tuple[typing.Union[int, float, str], str]] = [] self._view: typing.List[typing.Tuple[typing.Union[int, float, str, None], str]] = []
self.order: str = orders[0] self.order: str = orders[0]
self.filter = matchall self.filter = matchall
self._flush_period: float = self._FP_DEFAULT self._flush_period: float = self._FP_DEFAULT

View File

@ -53,6 +53,7 @@ class StickyCookie:
self.flt = None self.flt = None
def response(self, flow: http.HTTPFlow): def response(self, flow: http.HTTPFlow):
assert flow.response
if self.flt: if self.flt:
for name, (value, attrs) in flow.response.cookies.items(multi=True): for name, (value, attrs) in flow.response.cookies.items(multi=True):
# FIXME: We now know that Cookie.py screws up some cookies with # FIXME: We now know that Cookie.py screws up some cookies with

View File

@ -238,18 +238,24 @@ class View(collections.abc.Sequence):
""" """
Set focus to the next flow. Set focus to the next flow.
""" """
idx = self.focus.index + 1 if self.focus.index is not None:
if self.inbounds(idx): idx = self.focus.index + 1
self.focus.flow = self[idx] if self.inbounds(idx):
self.focus.flow = self[idx]
else:
pass
@command.command("view.focus.prev") @command.command("view.focus.prev")
def focus_prev(self) -> None: def focus_prev(self) -> None:
""" """
Set focus to the previous flow. Set focus to the previous flow.
""" """
idx = self.focus.index - 1 if self.focus.index is not None:
if self.inbounds(idx): idx = self.focus.index - 1
self.focus.flow = self[idx] if self.inbounds(idx):
self.focus.flow = self[idx]
else:
pass
# Order # Order
@command.command("view.order.options") @command.command("view.order.options")
@ -584,7 +590,7 @@ class Focus:
""" """
def __init__(self, v: View) -> None: def __init__(self, v: View) -> None:
self.view = v self.view = v
self._flow: mitmproxy.flow.Flow = None self._flow: typing.Optional[mitmproxy.flow.Flow] = None
self.sig_change = blinker.Signal() self.sig_change = blinker.Signal()
if len(self.view): if len(self.view):
self.flow = self.view[0] self.flow = self.view[0]

View File

@ -315,7 +315,12 @@ class CertStore:
ret.append(b"*." + b".".join(parts[i:])) ret.append(b"*." + b".".join(parts[i:]))
return ret return ret
def get_cert(self, commonname: typing.Optional[bytes], sans: typing.List[bytes], organization: typing.Optional[bytes] = None): def get_cert(
self,
commonname: typing.Optional[bytes],
sans: typing.List[bytes],
organization: typing.Optional[bytes] = None
) -> typing.Tuple["Cert", OpenSSL.SSL.PKey, str]:
""" """
Returns an (cert, privkey, cert_chain) tuple. Returns an (cert, privkey, cert_chain) tuple.

View File

@ -34,6 +34,8 @@ def typename(t: type) -> str:
class Command: class Command:
returntype: typing.Optional[typing.Type]
def __init__(self, manager, path, func) -> None: def __init__(self, manager, path, func) -> None:
self.path = path self.path = path
self.manager = manager self.manager = manager
@ -194,7 +196,7 @@ class CommandManager(mitmproxy.types._CommandBase):
parse: typing.List[ParseResult] = [] parse: typing.List[ParseResult] = []
params: typing.List[type] = [] params: typing.List[type] = []
typ: typing.Type = None typ: typing.Type
for i in range(len(parts)): for i in range(len(parts)):
if i == 0: if i == 0:
typ = mitmproxy.types.Cmd typ = mitmproxy.types.Cmd

View File

@ -135,7 +135,9 @@ def get_content_view(viewmode: View, data: bytes, **metadata):
# Third-party viewers can fail in unexpected ways... # Third-party viewers can fail in unexpected ways...
except Exception: except Exception:
desc = "Couldn't parse: falling back to Raw" desc = "Couldn't parse: falling back to Raw"
_, content = get("Raw")(data, **metadata) raw = get("Raw")
assert raw
content = raw(data, **metadata)[1]
error = "{} Content viewer failed: \n{}".format( error = "{} Content viewer failed: \n{}".format(
getattr(viewmode, "name"), getattr(viewmode, "name"),
traceback.format_exc() traceback.format_exc()

View File

@ -9,8 +9,8 @@ TViewResult = typing.Tuple[str, typing.Iterator[TViewLine]]
class View: class View:
name: str = None name: typing.ClassVar[str]
content_types: typing.List[str] = [] content_types: typing.ClassVar[typing.List[str]] = []
def __call__(self, data: bytes, **metadata) -> TViewResult: def __call__(self, data: bytes, **metadata) -> TViewResult:
""" """

View File

@ -1,7 +1,7 @@
import io import io
import re import re
import textwrap import textwrap
from typing import Iterable 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
@ -124,14 +124,14 @@ def indent_text(data: str, prefix: str) -> str:
return textwrap.indent(dedented, prefix[:32]) return textwrap.indent(dedented, prefix[:32])
def is_inline_text(a: Token, b: Token, c: Token) -> bool: def is_inline_text(a: Optional[Token], b: Optional[Token], c: Optional[Token]) -> bool:
if isinstance(a, Tag) and isinstance(b, Text) and isinstance(c, Tag): if isinstance(a, Tag) and isinstance(b, Text) and isinstance(c, Tag):
if a.is_opening and "\n" not in b.data and c.is_closing and a.tag == c.tag: if a.is_opening and "\n" not in b.data and c.is_closing and a.tag == c.tag:
return True return True
return False return False
def is_inline(prev2: Token, prev1: Token, t: Token, next1: Token, next2: Token) -> bool: def is_inline(prev2: Optional[Token], prev1: Optional[Token], t: Optional[Token], next1: Optional[Token], next2: Optional[Token]) -> bool:
if isinstance(t, Text): if isinstance(t, Text):
return is_inline_text(prev1, t, next1) return is_inline_text(prev1, t, next1)
elif isinstance(t, Tag): elif isinstance(t, Tag):

View File

@ -1,7 +1,7 @@
import mitmproxy.master # noqa import mitmproxy.log
import mitmproxy.log # noqa import mitmproxy.master
import mitmproxy.options # noqa import mitmproxy.options
master = None # type: mitmproxy.master.Master log: "mitmproxy.log.Log"
log: mitmproxy.log.Log = None master: "mitmproxy.master.Master"
options: mitmproxy.options.Options = None options: "mitmproxy.options.Options"

View File

@ -32,19 +32,17 @@
rex Equivalent to ~u rex rex Equivalent to ~u rex
""" """
import functools
import re import re
import sys import sys
import functools from typing import Callable, ClassVar, Optional, Sequence, Type
from mitmproxy import http
from mitmproxy import websocket
from mitmproxy import tcp
from mitmproxy import flow
from mitmproxy.utils import strutils
import pyparsing as pp import pyparsing as pp
from typing import Callable, Sequence, Type # noqa
from mitmproxy import flow
from mitmproxy import http
from mitmproxy import tcp
from mitmproxy import websocket
def only(*types): def only(*types):
@ -54,7 +52,9 @@ def only(*types):
if isinstance(flow, types): if isinstance(flow, types):
return fn(self, flow) return fn(self, flow)
return False return False
return filter_types return filter_types
return decorator return decorator
@ -69,8 +69,8 @@ class _Token:
class _Action(_Token): class _Action(_Token):
code: str = None code: ClassVar[str]
help: str = None help: ClassVar[str]
@classmethod @classmethod
def make(klass, s, loc, toks): def make(klass, s, loc, toks):
@ -146,10 +146,10 @@ class _Rex(_Action):
def __init__(self, expr): def __init__(self, expr):
self.expr = expr self.expr = expr
if self.is_binary: if self.is_binary:
expr = strutils.escaped_str_to_bytes(expr) expr = expr.encode()
try: try:
self.re = re.compile(expr, self.flags) self.re = re.compile(expr, self.flags)
except: except Exception:
raise ValueError("Cannot compile expression.") raise ValueError("Cannot compile expression.")
@ -336,6 +336,7 @@ class FUrl(_Rex):
code = "u" code = "u"
help = "URL" help = "URL"
is_binary = False is_binary = False
# FUrl is special, because it can be "naked". # FUrl is special, because it can be "naked".
@classmethod @classmethod
@ -469,69 +470,51 @@ def _make():
# Order is important - multi-char expressions need to come before narrow # Order is important - multi-char expressions need to come before narrow
# ones. # ones.
parts = [] parts = []
for klass in filter_unary: for cls in filter_unary:
f = pp.Literal("~%s" % klass.code) + pp.WordEnd() f = pp.Literal(f"~{cls.code}") + pp.WordEnd()
f.setParseAction(klass.make) f.setParseAction(cls.make)
parts.append(f) parts.append(f)
simplerex = "".join(c for c in pp.printables if c not in "()~'\"") # This is a bit of a hack to simulate Word(pyparsing_unicode.printables),
alphdevanagari = pp.pyparsing_unicode.Devanagari.alphas # which has a horrible performance with len(pyparsing.pyparsing_unicode.printables) == 1114060
alphcyrillic = pp.pyparsing_unicode.Cyrillic.alphas unicode_words = pp.CharsNotIn("()~'\"" + pp.ParserElement.DEFAULT_WHITE_CHARS)
alphgreek = pp.pyparsing_unicode.Greek.alphas unicode_words.skipWhitespace = True
alphchinese = pp.pyparsing_unicode.Chinese.alphas regex = (
alpharabic = pp.pyparsing_unicode.Arabic.alphas unicode_words
alphhebrew = pp.pyparsing_unicode.Hebrew.alphas | pp.QuotedString('"', escChar='\\')
alphjapanese = pp.pyparsing_unicode.Japanese.alphas | pp.QuotedString("'", escChar='\\')
alphkorean = pp.pyparsing_unicode.Korean.alphas )
alphlatin1 = pp.pyparsing_unicode.Latin1.alphas for cls in filter_rex:
alphlatinA = pp.pyparsing_unicode.LatinA.alphas f = pp.Literal(f"~{cls.code}") + pp.WordEnd() + regex.copy()
alphlatinB = pp.pyparsing_unicode.LatinB.alphas f.setParseAction(cls.make)
rex = pp.Word(simplerex) |\
pp.Word(alphcyrillic) |\
pp.Word(alphgreek) |\
pp.Word(alphchinese) |\
pp.Word(alpharabic) |\
pp.Word(alphdevanagari) |\
pp.Word(alphhebrew) |\
pp.Word(alphjapanese) |\
pp.Word(alphkorean) |\
pp.Word(alphlatin1) |\
pp.Word(alphlatinA) |\
pp.Word(alphlatinB) |\
pp.QuotedString("\"", escChar='\\') |\
pp.QuotedString("'", escChar='\\')
for klass in filter_rex:
f = pp.Literal("~%s" % klass.code) + pp.WordEnd() + rex.copy()
f.setParseAction(klass.make)
parts.append(f) parts.append(f)
for klass in filter_int: for cls in filter_int:
f = pp.Literal("~%s" % klass.code) + pp.WordEnd() + pp.Word(pp.nums) f = pp.Literal(f"~{cls.code}") + pp.WordEnd() + pp.Word(pp.nums)
f.setParseAction(klass.make) f.setParseAction(cls.make)
parts.append(f) parts.append(f)
# A naked rex is a URL rex: # A naked rex is a URL rex:
f = rex.copy() f = regex.copy()
f.setParseAction(FUrl.make) f.setParseAction(FUrl.make)
parts.append(f) parts.append(f)
atom = pp.MatchFirst(parts) atom = pp.MatchFirst(parts)
expr = pp.operatorPrecedence(atom, expr = pp.infixNotation(
[(pp.Literal("!").suppress(), atom,
1, [(pp.Literal("!").suppress(),
pp.opAssoc.RIGHT, 1,
lambda x: FNot(*x)), pp.opAssoc.RIGHT,
(pp.Literal("&").suppress(), lambda x: FNot(*x)),
2, (pp.Literal("&").suppress(),
pp.opAssoc.LEFT, 2,
lambda x: FAnd(*x)), pp.opAssoc.LEFT,
(pp.Literal("|").suppress(), lambda x: FAnd(*x)),
2, (pp.Literal("|").suppress(),
pp.opAssoc.LEFT, 2,
lambda x: FOr(*x)), pp.opAssoc.LEFT,
]) lambda x: FOr(*x)),
])
expr = pp.OneOrMore(expr) expr = pp.OneOrMore(expr)
return expr.setParseAction(lambda x: FAnd(x) if len(x) != 1 else x) return expr.setParseAction(lambda x: FAnd(x) if len(x) != 1 else x)
@ -540,7 +523,7 @@ bnf = _make()
TFilter = Callable[[flow.Flow], bool] TFilter = Callable[[flow.Flow], bool]
def parse(s: str) -> TFilter: def parse(s: str) -> Optional[TFilter]:
try: try:
flt = bnf.parseString(s, parseAll=True)[0] flt = bnf.parseString(s, parseAll=True)[0]
flt.pattern = s flt.pattern = s
@ -571,15 +554,15 @@ def match(flt, flow):
help = [] help = []
for a in filter_unary: for a in filter_unary:
help.append( help.append(
("~%s" % a.code, a.help) (f"~{a.code}", a.help)
) )
for b in filter_rex: for b in filter_rex:
help.append( help.append(
("~%s regex" % b.code, b.help) (f"~{b.code} regex", b.help)
) )
for c in filter_int: for c in filter_int:
help.append( help.append(
("~%s int" % c.code, c.help) (f"~{c.code} int", c.help)
) )
help.sort() help.sort()
help.extend( help.extend(

View File

@ -1,15 +1,13 @@
import html import html
from typing import Optional from typing import Optional
from mitmproxy import connections
from mitmproxy import flow from mitmproxy import flow
from mitmproxy.net import http
from mitmproxy import version from mitmproxy import version
from mitmproxy import connections # noqa from mitmproxy.net import http
class HTTPRequest(http.Request): class HTTPRequest(http.Request):
""" """
A mitmproxy HTTP request. A mitmproxy HTTP request.
""" """
@ -85,10 +83,10 @@ class HTTPRequest(http.Request):
class HTTPResponse(http.Response): class HTTPResponse(http.Response):
""" """
A mitmproxy HTTP response. A mitmproxy HTTP response.
""" """
# This is a very thin wrapper on top of :py:class:`mitmproxy.net.http.Response` and # This is a very thin wrapper on top of :py:class:`mitmproxy.net.http.Response` and
# may be removed in the future. # may be removed in the future.
@ -136,34 +134,28 @@ class HTTPResponse(http.Response):
class HTTPFlow(flow.Flow): 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
response: Optional[HTTPResponse] = None
error: Optional[flow.Error] = None
"""
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
from the server, but there was an error sending it back to the client.
"""
server_conn: connections.ServerConnection
client_conn: connections.ClientConnection
intercepted: bool = False
""" Is this flow currently being intercepted? """
mode: str
""" What mode was the proxy layer in when receiving this request? """
def __init__(self, client_conn, server_conn, live=None, mode="regular"): def __init__(self, client_conn, server_conn, live=None, mode="regular"):
super().__init__("http", client_conn, server_conn, live) super().__init__("http", client_conn, server_conn, live)
self.request: HTTPRequest = None
""" :py:class:`HTTPRequest` object """
self.response: HTTPResponse = None
""" :py:class:`HTTPResponse` object """
self.error: flow.Error = None
""" :py:class:`Error` object
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
from the server, but there was an error sending it back to the client.
"""
self.server_conn: connections.ServerConnection = server_conn
""" :py:class:`ServerConnection` object """
self.client_conn: connections.ClientConnection = client_conn
""":py:class:`ClientConnection` object """
self.intercepted: bool = False
""" Is this flow currently being intercepted? """
self.mode = mode self.mode = mode
""" What mode was the proxy layer in when receiving this request? """
_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
@ -205,8 +197,8 @@ 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,
) -> HTTPResponse: ) -> HTTPResponse:
reason = http.status_codes.RESPONSES.get(status_code, "Unknown") reason = http.status_codes.RESPONSES.get(status_code, "Unknown")
body = """ body = """

View File

@ -192,22 +192,22 @@ def parse(data_type: int, data: bytes) -> TSerializable:
try: try:
return int(data) return int(data)
except ValueError: except ValueError:
raise ValueError("not a tnetstring: invalid integer literal: {}".format(data)) raise ValueError(f"not a tnetstring: invalid integer literal: {data!r}")
if data_type == ord(b'^'): if data_type == ord(b'^'):
try: try:
return float(data) return float(data)
except ValueError: except ValueError:
raise ValueError("not a tnetstring: invalid float literal: {}".format(data)) raise ValueError(f"not a tnetstring: invalid float literal: {data!r}")
if data_type == ord(b'!'): if data_type == ord(b'!'):
if data == b'true': if data == b'true':
return True return True
elif data == b'false': elif data == b'false':
return False return False
else: else:
raise ValueError("not a tnetstring: invalid boolean literal: {}".format(data)) raise ValueError(f"not a tnetstring: invalid boolean literal: {data!r}")
if data_type == ord(b'~'): if data_type == ord(b'~'):
if data: if data:
raise ValueError("not a tnetstring: invalid null literal") raise ValueError(f"not a tnetstring: invalid null literal: {data!r}")
return None return None
if data_type == ord(b']'): if data_type == ord(b']'):
l = [] l = []
@ -236,7 +236,7 @@ def pop(data: bytes) -> typing.Tuple[TSerializable, bytes]:
blength, data = data.split(b':', 1) blength, data = data.split(b':', 1)
length = int(blength) length = int(blength)
except ValueError: except ValueError:
raise ValueError("not a tnetstring: missing or invalid length prefix: {}".format(data)) raise ValueError(f"not a tnetstring: missing or invalid length prefix: {data!r}")
try: try:
data, data_type, remain = data[:length], data[length], data[length + 1:] data, data_type, remain = data[:length], data[length], data[length + 1:]
except IndexError: except IndexError:

View File

@ -82,7 +82,7 @@ class Message(serializable.Serializable):
def raw_content(self, content): def raw_content(self, content):
self.data.content = content self.data.content = content
def get_content(self, strict: bool=True) -> bytes: def get_content(self, strict: bool=True) -> Optional[bytes]:
""" """
The uncompressed HTTP message body as bytes. The uncompressed HTTP message body as bytes.
@ -195,10 +195,9 @@ class Message(serializable.Serializable):
See also: :py:attr:`content`, :py:class:`raw_content` See also: :py:attr:`content`, :py:class:`raw_content`
""" """
if self.raw_content is None:
return None
content = self.get_content(strict) content = self.get_content(strict)
if content is None:
return None
enc = self._guess_encoding(content) enc = self._guess_encoding(content)
try: try:
return encoding.decode(content, enc) return encoding.decode(content, enc)

View File

@ -1,8 +1,43 @@
import re import re
import mimetypes
from urllib.parse import quote
from mitmproxy.net.http import headers from mitmproxy.net.http import headers
def encode(head, l):
k = head.get("content-type")
if k:
k = headers.parse_content_type(k)
if k is not None:
try:
boundary = k[2]["boundary"].encode("ascii")
boundary = quote(boundary)
except (KeyError, UnicodeError):
return b""
hdrs = []
for key, value in l:
file_type = mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8"
if key:
hdrs.append(b"--%b" % boundary.encode('utf-8'))
disposition = b'form-data; name="%b"' % key
hdrs.append(b"Content-Disposition: %b" % disposition)
hdrs.append(b"Content-Type: %b" % file_type.encode('utf-8'))
hdrs.append(b'')
hdrs.append(value)
hdrs.append(b'')
if value is not None:
# If boundary is found in value then raise ValueError
if re.search(rb"^--%b$" % re.escape(boundary.encode('utf-8')), value):
raise ValueError(b"boundary found in encoded string")
hdrs.append(b"--%b--\r\n" % boundary.encode('utf-8'))
temp = b"\r\n".join(hdrs)
return temp
def decode(hdrs, content): def decode(hdrs, content):
""" """
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.
@ -19,14 +54,14 @@ def decode(hdrs, content):
rx = re.compile(br'\bname="([^"]+)"') rx = re.compile(br'\bname="([^"]+)"')
r = [] r = []
if content is not None:
for i in content.split(b"--" + boundary): for i in content.split(b"--" + boundary):
parts = i.splitlines() parts = i.splitlines()
if len(parts) > 1 and parts[0][0:2] != b"--": if len(parts) > 1 and parts[0][0:2] != b"--":
match = rx.search(parts[1]) match = rx.search(parts[1])
if match: if match:
key = match.group(1) key = match.group(1)
value = b"".join(parts[3 + parts[2:].index(b""):]) value = b"".join(parts[3 + parts[2:].index(b""):])
r.append((key, value)) r.append((key, value))
return r return r
return [] return []

View File

@ -472,7 +472,8 @@ class Request(message.Message):
return () return ()
def _set_multipart_form(self, value): def _set_multipart_form(self, value):
raise NotImplementedError() self.content = mitmproxy.net.http.multipart.encode(self.headers, value)
self.headers["content-type"] = "multipart/form-data"
@property @property
def multipart_form(self): def multipart_form(self):

View File

@ -295,6 +295,17 @@ def create_client_context(
return context return context
def accept_all(
conn_: SSL.Connection,
x509: SSL.X509,
errno: int,
err_depth: int,
is_cert_verified: bool,
) -> bool:
# Return true to prevent cert verification error
return True
def create_server_context( def create_server_context(
cert: typing.Union[certs.Cert, str], cert: typing.Union[certs.Cert, str],
key: SSL.PKey, key: SSL.PKey,
@ -324,16 +335,6 @@ def create_server_context(
until then we're conservative. until then we're conservative.
""" """
def accept_all(
conn_: SSL.Connection,
x509: SSL.X509,
errno: int,
err_depth: int,
is_cert_verified: bool,
) -> bool:
# Return true to prevent cert verification error
return True
if request_client_cert: if request_client_cert:
verify = SSL.VERIFY_PEER verify = SSL.VERIFY_PEER
else: else:
@ -425,7 +426,7 @@ class ClientHello:
return self._client_hello.cipher_suites.cipher_suites return self._client_hello.cipher_suites.cipher_suites
@property @property
def sni(self): def sni(self) -> typing.Optional[bytes]:
if self._client_hello.extensions: if self._client_hello.extensions:
for extension in self._client_hello.extensions.extensions: for extension in self._client_hello.extensions.extensions:
is_valid_sni_extension = ( is_valid_sni_extension = (
@ -435,7 +436,7 @@ class ClientHello:
check.is_valid_host(extension.body.server_names[0].host_name) check.is_valid_host(extension.body.server_names[0].host_name)
) )
if is_valid_sni_extension: if is_valid_sni_extension:
return extension.body.server_names[0].host_name.decode("idna") return extension.body.server_names[0].host_name
return None return None
@property @property
@ -473,10 +474,8 @@ class ClientHello:
return cls(raw_client_hello) return cls(raw_client_hello)
except EOFError as e: except EOFError as e:
raise exceptions.TlsProtocolException( raise exceptions.TlsProtocolException(
'Cannot parse Client Hello: %s, Raw Client Hello: %s' % f"Cannot parse Client Hello: {e!r}, Raw Client Hello: {binascii.hexlify(raw_client_hello)!r}"
(repr(e), binascii.hexlify(raw_client_hello))
) )
def __repr__(self): def __repr__(self):
return "ClientHello(sni: %s, alpn_protocols: %s, cipher_suites: %s)" % \ return f"ClientHello(sni: {self.sni}, alpn_protocols: {self.alpn_protocols})"
(self.sni, self.alpn_protocols, self.cipher_suites)

View File

@ -551,7 +551,9 @@ def serialize(opts: OptManager, text: str, defaults: bool = False) -> str:
for k in list(data.keys()): for k in list(data.keys()):
if k not in opts._options: if k not in opts._options:
del data[k] del data[k]
return ruamel.yaml.round_trip_dump(data) ret = ruamel.yaml.round_trip_dump(data)
assert ret
return ret
def save(opts: OptManager, path: str, defaults: bool =False) -> None: def save(opts: OptManager, path: str, defaults: bool =False) -> None:

View File

@ -1,7 +1,7 @@
import re import re
import socket import socket
import sys import sys
from typing import Tuple from typing import Callable, Optional, Tuple
def init_transparent_mode() -> None: def init_transparent_mode() -> None:
@ -10,30 +10,34 @@ def init_transparent_mode() -> None:
""" """
def original_addr(csock: socket.socket) -> Tuple[str, int]: original_addr: Optional[Callable[[socket.socket], Tuple[str, int]]]
""" """
Get the original destination for the given socket. Get the original destination for the given socket.
This function will be None if transparent mode is not supported. This function will be None if transparent mode is not supported.
""" """
if re.match(r"linux(?:2)?", sys.platform): if re.match(r"linux(?:2)?", sys.platform):
from . import linux from . import linux
original_addr = linux.original_addr # noqa original_addr = linux.original_addr
elif sys.platform == "darwin" or sys.platform.startswith("freebsd"): elif sys.platform == "darwin" or sys.platform.startswith("freebsd"):
from . import osx from . import osx
original_addr = osx.original_addr # noqa original_addr = osx.original_addr
elif sys.platform.startswith("openbsd"): elif sys.platform.startswith("openbsd"):
from . import openbsd from . import openbsd
original_addr = openbsd.original_addr # noqa original_addr = openbsd.original_addr
elif sys.platform == "win32": elif sys.platform == "win32":
from . import windows from . import windows
resolver = windows.Resolver() resolver = windows.Resolver()
init_transparent_mode = resolver.setup # noqa init_transparent_mode = resolver.setup # noqa
original_addr = resolver.original_addr # noqa original_addr = resolver.original_addr
else: else:
original_addr = None # noqa original_addr = None
__all__ = [
"original_addr",
"init_transparent_mode"
]

View File

@ -34,9 +34,9 @@ class ProxyConfig:
def __init__(self, options: moptions.Options) -> None: def __init__(self, options: moptions.Options) -> None:
self.options = options self.options = options
self.check_filter: HostMatcher = None self.certstore: certs.CertStore
self.check_tcp: HostMatcher = None self.check_filter: typing.Optional[HostMatcher] = None
self.certstore: certs.CertStore = None self.check_tcp: typing.Optional[HostMatcher] = None
self.upstream_server: typing.Optional[server_spec.ServerSpec] = None self.upstream_server: typing.Optional[server_spec.ServerSpec] = None
self.configure(options, set(options.keys())) self.configure(options, set(options.keys()))
options.changed.connect(self.configure) options.changed.connect(self.configure)

View File

@ -263,7 +263,7 @@ class HttpLayer(base.Layer):
else: else:
msg = "Unexpected CONNECT request." msg = "Unexpected CONNECT request."
self.send_error_response(400, msg) self.send_error_response(400, msg)
raise exceptions.ProtocolException(msg) return False
validate_request_form(self.mode, request) validate_request_form(self.mode, request)
self.channel.ask("requestheaders", f) self.channel.ask("requestheaders", f)
@ -289,9 +289,12 @@ class HttpLayer(base.Layer):
f.request = None f.request = None
f.error = flow.Error(str(e)) f.error = flow.Error(str(e))
self.channel.ask("error", f) self.channel.ask("error", f)
raise exceptions.ProtocolException( self.log(
"HTTP protocol error in client request: {}".format(e) "request",
) from e "warn",
["HTTP protocol error in client request: {}".format(e)]
)
return False
self.log("request", "debug", [repr(request)]) self.log("request", "debug", [repr(request)])
@ -448,8 +451,8 @@ class HttpLayer(base.Layer):
return False # should never be reached return False # should never be reached
except (exceptions.ProtocolException, exceptions.NetlibException) as e: except (exceptions.ProtocolException, exceptions.NetlibException) as e:
self.send_error_response(502, repr(e))
if not f.response: if not f.response:
self.send_error_response(502, repr(e))
f.error = flow.Error(str(e)) f.error = flow.Error(str(e))
self.channel.ask("error", f) self.channel.ask("error", f)
return False return False

View File

@ -1,7 +1,7 @@
import threading import threading
import time import time
import functools import functools
from typing import Dict, Callable, Any, List # noqa from typing import Dict, Callable, Any, List, Optional # noqa
import h2.exceptions import h2.exceptions
from h2 import connection from h2 import connection
@ -382,15 +382,15 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr
ctx, name="Http2SingleStreamLayer-{}".format(stream_id) ctx, name="Http2SingleStreamLayer-{}".format(stream_id)
) )
self.h2_connection = h2_connection self.h2_connection = h2_connection
self.zombie: float = None self.zombie: Optional[float] = None
self.client_stream_id: int = stream_id self.client_stream_id: int = stream_id
self.server_stream_id: int = None self.server_stream_id: Optional[int] = None
self.request_headers = request_headers self.request_headers = request_headers
self.response_headers: mitmproxy.net.http.Headers = None self.response_headers: Optional[mitmproxy.net.http.Headers] = None
self.pushed = False self.pushed = False
self.timestamp_start: float = None self.timestamp_start: Optional[float] = None
self.timestamp_end: float = None self.timestamp_end: Optional[float] = None
self.request_arrived = threading.Event() self.request_arrived = threading.Event()
self.request_data_queue: queue.Queue[bytes] = queue.Queue() self.request_data_queue: queue.Queue[bytes] = queue.Queue()
@ -404,9 +404,9 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr
self.no_body = False self.no_body = False
self.priority_exclusive: bool = None self.priority_exclusive: bool
self.priority_depends_on: int = None self.priority_depends_on: Optional[int] = None
self.priority_weight: int = None self.priority_weight: Optional[int] = None
self.handled_priority_event: Any = None self.handled_priority_event: Any = None
def kill(self): def kill(self):

View File

@ -196,17 +196,14 @@ CIPHER_ID_NAME_MAP = {
} }
# We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default. # We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default.
# https://mozilla.github.io/server-side-tls/ssl-config-generator/?server=apache-2.2.15&openssl=1.0.2&hsts=yes&profile=old # https://ssl-config.mozilla.org/#config=old
DEFAULT_CLIENT_CIPHERS = ( DEFAULT_CLIENT_CIPHERS = (
"ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:" "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:"
"ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:" "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:"
"ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:" "DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:"
"ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:" "ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:"
"DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:" "ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:AES128-GCM-SHA256:"
"DHE-RSA-AES256-SHA:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:" "AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA"
"AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:DES-CBC3-SHA:"
"HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:"
"!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
) )
@ -323,14 +320,18 @@ class TlsLayer(base.Layer):
return self._server_tls return self._server_tls
@property @property
def server_sni(self): def server_sni(self) -> Optional[str]:
""" """
The Server Name Indication we want to send with the next server TLS handshake. The Server Name Indication we want to send with the next server TLS handshake.
""" """
if self._custom_server_sni is False: if self._custom_server_sni is False:
return None return None
elif self._custom_server_sni:
return self._custom_server_sni
elif self._client_hello and self._client_hello.sni:
return self._client_hello.sni.decode("idna")
else: else:
return self._custom_server_sni or self._client_hello and self._client_hello.sni return None
@property @property
def alpn_for_client_connection(self): def alpn_for_client_connection(self):
@ -391,11 +392,12 @@ class TlsLayer(base.Layer):
# raises ann error. # raises ann error.
self.client_conn.rfile.peek(1) self.client_conn.rfile.peek(1)
except exceptions.TlsException as e: except exceptions.TlsException as e:
sni_str = self._client_hello.sni and self._client_hello.sni.decode("idna")
raise exceptions.ClientHandshakeException( raise exceptions.ClientHandshakeException(
"Cannot establish TLS with client (sni: {sni}): {e}".format( "Cannot establish TLS with client (sni: {sni}): {e}".format(
sni=self._client_hello.sni, e=repr(e) sni=sni_str, e=repr(e)
), ),
self._client_hello.sni or repr(self.server_conn.address) sni_str or repr(self.server_conn.address)
) )
def _establish_tls_with_server(self): def _establish_tls_with_server(self):
@ -493,7 +495,7 @@ class TlsLayer(base.Layer):
organization = upstream_cert.organization organization = upstream_cert.organization
# Also add SNI values. # Also add SNI values.
if self._client_hello.sni: if self._client_hello.sni:
sans.add(self._client_hello.sni.encode("idna")) sans.add(self._client_hello.sni)
if self._custom_server_sni: if self._custom_server_sni:
sans.add(self._custom_server_sni.encode("idna")) sans.add(self._custom_server_sni.encode("idna"))

View File

@ -57,7 +57,8 @@ class RootContext:
except exceptions.TlsProtocolException as e: except exceptions.TlsProtocolException as e:
self.log("Cannot parse Client Hello: %s" % repr(e), "error") self.log("Cannot parse Client Hello: %s" % repr(e), "error")
else: else:
is_filtered = self.config.check_filter((client_hello.sni, 443)) sni_str = client_hello.sni and client_hello.sni.decode("idna")
is_filtered = self.config.check_filter((sni_str, 443))
if is_filtered: if is_filtered:
return protocol.RawTCPLayer(top_layer, ignore=True) return protocol.RawTCPLayer(top_layer, ignore=True)

View File

@ -35,6 +35,7 @@ class DummyServer:
class ProxyServer(tcp.TCPServer): class ProxyServer(tcp.TCPServer):
allow_reuse_address = True allow_reuse_address = True
bound = True bound = True
channel: controller.Channel
def __init__(self, config: config.ProxyConfig) -> None: def __init__(self, config: config.ProxyConfig) -> None:
""" """
@ -53,7 +54,6 @@ class ProxyServer(tcp.TCPServer):
raise exceptions.ServerException( raise exceptions.ServerException(
'Error starting proxy server: ' + repr(e) 'Error starting proxy server: ' + repr(e)
) from e ) from e
self.channel: controller.Channel = None
def set_channel(self, channel): def set_channel(self, channel):
self.channel = channel self.channel = channel

View File

@ -1,7 +1,5 @@
import typing
from typing import Any # noqa
from typing import MutableMapping # noqa
import json import json
import typing
from mitmproxy.coretypes import serializable from mitmproxy.coretypes import serializable
from mitmproxy.utils import typecheck from mitmproxy.utils import typecheck
@ -15,7 +13,7 @@ class StateObject(serializable.Serializable):
or StateObject instances themselves. or StateObject instances themselves.
""" """
_stateobject_attributes: MutableMapping[str, Any] = None _stateobject_attributes: typing.ClassVar[typing.MutableMapping[str, typing.Any]]
""" """
An attribute-name -> class-or-type dict containing all attributes that An attribute-name -> class-or-type dict containing all attributes that
should be serialized. If the attribute is a class, it must implement the should be serialized. If the attribute is a class, it must implement the
@ -42,7 +40,7 @@ class StateObject(serializable.Serializable):
if val is None: if val is None:
setattr(self, attr, val) setattr(self, attr, val)
else: else:
curr = getattr(self, attr) curr = getattr(self, attr, None)
if hasattr(curr, "set_state"): if hasattr(curr, "set_state"):
curr.set_state(val) curr.set_state(val)
else: else:

View File

@ -6,19 +6,16 @@ Feel free to import and use whatever new package you deem necessary.
import os import os
import sys import sys
import asyncio import asyncio
import argparse # noqa import argparse
import signal # noqa import signal
import typing # noqa import typing
from mitmproxy.tools import cmdline # noqa from mitmproxy.tools import cmdline
from mitmproxy import exceptions, master # noqa from mitmproxy import exceptions, master
from mitmproxy import options # noqa from mitmproxy import options
from mitmproxy import optmanager # noqa from mitmproxy import optmanager
from mitmproxy import proxy # noqa from mitmproxy import proxy
from mitmproxy import log # noqa from mitmproxy.utils import debug, arg_check
from mitmproxy.utils import debug, arg_check # noqa
OPTIONS_FILE_NAME = "config.yaml"
def assert_utf8_env(): def assert_utf8_env():
@ -90,7 +87,8 @@ def run(
opts.set(*args.setoptions, defer=True) opts.set(*args.setoptions, defer=True)
optmanager.load_paths( optmanager.load_paths(
opts, opts,
os.path.join(opts.confdir, OPTIONS_FILE_NAME), os.path.join(opts.confdir, "config.yaml"),
os.path.join(opts.confdir, "config.yml"),
) )
pconf = process_options(parser, opts, args) pconf = process_options(parser, opts, args)
server: typing.Any = None server: typing.Any = None

View File

@ -55,7 +55,7 @@ class CommandBuffer:
self.text = start self.text = start
# Cursor is always within the range [0:len(buffer)]. # Cursor is always within the range [0:len(buffer)].
self._cursor = len(self.text) self._cursor = len(self.text)
self.completion: CompletionState = None self.completion: typing.Optional[CompletionState] = None
@property @property
def cursor(self) -> int: def cursor(self) -> int:

View File

@ -38,7 +38,7 @@ KEY_MAX = 30
def format_keyvals( def format_keyvals(
entries: typing.List[typing.Tuple[str, typing.Union[None, str, urwid.Widget]]], entries: typing.Iterable[typing.Tuple[str, typing.Union[None, str, urwid.Widget]]],
key_format: str = "key", key_format: str = "key",
value_format: str = "text", value_format: str = "text",
indent: int = 0 indent: int = 0

View File

@ -381,7 +381,8 @@ class ConsoleAddon:
""" """
return [ return [
"cookies", "cookies",
"form", "urlencoded form",
"multipart form",
"path", "path",
"method", "method",
"query", "query",
@ -416,8 +417,10 @@ class ConsoleAddon:
flow.response = http.HTTPResponse.make() flow.response = http.HTTPResponse.make()
if part == "cookies": if part == "cookies":
self.master.switch_view("edit_focus_cookies") self.master.switch_view("edit_focus_cookies")
elif part == "form": elif part == "urlencoded form":
self.master.switch_view("edit_focus_form") self.master.switch_view("edit_focus_urlencoded_form")
elif part == "multipart form":
self.master.switch_view("edit_focus_multipart_form")
elif part == "path": elif part == "path":
self.master.switch_view("edit_focus_path") self.master.switch_view("edit_focus_path")
elif part == "query": elif part == "query":

View File

@ -254,7 +254,7 @@ FIRST_WIDTH_MAX = 40
class BaseGridEditor(urwid.WidgetWrap): class BaseGridEditor(urwid.WidgetWrap):
title = "" title: str = ""
keyctx = "grideditor" keyctx = "grideditor"
def __init__( def __init__(
@ -402,8 +402,8 @@ class BaseGridEditor(urwid.WidgetWrap):
class GridEditor(BaseGridEditor): class GridEditor(BaseGridEditor):
title: str = None title = ""
columns: typing.Sequence[Column] = None columns: typing.Sequence[Column] = ()
keyctx = "grideditor" keyctx = "grideditor"
def __init__( def __init__(

View File

@ -53,14 +53,30 @@ class ResponseHeaderEditor(HeaderEditor):
flow.response.headers = Headers(vals) flow.response.headers = Headers(vals)
class RequestFormEditor(base.FocusEditor): class RequestMultipartEditor(base.FocusEditor):
title = "Edit URL-encoded Form" title = "Edit Multipart Form"
columns = [ columns = [
col_text.Column("Key"), col_text.Column("Key"),
col_text.Column("Value") col_text.Column("Value")
] ]
def get_data(self, flow): def get_data(self, flow):
return flow.request.multipart_form.items(multi=True)
def set_data(self, vals, flow):
flow.request.multipart_form = vals
class RequestUrlEncodedEditor(base.FocusEditor):
title = "Edit UrlEncoded Form"
columns = [
col_text.Column("Key"),
col_text.Column("Value")
]
def get_data(self, flow):
return flow.request.urlencoded_form.items(multi=True) return flow.request.urlencoded_form.items(multi=True)
def set_data(self, vals, flow): def set_data(self, vals, flow):
@ -107,7 +123,7 @@ class CookieAttributeEditor(base.FocusEditor):
col_text.Column("Name"), col_text.Column("Name"),
col_text.Column("Value"), col_text.Column("Value"),
] ]
grideditor: base.BaseGridEditor = None grideditor: base.BaseGridEditor
def data_in(self, data): def data_in(self, data):
return [(k, v or "") for k, v in data] return [(k, v or "") for k, v in data]
@ -169,7 +185,7 @@ class SetCookieEditor(base.FocusEditor):
class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget): class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget):
title: str = None title = ""
columns = [ columns = [
col_text.Column("") col_text.Column("")
] ]
@ -189,7 +205,7 @@ class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget):
class DataViewer(base.GridEditor, layoutwidget.LayoutWidget): class DataViewer(base.GridEditor, layoutwidget.LayoutWidget):
title: str = None title = ""
def __init__( def __init__(
self, self,

View File

@ -120,7 +120,7 @@ class ConsoleMaster(master.Master):
with open(fd, "w" if text else "wb") as f: with open(fd, "w" if text else "wb") as f:
f.write(data) f.write(data)
# if no EDITOR is set, assume 'vi' # if no EDITOR is set, assume 'vi'
c = os.environ.get("EDITOR") or "vi" c = os.environ.get("MITMPROXY_EDITOR") or os.environ.get("EDITOR") or "vi"
cmd = shlex.split(c) cmd = shlex.split(c)
cmd.append(name) cmd.append(name)
with self.uistopped(): with self.uistopped():
@ -159,7 +159,7 @@ class ConsoleMaster(master.Master):
shell = True shell = True
if not cmd: if not cmd:
# hm which one should get priority? # hm which one should get priority?
c = os.environ.get("PAGER") or os.environ.get("EDITOR") c = os.environ.get("MITMPROXY_EDITOR") or os.environ.get("PAGER") or os.environ.get("EDITOR")
if not c: if not c:
c = "less" c = "less"
cmd = shlex.split(c) cmd = shlex.split(c)

View File

@ -42,7 +42,7 @@ class Palette:
'commander_command', 'commander_invalid', 'commander_hint' 'commander_command', 'commander_invalid', 'commander_hint'
] ]
_fields.extend(['gradient_%02d' % i for i in range(100)]) _fields.extend(['gradient_%02d' % i for i in range(100)])
high: typing.Mapping[str, typing.Sequence[str]] = None high: typing.Optional[typing.Mapping[str, typing.Sequence[str]]] = None
def palette(self, transparent): def palette(self, transparent):
l = [] l = []

View File

@ -64,7 +64,8 @@ class WindowStack:
edit_focus_cookies = grideditor.CookieEditor(master), edit_focus_cookies = grideditor.CookieEditor(master),
edit_focus_setcookies = grideditor.SetCookieEditor(master), edit_focus_setcookies = grideditor.SetCookieEditor(master),
edit_focus_setcookie_attrs = grideditor.CookieAttributeEditor(master), edit_focus_setcookie_attrs = grideditor.CookieAttributeEditor(master),
edit_focus_form = grideditor.RequestFormEditor(master), edit_focus_multipart_form=grideditor.RequestMultipartEditor(master),
edit_focus_urlencoded_form=grideditor.RequestUrlEncodedEditor(master),
edit_focus_path = grideditor.PathEditor(master), edit_focus_path = grideditor.PathEditor(master),
edit_focus_request_headers = grideditor.RequestHeaderEditor(master), edit_focus_request_headers = grideditor.RequestHeaderEditor(master),
edit_focus_response_headers = grideditor.ResponseHeaderEditor(master), edit_focus_response_headers = grideditor.ResponseHeaderEditor(master),

View File

@ -5,6 +5,7 @@ import logging
import os.path import os.path
import re import re
from io import BytesIO from io import BytesIO
from typing import ClassVar, Optional
import tornado.escape import tornado.escape
import tornado.web import tornado.web
@ -50,6 +51,8 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:
f["error"] = flow.error.get_state() f["error"] = flow.error.get_state()
if isinstance(flow, http.HTTPFlow): if isinstance(flow, http.HTTPFlow):
content_length: Optional[int]
content_hash: Optional[str]
if flow.request: if flow.request:
if flow.request.raw_content: if flow.request.raw_content:
content_length = len(flow.request.raw_content) content_length = len(flow.request.raw_content)
@ -193,7 +196,7 @@ class FilterHelp(RequestHandler):
class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler):
# raise an error if inherited class doesn't specify its own instance. # raise an error if inherited class doesn't specify its own instance.
connections: set = None connections: ClassVar[set]
def open(self): def open(self):
self.connections.add(self) self.connections.add(self)
@ -213,7 +216,7 @@ class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler):
class ClientConnection(WebSocketEventBroadcaster): class ClientConnection(WebSocketEventBroadcaster):
connections: set = set() connections: ClassVar[set] = set()
class Flows(RequestHandler): class Flows(RequestHandler):

View File

@ -423,7 +423,7 @@ class TypeManager:
for t in types: for t in types:
self.typemap[t.typ] = t() self.typemap[t.typ] = t()
def get(self, t: type, default=None) -> _BaseType: def get(self, t: typing.Optional[typing.Type], default=None) -> _BaseType:
if type(t) in self.typemap: if type(t) in self.typemap:
return self.typemap[type(t)] return self.typemap[type(t)]
return self.typemap.get(t, default) return self.typemap.get(t, default)

View File

@ -1,5 +1,5 @@
import itertools import itertools
from typing import TypeVar, Iterable, Iterator, Tuple, Optional from typing import TypeVar, Iterable, Iterator, Tuple, Optional, List
T = TypeVar('T') T = TypeVar('T')
@ -18,7 +18,7 @@ def window(iterator: Iterable[T], behind: int = 0, ahead: int = 0) -> Iterator[T
2 3 None 2 3 None
""" """
# TODO: move into utils # TODO: move into utils
iters = list(itertools.tee(iterator, behind + 1 + ahead)) iters: List[Iterator[Optional[T]]] = list(itertools.tee(iterator, behind + 1 + ahead))
for i in range(behind): for i in range(behind):
iters[i] = itertools.chain((behind - i) * [None], iters[i]) iters[i] = itertools.chain((behind - i) * [None], iters[i])
for i in range(ahead): for i in range(ahead):

View File

@ -1,10 +1,10 @@
import codecs
import io import io
import re import re
import codecs from typing import Iterable, Optional, Union, cast
from typing import AnyStr, Optional, cast, Iterable
def always_bytes(str_or_bytes: Optional[AnyStr], *encode_args) -> Optional[bytes]: def always_bytes(str_or_bytes: Union[str, bytes, None], *encode_args) -> Optional[bytes]:
if isinstance(str_or_bytes, bytes) or str_or_bytes is None: if isinstance(str_or_bytes, bytes) or str_or_bytes is None:
return cast(Optional[bytes], str_or_bytes) return cast(Optional[bytes], str_or_bytes)
elif isinstance(str_or_bytes, str): elif isinstance(str_or_bytes, str):
@ -13,13 +13,15 @@ def always_bytes(str_or_bytes: Optional[AnyStr], *encode_args) -> Optional[bytes
raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__)) raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__))
def always_str(str_or_bytes: Optional[AnyStr], *decode_args) -> Optional[str]: def always_str(str_or_bytes: Union[str, bytes, None], *decode_args) -> Optional[str]:
""" """
Returns, Returns,
str_or_bytes unmodified, if str_or_bytes unmodified, if
""" """
if isinstance(str_or_bytes, str) or str_or_bytes is None: if str_or_bytes is None:
return cast(Optional[str], str_or_bytes) return None
if isinstance(str_or_bytes, str):
return cast(str, str_or_bytes)
elif isinstance(str_or_bytes, bytes): elif isinstance(str_or_bytes, bytes):
return str_or_bytes.decode(*decode_args) return str_or_bytes.decode(*decode_args)
else: else:
@ -39,7 +41,6 @@ _control_char_trans_newline = _control_char_trans.copy()
for x in ("\r", "\n", "\t"): for x in ("\r", "\n", "\t"):
del _control_char_trans_newline[ord(x)] del _control_char_trans_newline[ord(x)]
_control_char_trans = str.maketrans(_control_char_trans) _control_char_trans = str.maketrans(_control_char_trans)
_control_char_trans_newline = str.maketrans(_control_char_trans_newline) _control_char_trans_newline = str.maketrans(_control_char_trans_newline)

View File

@ -25,9 +25,9 @@ def get_dev_version() -> str:
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
cwd=here, cwd=here,
) )
last_tag, tag_dist, commit = git_describe.decode().strip().rsplit("-", 2) last_tag, tag_dist_str, commit = git_describe.decode().strip().rsplit("-", 2)
commit = commit.lstrip("g")[:7] commit = commit.lstrip("g")[:7]
tag_dist = int(tag_dist) tag_dist = int(tag_dist_str)
except Exception: except Exception:
pass pass
else: else:

View File

@ -25,7 +25,7 @@ class Daemon:
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, type, value, traceback) -> bool: def __exit__(self, type, value, traceback):
self.logfp.truncate(0) self.logfp.truncate(0)
self.shutdown() self.shutdown()
return False return False

View File

@ -1,7 +1,7 @@
[flake8] [flake8]
max-line-length = 140 max-line-length = 140
max-complexity = 25 max-complexity = 25
ignore = E251,E252,C901,W292,W503,W504,W605,E722,E741 ignore = E251,E252,C901,W292,W503,W504,W605,E722,E741,E126
exclude = mitmproxy/contrib/*,test/mitmproxy/data/*,release/build/*,mitmproxy/io/proto/* exclude = mitmproxy/contrib/*,test/mitmproxy/data/*,release/build/*,mitmproxy/io/proto/*
addons = file,open,basestring,xrange,unicode,long,cmp addons = file,open,basestring,xrange,unicode,long,cmp

View File

@ -72,7 +72,7 @@ setup(
"kaitaistruct>=0.7,<0.9", "kaitaistruct>=0.7,<0.9",
"ldap3>=2.6.1,<2.7", "ldap3>=2.6.1,<2.7",
"passlib>=1.6.5, <1.8", "passlib>=1.6.5, <1.8",
"protobuf>=3.6.0, <3.10", "protobuf>=3.6.0, <3.11",
"pyasn1>=0.3.1,<0.5", "pyasn1>=0.3.1,<0.5",
"pyOpenSSL>=19.0.0,<20", "pyOpenSSL>=19.0.0,<20",
"pyparsing>=2.4.2,<2.5", "pyparsing>=2.4.2,<2.5",
@ -93,7 +93,7 @@ setup(
"asynctest>=0.12.0", "asynctest>=0.12.0",
"flake8>=3.7.8,<3.8", "flake8>=3.7.8,<3.8",
"Flask>=1.0,<1.2", "Flask>=1.0,<1.2",
"mypy>=0.590,<0.591", "mypy>=0.740,<0.741",
"parver>=0.1,<2.0", "parver>=0.1,<2.0",
"pytest-asyncio>=0.10.0,<0.11", "pytest-asyncio>=0.10.0,<0.11",
"pytest-cov>=2.7.1,<3", "pytest-cov>=2.7.1,<3",

View File

@ -1,5 +1,6 @@
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():
@ -22,3 +23,39 @@ def test_decode():
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茅莽'
headers = Headers(
content_type='multipart/form-data; boundary=' + boundary
)
result = multipart.decode(headers, content)
assert result == []
headers = Headers(
content_type=''
)
assert multipart.decode(headers, content) == []
def test_encode():
data = [("file".encode('utf-8'), "shell.jpg".encode('utf-8')),
("file_size".encode('utf-8'), "1000".encode('utf-8'))]
headers = Headers(
content_type='multipart/form-data; boundary=127824672498'
)
content = multipart.encode(headers, data)
assert b'Content-Disposition: form-data; name="file"' in content
assert b'Content-Type: text/plain; charset=utf-8\r\n\r\nshell.jpg\r\n\r\n--127824672498\r\n' in content
assert b'1000\r\n\r\n--127824672498--\r\n'
assert len(content) == 252
with pytest.raises(ValueError, match=r"boundary found in encoded string"):
multipart.encode(headers, [("key".encode('utf-8'), "--127824672498".encode('utf-8'))])
boundary = 'boundary茅莽'
headers = Headers(
content_type='multipart/form-data; boundary=' + boundary
)
result = multipart.encode(headers, data)
assert result == b''

View File

@ -371,6 +371,7 @@ class TestRequestUtils:
assert list(request.multipart_form.items()) == [] assert list(request.multipart_form.items()) == []
def test_set_multipart_form(self): def test_set_multipart_form(self):
request = treq(content=b"foobar") request = treq()
with pytest.raises(NotImplementedError): request.multipart_form = [("file", "shell.jpg"), ("file_size", "1000")]
request.multipart_form = "foobar" assert request.headers["Content-Type"] == 'multipart/form-data'
assert request.content is None

View File

@ -116,7 +116,7 @@ class TestClientHello:
) )
c = tls.ClientHello(data) c = tls.ClientHello(data)
assert repr(c) assert repr(c)
assert c.sni == 'example.com' assert c.sni == b'example.com'
assert c.cipher_suites == [ assert c.cipher_suites == [
49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49161, 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49161,
49171, 49162, 49172, 156, 157, 47, 53, 10 49171, 49162, 49172, 156, 157, 47, 53, 10