diff options
| author | Aldo Cortesi <aldo@nullcube.com> | 2014-03-02 13:45:35 +1300 | 
|---|---|---|
| committer | Aldo Cortesi <aldo@nullcube.com> | 2014-03-02 13:45:35 +1300 | 
| commit | 091e539a0203ca272e3a4ba2a9f23331bbd85005 (patch) | |
| tree | ca907e8b2983360d666d134a5000cb6a26be6512 | |
| parent | a1d0da2b533b986967a8714c02d567c943d11929 (diff) | |
| download | mitmproxy-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.py | 18 | ||||
| -rw-r--r-- | libpathod/pathod.py | 44 | ||||
| -rwxr-xr-x | pathoc | 4 | ||||
| -rwxr-xr-x | pathod | 34 | ||||
| -rw-r--r-- | test/test_pathoc.py | 2 | ||||
| -rw-r--r-- | test/test_pathod.py | 32 | ||||
| -rw-r--r-- | test/tutils.py | 8 | 
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 @@ -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( @@ -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): | 
