diff options
-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): |