mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 00:01:36 +00:00
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:
parent
a1d0da2b53
commit
091e539a02
@ -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))
|
||||
|
@ -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
4
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(
|
||||
|
34
pathod
34
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 = []
|
||||
@ -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,
|
||||
@ -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(
|
||||
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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"]
|
||||
|
||||
|
||||
|
||||
|
@ -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):
|
||||
|
Loading…
Reference in New Issue
Block a user