Add --allow_hosts option

Closes #3295
This commit is contained in:
Pierre Gordon 2019-04-19 12:39:53 -05:00
parent 8353f4a55a
commit 8d0c800d15
6 changed files with 82 additions and 15 deletions

View File

@ -67,6 +67,10 @@ class Options(optmanager.OptManager):
regular expression and matched on the ip or the hostname. regular expression and matched on the ip or the hostname.
""" """
) )
self.add_option(
"allow_hosts", Sequence[str], [],
"Opposite of --ignore_hosts."
)
self.add_option( self.add_option(
"listen_host", str, "", "listen_host", str, "",
"Address to bind proxy to." "Address to bind proxy to."

View File

@ -14,7 +14,8 @@ CONF_BASENAME = "mitmproxy"
class HostMatcher: class HostMatcher:
def __init__(self, patterns=tuple()): def __init__(self, handle, patterns=tuple()):
self.handle = handle
self.patterns = list(patterns) self.patterns = list(patterns)
self.regexes = [re.compile(p, re.IGNORECASE) for p in self.patterns] self.regexes = [re.compile(p, re.IGNORECASE) for p in self.patterns]
@ -22,8 +23,10 @@ class HostMatcher:
if not address: if not address:
return False return False
host = "%s:%s" % address host = "%s:%s" % address
if any(rex.search(host) for rex in self.regexes): if self.handle in ["ignore", "tcp"]:
return True return any(rex.search(host) for rex in self.regexes)
elif self.handle == "allow":
return any(not rex.search(host) for rex in self.regexes)
else: else:
return False return False
@ -36,7 +39,7 @@ class ProxyConfig:
def __init__(self, options: moptions.Options) -> None: def __init__(self, options: moptions.Options) -> None:
self.options = options self.options = options
self.check_ignore: HostMatcher = None self.check_filter: HostMatcher = None
self.check_tcp: HostMatcher = None self.check_tcp: HostMatcher = None
self.certstore: certs.CertStore = None self.certstore: certs.CertStore = None
self.upstream_server: typing.Optional[server_spec.ServerSpec] = None self.upstream_server: typing.Optional[server_spec.ServerSpec] = None
@ -44,10 +47,18 @@ class ProxyConfig:
options.changed.connect(self.configure) options.changed.connect(self.configure)
def configure(self, options: moptions.Options, updated: typing.Any) -> None: def configure(self, options: moptions.Options, updated: typing.Any) -> None:
if "ignore_hosts" in updated: if options.allow_hosts and options.ignore_hosts:
self.check_ignore = HostMatcher(options.ignore_hosts) raise exceptions.OptionsError("--ignore-hosts and --allow-hosts are mutually "
"exclusive; please choose one.")
if options.ignore_hosts:
self.check_filter = HostMatcher("ignore", options.ignore_hosts)
elif options.allow_hosts:
self.check_filter = HostMatcher("allow", options.allow_hosts)
else:
self.check_filter = HostMatcher(False)
if "tcp_hosts" in updated: if "tcp_hosts" in updated:
self.check_tcp = HostMatcher(options.tcp_hosts) self.check_tcp = HostMatcher("tcp", options.tcp_hosts)
certstore_path = os.path.expanduser(options.confdir) certstore_path = os.path.expanduser(options.confdir)
if not os.path.exists(os.path.dirname(certstore_path)): if not os.path.exists(os.path.dirname(certstore_path)):

View File

@ -48,17 +48,17 @@ class RootContext:
raise exceptions.ProtocolException(str(e)) raise exceptions.ProtocolException(str(e))
client_tls = tls.is_tls_record_magic(d) client_tls = tls.is_tls_record_magic(d)
# 1. check for --ignore # 1. check for filter
if self.config.check_ignore: if self.config.check_filter:
ignore = self.config.check_ignore(top_layer.server_conn.address) is_filtered = self.config.check_filter(top_layer.server_conn.address)
if not ignore and client_tls: if not is_filtered and client_tls:
try: try:
client_hello = tls.ClientHello.from_file(self.client_conn.rfile) client_hello = tls.ClientHello.from_file(self.client_conn.rfile)
except exceptions.TlsProtocolException as e: except exceptions.TlsProtocolException as e:
self.log("Cannot parse Client Hello: %s" % repr(e), "error") self.log("Cannot parse Client Hello: %s" % repr(e), "error")
else: else:
ignore = self.config.check_ignore((client_hello.sni, 443)) is_filtered = self.config.check_filter((client_hello.sni, 443))
if ignore: if is_filtered:
return protocol.RawTCPLayer(top_layer, ignore=True) return protocol.RawTCPLayer(top_layer, ignore=True)
# 2. Always insert a TLS layer, even if there's neither client nor server tls. # 2. Always insert a TLS layer, even if there's neither client nor server tls.

View File

@ -65,6 +65,7 @@ def common_options(parser, opts):
opts.make_parser(group, "listen_port", metavar="PORT", short="p") opts.make_parser(group, "listen_port", metavar="PORT", short="p")
opts.make_parser(group, "server", short="n") opts.make_parser(group, "server", short="n")
opts.make_parser(group, "ignore_hosts", metavar="HOST") opts.make_parser(group, "ignore_hosts", metavar="HOST")
opts.make_parser(group, "allow_hosts", metavar="HOST")
opts.make_parser(group, "tcp_hosts", metavar="HOST") opts.make_parser(group, "tcp_hosts", metavar="HOST")
opts.make_parser(group, "upstream_auth", metavar="USER:PASS") opts.make_parser(group, "upstream_auth", metavar="USER:PASS")
opts.make_parser(group, "proxyauth", metavar="SPEC") opts.make_parser(group, "proxyauth", metavar="SPEC")

View File

@ -215,6 +215,10 @@ class StatusBar(urwid.WidgetWrap):
r.append("[") r.append("[")
r.append(("heading_key", "I")) r.append(("heading_key", "I"))
r.append("gnore:%d]" % len(self.master.options.ignore_hosts)) r.append("gnore:%d]" % len(self.master.options.ignore_hosts))
elif self.master.options.allow_hosts:
r.append("[")
r.append(("heading_key", "A"))
r.append("llow:%d]" % len(self.master.options.allow_hosts))
if self.master.options.tcp_hosts: if self.master.options.tcp_hosts:
r.append("[") r.append("[")
r.append(("heading_key", "T")) r.append(("heading_key", "T"))

View File

@ -78,6 +78,16 @@ class TcpMixin:
self.options.ignore_hosts = self._ignore_backup self.options.ignore_hosts = self._ignore_backup
del self._ignore_backup del self._ignore_backup
def _allow_on(self):
assert not hasattr(self, "_allow_backup")
self._allow_backup = self.options.allow_hosts
self.options.allow_hosts = ["(127.0.0.1|None):\\d+"] + self.options.allow_hosts
def _allow_off(self):
assert hasattr(self, "_allow_backup")
self.options.allow_hosts = self._allow_backup
del self._allow_backup
def test_ignore(self): def test_ignore(self):
n = self.pathod("304") n = self.pathod("304")
self._ignore_on() self._ignore_on()
@ -111,6 +121,40 @@ class TcpMixin:
self._ignore_off() self._ignore_off()
def test_allow(self):
n = self.pathod("304")
self._allow_on()
i = self.pathod("305")
i2 = self.pathod("306")
self._allow_off()
assert n.status_code == 304
assert i.status_code == 305
assert i2.status_code == 306
assert any(f.response.status_code == 304 for f in self.master.state.flows)
assert any(f.response.status_code == 305 for f in self.master.state.flows)
assert any(f.response.status_code == 306 for f in self.master.state.flows)
# Test that we get the original SSL cert
if self.ssl:
i_cert = certs.Cert(i.sslinfo.certchain[0])
i2_cert = certs.Cert(i2.sslinfo.certchain[0])
n_cert = certs.Cert(n.sslinfo.certchain[0])
assert i_cert == i2_cert
assert i_cert != n_cert
# Test Non-HTTP traffic
spec = "200:i0,@100:d0" # this results in just 100 random bytes
# mitmproxy responds with bad gateway
assert self.pathod(spec).status_code == 502
self._allow_on()
self.pathod(spec) # pathoc parses answer as HTTP
self._allow_off()
def _tcpproxy_on(self): def _tcpproxy_on(self):
assert not hasattr(self, "_tcpproxy_backup") assert not hasattr(self, "_tcpproxy_backup")
self._tcpproxy_backup = self.options.tcp_hosts self._tcpproxy_backup = self.options.tcp_hosts
@ -852,10 +896,12 @@ class TestUpstreamProxySSL(
def _host_pattern_on(self, attr): def _host_pattern_on(self, attr):
""" """
Updates config.check_tcp or check_ignore, depending on attr. Updates config.check_tcp or check_filter, depending on attr.
""" """
assert not hasattr(self, "_ignore_%s_backup" % attr) assert not hasattr(self, "_ignore_%s_backup" % attr)
backup = [] backup = []
handle = attr
attr = "filter" if attr in ["allow", "ignore"] else attr
for proxy in self.chain: for proxy in self.chain:
old_matcher = getattr( old_matcher = getattr(
proxy.tmaster.server.config, proxy.tmaster.server.config,
@ -865,12 +911,13 @@ class TestUpstreamProxySSL(
setattr( setattr(
proxy.tmaster.server.config, proxy.tmaster.server.config,
"check_%s" % attr, "check_%s" % attr,
HostMatcher([".+:%s" % self.server.port] + old_matcher.patterns) HostMatcher(handle, [".+:%s" % self.server.port] + old_matcher.patterns)
) )
setattr(self, "_ignore_%s_backup" % attr, backup) setattr(self, "_ignore_%s_backup" % attr, backup)
def _host_pattern_off(self, attr): def _host_pattern_off(self, attr):
attr = "filter" if attr in ["allow", "ignore"] else attr
backup = getattr(self, "_ignore_%s_backup" % attr) backup = getattr(self, "_ignore_%s_backup" % attr)
for proxy in reversed(self.chain): for proxy in reversed(self.chain):
setattr( setattr(