diff options
-rw-r--r-- | .travis.yml | 6 | ||||
-rw-r--r-- | README.mkd | 8 | ||||
-rw-r--r-- | doc-src/features/passthrough.html | 7 | ||||
-rw-r--r-- | libmproxy/flow.py | 16 | ||||
-rw-r--r-- | libmproxy/protocol/http.py | 38 | ||||
-rw-r--r-- | libmproxy/script.py | 16 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | test/test_flow.py | 4 |
8 files changed, 67 insertions, 35 deletions
diff --git a/.travis.yml b/.travis.yml index ae4a9641..3dcc7e2f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,10 @@ language: python sudo: false python: - "2.7" -# - pypy # pypy 2.5.0 has a regression bug which breaks our test suite. + - pypy # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors install: - - "pip install --upgrade --src . -r requirements.txt" + - "pip install --src . -r requirements.txt" # command to run tests, e.g. python setup.py test script: - "nosetests --with-cov --cov-report term-missing" @@ -20,4 +20,4 @@ notifications: cache: directories: - /home/travis/virtualenv/python2.7.9/lib/python2.7/site-packages - - /home/travis/virtualenv/pypy-2.4.0/site-packages + - /home/travis/virtualenv/pypy-2.5.0/site-packages @@ -1,4 +1,8 @@ -[![Build Status](https://travis-ci.org/mitmproxy/mitmproxy.png?branch=master)](https://travis-ci.org/mitmproxy/mitmproxy) [![Coverage Status](https://coveralls.io/repos/mitmproxy/mitmproxy/badge.png?branch=master)](https://coveralls.io/r/mitmproxy/mitmproxy) +[![Build Status](https://travis-ci.org/mitmproxy/mitmproxy.svg?branch=master)](https://travis-ci.org/mitmproxy/mitmproxy) [![Coverage Status](https://coveralls.io/repos/mitmproxy/mitmproxy/badge.svg?branch=master)](https://coveralls.io/r/mitmproxy/mitmproxy) +[![Latest Version](https://pypip.in/version/mitmproxy/badge.svg?style=flat)](https://pypi.python.org/pypi/mitmproxy/) +[![Supported Python versions](https://pypip.in/py_versions/mitmproxy/badge.svg?style=flat)](https://pypi.python.org/pypi/mitmproxy) +[![Supported Python implementations](https://pypip.in/implementation/mitmproxy/badge.svg?style=flat)](https://pypi.python.org/pypi/mitmproxy/) + __mitmproxy__ is an interactive, SSL-capable man-in-the-middle proxy for HTTP with a console interface. @@ -70,7 +74,7 @@ This installs the latest GitHub versions of mitmproxy, netlib and pathod into `m ### Testing -The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py) and [pathod](http://pathod.net), version matching mitmproxy. Install these with: +The test suite requires the `dev` extra requirements listed in [setup.py](https://github.com/mitmproxy/mitmproxy/blob/master/setup.py). Install these with: `pip install "mitmproxy[dev]"` diff --git a/doc-src/features/passthrough.html b/doc-src/features/passthrough.html index 7c830639..15e36434 100644 --- a/doc-src/features/passthrough.html +++ b/doc-src/features/passthrough.html @@ -59,13 +59,16 @@ $ <span style="color: white">mitmproxy --ignore ^example\.com:443$</span> Here are some other examples for ignore patterns: <pre> -# Exempt traffic from the iOS App Store (usually just works): +# Exempt traffic from the iOS App Store (the regex is lax, but usually just works): --ignore apple.com:443 # "Correct" version without false-positives: --ignore ^(.+\.)?apple\.com:443$ -# Ignore example.com on all ports, but no subdomains: +# Ignore example.com, but not its subdomains: --ignore ^example.com: + +# Ignore everything but example.com and mitmproxy.org: +--ignore ^(?!example\.com)(?!mitmproxy\.org) # Transparent mode: --ignore 17\.178\.96\.59:443 diff --git a/libmproxy/flow.py b/libmproxy/flow.py index f9e2b94d..49ec5a0b 100644 --- a/libmproxy/flow.py +++ b/libmproxy/flow.py @@ -309,10 +309,10 @@ class StickyCookieState: # FIXME: We now know that Cookie.py screws up some cookies with # valid RFC 822/1123 datetime specifications for expiry. Sigh. c = Cookie.SimpleCookie(str(i)) - m = c.values()[0] - k = self.ckey(m, f) - if self.domain_match(f.request.host, k[0]): - self.jar[self.ckey(m, f)] = m + for m in c.values(): + k = self.ckey(m, f) + if self.domain_match(f.request.host, k[0]): + self.jar[k] = m def handle_request(self, f): l = [] @@ -824,12 +824,12 @@ class FlowMaster(controller.Master): if self.stickycookie_state: self.stickycookie_state.handle_response(f) - def replay_request(self, f, block=False): + def replay_request(self, f, block=False, run_scripthooks=True): """ Returns None if successful, or error message if not. """ - if f.live: - return "Can't replay request which is still live..." + if f.live and run_scripthooks: + return "Can't replay live request." if f.intercepted: return "Can't replay while intercepting..." if f.request.content == http.CONTENT_MISSING: @@ -845,7 +845,7 @@ class FlowMaster(controller.Master): rt = http.RequestReplayThread( self.server.config, f, - self.masterq, + self.masterq if run_scripthooks else False, self.should_exit ) rt.start() # pragma: no cover diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py index bebb4f7b..2f858a7c 100644 --- a/libmproxy/protocol/http.py +++ b/libmproxy/protocol/http.py @@ -1416,20 +1416,31 @@ class RequestReplayThread(threading.Thread): name = "RequestReplayThread" def __init__(self, config, flow, masterq, should_exit): - self.config, self.flow, self.channel = config, flow, controller.Channel(masterq, should_exit) - threading.Thread.__init__(self) + """ + masterqueue can be a queue or None, if no scripthooks should be processed. + """ + self.config, self.flow = config, flow + if masterq: + self.channel = controller.Channel(masterq, should_exit) + else: + self.channel = None + super(RequestReplayThread, self).__init__() def run(self): r = self.flow.request form_out_backup = r.form_out try: self.flow.response = None - request_reply = self.channel.ask("request", self.flow) - if request_reply is None or request_reply == KILL: - raise KillSignal() - elif isinstance(request_reply, HTTPResponse): - self.flow.response = request_reply - else: + + # If we have a channel, run script hooks. + if self.channel: + request_reply = self.channel.ask("request", self.flow) + if request_reply is None or request_reply == KILL: + raise KillSignal() + elif isinstance(request_reply, HTTPResponse): + self.flow.response = request_reply + + if not self.flow.response: # In all modes, we directly connect to the server displayed if self.config.mode == "upstream": server_address = self.config.mode.get_upstream_server(self.flow.client_conn)[2:] @@ -1453,13 +1464,16 @@ class RequestReplayThread(threading.Thread): self.flow.server_conn = server self.flow.response = HTTPResponse.from_stream(server.rfile, r.method, body_size_limit=self.config.body_size_limit) - response_reply = self.channel.ask("response", self.flow) - if response_reply is None or response_reply == KILL: - raise KillSignal() + if self.channel: + response_reply = self.channel.ask("response", self.flow) + if response_reply is None or response_reply == KILL: + raise KillSignal() except (proxy.ProxyError, http.HttpError, tcp.NetLibError) as v: self.flow.error = Error(repr(v)) - self.channel.ask("error", self.flow) + if self.channel: + self.channel.ask("error", self.flow) except KillSignal: + # KillSignal should only be raised if there's a channel in the first place. self.channel.tell("log", proxy.Log("Connection killed", "info")) finally: r.form_out = form_out_backup diff --git a/libmproxy/script.py b/libmproxy/script.py index b559615b..be226004 100644 --- a/libmproxy/script.py +++ b/libmproxy/script.py @@ -20,6 +20,12 @@ class ScriptContext: """ self._master.add_event(message, level) + def kill_flow(self, f): + """ + Kills a flow immediately. No further data will be sent to the client or the server. + """ + f.kill(self._master) + def duplicate_flow(self, f): """ Returns a duplicate of the specified flow. The flow is also @@ -36,7 +42,7 @@ class ScriptContext: Replay the request on the current flow. The response will be added to the flow object. """ - self._master.replay_request(f) + return self._master.replay_request(f, block=True, run_scripthooks=False) @property def app_registry(self): @@ -139,8 +145,12 @@ def _handle_concurrent_reply(fn, o, *args, **kwargs): def run(): fn(*args, **kwargs) - o.reply() # If the script did not call .reply(), we have to do it now. - threading.Thread(target=run, name="ScriptThread").start() + reply_proxy() # If the script did not call .reply(), we have to do it now. + ScriptThread(target=run).start() + + +class ScriptThread(threading.Thread): + name = "ScriptThread" def concurrent(fn): @@ -34,8 +34,7 @@ script_deps = { for script in scripts: deps.update(script_deps[script]) if os.name == "nt": - deps.add("pydivert>=0.0.4") # Transparent proxying on Windows - + deps.add("pydivert>=0.0.7") # Transparent proxying on Windows setup( name="mitmproxy", @@ -56,6 +55,8 @@ setup( "Programming Language :: Python", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Security", "Topic :: Internet", "Topic :: Internet :: WWW/HTTP", @@ -64,7 +65,7 @@ setup( ], packages=find_packages(), include_package_data=True, - scripts = scripts, + scripts=scripts, install_requires=list(deps), extras_require={ 'dev': [ diff --git a/test/test_flow.py b/test/test_flow.py index 6230ad73..6d77f075 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -47,7 +47,7 @@ class TestStickyCookieState: assert s.domain_match("google.com", ".google.com") def test_handle_response(self): - c = "SSID=mooo, FOO=bar; Domain=.google.com; Path=/; "\ + c = "SSID=mooo; domain=.google.com, FOO=bar; Domain=.google.com; Path=/; "\ "Expires=Wed, 13-Jan-2021 22:23:01 GMT; Secure; " s, f = self._response(c, "host") @@ -667,7 +667,7 @@ class TestFlowMaster: assert "intercepting" in fm.replay_request(f) f.live = True - assert "live" in fm.replay_request(f) + assert "live" in fm.replay_request(f, run_scripthooks=True) def test_script_reqerr(self): s = flow.State() |