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
This commit is contained in:
Aldo Cortesi 2014-03-02 13:45:35 +13:00
parent a1d0da2b53
commit 091e539a02
7 changed files with 101 additions and 41 deletions

View File

@ -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))

View File

@ -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

4
pathoc
View File

@ -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(

32
pathod
View File

@ -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 = []
@ -121,6 +118,11 @@ if __name__ == "__main__":
"-c", dest='craftanchor', default="/p/", type=str,
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.'
@ -158,17 +160,17 @@ if __name__ == "__main__":
group = parser.add_argument_group(
'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,

View File

@ -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)

View File

@ -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"]

View File

@ -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):