aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--examples/complex/dns_spoofing.py4
-rw-r--r--examples/simple/custom_contentview.py2
-rw-r--r--mitmproxy/contentviews/__init__.py5
-rw-r--r--mitmproxy/contentviews/protobuf.py21
-rw-r--r--mitmproxy/export.py4
-rw-r--r--mitmproxy/net/http/http1/assemble.py3
-rw-r--r--mitmproxy/net/http/http1/read.py2
-rw-r--r--mitmproxy/net/http/request.py47
-rw-r--r--mitmproxy/options.py15
-rw-r--r--mitmproxy/proxy/protocol/http.py7
-rw-r--r--mitmproxy/proxy/protocol/http2.py15
-rw-r--r--mitmproxy/tools/cmdline.py27
-rw-r--r--setup.cfg63
-rw-r--r--setup.py3
-rw-r--r--test/full_coverage_plugin.py4
-rw-r--r--test/individual_coverage.py82
-rw-r--r--test/mitmproxy/contentviews/test_protobuf.py52
-rw-r--r--test/mitmproxy/net/http/test_request.py47
-rw-r--r--test/mitmproxy/proxy/protocol/test_http2.py142
-rw-r--r--test/mitmproxy/test_flow.py112
-rw-r--r--test/mitmproxy/test_tcp.py60
-rw-r--r--test/mitmproxy/test_websocket.py63
-rw-r--r--tox.ini6
24 files changed, 531 insertions, 257 deletions
diff --git a/.travis.yml b/.travis.yml
index f534100b..4c85c46d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -44,6 +44,8 @@ matrix:
packages:
- libssl-dev
- python: 3.5
+ env: TOXENV=individual_coverage
+ - python: 3.5
env: TOXENV=docs
install:
diff --git a/examples/complex/dns_spoofing.py b/examples/complex/dns_spoofing.py
index c6d46b66..acda303d 100644
--- a/examples/complex/dns_spoofing.py
+++ b/examples/complex/dns_spoofing.py
@@ -34,7 +34,7 @@ class Rerouter:
The original host header is retrieved early
before flow.request is replaced by mitmproxy new outgoing request
"""
- flow.metadata["original_host"] = flow.request.headers["Host"]
+ flow.metadata["original_host"] = flow.request.host_header
def request(self, flow):
if flow.client_conn.ssl_established:
@@ -53,7 +53,7 @@ class Rerouter:
if m.group("port"):
port = int(m.group("port"))
- flow.request.headers["Host"] = host_header
+ flow.request.host_header = host_header
flow.request.host = sni or host_header
flow.request.port = port
diff --git a/examples/simple/custom_contentview.py b/examples/simple/custom_contentview.py
index 35216397..1f3a38ec 100644
--- a/examples/simple/custom_contentview.py
+++ b/examples/simple/custom_contentview.py
@@ -10,7 +10,7 @@ class ViewSwapCase(contentviews.View):
# We don't have a good solution for the keyboard shortcut yet -
# you manually need to find a free letter. Contributions welcome :)
- prompt = ("swap case text", "p")
+ prompt = ("swap case text", "z")
content_types = ["text/plain"]
def __call__(self, data: bytes, **metadata):
diff --git a/mitmproxy/contentviews/__init__.py b/mitmproxy/contentviews/__init__.py
index 357172e3..c7db6690 100644
--- a/mitmproxy/contentviews/__init__.py
+++ b/mitmproxy/contentviews/__init__.py
@@ -159,6 +159,7 @@ def get_content_view(viewmode: View, data: bytes, **metadata):
return desc, safe_to_print(content), error
+# The order in which ContentViews are added is important!
add(auto.ViewAuto())
add(raw.ViewRaw())
add(hex.ViewHex())
@@ -172,9 +173,7 @@ add(urlencoded.ViewURLEncoded())
add(multipart.ViewMultipart())
add(image.ViewImage())
add(query.ViewQuery())
-
-if protobuf.ViewProtobuf.is_available():
- add(protobuf.ViewProtobuf())
+add(protobuf.ViewProtobuf())
__all__ = [
"View", "VIEW_CUTOFF", "KEY_MAX", "format_text", "format_dict",
diff --git a/mitmproxy/contentviews/protobuf.py b/mitmproxy/contentviews/protobuf.py
index 620d9444..4bbb1580 100644
--- a/mitmproxy/contentviews/protobuf.py
+++ b/mitmproxy/contentviews/protobuf.py
@@ -15,31 +15,28 @@ class ViewProtobuf(base.View):
"application/x-protobuffer",
]
- @staticmethod
- def is_available():
+ def is_available(self):
try:
p = subprocess.Popen(
["protoc", "--version"],
stdout=subprocess.PIPE
)
out, _ = p.communicate()
- return out.startswith("libprotoc")
+ return out.startswith(b"libprotoc")
except:
return False
- def decode_protobuf(self, content):
+ def __call__(self, data, **metadata):
+ if not self.is_available():
+ raise NotImplementedError("protoc not found. Please make sure 'protoc' is available in $PATH.")
+
# if Popen raises OSError, it will be caught in
# get_content_view and fall back to Raw
p = subprocess.Popen(['protoc', '--decode_raw'],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
- out, err = p.communicate(input=content)
- if out:
- return out
- else:
- return err
-
- def __call__(self, data, **metadata):
- decoded = self.decode_protobuf(data)
+ decoded, _ = p.communicate(input=data)
+ if not decoded:
+ raise ValueError("Failed to parse input.")
return "Protobuf", base.format_text(decoded)
diff --git a/mitmproxy/export.py b/mitmproxy/export.py
index 0a261509..235e754a 100644
--- a/mitmproxy/export.py
+++ b/mitmproxy/export.py
@@ -73,7 +73,7 @@ def python_code(flow: http.HTTPFlow):
headers = flow.request.headers.copy()
# requests adds those by default.
- for x in ("host", "content-length"):
+ for x in (":authority", "host", "content-length"):
headers.pop(x, None)
writearg("headers", dict(headers))
try:
@@ -130,7 +130,7 @@ def locust_code(flow):
if flow.request.headers:
lines = [
(_native(k), _native(v)) for k, v in flow.request.headers.fields
- if _native(k).lower() not in ["host", "cookie"]
+ if _native(k).lower() not in [":authority", "host", "cookie"]
]
lines = [" '%s': '%s',\n" % (k, v) for k, v in lines]
headers += "\n headers = {\n%s }\n" % "".join(lines)
diff --git a/mitmproxy/net/http/http1/assemble.py b/mitmproxy/net/http/http1/assemble.py
index d718589f..8b7246f7 100644
--- a/mitmproxy/net/http/http1/assemble.py
+++ b/mitmproxy/net/http/http1/assemble.py
@@ -78,8 +78,9 @@ def _assemble_request_headers(request_data):
Args:
request_data (mitmproxy.net.http.request.RequestData)
"""
- headers = request_data.headers.copy()
+ headers = request_data.headers
if "host" not in headers and request_data.scheme and request_data.host and request_data.port:
+ headers = headers.copy()
headers["host"] = mitmproxy.net.http.url.hostport(
request_data.scheme,
request_data.host,
diff --git a/mitmproxy/net/http/http1/read.py b/mitmproxy/net/http/http1/read.py
index d0493da4..ef88fd6c 100644
--- a/mitmproxy/net/http/http1/read.py
+++ b/mitmproxy/net/http/http1/read.py
@@ -227,7 +227,7 @@ def _get_first_line(rfile):
if line == b"\r\n" or line == b"\n":
# Possible leftover from previous message
line = rfile.readline()
- except exceptions.TcpDisconnect:
+ except (exceptions.TcpDisconnect, exceptions.TlsException):
raise exceptions.HttpReadDisconnect("Remote disconnected")
if not line:
raise exceptions.HttpReadDisconnect("Remote disconnected")
diff --git a/mitmproxy/net/http/request.py b/mitmproxy/net/http/request.py
index 68a11ce7..b961e1e4 100644
--- a/mitmproxy/net/http/request.py
+++ b/mitmproxy/net/http/request.py
@@ -1,5 +1,6 @@
import re
import urllib
+from typing import Optional
from mitmproxy.types import multidict
from mitmproxy.utils import strutils
@@ -164,11 +165,44 @@ class Request(message.Message):
self.data.host = host
# Update host header
- if "host" in self.headers:
- if host:
- self.headers["host"] = host
+ if self.host_header is not None:
+ self.host_header = host
+
+ @property
+ def host_header(self) -> Optional[str]:
+ """
+ The request's host/authority header.
+
+ This property maps to either ``request.headers["Host"]`` or
+ ``request.headers[":authority"]``, depending on whether it's HTTP/1.x or HTTP/2.0.
+ """
+ if ":authority" in self.headers:
+ return self.headers[":authority"]
+ if "Host" in self.headers:
+ return self.headers["Host"]
+ return None
+
+ @host_header.setter
+ def host_header(self, val: Optional[str]) -> None:
+ if val is None:
+ self.headers.pop("Host", None)
+ self.headers.pop(":authority", None)
+ elif self.host_header is not None:
+ # Update any existing headers.
+ if ":authority" in self.headers:
+ self.headers[":authority"] = val
+ if "Host" in self.headers:
+ self.headers["Host"] = val
+ else:
+ # Only add the correct new header.
+ if self.http_version.upper().startswith("HTTP/2"):
+ self.headers[":authority"] = val
else:
- self.headers.pop("host")
+ self.headers["Host"] = val
+
+ @host_header.deleter
+ def host_header(self):
+ self.host_header = None
@property
def port(self):
@@ -211,9 +245,10 @@ class Request(message.Message):
def _parse_host_header(self):
"""Extract the host and port from Host header"""
- if "host" not in self.headers:
+ host = self.host_header
+ if not host:
return None, None
- host, port = self.headers["host"], None
+ port = None
m = host_header_re.match(host)
if m:
host = m.group("host").strip("[]")
diff --git a/mitmproxy/options.py b/mitmproxy/options.py
index 630d2964..2467b9dd 100644
--- a/mitmproxy/options.py
+++ b/mitmproxy/options.py
@@ -54,6 +54,7 @@ class Options(optmanager.OptManager):
server_replay_ignore_params: Sequence[str] = [],
server_replay_ignore_payload_params: Sequence[str] = [],
server_replay_ignore_host: bool = False,
+
# Proxy options
auth_nonanonymous: bool = False,
auth_singleuser: Optional[str] = None,
@@ -65,15 +66,18 @@ class Options(optmanager.OptManager):
ciphers_client: str=DEFAULT_CLIENT_CIPHERS,
ciphers_server: Optional[str]=None,
clientcerts: Optional[str] = None,
- http2: bool = True,
ignore_hosts: Sequence[str] = [],
listen_host: str = "",
listen_port: int = LISTEN_PORT,
upstream_bind_address: str = "",
mode: str = "regular",
no_upstream_cert: bool = False,
- rawtcp: bool = False,
+
+ http2: bool = True,
+ http2_priority: bool = False,
websocket: bool = True,
+ rawtcp: bool = False,
+
spoof_source_address: bool = False,
upstream_server: Optional[str] = None,
upstream_auth: Optional[str] = None,
@@ -152,15 +156,18 @@ class Options(optmanager.OptManager):
self.ciphers_client = ciphers_client
self.ciphers_server = ciphers_server
self.clientcerts = clientcerts
- self.http2 = http2
self.ignore_hosts = ignore_hosts
self.listen_host = listen_host
self.listen_port = listen_port
self.upstream_bind_address = upstream_bind_address
self.mode = mode
self.no_upstream_cert = no_upstream_cert
- self.rawtcp = rawtcp
+
+ self.http2 = http2
+ self.http2_priority = http2_priority
self.websocket = websocket
+ self.rawtcp = rawtcp
+
self.spoof_source_address = spoof_source_address
self.upstream_server = upstream_server
self.upstream_auth = upstream_auth
diff --git a/mitmproxy/proxy/protocol/http.py b/mitmproxy/proxy/protocol/http.py
index da9a8781..a7d56f24 100644
--- a/mitmproxy/proxy/protocol/http.py
+++ b/mitmproxy/proxy/protocol/http.py
@@ -291,7 +291,7 @@ class HttpLayer(base.Layer):
# update host header in reverse proxy mode
if self.config.options.mode == "reverse":
- f.request.headers["Host"] = self.config.upstream_server.address.host
+ f.request.host_header = self.config.upstream_server.address.host
# Determine .scheme, .host and .port attributes for inline scripts. For
# absolute-form requests, they are directly given in the request. For
@@ -301,11 +301,10 @@ class HttpLayer(base.Layer):
if self.mode is HTTPMode.transparent:
# Setting request.host also updates the host header, which we want
# to preserve
- host_header = f.request.headers.get("host", None)
+ host_header = f.request.host_header
f.request.host = self.__initial_server_conn.address.host
f.request.port = self.__initial_server_conn.address.port
- if host_header:
- f.request.headers["host"] = host_header
+ f.request.host_header = host_header # set again as .host overwrites this.
f.request.scheme = "https" if self.__initial_server_tls else "http"
self.channel.ask("request", f)
diff --git a/mitmproxy/proxy/protocol/http2.py b/mitmproxy/proxy/protocol/http2.py
index b7548221..cdce24b3 100644
--- a/mitmproxy/proxy/protocol/http2.py
+++ b/mitmproxy/proxy/protocol/http2.py
@@ -268,6 +268,10 @@ class Http2Layer(base.Layer):
return True
def _handle_priority_updated(self, eid, event):
+ if not self.config.options.http2_priority:
+ self.log("HTTP/2 PRIORITY frame surpressed. Use --http2-priority to enable forwarding.", "debug")
+ return True
+
if eid in self.streams and self.streams[eid].handled_priority_event is event:
# this event was already handled during stream creation
# HeadersFrame + Priority information as RequestReceived
@@ -527,9 +531,12 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr
if self.handled_priority_event:
# only send priority information if they actually came with the original HeadersFrame
# and not if they got updated before/after with a PriorityFrame
- priority_exclusive = self.priority_exclusive
- priority_depends_on = self._map_depends_on_stream_id(self.server_stream_id, self.priority_depends_on)
- priority_weight = self.priority_weight
+ if not self.config.options.http2_priority:
+ self.log("HTTP/2 PRIORITY information in HEADERS frame surpressed. Use --http2-priority to enable forwarding.", "debug")
+ else:
+ priority_exclusive = self.priority_exclusive
+ priority_depends_on = self._map_depends_on_stream_id(self.server_stream_id, self.priority_depends_on)
+ priority_weight = self.priority_weight
try:
self.connections[self.server_conn].safe_send_headers(
@@ -610,7 +617,7 @@ class Http2SingleStreamLayer(httpbase._HttpTransmissionLayer, basethread.BaseThr
chunks
)
- def __call__(self):
+ def __call__(self): # pragma: no cover
raise EnvironmentError('Http2SingleStreamLayer must be run as thread')
def run(self):
diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py
index 1c620fd6..bb0bb17a 100644
--- a/mitmproxy/tools/cmdline.py
+++ b/mitmproxy/tools/cmdline.py
@@ -137,7 +137,6 @@ def get_common_options(args):
ciphers_client = args.ciphers_client,
ciphers_server = args.ciphers_server,
clientcerts = args.clientcerts,
- http2 = args.http2,
ignore_hosts = args.ignore_hosts,
listen_host = args.addr,
listen_port = args.port,
@@ -145,8 +144,12 @@ def get_common_options(args):
mode = mode,
no_upstream_cert = args.no_upstream_cert,
spoof_source_address = args.spoof_source_address,
- rawtcp = args.rawtcp,
+
+ http2 = args.http2,
+ http2_priority = args.http2_priority,
websocket = args.websocket,
+ rawtcp = args.rawtcp,
+
upstream_server = upstream_server,
upstream_auth = args.upstream_auth,
ssl_version_client = args.ssl_version_client,
@@ -334,18 +337,26 @@ def proxy_options(parser):
)
http2 = group.add_mutually_exclusive_group()
- http2.add_argument("--no-http2", action="store_false", dest="http2",
+ http2.add_argument("--no-http2", action="store_false", dest="http2")
+ http2.add_argument("--http2", action="store_true", dest="http2",
help="Explicitly enable/disable HTTP/2 support. "
- "Enabled by default."
+ "HTTP/2 support is enabled by default.",
)
- http2.add_argument("--http2", action="store_true", dest="http2")
+
+ http2_priority = group.add_mutually_exclusive_group()
+ http2_priority.add_argument("--http2-priority", action="store_true", dest="http2_priority")
+ http2_priority.add_argument("--no-http2-priority", action="store_false", dest="http2_priority",
+ help="Explicitly enable/disable PRIORITY forwarding for HTTP/2 connections. "
+ "PRIORITY forwarding is disabled by default, "
+ "because some webservers fail at implementing the RFC properly.",
+ )
websocket = group.add_mutually_exclusive_group()
- websocket.add_argument("--no-websocket", action="store_false", dest="websocket",
+ websocket.add_argument("--no-websocket", action="store_false", dest="websocket")
+ websocket.add_argument("--websocket", action="store_true", dest="websocket",
help="Explicitly enable/disable WebSocket support. "
- "Enabled by default."
+ "WebSocket support is enabled by default.",
)
- websocket.add_argument("--websocket", action="store_true", dest="websocket")
parser.add_argument(
"--upstream-auth",
diff --git a/setup.cfg b/setup.cfg
index d01e12f3..52f51f26 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -22,7 +22,6 @@ exclude_lines =
[tool:full_coverage]
exclude =
mitmproxy/contentviews/__init__.py
- mitmproxy/contentviews/protobuf.py
mitmproxy/contentviews/wbxml.py
mitmproxy/contentviews/xml_html.py
mitmproxy/net/tcp.py
@@ -49,3 +48,65 @@ exclude =
pathod/pathod.py
pathod/test.py
pathod/protocols/http2.py
+
+[tool:individual_coverage]
+exclude =
+ mitmproxy/addonmanager.py
+ mitmproxy/addons/onboardingapp/app.py
+ mitmproxy/addons/termlog.py
+ mitmproxy/certs.py
+ mitmproxy/connections.py
+ mitmproxy/contentviews/base.py
+ mitmproxy/contentviews/wbxml.py
+ mitmproxy/contentviews/xml_html.py
+ mitmproxy/controller.py
+ mitmproxy/ctx.py
+ mitmproxy/exceptions.py
+ mitmproxy/export.py
+ mitmproxy/flow.py
+ mitmproxy/flowfilter.py
+ mitmproxy/http.py
+ mitmproxy/io.py
+ mitmproxy/io_compat.py
+ mitmproxy/log.py
+ mitmproxy/master.py
+ mitmproxy/net/check.py
+ mitmproxy/net/http/cookies.py
+ mitmproxy/net/http/headers.py
+ mitmproxy/net/http/message.py
+ mitmproxy/net/http/multipart.py
+ mitmproxy/net/http/url.py
+ mitmproxy/net/tcp.py
+ mitmproxy/options.py
+ mitmproxy/optmanager.py
+ mitmproxy/proxy/config.py
+ mitmproxy/proxy/modes/http_proxy.py
+ mitmproxy/proxy/modes/reverse_proxy.py
+ mitmproxy/proxy/modes/socks_proxy.py
+ mitmproxy/proxy/modes/transparent_proxy.py
+ mitmproxy/proxy/protocol/base.py
+ mitmproxy/proxy/protocol/http.py
+ mitmproxy/proxy/protocol/http1.py
+ mitmproxy/proxy/protocol/http2.py
+ mitmproxy/proxy/protocol/http_replay.py
+ mitmproxy/proxy/protocol/rawtcp.py
+ mitmproxy/proxy/protocol/tls.py
+ mitmproxy/proxy/protocol/websocket.py
+ mitmproxy/proxy/root_context.py
+ mitmproxy/proxy/server.py
+ mitmproxy/stateobject.py
+ mitmproxy/types/multidict.py
+ mitmproxy/utils/bits.py
+ pathod/language/actions.py
+ pathod/language/base.py
+ pathod/language/exceptions.py
+ pathod/language/generators.py
+ pathod/language/http.py
+ pathod/language/message.py
+ pathod/log.py
+ pathod/pathoc.py
+ pathod/pathod.py
+ pathod/protocols/http.py
+ pathod/protocols/http2.py
+ pathod/protocols/websockets.py
+ pathod/test.py
diff --git a/setup.py b/setup.py
index 1b3f08a4..a942a9e3 100644
--- a/setup.py
+++ b/setup.py
@@ -110,9 +110,6 @@ setup(
"sphinx_rtd_theme>=0.1.9, <0.2",
],
'contentviews': [
- "protobuf>=3.1.0, <3.3",
- # TODO: Find Python 3 replacement
- # "pyamf>=0.8.0, <0.9",
],
'examples': [
"beautifulsoup4>=4.4.1, <4.6",
diff --git a/test/full_coverage_plugin.py b/test/full_coverage_plugin.py
index e9951af9..d98c29d6 100644
--- a/test/full_coverage_plugin.py
+++ b/test/full_coverage_plugin.py
@@ -2,6 +2,8 @@ import os
import configparser
import pytest
+here = os.path.abspath(os.path.dirname(__file__))
+
enable_coverage = False
coverage_values = []
@@ -36,7 +38,7 @@ def pytest_configure(config):
)
c = configparser.ConfigParser()
- c.read('setup.cfg')
+ c.read(os.path.join(here, "..", "setup.cfg"))
fs = c['tool:full_coverage']['exclude'].split('\n')
no_full_cov = config.option.no_full_cov + [f.strip() for f in fs]
diff --git a/test/individual_coverage.py b/test/individual_coverage.py
new file mode 100644
index 00000000..35bcd27f
--- /dev/null
+++ b/test/individual_coverage.py
@@ -0,0 +1,82 @@
+import io
+import contextlib
+import os
+import sys
+import glob
+import multiprocessing
+import configparser
+import itertools
+import pytest
+
+
+def run_tests(src, test, fail):
+ stderr = io.StringIO()
+ stdout = io.StringIO()
+ with contextlib.redirect_stderr(stderr):
+ with contextlib.redirect_stdout(stdout):
+ e = pytest.main([
+ '-qq',
+ '--disable-pytest-warnings',
+ '--no-faulthandler',
+ '--cov', src.replace('.py', '').replace('/', '.'),
+ '--cov-fail-under', '100',
+ '--cov-report', 'term-missing:skip-covered',
+ test
+ ])
+
+ if e == 0:
+ if fail:
+ print("SUCCESS but should have FAILED:", src, "Please remove this file from setup.cfg tool:individual_coverage/exclude.")
+ e = 42
+ else:
+ print("SUCCESS:", src)
+ else:
+ if fail:
+ print("Ignoring fail:", src)
+ e = 0
+ else:
+ cov = [l for l in stdout.getvalue().split("\n") if (src in l) or ("was never imported" in l)]
+ if len(cov) == 1:
+ print("FAIL:", cov[0])
+ else:
+ print("FAIL:", src, test, stdout.getvalue(), stdout.getvalue())
+ print(stderr.getvalue())
+ print(stdout.getvalue())
+
+ sys.exit(e)
+
+
+def start_pytest(src, test, fail):
+ # run pytest in a new process, otherwise imports and modules might conflict
+ proc = multiprocessing.Process(target=run_tests, args=(src, test, fail))
+ proc.start()
+ proc.join()
+ return (src, test, proc.exitcode)
+
+
+def main():
+ c = configparser.ConfigParser()
+ c.read('setup.cfg')
+ fs = c['tool:individual_coverage']['exclude'].strip().split('\n')
+ no_individual_cov = [f.strip() for f in fs]
+
+ excluded = ['mitmproxy/contrib/', 'mitmproxy/test/', 'mitmproxy/tools/', 'mitmproxy/platform/']
+ src_files = glob.glob('mitmproxy/**/*.py', recursive=True) + glob.glob('pathod/**/*.py', recursive=True)
+ src_files = [f for f in src_files if os.path.basename(f) != '__init__.py']
+ src_files = [f for f in src_files if not any(os.path.normpath(p) in f for p in excluded)]
+
+ ps = []
+ for src in sorted(src_files):
+ test = os.path.join("test", os.path.dirname(src), "test_" + os.path.basename(src))
+ if os.path.isfile(test):
+ ps.append((src, test, src in no_individual_cov))
+
+ result = list(itertools.starmap(start_pytest, ps))
+
+ if any(e != 0 for _, _, e in result):
+ sys.exit(1)
+ pass
+
+
+if __name__ == '__main__':
+ main()
diff --git a/test/mitmproxy/contentviews/test_protobuf.py b/test/mitmproxy/contentviews/test_protobuf.py
index 1224b8db..31e382ec 100644
--- a/test/mitmproxy/contentviews/test_protobuf.py
+++ b/test/mitmproxy/contentviews/test_protobuf.py
@@ -1,12 +1,50 @@
+from unittest import mock
+import pytest
+
from mitmproxy.contentviews import protobuf
from mitmproxy.test import tutils
from . import full_eval
-if protobuf.ViewProtobuf.is_available():
- def test_view_protobuf_request():
- v = full_eval(protobuf.ViewProtobuf())
- p = tutils.test_data.path("mitmproxy/data/protobuf01")
- content_type, output = v(open(p, "rb").read())
- assert content_type == "Protobuf"
- assert output.next()[0][1] == '1: "3bbc333c-e61c-433b-819a-0b9a8cc103b8"'
+def test_view_protobuf_request():
+ v = full_eval(protobuf.ViewProtobuf())
+ p = tutils.test_data.path("mitmproxy/data/protobuf01")
+
+ with mock.patch('mitmproxy.contentviews.protobuf.ViewProtobuf.is_available'):
+ with mock.patch('subprocess.Popen') as n:
+ m = mock.Mock()
+ attrs = {'communicate.return_value': (b'1: "3bbc333c-e61c-433b-819a-0b9a8cc103b8"', True)}
+ m.configure_mock(**attrs)
+ n.return_value = m
+
+ content_type, output = v(open(p, "rb").read())
+ assert content_type == "Protobuf"
+ assert output[0] == [('text', b'1: "3bbc333c-e61c-433b-819a-0b9a8cc103b8"')]
+
+ m.communicate = mock.MagicMock()
+ m.communicate.return_value = (None, None)
+ with pytest.raises(ValueError, matches="Failed to parse input."):
+ v(b'foobar')
+
+
+def test_view_protobuf_availability():
+ with mock.patch('subprocess.Popen') as n:
+ m = mock.Mock()
+ attrs = {'communicate.return_value': (b'libprotoc fake version', True)}
+ m.configure_mock(**attrs)
+ n.return_value = m
+ assert protobuf.ViewProtobuf().is_available()
+
+ m = mock.Mock()
+ attrs = {'communicate.return_value': (b'command not found', True)}
+ m.configure_mock(**attrs)
+ n.return_value = m
+ assert not protobuf.ViewProtobuf().is_available()
+
+
+def test_view_protobuf_fallback():
+ with mock.patch('subprocess.Popen.communicate') as m:
+ m.side_effect = OSError()
+ v = full_eval(protobuf.ViewProtobuf())
+ with pytest.raises(NotImplementedError, matches='protoc not found'):
+ v(b'foobar')
diff --git a/test/mitmproxy/net/http/test_request.py b/test/mitmproxy/net/http/test_request.py
index 6fe57010..90ec31fe 100644
--- a/test/mitmproxy/net/http/test_request.py
+++ b/test/mitmproxy/net/http/test_request.py
@@ -97,7 +97,7 @@ class TestRequestCore:
request.host = d
assert request.data.host == b"foo\xFF\x00bar"
- def test_host_header_update(self):
+ def test_host_update_also_updates_header(self):
request = treq()
assert "host" not in request.headers
request.host = "example.com"
@@ -107,6 +107,51 @@ class TestRequestCore:
request.host = "example.org"
assert request.headers["Host"] == "example.org"
+ def test_get_host_header(self):
+ no_hdr = treq()
+ assert no_hdr.host_header is None
+
+ h1 = treq(headers=(
+ (b"host", b"example.com"),
+ ))
+ assert h1.host_header == "example.com"
+
+ h2 = treq(headers=(
+ (b":authority", b"example.org"),
+ ))
+ assert h2.host_header == "example.org"
+
+ both_hdrs = treq(headers=(
+ (b"host", b"example.org"),
+ (b":authority", b"example.com"),
+ ))
+ assert both_hdrs.host_header == "example.com"
+
+ def test_modify_host_header(self):
+ h1 = treq()
+ assert "host" not in h1.headers
+ assert ":authority" not in h1.headers
+ h1.host_header = "example.com"
+ assert "host" in h1.headers
+ assert ":authority" not in h1.headers
+ h1.host_header = None
+ assert "host" not in h1.headers
+
+ h2 = treq(http_version=b"HTTP/2.0")
+ h2.host_header = "example.org"
+ assert "host" not in h2.headers
+ assert ":authority" in h2.headers
+ del h2.host_header
+ assert ":authority" not in h2.headers
+
+ both_hdrs = treq(headers=(
+ (b":authority", b"example.com"),
+ (b"host", b"example.org"),
+ ))
+ both_hdrs.host_header = "foo.example.com"
+ assert both_hdrs.headers["Host"] == "foo.example.com"
+ assert both_hdrs.headers[":authority"] == "foo.example.com"
+
class TestRequestUtils:
"""
diff --git a/test/mitmproxy/proxy/protocol/test_http2.py b/test/mitmproxy/proxy/protocol/test_http2.py
index cede0b80..cb9c0474 100644
--- a/test/mitmproxy/proxy/protocol/test_http2.py
+++ b/test/mitmproxy/proxy/protocol/test_http2.py
@@ -4,7 +4,7 @@
import os
import tempfile
import traceback
-
+import pytest
import h2
from mitmproxy import options
@@ -272,13 +272,13 @@ class TestSimple(_Http2Test):
@requires_alpn
-class TestRequestWithPriority(_Http2Test):
+class TestForbiddenHeaders(_Http2Test):
@classmethod
def handle_server_event(cls, event, h2_conn, rfile, wfile):
if isinstance(event, h2.events.ConnectionTerminated):
return False
- elif isinstance(event, h2.events.RequestReceived):
+ elif isinstance(event, h2.events.StreamEnded):
import warnings
with warnings.catch_warnings():
# Ignore UnicodeWarning:
@@ -289,60 +289,18 @@ class TestRequestWithPriority(_Http2Test):
warnings.simplefilter("ignore")
- headers = [(':status', '200')]
- if event.priority_updated:
- headers.append(('priority_exclusive', str(event.priority_updated.exclusive).encode()))
- headers.append(('priority_depends_on', str(event.priority_updated.depends_on).encode()))
- headers.append(('priority_weight', str(event.priority_updated.weight).encode()))
- h2_conn.send_headers(event.stream_id, headers)
+ h2_conn.config.validate_outbound_headers = False
+ h2_conn.send_headers(event.stream_id, [
+ (':status', '200'),
+ ('keep-alive', 'foobar'),
+ ])
+ h2_conn.send_data(event.stream_id, b'response body')
h2_conn.end_stream(event.stream_id)
wfile.write(h2_conn.data_to_send())
wfile.flush()
return True
- def test_request_with_priority(self):
- client, h2_conn = self._setup_connection()
-
- self._send_request(
- client.wfile,
- h2_conn,
- headers=[
- (':authority', "127.0.0.1:{}".format(self.server.server.address.port)),
- (':method', 'GET'),
- (':scheme', 'https'),
- (':path', '/'),
- ],
- priority_exclusive=True,
- priority_depends_on=42424242,
- priority_weight=42,
- )
-
- done = False
- while not done:
- try:
- raw = b''.join(http2.read_raw_frame(client.rfile))
- events = h2_conn.receive_data(raw)
- except exceptions.HttpException:
- print(traceback.format_exc())
- assert False
-
- client.wfile.write(h2_conn.data_to_send())
- client.wfile.flush()
-
- for event in events:
- if isinstance(event, h2.events.StreamEnded):
- done = True
-
- h2_conn.close_connection()
- client.wfile.write(h2_conn.data_to_send())
- client.wfile.flush()
-
- assert len(self.master.state.flows) == 1
- assert self.master.state.flows[0].response.headers['priority_exclusive'] == 'True'
- assert self.master.state.flows[0].response.headers['priority_depends_on'] == '42424242'
- assert self.master.state.flows[0].response.headers['priority_weight'] == '42'
-
- def test_request_without_priority(self):
+ def test_forbidden_headers(self):
client, h2_conn = self._setup_connection()
self._send_request(
@@ -353,8 +311,7 @@ class TestRequestWithPriority(_Http2Test):
(':method', 'GET'),
(':scheme', 'https'),
(':path', '/'),
- ],
- )
+ ])
done = False
while not done:
@@ -369,7 +326,9 @@ class TestRequestWithPriority(_Http2Test):
client.wfile.flush()
for event in events:
- if isinstance(event, h2.events.StreamEnded):
+ if isinstance(event, h2.events.ResponseReceived):
+ assert 'keep-alive' not in event.headers
+ elif isinstance(event, h2.events.StreamEnded):
done = True
h2_conn.close_connection()
@@ -377,21 +336,17 @@ class TestRequestWithPriority(_Http2Test):
client.wfile.flush()
assert len(self.master.state.flows) == 1
- assert 'priority_exclusive' not in self.master.state.flows[0].response.headers
- assert 'priority_depends_on' not in self.master.state.flows[0].response.headers
- assert 'priority_weight' not in self.master.state.flows[0].response.headers
+ assert self.master.state.flows[0].response.status_code == 200
+ assert self.master.state.flows[0].response.headers['keep-alive'] == 'foobar'
@requires_alpn
-class TestPriority(_Http2Test):
- priority_data = None
+class TestRequestWithPriority(_Http2Test):
@classmethod
def handle_server_event(cls, event, h2_conn, rfile, wfile):
if isinstance(event, h2.events.ConnectionTerminated):
return False
- elif isinstance(event, h2.events.PriorityUpdated):
- cls.priority_data = (event.exclusive, event.depends_on, event.weight)
elif isinstance(event, h2.events.RequestReceived):
import warnings
with warnings.catch_warnings():
@@ -404,18 +359,26 @@ class TestPriority(_Http2Test):
warnings.simplefilter("ignore")
headers = [(':status', '200')]
+ if event.priority_updated:
+ headers.append(('priority_exclusive', str(event.priority_updated.exclusive).encode()))
+ headers.append(('priority_depends_on', str(event.priority_updated.depends_on).encode()))
+ headers.append(('priority_weight', str(event.priority_updated.weight).encode()))
h2_conn.send_headers(event.stream_id, headers)
h2_conn.end_stream(event.stream_id)
wfile.write(h2_conn.data_to_send())
wfile.flush()
return True
- def test_priority(self):
- client, h2_conn = self._setup_connection()
+ @pytest.mark.parametrize("http2_priority_enabled, priority, expected_priority", [
+ (True, (True, 42424242, 42), ('True', '42424242', '42')),
+ (False, (True, 42424242, 42), (None, None, None)),
+ (True, (None, None, None), (None, None, None)),
+ (False, (None, None, None), (None, None, None)),
+ ])
+ def test_request_with_priority(self, http2_priority_enabled, priority, expected_priority):
+ self.config.options.http2_priority = http2_priority_enabled
- h2_conn.prioritize(1, exclusive=True, depends_on=0, weight=42)
- client.wfile.write(h2_conn.data_to_send())
- client.wfile.flush()
+ client, h2_conn = self._setup_connection()
self._send_request(
client.wfile,
@@ -426,6 +389,9 @@ class TestPriority(_Http2Test):
(':scheme', 'https'),
(':path', '/'),
],
+ priority_exclusive=priority[0],
+ priority_depends_on=priority[1],
+ priority_weight=priority[2],
)
done = False
@@ -449,12 +415,15 @@ class TestPriority(_Http2Test):
client.wfile.flush()
assert len(self.master.state.flows) == 1
- assert self.priority_data == (True, 0, 42)
+
+ resp = self.master.state.flows[0].response
+ assert resp.headers.get('priority_exclusive', None) == expected_priority[0]
+ assert resp.headers.get('priority_depends_on', None) == expected_priority[1]
+ assert resp.headers.get('priority_weight', None) == expected_priority[2]
@requires_alpn
-class TestPriorityWithExistingStream(_Http2Test):
- priority_data = []
+class TestPriority(_Http2Test):
@classmethod
def handle_server_event(cls, event, h2_conn, rfile, wfile):
@@ -463,8 +432,6 @@ class TestPriorityWithExistingStream(_Http2Test):
elif isinstance(event, h2.events.PriorityUpdated):
cls.priority_data.append((event.exclusive, event.depends_on, event.weight))
elif isinstance(event, h2.events.RequestReceived):
- assert not event.priority_updated
-
import warnings
with warnings.catch_warnings():
# Ignore UnicodeWarning:
@@ -477,17 +444,27 @@ class TestPriorityWithExistingStream(_Http2Test):
headers = [(':status', '200')]
h2_conn.send_headers(event.stream_id, headers)
- wfile.write(h2_conn.data_to_send())
- wfile.flush()
- elif isinstance(event, h2.events.StreamEnded):
h2_conn.end_stream(event.stream_id)
wfile.write(h2_conn.data_to_send())
wfile.flush()
return True
- def test_priority_with_existing_stream(self):
+ @pytest.mark.parametrize("prioritize_before", [True, False])
+ @pytest.mark.parametrize("http2_priority_enabled, priority, expected_priority", [
+ (True, (True, 42424242, 42), [(True, 42424242, 42)]),
+ (False, (True, 42424242, 42), []),
+ ])
+ def test_priority(self, prioritize_before, http2_priority_enabled, priority, expected_priority):
+ self.config.options.http2_priority = http2_priority_enabled
+ self.__class__.priority_data = []
+
client, h2_conn = self._setup_connection()
+ if prioritize_before:
+ h2_conn.prioritize(1, exclusive=priority[0], depends_on=priority[1], weight=priority[2])
+ client.wfile.write(h2_conn.data_to_send())
+ client.wfile.flush()
+
self._send_request(
client.wfile,
h2_conn,
@@ -497,13 +474,14 @@ class TestPriorityWithExistingStream(_Http2Test):
(':scheme', 'https'),
(':path', '/'),
],
- end_stream=False,
+ end_stream=prioritize_before,
)
- h2_conn.prioritize(1, exclusive=True, depends_on=0, weight=42)
- h2_conn.end_stream(1)
- client.wfile.write(h2_conn.data_to_send())
- client.wfile.flush()
+ if not prioritize_before:
+ h2_conn.prioritize(1, exclusive=priority[0], depends_on=priority[1], weight=priority[2])
+ h2_conn.end_stream(1)
+ client.wfile.write(h2_conn.data_to_send())
+ client.wfile.flush()
done = False
while not done:
@@ -526,7 +504,7 @@ class TestPriorityWithExistingStream(_Http2Test):
client.wfile.flush()
assert len(self.master.state.flows) == 1
- assert self.priority_data == [(True, 0, 42)]
+ assert self.priority_data == expected_priority
@requires_alpn
diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py
index 65e6845f..a78e5f80 100644
--- a/test/mitmproxy/test_flow.py
+++ b/test/mitmproxy/test_flow.py
@@ -10,7 +10,6 @@ from mitmproxy.exceptions import FlowReadException, Kill
from mitmproxy import flow
from mitmproxy import http
from mitmproxy import connections
-from mitmproxy import tcp
from mitmproxy.proxy import ProxyConfig
from mitmproxy.proxy.server import DummyServer
from mitmproxy import master
@@ -157,117 +156,6 @@ class TestHTTPFlow:
assert f.response.raw_content == b"abarb"
-class TestWebSocketFlow:
-
- def test_copy(self):
- f = tflow.twebsocketflow()
- f.get_state()
- f2 = f.copy()
- a = f.get_state()
- b = f2.get_state()
- del a["id"]
- del b["id"]
- del a["handshake_flow"]["id"]
- del b["handshake_flow"]["id"]
- assert a == b
- assert not f == f2
- assert f is not f2
-
- assert f.client_key == f2.client_key
- assert f.client_protocol == f2.client_protocol
- assert f.client_extensions == f2.client_extensions
- assert f.server_accept == f2.server_accept
- assert f.server_protocol == f2.server_protocol
- assert f.server_extensions == f2.server_extensions
- assert f.messages is not f2.messages
- assert f.handshake_flow is not f2.handshake_flow
-
- for m in f.messages:
- m2 = m.copy()
- m2.set_state(m2.get_state())
- assert m is not m2
- assert m.get_state() == m2.get_state()
-
- f = tflow.twebsocketflow(err=True)
- f2 = f.copy()
- assert f is not f2
- assert f.handshake_flow is not f2.handshake_flow
- assert f.error.get_state() == f2.error.get_state()
- assert f.error is not f2.error
-
- def test_match(self):
- f = tflow.twebsocketflow()
- assert not flowfilter.match("~b nonexistent", f)
- assert flowfilter.match(None, f)
- assert not flowfilter.match("~b nonexistent", f)
-
- f = tflow.twebsocketflow(err=True)
- assert flowfilter.match("~e", f)
-
- with pytest.raises(ValueError):
- flowfilter.match("~", f)
-
- def test_repr(self):
- f = tflow.twebsocketflow()
- assert 'WebSocketFlow' in repr(f)
- assert 'binary message: ' in repr(f.messages[0])
- assert 'text message: ' in repr(f.messages[1])
-
-
-class TestTCPFlow:
-
- def test_copy(self):
- f = tflow.ttcpflow()
- f.get_state()
- f2 = f.copy()
- a = f.get_state()
- b = f2.get_state()
- del a["id"]
- del b["id"]
- assert a == b
- assert not f == f2
- assert f is not f2
-
- assert f.messages is not f2.messages
-
- for m in f.messages:
- assert m.get_state()
- m2 = m.copy()
- assert not m == m2
- assert m is not m2
-
- a = m.get_state()
- b = m2.get_state()
- assert a == b
-
- m = tcp.TCPMessage(False, 'foo')
- m.set_state(f.messages[0].get_state())
- assert m.timestamp == f.messages[0].timestamp
-
- f = tflow.ttcpflow(err=True)
- f2 = f.copy()
- assert f is not f2
- assert f.error.get_state() == f2.error.get_state()
- assert f.error is not f2.error
-
- def test_match(self):
- f = tflow.ttcpflow()
- assert not flowfilter.match("~b nonexistent", f)
- assert flowfilter.match(None, f)
- assert not flowfilter.match("~b nonexistent", f)
-
- f = tflow.ttcpflow(err=True)
- assert flowfilter.match("~e", f)
-
- with pytest.raises(ValueError):
- flowfilter.match("~", f)
-
- def test_repr(self):
- f = tflow.ttcpflow()
- assert 'TCPFlow' in repr(f)
- assert '-> ' in repr(f.messages[0])
-
-
class TestSerialize:
def _treader(self):
diff --git a/test/mitmproxy/test_tcp.py b/test/mitmproxy/test_tcp.py
index 777ab4dd..dce6493c 100644
--- a/test/mitmproxy/test_tcp.py
+++ b/test/mitmproxy/test_tcp.py
@@ -1 +1,59 @@
-# TODO: write tests
+import pytest
+
+from mitmproxy import tcp
+from mitmproxy import flowfilter
+from mitmproxy.test import tflow
+
+
+class TestTCPFlow:
+
+ def test_copy(self):
+ f = tflow.ttcpflow()
+ f.get_state()
+ f2 = f.copy()
+ a = f.get_state()
+ b = f2.get_state()
+ del a["id"]
+ del b["id"]
+ assert a == b
+ assert not f == f2
+ assert f is not f2
+
+ assert f.messages is not f2.messages
+
+ for m in f.messages:
+ assert m.get_state()
+ m2 = m.copy()
+ assert not m == m2
+ assert m is not m2
+
+ a = m.get_state()
+ b = m2.get_state()
+ assert a == b
+
+ m = tcp.TCPMessage(False, 'foo')
+ m.set_state(f.messages[0].get_state())
+ assert m.timestamp == f.messages[0].timestamp
+
+ f = tflow.ttcpflow(err=True)
+ f2 = f.copy()
+ assert f is not f2
+ assert f.error.get_state() == f2.error.get_state()
+ assert f.error is not f2.error
+
+ def test_match(self):
+ f = tflow.ttcpflow()
+ assert not flowfilter.match("~b nonexistent", f)
+ assert flowfilter.match(None, f)
+ assert not flowfilter.match("~b nonexistent", f)
+
+ f = tflow.ttcpflow(err=True)
+ assert flowfilter.match("~e", f)
+
+ with pytest.raises(ValueError):
+ flowfilter.match("~", f)
+
+ def test_repr(self):
+ f = tflow.ttcpflow()
+ assert 'TCPFlow' in repr(f)
+ assert '-> ' in repr(f.messages[0])
diff --git a/test/mitmproxy/test_websocket.py b/test/mitmproxy/test_websocket.py
index 777ab4dd..f2963390 100644
--- a/test/mitmproxy/test_websocket.py
+++ b/test/mitmproxy/test_websocket.py
@@ -1 +1,62 @@
-# TODO: write tests
+import pytest
+
+from mitmproxy import flowfilter
+from mitmproxy.test import tflow
+
+
+class TestWebSocketFlow:
+
+ def test_copy(self):
+ f = tflow.twebsocketflow()
+ f.get_state()
+ f2 = f.copy()
+ a = f.get_state()
+ b = f2.get_state()
+ del a["id"]
+ del b["id"]
+ del a["handshake_flow"]["id"]
+ del b["handshake_flow"]["id"]
+ assert a == b
+ assert not f == f2
+ assert f is not f2
+
+ assert f.client_key == f2.client_key
+ assert f.client_protocol == f2.client_protocol
+ assert f.client_extensions == f2.client_extensions
+ assert f.server_accept == f2.server_accept
+ assert f.server_protocol == f2.server_protocol
+ assert f.server_extensions == f2.server_extensions
+ assert f.messages is not f2.messages
+ assert f.handshake_flow is not f2.handshake_flow
+
+ for m in f.messages:
+ m2 = m.copy()
+ m2.set_state(m2.get_state())
+ assert m is not m2
+ assert m.get_state() == m2.get_state()
+
+ f = tflow.twebsocketflow(err=True)
+ f2 = f.copy()
+ assert f is not f2
+ assert f.handshake_flow is not f2.handshake_flow
+ assert f.error.get_state() == f2.error.get_state()
+ assert f.error is not f2.error
+
+ def test_match(self):
+ f = tflow.twebsocketflow()
+ assert not flowfilter.match("~b nonexistent", f)
+ assert flowfilter.match(None, f)
+ assert not flowfilter.match("~b nonexistent", f)
+
+ f = tflow.twebsocketflow(err=True)
+ assert flowfilter.match("~e", f)
+
+ with pytest.raises(ValueError):
+ flowfilter.match("~", f)
+
+ def test_repr(self):
+ f = tflow.twebsocketflow()
+ assert f.message_info(f.messages[0])
+ assert 'WebSocketFlow' in repr(f)
+ assert 'binary message: ' in repr(f.messages[0])
+ assert 'text message: ' in repr(f.messages[1])
diff --git a/tox.ini b/tox.ini
index 352d0e3c..4994b119 100644
--- a/tox.ini
+++ b/tox.ini
@@ -36,6 +36,12 @@ commands =
mitmproxy/tools/web/ \
mitmproxy/contentviews/
+[testenv:individual_coverage]
+deps =
+ -rrequirements.txt
+commands =
+ python3 test/individual_coverage.py
+
[testenv:wheel]
recreate = True
deps =