diff options
37 files changed, 408 insertions, 281 deletions
diff --git a/docs/pathod/intro.rst b/docs/pathod/intro.rst index bf0c531f..1c1ad60e 100644 --- a/docs/pathod/intro.rst +++ b/docs/pathod/intro.rst @@ -3,6 +3,7 @@ Pathology 101 ============= +.. _pathod: pathod ------ @@ -83,15 +84,14 @@ distinguish them from crafted responses. For example, a request to: .. _pathoc: - pathoc ------ Pathoc is a perverse HTTP daemon designed to let you craft almost any conceivable HTTP request, including ones that creatively violate the standards. HTTP requests are specified using a :ref:`small, terse language <language>`, -which pathod shares with its server-side twin pathod. To view pathoc's complete -range of options, use the command-line help: +which pathoc shares with its server-side twin :ref:`pathod`. To view pathoc's +complete range of options, use the command-line help: >>> pathoc --help diff --git a/mitmproxy/addons/__init__.py b/mitmproxy/addons/__init__.py index 24cf2270..62135765 100644 --- a/mitmproxy/addons/__init__.py +++ b/mitmproxy/addons/__init__.py @@ -1,7 +1,6 @@ from mitmproxy.addons import allowremote from mitmproxy.addons import anticache from mitmproxy.addons import anticomp -from mitmproxy.addons import check_alpn from mitmproxy.addons import check_ca from mitmproxy.addons import clientplayback from mitmproxy.addons import core_option_validation @@ -29,7 +28,6 @@ def default_addons(): allowremote.AllowRemote(), anticache.AntiCache(), anticomp.AntiComp(), - check_alpn.CheckALPN(), check_ca.CheckCA(), clientplayback.ClientPlayback(), cut.Cut(), diff --git a/mitmproxy/addons/check_alpn.py b/mitmproxy/addons/check_alpn.py deleted file mode 100644 index 193159b2..00000000 --- a/mitmproxy/addons/check_alpn.py +++ /dev/null @@ -1,17 +0,0 @@ -import mitmproxy -from mitmproxy.net import tcp -from mitmproxy import ctx - - -class CheckALPN: - def __init__(self): - self.failed = False - - def configure(self, updated): - self.failed = mitmproxy.ctx.master.options.http2 and not tcp.HAS_ALPN - if self.failed: - ctx.log.warn( - "HTTP/2 is disabled because ALPN support missing!\n" - "OpenSSL 1.0.2+ required to support HTTP/2 connections.\n" - "Use --no-http2 to silence this warning." - ) diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index d4319468..b2db0171 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -10,6 +10,7 @@ The View: """ import collections import typing +import os import blinker import sortedcontainers @@ -339,12 +340,17 @@ class View(collections.Sequence): """ Load flows into the view, without processing them with addons. """ - with open(path, "rb") as f: - for i in io.FlowReader(f).stream(): - # Do this to get a new ID, so we can load the same file N times and - # get new flows each time. It would be more efficient to just have a - # .newid() method or something. - self.add([i.copy()]) + path = os.path.expanduser(path) + try: + with open(path, "rb") as f: + for i in io.FlowReader(f).stream(): + # Do this to get a new ID, so we can load the same file N times and + # get new flows each time. It would be more efficient to just have a + # .newid() method or something. + self.add([i.copy()]) + except IOError as e: + ctx.log.error(e.strerror) + return @command.command("view.go") def go(self, dst: int) -> None: diff --git a/mitmproxy/net/tcp.py b/mitmproxy/net/tcp.py index fce0b744..0c2f0e28 100644 --- a/mitmproxy/net/tcp.py +++ b/mitmproxy/net/tcp.py @@ -14,18 +14,12 @@ from typing import Optional # noqa from mitmproxy.utils import strutils import certifi -import OpenSSL from OpenSSL import SSL from mitmproxy import certs -from mitmproxy.utils import version_check from mitmproxy import exceptions from mitmproxy.types import basethread -# This is a rather hackish way to make sure that -# the latest version of pyOpenSSL is actually installed. -version_check.check_pyopenssl_version() - socket_fileobject = socket.SocketIO # workaround for https://bugs.python.org/issue29515 @@ -33,7 +27,6 @@ socket_fileobject = socket.SocketIO IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) EINTR = 4 -HAS_ALPN = SSL._lib.Cryptography_HAS_ALPN # To enable all SSL methods use: SSLv23 # then add options to disable certain methods @@ -503,7 +496,6 @@ class _Connection: if cipher_list: try: context.set_cipher_list(cipher_list.encode()) - context.set_tmp_ecdh(OpenSSL.crypto.get_elliptic_curve('prime256v1')) except SSL.Error as v: raise exceptions.TlsException("SSL cipher specification error: %s" % str(v)) @@ -511,24 +503,23 @@ class _Connection: if log_ssl_key: context.set_info_callback(log_ssl_key) - if HAS_ALPN: # pragma: openssl-old no cover - if alpn_protos is not None: - # advertise application layer protocols - context.set_alpn_protos(alpn_protos) - elif alpn_select is not None and alpn_select_callback is None: - # select application layer protocol - def alpn_select_callback(conn_, options): - if alpn_select in options: - return bytes(alpn_select) - else: # pragma: no cover - return options[0] - context.set_alpn_select_callback(alpn_select_callback) - elif alpn_select_callback is not None and alpn_select is None: - if not callable(alpn_select_callback): - raise exceptions.TlsException("ALPN error: alpn_select_callback must be a function.") - context.set_alpn_select_callback(alpn_select_callback) - elif alpn_select_callback is not None and alpn_select is not None: - raise exceptions.TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (function).") + if alpn_protos is not None: + # advertise application layer protocols + context.set_alpn_protos(alpn_protos) + elif alpn_select is not None and alpn_select_callback is None: + # select application layer protocol + def alpn_select_callback(conn_, options): + if alpn_select in options: + return bytes(alpn_select) + else: # pragma: no cover + return options[0] + context.set_alpn_select_callback(alpn_select_callback) + elif alpn_select_callback is not None and alpn_select is None: + if not callable(alpn_select_callback): + raise exceptions.TlsException("ALPN error: alpn_select_callback must be a function.") + context.set_alpn_select_callback(alpn_select_callback) + elif alpn_select_callback is not None and alpn_select is not None: + raise exceptions.TlsException("ALPN error: only define alpn_select (string) OR alpn_select_callback (function).") return context @@ -720,7 +711,7 @@ class TCPClient(_Connection): return self.connection.gettimeout() def get_alpn_proto_negotiated(self): - if HAS_ALPN and self.ssl_established: # pragma: openssl-old no cover + if self.ssl_established: return self.connection.get_alpn_proto_negotiated() else: return b"" @@ -827,7 +818,7 @@ class BaseHandler(_Connection): self.connection.settimeout(n) def get_alpn_proto_negotiated(self): - if HAS_ALPN and self.ssl_established: # pragma: openssl-old no cover + if self.ssl_established: return self.connection.get_alpn_proto_negotiated() else: return b"" diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index e1d74b8e..c28ec685 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -518,6 +518,7 @@ def save(opts, path, defaults=False): Raises OptionsError if the existing data is corrupt. """ + path = os.path.expanduser(path) if os.path.exists(path) and os.path.isfile(path): with open(path, "rt", encoding="utf8") as f: try: diff --git a/mitmproxy/proxy/protocol/http.py b/mitmproxy/proxy/protocol/http.py index 502280c1..a366861d 100644 --- a/mitmproxy/proxy/protocol/http.py +++ b/mitmproxy/proxy/protocol/http.py @@ -217,16 +217,19 @@ class HttpLayer(base.Layer): return False def handle_upstream_connect(self, f): - self.establish_server_connection( - f.request.host, - f.request.port, - f.request.scheme - ) - self.send_request(f.request) - f.response = self.read_response_headers() - f.response.data.content = b"".join( - self.read_response_body(f.request, f.response) - ) + # if the user specifies a response in the http_connect hook, we do not connect upstream here. + # https://github.com/mitmproxy/mitmproxy/pull/2473 + if not f.response: + self.establish_server_connection( + f.request.host, + f.request.port, + f.request.scheme + ) + self.send_request(f.request) + f.response = self.read_response_headers() + f.response.data.content = b"".join( + self.read_response_body(f.request, f.response) + ) self.send_response(f.response) if is_ok(f.response.status_code): layer = UpstreamConnectLayer(self, f.request) diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index 7debb3e0..58900d29 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -16,7 +16,6 @@ from mitmproxy import exceptions # noqa from mitmproxy import options # noqa from mitmproxy import optmanager # noqa from mitmproxy import proxy # noqa -from mitmproxy.utils import version_check # noqa from mitmproxy.utils import debug # noqa @@ -58,7 +57,6 @@ def run(MasterKlass, args, extra=None): # pragma: no cover extra: Extra argument processing callable which returns a dict of options. """ - version_check.check_pyopenssl_version() debug.register_info_dumpers() opts = options.Options() diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index c6fb2ef6..9a447fe7 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -18,6 +18,7 @@ from mitmproxy import io from mitmproxy import log from mitmproxy import version from mitmproxy import optmanager +from mitmproxy.tools.cmdline import CONFIG_PATH import mitmproxy.tools.web.master # noqa @@ -451,6 +452,14 @@ class Options(RequestHandler): raise APIError(400, "{}".format(err)) +class SaveOptions(RequestHandler): + def post(self): + try: + optmanager.save(self.master.options, CONFIG_PATH, True) + except Exception as err: + raise APIError(400, "{}".format(err)) + + class Application(tornado.web.Application): def __init__(self, master, debug): self.master = master @@ -475,7 +484,8 @@ class Application(tornado.web.Application): FlowContentView), (r"/settings", Settings), (r"/clear", ClearAll), - (r"/options", Options) + (r"/options", Options), + (r"/options/save", SaveOptions) ] settings = dict( template_path=os.path.join(os.path.dirname(__file__), "templates"), diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py index 8c2433ec..dc5b2627 100644 --- a/mitmproxy/tools/web/master.py +++ b/mitmproxy/tools/web/master.py @@ -125,7 +125,6 @@ class WebMaster(master.Master): "No web browser found. Please open a browser and point it to {}".format(web_url), "info" ) - try: iol.start() except KeyboardInterrupt: diff --git a/mitmproxy/utils/version_check.py b/mitmproxy/utils/version_check.py deleted file mode 100644 index 22d6d75c..00000000 --- a/mitmproxy/utils/version_check.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -Having installed a wrong version of pyOpenSSL is unfortunately a very common -source of error. Check before every start that both versions are somewhat okay. -""" -import sys -import inspect -import os.path - -import OpenSSL - -PYOPENSSL_MIN_VERSION = (16, 0) - - -def check_pyopenssl_version(min_version=PYOPENSSL_MIN_VERSION, fp=sys.stderr): - min_version_str = ".".join(str(x) for x in min_version) - try: - v = tuple(int(x) for x in OpenSSL.__version__.split(".")[:2]) - except ValueError: - print( - "Cannot parse pyOpenSSL version: {}" - "mitmproxy requires pyOpenSSL {} or greater.".format( - OpenSSL.__version__, min_version_str - ), - file=fp - ) - return - if v < min_version: - print( - "You are using an outdated version of pyOpenSSL: " - "mitmproxy requires pyOpenSSL {} or greater.".format(min_version_str), - file=fp - ) - # Some users apparently have multiple versions of pyOpenSSL installed. - # Report which one we got. - pyopenssl_path = os.path.dirname(inspect.getfile(OpenSSL)) - print( - "Your pyOpenSSL {} installation is located at {}".format( - OpenSSL.__version__, pyopenssl_path - ), - file=fp - ) - sys.exit(1) diff --git a/pathod/language/generators.py b/pathod/language/generators.py index 1961df74..70c6ad16 100644 --- a/pathod/language/generators.py +++ b/pathod/language/generators.py @@ -75,7 +75,7 @@ class RandomGenerator: class FileGenerator: def __init__(self, path): - self.path = path + self.path = os.path.expanduser(path) def __len__(self): return os.path.getsize(self.path) diff --git a/pathod/pathoc.py b/pathod/pathoc.py index 4a613349..63a15b55 100644 --- a/pathod/pathoc.py +++ b/pathod/pathoc.py @@ -223,14 +223,6 @@ class Pathoc(tcp.TCPClient): self.ws_framereader = None if self.use_http2: - if not tcp.HAS_ALPN: # pragma: no cover - log.write_raw( - self.fp, - "HTTP/2 requires ALPN support. " - "Please use OpenSSL >= 1.0.2. " - "Pathoc might not be working as expected without ALPN.", - timestamp=False - ) self.protocol = http2.HTTP2StateProtocol(self, dump_frames=self.http2_framedump) else: self.protocol = net_http.http1 diff --git a/pathod/pathoc_cmdline.py b/pathod/pathoc_cmdline.py index 3b738d47..0854f6ad 100644 --- a/pathod/pathoc_cmdline.py +++ b/pathod/pathoc_cmdline.py @@ -208,6 +208,7 @@ def args_pathoc(argv, stdout=sys.stdout, stderr=sys.stderr): reqs = [] for r in args.requests: + r = os.path.expanduser(r) if os.path.isfile(r): with open(r) as f: r = f.read() diff --git a/pathod/pathod_cmdline.py b/pathod/pathod_cmdline.py index dee19f4f..c646aaee 100644 --- a/pathod/pathod_cmdline.py +++ b/pathod/pathod_cmdline.py @@ -215,6 +215,7 @@ def args_pathod(argv, stdout_=sys.stdout, stderr_=sys.stderr): anchors = [] for patt, spec in args.anchors: + spec = os.path.expanduser(spec) if os.path.isfile(spec): with open(spec) as f: data = f.read() diff --git a/test/conftest.py b/test/conftest.py index bb913548..b0842bc3 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,15 +1,8 @@ import os import pytest -import OpenSSL - -import mitmproxy.net.tcp pytest_plugins = ('test.full_coverage_plugin',) -requires_alpn = pytest.mark.skipif( - not mitmproxy.net.tcp.HAS_ALPN, - reason='requires OpenSSL with ALPN support') - skip_windows = pytest.mark.skipif( os.name == "nt", reason='Skipping due to Windows' @@ -24,9 +17,3 @@ skip_appveyor = pytest.mark.skipif( "APPVEYOR" in os.environ, reason='Skipping due to Appveyor' ) - - -@pytest.fixture() -def disable_alpn(monkeypatch): - monkeypatch.setattr(mitmproxy.net.tcp, 'HAS_ALPN', False) - monkeypatch.setattr(OpenSSL.SSL._lib, 'Cryptography_HAS_ALPN', False) diff --git a/test/mitmproxy/addons/test_check_alpn.py b/test/mitmproxy/addons/test_check_alpn.py deleted file mode 100644 index 2b1d6058..00000000 --- a/test/mitmproxy/addons/test_check_alpn.py +++ /dev/null @@ -1,23 +0,0 @@ -from mitmproxy.addons import check_alpn -from mitmproxy.test import taddons -from ...conftest import requires_alpn - - -class TestCheckALPN: - - @requires_alpn - def test_check_alpn(self): - msg = 'ALPN support missing' - - with taddons.context() as tctx: - a = check_alpn.CheckALPN() - tctx.configure(a) - assert not tctx.master.has_log(msg) - - def test_check_no_alpn(self, disable_alpn): - msg = 'ALPN support missing' - - with taddons.context() as tctx: - a = check_alpn.CheckALPN() - tctx.configure(a) - assert tctx.master.has_log(msg) diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 40136f1f..e8eeb591 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -170,6 +170,10 @@ def test_load(tmpdir): assert len(v) == 2 v.load_file(path) assert len(v) == 4 + try: + v.load_file("nonexistent_file_path") + except IOError: + assert False def test_resolve(): diff --git a/test/mitmproxy/data/no_common_name.pem b/test/mitmproxy/data/no_common_name.pem index fc271a0e..d46448f5 100644 --- a/test/mitmproxy/data/no_common_name.pem +++ b/test/mitmproxy/data/no_common_name.pem @@ -1,20 +1,84 @@ -----BEGIN RSA PRIVATE KEY----- -MIIBOQIBAAJBAKVJ43C+8SjOvN9/pP/8HwzmHGQmRvdK/R6KlWdr7He6iiXDQNfH -RAp+gqX0hBRT80eRjGhSmTTBLCWiXVny4UUCAwEAAQJAUQ8nZ0d85VJd9g2XUaLH -Z4ACNGtBKk2wTKYSFyIqWZxsF5qhh7HGshJIAP6tYiX8ZW+mMSfme+zsJzWe8ChL -gQIhAM8QpAgUHnNteZvkv0XqceX1GILEWifMt+hO9yTp4dY5AiEAzFnKr77CKCri -/DPig4R/5q4KMpMx9EqJufHdGNmIA20CICMARxnufK86RCIr6oEg/hvG8Fu6YRr1 -Kekk3/XnavtRAiBVLVQ7vwKE5aNpRmMzOKZrS736aLpYvjz8IaFr+zgjXQIgdad5 -QZoTD49NTyMEgyZp70gTXcXQLrX2PgQKL4uNmoU= +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 -----END RSA PRIVATE KEY----- -----BEGIN CERTIFICATE----- -MIIBgTCCASugAwIBAgIJAKlcXsPLQAQuMA0GCSqGSIb3DQEBBQUAMA0xCzAJBgNV -BAYTAkFVMB4XDTEzMTIxMjAxMzA1NVoXDTE0MDExMTAxMzA1NVowDTELMAkGA1UE -BhMCQVUwXDANBgkqhkiG9w0BAQEFAANLADBIAkEApUnjcL7xKM6833+k//wfDOYc -ZCZG90r9HoqVZ2vsd7qKJcNA18dECn6CpfSEFFPzR5GMaFKZNMEsJaJdWfLhRQID -AQABo24wbDAdBgNVHQ4EFgQUJm8BXcVRsROy0PVt5stkB3eVnEgwPQYDVR0jBDYw -NIAUJm8BXcVRsROy0PVt5stkB3eVnEihEaQPMA0xCzAJBgNVBAYTAkFVggkAqVxe -w8tABC4wDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAANBAHHxcBEpWrIqtLVH -m6Yn1hgqrAbfMj9IK6zY9C5Cbad/DfUj3AZMb5u758WJK0x9brmckgqdrQsuf9He -Ef51/SU= +MIIFtTCCA52gAwIBAgIJAM/qkBYP5ExSMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTcwNzI1MDg0MDAwWhcNMTgwNzI1MDg0MDAwWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1TdGF0ZTEhMB8GA1UEChMYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEA4Jut+R51opC773ToeUhwJOVGnpxNqzZTDMImO141WPvKMjMsi15f0U3O +KqK8YERDfDzaAbgqz6MNgqc8QbNJ0e9VxtMUzTkCwSlbDHMFgZNyXVRXOQEBJ/fT +MlU+LimOH38QY0orifdAHH+kPUYIiqTBzgJvCy8w1o4hGSlzf2HW400dmlRSJEVg +Bj6nXQENbVxmxf6f9H19eWpnLuP5aJIwP4LEsGdqLP0ESnWfZVPIAiEsLuYkhbRX +qiuJfnc1am8LexzLi4VQMCw0K4Tm1lTbapcnOUakO6orQvX7MOEKYBU+ogGGby0M +yOTaCkuUFi0YdTtU4DEdWJ9HfogRO/uG1325/8/T+tq8RgOkp9cUvjU3ONqpLZC6 +zs0YR8OzlTbXClmV+Mr6d1qEb3jk6zWlLykLVozOS3z5vdVxpbJqM/CtHporAA0c +Ser2EGtN68OB3Hy3Bh7MPjqpSFSJ1uTQ755jS3qOzwggoQFCz2dBmfyi7nOGHFW4 +h/5NaF1mnIlJJVnJIZajSNl9e6klGeXmJv4ZtiqJd/CG0jTUnGWOTimukSmxVNcW +s2vFjwQhuRJwawzGo7O1gZPkq3/0/F+yLp5karmRJs8sQ/JDvGL4rW5YqW7u1WuY +QeHYHscfPwf0be8teKWcURIqBoHPxdJV3s9zf8Y/AN9OFFdPqwcCAwEAAaOBpzCB +pDAdBgNVHQ4EFgQUZrQUSE9A8i0N4ZuQZq/F4I74QlwwdQYDVR0jBG4wbIAUZrQU +SE9A8i0N4ZuQZq/F4I74QlyhSaRHMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpT +b21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGSCCQDP +6pAWD+RMUjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4ICAQDPeylCUY4k +nG1KoT139g5T5G1/lxgmYnDqQB1+5JYCQWsPK7sy19tD58bq3+2N2Tozu2/f/GkG +LZtouLyRciFtcAWy4LQlSR4hTLAWeik2WV/h+ovfv4XwvRuwS5PYVQNHQsOAO3ea +hX+W+w+rwI+MFlgEHJO85P61ijcNVbiTpgd0s47RViKyJVDqfhCmpobzS5eTbbXn +F1oFlV84lgEt84BE4RJxlr6fSIxZn6rQPjdbY65snol7Zs2oAt7nLb3hpZgWKobF +3xAfkC9m19nQHeYz3JlNC7sf80top2H2HEZMVVOAD+MxXkcAjNbjBRT3/KAIyWex +2fmoGRbvCIU0LFyyyk7/tG1xTgxNuBmd4Byz1LI25uz6eK4Ey8LeZmp5mvaewI53 +t65sAGBkx+LIRt0yGmMCRRFl735Ya4SJD7je1GTiw9I3Yd63dtaJTVd65kkFkLOk +LD56iJHSyCY6JkDXd8RjozIVoaXkVQh2wFq/ZfXzAgIx/u5cJQCMG2DAu6/WI74+ +7invOv7dbYfoI02N4iB57iRbPxE4gSrRayYxUVdH1R/tlXbN9Fkd30fl2WfSO897 +QC/ODA9w86FSFANhn6nv2KuKIMUSEW+5ZhBowSFIBEdAaMS7yj9uuBWmQKrWNfOh +mZJF1YiFmgRybkdKHPrlCSZyvVBdmnmM6g== -----END CERTIFICATE----- diff --git a/test/mitmproxy/net/test_tcp.py b/test/mitmproxy/net/test_tcp.py index 73de0879..3345840e 100644 --- a/test/mitmproxy/net/test_tcp.py +++ b/test/mitmproxy/net/test_tcp.py @@ -3,7 +3,6 @@ import queue import time import socket import random -import os import threading import pytest from unittest import mock @@ -15,7 +14,6 @@ from mitmproxy import exceptions from mitmproxy.test import tutils from . import tservers -from ...conftest import requires_alpn class EchoHandler(tcp.BaseHandler): @@ -534,36 +532,18 @@ class TestTimeOut(tservers.ServerTestBase): c.rfile.read(10) -class TestCryptographyALPN: - - def test_has_alpn(self): - if os.environ.get("OPENSSL") == "with-alpn": - assert tcp.HAS_ALPN - assert SSL._lib.Cryptography_HAS_ALPN - elif os.environ.get("OPENSSL") == "old": - assert not tcp.HAS_ALPN - assert not SSL._lib.Cryptography_HAS_ALPN - - class TestALPNClient(tservers.ServerTestBase): handler = ALPNHandler ssl = dict( alpn_select=b"bar" ) - @requires_alpn - @pytest.mark.parametrize('has_alpn,alpn_protos, expected_negotiated, expected_response', [ - (True, [b"foo", b"bar", b"fasel"], b'bar', b'bar'), - (True, [], b'', b'NONE'), - (True, None, b'', b'NONE'), - (False, [b"foo", b"bar", b"fasel"], b'', b'NONE'), - (False, [], b'', b'NONE'), - (False, None, b'', b'NONE'), + @pytest.mark.parametrize('alpn_protos, expected_negotiated, expected_response', [ + ([b"foo", b"bar", b"fasel"], b'bar', b'bar'), + ([], b'', b'NONE'), + (None, b'', b'NONE'), ]) - def test_alpn(self, monkeypatch, has_alpn, alpn_protos, expected_negotiated, expected_response): - monkeypatch.setattr(tcp, 'HAS_ALPN', has_alpn) - monkeypatch.setattr(SSL._lib, 'Cryptography_HAS_ALPN', has_alpn) - + def test_alpn(self, monkeypatch, alpn_protos, expected_negotiated, expected_response): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): c.convert_to_ssl(alpn_protos=alpn_protos) @@ -574,7 +554,7 @@ class TestALPNClient(tservers.ServerTestBase): class TestNoSSLNoALPNClient(tservers.ServerTestBase): handler = ALPNHandler - def test_no_ssl_no_alpn(self, disable_alpn): + def test_no_ssl_no_alpn(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): assert c.get_alpn_proto_negotiated() == b"" @@ -857,9 +837,8 @@ class TestSSLInvalid(tservers.ServerTestBase): def test_alpn_error(self): c = tcp.TCPClient(("127.0.0.1", self.port)) with c.connect(): - if tcp.HAS_ALPN: - with pytest.raises(exceptions.TlsException, match="must be a function"): - c.create_ssl_context(alpn_select_callback="foo") + with pytest.raises(exceptions.TlsException, match="must be a function"): + c.create_ssl_context(alpn_select_callback="foo") - with pytest.raises(exceptions.TlsException, match="ALPN error"): - c.create_ssl_context(alpn_select="foo", alpn_select_callback="bar") + with pytest.raises(exceptions.TlsException, match="ALPN error"): + c.create_ssl_context(alpn_select="foo", alpn_select_callback="bar") diff --git a/test/mitmproxy/proxy/protocol/test_http2.py b/test/mitmproxy/proxy/protocol/test_http2.py index 487d8890..583e6e27 100644 --- a/test/mitmproxy/proxy/protocol/test_http2.py +++ b/test/mitmproxy/proxy/protocol/test_http2.py @@ -17,7 +17,6 @@ from mitmproxy.net.http import http1, http2 from pathod.language import generators from ... import tservers -from ....conftest import requires_alpn import logging logging.getLogger("hyper.packages.hpack.hpack").setLevel(logging.WARNING) @@ -203,7 +202,6 @@ class _Http2Test(_Http2TestBase, _Http2ServerBase): _Http2ServerBase.teardown_class() -@requires_alpn class TestSimple(_Http2Test): request_body_buffer = b'' @@ -286,7 +284,6 @@ class TestSimple(_Http2Test): assert response_body_buffer == b'response body' -@requires_alpn class TestRequestWithPriority(_Http2Test): @classmethod @@ -368,7 +365,6 @@ class TestRequestWithPriority(_Http2Test): assert resp.headers.get('priority_weight', None) == expected_priority[2] -@requires_alpn class TestPriority(_Http2Test): @classmethod @@ -453,7 +449,6 @@ class TestPriority(_Http2Test): assert self.priority_data == expected_priority -@requires_alpn class TestStreamResetFromServer(_Http2Test): @classmethod @@ -504,7 +499,6 @@ class TestStreamResetFromServer(_Http2Test): assert self.master.state.flows[0].response is None -@requires_alpn class TestBodySizeLimit(_Http2Test): @classmethod @@ -554,7 +548,6 @@ class TestBodySizeLimit(_Http2Test): assert len(self.master.state.flows) == 0 -@requires_alpn class TestPushPromise(_Http2Test): @classmethod @@ -723,7 +716,6 @@ class TestPushPromise(_Http2Test): # the other two bodies might not be transmitted before the reset -@requires_alpn class TestConnectionLost(_Http2Test): @classmethod @@ -765,7 +757,6 @@ class TestConnectionLost(_Http2Test): assert self.master.state.flows[0].response is None -@requires_alpn class TestMaxConcurrentStreams(_Http2Test): @classmethod @@ -826,7 +817,6 @@ class TestMaxConcurrentStreams(_Http2Test): assert b"Stream-ID " in flow.response.content -@requires_alpn class TestConnectionTerminated(_Http2Test): @classmethod @@ -867,7 +857,6 @@ class TestConnectionTerminated(_Http2Test): assert connection_terminated_event.additional_data == b'foobar' -@requires_alpn class TestRequestStreaming(_Http2Test): @classmethod @@ -926,7 +915,6 @@ class TestRequestStreaming(_Http2Test): assert connection_terminated_event is None -@requires_alpn class TestResponseStreaming(_Http2Test): @classmethod diff --git a/test/mitmproxy/tools/console/conftest.py b/test/mitmproxy/tools/console/conftest.py new file mode 100644 index 00000000..afd94c6a --- /dev/null +++ b/test/mitmproxy/tools/console/conftest.py @@ -0,0 +1,9 @@ +from unittest import mock + +import pytest + + +@pytest.fixture(scope="module", autouse=True) +def definitely_atty(): + with mock.patch("sys.stdout.isatty", lambda: True): + yield diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py index 7732483f..a3478bdc 100644 --- a/test/mitmproxy/tools/console/test_master.py +++ b/test/mitmproxy/tools/console/test_master.py @@ -1,20 +1,12 @@ -import pytest +import urwid +from mitmproxy import options +from mitmproxy import proxy from mitmproxy.test import tflow from mitmproxy.test import tutils from mitmproxy.tools import console -from mitmproxy import proxy -from mitmproxy import options from mitmproxy.tools.console import common from ... import tservers -import urwid -from unittest import mock - - -@pytest.fixture(scope="module", autouse=True) -def definitely_atty(): - with mock.patch("sys.stdout.isatty", lambda: True): - yield def test_format_keyvals(): diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py new file mode 100644 index 00000000..55a3c4a0 --- /dev/null +++ b/test/mitmproxy/tools/console/test_statusbar.py @@ -0,0 +1,34 @@ +from mitmproxy import options, proxy +from mitmproxy.tools.console import statusbar, master + + +def test_statusbar(monkeypatch): + o = options.Options( + setheaders=[":~q:foo:bar"], + replacements=[":~q:foo:bar"], + ignore_hosts=["example.com", "example.org"], + tcp_hosts=["example.tcp"], + intercept="~q", + view_filter="~dst example.com", + stickycookie="~dst example.com", + stickyauth="~dst example.com", + default_contentview="javascript", + console_order="url", + anticache=True, + anticomp=True, + showhost=True, + refresh_server_playback=False, + replay_kill_extra=True, + upstream_cert=False, + console_focus_follow=True, + stream_large_bodies="3m", + mode="transparent", + scripts=["nonexistent"], + save_stream_file="foo", + ) + m = master.ConsoleMaster(o, proxy.DummyServer()) + monkeypatch.setattr(m.addons.get("clientplayback"), "count", lambda: 42) + monkeypatch.setattr(m.addons.get("serverplayback"), "count", lambda: 42) + + bar = statusbar.StatusBar(m) # this already causes a redraw + assert bar.ib._w diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index bb439b34..4d290284 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -263,6 +263,9 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): assert self.put_json("/options", {"wtf": True}).code == 400 assert self.put_json("/options", {"anticache": "foo"}).code == 400 + def test_option_save(self): + assert self.fetch("/options/save", method="POST").code == 200 + def test_err(self): with mock.patch("mitmproxy.tools.web.app.IndexHandler.get") as f: f.side_effect = RuntimeError @@ -279,7 +282,6 @@ class TestApp(tornado.testing.AsyncHTTPTestCase): r2 = yield ws_client.read_message() j1 = _json.loads(r1) j2 = _json.loads(r2) - print(j1) response = dict() response[j1['resource']] = j1 response[j2['resource']] = j2 diff --git a/test/mitmproxy/utils/test_version_check.py b/test/mitmproxy/utils/test_version_check.py deleted file mode 100644 index d7929378..00000000 --- a/test/mitmproxy/utils/test_version_check.py +++ /dev/null @@ -1,25 +0,0 @@ -import io -from unittest import mock -from mitmproxy.utils import version_check - - -@mock.patch("sys.exit") -def test_check_pyopenssl_version(sexit): - fp = io.StringIO() - version_check.check_pyopenssl_version(fp=fp) - assert not fp.getvalue() - assert not sexit.called - - version_check.check_pyopenssl_version((9999,), fp=fp) - assert "outdated" in fp.getvalue() - assert sexit.called - - -@mock.patch("sys.exit") -@mock.patch("OpenSSL.__version__") -def test_unparseable_pyopenssl_version(version, sexit): - version.split.return_value = ["foo", "bar"] - fp = io.StringIO() - version_check.check_pyopenssl_version(fp=fp) - assert "Cannot parse" in fp.getvalue() - assert not sexit.called diff --git a/test/pathod/protocols/test_http2.py b/test/pathod/protocols/test_http2.py index c16a6d40..b1eebc73 100644 --- a/test/pathod/protocols/test_http2.py +++ b/test/pathod/protocols/test_http2.py @@ -11,8 +11,6 @@ from ...mitmproxy.net import tservers as net_tservers from pathod.protocols.http2 import HTTP2StateProtocol, TCPHandler -from ...conftest import requires_alpn - class TestTCPHandlerWrapper: def test_wrapped(self): @@ -68,7 +66,6 @@ class TestProtocol: assert mock_server_method.called -@requires_alpn class TestCheckALPNMatch(net_tservers.ServerTestBase): handler = EchoHandler ssl = dict( @@ -83,7 +80,6 @@ class TestCheckALPNMatch(net_tservers.ServerTestBase): assert protocol.check_alpn() -@requires_alpn class TestCheckALPNMismatch(net_tservers.ServerTestBase): handler = EchoHandler ssl = dict( diff --git a/test/pathod/test_pathoc.py b/test/pathod/test_pathoc.py index 2dd29e20..4b50e2a7 100644 --- a/test/pathod/test_pathoc.py +++ b/test/pathod/test_pathoc.py @@ -11,7 +11,6 @@ from pathod.protocols.http2 import HTTP2StateProtocol from mitmproxy.test import tutils from . import tservers -from ..conftest import requires_alpn def test_response(): @@ -216,7 +215,6 @@ class TestDaemonHTTP2(PathocTestDaemon): ssl = True explain = False - @requires_alpn def test_http2(self): c = pathoc.Pathoc( ("127.0.0.1", self.d.port), @@ -231,7 +229,6 @@ class TestDaemonHTTP2(PathocTestDaemon): ) assert c.protocol == http1 - @requires_alpn def test_http2_alpn(self): c = pathoc.Pathoc( ("127.0.0.1", self.d.port), @@ -248,7 +245,6 @@ class TestDaemonHTTP2(PathocTestDaemon): _, kwargs = c.convert_to_ssl.call_args assert set(kwargs['alpn_protos']) == set([b'http/1.1', b'h2']) - @requires_alpn def test_request(self): c = pathoc.Pathoc( ("127.0.0.1", self.d.port), @@ -259,14 +255,3 @@ class TestDaemonHTTP2(PathocTestDaemon): with c.connect(): resp = c.request("get:/p/200") assert resp.status_code == 200 - - def test_failing_request(self, disable_alpn): - c = pathoc.Pathoc( - ("127.0.0.1", self.d.port), - fp=None, - ssl=True, - use_http2=True, - ) - with pytest.raises(NotImplementedError): - with c.connect(): - c.request("get:/p/200") diff --git a/test/pathod/test_pathod.py b/test/pathod/test_pathod.py index 88480a59..5f191c0d 100644 --- a/test/pathod/test_pathod.py +++ b/test/pathod/test_pathod.py @@ -8,7 +8,6 @@ from mitmproxy import exceptions from mitmproxy.test import tutils from . import tservers -from ..conftest import requires_alpn class TestPathod: @@ -257,11 +256,6 @@ class TestHTTP2(tservers.DaemonTests): ssl = True nohang = True - @requires_alpn def test_http2(self): r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True) assert r[0].status_code == 800 - - def test_no_http2(self, disable_alpn): - with pytest.raises(NotImplementedError): - r, _ = self.pathoc(["GET:/"], ssl=True, use_http2=True) diff --git a/web/src/js/__tests__/components/Modal/OptionModalSpec.js b/web/src/js/__tests__/components/Modal/OptionModalSpec.js new file mode 100644 index 00000000..dd4e70a2 --- /dev/null +++ b/web/src/js/__tests__/components/Modal/OptionModalSpec.js @@ -0,0 +1,54 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import { PureOptionDefault } from '../../../components/Modal/OptionModal' + +describe('PureOptionDefault Component', () => { + + it('should return null when the value is default', () => { + let pureOptionDefault = renderer.create( + <PureOptionDefault value="foo" defaultVal="foo"/> + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle boolean type', () => { + let pureOptionDefault = renderer.create( + <PureOptionDefault value={true} defaultVal={false}/> + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle array', () => { + let a = [""], b = [], c = ['c'], + pureOptionDefault = renderer.create( + <PureOptionDefault value={a} defaultVal={b}/> + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + + pureOptionDefault = renderer.create( + <PureOptionDefault value={a} defaultVal={c}/> + ) + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle string', () => { + let pureOptionDefault = renderer.create( + <PureOptionDefault value="foo" defaultVal=""/> + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should handle null value', () => { + let pureOptionDefault = renderer.create( + <PureOptionDefault value="foo" defaultVal={null}/> + ), + tree = pureOptionDefault.toJSON() + expect(tree).toMatchSnapshot() + }) + +}) diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap index bfd855bd..8d9271f1 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/ModalSpec.js.snap @@ -117,7 +117,7 @@ exports[`Modal Component should render correctly 2`] = ` name="choiceOption" onChange={[Function]} onKeyDown={[Function]} - selected="b" + value="b" > <option value="a" @@ -136,6 +136,17 @@ exports[`Modal Component should render correctly 2`] = ` </option> </select> </div> + <div + className="small" + > + Default: + <strong> + + a + + </strong> + + </div> </div> </div> <div @@ -170,6 +181,17 @@ exports[`Modal Component should render correctly 2`] = ` value={1} /> </div> + <div + className="small" + > + Default: + <strong> + + 0 + + </strong> + + </div> </div> </div> <div @@ -207,6 +229,17 @@ exports[`Modal Component should render correctly 2`] = ` <div className="small text-danger" /> + <div + className="small" + > + Default: + <strong> + + null + + </strong> + + </div> </div> </div> </div> diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap new file mode 100644 index 00000000..68f1c9fc --- /dev/null +++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionModalSpec.js.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PureOptionDefault Component should handle array 1`] = `null`; + +exports[`PureOptionDefault Component should handle array 2`] = ` +<div + className="small" +> + Default: + <strong> + + [ ] + + </strong> + +</div> +`; + +exports[`PureOptionDefault Component should handle boolean type 1`] = ` +<div + className="small" +> + Default: + <strong> + + false + + </strong> + +</div> +`; + +exports[`PureOptionDefault Component should handle null value 1`] = ` +<div + className="small" +> + Default: + <strong> + + null + + </strong> + +</div> +`; + +exports[`PureOptionDefault Component should handle string 1`] = ` +<div + className="small" +> + Default: + <strong> + + "" + + </strong> + +</div> +`; + +exports[`PureOptionDefault Component should return null when the value is default 1`] = `null`; diff --git a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap index 514e0eb5..257bddce 100644 --- a/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap +++ b/web/src/js/__tests__/components/Modal/__snapshots__/OptionSpec.js.snap @@ -18,7 +18,7 @@ exports[`BooleanOption Component should render correctly 1`] = ` exports[`ChoiceOption Component should render correctly 1`] = ` <select onChange={[Function]} - selected="a" + value="a" > <option value="a" diff --git a/web/src/js/__tests__/ducks/optionsSpec.js b/web/src/js/__tests__/ducks/optionsSpec.js index 0925fcc1..9178c14e 100644 --- a/web/src/js/__tests__/ducks/optionsSpec.js +++ b/web/src/js/__tests__/ducks/optionsSpec.js @@ -49,3 +49,18 @@ describe('sendUpdate', () => { ]) }) }) + +describe('save', () => { + + it('should dump options', () => { + global.fetch = jest.fn() + store.dispatch(OptionsActions.save()) + expect(fetch).toBeCalledWith( + '/options/save?_xsrf=undefined', + { + credentials: "same-origin", + method: "POST" + } + ) + }) +}) diff --git a/web/src/js/components/Modal/Option.jsx b/web/src/js/components/Modal/Option.jsx index 58b863d1..38e2f239 100644 --- a/web/src/js/components/Modal/Option.jsx +++ b/web/src/js/components/Modal/Option.jsx @@ -74,7 +74,7 @@ export function ChoicesOption({ value, onChange, choices, ...props }) { return ( <select onChange={(e) => onChange(e.target.value)} - selected={value} + value={value} {...props} > { choices.map( diff --git a/web/src/js/components/Modal/OptionModal.jsx b/web/src/js/components/Modal/OptionModal.jsx index 5741ee8c..82ef8350 100644 --- a/web/src/js/components/Modal/OptionModal.jsx +++ b/web/src/js/components/Modal/OptionModal.jsx @@ -1,7 +1,9 @@ import React, { Component } from "react" import { connect } from "react-redux" import * as modalAction from "../../ducks/ui/modal" +import * as optionAction from "../../ducks/options" import Option from "./Option" +import _ from "lodash" function PureOptionHelp({help}){ return <div className="help-block small">{help}</div>; @@ -18,6 +20,31 @@ const OptionError = connect((state, {name}) => ({ error: state.ui.optionsEditor[name] && state.ui.optionsEditor[name].error }))(PureOptionError); +export function PureOptionDefault({value, defaultVal}){ + if( value === defaultVal ) { + return null + } else { + if (typeof(defaultVal) === 'boolean') { + defaultVal = defaultVal ? 'true' : 'false' + } else if (Array.isArray(defaultVal)){ + if (_.isEmpty(_.compact(value)) && // filter the empty string in array + _.isEmpty(defaultVal)){ + return null + } + defaultVal = '[ ]' + } else if (defaultVal === ''){ + defaultVal = '\"\"' + } else if (defaultVal === null){ + defaultVal = 'null' + } + return <div className="small">Default: <strong> {defaultVal} </strong> </div> + } +} +const OptionDefault = connect((state, {name}) => ({ + value: state.options[name].value, + defaultVal: state.options[name].default +}))(PureOptionDefault) + class PureOptionModal extends Component { constructor(props, context) { @@ -25,6 +52,10 @@ class PureOptionModal extends Component { this.state = { title: 'Options' } } + componentWillUnmount(){ + this.props.save() + } + render() { const { hideModal, options } = this.props const { title } = this.state @@ -53,6 +84,7 @@ class PureOptionModal extends Component { <div className="col-xs-6"> <Option name={name}/> <OptionError name={name}/> + <OptionDefault name={name}/> </div> </div> ) @@ -73,5 +105,6 @@ export default connect( }), { hideModal: modalAction.hideModal, + save: optionAction.save, } )(PureOptionModal) diff --git a/web/src/js/ducks/options.js b/web/src/js/ducks/options.js index 06144a3c..0da0fb8c 100644 --- a/web/src/js/ducks/options.js +++ b/web/src/js/ducks/options.js @@ -44,3 +44,7 @@ export function update(option, value) { sendUpdate(option, value, dispatch); } } + +export function save() { + return dispatch => fetchApi('/options/save', { method: 'POST' }) +} |