Unify mode specification

We now have:

    --mode regular (the default)
    --mode transparent
    --mode socks5
    --mode reverse:SPEC
    --mode upstream:SPEC

Where SPEC is a host specification.
This commit is contained in:
Aldo Cortesi 2017-03-06 20:30:49 +13:00
parent f5fb6972aa
commit 82163a1e68
14 changed files with 80 additions and 129 deletions

View File

@ -3,18 +3,37 @@
checked by other addons. checked by other addons.
""" """
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import options
from mitmproxy import platform
from mitmproxy.utils import human from mitmproxy.utils import human
class Core: class Core:
def configure(self, options, updated): def configure(self, opts, updated):
if "body_size_limit" in updated and options.body_size_limit: if "body_size_limit" in updated and opts.body_size_limit:
try: try:
options._processed["body_size_limit"] = human.parse_size( opts._processed["body_size_limit"] = human.parse_size(
options.body_size_limit opts.body_size_limit
) )
except ValueError as e: except ValueError as e:
raise exceptions.OptionsError( raise exceptions.OptionsError(
"Invalid body size limit specification: %s" % "Invalid body size limit specification: %s" %
options.body_size_limit opts.body_size_limit
)
if "mode" in updated:
mode = opts.mode
if mode.startswith("reverse:") or mode.startswith("upstream:"):
spec = options.get_mode_spec(mode)
if not spec:
raise exceptions.OptionsError(
"Invalid mode specification: %s" % mode
)
elif mode == "transparent":
if not platform.original_addr:
raise exceptions.OptionsError(
"Transparent mode not supported on this platform."
)
elif mode not in ["regular", "socks5"]:
raise exceptions.OptionsError(
"Invalid mode specification: %s" % mode
) )

View File

@ -148,7 +148,7 @@ class Master:
Loads a flow Loads a flow
""" """
if isinstance(f, http.HTTPFlow): if isinstance(f, http.HTTPFlow):
if self.server and self.options.mode == "reverse": if self.server and self.options.mode.startswith("reverse:"):
f.request.host = self.server.config.upstream_server.address[0] f.request.host = self.server.config.upstream_server.address[0]
f.request.port = self.server.config.upstream_server.address[1] f.request.port = self.server.config.upstream_server.address[1]
f.request.scheme = self.server.config.upstream_server.scheme f.request.scheme = self.server.config.upstream_server.scheme

View File

