aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAldo Cortesi <aldo@nullcube.com>2014-03-02 13:45:35 +1300
committerAldo Cortesi <aldo@nullcube.com>2014-03-02 13:45:35 +1300
commit091e539a0203ca272e3a4ba2a9f23331bbd85005 (patch)
treeca907e8b2983360d666d134a5000cb6a26be6512
parenta1d0da2b533b986967a8714c02d567c943d11929 (diff)
downloadmitmproxy-091e539a0203ca272e3a4ba2a9f23331bbd85005.tar.gz
mitmproxy-091e539a0203ca272e3a4ba2a9f23331bbd85005.tar.bz2
mitmproxy-091e539a0203ca272e3a4ba2a9f23331bbd85005.zip
Big improvements to SSL handling
- pathod now dynamically generates SSL certs, using the ~/.mitmproxy cacert - pathoc returns data on SSL peer certificates - Pathod certificate CN can be specified on command line - Support SSLv23
-rw-r--r--libpathod/pathoc.py18
-rw-r--r--libpathod/pathod.py44
-rwxr-xr-xpathoc4
-rwxr-xr-xpathod34
-rw-r--r--test/test_pathoc.py2
-rw-r--r--test/test_pathod.py32
-rw-r--r--test/tutils.py8
7 files changed, 101 insertions, 41 deletions
diff --git a/libpathod/pathoc.py b/libpathod/pathoc.py
index 4e807002..56708696 100644
--- a/libpathod/pathoc.py
+++ b/libpathod/pathoc.py
@@ -6,14 +6,21 @@ import language, utils
class PathocError(Exception): pass
+class SSLInfo:
+ def __init__(self, certchain):
+ self.certchain = certchain
+
+
class Response:
- def __init__(self, httpversion, status_code, msg, headers, content):
+ def __init__(self, httpversion, status_code, msg, headers, content, sslinfo):
self.httpversion, self.status_code, self.msg = httpversion, status_code, msg
self.headers, self.content = headers, content
+ self.sslinfo = sslinfo
def __repr__(self):
return "Response(%s - %s)"%(self.status_code, self.msg)
+
class Pathoc(tcp.TCPClient):
def __init__(self, address, ssl=None, sni=None, sslversion=1, clientcert=None, ciphers=None):
tcp.TCPClient.__init__(self, address)
@@ -48,6 +55,7 @@ class Pathoc(tcp.TCPClient):
tcp.TCPClient.connect(self)
if connect_to:
self.http_connect(connect_to)
+ self.sslinfo = None
if self.ssl:
try:
self.convert_to_ssl(
@@ -58,6 +66,10 @@ class Pathoc(tcp.TCPClient):
)
except tcp.NetLibError, v:
raise PathocError(str(v))
+ self.sslinfo = SSLInfo(
+ self.connection.get_peer_cert_chain()
+ )
+
def request(self, spec):
"""
@@ -69,7 +81,9 @@ class Pathoc(tcp.TCPClient):
r = language.parse_request(self.settings, spec)
language.serve(r, self.wfile, self.settings, self.address.host)
self.wfile.flush()
- return Response(*http.read_response(self.rfile, r.method, None))
+ ret = list(http.read_response(self.rfile, r.method, None))
+ ret.append(self.sslinfo)
+ return Response(*ret)
def _show_summary(self, fp, httpversion, code, msg, headers, content):
print >> fp, "<< %s %s: %s bytes"%(code, utils.xrepr(msg), len(content))
diff --git a/libpathod/pathod.py b/libpathod/pathod.py
index a8c2a29f..c0c89ff1 100644
--- a/libpathod/pathod.py
+++ b/libpathod/pathod.py
@@ -1,24 +1,37 @@
-import urllib, threading, re, logging
+import urllib, threading, re, logging, os
from netlib import tcp, http, wsgi, certutils
import netlib.utils
import version, app, language, utils
+
+DEFAULT_CERT_DOMAIN = "pathod.net"
+CONFDIR = "~/.mitmproxy"
+CA_CERT_NAME = "mitmproxy-ca.pem"
+
logger = logging.getLogger('pathod')
class PathodError(Exception): pass
class SSLOptions:
- def __init__(self, certfile=None, keyfile=None, not_after_connect=None, request_client_cert=False, sslversion=tcp.SSLv23_METHOD, ciphers=None):
- self.keyfile = keyfile or utils.data.path("resources/server.key")
- self.certfile = certfile or utils.data.path("resources/server.crt")
- self.cert = certutils.SSLCert.from_pem(file(self.certfile, "rb").read())
+ def __init__(self, confdir=CONFDIR, cn=None, certfile=None,
+ not_after_connect=None, request_client_cert=False,
+ sslversion=tcp.SSLv23_METHOD, ciphers=None):
+ self.confdir = confdir
+ self.cn = cn
+ cacert = os.path.join(confdir, CA_CERT_NAME)
+ self.cacert = os.path.expanduser(cacert)
+ if not os.path.exists(self.cacert):
+ certutils.dummy_ca(self.cacert)
+ self.certstore = certutils.CertStore(self.cacert)
+ self.certfile = certfile
self.not_after_connect = not_after_connect
self.request_client_cert = request_client_cert
self.ciphers = ciphers
self.sslversion = sslversion
+
class PathodHandler(tcp.BaseHandler):
wbufsize = 0
sni = None
@@ -78,8 +91,8 @@ class PathodHandler(tcp.BaseHandler):
if not self.server.ssloptions.not_after_connect:
try:
self.convert_to_ssl(
- self.server.ssloptions.cert,
- self.server.ssloptions.keyfile,
+ self.server.ssloptions.certstore.get_cert(DEFAULT_CERT_DOMAIN, []),
+ self.server.ssloptions.cacert,
handle_sni = self.handle_sni,
request_client_cert = self.server.ssloptions.request_client_cert,
cipher_list = self.server.ssloptions.ciphers,
@@ -186,8 +199,11 @@ class PathodHandler(tcp.BaseHandler):
if self.server.ssl:
try:
self.convert_to_ssl(
- self.server.ssloptions.cert,
- self.server.ssloptions.keyfile,
+ self.server.ssloptions.certstore.get_cert(
+ self.server.ssloptions.cn or DEFAULT_CERT_DOMAIN,
+ []
+ ),
+ self.server.ssloptions.cacert,
handle_sni = self.handle_sni,
request_client_cert = self.server.ssloptions.request_client_cert,
cipher_list = self.server.ssloptions.ciphers,
@@ -224,10 +240,12 @@ class PathodHandler(tcp.BaseHandler):
class Pathod(tcp.TCPServer):
LOGBUF = 500
- def __init__( self,
- addr, ssl=False, ssloptions=None, craftanchor="/p/", staticdir=None, anchors=None,
- sizelimit=None, noweb=False, nocraft=False, noapi=False, nohang=False,
- timeout=None, logreq=False, logresp=False, explain=False, hexdump=False
+ def __init__(
+ self, addr, confdir=CONFDIR, ssl=False, ssloptions=None,
+ craftanchor="/p/", staticdir=None, anchors=None,
+ sizelimit=None, noweb=False, nocraft=False, noapi=False,
+ nohang=False, timeout=None, logreq=False, logresp=False,
+ explain=False, hexdump=False
):
"""
addr: (address, port) tuple. If port is 0, a free port will be
diff --git a/pathoc b/pathoc
index 18faf892..8f1bd479 100755
--- a/pathoc
+++ b/pathoc
@@ -65,9 +65,9 @@ if __name__ == "__main__":
help="SSL cipher specification"
)
group.add_argument(
- "--sslversion", dest="sslversion", type=int, default=1,
+ "--sslversion", dest="sslversion", type=int, default=4,
choices=[1, 2, 3, 4],
- help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to TLSv1."
+ help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
)
group = parser.add_argument_group(
diff --git a/pathod b/pathod
index ceadfa98..5b82f97e 100755
--- a/pathod
+++ b/pathod
@@ -31,16 +31,13 @@ def daemonize (stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
def main(parser, args):
- sl = [args.ssl_keyfile, args.ssl_certfile]
- if any(sl) and not all(sl):
- parser.error("Both --certfile and --keyfile must be specified.")
-
ssloptions = pathod.SSLOptions(
- keyfile = args.ssl_keyfile,
- certfile = args.ssl_certfile,
- not_after_connect = args.ssl_not_after_connect,
- ciphers = args.ciphers,
- sslversion = utils.SSLVERSIONS[args.sslversion]
+ cn = args.cn,
+ confdir = args.confdir,
+ certfile = args.ssl_certfile,
+ not_after_connect = args.ssl_not_after_connect,
+ ciphers = args.ciphers,
+ sslversion = utils.SSLVERSIONS[args.sslversion]
)
alst = []
@@ -122,6 +119,11 @@ if __name__ == "__main__":
help='Anchorpoint for URL crafting commands.'
)
parser.add_argument(
+ "--confdir",
+ action="store", type = str, dest="confdir", default='~/.mitmproxy',
+ help = "Configuration directory. (~/.mitmproxy)"
+ )
+ parser.add_argument(
"-d", dest='staticdir', default=None, type=str,
help='Directory for static files.'
)
@@ -159,16 +161,16 @@ if __name__ == "__main__":
'SSL',
)
group.add_argument(
- "-C", dest='ssl_not_after_connect', default=False, action="store_true",
- help="Don't expect SSL after a CONNECT request."
- )
- group.add_argument(
"-s", dest='ssl', default=False, action="store_true",
help='Run in HTTPS mode.'
)
group.add_argument(
- "--keyfile", dest='ssl_keyfile', default=None, type=str,
- help='SSL key file. If not specified, a default key is used.'
+ "--cn", dest="cn", type=str, default=None,
+ help="CN for generated SSL certs. Default: %s"%pathod.DEFAULT_CERT_DOMAIN
+ )
+ group.add_argument(
+ "-C", dest='ssl_not_after_connect', default=False, action="store_true",
+ help="Don't expect SSL after a CONNECT request."
)
group.add_argument(
"--certfile", dest='ssl_certfile', default=None, type=str,
@@ -181,7 +183,7 @@ if __name__ == "__main__":
group.add_argument(
"--sslversion", dest="sslversion", type=int, default=4,
choices=[1, 2, 3, 4],
- help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
+ help="Use a specified protocol - TLSv1, SSLv2, SSLv3, SSLv23. Default to SSLv23."
)
group = parser.add_argument_group(
diff --git a/test/test_pathoc.py b/test/test_pathoc.py
index d96a1728..5d676d25 100644
--- a/test/test_pathoc.py
+++ b/test/test_pathoc.py
@@ -3,7 +3,7 @@ from libpathod import pathoc, test, version, pathod
import tutils
def test_response():
- r = pathoc.Response("1.1", 200, "Message", {}, None)
+ r = pathoc.Response("1.1", 200, "Message", {}, None, None)
assert repr(r)
diff --git a/test/test_pathod.py b/test/test_pathod.py
index 9ab6d66d..6fc31677 100644
--- a/test/test_pathod.py
+++ b/test/test_pathod.py
@@ -1,3 +1,4 @@
+import pprint
from libpathod import pathod, version
from netlib import tcp, http
import requests
@@ -54,12 +55,26 @@ class TestNoApi(tutils.DaemonTests):
class TestNotAfterConnect(tutils.DaemonTests):
ssl = False
- not_after_connect = True
+ ssloptions = dict(
+ not_after_connect = True
+ )
def test_connect(self):
r = self.pathoc(r"get:'http://foo.com/p/202':da", connect_to=("localhost", self.d.port))
assert r.status_code == 202
+class TestSSLCN(tutils.DaemonTests):
+ ssl = True
+ ssloptions = dict(
+ cn = "foo.com"
+ )
+ def test_connect(self):
+ r = self.pathoc(r"get:/p/202")
+ assert r.status_code == 202
+ assert r.sslinfo
+ assert r.sslinfo.certchain[0].get_subject().CN == "foo.com"
+
+
class TestNohang(tutils.DaemonTests):
nohang = True
def test_nohang(self):
@@ -159,11 +174,20 @@ class CommonTests(tutils.DaemonTests):
class TestDaemon(CommonTests):
ssl = False
def test_connect(self):
- r = self.pathoc(r"get:'http://foo.com/p/202':da", connect_to=("localhost", self.d.port), ssl=True)
+ r = self.pathoc(
+ r"get:'http://foo.com/p/202':da",
+ connect_to=("localhost", self.d.port),
+ ssl=True
+ )
assert r.status_code == 202
def test_connect_err(self):
- tutils.raises(http.HttpError, self.pathoc, r"get:'http://foo.com/p/202':da", connect_to=("localhost", self.d.port))
+ tutils.raises(
+ http.HttpError,
+ self.pathoc,
+ r"get:'http://foo.com/p/202':da",
+ connect_to=("localhost", self.d.port)
+ )
class TestDaemonSSL(CommonTests):
@@ -182,5 +206,3 @@ class TestDaemonSSL(CommonTests):
assert l["type"] == "error"
assert "SSL" in l["msg"]
-
-
diff --git a/test/tutils.py b/test/tutils.py
index 1baf16e2..2c3a2c9d 100644
--- a/test/tutils.py
+++ b/test/tutils.py
@@ -10,10 +10,13 @@ class DaemonTests:
ssl = False
timeout = None
hexdump = False
- not_after_connect = False
+ ssloptions = None
@classmethod
def setUpAll(self):
- so = pathod.SSLOptions(not_after_connect = self.not_after_connect)
+ opts = self.ssloptions or {}
+ self.confdir = tempfile.mkdtemp()
+ opts["confdir"] = self.confdir
+ so = pathod.SSLOptions(**opts)
self.d = test.Daemon(
staticdir=test_data.path("data"),
anchors=[("/anchor/.*", "202:da")],
@@ -33,6 +36,7 @@ class DaemonTests:
@classmethod
def tearDownAll(self):
self.d.shutdown()
+ shutil.rmtree(self.confdir)
def setUp(self):
if not (self.noweb or self.noapi):