aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMarcelo Glezer <mg@tekii.com.ar>2014-12-11 14:54:14 -0300
committerMarcelo Glezer <mg@tekii.com.ar>2014-12-11 14:54:14 -0300
commit4952643a0d76eb1e9bd51cbbe95c565ae48b97a2 (patch)
treef43fc647bdfabb522bdef32e21ea4a36404cc311
parent83b1d4e0e0490e5be05943da459c925a3ee3ff14 (diff)
parentffb95a1db742d71d7671f9e9c6db552774bb0ead (diff)
downloadmitmproxy-4952643a0d76eb1e9bd51cbbe95c565ae48b97a2.tar.gz
mitmproxy-4952643a0d76eb1e9bd51cbbe95c565ae48b97a2.tar.bz2
mitmproxy-4952643a0d76eb1e9bd51cbbe95c565ae48b97a2.zip
Merge remote-tracking branch 'base/master'
-rw-r--r--.coveragerc2
-rw-r--r--CHANGELOG50
-rw-r--r--CONTRIBUTORS68
-rw-r--r--MANIFEST.in5
-rw-r--r--README.mkd44
-rw-r--r--doc-src/_nav.html8
-rw-r--r--doc-src/features/index.py2
-rw-r--r--doc-src/features/passthrough.html12
-rw-r--r--doc-src/features/responsestreaming.html6
-rw-r--r--doc-src/features/reverseproxy.html44
-rw-r--r--doc-src/features/socksproxy.html10
-rw-r--r--doc-src/features/tcpproxy.html30
-rw-r--r--doc-src/features/upstreamproxy.html17
-rw-r--r--doc-src/index.html25
-rw-r--r--doc-src/index.py13
-rw-r--r--doc-src/install.html30
-rw-r--r--doc-src/modes.html298
-rw-r--r--doc-src/schematics/proxy-modes-transparent-1.pngbin16305 -> 14558 bytes
-rw-r--r--doc-src/schematics/proxy-modes-transparent-2.pngbin23041 -> 23375 bytes
-rw-r--r--doc-src/schematics/proxy-modes-transparent-wrong.pngbin17568 -> 14719 bytes
-rw-r--r--doc-src/schematics/proxy-modes.vsdxbin195186 -> 190788 bytes
-rw-r--r--doc-src/ssl.html31
-rw-r--r--examples/change_upstream_proxy.py4
-rwxr-xr-xexamples/flowbasic2
-rw-r--r--examples/har_extractor.py212
-rw-r--r--examples/ignore_websocket.py34
-rwxr-xr-xexamples/stickycookies5
-rw-r--r--libmproxy/cmdline.py316
-rw-r--r--libmproxy/console/__init__.py37
-rw-r--r--libmproxy/console/flowlist.py6
-rw-r--r--libmproxy/console/flowview.py5
-rw-r--r--libmproxy/console/grideditor.py13
-rw-r--r--libmproxy/console/help.py1
-rw-r--r--libmproxy/dump.py15
-rw-r--r--libmproxy/filt.py4
-rw-r--r--libmproxy/flow.py93
-rw-r--r--libmproxy/main.py165
-rw-r--r--libmproxy/onboarding/app.py4
-rw-r--r--libmproxy/onboarding/templates/index.html11
-rw-r--r--libmproxy/platform/windows.py4
-rw-r--r--libmproxy/protocol/http.py425
-rw-r--r--libmproxy/protocol/primitives.py19
-rw-r--r--libmproxy/protocol/tcp.py35
-rw-r--r--libmproxy/proxy/config.py91
-rw-r--r--libmproxy/proxy/primitives.py81
-rw-r--r--libmproxy/proxy/server.py42
-rw-r--r--libmproxy/version.py6
-rw-r--r--libmproxy/web/static/flows.json28
-rwxr-xr-xrelease/osx-binaries3
-rw-r--r--release/release-checklist21
-rwxr-xr-xrelease/test-release15
-rw-r--r--requirements.txt2
-rw-r--r--setup.py27
-rw-r--r--test/fuzzing/.env6
-rw-r--r--test/fuzzing/README14
-rw-r--r--test/fuzzing/client_patterns4
-rwxr-xr-xtest/fuzzing/go_proxy15
-rw-r--r--test/fuzzing/reverse_patterns9
-rw-r--r--test/fuzzing/straight_stream6
-rw-r--r--test/fuzzing/straight_stream_patterns17
-rw-r--r--test/fuzzing/straight_stream_ssl6
-rw-r--r--test/test_cmdline.py18
-rw-r--r--test/test_dump.py16
-rw-r--r--test/test_examples.py7
-rw-r--r--test/test_flow.py36
-rw-r--r--test/test_protocol_http.py45
-rw-r--r--test/test_proxy.py12
-rw-r--r--test/test_server.py101
-rw-r--r--test/tools/passive_close.py21
-rw-r--r--test/tservers.py6
70 files changed, 1979 insertions, 781 deletions
diff --git a/.coveragerc b/.coveragerc
index 7a4e3ab7..70ff48e7 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -2,5 +2,5 @@
branch = True
[report]
-omit = *contrib*, *tnetstring*, *platform*, *console*
+omit = *contrib*, *tnetstring*, *platform*, *console*, *main.py
include = *libmproxy*
diff --git a/CHANGELOG b/CHANGELOG
index 69e7339b..86c41b13 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,53 @@
+
+15 November 2014: mitmproxy 0.11.1:
+
+ * Bug fixes: connection leaks some crashes
+
+
+7 November 2014: mitmproxy 0.11:
+
+ * Performance improvements for mitmproxy console
+
+ * SOCKS5 proxy mode allows mitmproxy to act as a SOCKS5 proxy server
+
+ * Data streaming for response bodies exceeding a threshold
+ (bradpeabody@gmail.com)
+
+ * Ignore hosts or IP addresses, forwarding both HTTP and HTTPS traffic
+ untouched
+
+ * Finer-grained control of traffic replay, including options to ignore
+ contents or parameters when matching flows (marcelo.glezer@gmail.com)
+
+ * Pass arguments to inline scripts
+
+ * Configurable size limit on HTTP request and response bodies
+
+ * Per-domain specification of interception certificates and keys (see
+ --cert option)
+
+ * Certificate forwarding, relaying upstream SSL certificates verbatim (see
+ --cert-forward)
+
+ * Search and highlighting for HTTP request and response bodies in
+ mitmproxy console (pedro@worcel.com)
+
+ * Transparent proxy support on Windows
+
+ * Improved error messages and logging
+
+ * Support for FreeBSD in transparent mode, using pf (zbrdge@gmail.com)
+
+ * Content view mode for WBXML (davidshaw835@air-watch.com)
+
+ * Better documentation, with a new section on proxy modes
+
+ * Generic TCP proxy mode
+
+ * Countless bugfixes and other small improvements
+
+
+
28 January 2014: mitmproxy 0.10:
* Support for multiple scripts and multiple script arguments
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index bed636fa..a9688d92 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -1,51 +1,65 @@
- 854 Aldo Cortesi
- 64 Maximilian Hils
+ 902 Aldo Cortesi
+ 323 Maximilian Hils
18 Henrik Nordstrom
13 Thomas Roth
+ 12 Pedro Worcel
11 Stephen Altamirano
10 András Veres-Szentkirályi
- 8 Jason A. Novak
8 Rouli
+ 8 Jason A. Novak
7 Alexis Hildebrandt
- 6 Pedro Worcel
5 Tomaz Muraus
+ 5 Brad Peabody
5 Matthias Urlichs
4 root
- 4 Bryan Bishop
4 Marc Liyanage
4 Valtteri Virtanen
- 3 Kyle Manna
+ 4 Bryan Bishop
3 Chris Neasbitt
- 2 alts
- 2 Heikki Hannikainen
- 2 Jim Lloyd
+ 3 Zack B
+ 3 Eli Shvartsman
+ 3 Kyle Manna
2 Michael Frister
+ 2 Bennett Blodinger
+ 2 Jim Lloyd
2 Rob Wills
- 2 Jaime Soriano Pastor
2 israel
+ 2 Jaime Soriano Pastor
+ 2 Heikki Hannikainen
2 Mark E. Haase
+ 2 alts
+ 1 davidpshaw
+ 1 deployable
+ 1 joebowbeer
+ 1 meeee
+ 1 phil plante
+ 1 Michael Bisbjerg
+ 1 Andy Smith
+ 1 Dan Wilbraham
+ 1 David Shaw
+ 1 Eric Entzel
+ 1 Felix Wolfsteller
+ 1 Henrik Nordström
+ 1 Ivaylo Popov
+ 1 JC
+ 1 Jakub Nawalaniec
+ 1 James Billingham
+ 1 Jean Regisser
+ 1 Kit Randel
+ 1 Marcelo Glezer
+ 1 Mathieu Mitchell
+ 1 Mikhail Korobov
+ 1 Nicolas Esteves
+ 1 Oleksandr Sheremet
1 Paul
1 Rich Somerfield
1 Rory McCann
- 1 Felix Wolfsteller
1 Rune Halvorsen
1 Sahn Lam
- 1 Eric Entzel
- 1 Dan Wilbraham
+ 1 Seppo Yli-Olli
+ 1 Sergey Chipiga
+ 1 Steven Van Acker
1 Ulrich Petri
- 1 Andy Smith
+ 1 Vyacheslav Bakhmutov
1 Yuangxuan Wang
1 capt8bit
- 1 joebowbeer
- 1 meeee
- 1 James Billingham
- 1 Jakub Nawalaniec
- 1 JC
- 1 Kit Randel
- 1 phil plante
- 1 Mathieu Mitchell
- 1 Ivaylo Popov
- 1 Henrik Nordström
- 1 Michael Bisbjerg
- 1 Nicolas Esteves
- 1 Oleksandr Sheremet
diff --git a/MANIFEST.in b/MANIFEST.in
index efe18f43..cc048b61 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,10 +2,7 @@ include mitmproxy mitmdump
include LICENSE CHANGELOG CONTRIBUTORS README.txt
exclude README.mkd
recursive-include examples *
-recursive-exclude examples *.pyc *.pyo *.swo *.swp
recursive-include doc *
-recursive-exclude doc *.pyc *.pyo *.swo *.swp
recursive-include test *
-recursive-exclude test *.pyc *.pyo *.swo *.swp
recursive-include libmproxy *
-recursive-exclude libmproxy *.pyc *.pyo *.swo *.swp \ No newline at end of file
+recursive-exclude * *.pyc *.pyo *.swo *.swp \ No newline at end of file
diff --git a/README.mkd b/README.mkd
index 49582612..bc18cb48 100644
--- a/README.mkd
+++ b/README.mkd
@@ -13,6 +13,9 @@ mitmproxy.org website:
[mitmproxy.org](http://mitmproxy.org).
+You can find complete directions for installing mitmproxy [here](http://mitmproxy.org/doc/install.html).
+
+
Features
--------
@@ -26,17 +29,17 @@ Features
- SSL certificates for interception are generated on the fly.
- And much, much more.
+__mitmproxy__ is tested and developed on OSX, Linux and OpenBSD. On Windows,
+only mitmdump is supported, which does not have a graphical user interface.
-Installation
-------------
-The recommended way to install mitmproxy is running <code>pip install mitmproxy</code>.
-For convenience, we provide binary packages on [mitmproxy.org](http://mitmproxy.org/).
+Hacking
+-------
-Requirements
-------------
+### Requirements
+
* [Python](http://www.python.org) 2.7.x.
* [netlib](http://pypi.python.org/pypi/netlib), version matching mitmproxy.
@@ -49,28 +52,35 @@ Optional packages for extended content decoding:
* [cssutils](http://cthedot.de/cssutils/) version 1.0 or newer.
For convenience, all optional dependencies can be installed with
-`pip install mitmproxy[contenviews]`
-
-__mitmproxy__ is tested and developed on OSX, Linux and OpenBSD. On Windows,
-only mitmdump is supported, which does not have a graphical user interface.
-
-Hacking
--------
+`pip install "mitmproxy[contentviews]"`
-The following components are needed if you plan to hack on mitmproxy:
+### Setting up a dev environment
-* 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.
-* Rendering the documentation requires [countershape](http://github.com/cortesi/countershape).
+The following procedure is recommended to set up your dev environment:
-For convenience, the following procedure is recommended to set up your environment:
```
$ git clone https://github.com/mitmproxy/mitmproxy.git
$ cd mitmproxy
$ pip install --src . -r requirements.txt
```
+
This installs the latest GitHub versions of mitmproxy, netlib and pathod into `mitmproxy/`. All other development dependencies save countershape are installed into their usual locations.
+
+### 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:
+
+`pip install "mitmproxy[dev]"`
+
+
Please ensure that all patches are accompanied by matching changes in the test
suite. The project maintains 100% test coverage.
+
+### Docs
+
+Rendering the documentation requires [countershape](http://github.com/cortesi/countershape). After installation, you can render the documentation to the doc like this:
+
+`cshape doc-src doc`
diff --git a/doc-src/_nav.html b/doc-src/_nav.html
index 822e9fa6..0ae0fa67 100644
--- a/doc-src/_nav.html
+++ b/doc-src/_nav.html
@@ -17,12 +17,14 @@
$!nav("serverreplay.html", this, state)!$
$!nav("setheaders.html", this, state)!$
$!nav("passthrough.html", this, state)!$
- $!nav("sticky.html", this, state)!$
+ $!nav("proxyauth.html", this, state)!$
$!nav("reverseproxy.html", this, state)!$
+ $!nav("responsestreaming.html", this, state)!$
+ $!nav("socksproxy.html", this, state)!$
+ $!nav("sticky.html", this, state)!$
+ $!nav("tcpproxy.html", this, state)!$
$!nav("upstreamproxy.html", this, state)!$
$!nav("upstreamcerts.html", this, state)!$
- $!nav("proxyauth.html", this, state)!$
- $!nav("responsestreaming.html", this, state)!$
<li class="nav-header">Installing Certificates</li>
diff --git a/doc-src/features/index.py b/doc-src/features/index.py
index 477bb8af..693b4439 100644
--- a/doc-src/features/index.py
+++ b/doc-src/features/index.py
@@ -9,9 +9,11 @@ pages = [
Page("replacements.html", "Replacements"),
Page("responsestreaming.html", "Response Streaming"),
Page("reverseproxy.html", "Reverse proxy mode"),
+ Page("socksproxy.html", "SOCKS Mode"),
Page("setheaders.html", "Set Headers"),
Page("serverreplay.html", "Server-side replay"),
Page("sticky.html", "Sticky cookies and auth"),
+ Page("tcpproxy.html", "TCP Proxy"),
Page("upstreamcerts.html", "Upstream Certs"),
Page("upstreamproxy.html", "Upstream proxy mode"),
] \ No newline at end of file
diff --git a/doc-src/features/passthrough.html b/doc-src/features/passthrough.html
index 039d6b58..7c830639 100644
--- a/doc-src/features/passthrough.html
+++ b/doc-src/features/passthrough.html
@@ -1,13 +1,12 @@
-There are a couple of reasons why you may want to exempt some traffic from mitmproxy's interception mechanism:
+There are two main reasons why you may want to exempt some traffic from mitmproxy's interception mechanism:
- **Certificate pinning:** Some traffic is is protected using
[certificate pinning](https://security.stackexchange.com/questions/29988/what-is-certificate-pinning) and mitmproxy's
interception leads to errors. For example, Windows Update or the Apple App Store fail to work if mitmproxy is active.
-- **Non-HTTP traffic:** WebSockets or other non-http protocols are not supported by mitmproxy yet. You can exempt the
- domain from processing, which would otherwise fail.
- **Convenience:** You really don't care about some parts of the traffic and just want them to go away.
-If you want to ignore traffic from mitmproxy's processing because of large response bodies, check out the
+If you want to peek into (SSL-protected) non-HTTP connections, check out the [tcp proxy](@!urlTo("tcpproxy.html")!@) feature.
+If you want to ignore traffic from mitmproxy's processing because of large response bodies, take a look at the
[response streaming](@!urlTo("responsestreaming.html")!@) feature.
## How it works
@@ -74,4 +73,9 @@ Here are some other examples for ignore patterns:
--ignore 17\.178\.\d+\.\d+:443
</pre>
+### See Also
+
+- [TCP Proxy](@!urlTo("tcpproxy.html")!@)
+- [Response Streaming](@!urlTo("responsestreaming.html")!@)
+
[^explicithttp]: This stems from an limitation of explicit HTTP proxying: A single connection can be re-used for multiple target domains - a <code>GET http://example.com/</code> request may be followed by a <code>GET http://evil.com/</code> request on the same connection. If we start to ignore the connection after the first request, we would miss the relevant second one. \ No newline at end of file
diff --git a/doc-src/features/responsestreaming.html b/doc-src/features/responsestreaming.html
index d20af65c..47fafef7 100644
--- a/doc-src/features/responsestreaming.html
+++ b/doc-src/features/responsestreaming.html
@@ -47,4 +47,8 @@ When response streaming is enabled, portions of the code which would have otherw
on the response body will see an empty response body instead (<code>libmproxy.protocol.http.CONTENT_MISSING</code>). Any modifications will be ignored.
Streamed responses are usually sent in chunks of 4096 bytes. If the response is sent with a <code>Transfer-Encoding:
- chunked</code> header, the response will be streamed one chunk at a time. \ No newline at end of file
+ chunked</code> header, the response will be streamed one chunk at a time.
+
+### See Also
+
+- [Ignore Domains](@!urlTo("passthrough.html")!@)
diff --git a/doc-src/features/reverseproxy.html b/doc-src/features/reverseproxy.html
index e6de4f33..5ef4efc5 100644
--- a/doc-src/features/reverseproxy.html
+++ b/doc-src/features/reverseproxy.html
@@ -7,10 +7,46 @@ mitmproxy forwards HTTP proxy requests to an upstream proxy server.
<table class="table">
<tbody>
<tr>
- <th width="20%">command-line</th> <td>-R http[s]://hostname[:port]</td>
- </tr>
- <tr>
- <th>mitmproxy shortcut</th> <td><b>P</b></td>
+ <th width="20%">command-line</th> <td>-R <i>schema</i>://hostname[:port]</td>
</tr>
</tbody>
</table>
+
+Here, **schema** is one of http, https, http2https or https2http. The latter
+two extended schema specifications control the use of HTTP and HTTPS on
+mitmproxy and the upstream server. You can indicate that mitmproxy should use
+HTTP, and the upstream server uses HTTPS like this:
+
+ http2https://hostname:port
+
+And you can indicate that mitmproxy should use HTTPS while the upstream
+service uses HTTP like this:
+
+ https2http://hostname:port
+
+
+### Host Header
+
+In reverse proxy mode, mitmproxy does not rewrite the host header. While often useful, this
+may lead to issues with public web servers. For example, consider the following scenario:
+
+ $ python mitmdump -d -R http://example.com/ &
+ $ curl http://localhost:8080/
+
+ >> GET https://example.com/
+ Host: localhost:8080
+ User-Agent: curl/7.35.0
+ [...]
+
+ << 404 Not Found 345B
+
+Since the Host header doesn't match <samp>example.com</samp>, an error is returned.<br>
+There are two ways to solve this:
+<ol>
+ <li>Modify the hosts file of your OS so that example.com resolves to 127.0.0.1.</li>
+ <li>
+ Instruct mitmproxy to rewrite the host header by passing <kbd>&#8209;&#8209;setheader&nbsp;:~q:Host:example.com</kbd>.
+ However, keep in mind that absolute URLs within the returned document or HTTP redirects will cause the client application
+ to bypass the proxy.
+ </li>
+</ol> \ No newline at end of file
diff --git a/doc-src/features/socksproxy.html b/doc-src/features/socksproxy.html
new file mode 100644
index 00000000..f436cbf5
--- /dev/null
+++ b/doc-src/features/socksproxy.html
@@ -0,0 +1,10 @@
+
+In this mode, mitmproxy acts as a SOCKS5 proxy server.
+
+<table class="table">
+ <tbody>
+ <tr>
+ <th width="20%">command-line</th> <td>--socks</td>
+ </tr>
+ </tbody>
+</table>
diff --git a/doc-src/features/tcpproxy.html b/doc-src/features/tcpproxy.html
new file mode 100644
index 00000000..819cf297
--- /dev/null
+++ b/doc-src/features/tcpproxy.html
@@ -0,0 +1,30 @@
+WebSockets or other non-HTTP protocols are not supported by mitmproxy yet. However, you can exempt hostnames from
+processing, so that mitmproxy acts as a generic TCP forwarder. This feature is closely related to the
+[ignore domains](@!urlTo("passthrough.html")!@) functionality, but differs in two important aspects:
+
+- The raw TCP messages are printed to the event log.
+- SSL connections will be intercepted.
+
+Please note that message interception or modification are not possible yet.
+If you are not interested in the raw TCP messages, you should use the ignore domains feature.
+
+## How it works
+
+
+<table class="table">
+ <tbody>
+ <tr>
+ <th width="20%">command-line</th> <td>--tcp HOST</td>
+ </tr>
+ <tr>
+ <th>mitmproxy shortcut</th> <td><b>T</b></td>
+ </tr>
+ </tbody>
+</table>
+
+For a detailed description on the structure of the hostname pattern, please refer to the [Ignore Domains](@!urlTo("passthrough.html")!@) feature.
+
+### See Also
+
+- [Ignore Domains](@!urlTo("passthrough.html")!@)
+- [Response Streaming](@!urlTo("responsestreaming.html")!@)
diff --git a/doc-src/features/upstreamproxy.html b/doc-src/features/upstreamproxy.html
index 6039f4df..47bc115d 100644
--- a/doc-src/features/upstreamproxy.html
+++ b/doc-src/features/upstreamproxy.html
@@ -9,8 +9,19 @@ mitmproxy forwards ordinary HTTP requests to an upstream server.
<tr>
<th width="20%">command-line</th> <td>-U http://hostname[:port]</td>
</tr>
- <tr>
- <th>mitmproxy shortcut</th> <td><b>U</b></td>
- </tr>
</tbody>
</table>
+
+Here, **schema** is one of http, https, http2https or https2http. The latter
+two extended schema specifications control the use of HTTP and HTTPS on
+mitmproxy and the upstream server. You can indicate that mitmproxy should use
+HTTP, and the upstream server uses HTTPS like this:
+
+ http2https://hostname:port
+
+And you can indicate that mitmproxy should use HTTPS while the upstream
+service uses HTTP like this:
+
+ https2http://hostname:port
+
+
diff --git a/doc-src/index.html b/doc-src/index.html
index 79687ec6..23da7223 100644
--- a/doc-src/index.html
+++ b/doc-src/index.html
@@ -1,4 +1,27 @@
-@!index_contents!@
+__mitmproxy__ is an interactive, SSL-capable man-in-the-middle proxy for HTTP
+with a console interface.
+__mitmdump__ is the command-line version of mitmproxy. Think tcpdump for HTTP.
+
+__libmproxy__ is the library that mitmproxy and mitmdump are built on.
+
+Documentation, tutorials and distribution packages can be found on the
+mitmproxy.org website:
+
+[mitmproxy.org](http://mitmproxy.org).
+
+
+Features
+--------
+
+- Intercept HTTP requests and responses and modify them on the fly.
+- Save complete HTTP conversations for later replay and analysis.
+- Replay the client-side of an HTTP conversations.
+- Replay HTTP responses of a previously recorded server.
+- Reverse proxy mode to forward traffic to a specified server.
+- Transparent proxy mode on OSX and Linux.
+- Make scripted changes to HTTP traffic using Python.
+- SSL certificates for interception are generated on the fly.
+- And much, much more.
diff --git a/doc-src/index.py b/doc-src/index.py
index b7ab9995..e6064e3a 100644
--- a/doc-src/index.py
+++ b/doc-src/index.py
@@ -1,6 +1,8 @@
-import os, sys, datetime
+import os
+import sys
+import datetime
import countershape
-from countershape import Page, Directory, PythonModule, markup, model
+from countershape import Page, Directory, markup, model
import countershape.template
sys.path.insert(0, "..")
from libmproxy import filt, version
@@ -23,18 +25,18 @@ ns.docMaintainer = "Aldo Cortesi"
ns.docMaintainerEmail = "aldo@corte.si"
ns.copyright = u"\u00a9 mitmproxy project, %s" % datetime.date.today().year
+
def mpath(p):
p = os.path.join(MITMPROXY_SRC, p)
return os.path.expanduser(p)
-with open(mpath("README.mkd")) as f:
- readme = f.read()
- ns.index_contents = readme.split("\n", 1)[1] #remove first line (contains build status)
def example(s):
d = file(mpath(s)).read().rstrip()
extemp = """<div class="example">%s<div class="example_legend">(%s)</div></div>"""
return extemp%(countershape.template.Syntax("py")(d), s)
+
+
ns.example = example
@@ -73,6 +75,7 @@ def nav(page, current, state):
ns.nav = nav
ns.navbar = countershape.template.File(None, "_nav.html")
+
pages = [
Page("index.html", "Introduction"),
Page("install.html", "Installation"),
diff --git a/doc-src/install.html b/doc-src/install.html
index 5d412459..682e317e 100644
--- a/doc-src/install.html
+++ b/doc-src/install.html
@@ -1,40 +1,33 @@
+## Installing from source
+
The preferred way to install mitmproxy - whether you're installing the latest
release or from source - is to use [pip](http://www.pip-installer.org/). If you
don't already have pip on your system, you can find installation instructions
[here](http://www.pip-installer.org/en/latest/installing.html).
-
-## Installing the latest release
-
-A single command will download and install the latest release of mitmproxy,
-along with all its dependencies:
-
<pre class="terminal">
pip install mitmproxy
</pre>
+If you also want to install the optional packages AMF, protobuf and CSS
+content views, do this:
-## Installing from source
-
-When installing from source, the easiest method is still to use pip. In this
-case run:
-
<pre class="terminal">
-pip install /path/to/source
+pip install "mitmproxy[contentviews]"
</pre>
-Note that if you're installing current git master, you will also have to
-install the current git master of [netlib](http://github.com/mitmproxy/netlib) by
-hand.
## OSX
+The easiest way to get up and running on OSX is to download the pre-built
+binary packages from [mitmproxy.org](http://mitmproxy.org). If you still want
+to install using pip, there are a few things to keep in mind:
+
- If you're running a Python interpreter installed with homebrew (or similar),
you may have to install some dependencies by hand.
- Make sure that XCode is installed from the App Store, and that the
command-line tools have been downloaded (XCode/Preferences/Downloads).
-- Now use __pip__ to do the installation, as above.
There are a few bits of customization you might want to do to make mitmproxy
comfortable to use on OSX. The default color scheme is optimized for a dark
@@ -64,8 +57,3 @@ from source:
- libxslt1-dev
-
-
-
-
-
diff --git a/doc-src/modes.html b/doc-src/modes.html
index 77bd1b05..8870009d 100644
--- a/doc-src/modes.html
+++ b/doc-src/modes.html
@@ -1,210 +1,222 @@
-Mitmproxy comes with several modes of operation, which allow you to use mitmproxy in a variety of scenarios.
-This documents briefly explains each mode and possible setups.
-<hr>
-Mitmproxy has four modes of operation:
-<ul>
- <li>Regular Mode (this is what you get by default)</li>
- <li>Transparent Mode</li>
- <li>Reverse Proxy Mode</li>
- <li>Upstream Proxy Mode</li>
-</ul>
-
-<p>Now, which one should you pick? Use this flow chart:
-</p>
-
-<img src="@!urlTo('schematics/proxy-modes-flowchart.png')!@"><br><br>
+
+Mitmproxy has four modes of operation that allow you to use mitmproxy in a
+variety of scenarios:
+
+- **Regular** (the default)
+- **Transparent**
+- **Reverse Proxy**
+- **Upstream Proxy**
+
+Now, which one should you pick? Use this flow chart:
+
+<img src="@!urlTo('schematics/proxy-modes-flowchart.png')!@"/>
<div class="page-header">
<h1>Regular Proxy</h1>
</div>
-Mitmproxy's regular mode it the most simple one and the easiest to set up.
+Mitmproxy's regular mode is the simplest and the easiest to set up.
-<ol>
- <li>Start mitmproxy.</li>
- <li>Configure your client to use mitmproxy. This means that you either adjust the proxy setting of your local browser
- or point an external device to your proxy (which should look like
- <a href="@!urlTo('screenshots/ios-manual.png')!@">this</a>).</li>
- <li>Quick Check: You can already visit an unencrypted HTTP site over the proxy.</li>
- <li>Open the magic domain <strong>mitm.it</strong> and install the certificate for your device.</li>
-</ol>
+1. Start mitmproxy.
+2. Configure your client to use mitmproxy. For instance on IOS, the settings might look like <a href="@!urlTo('screenshots/ios-manual.png')!@">this</a>.
+3. Quick Check: You should already be able to visit an unencrypted HTTP site
+through the proxy.
+4. Open the magic domain <strong>mitm.it</strong> and install the certificate for your device.
<div class="well">
- <strong>Heads Up:</strong> Unfortunately, some applications prefer to bypass the HTTP proxy settings of the system -
- Android applications are a common example. In these cases, you need to use mitmproxy's transparent mode.
+<strong>Heads Up:</strong> Unfortunately, some applications bypass the
+system HTTP proxy settings - Android applications are a common example. In
+these cases, you need to use mitmproxy's transparent mode.
</div>
-<p>If you are proxying an external device, your network will probably look like this:</p>
+If you are proxying an external device, your network will probably look like this:
+
<img src="@!urlTo('schematics/proxy-modes-regular.png')!@">
-<br><br>
-<p>The square brackets signify the source and destination IP addresses. Your client explicitly connects
- to mitmproxy and mitmproxy explicitly connects to the target server.
-</p>
+
+The square brackets signify the source and destination IP addresses. Your
+client explicitly connects to mitmproxy and mitmproxy explicitly connects
+to the target server.
<div class="page-header">
<h1>Transparent Proxy</h1>
</div>
-When a transparent proxy is used, traffic is redirected into a proxy at the network layer, without any client
-configuration being required. This makes transparent proxying ideal for those situations where you can't change client
-behaviour. The basic principle is that mitmproxy sits somewhere on the line from the client to the internet and
-transparently intercepts the request. In the graphic below, a machine running mitmproxy has been inserted between
-the router and the internet:
+In transparent mode, traffic is directed into a proxy at the network layer,
+without any client configuration required. This makes transparent proxying
+ideal for situations where you can't change client behaviour. In the graphic
+below, a machine running mitmproxy has been inserted between the router and
+the internet:
<a href="@!urlTo('schematics/proxy-modes-transparent-1.png')!@">
- <img src="@!urlTo('schematics/proxy-modes-transparent-1.png')!@"></a>
-<p>The square brackets signify the source and destination IP addresses. Round brackets mark the next
- hop on the <strong>Ethernet</strong>/data link layer. This distinction is important to make: When the packet arrives
- at the mitmproxy machine, it must still be addressed to the target server. In other words: A simple IP redirect on
- the router does not work - this would remove the target information, leaving mitmproxy unable to
- determine the real destination.
-</p>
+ <img src="@!urlTo('schematics/proxy-modes-transparent-1.png')!@">
+</a>
+
+The square brackets signify the source and destination IP addresses. Round
+brackets mark the next hop on the *Ethernet/data link* layer. This distinction
+is important: when the packet arrives at the mitmproxy machine, it must still
+be addressed to the target server. This means that Network Address Translation
+should not be applied before the traffic reaches mitmproxy, since this would
+remove the target information, leaving mitmproxy unable to determine the real
+destination.
+
<a href="@!urlTo('schematics/proxy-modes-transparent-wrong.png')!@">
<img src="@!urlTo('schematics/proxy-modes-transparent-wrong.png')!@"></a>
<h2>Common Configurations</h2>
-The first graphic is a little bit idealistic: Usually, you'll have your local wireless lan network and no
-machines between your router and the internet. Fortunately, there are other ways to configure your network:
-(a) Configuring the client to use a custom gateway/router/"next hop", (b) Implementing custom routing on the router
-or (c) setting up a separate wireless network router which gets proxied.
-There are of course other options, but we'll look at these three. In most cases, setting (a) is recommended due to its
-ease of use.
+There are many ways to configure your network for transparent proxying. We'll
+look at three common scenarios:
+
+1. Configuring the client to use a custom gateway/router/"next hop"
+2. Implementing custom routing on the router
+
+In most cases, the first option is recommended due to its ease of use.
<h3>(a) Custom Gateway</h3>
-<p>Looking at your local home network, it's clear what happens if you enter "example.com" into your address bar: After you
-press enter, your OS sends a packet to your router, which then sends this to your ISP, which then sends it to some
-Tier-1 carrier, which then sends it... I think you get the idea. The important part for us is the first step here:
-Your machine is configured to use your router as the next hop. Your router certainly doesn't host example.com, but your
-machine knows that your router will forward it upstream. On the technical level, your router probably provides a DHCP
-server, which instructs all clients to use his address as the <em>Default Gateway</em> for connections that leave the
-current subnet (your local network).</p>
-<p>
-How does this help us? Here comes our trick: By configuring the client to use our machine as its Gateway, all traffic
-will be sent to our machine, which then forwards it to the router. This provides us with the scenario we'd like to have,
-namely packets on our doorstep that are addressed for someone else:
-</p>
+One simple way to get traffic to the mitmproxy machine with the destination IP
+intact, is to simply configure the client with the mitmproxy box as the
+default gateway.
+
<a href="@!urlTo('schematics/proxy-modes-transparent-2.png')!@">
<img src="@!urlTo('schematics/proxy-modes-transparent-2.png')!@"></a>
-Given this concept, we can set up mitmproxy:
-<ol>
- <li>Configure your proxy machine for transparent mode.<br>You can find instructions
- in the <em>Transparent Proxying</em> section of the mitmproxy docs.</li>
- <li>Configure your client to use your proxy machine's IP as the default gateway. This setting is usually called
- <em>Standard Gateway, Router</em> or something along these lines
- (<a href="@!urlTo('screenshots/ios-gateway.png')!@">iOS screenshot</a>).</li>
- <li>Quick Check: You can already visit an unencrypted HTTP site over the proxy.</li>
- <li>Open the magic domain <strong>mitm.it</strong> and install the certificate for your device.</li>
-</ol>
+In this scenario, we would:
+
+- Configure the proxy machine for transparent mode. You can find instructions
+in the <em>Transparent Proxying</em> section of the mitmproxy docs.
+
+- Configure the client to use the proxy machine's IP as the default gateway.
+<a href="@!urlTo('screenshots/ios-gateway.png')!@">Here</a> is what this would
+look like on IOS.
+
+- Quick Check: At this point, you should already be able to visit an
+unencrypted HTTP site over the proxy.
+
+- Open the magic domain <strong>mitm.it</strong> and install the certificate
+for your device.
+
+Setting the custom gateway on clients can be automated by serving the settings
+out to clients over DHCP. This lets set up an interception network where all
+clients are proxied automatically, which can save time and effort.
+
<div class="well">
<strong style="text-align: center; display: block">Troubleshooting Transparent Mode</strong>
- <p>Wrong transparent mode configurations are a frequent source of
+
+ <p>Incorrect transparent mode configurations are a frequent source of
error. If it doesn't work for you, try the following things:</p>
+
<ul>
- <li>Open mitmproxy's event log (press `e`) - can you spot clientconnect messages?
- If not, the packets are not arriving at the proxy. A common source is the occurence of ICMP redirects,
- which means that your machine is telling the client that there's a faster way to the internet by contacting
- your router directly (see the <em>Transparent Proxying</em> section on how to disable them). If in doubt,
- <a href="https://wireshark.org/">Wireshark</a> may help you to see whether something arrives at your machine
- or not.
+ <li>
+ Open mitmproxy's event log (press `e`) - do you see clientconnect
+ messages? If not, the packets are not arriving at the proxy. One common
+ cause is the occurrence of ICMP redirects, which means that your
+ machine is telling the client that there's a faster way to the
+ internet by contacting your router directly (see the
+ <em>Transparent Proxying</em> section on how to disable them). If in
+ doubt, <a href="https://wireshark.org/">Wireshark</a> may help you
+ to see whether something arrives at your machine or not.
</li>
<li>
- Have you explicitly configured an HTTP proxy on your device? You do not need mitmproxy's transparent mode
- then, just start mitmproxy normally. Explicitly setting a proxy and transparent mode contradict each other,
- settle for one. Do not explicitly redirect traffic to mitmproxy anywhere except for the Gateway setting.
+ Make sure you have not explicitly configured an HTTP proxy on the
+ client. This is not needed in transparent mode.
</li>
<li>
Re-check the instructions in the <em>Transparent Proxying</em> section. Anything you missed?
</li>
</ul>
+
If you encounter any other pitfalls that should be listed here, please let us know!
+
</div>
<h3>(b) Custom Routing</h3>
-Custom routing is a fairly advanced setup which we'll only document briefly here.
-First and foremost, it usually requires root on your router. The basic idea is to teach your router a custom routing
-table that says "for requests from ip X, the proxy machine is the next gateway".
+In some cases, you may need more fine-grained control of which traffic reaches
+the mitmproxy instance, and which doesn't. You may, for instance, choose only
+to divert traffic to some hosts into the transparent proxy. There are a huge
+number of ways to accomplish this, and much will depend on the router or
+packet filter you're using. In most cases, the configuration will look like
+this:
<a href="@!urlTo('schematics/proxy-modes-transparent-3.png')!@">
- <img src="@!urlTo('schematics/proxy-modes-transparent-3.png')!@"></a>
-
-For this setup, we expect you to have a basic understanding of networking in general. In short, you should get started
-with <a href="@!urlTo('custom-routing.txt')!@">these routing commands</a>. The Troubleshooting part directly above this
-section might be helpful for you as well.
-
-<h3>(c) Separate Network</h3>
-
-Setting up a separate network using a cheap router might be a viable option, too. Such a configuration mostly resembles
-the idealistic graphic from the beginning (Variant 1). Take a look at the
-<a href="@!urlTo('tutorials/transparent-dhcp.html')!@">Transparently proxify virtual machines</a> tutorial to see how
-such a network could be implemented. The troubleshooting section for custom gateways may be helpful for you, too.
+ <img src="@!urlTo('schematics/proxy-modes-transparent-3.png')!@">
+</a>
<div class="page-header">
<h1>Reverse Proxy</h1>
</div>
-Mitmproxy is usually used with a client that uses the proxy to access the Internet. Using reverse proxy mode, you can
-use mitmproxy to represent a server:
+Mitmproxy is usually used with a client that uses the proxy to access the
+Internet. Using reverse proxy mode, you can use mitmproxy to act like a normal
+HTTP server:
<a href="@!urlTo('schematics/proxy-modes-reverse.png')!@">
- <img src="@!urlTo('schematics/proxy-modes-reverse.png')!@"></a>
+ <img src="@!urlTo('schematics/proxy-modes-reverse.png')!@">
+</a>
There are various use-cases:
-<ul>
-<li>
- Say you have an internal API running at http://example.local/. You could now setup mitmproxy in
- reverse proxy mode at http://debug.example.local/ and dynamically point clients to this new API endpoint,
- which provides clients with the same data and you with debug information. Similarly, you could move your real server
- to a different ip/port and setup mitmproxy at the original place to debug all sessions.
-</li>
-<li>
- Say you're a web developer working on example.com (with a development version running on localhost:8000).
- You can modify your hosts file so that example.com points to 127.0.0.1 and then run mitmproxy in reverse proxy
- mode on port 80. You can test your app on the example.com domain and get all requests recorded in mitmproxy.
-</li>
-<li>
- Say you have some toy project that should get SSL support. Simply setup mitmproxy with SSL termination and you're
- done (<code>mitmdump -p 443 -R https2http://localhost:80/</code>). There are better tools for this specific task (we don't
- have C performance obviously), but it's definitely a nice and very quick way to setup an SSL-speaking server.
-</li>
-<li>
- Want to add a non-SSL-capable compression proxy in front of your server? You could even spawn a mitmproxy instance
- that terminates SSL (https2http://...), point it to the compression proxy and let the compression proxy point
- to a SSL-initiating mitmproxy (http2https://...), which then points to the real server. As you see, it's a fairly
- flexible thing.
-</li>
-</ul>
-
-<p>
-Please note that cloning Google by using <code>mitmproxy -R http://google.com/</code> does <em>not</em> really work
-(as in <a href="@!urlTo('screenshots/ios-reverse.png')!@">this screenshot</a>).
-This may work for the first request, but the HTML remains unchanged: As soon as the user clicks on an non-relative URL
-(or downloads a non-relative image resource), they speak with Google directly again.
-</p>
-<p>
- On another note, mitmproxy either supports an HTTP or an HTTPS upstream server, not both at the same time. You can
- simply work around this by spawning a second mitmproxy instance. Each instance listens to one port and talks to one
- port.
-</p>
+
+- Say you have an internal API running at http://example.local/. You could now
+set up mitmproxy in reverse proxy mode at http://debug.example.local/ and
+dynamically point clients to this new API endpoint, which provides clients
+with the same data and you with debug information. Similarly, you could move
+your real server to a different IP/port and set up mitmproxy at the original
+place to debug all sessions.
+
+- Say you're a web developer working on example.com (with a development
+version running on localhost:8000). You can modify your hosts file so that
+example.com points to 127.0.0.1 and then run mitmproxy in reverse proxy mode
+on port 80. You can test your app on the example.com domain and get all
+requests recorded in mitmproxy.
+
+- Say you have some toy project that should get SSL support. Simply set up
+mitmproxy with SSL termination and you're done (<code>mitmdump -p 443 -R
+https2http://localhost:80/</code>). There are better tools for this specific
+task, but mitmproxy is very quick and simple way to set up an SSL-speaking
+server.
+
+- Want to add a non-SSL-capable compression proxy in front of your server? You
+could even spawn a mitmproxy instance that terminates SSL (https2http://...),
+point it to the compression proxy and let the compression proxy point to a
+SSL-initiating mitmproxy (http2https://...), which then points to the real
+server. As you see, it's a fairly flexible thing.
+
+Note that mitmproxy supports either an HTTP or an HTTPS upstream server, not
+both at the same time. You can work around this by spawning a second mitmproxy
+instance.
+
+<div class="well">
+ <strong style="text-align: center; display: block">Caveat: Interactive Use</strong>
+
+
+One caveat is that reverse proxy mode is often not sufficient for interactive
+browsing. Consider trying to clone Google by using:
+
+<code>mitmproxy -R http://google.com/</code>
+
+This works for the initial request, but the HTML served to the client remains
+unchanged. As soon as the user clicks on an non-relative URL (or downloads a
+non-relative image resource), traffic no longer passes through mitmproxy, and
+the client connects to Google directly again.
+
+</div>
+
+
<div class="page-header">
<h1>Upstream Proxy</h1>
</div>
-<p>
-If you want to add mitmproxy in front of a different proxy appliance, you can use mitmproxy's upstream mode.
-In upstream mode, all requests are unconditionally transferred to an upstream proxy or your choice.
-</p>
+If you want to chain proxies by adding mitmproxy in front of a different proxy
+appliance, you can use mitmproxy's upstream mode. In upstream mode, all
+requests are unconditionally transferred to an upstream proxy of your choice.
<a href="@!urlTo('schematics/proxy-modes-upstream.png')!@">
<img src="@!urlTo('schematics/proxy-modes-upstream.png')!@"></a>
-<p>
-mitmproxy supports both explicit HTTP and explicit HTTPS in upstream proxy mode. You could in theory chain multiple
-mitmproxy instances in a row, but that doesn't make any sense in practice (i.e. outside of our tests).
-</p> \ No newline at end of file
+mitmproxy supports both explicit HTTP and explicit HTTPS in upstream proxy
+mode. You could in theory chain multiple mitmproxy instances in a row, but
+that doesn't make any sense in practice (i.e. outside of our tests).
diff --git a/doc-src/schematics/proxy-modes-transparent-1.png b/doc-src/schematics/proxy-modes-transparent-1.png
index c2027432..002e0e76 100644
--- a/doc-src/schematics/proxy-modes-transparent-1.png
+++ b/doc-src/schematics/proxy-modes-transparent-1.png
Binary files differ
diff --git a/doc-src/schematics/proxy-modes-transparent-2.png b/doc-src/schematics/proxy-modes-transparent-2.png
index 1129e343..41997b05 100644
--- a/doc-src/schematics/proxy-modes-transparent-2.png
+++ b/doc-src/schematics/proxy-modes-transparent-2.png
Binary files differ
diff --git a/doc-src/schematics/proxy-modes-transparent-wrong.png b/doc-src/schematics/proxy-modes-transparent-wrong.png
index 6bac491f..ca501e93 100644
--- a/doc-src/schematics/proxy-modes-transparent-wrong.png
+++ b/doc-src/schematics/proxy-modes-transparent-wrong.png
Binary files differ
diff --git a/doc-src/schematics/proxy-modes.vsdx b/doc-src/schematics/proxy-modes.vsdx
index 74d425fc..c78cf8d0 100644
--- a/doc-src/schematics/proxy-modes.vsdx
+++ b/doc-src/schematics/proxy-modes.vsdx
Binary files differ
diff --git a/doc-src/ssl.html b/doc-src/ssl.html
index 91225d79..3fa0e070 100644
--- a/doc-src/ssl.html
+++ b/doc-src/ssl.html
@@ -41,10 +41,26 @@ The files created by mitmproxy in the .mitmproxy directory are as follows:
Using a custom certificate
--------------------------
-You can use your own certificate by passing the __--cert__ option to mitmproxy.
+You can use your own certificate by passing the <kbd>--cert</kbd> option to mitmproxy. mitmproxy then uses the provided
+certificate for interception of the specified domains instead of generating a cert signed by its own CA.
-The certificate file is expected to be in the PEM format. You can generate
-a certificate in this format using these instructions:
+The certificate file is expected to be in the PEM format.
+You can include intermediary certificates right below your leaf certificate, so that you PEM file roughly looks like
+this:
+
+<pre>
+-----BEGIN PRIVATE KEY-----
+&lt;private key&gt;
+-----END PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+&lt;cert&gt;
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+&lt;intermediary cert (optional)&gt;
+-----END CERTIFICATE-----
+</pre>
+
+For example, you can generate a certificate in this format using these instructions:
<pre class="terminal">
> openssl genrsa -out cert.key 8192
@@ -55,6 +71,15 @@ a certificate in this format using these instructions:
</pre>
+Using a custom certificate authority
+------------------------------------
+
+By default, mitmproxy will (generate and) use <samp>~/.mitmproxy/mitmproxy-ca.pem</samp> as the default certificate
+authority to generate certificates for all domains for which no custom certificate is provided (see above).
+You can use your own certificate authority by passing the <kbd>--confdir</kbd> option to mitmproxy.
+mitmproxy will then look for <samp>mitmproxy-ca.pem</samp> in the specified directory. If no such file exists,
+it will be generated automatically.
+
Installing the mitmproxy CA
---------------------------
diff --git a/examples/change_upstream_proxy.py b/examples/change_upstream_proxy.py
index e063ca4f..74a43bd0 100644
--- a/examples/change_upstream_proxy.py
+++ b/examples/change_upstream_proxy.py
@@ -1,7 +1,7 @@
-# This scripts demonstrates how mitmproxy can switch to a different upstream proxy
+# This scripts demonstrates how mitmproxy can switch to a second/different upstream proxy
# in upstream proxy mode.
#
-# Usage: mitmdump -s "change_upstream_proxy.py host"
+# Usage: mitmdump -U http://default-upstream-proxy.local:8080/ -s "change_upstream_proxy.py host"
from libmproxy.protocol.http import send_connect_request
alternative_upstream_proxy = ("localhost", 8082)
diff --git a/examples/flowbasic b/examples/flowbasic
index 21d31efa..c71debc9 100755
--- a/examples/flowbasic
+++ b/examples/flowbasic
@@ -36,7 +36,7 @@ class MyMaster(flow.FlowMaster):
config = proxy.ProxyConfig(
port=8080,
- ca_file=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem")
+ cadir="~/.mitmproxy/" # use ~/.mitmproxy/mitmproxy-ca.pem as default CA file.
)
state = flow.State()
server = ProxyServer(config)
diff --git a/examples/har_extractor.py b/examples/har_extractor.py
new file mode 100644
index 00000000..531f32aa
--- /dev/null
+++ b/examples/har_extractor.py
@@ -0,0 +1,212 @@
+"""
+ This inline script utilizes harparser.HAR from https://github.com/JustusW/harparser
+ to generate a HAR log object.
+"""
+try:
+ from harparser import HAR
+ from pytz import UTC
+except ImportError as e:
+ import sys
+ print >> sys.stderr, "\r\nMissing dependencies: please run `pip install mitmproxy[examples]`.\r\n"
+ raise
+
+from datetime import datetime, timedelta, tzinfo
+
+
+class _HARLog(HAR.log):
+ # The attributes need to be registered here for them to actually be available later via self. This is
+ # due to HAREncodable linking __getattr__ to __getitem__. Anything that is set only in __init__ will
+ # just be added as key/value pair to self.__classes__.
+ __page_list__ = []
+ __page_count__ = 0
+ __page_ref__ = {}
+
+ def __init__(self, page_list):
+ self.__page_list__ = page_list
+ self.__page_count__ = 0
+ self.__page_ref__ = {}
+
+ HAR.log.__init__(self, {"version": "1.2",
+ "creator": {"name": "MITMPROXY HARExtractor",
+ "version": "0.1",
+ "comment": ""},
+ "pages": [],
+ "entries": []})
+
+ def reset(self):
+ self.__init__(self.__page_list__)
+
+ def add(self, obj):
+ if isinstance(obj, HAR.pages):
+ self['pages'].append(obj)
+ if isinstance(obj, HAR.entries):
+ self['entries'].append(obj)
+
+ def create_page_id(self):
+ self.__page_count__ += 1
+ return "autopage_%s" % str(self.__page_count__)
+
+ def set_page_ref(self, page, ref):
+ self.__page_ref__[page] = ref
+
+ def get_page_ref(self, page):
+ return self.__page_ref__.get(page, None)
+
+ def get_page_list(self):
+ return self.__page_list__
+
+
+def start(context, argv):
+ """
+ On start we create a HARLog instance. You will have to adapt this to suit your actual needs
+ of HAR generation. As it will probably be necessary to cluster logs by IPs or reset them
+ from time to time.
+ """
+ context.dump_file = None
+ if len(argv) > 1:
+ context.dump_file = argv[1]
+ else:
+ raise ValueError('Usage: -s "har_extractor.py filename" '
+ '(- will output to stdout, filenames ending with .zhar will result in compressed har)')
+ context.HARLog = _HARLog(['https://github.com'])
+ context.seen_server = set()
+
+
+def response(context, flow):
+ """
+ Called when a server response has been received. At the time of this message both
+ a request and a response are present and completely done.
+ """
+ # Values are converted from float seconds to int milliseconds later.
+ ssl_time = -.001
+ connect_time = -.001
+ if flow.server_conn not in context.seen_server:
+ # Calculate the connect_time for this server_conn. Afterwards add it to seen list, in
+ # order to avoid the connect_time being present in entries that use an existing connection.
+ connect_time = flow.server_conn.timestamp_tcp_setup - flow.server_conn.timestamp_start
+ context.seen_server.add(flow.server_conn)
+
+ if flow.server_conn.timestamp_ssl_setup is not None:
+ # Get the ssl_time for this server_conn as the difference between the start of the successful
+ # tcp setup and the successful ssl setup. If no ssl setup has been made it is left as -1 since
+ # it doesn't apply to this connection.
+ ssl_time = flow.server_conn.timestamp_ssl_setup - flow.server_conn.timestamp_tcp_setup
+
+ # Calculate the raw timings from the different timestamps present in the request and response object.
+ # For lack of a way to measure it dns timings can not be calculated. The same goes for HAR blocked:
+ # MITMProxy will open a server connection as soon as it receives the host and port from the client
+ # connection. So the time spent waiting is actually spent waiting between request.timestamp_end and
+ # response.timestamp_start thus it correlates to HAR wait instead.
+ timings_raw = {'send': flow.request.timestamp_end - flow.request.timestamp_start,
+ 'wait': flow.response.timestamp_start - flow.request.timestamp_end,
+ 'receive': flow.response.timestamp_end - flow.response.timestamp_start,
+ 'connect': connect_time,
+ 'ssl': ssl_time}
+
+ # HAR timings are integers in ms, so we have to re-encode the raw timings to that format.
+ timings = dict([(key, int(1000 * value)) for key, value in timings_raw.iteritems()])
+
+ # The full_time is the sum of all timings. Timings set to -1 will be ignored as per spec.
+ full_time = 0
+ for item in timings.values():
+ if item > -1:
+ full_time += item
+
+ started_date_time = datetime.fromtimestamp(flow.request.timestamp_start, tz=utc).isoformat()
+
+ request_query_string = [{"name": k, "value": v} for k, v in flow.request.get_query()]
+ request_http_version = ".".join([str(v) for v in flow.request.httpversion])
+ # Cookies are shaped as tuples by MITMProxy.
+ request_cookies = [{"name": k.strip(), "value": v[0]} for k, v in (flow.request.get_cookies() or {}).iteritems()]
+ request_headers = [{"name": k, "value": v} for k, v in flow.request.headers]
+ request_headers_size = len(str(flow.request.headers))
+ request_body_size = len(flow.request.content)
+
+ response_http_version = ".".join([str(v) for v in flow.response.httpversion])
+ # Cookies are shaped as tuples by MITMProxy.
+ response_cookies = [{"name": k.strip(), "value": v[0]} for k, v in (flow.response.get_cookies() or {}).iteritems()]
+ response_headers = [{"name": k, "value": v} for k, v in flow.response.headers]
+ response_headers_size = len(str(flow.response.headers))
+ response_body_size = len(flow.response.content)
+ response_body_decoded_size = len(flow.response.get_decoded_content())
+ response_body_compression = response_body_decoded_size - response_body_size
+ response_mime_type = flow.response.headers.get_first('Content-Type', '')
+ response_redirect_url = flow.response.headers.get_first('Location', '')
+
+ entry = HAR.entries({"startedDateTime": started_date_time,
+ "time": full_time,
+ "request": {"method": flow.request.method,
+ "url": flow.request.url,
+ "httpVersion": request_http_version,
+ "cookies": request_cookies,
+ "headers": request_headers,
+ "queryString": request_query_string,
+ "headersSize": request_headers_size,
+ "bodySize": request_body_size, },
+ "response": {"status": flow.response.code,
+ "statusText": flow.response.msg,
+ "httpVersion": response_http_version,
+ "cookies": response_cookies,
+ "headers": response_headers,
+ "content": {"size": response_body_size,
+ "compression": response_body_compression,
+ "mimeType": response_mime_type},
+ "redirectURL": response_redirect_url,
+ "headersSize": response_headers_size,
+ "bodySize": response_body_size, },
+ "cache": {},
+ "timings": timings, })
+
+ # If the current url is in the page list of context.HARLog or does not have a referrer we add it as a new
+ # pages object.
+ if flow.request.url in context.HARLog.get_page_list() or flow.request.headers.get('Referer', None) is None:
+ page_id = context.HARLog.create_page_id()
+ context.HARLog.add(HAR.pages({"startedDateTime": entry['startedDateTime'],
+ "id": page_id,
+ "title": flow.request.url, }))
+ context.HARLog.set_page_ref(flow.request.url, page_id)
+ entry['pageref'] = page_id
+
+ # Lookup the referer in the page_ref of context.HARLog to point this entries pageref attribute to the right
+ # pages object, then set it as a new reference to build a reference tree.
+ elif context.HARLog.get_page_ref(flow.request.headers.get('Referer', (None, ))[0]) is not None:
+ entry['pageref'] = context.HARLog.get_page_ref(flow.request.headers['Referer'][0])
+ context.HARLog.set_page_ref(flow.request.headers['Referer'][0], entry['pageref'])
+
+ context.HARLog.add(entry)
+
+
+def done(context):
+ """
+ Called once on script shutdown, after any other events.
+ """
+ from pprint import pprint
+ import json
+
+ json_dump = context.HARLog.json()
+ compressed_json_dump = context.HARLog.compress()
+
+ print "=" * 100
+ if context.dump_file == '-':
+ pprint(json.loads(json_dump))
+ elif context.dump_file.endswith('.zhar'):
+ file(context.dump_file, "w").write(compressed_json_dump)
+ else:
+ file(context.dump_file, "w").write(json_dump)
+ print "=" * 100
+ print "HAR log finished with %s bytes (%s bytes compressed)" % (len(json_dump), len(compressed_json_dump))
+ print "Compression rate is %s%%" % str(100. * len(compressed_json_dump) / len(json_dump))
+ print "=" * 100
+
+
+def print_attributes(obj, filter_string=None, hide_privates=False):
+ """
+ Useful helper method to quickly get all attributes of an object and its values.
+ """
+ for attr in dir(obj):
+ if hide_privates and "__" in attr:
+ continue
+ if filter_string is not None and filter_string not in attr:
+ continue
+ value = getattr(obj, attr)
+ print "%s.%s" % ('obj', attr), value, type(value) \ No newline at end of file
diff --git a/examples/ignore_websocket.py b/examples/ignore_websocket.py
new file mode 100644
index 00000000..48093951
--- /dev/null
+++ b/examples/ignore_websocket.py
@@ -0,0 +1,34 @@
+# This script makes mitmproxy switch to passthrough mode for all HTTP
+# responses with "Connection: Upgrade" header. This is useful to make
+# WebSockets work in untrusted environments.
+#
+# Note: Chrome (and possibly other browsers), when explicitly configured
+# to use a proxy (i.e. mitmproxy's regular mode), send a CONNECT request
+# to the proxy before they initiate the websocket connection.
+# To make WebSockets work in these cases, supply
+# `--ignore :80$` as an additional parameter.
+# (see http://mitmproxy.org/doc/features/passthrough.html)
+
+from libmproxy.protocol.http import HTTPRequest
+from libmproxy.protocol.tcp import TCPHandler
+from libmproxy.protocol import KILL
+from libmproxy.script import concurrent
+
+
+def start(context, argv):
+ HTTPRequest._headers_to_strip_off.remove("Connection")
+ HTTPRequest._headers_to_strip_off.remove("Upgrade")
+
+
+def done(context):
+ HTTPRequest._headers_to_strip_off.append("Connection")
+ HTTPRequest._headers_to_strip_off.append("Upgrade")
+
+@concurrent
+def response(context, flow):
+ if flow.response.headers.get_first("Connection", None) == "Upgrade":
+ # We need to send the response manually now...
+ flow.client_conn.send(flow.response.assemble())
+ # ...and then delegate to tcp passthrough.
+ TCPHandler(flow.live.c, log=False).handle_messages()
+ flow.reply(KILL) \ No newline at end of file
diff --git a/examples/stickycookies b/examples/stickycookies
index 132e4dc7..67b31da1 100755
--- a/examples/stickycookies
+++ b/examples/stickycookies
@@ -36,10 +36,7 @@ class StickyMaster(controller.Master):
flow.reply()
-config = proxy.ProxyConfig(
- port=8080,
- ca_file=os.path.expanduser("~/.mitmproxy/mitmproxy-ca.pem")
-)
+config = proxy.ProxyConfig(port=8080)
server = ProxyServer(config)
m = StickyMaster(server)
m.run()
diff --git a/libmproxy/cmdline.py b/libmproxy/cmdline.py
index f6cd1ab8..b892f1fd 100644
--- a/libmproxy/cmdline.py
+++ b/libmproxy/cmdline.py
@@ -1,9 +1,9 @@
from __future__ import absolute_import
+import os
import re
-import argparse
-from argparse import ArgumentTypeError
+import configargparse
from netlib import http
-from . import filt, utils
+from . import filt, utils, version
from .proxy import config
APP_HOST = "mitm.it"
@@ -23,7 +23,9 @@ def _parse_hook(s):
elif len(parts) == 3:
patt, a, b = parts
else:
- raise ParseException("Malformed hook specifier - too few clauses: %s" % s)
+ raise ParseException(
+ "Malformed hook specifier - too few clauses: %s" % s
+ )
if not a:
raise ParseException("Empty clause: %s" % str(patt))
@@ -102,7 +104,9 @@ def parse_server_spec(url):
p = http.parse_url(normalized_url)
if not p or not p[1]:
- raise ArgumentTypeError("Invalid server specification: %s" % url)
+ raise configargparse.ArgumentTypeError(
+ "Invalid server specification: %s" % url
+ )
if url.lower().startswith("https2http"):
ssl = [True, False]
@@ -131,17 +135,19 @@ def get_common_options(options):
try:
p = parse_replace_hook(i)
except ParseException, e:
- raise ArgumentTypeError(e.message)
+ raise configargparse.ArgumentTypeError(e.message)
reps.append(p)
for i in options.replace_file:
try:
patt, rex, path = parse_replace_hook(i)
except ParseException, e:
- raise ArgumentTypeError(e.message)
+ raise configargparse.ArgumentTypeError(e.message)
try:
v = open(path, "rb").read()
except IOError, e:
- raise ArgumentTypeError("Could not read replace file: %s" % path)
+ raise configargparse.ArgumentTypeError(
+ "Could not read replace file: %s" % path
+ )
reps.append((patt, rex, v))
setheaders = []
@@ -149,7 +155,7 @@ def get_common_options(options):
try:
p = parse_setheader(i)
except ParseException, e:
- raise ArgumentTypeError(e.message)
+ raise configargparse.ArgumentTypeError(e.message)
setheaders.append(p)
return dict(
@@ -183,14 +189,23 @@ def get_common_options(options):
def common_options(parser):
parser.add_argument(
+ '--version',
+ action= 'version',
+ version= "%(prog)s" + " " + version.VERSION
+ )
+ parser.add_argument(
"--anticache",
action="store_true", dest="anticache", default=False,
- help="Strip out request headers that might cause the server to return 304-not-modified."
+
+ help="""
+ Strip out request headers that might cause the server to return
+ 304-not-modified.
+ """
)
parser.add_argument(
- "--confdir",
- action="store", type=str, dest="confdir", default='~/.mitmproxy',
- help="Configuration directory. (~/.mitmproxy)"
+ "--cadir",
+ action="store", type=str, dest="cadir", default=config.CA_DIR,
+ help="Location of the default mitmproxy CA files. (%s)"%config.CA_DIR
)
parser.add_argument(
"--host",
@@ -198,112 +213,150 @@ def common_options(parser):
help="Use the Host header to construct URLs for display."
)
parser.add_argument(
- "-q",
+ "-q", "--quiet",
action="store_true", dest="quiet",
help="Quiet."
)
parser.add_argument(
- "-r",
+ "-r", "--read-flows",
action="store", dest="rfile", default=None,
help="Read flows from file."
)
parser.add_argument(
- "-s",
+ "-s", "--script",
action="append", type=str, dest="scripts", default=[],
metavar='"script.py --bar"',
- help="Run a script. Surround with quotes to pass script arguments. Can be passed multiple times."
+ help="""
+ Run a script. Surround with quotes to pass script arguments. Can be
+ passed multiple times.
+ """
)
parser.add_argument(
- "-t",
- action="store", dest="stickycookie_filt", default=None, metavar="FILTER",
+ "-t", "--stickycookie",
+ action="store",
+ dest="stickycookie_filt",
+ default=None,
+ metavar="FILTER",
help="Set sticky cookie filter. Matched against requests."
)
parser.add_argument(
- "-u",
+ "-u", "--stickyauth",
action="store", dest="stickyauth_filt", default=None, metavar="FILTER",
help="Set sticky auth filter. Matched against requests."
)
parser.add_argument(
- "-v",
+ "-v", "--verbose",
action="store_const", dest="verbose", default=1, const=2,
help="Increase event log verbosity."
)
parser.add_argument(
- "-w",
+ "-w", "--wfile",
action="store", dest="wfile", default=None,
help="Write flows to file."
)
parser.add_argument(
- "-z",
+ "-z", "--anticomp",
action="store_true", dest="anticomp", default=False,
help="Try to convince servers to send us un-compressed data."
)
parser.add_argument(
- "-Z",
+ "-Z", "--body-size-limit",
action="store", dest="body_size_limit", default=None,
metavar="SIZE",
- help="Byte size limit of HTTP request and response bodies." \
+ help="Byte size limit of HTTP request and response bodies."
" Understands k/m/g suffixes, i.e. 3m for 3 megabytes."
)
parser.add_argument(
"--stream",
action="store", dest="stream_large_bodies", default=None,
metavar="SIZE",
- help="Stream data to the client if response body exceeds the given threshold. "
- "If streamed, the body will not be stored in any way. Understands k/m/g suffixes, i.e. 3m for 3 megabytes."
+ help="""
+ Stream data to the client if response body exceeds the given
+ threshold. If streamed, the body will not be stored in any way.
+ Understands k/m/g suffixes, i.e. 3m for 3 megabytes.
+ """
)
group = parser.add_argument_group("Proxy Options")
- # We could make a mutually exclusive group out of -R, -U, -T, but we don't do that because
- # - --upstream-server should be in that group as well, but it's already in a different group.
- # - our own error messages are more helpful
+ # We could make a mutually exclusive group out of -R, -U, -T, but we don't
+ # do that because - --upstream-server should be in that group as well, but
+ # it's already in a different group. - our own error messages are more
+ # helpful
group.add_argument(
- "-b",
+ "-b", "--bind-address",
action="store", type=str, dest="addr", default='',
help="Address to bind proxy to (defaults to all interfaces)"
)
group.add_argument(
"-I", "--ignore",
- action="append", type=str, dest="ignore", default=[],
+ action="append", type=str, dest="ignore_hosts", default=[],
metavar="HOST",
- help="Ignore host and forward all traffic without processing it. "
- "In transparent mode, it is recommended to use an IP address (range), not the hostname. "
- "In regular mode, only SSL traffic is ignored and the hostname should be used. "
- "The supplied value is interpreted as a regular expression and matched on the ip or the hostname. "
- "Can be passed multiple times. "
+ help="""
+ Ignore host and forward all traffic without processing it. In
+ transparent mode, it is recommended to use an IP address (range),
+ not the hostname. In regular mode, only SSL traffic is ignored and
+ the hostname should be used. The supplied value is interpreted as a
+ regular expression and matched on the ip or the hostname. Can be
+ passed multiple times.
+ """
)
group.add_argument(
- "-n",
+ "--tcp",
+ action="append", type=str, dest="tcp_hosts", default=[],
+ metavar="HOST",
+ help="""
+ Generic TCP SSL proxy mode for all hosts that match the pattern.
+ Similar to --ignore, but SSL connections are intercepted. The
+ communication contents are printed to the event log in verbose mode.
+ """
+ )
+ group.add_argument(
+ "-n", "--no-server",
action="store_true", dest="no_server",
help="Don't start a proxy server."
)
group.add_argument(
- "-p",
+ "-p", "--port",
action="store", type=int, dest="port", default=8080,
help="Proxy service port."
)
group.add_argument(
- "-R",
- action="store", type=parse_server_spec, dest="reverse_proxy", default=None,
- help="Forward all requests to upstream HTTP server: http[s][2http[s]]://host[:port]"
+ "-R", "--reverse",
+ action="store",
+ type=parse_server_spec,
+ dest="reverse_proxy",
+ default=None,
+ help="""
+ Forward all requests to upstream HTTP server:
+ http[s][2http[s]]://host[:port]
+ """
)
group.add_argument(
- "-T",
+ "--socks",
+ action="store_true", dest="socks_proxy", default=False,
+ help="Set SOCKS5 proxy mode."
+ )
+ group.add_argument(
+ "-T", "--transparent",
action="store_true", dest="transparent_proxy", default=False,
help="Set transparent proxy mode."
)
group.add_argument(
- "-U",
- action="store", type=parse_server_spec, dest="upstream_proxy", default=None,
+ "-U", "--upstream",
+ action="store",
+ type=parse_server_spec,
+ dest="upstream_proxy",
+ default=None,
help="Forward all requests to upstream proxy server: http://host[:port]"
)
group = parser.add_argument_group(
"Advanced Proxy Options",
"""
- The following options allow a custom adjustment of the proxy behavior.
- Normally, you don't want to use these options directly and use the provided wrappers instead (-R, -U, -T).
- """.strip()
+ The following options allow a custom adjustment of the proxy
+ behavior. Normally, you don't want to use these options directly and
+ use the provided wrappers instead (-R, -U, -T).
+ """
)
group.add_argument(
"--http-form-in", dest="http_form_in", default=None,
@@ -318,38 +371,44 @@ def common_options(parser):
group = parser.add_argument_group("Onboarding App")
group.add_argument(
- "-a",
+ "-a", "--noapp",
action="store_false", dest="app", default=True,
help="Disable the mitmproxy onboarding app."
)
group.add_argument(
"--app-host",
action="store", dest="app_host", default=APP_HOST, metavar="host",
- help="Domain to serve the onboarding app from. For transparent mode, use an IP when\
- a DNS entry for the app domain is not present. Default: %s" % APP_HOST
-
+ help="""
+ Domain to serve the onboarding app from. For transparent mode, use
+ an IP when a DNS entry for the app domain is not present. Default:
+ %s
+ """ % APP_HOST
)
group.add_argument(
"--app-port",
- action="store", dest="app_port", default=APP_PORT, type=int, metavar="80",
+ action="store",
+ dest="app_port",
+ default=APP_PORT,
+ type=int,
+ metavar="80",
help="Port to serve the onboarding app from."
)
group = parser.add_argument_group("Client Replay")
group.add_argument(
- "-c",
+ "-c", "--client-replay",
action="store", dest="client_replay", default=None, metavar="PATH",
help="Replay client requests from a saved file."
)
group = parser.add_argument_group("Server Replay")
group.add_argument(
- "-S",
+ "-S", "--server-replay",
action="store", dest="server_replay", default=None, metavar="PATH",
help="Replay server responses from a saved file."
)
group.add_argument(
- "-k",
+ "-k", "--kill",
action="store_true", dest="kill", default=False,
help="Kill extra requests during replay."
)
@@ -362,8 +421,10 @@ def common_options(parser):
group.add_argument(
"--norefresh",
action="store_true", dest="norefresh", default=False,
- help="Disable response refresh, "
- "which updates times in cookies and headers for replayed responses."
+ help="""
+ Disable response refresh, which updates times in cookies and headers
+ for replayed responses.
+ """
)
group.add_argument(
"--no-pop",
@@ -374,14 +435,18 @@ def common_options(parser):
group.add_argument(
"--replay-ignore-content",
action="store_true", dest="replay_ignore_content", default=False,
- help="Ignore request's content while searching for a saved flow to replay"
+ help="""
+ Ignore request's content while searching for a saved flow to replay
+ """
)
group.add_argument(
"--replay-ignore-param",
action="append", dest="replay_ignore_params", type=str,
- help="Request's parameters to be ignored while searching for a saved flow to replay"
- "Can be passed multiple times."
- )
+ help="""
+ Request's parameters to be ignored while searching for a saved flow
+ to replay. Can be passed multiple times.
+ """
+ )
group = parser.add_argument_group(
"Replacements",
@@ -399,9 +464,12 @@ def common_options(parser):
)
group.add_argument(
"--replace-from-file",
- action="append", type=str, dest="replace_file", default=[],
- metavar="PATH",
- help="Replacement pattern, where the replacement clause is a path to a file."
+ action = "append", type=str, dest="replace_file", default=[],
+ metavar = "PATH",
+ help = """
+ Replacement pattern, where the replacement clause is a path to a
+ file.
+ """
)
group = parser.add_argument_group(
@@ -437,7 +505,10 @@ def common_options(parser):
"--singleuser",
action="store", dest="auth_singleuser", type=str,
metavar="USER",
- help="Allows access to a a single user, specified in the form username:password."
+ help="""
+ Allows access to a a single user, specified in the form
+ username:password.
+ """
)
user_specification_group.add_argument(
"--htpasswd",
@@ -447,3 +518,116 @@ def common_options(parser):
)
config.ssl_option_group(parser)
+
+
+def mitmproxy():
+ # Don't import libmproxy.console for mitmdump, urwid is not available on all
+ # platforms.
+ from .console import palettes
+
+ parser = configargparse.ArgumentParser(
+ usage="%(prog)s [options]",
+ args_for_setting_config_path = ["--conf"],
+ default_config_files = [
+ os.path.join(config.CA_DIR, "common.conf"),
+ os.path.join(config.CA_DIR, "mitmproxy.conf")
+ ],
+ add_config_file_help = True,
+ add_env_var_help = True
+ )
+ common_options(parser)
+ parser.add_argument(
+ "--palette", type=str, default="dark",
+ action="store", dest="palette",
+ help="Select color palette: " + ", ".join(palettes.palettes.keys())
+ )
+ parser.add_argument(
+ "-e", "--eventlog",
+ action="store_true", dest="eventlog",
+ help="Show event log."
+ )
+ group = parser.add_argument_group(
+ "Filters",
+ "See help in mitmproxy for filter expression syntax."
+ )
+ group.add_argument(
+ "-i", "--intercept", action="store",
+ type=str, dest="intercept", default=None,
+ help="Intercept filter expression."
+ )
+ return parser
+
+
+def mitmdump():
+ parser = configargparse.ArgumentParser(
+ usage="%(prog)s [options] [filter]",
+ args_for_setting_config_path = ["--conf"],
+ default_config_files = [
+ os.path.join(config.CA_DIR, "common.conf"),
+ os.path.join(config.CA_DIR, "mitmdump.conf")
+ ],
+ add_config_file_help = True,
+ add_env_var_help = True
+ )
+
+ common_options(parser)
+ parser.add_argument(
+ "--keepserving",
+ action= "store_true", dest="keepserving", default=False,
+ help= """
+ Continue serving after client playback or file read. We exit by
+ default.
+ """
+ )
+ parser.add_argument(
+ "-d", "--detail",
+ action="count", dest="flow_detail", default=1,
+ help="Increase flow detail display level. Can be passed multiple times."
+ )
+ parser.add_argument('args', nargs="...")
+ return parser
+
+
+def mitmweb():
+ parser = configargparse.ArgumentParser(
+ usage="%(prog)s [options]",
+ args_for_setting_config_path = ["--conf"],
+ default_config_files = [
+ os.path.join(config.CA_DIR, "common.conf"),
+ os.path.join(config.CA_DIR, "mitmweb.conf")
+ ],
+ add_config_file_help = True,
+ add_env_var_help = True
+ )
+
+ group = parser.add_argument_group("Mitmweb")
+ group.add_argument(
+ "--wport",
+ action="store", type=int, dest="wport", default=8081,
+ metavar="PORT",
+ help="Mitmweb port."
+ )
+ group.add_argument(
+ "--wiface",
+ action="store", dest="wiface", default="127.0.0.1",
+ metavar="IFACE",
+ help="Mitmweb interface."
+ )
+ group.add_argument(
+ "--wdebug",
+ action="store_true", dest="wdebug",
+ help="Turn on mitmweb debugging"
+ )
+
+ common_options(parser)
+ group = parser.add_argument_group(
+ "Filters",
+ "See help in mitmproxy for filter expression syntax."
+ )
+ group.add_argument(
+ "-i", "--intercept", action="store",
+ type=str, dest="intercept", default=None,
+ help="Intercept filter expression."
+ )
+ return parser
+
diff --git a/libmproxy/console/__init__.py b/libmproxy/console/__init__.py
index 9c4b4827..e6bc9b41 100644
--- a/libmproxy/console/__init__.py
+++ b/libmproxy/console/__init__.py
@@ -129,10 +129,14 @@ class StatusBar(common.WWrap):
r.append(":%s in file]"%self.master.server_playback.count())
else:
r.append(":%s to go]"%self.master.server_playback.count())
- if self.master.get_ignore():
+ if self.master.get_ignore_filter():
r.append("[")
r.append(("heading_key", "I"))
- r.append("gnore:%d]"%len(self.master.get_ignore()))
+ r.append("gnore:%d]" % len(self.master.get_ignore_filter()))
+ if self.master.get_tcp_filter():
+ r.append("[")
+ r.append(("heading_key", "T"))
+ r.append("CP:%d]" % len(self.master.get_tcp_filter()))
if self.master.state.intercept_txt:
r.append("[")
r.append(("heading_key", "i"))
@@ -512,7 +516,8 @@ class ConsoleMaster(flow.FlowMaster):
self.start_server_playback(
ret,
self.killextra, self.rheaders,
- False, self.nopop
+ False, self.nopop,
+ self.options.replay_ignore_params, self.options.replay_ignore_content
)
def spawn_editor(self, data):
@@ -798,9 +803,13 @@ class ConsoleMaster(flow.FlowMaster):
for command in commands:
self.load_script(command)
- def edit_ignore(self, ignore):
+ def edit_ignore_filter(self, ignore):
patterns = (x[0] for x in ignore)
- self.set_ignore(patterns)
+ self.set_ignore_filter(patterns)
+
+ def edit_tcp_filter(self, tcp):
+ patterns = (x[0] for x in tcp)
+ self.set_tcp_filter(patterns)
def loop(self):
changed = True
@@ -811,7 +820,7 @@ class ConsoleMaster(flow.FlowMaster):
self.statusbar.redraw()
size = self.drawscreen()
changed = self.tick(self.masterq, 0.01)
- self.ui.set_input_timeouts(max_wait=0.1)
+ self.ui.set_input_timeouts(max_wait=0.01)
keys = self.ui.get_input()
if keys:
changed = True
@@ -860,10 +869,18 @@ class ConsoleMaster(flow.FlowMaster):
)
elif k == "I":
self.view_grideditor(
- grideditor.IgnoreEditor(
+ grideditor.HostPatternEditor(
+ self,
+ [[x] for x in self.get_ignore_filter()],
+ self.edit_ignore_filter
+ )
+ )
+ elif k == "T":
+ self.view_grideditor(
+ grideditor.HostPatternEditor(
self,
- [[x] for x in self.get_ignore()],
- self.edit_ignore
+ [[x] for x in self.get_tcp_filter()],
+ self.edit_tcp_filter
)
)
elif k == "i":
@@ -1033,7 +1050,7 @@ class ConsoleMaster(flow.FlowMaster):
self.eventlist[:] = []
def add_event(self, e, level="info"):
- needed = dict(error=1, info=1, debug=2).get(level, 1)
+ needed = dict(error=0, info=1, debug=2).get(level, 1)
if self.options.verbosity < needed:
return
diff --git a/libmproxy/console/flowlist.py b/libmproxy/console/flowlist.py
index e0330171..3eb4eb1a 100644
--- a/libmproxy/console/flowlist.py
+++ b/libmproxy/console/flowlist.py
@@ -120,13 +120,15 @@ class ConnectionItem(common.WWrap):
self.master.start_server_playback(
[i.copy() for i in self.master.state.view],
self.master.killextra, self.master.rheaders,
- False, self.master.nopop
+ False, self.master.nopop,
+ self.master.options.replay_ignore_params, self.master.options.replay_ignore_content
)
elif k == "t":
self.master.start_server_playback(
[self.flow.copy()],
self.master.killextra, self.master.rheaders,
- False, self.master.nopop
+ False, self.master.nopop,
+ self.master.options.replay_ignore_params, self.master.options.replay_ignore_content
)
else:
self.master.path_prompt(
diff --git a/libmproxy/console/flowview.py b/libmproxy/console/flowview.py
index b2c46147..1ec57a4e 100644
--- a/libmproxy/console/flowview.py
+++ b/libmproxy/console/flowview.py
@@ -574,9 +574,8 @@ class FlowView(common.WWrap):
else:
if not self.flow.response:
self.flow.response = HTTPResponse(
- self.flow.request,
self.flow.request.httpversion,
- 200, "OK", flow.ODictCaseless(), "", None
+ 200, "OK", flow.ODictCaseless(), ""
)
self.flow.response.reply = controller.DummyReply()
conn = self.flow.response
@@ -749,7 +748,7 @@ class FlowView(common.WWrap):
self.master.statusbar.message("")
elif key == "m":
p = list(contentview.view_prompts)
- p.insert(0, ("clear", "c"))
+ p.insert(0, ("Clear", "C"))
self.master.prompt_onekey(
"Display mode",
p,
diff --git a/libmproxy/console/grideditor.py b/libmproxy/console/grideditor.py
index d629ec82..438d0ad7 100644
--- a/libmproxy/console/grideditor.py
+++ b/libmproxy/console/grideditor.py
@@ -123,12 +123,13 @@ class GridWalker(urwid.ListWalker):
except ValueError:
self.editor.master.statusbar.message("Invalid Python-style string encoding.", 1000)
return
-
errors = self.lst[self.focus][1]
emsg = self.editor.is_error(self.focus_col, val)
if emsg:
self.editor.master.statusbar.message(emsg, 1000)
errors.add(self.focus_col)
+ else:
+ errors.discard(self.focus_col)
row = list(self.lst[self.focus][0])
row[self.focus_col] = val
@@ -320,9 +321,11 @@ class GridEditor(common.WWrap):
elif key == "d":
self.walker.delete_focus()
elif key == "r":
- self.master.path_prompt("Read file: ", "", self.read_file)
+ if self.walker.get_current_value() is not None:
+ self.master.path_prompt("Read file: ", "", self.read_file)
elif key == "R":
- self.master.path_prompt("Read unescaped file: ", "", self.read_file, True)
+ if self.walker.get_current_value() is not None:
+ self.master.path_prompt("Read unescaped file: ", "", self.read_file, True)
elif key == "e":
o = self.walker.get_current_value()
if o is not None:
@@ -495,8 +498,8 @@ class ScriptEditor(GridEditor):
return str(v)
-class IgnoreEditor(GridEditor):
- title = "Editing ignore patterns"
+class HostPatternEditor(GridEditor):
+ title = "Editing host patterns"
columns = 1
headings = ("Regex (matched on hostname:port / ip:port)",)
diff --git a/libmproxy/console/help.py b/libmproxy/console/help.py
index bdcf3fd9..27288a36 100644
--- a/libmproxy/console/help.py
+++ b/libmproxy/console/help.py
@@ -119,6 +119,7 @@ class HelpView(urwid.ListBox):
("s", "add/remove scripts"),
("S", "server replay"),
("t", "set sticky cookie expression"),
+ ("T", "set tcp proxying pattern"),
("u", "set sticky auth expression"),
]
text.extend(common.format_keyvals(keys, key="key", val="text", indent=4))
diff --git a/libmproxy/dump.py b/libmproxy/dump.py
index ccb2b5b5..0d9432c9 100644
--- a/libmproxy/dump.py
+++ b/libmproxy/dump.py
@@ -1,10 +1,13 @@
from __future__ import absolute_import
-import sys, os
+import sys
+import os
import netlib.utils
from . import flow, filt, utils
from .protocol import http
-class DumpError(Exception): pass
+
+class DumpError(Exception):
+ pass
class Options(object):
@@ -37,6 +40,7 @@ class Options(object):
"replay_ignore_content",
"replay_ignore_params",
]
+
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
@@ -71,7 +75,7 @@ class DumpMaster(flow.FlowMaster):
self.anticache = options.anticache
self.anticomp = options.anticomp
self.showhost = options.showhost
- self.replay_ignore_params = options.replay_ignore_params
+ self.replay_ignore_params = options.replay_ignore_params
self.replay_ignore_content = options.replay_ignore_content
self.refresh_server_playback = options.refresh_server_playback
@@ -88,7 +92,6 @@ class DumpMaster(flow.FlowMaster):
if options.stickyauth:
self.set_stickyauth(options.stickyauth)
-
if options.wfile:
path = os.path.expanduser(options.wfile)
try:
@@ -152,7 +155,7 @@ class DumpMaster(flow.FlowMaster):
return flows
def add_event(self, e, level="info"):
- needed = dict(error=1, info=1, debug=2).get(level, 1)
+ needed = dict(error=0, info=1, debug=2).get(level, 1)
if self.o.verbosity >= needed:
print >> self.outfile, e
self.outfile.flush()
@@ -202,7 +205,7 @@ class DumpMaster(flow.FlowMaster):
elif self.o.flow_detail >= 3:
print >> self.outfile, str_request(f, self.showhost)
print >> self.outfile, self.indent(4, f.request.headers)
- if utils.isBin(f.request.content):
+ if f.request.content != http.CONTENT_MISSING and utils.isBin(f.request.content):
d = netlib.utils.hexdump(f.request.content)
d = "\n".join("%s\t%s %s"%i for i in d)
print >> self.outfile, self.indent(4, d)
diff --git a/libmproxy/filt.py b/libmproxy/filt.py
index 7d2bd737..5d259096 100644
--- a/libmproxy/filt.py
+++ b/libmproxy/filt.py
@@ -343,7 +343,9 @@ bnf = _make()
def parse(s):
try:
- return bnf.parseString(s, parseAll=True)[0]
+ filt = bnf.parseString(s, parseAll=True)[0]
+ filt.pattern = s
+ return filt
except pp.ParseException:
return None
except ValueError:
diff --git a/libmproxy/flow.py b/libmproxy/flow.py
index 440798bc..a6bf17d8 100644
--- a/libmproxy/flow.py
+++ b/libmproxy/flow.py
@@ -11,7 +11,7 @@ import netlib.http
from . import controller, protocol, tnetstring, filt, script, version
from .onboarding import app
from .protocol import http, handle
-from .proxy.config import parse_host_pattern
+from .proxy.config import HostMatcher
import urlparse
ODict = odict.ODict
@@ -27,7 +27,12 @@ class AppRegistry:
Add a WSGI app to the registry, to be served for requests to the
specified domain, on the specified port.
"""
- self.apps[(domain, port)] = wsgi.WSGIAdaptor(app, domain, port, version.NAMEVERSION)
+ self.apps[(domain, port)] = wsgi.WSGIAdaptor(
+ app,
+ domain,
+ port,
+ version.NAMEVERSION
+ )
def get(self, request):
"""
@@ -72,7 +77,8 @@ class ReplaceHooks:
def get_specs(self):
"""
- Retrieve the hook specifcations. Returns a list of (fpatt, rex, s) tuples.
+ Retrieve the hook specifcations. Returns a list of (fpatt, rex, s)
+ tuples.
"""
return [i[:3] for i in self.lst]
@@ -119,7 +125,8 @@ class SetHeaders:
def get_specs(self):
"""
- Retrieve the hook specifcations. Returns a list of (fpatt, rex, s) tuples.
+ Retrieve the hook specifcations. Returns a list of (fpatt, rex, s)
+ tuples.
"""
return [i[:3] for i in self.lst]
@@ -162,6 +169,7 @@ class ClientPlaybackState:
def __init__(self, flows, exit):
self.flows, self.exit = flows, exit
self.current = None
+ self.testing = False # Disables actual replay for testing.
def count(self):
return len(self.flows)
@@ -179,18 +187,16 @@ class ClientPlaybackState:
if flow is self.current:
self.current = None
- def tick(self, master, testing=False):
- """
- testing: Disables actual replay for testing.
- """
+ def tick(self, master):
if self.flows and not self.current:
- n = self.flows.pop(0)
- n.reply = controller.DummyReply()
- self.current = master.handle_request(n)
- if not testing and not self.current.response:
- master.replay_request(self.current) # pragma: no cover
- elif self.current.response:
- master.handle_response(self.current)
+ self.current = self.flows.pop(0).copy()
+ if not self.testing:
+ master.replay_request(self.current)
+ else:
+ self.current.reply = controller.DummyReply()
+ master.handle_request(self.current)
+ if self.current.response:
+ master.handle_response(self.current)
class ServerPlaybackState:
@@ -219,9 +225,10 @@ class ServerPlaybackState:
queriesArray = urlparse.parse_qsl(query)
filtered = []
+ ignore_params = self.ignore_params or []
for p in queriesArray:
- if p[0] not in self.ignore_params:
- filtered.append(p)
+ if p[0] not in ignore_params:
+ filtered.append(p)
key = [
str(r.host),
@@ -339,11 +346,13 @@ class State(object):
# These are compiled filt expressions:
self._limit = None
self.intercept = None
- self._limit_txt = None
@property
def limit_txt(self):
- return self._limit_txt
+ if self._limit:
+ return self._limit.pattern
+ else:
+ return None
def flow_count(self):
return len(self._flow_list)
@@ -362,6 +371,8 @@ class State(object):
"""
Add a request to the state. Returns the matching flow.
"""
+ if flow in self._flow_list: # catch flow replay
+ return flow
self._flow_list.append(flow)
if flow.match(self._limit):
self.view.append(flow)
@@ -398,10 +409,8 @@ class State(object):
if not f:
return "Invalid filter expression."
self._limit = f
- self._limit_txt = txt
else:
self._limit = None
- self._limit_txt = None
self.recalculate_view()
def set_intercept(self, txt):
@@ -465,7 +474,7 @@ class FlowMaster(controller.Master):
self.refresh_server_playback = False
self.replacehooks = ReplaceHooks()
self.setheaders = SetHeaders()
- self.replay_ignore_params = False
+ self.replay_ignore_params = False
self.replay_ignore_content = None
@@ -515,11 +524,17 @@ class FlowMaster(controller.Master):
for script in self.scripts:
self.run_single_script_hook(script, name, *args, **kwargs)
- def get_ignore(self):
- return [i.pattern for i in self.server.config.ignore]
+ def get_ignore_filter(self):
+ return self.server.config.check_ignore.patterns
+
+ def set_ignore_filter(self, host_patterns):
+ self.server.config.check_ignore = HostMatcher(host_patterns)
- def set_ignore(self, ignore):
- self.server.config.ignore = parse_host_pattern(ignore)
+ def get_tcp_filter(self):
+ return self.server.config.check_tcp.patterns
+
+ def set_tcp_filter(self, host_patterns):
+ self.server.config.check_tcp = HostMatcher(host_patterns)
def set_stickycookie(self, txt):
if txt:
@@ -601,7 +616,7 @@ class FlowMaster(controller.Master):
]
if all(e):
self.shutdown()
- self.client_playback.tick(self, timeout)
+ self.client_playback.tick(self)
return controller.Master.tick(self, q, timeout)
@@ -612,6 +627,11 @@ class FlowMaster(controller.Master):
"""
Loads a flow, and returns a new flow object.
"""
+
+ if self.server and self.server.config.mode == "reverse":
+ f.request.host, f.request.port = self.server.config.mode.dst[2:]
+ f.request.scheme = "https" if self.server.config.mode.dst[1] else "http"
+
f.reply = controller.DummyReply()
if f.request:
self.handle_request(f)
@@ -656,6 +676,8 @@ class FlowMaster(controller.Master):
"""
Returns None if successful, or error message if not.
"""
+ if f.live:
+ return "Can't replay request which is still live..."
if f.intercepting:
return "Can't replay while intercepting..."
if f.request.content == http.CONTENT_MISSING:
@@ -705,7 +727,11 @@ class FlowMaster(controller.Master):
if f.live:
app = self.apps.get(f.request)
if app:
- err = app.serve(f, f.client_conn.wfile, **{"mitmproxy.master": self})
+ err = app.serve(
+ f,
+ f.client_conn.wfile,
+ **{"mitmproxy.master": self}
+ )
if err:
self.add_event("Error in wsgi app. %s"%err, "error")
f.reply(protocol.KILL)
@@ -720,8 +746,12 @@ class FlowMaster(controller.Master):
def handle_responseheaders(self, f):
self.run_script_hook("responseheaders", f)
- if self.stream_large_bodies:
- self.stream_large_bodies.run(f, False)
+ try:
+ if self.stream_large_bodies:
+ self.stream_large_bodies.run(f, False)
+ except netlib.http.HttpError:
+ f.reply(protocol.KILL)
+ return
f.reply()
return f
@@ -755,7 +785,6 @@ class FlowMaster(controller.Master):
self.stream = None
-
class FlowWriter:
def __init__(self, fo):
self.fo = fo
@@ -787,7 +816,7 @@ class FlowReader:
v = ".".join(str(i) for i in data["version"])
raise FlowReadError("Incompatible serialized data version: %s"%v)
off = self.fo.tell()
- yield handle.protocols[data["conntype"]]["flow"].from_state(data)
+ yield handle.protocols[data["type"]]["flow"].from_state(data)
except ValueError, v:
# Error is due to EOF
if self.fo.tell() == off and self.fo.read() == '':
diff --git a/libmproxy/main.py b/libmproxy/main.py
index 2d6a0119..e5b7f56b 100644
--- a/libmproxy/main.py
+++ b/libmproxy/main.py
@@ -1,5 +1,4 @@
from __future__ import print_function, absolute_import
-import argparse
import os
import signal
import sys
@@ -9,27 +8,43 @@ from .proxy import process_proxy_options, ProxyServerError
from .proxy.server import DummyServer, ProxyServer
+# This file is not included in coverage analysis or tests - anything that can be
+# tested should live elsewhere.
+
def check_versions():
"""
- Having installed a wrong version of pyOpenSSL or netlib is unfortunately a very common source of error.
- Check before every start that both versions are somewhat okay.
+ Having installed a wrong version of pyOpenSSL or netlib is unfortunately a
+ very common source of error. Check before every start that both versions are
+ somewhat okay.
"""
- # We don't introduce backward-incompatible changes in patch versions. Only consider major and minor version.
+ # We don't introduce backward-incompatible changes in patch versions. Only
+ # consider major and minor version.
if netlib.version.IVERSION[:2] != version.IVERSION[:2]:
print(
"Warning: You are using mitmdump %s with netlib %s. "
- "Most likely, that doesn't work - please upgrade!" % (version.VERSION, netlib.version.VERSION),
- file=sys.stderr)
- import OpenSSL, inspect
-
+ "Most likely, that won't work - please upgrade!" % (
+ version.VERSION, netlib.version.VERSION
+ ),
+ file=sys.stderr
+ )
+ import OpenSSL
+ import inspect
v = tuple([int(x) for x in OpenSSL.__version__.split(".")][:2])
if v < (0, 14):
- print("You are using an outdated version of pyOpenSSL: mitmproxy requires pyOpenSSL 0.14 or greater.",
- file=sys.stderr)
- # Some users apparently have multiple versions of pyOpenSSL installed. Report which one we got.
+ print(
+ "You are using an outdated version of pyOpenSSL:"
+ " mitmproxy requires pyOpenSSL 0.14 or greater.",
+ file=sys.stderr
+ )
+ # 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 %s installation is located at %s" % (OpenSSL.__version__, pyopenssl_path),
- file=sys.stderr)
+ print(
+ "Your pyOpenSSL %s installation is located at %s" % (
+ OpenSSL.__version__, pyopenssl_path
+ ),
+ file=sys.stderr
+ )
sys.exit(1)
@@ -38,8 +53,14 @@ def assert_utf8_env():
for i in ["LANG", "LC_CTYPE", "LC_ALL"]:
spec += os.environ.get(i, "").lower()
if "utf" not in spec:
- print("Error: mitmproxy requires a UTF console environment.", file=sys.stderr)
- print("Set your LANG enviroment variable to something like en_US.UTF-8", file=sys.stderr)
+ print(
+ "Error: mitmproxy requires a UTF console environment.",
+ file=sys.stderr
+ )
+ print(
+ "Set your LANG enviroment variable to something like en_US.UTF-8",
+ file=sys.stderr
+ )
sys.exit(1)
@@ -54,34 +75,13 @@ def get_server(dummy_server, options):
sys.exit(1)
-def mitmproxy_cmdline():
- # Don't import libmproxy.console for mitmdump, urwid is not available on all platforms.
+def mitmproxy(): # pragma: nocover
from . import console
- from .console import palettes
-
- parser = argparse.ArgumentParser(usage="%(prog)s [options]")
- parser.add_argument('--version', action='version', version=version.NAMEVERSION)
- cmdline.common_options(parser)
- parser.add_argument(
- "--palette", type=str, default="dark",
- action="store", dest="palette",
- help="Select color palette: " + ", ".join(palettes.palettes.keys())
- )
- parser.add_argument(
- "-e",
- action="store_true", dest="eventlog",
- help="Show event log."
- )
- group = parser.add_argument_group(
- "Filters",
- "See help in mitmproxy for filter expression syntax."
- )
- group.add_argument(
- "-i", "--intercept", action="store",
- type=str, dest="intercept", default=None,
- help="Intercept filter expression."
- )
+ check_versions()
+ assert_utf8_env()
+
+ parser = cmdline.mitmproxy()
options = parser.parse_args()
if options.quiet:
options.verbose = 0
@@ -92,15 +92,6 @@ def mitmproxy_cmdline():
console_options.eventlog = options.eventlog
console_options.intercept = options.intercept
- return console_options, proxy_config
-
-
-def mitmproxy(): # pragma: nocover
- from . import console
-
- check_versions()
- assert_utf8_env()
- console_options, proxy_config = mitmproxy_cmdline()
server = get_server(console_options.no_server, proxy_config)
m = console.ConsoleMaster(server, console_options)
@@ -110,24 +101,12 @@ def mitmproxy(): # pragma: nocover
pass
-def mitmdump_cmdline():
+def mitmdump(): # pragma: nocover
from . import dump
- parser = argparse.ArgumentParser(usage="%(prog)s [options] [filter]")
- parser.add_argument('--version', action='version', version="mitmdump" + " " + version.VERSION)
- cmdline.common_options(parser)
- parser.add_argument(
- "--keepserving",
- action="store_true", dest="keepserving", default=False,
- help="Continue serving after client playback or file read. We exit by default."
- )
- parser.add_argument(
- "-d",
- action="count", dest="flow_detail", default=1,
- help="Increase flow detail display level. Can be passed multiple times."
- )
- parser.add_argument('args', nargs=argparse.REMAINDER)
+ check_versions()
+ parser = cmdline.mitmdump()
options = parser.parse_args()
if options.quiet:
options.verbose = 0
@@ -139,14 +118,6 @@ def mitmdump_cmdline():
dump_options.keepserving = options.keepserving
dump_options.filtstr = " ".join(options.args) if options.args else None
- return dump_options, proxy_config
-
-
-def mitmdump(): # pragma: nocover
- from . import dump
-
- check_versions()
- dump_options, proxy_config = mitmdump_cmdline()
server = get_server(dump_options.no_server, proxy_config)
try:
@@ -164,44 +135,11 @@ def mitmdump(): # pragma: nocover
pass
-def mitmweb_cmdline():
+def mitmweb(): # pragma: nocover
from . import web
- parser = argparse.ArgumentParser(usage="%(prog)s [options]")
- parser.add_argument(
- '--version',
- action='version',
- version="mitmweb" + " " + version.VERSION
- )
-
- group = parser.add_argument_group("Mitmweb")
- group.add_argument(
- "--wport",
- action="store", type=int, dest="wport", default=8081,
- metavar="PORT",
- help="Mitmweb port."
- )
- group.add_argument(
- "--wiface",
- action="store", dest="wiface", default="127.0.0.1",
- metavar="IFACE",
- help="Mitmweb interface."
- )
- group.add_argument(
- "--wdebug",
- action="store_true", dest="wdebug",
- help="Turn on mitmweb debugging"
- )
-
- cmdline.common_options(parser)
- group = parser.add_argument_group(
- "Filters",
- "See help in mitmproxy for filter expression syntax."
- )
- group.add_argument(
- "-i", "--intercept", action="store",
- type=str, dest="intercept", default=None,
- help="Intercept filter expression."
- )
+
+ check_versions()
+ parser = cmdline.mitmweb()
options = parser.parse_args()
if options.quiet:
@@ -213,14 +151,7 @@ def mitmweb_cmdline():
web_options.wdebug = options.wdebug
web_options.wiface = options.wiface
web_options.wport = options.wport
- return web_options, proxy_config
-
-def mitmweb(): # pragma: nocover
- from . import web
-
- check_versions()
- web_options, proxy_config = mitmweb_cmdline()
server = get_server(web_options.no_server, proxy_config)
m = web.WebMaster(server, web_options)
diff --git a/libmproxy/onboarding/app.py b/libmproxy/onboarding/app.py
index 9b5db38a..4023fae2 100644
--- a/libmproxy/onboarding/app.py
+++ b/libmproxy/onboarding/app.py
@@ -18,12 +18,12 @@ def index():
@mapp.route("/cert/pem")
def certs_pem():
- p = os.path.join(master().server.config.confdir, config.CONF_BASENAME + "-ca-cert.pem")
+ p = os.path.join(master().server.config.cadir, config.CONF_BASENAME + "-ca-cert.pem")
return flask.Response(open(p, "rb").read(), mimetype='application/x-x509-ca-cert')
@mapp.route("/cert/p12")
def certs_p12():
- p = os.path.join(master().server.config.confdir, config.CONF_BASENAME + "-ca-cert.p12")
+ p = os.path.join(master().server.config.cadir, config.CONF_BASENAME + "-ca-cert.p12")
return flask.Response(open(p, "rb").read(), mimetype='application/x-pkcs12')
diff --git a/libmproxy/onboarding/templates/index.html b/libmproxy/onboarding/templates/index.html
index 50cfd5db..65fda5d2 100644
--- a/libmproxy/onboarding/templates/index.html
+++ b/libmproxy/onboarding/templates/index.html
@@ -1,5 +1,5 @@
{% extends "frame.html" %}
-{% block body %}
+{% block body %}
<center>
<h2> Click to install the mitmproxy certificate: </h2>
@@ -23,4 +23,13 @@
</div>
</div>
+<hr/>
+<div class="text-center">
+ Other mitmproxy users cannot intercept your connection.
+</div>
+<div class="text-center text-muted">
+ This page is served by your local mitmproxy instance. The certificate you are about to install has been uniquely generated on mitmproxy's first run and is not shared
+ between mitmproxy installations.
+</div>
+
{% endblock %}
diff --git a/libmproxy/platform/windows.py b/libmproxy/platform/windows.py
index ddbbed52..066a377d 100644
--- a/libmproxy/platform/windows.py
+++ b/libmproxy/platform/windows.py
@@ -1,4 +1,4 @@
-import argparse
+import configargparse
import cPickle as pickle
from ctypes import byref, windll, Structure
from ctypes.wintypes import DWORD
@@ -361,7 +361,7 @@ class TransparentProxy(object):
if __name__ == "__main__":
- parser = argparse.ArgumentParser(description="Windows Transparent Proxy")
+ parser = configargparse.ArgumentParser(description="Windows Transparent Proxy")
parser.add_argument('--mode', choices=['forward', 'local', 'both'], default="both",
help='redirection operation mode: "forward" to only redirect forwarded packets, '
'"local" to only redirect packets originating from the local machine')
diff --git a/libmproxy/protocol/http.py b/libmproxy/protocol/http.py
index de5f9950..49f5e8c0 100644
--- a/libmproxy/protocol/http.py
+++ b/libmproxy/protocol/http.py
@@ -18,12 +18,17 @@ HDR_FORM_URLENCODED = "application/x-www-form-urlencoded"
CONTENT_MISSING = 0
+class KillSignal(Exception):
+ pass
+
+
def get_line(fp):
"""
Get a line, possibly preceded by a blank.
"""
line = fp.readline()
- if line == "\r\n" or line == "\n": # Possible leftover from previous message
+ if line == "\r\n" or line == "\n":
+ # Possible leftover from previous message
line = fp.readline()
if line == "":
raise tcp.NetLibDisconnect()
@@ -237,25 +242,47 @@ class HTTPRequest(HTTPMessage):
is content associated, but not present. CONTENT_MISSING evaluates
to False to make checking for the presence of content natural.
- form_in: The request form which mitmproxy has received. The following values are possible:
- - relative (GET /index.html, OPTIONS *) (covers origin form and asterisk form)
- - absolute (GET http://example.com:80/index.html)
- - authority-form (CONNECT example.com:443)
- Details: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-25#section-5.3
+ form_in: The request form which mitmproxy has received. The following
+ values are possible:
+
+ - relative (GET /index.html, OPTIONS *) (covers origin form and
+ asterisk form)
+ - absolute (GET http://example.com:80/index.html)
+ - authority-form (CONNECT example.com:443)
+ Details: http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-25#section-5.3
- form_out: The request form which mitmproxy has send out to the destination
+ form_out: The request form which mitmproxy will send out to the
+ destination
timestamp_start: Timestamp indicating when request transmission started
timestamp_end: Timestamp indicating when request transmission ended
"""
- def __init__(self, form_in, method, scheme, host, port, path, httpversion, headers,
- content, timestamp_start=None, timestamp_end=None, form_out=None):
+ def __init__(
+ self,
+ form_in,
+ method,
+ scheme,
+ host,
+ port,
+ path,
+ httpversion,
+ headers,
+ content,
+ timestamp_start=None,
+ timestamp_end=None,
+ form_out=None
+ ):
assert isinstance(headers, ODictCaseless) or not headers
- HTTPMessage.__init__(self, httpversion, headers, content, timestamp_start,
- timestamp_end)
-
+ HTTPMessage.__init__(
+ self,
+ httpversion,
+ headers,
+ content,
+ timestamp_start,
+ timestamp_end
+ )
self.form_in = form_in
self.method = method
self.scheme = scheme
@@ -308,30 +335,43 @@ class HTTPRequest(HTTPMessage):
request_line = get_line(rfile)
- if hasattr(rfile, "first_byte_timestamp"): # more accurate timestamp_start
+ if hasattr(rfile, "first_byte_timestamp"):
+ # more accurate timestamp_start
timestamp_start = rfile.first_byte_timestamp
request_line_parts = http.parse_init(request_line)
if not request_line_parts:
- raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line))
+ raise http.HttpError(
+ 400,
+ "Bad HTTP request line: %s" % repr(request_line)
+ )
method, path, httpversion = request_line_parts
if path == '*' or path.startswith("/"):
form_in = "relative"
if not netlib.utils.isascii(path):
- raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line))
+ raise http.HttpError(
+ 400,
+ "Bad HTTP request line: %s" % repr(request_line)
+ )
elif method.upper() == 'CONNECT':
form_in = "authority"
r = http.parse_init_connect(request_line)
if not r:
- raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line))
+ raise http.HttpError(
+ 400,
+ "Bad HTTP request line: %s" % repr(request_line)
+ )
host, port, _ = r
path = None
else:
form_in = "absolute"
r = http.parse_init_proxy(request_line)
if not r:
- raise http.HttpError(400, "Bad HTTP request line: %s" % repr(request_line))
+ raise http.HttpError(
+ 400,
+ "Bad HTTP request line: %s" % repr(request_line)
+ )
_, scheme, host, port, path, _ = r
headers = http.read_headers(rfile)
@@ -343,50 +383,69 @@ class HTTPRequest(HTTPMessage):
method, None, True)
timestamp_end = utils.timestamp()
- return HTTPRequest(form_in, method, scheme, host, port, path, httpversion, headers,
- content, timestamp_start, timestamp_end)
+ return HTTPRequest(
+ form_in,
+ method,
+ scheme,
+ host,
+ port,
+ path,
+ httpversion,
+ headers,
+ content,
+ timestamp_start,
+ timestamp_end
+ )
def _assemble_first_line(self, form=None):
form = form or self.form_out
if form == "relative":
- path = self.path if self.method != "OPTIONS" else "*"
- request_line = '%s %s HTTP/%s.%s' % \
- (self.method, path, self.httpversion[0], self.httpversion[1])
+ request_line = '%s %s HTTP/%s.%s' % (
+ self.method, self.path, self.httpversion[0], self.httpversion[1]
+ )
elif form == "authority":
- request_line = '%s %s:%s HTTP/%s.%s' % (self.method, self.host, self.port,
- self.httpversion[0], self.httpversion[1])
+ request_line = '%s %s:%s HTTP/%s.%s' % (
+ self.method, self.host, self.port, self.httpversion[0],
+ self.httpversion[1]
+ )
elif form == "absolute":
- request_line = '%s %s://%s:%s%s HTTP/%s.%s' % \
- (self.method, self.scheme, self.host, self.port, self.path,
- self.httpversion[0], self.httpversion[1])
+ request_line = '%s %s://%s:%s%s HTTP/%s.%s' % (
+ self.method, self.scheme, self.host,
+ self.port, self.path, self.httpversion[0],
+ self.httpversion[1]
+ )
else:
raise http.HttpError(400, "Invalid request form")
return request_line
+ # This list is adopted legacy code.
+ # We probably don't need to strip off keep-alive.
+ _headers_to_strip_off = ['Proxy-Connection',
+ 'Keep-Alive',
+ 'Connection',
+ 'Transfer-Encoding',
+ 'Upgrade']
+
def _assemble_headers(self):
headers = self.headers.copy()
- for k in ['Proxy-Connection',
- 'Keep-Alive',
- 'Connection',
- 'Transfer-Encoding']:
+ for k in self._headers_to_strip_off:
del headers[k]
- if headers["Upgrade"] == ["h2c"]: # Suppress HTTP2 https://http2.github.io/http2-spec/index.html#discover-http
- del headers["Upgrade"]
- if not 'host' in headers and self.scheme and self.host and self.port:
+ if 'host' not in headers and self.scheme and self.host and self.port:
headers["Host"] = [utils.hostport(self.scheme,
self.host,
self.port)]
- if self.content:
+ # If content is defined (i.e. not None or CONTENT_MISSING), we always add a content-length header.
+ if self.content or self.content == "":
headers["Content-Length"] = [str(len(self.content))]
- elif 'Transfer-Encoding' in self.headers: # content-length for e.g. chuncked transfer-encoding with no content
- headers["Content-Length"] = ["0"]
return str(headers)
def _assemble_head(self, form=None):
- return "%s\r\n%s\r\n" % (self._assemble_first_line(form), self._assemble_headers())
+ return "%s\r\n%s\r\n" % (
+ self._assemble_first_line(form), self._assemble_headers()
+ )
def assemble(self, form=None):
"""
@@ -396,7 +455,10 @@ class HTTPRequest(HTTPMessage):
Raises an Exception if the request cannot be assembled.
"""
if self.content == CONTENT_MISSING:
- raise proxy.ProxyError(502, "Cannot assemble flow with CONTENT_MISSING")
+ raise proxy.ProxyError(
+ 502,
+ "Cannot assemble flow with CONTENT_MISSING"
+ )
head = self._assemble_head(form)
if self.content:
return head + self.content
@@ -644,7 +706,9 @@ class HTTPResponse(HTTPMessage):
return "<HTTPResponse: {code} {msg} ({contenttype}, {size})>".format(
code=self.code,
msg=self.msg,
- contenttype=self.headers.get_first("content-type", "unknown content type"),
+ contenttype=self.headers.get_first(
+ "content-type", "unknown content type"
+ ),
size=size
)
@@ -665,7 +729,8 @@ class HTTPResponse(HTTPMessage):
body_size_limit,
include_body=include_body)
- if hasattr(rfile, "first_byte_timestamp"): # more accurate timestamp_start
+ if hasattr(rfile, "first_byte_timestamp"):
+ # more accurate timestamp_start
timestamp_start = rfile.first_byte_timestamp
if include_body:
@@ -687,26 +752,30 @@ class HTTPResponse(HTTPMessage):
return 'HTTP/%s.%s %s %s' % \
(self.httpversion[0], self.httpversion[1], self.code, self.msg)
+ _headers_to_strip_off = ['Proxy-Connection',
+ 'Alternate-Protocol',
+ 'Alt-Svc']
+
def _assemble_headers(self, preserve_transfer_encoding=False):
headers = self.headers.copy()
- for k in ['Proxy-Connection',
- 'Alternate-Protocol',
- 'Alt-Svc']:
+ for k in self._headers_to_strip_off:
del headers[k]
if not preserve_transfer_encoding:
del headers['Transfer-Encoding']
- if self.content:
+ # If content is defined (i.e. not None or CONTENT_MISSING), we always add a content-length header.
+ if self.content or self.content == "":
headers["Content-Length"] = [str(len(self.content))]
- # add content-length for chuncked transfer-encoding with no content
- elif not preserve_transfer_encoding and 'Transfer-Encoding' in self.headers:
- headers["Content-Length"] = ["0"]
return str(headers)
def _assemble_head(self, preserve_transfer_encoding=False):
return '%s\r\n%s\r\n' % (
- self._assemble_first_line(), self._assemble_headers(preserve_transfer_encoding=preserve_transfer_encoding))
+ self._assemble_first_line(),
+ self._assemble_headers(
+ preserve_transfer_encoding=preserve_transfer_encoding
+ )
+ )
def assemble(self):
"""
@@ -716,7 +785,10 @@ class HTTPResponse(HTTPMessage):
Raises an Exception if the request cannot be assembled.
"""
if self.content == CONTENT_MISSING:
- raise proxy.ProxyError(502, "Cannot assemble flow with CONTENT_MISSING")
+ raise proxy.ProxyError(
+ 502,
+ "Cannot assemble flow with CONTENT_MISSING"
+ )
head = self._assemble_head()
if self.content:
return head + self.content
@@ -783,8 +855,9 @@ class HTTPResponse(HTTPMessage):
pairs = [pair.partition("=") for pair in header.split(';')]
cookie_name = pairs[0][0] # the key of the first key/value pairs
cookie_value = pairs[0][2] # the value of the first key/value pairs
- cookie_parameters = {key.strip().lower(): value.strip() for key, sep, value in
- pairs[1:]}
+ cookie_parameters = {
+ key.strip().lower(): value.strip() for key, sep, value in pairs[1:]
+ }
cookies.append((cookie_name, (cookie_value, cookie_parameters)))
return dict(cookies)
@@ -817,7 +890,8 @@ class HTTPFlow(Flow):
self.response = None
"""@type: HTTPResponse"""
- self.intercepting = False # FIXME: Should that rather be an attribute of Flow?
+ # FIXME: Should that rather be an attribute of Flow?
+ self.intercepting = False
_stateobject_attributes = Flow._stateobject_attributes.copy()
_stateobject_attributes.update(
@@ -905,7 +979,9 @@ class HTTPFlow(Flow):
class HttpAuthenticationError(Exception):
def __init__(self, auth_headers=None):
- super(HttpAuthenticationError, self).__init__("Proxy Authentication Required")
+ super(HttpAuthenticationError, self).__init__(
+ "Proxy Authentication Required"
+ )
self.headers = auth_headers
self.code = 407
@@ -937,16 +1013,23 @@ class HTTPHandler(ProtocolHandler):
try:
self.c.server_conn.send(request_raw)
# Only get the headers at first...
- flow.response = HTTPResponse.from_stream(self.c.server_conn.rfile, flow.request.method,
- body_size_limit=self.c.config.body_size_limit,
- include_body=False)
+ flow.response = HTTPResponse.from_stream(
+ self.c.server_conn.rfile, flow.request.method,
+ body_size_limit=self.c.config.body_size_limit,
+ include_body=False
+ )
break
except (tcp.NetLibDisconnect, http.HttpErrorConnClosed), v:
- self.c.log("error in server communication: %s" % repr(v), level="debug")
+ self.c.log(
+ "error in server communication: %s" % repr(v),
+ level="debug"
+ )
if attempt == 0:
- # In any case, we try to reconnect at least once.
- # This is necessary because it might be possible that we already initiated an upstream connection
- # after clientconnect that has already been expired, e.g consider the following event log:
+ # In any case, we try to reconnect at least once. This is
+ # necessary because it might be possible that we already
+ # initiated an upstream connection after clientconnect that
+ # has already been expired, e.g consider the following event
+ # log:
# > clientconnect (transparent mode destination known)
# > serverconnect
# > read n% of large request
@@ -959,19 +1042,21 @@ class HTTPHandler(ProtocolHandler):
# call the appropriate script hook - this is an opportunity for an
# inline script to set flow.stream = True
- self.c.channel.ask("responseheaders", flow)
-
- # now get the rest of the request body, if body still needs to be read
- # but not streaming this response
- if flow.response.stream:
- flow.response.content = CONTENT_MISSING
+ flow = self.c.channel.ask("responseheaders", flow)
+ if flow is None or flow == KILL:
+ raise KillSignal()
else:
- flow.response.content = http.read_http_body(
- self.c.server_conn.rfile, flow.response.headers,
- self.c.config.body_size_limit,
- flow.request.method, flow.response.code, False
- )
- flow.response.timestamp_end = utils.timestamp()
+ # now get the rest of the request body, if body still needs to be
+ # read but not streaming this response
+ if flow.response.stream:
+ flow.response.content = CONTENT_MISSING
+ else:
+ flow.response.content = http.read_http_body(
+ self.c.server_conn.rfile, flow.response.headers,
+ self.c.config.body_size_limit,
+ flow.request.method, flow.response.code, False
+ )
+ flow.response.timestamp_end = utils.timestamp()
def handle_flow(self):
flow = HTTPFlow(self.c.client_conn, self.c.server_conn, self.live)
@@ -1001,10 +1086,10 @@ class HTTPHandler(ProtocolHandler):
# sent through to the Master.
flow.request = req
request_reply = self.c.channel.ask("request", flow)
- self.process_server_address(flow) # The inline script may have changed request.host
-
if request_reply is None or request_reply == KILL:
- return False
+ raise KillSignal()
+
+ self.process_server_address(flow) # The inline script may have changed request.host
if isinstance(request_reply, HTTPResponse):
flow.response = request_reply
@@ -1018,7 +1103,7 @@ class HTTPHandler(ProtocolHandler):
self.c.log("response", "debug", [flow.response._assemble_first_line()])
response_reply = self.c.channel.ask("response", flow)
if response_reply is None or response_reply == KILL:
- return False
+ raise KillSignal()
self.send_response_to_client(flow)
@@ -1050,15 +1135,27 @@ class HTTPHandler(ProtocolHandler):
flow.live.restore_server()
return True # Next flow please.
- except (HttpAuthenticationError, http.HttpError, proxy.ProxyError, tcp.NetLibError), e:
+ except (
+ HttpAuthenticationError,
+ http.HttpError,
+ proxy.ProxyError,
+ tcp.NetLibError,
+ ), e:
self.handle_error(e, flow)
+ except KillSignal:
+ self.c.log("Connection killed", "info")
finally:
flow.live = None # Connection is not live anymore.
return False
def handle_server_reconnect(self, state):
if state["state"] == "connect":
- send_connect_request(self.c.server_conn, state["host"], state["port"], update_state=False)
+ send_connect_request(
+ self.c.server_conn,
+ state["host"],
+ state["port"],
+ update_state=False
+ )
else: # pragma: nocover
raise RuntimeError("Unknown State: %s" % state["state"])
@@ -1079,14 +1176,14 @@ class HTTPHandler(ProtocolHandler):
if message:
self.c.log(message, level="info")
if message_debug:
- self.c.log(message, level="debug")
+ self.c.log(message_debug, level="debug")
if flow:
- # TODO: no flows without request or with both request and response at the moment.
+ # TODO: no flows without request or with both request and response
+ # at the moment.
if flow.request and not flow.response:
flow.error = Error(message or message_debug)
self.c.channel.ask("error", flow)
-
try:
code = getattr(error, "code", 502)
headers = getattr(error, "headers", None)
@@ -1100,12 +1197,22 @@ class HTTPHandler(ProtocolHandler):
def send_error(self, code, message, headers):
response = http_status.RESPONSES.get(code, "Unknown")
- html_content = '<html><head>\n<title>%d %s</title>\n</head>\n<body>\n%s\n</body>\n</html>' % \
- (code, response, message)
+ html_content = """
+ <html>
+ <head>
+ <title>%d %s</title>
+ </head>
+ <body>%s</body>
+ </html>
+ """ % (code, response, message)
self.c.client_conn.wfile.write("HTTP/1.1 %s %s\r\n" % (code, response))
- self.c.client_conn.wfile.write("Server: %s\r\n" % self.c.config.server_version)
+ self.c.client_conn.wfile.write(
+ "Server: %s\r\n" % self.c.config.server_version
+ )
self.c.client_conn.wfile.write("Content-type: text/html\r\n")
- self.c.client_conn.wfile.write("Content-Length: %d\r\n" % len(html_content))
+ self.c.client_conn.wfile.write(
+ "Content-Length: %d\r\n" % len(html_content)
+ )
if headers:
for key, value in headers.items():
self.c.client_conn.wfile.write("%s: %s\r\n" % (key, value))
@@ -1145,11 +1252,15 @@ class HTTPHandler(ProtocolHandler):
# Now we can process the request.
if request.form_in == "authority":
if self.c.client_conn.ssl_established:
- raise http.HttpError(400, "Must not CONNECT on already encrypted connection")
+ raise http.HttpError(
+ 400,
+ "Must not CONNECT on already encrypted connection"
+ )
if self.c.config.mode == "regular":
self.c.set_server_address((request.host, request.port))
- flow.server_conn = self.c.server_conn # Update server_conn attribute on the flow
+ # Update server_conn attribute on the flow
+ flow.server_conn = self.c.server_conn
self.c.establish_server_connection()
self.c.client_conn.send(
'HTTP/1.1 200 Connection established\r\n' +
@@ -1161,7 +1272,9 @@ class HTTPHandler(ProtocolHandler):
elif self.c.config.mode == "upstream":
return None
else:
- pass # CONNECT should never occur if we don't expect absolute-form requests
+ # CONNECT should never occur if we don't expect absolute-form
+ # requests
+ pass
elif request.form_in == self.expected_form_in:
@@ -1169,61 +1282,77 @@ class HTTPHandler(ProtocolHandler):
if request.form_in == "absolute":
if request.scheme != "http":
- raise http.HttpError(400, "Invalid request scheme: %s" % request.scheme)
+ raise http.HttpError(
+ 400,
+ "Invalid request scheme: %s" % request.scheme
+ )
if self.c.config.mode == "regular":
- # Update info so that an inline script sees the correct value at flow.server_conn
+ # Update info so that an inline script sees the correct
+ # value at flow.server_conn
self.c.set_server_address((request.host, request.port))
flow.server_conn = self.c.server_conn
return None
-
- raise http.HttpError(400, "Invalid HTTP request form (expected: %s, got: %s)" %
- (self.expected_form_in, request.form_in))
+ raise http.HttpError(
+ 400, "Invalid HTTP request form (expected: %s, got: %s)" % (
+ self.expected_form_in, request.form_in
+ )
+ )
def process_server_address(self, flow):
# Depending on the proxy mode, server handling is entirely different
- # We provide a mostly unified API to the user, which needs to be unfiddled here
+ # We provide a mostly unified API to the user, which needs to be
+ # unfiddled here
# ( See also: https://github.com/mitmproxy/mitmproxy/issues/337 )
address = netlib.tcp.Address((flow.request.host, flow.request.port))
ssl = (flow.request.scheme == "https")
if self.c.config.mode == "upstream":
-
- # The connection to the upstream proxy may have a state we may need to take into account.
+ # The connection to the upstream proxy may have a state we may need
+ # to take into account.
connected_to = None
for s in flow.server_conn.state:
if s[0] == "http" and s[1]["state"] == "connect":
connected_to = tcp.Address((s[1]["host"], s[1]["port"]))
- # We need to reconnect if the current flow either requires a (possibly impossible)
- # change to the connection state, e.g. the host has changed but we already CONNECTed somewhere else.
+ # We need to reconnect if the current flow either requires a
+ # (possibly impossible) change to the connection state, e.g. the
+ # host has changed but we already CONNECTed somewhere else.
needs_server_change = (
ssl != self.c.server_conn.ssl_established
or
- (connected_to and address != connected_to) # HTTP proxying is "stateless", CONNECT isn't.
+ # HTTP proxying is "stateless", CONNECT isn't.
+ (connected_to and address != connected_to)
)
if needs_server_change:
# force create new connection to the proxy server to reset state
self.live.change_server(self.c.server_conn.address, force=True)
if ssl:
- send_connect_request(self.c.server_conn, address.host, address.port)
+ send_connect_request(
+ self.c.server_conn,
+ address.host,
+ address.port
+ )
self.c.establish_ssl(server=True)
else:
- # If we're not in upstream mode, we just want to update the host and possibly establish TLS.
- self.live.change_server(address, ssl=ssl) # this is a no op if the addresses match.
+ # If we're not in upstream mode, we just want to update the host and
+ # possibly establish TLS. This is a no op if the addresses match.
+ self.live.change_server(address, ssl=ssl)
flow.server_conn = self.c.server_conn
def send_response_to_client(self, flow):
if not flow.response.stream:
# no streaming:
- # we already received the full response from the server and can send it to the client straight away.
+ # we already received the full response from the server and can send
+ # it to the client straight away.
self.c.client_conn.send(flow.response.assemble())
else:
# streaming:
- # First send the headers and then transfer the response incrementally:
+ # First send the headers and then transfer the response
+ # incrementally:
h = flow.response._assemble_head(preserve_transfer_encoding=True)
self.c.client_conn.send(h)
for chunk in http.read_http_body_chunked(self.c.server_conn.rfile,
@@ -1237,7 +1366,8 @@ class HTTPHandler(ProtocolHandler):
def check_close_connection(self, flow):
"""
- Checks if the connection should be closed depending on the HTTP semantics. Returns True, if so.
+ Checks if the connection should be closed depending on the HTTP
+ semantics. Returns True, if so.
"""
close_connection = (
http.connection_close(flow.request.httpversion, flow.request.headers) or
@@ -1260,20 +1390,39 @@ class HTTPHandler(ProtocolHandler):
Returns False, if the connection should be closed immediately.
"""
address = tcp.Address.wrap(address)
- if self.c.check_ignore_address(address):
+ if self.c.config.check_ignore(address):
self.c.log("Ignore host: %s:%s" % address(), "info")
- TCPHandler(self.c).handle_messages()
+ TCPHandler(self.c, log=False).handle_messages()
return False
else:
self.expected_form_in = "relative"
self.expected_form_out = "relative"
self.skip_authentication = True
- if address.port in self.c.config.ssl_ports:
+ # In practice, nobody issues a CONNECT request to send unencrypted HTTP requests afterwards.
+ # If we don't delegate to TCP mode, we should always negotiate a SSL connection.
+ #
+ # FIXME:
+ # Turns out the previous statement isn't entirely true. Chrome on Windows CONNECTs to :80
+ # if an explicit proxy is configured and a websocket connection should be established.
+ # We don't support websocket at the moment, so it fails anyway, but we should come up with
+ # a better solution to this if we start to support WebSockets.
+ should_establish_ssl = (
+ address.port in self.c.config.ssl_ports
+ or
+ not self.c.config.check_tcp(address)
+ )
+
+ if should_establish_ssl:
self.c.log("Received CONNECT request to SSL port. Upgrading to SSL...", "debug")
self.c.establish_ssl(server=True, client=True)
self.c.log("Upgrade to SSL completed.", "debug")
+ if self.c.config.check_tcp(address):
+ self.c.log("Generic TCP mode for host: %s:%s" % address(), "info")
+ TCPHandler(self.c).handle_messages()
+ return False
+
return True
def authenticate(self, request):
@@ -1297,31 +1446,43 @@ class RequestReplayThread(threading.Thread):
r = self.flow.request
form_out_backup = r.form_out
try:
- # 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:]
- server = ServerConnection(server_address)
- server.connect()
- if r.scheme == "https":
- send_connect_request(server, r.host, r.port)
- server.establish_ssl(self.config.clientcerts, sni=r.host)
- r.form_out = "relative"
- else:
- r.form_out = "absolute"
+ 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:
- server_address = (r.host, r.port)
- server = ServerConnection(server_address)
- server.connect()
- if r.scheme == "https":
- server.establish_ssl(self.config.clientcerts, sni=r.host)
- r.form_out = "relative"
-
- server.send(r.assemble())
- self.flow.response = HTTPResponse.from_stream(server.rfile, r.method,
- body_size_limit=self.config.body_size_limit)
- self.channel.ask("response", self.flow)
- except (proxy.ProxyError, http.HttpError, tcp.NetLibError), v:
+ # 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:]
+ server = ServerConnection(server_address)
+ server.connect()
+ if r.scheme == "https":
+ send_connect_request(server, r.host, r.port)
+ server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni)
+ r.form_out = "relative"
+ else:
+ r.form_out = "absolute"
+ else:
+ server_address = (r.host, r.port)
+ server = ServerConnection(server_address)
+ server.connect()
+ if r.scheme == "https":
+ server.establish_ssl(self.config.clientcerts, sni=self.flow.server_conn.sni)
+ r.form_out = "relative"
+
+ server.send(r.assemble())
+ 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()
+ except (proxy.ProxyError, http.HttpError, tcp.NetLibError) as v:
self.flow.error = Error(repr(v))
self.channel.ask("error", self.flow)
+ except KillSignal:
+ self.channel.tell("log", proxy.Log("Connection killed", "info"))
finally:
r.form_out = form_out_backup
diff --git a/libmproxy/protocol/primitives.py b/libmproxy/protocol/primitives.py
index 519693db..3be1cc45 100644
--- a/libmproxy/protocol/primitives.py
+++ b/libmproxy/protocol/primitives.py
@@ -59,8 +59,8 @@ class Flow(stateobject.StateObject):
A Flow is a collection of objects representing a single transaction.
This class is usually subclassed for each protocol, e.g. HTTPFlow.
"""
- def __init__(self, conntype, client_conn, server_conn, live=None):
- self.conntype = conntype
+ def __init__(self, type, client_conn, server_conn, live=None):
+ self.type = type
self.id = str(uuid.uuid4())
self.client_conn = client_conn
"""@type: ClientConnection"""
@@ -78,7 +78,7 @@ class Flow(stateobject.StateObject):
error=Error,
client_conn=ClientConnection,
server_conn=ServerConnection,
- conntype=str
+ type=str
)
def get_state(self, short=False):
@@ -174,7 +174,7 @@ class LiveConnection(object):
self._backup_server_conn = None
"""@type: libmproxy.proxy.connection.ServerConnection"""
- def change_server(self, address, ssl=None, force=False, persistent_change=False):
+ def change_server(self, address, ssl=None, sni=None, force=False, persistent_change=False):
"""
Change the server connection to the specified address.
@returns:
@@ -183,7 +183,14 @@ class LiveConnection(object):
"""
address = netlib.tcp.Address.wrap(address)
- ssl_mismatch = (ssl is not None and ssl != self.c.server_conn.ssl_established)
+ ssl_mismatch = (
+ ssl is not None and
+ (
+ ssl != self.c.server_conn.ssl_established
+ or
+ (sni is not None and sni != self.c.sni)
+ )
+ )
address_mismatch = (address != self.c.server_conn.address)
if persistent_change:
@@ -212,6 +219,8 @@ class LiveConnection(object):
self.c.set_server_address(address)
self.c.establish_server_connection(ask=False)
+ if sni:
+ self.c.sni = sni
if ssl:
self.c.establish_ssl(server=True)
return True
diff --git a/libmproxy/protocol/tcp.py b/libmproxy/protocol/tcp.py
index a56bf07b..da0c9087 100644
--- a/libmproxy/protocol/tcp.py
+++ b/libmproxy/protocol/tcp.py
@@ -13,6 +13,10 @@ class TCPHandler(ProtocolHandler):
chunk_size = 4096
+ def __init__(self, c, log=True):
+ super(TCPHandler, self).__init__(c)
+ self.log = log
+
def handle_messages(self):
self.c.establish_server_connection()
@@ -63,26 +67,25 @@ class TCPHandler(ProtocolHandler):
# if one of the peers is over SSL, we need to send
# bytes/strings
if not src.ssl_established:
- # only ssl to dst, i.e. we revc'd into buf but need
- # bytes/string now.
+ # we revc'd into buf but need bytes/string now.
contents = buf[:size].tobytes()
- self.c.log(
- "%s %s\r\n%s" % (
- direction, dst_str, cleanBin(contents)
- ),
- "debug"
- )
+ if self.log:
+ self.c.log(
+ "%s %s\r\n%s" % (
+ direction, dst_str, cleanBin(contents)
+ ),
+ "info"
+ )
dst.connection.send(contents)
else:
# socket.socket.send supports raw bytearrays/memoryviews
- self.c.log(
- "%s %s\r\n%s" % (
- direction,
- dst_str,
- cleanBin(buf.tobytes())
- ),
- "debug"
- )
+ if self.log:
+ self.c.log(
+ "%s %s\r\n%s" % (
+ direction, dst_str, cleanBin(buf.tobytes())
+ ),
+ "info"
+ )
dst.connection.send(buf[:size])
except socket.error as e:
self.c.log("TCP connection closed unexpectedly.", "debug")
diff --git a/libmproxy/proxy/config.py b/libmproxy/proxy/config.py
index 62104a24..3d373a28 100644
--- a/libmproxy/proxy/config.py
+++ b/libmproxy/proxy/config.py
@@ -1,26 +1,54 @@
from __future__ import absolute_import
import os
import re
-from netlib import http_auth, certutils
+from netlib import http_auth, certutils, tcp
from .. import utils, platform, version
-from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode
+from .primitives import RegularProxyMode, TransparentProxyMode, UpstreamProxyMode, ReverseProxyMode, Socks5ProxyMode
TRANSPARENT_SSL_PORTS = [443, 8443]
CONF_BASENAME = "mitmproxy"
-CONF_DIR = "~/.mitmproxy"
+CA_DIR = "~/.mitmproxy"
-def parse_host_pattern(patterns):
- return [re.compile(p, re.IGNORECASE) for p in patterns]
+class HostMatcher(object):
+ def __init__(self, patterns=[]):
+ self.patterns = list(patterns)
+ self.regexes = [re.compile(p, re.IGNORECASE) for p in self.patterns]
+
+ def __call__(self, address):
+ address = tcp.Address.wrap(address)
+ host = "%s:%s" % (address.host, address.port)
+ if any(rex.search(host) for rex in self.regexes):
+ return True
+ else:
+ return False
+
+ def __nonzero__(self):
+ return bool(self.patterns)
class ProxyConfig:
- def __init__(self, host='', port=8080, server_version=version.NAMEVERSION,
- confdir=CONF_DIR, ca_file=None, clientcerts=None,
- no_upstream_cert=False, body_size_limit=None,
- mode=None, upstream_server=None, http_form_in=None, http_form_out=None,
- authenticator=None, ignore=[],
- ciphers=None, certs=[], certforward=False, ssl_ports=TRANSPARENT_SSL_PORTS):
+ def __init__(
+ self,
+ host='',
+ port=8080,
+ server_version=version.NAMEVERSION,
+ cadir=CA_DIR,
+ clientcerts=None,
+ no_upstream_cert=False,
+ body_size_limit=None,
+ mode=None,
+ upstream_server=None,
+ http_form_in=None,
+ http_form_out=None,
+ authenticator=None,
+ ignore_hosts=[],
+ tcp_hosts=[],
+ ciphers=None,
+ certs=[],
+ certforward=False,
+ ssl_ports=TRANSPARENT_SSL_PORTS
+ ):
self.host = host
self.port = port
self.server_version = server_version
@@ -30,7 +58,9 @@ class ProxyConfig:
self.body_size_limit = body_size_limit
if mode == "transparent":
- self.mode = TransparentProxyMode(platform.resolver(), TRANSPARENT_SSL_PORTS)
+ self.mode = TransparentProxyMode(platform.resolver(), ssl_ports)
+ elif mode == "socks5":
+ self.mode = Socks5ProxyMode(ssl_ports)
elif mode == "reverse":
self.mode = ReverseProxyMode(upstream_server)
elif mode == "upstream":
@@ -42,11 +72,11 @@ class ProxyConfig:
self.mode.http_form_in = http_form_in or self.mode.http_form_in
self.mode.http_form_out = http_form_out or self.mode.http_form_out
- self.ignore = parse_host_pattern(ignore)
+ self.check_ignore = HostMatcher(ignore_hosts)
+ self.check_tcp = HostMatcher(tcp_hosts)
self.authenticator = authenticator
- self.confdir = os.path.expanduser(confdir)
- self.ca_file = ca_file or os.path.join(self.confdir, CONF_BASENAME + "-ca.pem")
- self.certstore = certutils.CertStore.from_store(self.confdir, CONF_BASENAME)
+ self.cadir = os.path.expanduser(cadir)
+ self.certstore = certutils.CertStore.from_store(self.cadir, CONF_BASENAME)
for spec, cert in certs:
self.certstore.add_cert_file(spec, cert)
self.certforward = certforward
@@ -63,6 +93,9 @@ def process_proxy_options(parser, options):
if not platform.resolver:
return parser.error("Transparent mode not supported on this platform.")
mode = "transparent"
+ if options.socks_proxy:
+ c += 1
+ mode = "socks5"
if options.reverse_proxy:
c += 1
mode = "reverse"
@@ -72,7 +105,7 @@ def process_proxy_options(parser, options):
mode = "upstream"
upstream_server = options.upstream_proxy
if c > 1:
- return parser.error("Transparent mode, reverse mode and upstream proxy mode "
+ return parser.error("Transparent, SOCKS5, reverse and upstream proxy mode "
"are mutually exclusive.")
if options.clientcerts:
@@ -109,10 +142,16 @@ def process_proxy_options(parser, options):
parser.error("Certificate file does not exist: %s" % parts[1])
certs.append(parts)
+ ssl_ports = options.ssl_ports
+ if options.ssl_ports != TRANSPARENT_SSL_PORTS:
+ # arparse appends to default value by default, strip that off.
+ # see http://bugs.python.org/issue16399
+ ssl_ports = ssl_ports[len(TRANSPARENT_SSL_PORTS):]
+
return ProxyConfig(
host=options.addr,
port=options.port,
- confdir=options.confdir,
+ cadir=options.cadir,
clientcerts=options.clientcerts,
no_upstream_cert=options.no_upstream_cert,
body_size_limit=body_size_limit,
@@ -120,11 +159,13 @@ def process_proxy_options(parser, options):
upstream_server=upstream_server,
http_form_in=options.http_form_in,
http_form_out=options.http_form_out,
- ignore=options.ignore,
+ ignore_hosts=options.ignore_hosts,
+ tcp_hosts=options.tcp_hosts,
authenticator=authenticator,
ciphers=options.ciphers,
certs=certs,
certforward=options.certforward,
+ ssl_ports=ssl_ports
)
@@ -133,10 +174,12 @@ def ssl_option_group(parser):
group.add_argument(
"--cert", dest='certs', default=[], type=str,
metavar="SPEC", action="append",
- help='Add an SSL certificate. SPEC is of the form "[domain=]path". ' \
- 'The domain may include a wildcard, and is equal to "*" if not specified. ' \
- 'The file at path is a certificate in PEM format. If a private key is included in the PEM, ' \
- 'it is used, else the default key in the conf dir is used. Can be passed multiple times.'
+ help='Add an SSL certificate. SPEC is of the form "[domain=]path". '
+ 'The domain may include a wildcard, and is equal to "*" if not specified. '
+ 'The file at path is a certificate in PEM format. If a private key is included in the PEM, '
+ 'it is used, else the default key in the conf dir is used. '
+ 'The PEM file should contain the full certificate chain, with the leaf certificate as the first entry. '
+ 'Can be passed multiple times.'
)
group.add_argument(
"--client-certs", action="store",
@@ -159,7 +202,7 @@ def ssl_option_group(parser):
help="Don't connect to upstream server to look up certificate details."
)
group.add_argument(
- "--ssl-port", action="append", type=int, dest="ssl_ports", default=TRANSPARENT_SSL_PORTS,
+ "--ssl-port", action="append", type=int, dest="ssl_ports", default=list(TRANSPARENT_SSL_PORTS),
metavar="PORT",
help="Can be passed multiple times. Specify destination ports which are assumed to be SSL. "
"Defaults to %s." % str(TRANSPARENT_SSL_PORTS)
diff --git a/libmproxy/proxy/primitives.py b/libmproxy/proxy/primitives.py
index 23d089d3..c0ae424d 100644
--- a/libmproxy/proxy/primitives.py
+++ b/libmproxy/proxy/primitives.py
@@ -1,5 +1,5 @@
from __future__ import absolute_import
-
+from netlib import socks
class ProxyError(Exception):
def __init__(self, code, message, headers=None):
@@ -15,7 +15,7 @@ class ProxyMode(object):
http_form_in = None
http_form_out = None
- def get_upstream_server(self, conn):
+ def get_upstream_server(self, client_conn):
"""
Returns the address of the server to connect to.
Returns None if the address needs to be determined on the protocol level (regular proxy mode)
@@ -46,7 +46,7 @@ class RegularProxyMode(ProxyMode):
http_form_in = "absolute"
http_form_out = "relative"
- def get_upstream_server(self, conn):
+ def get_upstream_server(self, client_conn):
return None
@@ -58,9 +58,9 @@ class TransparentProxyMode(ProxyMode):
self.resolver = resolver
self.sslports = sslports
- def get_upstream_server(self, conn):
+ def get_upstream_server(self, client_conn):
try:
- dst = self.resolver.original_addr(conn)
+ dst = self.resolver.original_addr(client_conn.connection)
except Exception, e:
raise ProxyError(502, "Transparent mode failure: %s" % str(e))
@@ -71,11 +71,80 @@ class TransparentProxyMode(ProxyMode):
return [ssl, ssl] + list(dst)
+class Socks5ProxyMode(ProxyMode):
+ http_form_in = "relative"
+ http_form_out = "relative"
+
+ def __init__(self, sslports):
+ self.sslports = sslports
+
+ @staticmethod
+ def _assert_socks5(msg):
+ if msg.ver != socks.VERSION.SOCKS5:
+ if msg.ver == ord("G") and len(msg.methods) == ord("E"):
+ guess = "Probably not a SOCKS request but a regular HTTP request. "
+ else:
+ guess = ""
+ raise socks.SocksError(
+ socks.REP.GENERAL_SOCKS_SERVER_FAILURE,
+ guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % msg.ver)
+
+ def get_upstream_server(self, client_conn):
+ try:
+ # Parse Client Greeting
+ client_greet = socks.ClientGreeting.from_file(client_conn.rfile)
+ self._assert_socks5(client_greet)
+ if socks.METHOD.NO_AUTHENTICATION_REQUIRED not in client_greet.methods:
+ raise socks.SocksError(
+ socks.METHOD.NO_ACCEPTABLE_METHODS,
+ "mitmproxy only supports SOCKS without authentication"
+ )
+
+ # Send Server Greeting
+ server_greet = socks.ServerGreeting(
+ socks.VERSION.SOCKS5,
+ socks.METHOD.NO_AUTHENTICATION_REQUIRED
+ )
+ server_greet.to_file(client_conn.wfile)
+ client_conn.wfile.flush()
+
+ # Parse Connect Request
+ connect_request = socks.Message.from_file(client_conn.rfile)
+ self._assert_socks5(connect_request)
+ if connect_request.msg != socks.CMD.CONNECT:
+ raise socks.SocksError(
+ socks.REP.COMMAND_NOT_SUPPORTED,
+ "mitmproxy only supports SOCKS5 CONNECT."
+ )
+
+ # We do not connect here yet, as the clientconnect event has not been handled yet.
+
+ connect_reply = socks.Message(
+ socks.VERSION.SOCKS5,
+ socks.REP.SUCCEEDED,
+ socks.ATYP.DOMAINNAME,
+ client_conn.address # dummy value, we don't have an upstream connection yet.
+ )
+ connect_reply.to_file(client_conn.wfile)
+ client_conn.wfile.flush()
+
+ ssl = bool(connect_request.addr.port in self.sslports)
+ return ssl, ssl, connect_request.addr.host, connect_request.addr.port
+
+ except socks.SocksError as e:
+ msg = socks.Message(5, e.code, socks.ATYP.DOMAINNAME, repr(e))
+ try:
+ msg.to_file(client_conn.wfile)
+ except:
+ pass
+ raise ProxyError(502, "SOCKS5 mode failure: %s" % str(e))
+
+
class _ConstDestinationProxyMode(ProxyMode):
def __init__(self, dst):
self.dst = dst
- def get_upstream_server(self, conn):
+ def get_upstream_server(self, client_conn):
return self.dst
diff --git a/libmproxy/proxy/server.py b/libmproxy/proxy/server.py
index 307a4bcd..55e2b30e 100644
--- a/libmproxy/proxy/server.py
+++ b/libmproxy/proxy/server.py
@@ -70,13 +70,15 @@ class ConnectionHandler:
# Can we already identify the target server and connect to it?
client_ssl, server_ssl = False, False
- upstream_info = self.config.mode.get_upstream_server(self.client_conn.connection)
+ conn_kwargs = dict()
+ upstream_info = self.config.mode.get_upstream_server(self.client_conn)
if upstream_info:
self.set_server_address(upstream_info[2:])
client_ssl, server_ssl = upstream_info[:2]
- if self.check_ignore_address(self.server_conn.address):
+ if self.config.check_ignore(self.server_conn.address):
self.log("Ignore host: %s:%s" % self.server_conn.address(), "info")
self.conntype = "tcp"
+ conn_kwargs["log"] = False
client_ssl, server_ssl = False, False
else:
pass # No upstream info from the metadata: upstream info in the protocol (e.g. HTTP absolute-form)
@@ -90,15 +92,18 @@ class ConnectionHandler:
if client_ssl or server_ssl:
self.establish_ssl(client=client_ssl, server=server_ssl)
+ if self.config.check_tcp(self.server_conn.address):
+ self.log("Generic TCP mode for host: %s:%s" % self.server_conn.address(), "info")
+ self.conntype = "tcp"
+
# Delegate handling to the protocol handler
- protocol_handler(self.conntype)(self).handle_messages()
+ protocol_handler(self.conntype)(self, **conn_kwargs).handle_messages()
- self.del_server_connection()
self.log("clientdisconnect", "info")
self.channel.tell("clientdisconnect", self)
except ProxyError as e:
- protocol_handler(self.conntype)(self).handle_error(e)
+ protocol_handler(self.conntype)(self, **conn_kwargs).handle_error(e)
except Exception:
import traceback, sys
@@ -106,6 +111,10 @@ class ConnectionHandler:
print >> sys.stderr, traceback.format_exc()
print >> sys.stderr, "mitmproxy has crashed!"
print >> sys.stderr, "Please lodge a bug report at: https://github.com/mitmproxy/mitmproxy"
+ finally:
+ # Make sure that we close the server connection in any case.
+ # The client connection is closed by the ProxyServer and does not have be handled here.
+ self.del_server_connection()
def del_server_connection(self):
"""
@@ -113,20 +122,13 @@ class ConnectionHandler:
"""
if self.server_conn and self.server_conn.connection:
self.server_conn.finish()
+ self.server_conn.close()
self.log("serverdisconnect", "debug", ["%s:%s" % (self.server_conn.address.host,
self.server_conn.address.port)])
self.channel.tell("serverdisconnect", self)
self.server_conn = None
self.sni = None
- def check_ignore_address(self, address):
- address = tcp.Address.wrap(address)
- host = "%s:%s" % (address.host, address.port)
- if host and any(rex.search(host) for rex in self.config.ignore):
- return True
- else:
- return False
-
def set_server_address(self, address):
"""
Sets a new server address with the given priority.
@@ -190,14 +192,14 @@ class ConnectionHandler:
if client:
if self.client_conn.ssl_established:
raise ProxyError(502, "SSL to Client already established.")
- cert, key = self.find_cert()
+ cert, key, chain_file = self.find_cert()
try:
self.client_conn.convert_to_ssl(
cert, key,
handle_sni=self.handle_sni,
cipher_list=self.config.ciphers,
dhparams=self.config.certstore.dhparams,
- ca_file=self.config.ca_file
+ chain_file=chain_file
)
except tcp.NetLibError as v:
raise ProxyError(400, repr(v))
@@ -234,7 +236,7 @@ class ConnectionHandler:
def find_cert(self):
if self.config.certforward and self.server_conn.ssl_established:
- return self.server_conn.cert, self.config.certstore.gen_pkey(self.server_conn.cert)
+ return self.server_conn.cert, self.config.certstore.gen_pkey(self.server_conn.cert), None
else:
host = self.server_conn.address.host
sans = []
@@ -264,17 +266,17 @@ class ConnectionHandler:
self.log("SNI received: %s" % self.sni, "debug")
self.server_reconnect() # reconnect to upstream server with SNI
# Now, change client context to reflect changed certificate:
- cert, key = self.find_cert()
+ cert, key, chain_file = self.find_cert()
new_context = self.client_conn._create_ssl_context(
cert, key,
method=SSL.TLSv1_METHOD,
cipher_list=self.config.ciphers,
dhparams=self.config.certstore.dhparams,
- ca_file=self.config.ca_file
+ chain_file=chain_file
)
connection.set_context(new_context)
# An unhandled exception in this method will core dump PyOpenSSL, so
# make dang sure it doesn't happen.
- except Exception: # pragma: no cover
+ except: # pragma: no cover
import traceback
- self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error") \ No newline at end of file
+ self.log("Error in handle_sni:\r\n" + traceback.format_exc(), "error")
diff --git a/libmproxy/version.py b/libmproxy/version.py
index 3483625d..8dcaecc8 100644
--- a/libmproxy/version.py
+++ b/libmproxy/version.py
@@ -1,5 +1,9 @@
-IVERSION = (0, 11)
+IVERSION = (0, 11, 1)
VERSION = ".".join(str(i) for i in IVERSION)
MINORVERSION = ".".join(str(i) for i in IVERSION[:2])
NAME = "mitmproxy"
NAMEVERSION = NAME + " " + VERSION
+
+NEXT_MINORVERSION = list(IVERSION)
+NEXT_MINORVERSION[1] += 1
+NEXT_MINORVERSION = ".".join(str(i) for i in NEXT_MINORVERSION[:2]) \ No newline at end of file
diff --git a/libmproxy/web/static/flows.json b/libmproxy/web/static/flows.json
index a0358db0..35accd38 100644
--- a/libmproxy/web/static/flows.json
+++ b/libmproxy/web/static/flows.json
@@ -93,7 +93,7 @@
"clientcert": null,
"ssl_established": true
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -259,7 +259,7 @@
"clientcert": null,
"ssl_established": true
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -425,7 +425,7 @@
"clientcert": null,
"ssl_established": true
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -595,7 +595,7 @@
"clientcert": null,
"ssl_established": true
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -765,7 +765,7 @@
"clientcert": null,
"ssl_established": true
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -919,7 +919,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1057,7 +1057,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1195,7 +1195,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1329,7 +1329,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1483,7 +1483,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1633,7 +1633,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1767,7 +1767,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -1901,7 +1901,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
@@ -2027,7 +2027,7 @@
"clientcert": null,
"ssl_established": false
},
- "conntype": "http",
+ "type": "http",
"version": [
0,
11
diff --git a/release/osx-binaries b/release/osx-binaries
index 4be85800..9945e471 100755
--- a/release/osx-binaries
+++ b/release/osx-binaries
@@ -10,7 +10,8 @@
# answer is to touch the __init__.py file in the zope directory. On my system:
# touch /Library/Python/2.7/site-packages/zope/__init__.py
-# To run, change into the pyinstaller directory, and then run this script.
+# To run, first install netlib and mitmproxy, then change into the pyinstaller
+# directory, and then run this script.
DST=/tmp/osx-mitmproxy
MITMPROXY=~/mitmproxy/mitmproxy
diff --git a/release/release-checklist b/release/release-checklist
index d0bf8aad..c0d8d82d 100644
--- a/release/release-checklist
+++ b/release/release-checklist
@@ -2,8 +2,6 @@
- Bump the version number:
mitmproxy/libmproxy/version.py
- mitmproxy/requirements.txt
- mitmproxy/test/requirements.txt
netlib/netlib/version.py
netlib/requirements.txt
netlib/test/requirements.txt
@@ -26,3 +24,22 @@
- tar -xzvf pkgfile.tgz
- virtualenv venv
+- Build the OSX binaries
+ - Follow instructions in osxbinaries
+ - Package:
+ cp -r ./doc /tmp/osx-mitmproxy/
+ mv /tmp/osx-mitmproxy /tmp/osx-mitmproxy-VERSION
+ tar -czvf /tmp/osx-mitmproxy-VERSION.tar.gz /tmp/osx-mitmproxy-VERSION
+ mv /tmp/osx-mitmproxy-VERSION.tar.gz ~/mitmproxy/www.mitmproxy.org/src/download
+
+- Build the sources for each project:
+ python ./setup.py sdist
+ mv ./dist/FILE ~/mitmproxy/www.mitmproxy.org/src/download
+
+
+- Adjust links on www.mitmproxy.org
+
+- Upload to pypi for each project:
+
+ python ./setup.py sdist upload
+
diff --git a/release/test-release b/release/test-release
index 8b53c14c..8cbcea8c 100755
--- a/release/test-release
+++ b/release/test-release
@@ -23,15 +23,16 @@ python ./setup.py -q sdist --dist-dir $DST
echo "Creating virtualenv for test install..."
virtualenv -q $DST/venv
+cd $DST
echo "Installing netlib..."
-$DST/venv/bin/pip -q install --download-cache ~/.pipcache $DST/netlib*
+./venv/bin/pip -q install --download-cache ~/.pipcache ./netlib*
echo "Installing pathod..."
-$DST/venv/bin/pip -q install --download-cache ~/.pipcache $DST/pathod*
+./venv/bin/pip -q install --download-cache ~/.pipcache ./pathod*
echo "Installing mitmproxy..."
-$DST/venv/bin/pip -q install --download-cache ~/.pipcache $DST/mitmproxy*
+./venv/bin/pip -q install --download-cache ~/.pipcache ./mitmproxy*
echo "Running binaries..."
-$DST/venv/bin/mitmproxy --version
-$DST/venv/bin/mitmdump --version
-$DST/venv/bin/pathod --version
-$DST/venv/bin/pathoc --version
+./venv/bin/mitmproxy --version
+./venv/bin/mitmdump --version
+./venv/bin/pathod --version
+./venv/bin/pathoc --version
diff --git a/requirements.txt b/requirements.txt
index d84347b7..946e5ffe 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,3 @@
-e git+https://github.com/mitmproxy/netlib.git#egg=netlib
-e git+https://github.com/mitmproxy/pathod.git#egg=pathod
--e .[dev] \ No newline at end of file
+-e .[dev,examples] \ No newline at end of file
diff --git a/setup.py b/setup.py
index d6983d4f..79398a18 100644
--- a/setup.py
+++ b/setup.py
@@ -16,13 +16,12 @@ if os.name != "nt":
scripts.append("mitmproxy")
deps = {
- "netlib>=%s" % version.MINORVERSION,
+ "netlib>=%s, <%s" % (version.MINORVERSION, version.NEXT_MINORVERSION),
"pyasn1>0.1.2",
- "requests>=2.4.0",
"pyOpenSSL>=0.14",
"Flask>=0.10.1",
"tornado>=4.0.2",
- "sortedcontainers>=0.9.1"
+ "configargparse>=0.9.3"
}
script_deps = {
"mitmproxy": {
@@ -37,10 +36,6 @@ for script in scripts:
if os.name == "nt":
deps.add("pydivert>=0.0.4") # Transparent proxying on Windows
-console_scripts = [
- "%s = libmproxy.main:%s" % (s, s) for s in scripts
-]
-
setup(
name="mitmproxy",
@@ -67,14 +62,9 @@ setup(
"Topic :: Internet :: Proxy Servers",
"Topic :: Software Development :: Testing"
],
-
packages=find_packages(),
include_package_data=True,
-
- entry_points={
- 'console_scripts': console_scripts
- },
-
+ scripts = scripts,
install_requires=list(deps),
extras_require={
'dev': [
@@ -82,12 +72,19 @@ setup(
"nose>=1.3.0",
"nose-cov>=1.6",
"coveralls>=0.4.1",
- "pathod>=%s" % version.MINORVERSION
+ "pathod>=%s, <%s" % (
+ version.MINORVERSION, version.NEXT_MINORVERSION
+ )
],
'contentviews': [
"pyamf>=0.6.1",
"protobuf>=2.5.0",
"cssutils>=1.0"
+ ],
+ 'examples': [
+ "pytz",
+ "harparser",
+ "beautifulsoup4"
]
}
-) \ No newline at end of file
+)
diff --git a/test/fuzzing/.env b/test/fuzzing/.env
new file mode 100644
index 00000000..82ae6a8d
--- /dev/null
+++ b/test/fuzzing/.env
@@ -0,0 +1,6 @@
+
+MITMDUMP=../../mitmdump
+PATHOD=../../../pathod/pathod
+PATHOC=../../../pathod/pathoc
+FUZZ_SETTINGS=-remTt 1 -n 0
+
diff --git a/test/fuzzing/README b/test/fuzzing/README
new file mode 100644
index 00000000..2760506f
--- /dev/null
+++ b/test/fuzzing/README
@@ -0,0 +1,14 @@
+
+A fuzzing architecture for mitmproxy
+====================================
+
+Quick start:
+
+ honcho -f ./straight_stream start
+
+
+Notes:
+
+ - Processes are managed using honcho (pip install honcho)
+ - Paths and common settings live in .env
+
diff --git a/test/fuzzing/client_patterns b/test/fuzzing/client_patterns
new file mode 100644
index 00000000..83457b6f
--- /dev/null
+++ b/test/fuzzing/client_patterns
@@ -0,0 +1,4 @@
+get:'http://localhost:9999/p/200':ir,"\n"
+get:'http://localhost:9999/p/200':ir,"\0"
+get:'http://localhost:9999/p/200':ir,@5
+get:'http://localhost:9999/p/200':dr
diff --git a/test/fuzzing/go_proxy b/test/fuzzing/go_proxy
index c9b6aef6..ea29400f 100755
--- a/test/fuzzing/go_proxy
+++ b/test/fuzzing/go_proxy
@@ -3,20 +3,27 @@
# mitmproxy/mitmdump is running on port 8080 in straight proxy mode.
# pathod is running on port 9999
-BASE_HTTP="/Users/aldo/git/public/pathod/pathoc -Tt 1 -eo -I 200,400,405,502 -p 8080 localhost "
+BASE="../../../"
+BASE_HTTP=$BASE"/pathod/pathoc -Tt 1 -e -I 200,400,405,502 -p 8080 localhost "
+BASE_HTTPS=$BASE"/pathod/pathoc -sc localhost:9999 -Tt 1 -eo -I 200,400,404,405,502,800 -p 8080 localhost "
+
#$BASE_HTTP -n 10000 "get:'http://localhost:9999':ir,@1"
#$BASE_HTTP -n 100 "get:'http://localhost:9999':dr"
-#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@300.0
+#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200':ir,@300"
+
+#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@1'"
+#$BASE_HTTP -n 100 "get:'http://localhost:9999/p/200:dr'"
+#$BASE_HTTP -n 10000 "get:'http://localhost:9999/p/200:ir,@100'"
# Assuming:
# mitmproxy/mitmdump is running on port 8080 in straight proxy mode.
# pathod with SSL enabled is running on port 9999
-BASE_HTTPS="/Users/aldo/git/public/pathod/pathoc -sc localhost:9999 -Tt 1 -eo -I 200,400,404,405,502,800 -p 8080 localhost "
-$BASE_HTTPS -en 10000 "get:'/p/200:b@10:ir,@1'"
+#$BASE_HTTPS -en 10000 "get:'/p/200:b@100:ir,@1'"
#$BASE_HTTPS -en 10000 "get:'/p/200:ir,@1'"
#$BASE_HTTPS -n 100 "get:'/p/200:dr'"
#$BASE_HTTPS -n 10000 "get:'/p/200:ir,@3000'"
#$BASE_HTTPS -n 10000 "get:'/p/200:ir,\"\\n\"'"
+
diff --git a/test/fuzzing/reverse_patterns b/test/fuzzing/reverse_patterns
new file mode 100644
index 00000000..8d1d76a2
--- /dev/null
+++ b/test/fuzzing/reverse_patterns
@@ -0,0 +1,9 @@
+get:'/p/200':b@10:ir,"\n"
+get:'/p/200':b@10:ir,"\r\n"
+get:'/p/200':b@10:ir,"\0"
+get:'/p/200':b@10:ir,@5
+get:'/p/200':b@10:dr
+
+get:'/p/200:b@10:ir,@1'
+get:'/p/200:b@10:dr'
+get:'/p/200:b@10:ir,@100'
diff --git a/test/fuzzing/straight_stream b/test/fuzzing/straight_stream
new file mode 100644
index 00000000..41e2a6e1
--- /dev/null
+++ b/test/fuzzing/straight_stream
@@ -0,0 +1,6 @@
+
+mitmdump: $MITMDUMP
+pathod: $PATHOD
+pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns
+#pathoc: sleep 2 && $PATHOC localhost:8080 /tmp/err
+
diff --git a/test/fuzzing/straight_stream_patterns b/test/fuzzing/straight_stream_patterns
new file mode 100644
index 00000000..93a066e6
--- /dev/null
+++ b/test/fuzzing/straight_stream_patterns
@@ -0,0 +1,17 @@
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,'\n'
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,'a'
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,'9'
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,':'
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,'"'
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,'-'
+
+get:'http://localhost:9999/p/':s'200:b"foo":ir,"\n"'
+get:'http://localhost:9999/p/':s'200:b"foo":ir,"a"'
+get:'http://localhost:9999/p/':s'200:b"foo":ir,"9"'
+get:'http://localhost:9999/p/':s'200:b"foo":ir,":"'
+get:'http://localhost:9999/p/':s"200:b'foo':ir,'\"'"
+get:'http://localhost:9999/p/':s'200:b"foo":ir,"-"'
+get:'http://localhost:9999/p/':s'200:b"foo":dr'
+
+get:'http://localhost:9999/p/':s'200:b"foo"':ir,@2
+get:'http://localhost:9999/p/':s'200:b"foo":ir,@2'
diff --git a/test/fuzzing/straight_stream_ssl b/test/fuzzing/straight_stream_ssl
new file mode 100644
index 00000000..708ff0b3
--- /dev/null
+++ b/test/fuzzing/straight_stream_ssl
@@ -0,0 +1,6 @@
+
+mitmdump: $MITMDUMP -q --stream 1
+pathod: $PATHOD
+pathoc: sleep 2 && $PATHOC $FUZZ_SETTINGS localhost:8080 ./straight_stream_patterns
+#pathoc: sleep 2 && $PATHOC localhost:8080 /tmp/err
+
diff --git a/test/test_cmdline.py b/test/test_cmdline.py
index 12e8aa89..476fc620 100644
--- a/test/test_cmdline.py
+++ b/test/test_cmdline.py
@@ -1,7 +1,6 @@
import argparse
from libmproxy import cmdline
import tutils
-import os.path
def test_parse_replace_hook():
@@ -51,6 +50,7 @@ def test_parse_setheaders():
x = cmdline.parse_setheader("/foo/bar/voing")
assert x == ("foo", "bar", "voing")
+
def test_common():
parser = argparse.ArgumentParser()
cmdline.common_options(parser)
@@ -108,3 +108,19 @@ def test_common():
assert len(v) == 1
assert v[0][2].strip() == "replacecontents"
+
+def test_mitmproxy():
+ ap = cmdline.mitmproxy()
+ assert ap
+
+
+def test_mitmdump():
+ ap = cmdline.mitmdump()
+ assert ap
+
+
+def test_mitmweb():
+ ap = cmdline.mitmweb()
+ assert ap
+
+
diff --git a/test/test_dump.py b/test/test_dump.py
index 2e58e073..e9cb4d33 100644
--- a/test/test_dump.py
+++ b/test/test_dump.py
@@ -1,10 +1,12 @@
import os
from cStringIO import StringIO
-from libmproxy import dump, flow, proxy
+from libmproxy import dump, flow
+from libmproxy.protocol import http
from libmproxy.proxy.primitives import Log
import tutils
import mock
+
def test_strfuncs():
t = tutils.tresp()
t.is_replay = True
@@ -58,6 +60,18 @@ class TestDumpMaster:
assert m.handle_error(f)
assert "error" in cs.getvalue()
+ def test_missing_content(self):
+ cs = StringIO()
+ o = dump.Options(flow_detail=3)
+ m = dump.DumpMaster(None, o, outfile=cs)
+ f = tutils.tflow()
+ f.request.content = http.CONTENT_MISSING
+ m.handle_request(f)
+ f.response = tutils.tresp()
+ f.response.content = http.CONTENT_MISSING
+ m.handle_response(f)
+ assert "content missing" in cs.getvalue()
+
def test_replay(self):
cs = StringIO()
diff --git a/test/test_examples.py b/test/test_examples.py
index fd42e6f0..deb97b49 100644
--- a/test/test_examples.py
+++ b/test/test_examples.py
@@ -1,10 +1,8 @@
import glob
-import mock
from libmproxy import utils, script
from libmproxy.proxy import config
import tservers
-@mock.patch.dict("sys.modules", {"bs4": mock.Mock()})
def test_load_scripts():
example_dir = utils.Data("libmproxy").path("../examples")
scripts = glob.glob("%s/*.py" % example_dir)
@@ -12,8 +10,11 @@ def test_load_scripts():
tmaster = tservers.TestMaster(config.ProxyConfig())
for f in scripts:
+ if "har_extractor" in f:
+ f += " foo"
if "iframe_injector" in f:
f += " foo" # one argument required
if "modify_response_body" in f:
f += " foo bar" # two arguments required
- script.Script(f, tmaster) # Loads the script file. \ No newline at end of file
+ s = script.Script(f, tmaster) # Loads the script file.
+ s.unload() \ No newline at end of file
diff --git a/test/test_flow.py b/test/test_flow.py
index b74119dd..22abb4d4 100644
--- a/test/test_flow.py
+++ b/test/test_flow.py
@@ -5,8 +5,10 @@ import mock
from libmproxy import filt, protocol, controller, utils, tnetstring, flow
from libmproxy.protocol.primitives import Error, Flow
from libmproxy.protocol.http import decoded, CONTENT_MISSING
-from libmproxy.proxy.connection import ClientConnection, ServerConnection
-from netlib import tcp
+from libmproxy.proxy.config import HostMatcher
+from libmproxy.proxy import ProxyConfig
+from libmproxy.proxy.server import DummyServer
+from libmproxy.proxy.connection import ClientConnection
import tutils
@@ -84,19 +86,20 @@ class TestClientPlaybackState:
fm = flow.FlowMaster(None, s)
fm.start_client_playback([first, tutils.tflow()], True)
c = fm.client_playback
+ c.testing = True
assert not c.done()
assert not s.flow_count()
assert c.count() == 2
- c.tick(fm, testing=True)
+ c.tick(fm)
assert s.flow_count()
assert c.count() == 1
- c.tick(fm, testing=True)
+ c.tick(fm)
assert c.count() == 1
c.clear(c.current)
- c.tick(fm, testing=True)
+ c.tick(fm)
assert c.count() == 0
c.clear(c.current)
assert c.done()
@@ -531,6 +534,14 @@ class TestSerialize:
fm.load_flows(r)
assert len(s._flow_list) == 6
+ def test_load_flows_reverse(self):
+ r = self._treader()
+ s = flow.State()
+ conf = ProxyConfig(mode="reverse", upstream_server=[True,True,"use-this-domain",80])
+ fm = flow.FlowMaster(DummyServer(conf), s)
+ fm.load_flows(r)
+ assert s._flow_list[0].request.host == "use-this-domain"
+
def test_filter(self):
sio = StringIO()
fl = filt.parse("~c 200")
@@ -584,11 +595,11 @@ class TestFlowMaster:
def test_getset_ignore(self):
p = mock.Mock()
- p.config.ignore = []
+ p.config.check_ignore = HostMatcher()
fm = flow.FlowMaster(p, flow.State())
- assert not fm.get_ignore()
- fm.set_ignore(["^apple\.com:", ":443$"])
- assert fm.get_ignore()
+ assert not fm.get_ignore_filter()
+ fm.set_ignore_filter(["^apple\.com:", ":443$"])
+ assert fm.get_ignore_filter()
def test_replay(self):
s = flow.State()
@@ -600,6 +611,9 @@ class TestFlowMaster:
f.intercepting = True
assert "intercepting" in fm.replay_request(f)
+ f.live = True
+ assert "live" in fm.replay_request(f)
+
def test_script_reqerr(self):
s = flow.State()
fm = flow.FlowMaster(None, s)
@@ -679,9 +693,11 @@ class TestFlowMaster:
f = tutils.tflow(resp=True)
pb = [tutils.tflow(resp=True), f]
- fm = flow.FlowMaster(None, s)
+
+ fm = flow.FlowMaster(DummyServer(ProxyConfig()), s)
assert not fm.start_server_playback(pb, False, [], False, False, None, False)
assert not fm.start_client_playback(pb, False)
+ fm.client_playback.testing = True
q = Queue.Queue()
assert not fm.state.flow_count()
diff --git a/test/test_protocol_http.py b/test/test_protocol_http.py
index ea6cf3fd..16870777 100644
--- a/test/test_protocol_http.py
+++ b/test/test_protocol_http.py
@@ -23,7 +23,7 @@ def test_stripped_chunked_encoding_no_content():
class TestHTTPRequest:
- def test_asterisk_form(self):
+ def test_asterisk_form_in(self):
s = StringIO("OPTIONS * HTTP/1.1")
f = tutils.tflow(req=None)
f.request = HTTPRequest.from_stream(s)
@@ -31,9 +31,11 @@ class TestHTTPRequest:
f.request.host = f.server_conn.address.host
f.request.port = f.server_conn.address.port
f.request.scheme = "http"
- assert f.request.assemble() == "OPTIONS * HTTP/1.1\r\nHost: address:22\r\n\r\n"
+ assert f.request.assemble() == ("OPTIONS * HTTP/1.1\r\n"
+ "Host: address:22\r\n"
+ "Content-Length: 0\r\n\r\n")
- def test_origin_form(self):
+ def test_relative_form_in(self):
s = StringIO("GET /foo\xff HTTP/1.1")
tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s)
s = StringIO("GET /foo HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: h2c")
@@ -52,22 +54,47 @@ class TestHTTPRequest:
r.update_host_header()
assert "Host" in r.headers
-
- def test_authority_form(self):
+ def test_authority_form_in(self):
s = StringIO("CONNECT oops-no-port.com HTTP/1.1")
tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s)
s = StringIO("CONNECT address:22 HTTP/1.1")
r = HTTPRequest.from_stream(s)
r.scheme, r.host, r.port = "http", "address", 22
- assert r.assemble() == "CONNECT address:22 HTTP/1.1\r\nHost: address:22\r\n\r\n"
+ assert r.assemble() == ("CONNECT address:22 HTTP/1.1\r\n"
+ "Host: address:22\r\n"
+ "Content-Length: 0\r\n\r\n")
assert r.pretty_url(False) == "address:22"
- def test_absolute_form(self):
+ def test_absolute_form_in(self):
s = StringIO("GET oops-no-protocol.com HTTP/1.1")
tutils.raises("Bad HTTP request line", HTTPRequest.from_stream, s)
s = StringIO("GET http://address:22/ HTTP/1.1")
r = HTTPRequest.from_stream(s)
- assert r.assemble() == "GET http://address:22/ HTTP/1.1\r\nHost: address:22\r\n\r\n"
+ assert r.assemble() == "GET http://address:22/ HTTP/1.1\r\nHost: address:22\r\nContent-Length: 0\r\n\r\n"
+
+ def test_http_options_relative_form_in(self):
+ """
+ Exercises fix for Issue #392.
+ """
+ s = StringIO("OPTIONS /secret/resource HTTP/1.1")
+ r = HTTPRequest.from_stream(s)
+ r.host = 'address'
+ r.port = 80
+ r.scheme = "http"
+ assert r.assemble() == ("OPTIONS /secret/resource HTTP/1.1\r\n"
+ "Host: address\r\n"
+ "Content-Length: 0\r\n\r\n")
+
+ def test_http_options_absolute_form_in(self):
+ s = StringIO("OPTIONS http://address/secret/resource HTTP/1.1")
+ r = HTTPRequest.from_stream(s)
+ r.host = 'address'
+ r.port = 80
+ r.scheme = "http"
+ assert r.assemble() == ("OPTIONS http://address:80/secret/resource HTTP/1.1\r\n"
+ "Host: address\r\n"
+ "Content-Length: 0\r\n\r\n")
+
def test_assemble_unknown_form(self):
r = tutils.treq()
@@ -133,4 +160,4 @@ class TestInvalidRequests(tservers.HTTPProxTest):
p.connect()
r = p.request("get:/p/200")
assert r.status_code == 400
- assert "Invalid HTTP request form" in r.content \ No newline at end of file
+ assert "Invalid HTTP request form" in r.content
diff --git a/test/test_proxy.py b/test/test_proxy.py
index c396183b..641b4f47 100644
--- a/test/test_proxy.py
+++ b/test/test_proxy.py
@@ -70,9 +70,9 @@ class TestProcessProxyOptions:
def test_simple(self):
assert self.p()
- def test_confdir(self):
- with tutils.tmpdir() as confdir:
- self.assert_noerr("--confdir", confdir)
+ def test_cadir(self):
+ with tutils.tmpdir() as cadir:
+ self.assert_noerr("--cadir", cadir)
@mock.patch("libmproxy.platform.resolver", None)
def test_no_transparent(self):
@@ -94,12 +94,12 @@ class TestProcessProxyOptions:
self.assert_err("mutually exclusive", "-R", "http://localhost", "-T")
def test_client_certs(self):
- with tutils.tmpdir() as confdir:
- self.assert_noerr("--client-certs", confdir)
+ with tutils.tmpdir() as cadir:
+ self.assert_noerr("--client-certs", cadir)
self.assert_err("directory does not exist", "--client-certs", "nonexistent")
def test_certs(self):
- with tutils.tmpdir() as confdir:
+ with tutils.tmpdir() as cadir:
self.assert_noerr("--cert", tutils.test_data.path("data/testkey.pem"))
self.assert_err("does not exist", "--cert", "nonexistent")
diff --git a/test/test_server.py b/test/test_server.py
index 0ce5d056..c81eab2b 100644
--- a/test/test_server.py
+++ b/test/test_server.py
@@ -1,5 +1,5 @@
import socket, time
-from libmproxy.proxy.config import parse_host_pattern
+from libmproxy.proxy.config import HostMatcher
from netlib import tcp, http_auth, http
from libpathod import pathoc, pathod
from netlib.certutils import SSLCert
@@ -19,6 +19,17 @@ class CommonMixin:
def test_large(self):
assert len(self.pathod("200:b@50k").content) == 1024*50
+ @staticmethod
+ def wait_until_not_live(flow):
+ """
+ Race condition: We don't want to replay the flow while it is still live.
+ """
+ s = time.time()
+ while flow.live:
+ time.sleep(0.001)
+ if time.time() - s > 5:
+ raise RuntimeError("Flow is live for too long.")
+
def test_replay(self):
assert self.pathod("304").status_code == 304
if isinstance(self, tservers.HTTPUpstreamProxTest) and self.ssl:
@@ -28,6 +39,7 @@ class CommonMixin:
l = self.master.state.view[-1]
assert l.response.code == 304
l.request.path = "/p/305"
+ self.wait_until_not_live(l)
rt = self.master.replay_request(l, block=True)
assert l.response.code == 305
@@ -79,11 +91,14 @@ class CommonMixin:
class TcpMixin:
def _ignore_on(self):
- ignore = parse_host_pattern([".+:%s" % self.server.port])[0]
- self.config.ignore.append(ignore)
+ assert not hasattr(self, "_ignore_backup")
+ self._ignore_backup = self.config.check_ignore
+ self.config.check_ignore = HostMatcher([".+:%s" % self.server.port] + self.config.check_ignore.patterns)
def _ignore_off(self):
- self.config.ignore.pop()
+ assert hasattr(self, "_ignore_backup")
+ self.config.check_ignore = self._ignore_backup
+ del self._ignore_backup
def test_ignore(self):
spec = '304:h"Alternate-Protocol"="mitmproxy-will-remove-this"'
@@ -114,6 +129,40 @@ class TcpMixin:
tutils.raises("invalid server response", self.pathod, spec) # pathoc tries to parse answer as HTTP
self._ignore_off()
+ def _tcpproxy_on(self):
+ assert not hasattr(self, "_tcpproxy_backup")
+ self._tcpproxy_backup = self.config.check_tcp
+ self.config.check_tcp = HostMatcher([".+:%s" % self.server.port] + self.config.check_tcp.patterns)
+
+ def _tcpproxy_off(self):
+ assert hasattr(self, "_tcpproxy_backup")
+ self.config.check_ignore = self._tcpproxy_backup
+ del self._tcpproxy_backup
+
+
+ def test_tcp(self):
+ spec = '304:h"Alternate-Protocol"="mitmproxy-will-remove-this"'
+ n = self.pathod(spec)
+ self._tcpproxy_on()
+ i = self.pathod(spec)
+ i2 = self.pathod(spec)
+ self._tcpproxy_off()
+
+ assert i.status_code == i2.status_code == n.status_code == 304
+ assert "Alternate-Protocol" in i.headers
+ assert "Alternate-Protocol" in i2.headers
+ assert "Alternate-Protocol" not in n.headers
+
+ # Test that we get the original SSL cert
+ if self.ssl:
+ i_cert = SSLCert(i.sslinfo.certchain[0])
+ i2_cert = SSLCert(i2.sslinfo.certchain[0])
+ n_cert = SSLCert(n.sslinfo.certchain[0])
+
+ assert i_cert == i2_cert == n_cert
+
+ # Make sure that TCP messages are in the event log.
+ assert any("mitmproxy-will-remove-this" in m for m in self.master.log)
class AppMixin:
def test_app(self):
@@ -579,16 +628,50 @@ class TestUpstreamProxy(tservers.HTTPUpstreamProxTest, CommonMixin, AppMixin):
class TestUpstreamProxySSL(tservers.HTTPUpstreamProxTest, CommonMixin, TcpMixin):
ssl = True
+ def _host_pattern_on(self, attr):
+ """
+ Updates config.check_tcp or check_ignore, depending on attr.
+ """
+ assert not hasattr(self, "_ignore_%s_backup" % attr)
+ backup = []
+ for proxy in self.chain:
+ old_matcher = getattr(proxy.tmaster.server.config, "check_%s" % attr)
+ backup.append(old_matcher)
+ setattr(
+ proxy.tmaster.server.config,
+ "check_%s" % attr,
+ HostMatcher([".+:%s" % self.server.port] + old_matcher.patterns)
+ )
+
+ setattr(self, "_ignore_%s_backup" % attr, backup)
+
+ def _host_pattern_off(self, attr):
+ backup = getattr(self, "_ignore_%s_backup" % attr)
+ for proxy in reversed(self.chain):
+ setattr(
+ proxy.tmaster.server.config,
+ "check_%s" % attr,
+ backup.pop()
+ )
+
+ assert not backup
+ delattr(self, "_ignore_%s_backup" % attr)
+
def _ignore_on(self):
super(TestUpstreamProxySSL, self)._ignore_on()
- ignore = parse_host_pattern([".+:%s" % self.server.port])[0]
- for proxy in self.chain:
- proxy.tmaster.server.config.ignore.append(ignore)
+ self._host_pattern_on("ignore")
def _ignore_off(self):
super(TestUpstreamProxySSL, self)._ignore_off()
- for proxy in self.chain:
- proxy.tmaster.server.config.ignore.pop()
+ self._host_pattern_off("ignore")
+
+ def _tcpproxy_on(self):
+ super(TestUpstreamProxySSL, self)._tcpproxy_on()
+ self._host_pattern_on("tcp")
+
+ def _tcpproxy_off(self):
+ super(TestUpstreamProxySSL, self)._tcpproxy_off()
+ self._host_pattern_off("tcp")
def test_simple(self):
p = self.pathoc()
diff --git a/test/tools/passive_close.py b/test/tools/passive_close.py
new file mode 100644
index 00000000..d0b36e7f
--- /dev/null
+++ b/test/tools/passive_close.py
@@ -0,0 +1,21 @@
+import SocketServer
+from threading import Thread
+from time import sleep
+
+class service(SocketServer.BaseRequestHandler):
+ def handle(self):
+ data = 'dummy'
+ print "Client connected with ", self.client_address
+ while True:
+ self.request.send("HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\ncontent")
+ data = self.request.recv(1024)
+ if not len(data):
+ print "Connection closed by remote: ", self.client_address
+ sleep(3600)
+
+
+class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
+ pass
+
+server = ThreadedTCPServer(('',1520), service)
+server.serve_forever()
diff --git a/test/tservers.py b/test/tservers.py
index 93c8a80a..12154ba7 100644
--- a/test/tservers.py
+++ b/test/tservers.py
@@ -99,7 +99,7 @@ class ProxTestBase(object):
@classmethod
def teardownAll(cls):
- shutil.rmtree(cls.confdir)
+ shutil.rmtree(cls.cadir)
cls.proxy.shutdown()
cls.server.shutdown()
cls.server2.shutdown()
@@ -116,10 +116,10 @@ class ProxTestBase(object):
@classmethod
def get_proxy_config(cls):
- cls.confdir = os.path.join(tempfile.gettempdir(), "mitmproxy")
+ cls.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy")
return dict(
no_upstream_cert = cls.no_upstream_cert,
- confdir = cls.confdir,
+ cadir = cls.cadir,
authenticator = cls.authenticator,
certforward = cls.certforward,
ssl_ports=([cls.server.port, cls.server2.port] if cls.ssl else []),