@ -22,6 +22,11 @@ view_orders = [
"size", "size",
] ]
def get_mode_spec(m):
return m.split(":", maxsplit=1)[1]
APP_HOST = "mitm.it" APP_HOST = "mitm.it"
APP_PORT = 80 APP_PORT = 80
CA_DIR = "~/.mitmproxy" CA_DIR = "~/.mitmproxy"
@ -247,7 +252,14 @@ class Options(optmanager.OptManager):
"upstream_bind_address", "", str, "upstream_bind_address", "", str,
"Address to bind upstream requests to (defaults to none)" "Address to bind upstream requests to (defaults to none)"
) )
self.add_option("mode", "regular", str) self.add_option(
"mode", "regular", str,
"""
Mode can be "regular", "transparent", "socks5", "reverse:SPEC",
or "upstream:SPEC". For reverse and upstream proxy modes, SPEC
is proxy specification in the form of "http[s]://host[:port]".
"""
)
self.add_option( self.add_option(
"upstream_cert", True, bool, "upstream_cert", True, bool,
"Connect to upstream server to look up certificate details." "Connect to upstream server to look up certificate details."
@ -285,7 +297,6 @@ class Options(optmanager.OptManager):
"Use the client's IP for server-side connections. " "Use the client's IP for server-side connections. "
"Combine with --upstream-bind-address to spoof a fixed source address." "Combine with --upstream-bind-address to spoof a fixed source address."
) )
self.add_option("upstream_server", None, Optional[str])
self.add_option( self.add_option(
"upstream_auth", None, Optional[str], "upstream_auth", None, Optional[str],
""" """

View File

@ -121,7 +121,7 @@ class ProxyConfig:
raise exceptions.OptionsError( raise exceptions.OptionsError(
"Invalid certificate format: %s" % cert "Invalid certificate format: %s" % cert
) )
m = options.mode
self.upstream_server = None if m.startswith("upstream:") or m.startswith("reverse:"):
if options.upstream_server: spec = moptions.get_mode_spec(options.mode)
self.upstream_server = parse_server_spec(options.upstream_server) self.upstream_server = parse_server_spec(spec)

View File

@ -290,7 +290,7 @@ class HttpLayer(base.Layer):
request.first_line_format = "relative" request.first_line_format = "relative"
# update host header in reverse proxy mode # update host header in reverse proxy mode
if self.config.options.mode == "reverse" and not self.config.options.keep_host_header: if self.config.options.mode.startswith("reverse:") and not self.config.options.keep_host_header:
f.request.host_header = self.config.upstream_server.address[0] f.request.host_header = self.config.upstream_server.address[0]
# Determine .scheme, .host and .port attributes for inline scripts. For # Determine .scheme, .host and .port attributes for inline scripts. For

View File

@ -45,7 +45,7 @@ class RequestReplayThread(basethread.BaseThread):
if not self.f.response: if not self.f.response:
# In all modes, we directly connect to the server displayed # In all modes, we directly connect to the server displayed
if self.config.options.mode == "upstream": if self.config.options.mode.startswith("upstream:"):
server_address = self.config.upstream_server.address server_address = self.config.upstream_server.address
server = connections.ServerConnection(server_address, (self.config.options.listen_host, 0)) server = connections.ServerConnection(server_address, (self.config.options.listen_host, 0))
server.connect() server.connect()

View File

@ -85,14 +85,14 @@ class ConnectionHandler:
) )
mode = self.config.options.mode mode = self.config.options.mode
if mode == "upstream": if mode.startswith("upstream:"):
return modes.HttpUpstreamProxy( return modes.HttpUpstreamProxy(
root_ctx, root_ctx,
self.config.upstream_server.address self.config.upstream_server.address
) )
elif mode == "transparent": elif mode == "transparent":
return modes.TransparentProxy(root_ctx) return modes.TransparentProxy(root_ctx)
elif mode == "reverse": elif mode.startswith("reverse:"):
server_tls = self.config.upstream_server.scheme == "https" server_tls = self.config.upstream_server.scheme == "https"
return modes.ReverseProxy( return modes.ReverseProxy(
root_ctx, root_ctx,

View File

@ -3,7 +3,6 @@ import os
from mitmproxy import exceptions from mitmproxy import exceptions
from mitmproxy import options from mitmproxy import options
from mitmproxy import platform
from mitmproxy import version from mitmproxy import version
@ -15,34 +14,6 @@ class ParseException(Exception):
def get_common_options(args): def get_common_options(args):
# Establish proxy mode
c = 0
mode, upstream_server = "regular", None
if args.transparent_proxy:
c += 1
if not platform.original_addr:
raise exceptions.OptionsError(
"Transparent mode not supported on this platform."
)
mode = "transparent"
if args.socks_proxy:
c += 1
mode = "socks5"
if args.reverse_proxy:
c += 1
mode = "reverse"
upstream_server = args.reverse_proxy
if args.upstream_proxy:
c += 1
mode = "upstream"
upstream_server = args.upstream_proxy
if c > 1:
raise exceptions.OptionsError(
"Transparent, SOCKS5, reverse and upstream proxy mode "
"are mutually exclusive. Read the docs on proxy modes "
"to understand why."
)
if args.add_upstream_certs_to_client_chain and not args.upstream_cert: if args.add_upstream_certs_to_client_chain and not args.upstream_cert:
raise exceptions.OptionsError( raise exceptions.OptionsError(
"The no-upstream-cert and add-upstream-certs-to-client-chain " "The no-upstream-cert and add-upstream-certs-to-client-chain "
@ -99,7 +70,7 @@ def get_common_options(args):
listen_host = args.listen_host, listen_host = args.listen_host,
listen_port = args.listen_port, listen_port = args.listen_port,
upstream_bind_address = args.upstream_bind_address, upstream_bind_address = args.upstream_bind_address,
mode = mode, mode = args.mode,
upstream_cert = args.upstream_cert, upstream_cert = args.upstream_cert,
spoof_source_address = args.spoof_source_address, spoof_source_address = args.spoof_source_address,
@ -108,7 +79,6 @@ def get_common_options(args):
websocket = args.websocket, websocket = args.websocket,
rawtcp = args.rawtcp, rawtcp = args.rawtcp,
upstream_server = upstream_server,
upstream_auth = args.upstream_auth, upstream_auth = args.upstream_auth,
ssl_version_client = args.ssl_version_client, ssl_version_client = args.ssl_version_client,
ssl_version_server = args.ssl_version_server, ssl_version_server = args.ssl_version_server,
@ -154,39 +124,6 @@ def basic_options(parser, opts):
opts.make_parser(parser, "stream_large_bodies") opts.make_parser(parser, "stream_large_bodies")
def proxy_modes(parser, opts):
group = parser.add_argument_group("Proxy Modes")
group.add_argument(
"-R", "--reverse",
action="store",
type=str,
dest="reverse_proxy",
help="""
Forward all requests to upstream HTTP server:
http[s]://host[:port]. Clients can always connect both
via HTTPS and HTTP, the connection to the server is
determined by the specified scheme.
"""
)
group.add_argument(
"--socks",
action="store_true", dest="socks_proxy",
help="Set SOCKS5 proxy mode."
)
group.add_argument(
"-T", "--transparent",
action="store_true", dest="transparent_proxy",
help="Set transparent proxy mode."
)
group.add_argument(
"-U", "--upstream",
action="store",
type=str,
dest="upstream_proxy",
help="Forward all requests to upstream proxy server: http://host[:port]"
)
def proxy_options(parser, opts): def proxy_options(parser, opts):
group = parser.add_argument_group("Proxy Options") group = parser.add_argument_group("Proxy Options")
opts.make_parser(group, "listen_host") opts.make_parser(group, "listen_host")
@ -315,8 +252,8 @@ def common_options(parser, opts):
metavar="PATH", metavar="PATH",
help="Configuration file" help="Configuration file"
) )
opts.make_parser(parser, "mode")
basic_options(parser, opts) basic_options(parser, opts)
proxy_modes(parser, opts)
proxy_options(parser, opts) proxy_options(parser, opts)
proxy_ssl_options(parser, opts) proxy_ssl_options(parser, opts)
onboarding_app(parser, opts) onboarding_app(parser, opts)

View File

@ -2,7 +2,6 @@ import os.path
import urwid import urwid
import mitmproxy.net.http.url
from mitmproxy.tools.console import common from mitmproxy.tools.console import common
from mitmproxy.tools.console import pathedit from mitmproxy.tools.console import pathedit
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
@ -234,13 +233,8 @@ class StatusBar(urwid.WidgetWrap):
if opts: if opts:
r.append("[%s]" % (":".join(opts))) r.append("[%s]" % (":".join(opts)))
if self.master.options.mode in ["reverse", "upstream"]: if self.master.options.mode != "regular":
dst = self.master.server.config.upstream_server r.append("[%s]" % self.master.options.mode)
r.append("[dest:%s]" % mitmproxy.net.http.url.unparse(
dst.scheme,
dst.address[0],
dst.address[1],
))
if self.master.options.scripts: if self.master.options.scripts:
r.append("[") r.append("[")
r.append(("heading_key", "s")) r.append(("heading_key", "s"))

View File

@ -2,6 +2,7 @@ from mitmproxy import exceptions
from mitmproxy.addons import core from mitmproxy.addons import core
from mitmproxy.test import taddons from mitmproxy.test import taddons
import pytest import pytest
from unittest import mock
def test_simple(): def test_simple():
@ -11,3 +12,20 @@ def test_simple():
tctx.configure(sa, body_size_limit = "invalid") tctx.configure(sa, body_size_limit = "invalid")
tctx.configure(sa, body_size_limit = "1m") tctx.configure(sa, body_size_limit = "1m")
assert tctx.options._processed["body_size_limit"] assert tctx.options._processed["body_size_limit"]
@mock.patch("mitmproxy.platform.original_addr", None)
def test_no_transparent():
sa = core.Core()
with taddons.context() as tctx:
with pytest.raises(Exception, match="Transparent mode not supported"):
tctx.configure(sa, mode = "transparent")
@mock.patch("mitmproxy.platform.original_addr")
def test_modes(m):
sa = core.Core()
with taddons.context() as tctx:
tctx.configure(sa, mode = "reverse:http://localhost")
with pytest.raises(Exception, match="Invalid mode"):
tctx.configure(sa, mode = "reverse:")

View File

@ -10,7 +10,7 @@ from mitmproxy import options
from mitmproxy.addons import script from mitmproxy.addons import script
from mitmproxy.addons import proxyauth from mitmproxy.addons import proxyauth
from mitmproxy import http from mitmproxy import http
from mitmproxy.proxy.config import HostMatcher, parse_server_spec from mitmproxy.proxy.config import HostMatcher
import mitmproxy.net.http import mitmproxy.net.http
from mitmproxy.net import tcp from mitmproxy.net import tcp
from mitmproxy.net import socks from mitmproxy.net import socks
@ -579,8 +579,6 @@ class TestHttps2Http(tservers.ReverseProxyTest):
@classmethod @classmethod
def get_options(cls): def get_options(cls):
opts = super().get_options() opts = super().get_options()
s = parse_server_spec(opts.upstream_server)
opts.upstream_server = "http://{}:{}".format(s.address[0], s.address[1])
return opts return opts
def pathoc(self, ssl, sni=None): def pathoc(self, ssl, sni=None):

View File

@ -63,8 +63,7 @@ class TestSerialize:
r = self._treader() r = self._treader()
s = tservers.TestState() s = tservers.TestState()
opts = options.Options( opts = options.Options(
mode="reverse", mode="reverse:https://use-this-domain"
upstream_server="https://use-this-domain"
) )
conf = ProxyConfig(opts) conf = ProxyConfig(opts)
fm = master.Master(opts, DummyServer(conf)) fm = master.Master(opts, DummyServer(conf))

View File

@ -49,31 +49,6 @@ class TestProcessProxyOptions:
with tutils.tmpdir() as cadir: with tutils.tmpdir() as cadir:
self.assert_noerr("--cadir", cadir) self.assert_noerr("--cadir", cadir)
@mock.patch("mitmproxy.platform.original_addr", None)
def test_no_transparent(self):
with pytest.raises(Exception, match="Transparent mode not supported"):
self.p("-T")
@mock.patch("mitmproxy.platform.original_addr")
def test_modes(self, _):
self.assert_noerr("-R", "http://localhost")
with pytest.raises(Exception, match="expected one argument"):
self.p("-R")
with pytest.raises(Exception, match="Invalid server specification"):
self.p("-R", "reverse")
self.assert_noerr("-T")
self.assert_noerr("-U", "http://localhost")
with pytest.raises(Exception, match="Invalid server specification"):
self.p("-U", "upstream")
self.assert_noerr("--upstream-auth", "test:test")
with pytest.raises(Exception, match="expected one argument"):
self.p("--upstream-auth")
with pytest.raises(Exception, match="mutually exclusive"):
self.p("-R", "http://localhost", "-T")
def test_client_certs(self): def test_client_certs(self):
with tutils.tmpdir() as cadir: with tutils.tmpdir() as cadir:
self.assert_noerr("--client-certs", cadir) self.assert_noerr("--client-certs", cadir)
@ -131,19 +106,19 @@ class TestDummyServer:
class TestConnectionHandler: class TestConnectionHandler:
def test_fatal_error(self, capsys): def test_fatal_error(self, capsys):
config = mock.Mock() opts = options.Options()
root_layer = mock.Mock() pconf = config.ProxyConfig(opts)
root_layer.side_effect = RuntimeError
config.options.mode.return_value = root_layer
channel = mock.Mock() channel = mock.Mock()
def ask(_, x): def ask(_, x):
return x raise RuntimeError
channel.ask = ask channel.ask = ask
c = ConnectionHandler( c = ConnectionHandler(
mock.MagicMock(), mock.MagicMock(),
("127.0.0.1", 8080), ("127.0.0.1", 8080),
config, pconf,
channel channel
) )
c.handle() c.handle()

View File

@ -288,7 +288,7 @@ class ReverseProxyTest(ProxyTestBase):
@classmethod @classmethod
def get_options(cls): def get_options(cls):
opts = ProxyTestBase.get_options() opts = ProxyTestBase.get_options()
opts.upstream_server = "".join( s = "".join(
[ [
"https" if cls.ssl else "http", "https" if cls.ssl else "http",
"://", "://",
@ -296,7 +296,7 @@ class ReverseProxyTest(ProxyTestBase):
str(cls.server.port) str(cls.server.port)
] ]
) )
opts.mode = "reverse" opts.mode = "reverse:" + s
return opts return opts
def pathoc(self, sni=None): def pathoc(self, sni=None):
@ -373,9 +373,9 @@ class ChainProxyTest(ProxyTestBase):
def get_options(cls): def get_options(cls):
opts = super().get_options() opts = super().get_options()
if cls.chain: # First proxy is in normal mode. if cls.chain: # First proxy is in normal mode.
s = "http://127.0.0.1:%s" % cls.chain[0].port
opts.update( opts.update(
mode="upstream", mode="upstream:" + s,
upstream_server="http://127.0.0.1:%s" % cls.chain[0].port
) )
return opts return opts