Merge pull request #3109 from cortesi/kmap

console keybindings: define a yaml format, load CONFDIR/keys.yaml on startup
This commit is contained in:
Aldo Cortesi 2018-05-10 17:30:49 +12:00 committed by GitHub
commit ab89079c65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 231 additions and 35 deletions

View File

@ -423,7 +423,7 @@
24 July 2015: mitmproxy 0.13 24 July 2015: mitmproxy 0.13
* Upstream certificate validation. See the --verify-upstream-cert, * Upstream certificate validation. See the --verify-upstream-cert,
--upstream-trusted-cadir and --upstream-trusted-ca parameters. Thanks to --upstream-trusted-confdir and --upstream-trusted-ca parameters. Thanks to
Kyle Morton (github.com/kyle-m) for his work on this. Kyle Morton (github.com/kyle-m) for his work on this.
* Add HTTP transparent proxy mode. This uses the host headers from HTTP * Add HTTP transparent proxy mode. This uses the host headers from HTTP

View File

@ -143,7 +143,7 @@ mitmproxy --cert *.example.com=cert.pem
By default, mitmproxy will use `~/.mitmproxy/mitmproxy-ca.pem` as the By default, mitmproxy will use `~/.mitmproxy/mitmproxy-ca.pem` as the
certificate authority to generate certificates for all domains for which certificate authority to generate certificates for all domains for which
no custom certificate is provided (see above). You can use your own no custom certificate is provided (see above). You can use your own
certificate authority by passing the `--set cadir=DIRECTORY` option to certificate authority by passing the `--set confdir=DIRECTORY` option to
mitmproxy. Mitmproxy will then look for `mitmproxy-ca.pem` in the mitmproxy. Mitmproxy will then look for `mitmproxy-ca.pem` in the
specified directory. If no such file exists, it will be generated specified directory. If no such file exists, it will be generated
automatically. automatically.

View File

@ -14,7 +14,7 @@ from mitmproxy.net.http import status_codes
import mitmproxy.types import mitmproxy.types
CA_DIR = "~/.mitmproxy" CONF_DIR = "~/.mitmproxy"
LISTEN_PORT = 8080 LISTEN_PORT = 8080

View File

@ -45,7 +45,7 @@ class PEM(tornado.web.RequestHandler):
return config.CONF_BASENAME + "-ca-cert.pem" return config.CONF_BASENAME + "-ca-cert.pem"
def head(self): def head(self):
p = os.path.join(self.request.master.options.cadir, self.filename) p = os.path.join(self.request.master.options.confdir, self.filename)
p = os.path.expanduser(p) p = os.path.expanduser(p)
content_length = os.path.getsize(p) content_length = os.path.getsize(p)
@ -57,7 +57,7 @@ class PEM(tornado.web.RequestHandler):
self.set_header("Content-Length", content_length) self.set_header("Content-Length", content_length)
def get(self): def get(self):
p = os.path.join(self.request.master.options.cadir, self.filename) p = os.path.join(self.request.master.options.confdir, self.filename)
p = os.path.expanduser(p) p = os.path.expanduser(p)
self.set_header("Content-Type", "application/x-x509-ca-cert") self.set_header("Content-Type", "application/x-x509-ca-cert")
self.set_header( self.set_header(
@ -76,7 +76,7 @@ class P12(tornado.web.RequestHandler):
return config.CONF_BASENAME + "-ca-cert.p12" return config.CONF_BASENAME + "-ca-cert.p12"
def head(self): def head(self):
p = os.path.join(self.request.master.options.cadir, self.filename) p = os.path.join(self.request.master.options.confdir, self.filename)
p = os.path.expanduser(p) p = os.path.expanduser(p)
content_length = os.path.getsize(p) content_length = os.path.getsize(p)
@ -89,7 +89,7 @@ class P12(tornado.web.RequestHandler):
self.set_header("Content-Length", content_length) self.set_header("Content-Length", content_length)
def get(self): def get(self):
p = os.path.join(self.request.master.options.cadir, self.filename) p = os.path.join(self.request.master.options.confdir, self.filename)
p = os.path.expanduser(p) p = os.path.expanduser(p)
self.set_header("Content-Type", "application/x-pkcs12") self.set_header("Content-Type", "application/x-pkcs12")
self.set_header( self.set_header(

View File

@ -71,7 +71,7 @@ def client_arguments_from_options(options: "mitmproxy.options.Options") -> dict:
"verify": verify, "verify": verify,
"method": method, "method": method,
"options": tls_options, "options": tls_options,
"ca_path": options.ssl_verify_upstream_trusted_cadir, "ca_path": options.ssl_verify_upstream_trusted_confdir,
"ca_pemfile": options.ssl_verify_upstream_trusted_ca, "ca_pemfile": options.ssl_verify_upstream_trusted_ca,
"client_certs": options.client_certs, "client_certs": options.client_certs,
"cipher_list": options.ciphers_server, "cipher_list": options.ciphers_server,

View File

@ -4,7 +4,7 @@ from mitmproxy import optmanager
from mitmproxy.net import tls from mitmproxy.net import tls
CA_DIR = "~/.mitmproxy" CONF_DIR = "~/.mitmproxy"
LISTEN_PORT = 8080 LISTEN_PORT = 8080
@ -30,8 +30,8 @@ class Options(optmanager.OptManager):
""" """
) )
self.add_option( self.add_option(
"cadir", str, CA_DIR, "confdir", str, CONF_DIR,
"Location of the default mitmproxy CA files." "Location of the default mitmproxy configuration files."
) )
self.add_option( self.add_option(
"certs", Sequence[str], [], "certs", Sequence[str], [],
@ -143,7 +143,7 @@ class Options(optmanager.OptManager):
"Do not verify upstream server SSL/TLS certificates." "Do not verify upstream server SSL/TLS certificates."
) )
self.add_option( self.add_option(
"ssl_verify_upstream_trusted_cadir", Optional[str], None, "ssl_verify_upstream_trusted_confdir", Optional[str], None,
""" """
Path to a directory of trusted CA certificates for upstream server Path to a directory of trusted CA certificates for upstream server
verification prepared using the c_rehash tool. verification prepared using the c_rehash tool.

View File

@ -49,7 +49,7 @@ class ProxyConfig:
if "tcp_hosts" in updated: if "tcp_hosts" in updated:
self.check_tcp = HostMatcher(options.tcp_hosts) self.check_tcp = HostMatcher(options.tcp_hosts)
certstore_path = os.path.expanduser(options.cadir) 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)):
raise exceptions.OptionsError( raise exceptions.OptionsError(
"Certificate Authority parent directory does not exist: %s" % "Certificate Authority parent directory does not exist: %s" %

View File

@ -4,7 +4,7 @@ import os
from mitmproxy.addons import core from mitmproxy.addons import core
CONFIG_PATH = os.path.join(core.CA_DIR, "config.yaml") CONFIG_PATH = os.path.join(core.CONF_DIR, "config.yaml")
def common_options(parser, opts): def common_options(parser, opts):

View File

@ -1,6 +1,18 @@
import typing import typing
import os
import ruamel.yaml
from mitmproxy import command
from mitmproxy.tools.console import commandexecutor from mitmproxy.tools.console import commandexecutor
from mitmproxy.tools.console import signals from mitmproxy.tools.console import signals
from mitmproxy import ctx
from mitmproxy import exceptions
import mitmproxy.types
class KeyBindingError(Exception):
pass
Contexts = { Contexts = {
@ -139,3 +151,91 @@ class Keymap:
if b: if b:
return self.executor(b.command) return self.executor(b.command)
return key return key
keyAttrs = {
"key": lambda x: isinstance(x, str),
"cmd": lambda x: isinstance(x, str),
"ctx": lambda x: isinstance(x, list) and [isinstance(v, str) for v in x],
"help": lambda x: isinstance(x, str),
}
requiredKeyAttrs = set(["key", "cmd"])
class KeymapConfig:
defaultFile = "keys.yaml"
@command.command("console.keymap.load")
def keymap_load_path(self, path: mitmproxy.types.Path) -> None:
try:
self.load_path(ctx.master.keymap, path) # type: ignore
except (OSError, KeyBindingError) as e:
raise exceptions.CommandError(
"Could not load key bindings - %s" % e
) from e
def running(self):
p = os.path.join(os.path.expanduser(ctx.options.confdir), self.defaultFile)
if os.path.exists(p):
try:
self.load_path(ctx.master.keymap, p)
except KeyBindingError as e:
ctx.log.error(e)
def load_path(self, km, p):
if os.path.exists(p) and os.path.isfile(p):
with open(p, "rt", encoding="utf8") as f:
try:
txt = f.read()
except UnicodeDecodeError as e:
raise KeyBindingError(
"Encoding error - expected UTF8: %s: %s" % (p, e)
)
try:
vals = self.parse(txt)
except KeyBindingError as e:
raise KeyBindingError(
"Error reading %s: %s" % (p, e)
) from e
for v in vals:
try:
km.add(
key = v["key"],
command = v["cmd"],
contexts = v.get("ctx", ["global"]),
help = v.get("help", None),
)
except ValueError as e:
raise KeyBindingError(
"Error reading %s: %s" % (p, e)
) from e
def parse(self, text):
try:
data = ruamel.yaml.safe_load(text)
except ruamel.yaml.error.YAMLError as v:
if hasattr(v, "problem_mark"):
snip = v.problem_mark.get_snippet()
raise KeyBindingError(
"Key binding config error at line %s:\n%s\n%s" %
(v.problem_mark.line + 1, snip, v.problem)
)
else:
raise KeyBindingError("Could not parse key bindings.")
if not data:
return []
if not isinstance(data, list):
raise KeyBindingError("Inalid keybinding config - expected a list of keys")
for k in data:
unknown = k.keys() - keyAttrs.keys()
if unknown:
raise KeyBindingError("Unknown key attributes: %s" % unknown)
missing = requiredKeyAttrs - k.keys()
if missing:
raise KeyBindingError("Missing required key attributes: %s" % unknown)
for attr in k.keys():
if not keyAttrs[attr](k[attr]):
raise KeyBindingError("Invalid type for %s" % attr)
return data

View File

@ -56,6 +56,7 @@ class ConsoleMaster(master.Master):
consoleaddons.UnsupportedLog(), consoleaddons.UnsupportedLog(),
readfile.ReadFile(), readfile.ReadFile(),
consoleaddons.ConsoleAddon(self), consoleaddons.ConsoleAddon(self),
keymap.KeymapConfig(),
) )
def sigint_handler(*args, **kwargs): def sigint_handler(*args, **kwargs):

View File

@ -1,7 +1,7 @@
import sys import sys
DEPRECATED = """ DEPRECATED = """
--cadir --confdir
-Z -Z
--body-size-limit --body-size-limit
--stream --stream
@ -22,7 +22,7 @@ DEPRECATED = """
--client-certs --client-certs
--no-upstream-cert --no-upstream-cert
--add-upstream-certs-to-client-chain --add-upstream-certs-to-client-chain
--upstream-trusted-cadir --upstream-trusted-confdir
--upstream-trusted-ca --upstream-trusted-ca
--ssl-version-client --ssl-version-client
--ssl-version-server --ssl-version-server
@ -72,7 +72,7 @@ REPLACEMENTS = {
"--no-http2-priority": "http2_priority", "--no-http2-priority": "http2_priority",
"--no-websocket": "websocket", "--no-websocket": "websocket",
"--no-upstream-cert": "upstream_cert", "--no-upstream-cert": "upstream_cert",
"--upstream-trusted-cadir": "ssl_verify_upstream_trusted_cadir", "--upstream-trusted-confdir": "ssl_verify_upstream_trusted_confdir",
"--upstream-trusted-ca": "ssl_verify_upstream_trusted_ca", "--upstream-trusted-ca": "ssl_verify_upstream_trusted_ca",
"--no-onboarding": "onboarding", "--no-onboarding": "onboarding",
"--no-pop": "server_replay_nopop", "--no-pop": "server_replay_nopop",

View File

@ -338,7 +338,7 @@ class TestSSLUpstreamCertVerificationWValidCertChain(tservers.ServerTestBase):
c.wfile.flush() c.wfile.flush()
assert c.rfile.readline() == testval assert c.rfile.readline() == testval
def test_mode_strict_w_cadir_should_pass(self, tdata): def test_mode_strict_w_confdir_should_pass(self, tdata):
c = tcp.TCPClient(("127.0.0.1", self.port)) c = tcp.TCPClient(("127.0.0.1", self.port))
with c.connect(): with c.connect():
c.convert_to_tls( c.convert_to_tls(

View File

@ -103,7 +103,7 @@ class _Http2TestBase:
upstream_cert=True, upstream_cert=True,
ssl_insecure=True ssl_insecure=True
) )
opts.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy") opts.confdir = os.path.join(tempfile.gettempdir(), "mitmproxy")
return opts return opts
@property @property

View File

@ -67,7 +67,7 @@ class _WebSocketTestBase:
ssl_insecure=True, ssl_insecure=True,
websocket=True, websocket=True,
) )
opts.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy") opts.confdir = os.path.join(tempfile.gettempdir(), "mitmproxy")
return opts return opts
@property @property

View File

@ -6,9 +6,9 @@ from mitmproxy.proxy.config import ProxyConfig
class TestProxyConfig: class TestProxyConfig:
def test_invalid_cadir(self): def test_invalid_confdir(self):
opts = options.Options() opts = options.Options()
opts.cadir = "foo" opts.confdir = "foo"
with pytest.raises(exceptions.OptionsError, match="parent directory does not exist"): with pytest.raises(exceptions.OptionsError, match="parent directory does not exist"):
ProxyConfig(opts) ProxyConfig(opts)

View File

@ -309,10 +309,10 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest):
with p.connect(): with p.connect():
return p.request("get:/p/242") return p.request("get:/p/242")
def test_verification_w_cadir(self, tdata): def test_verification_w_confdir(self, tdata):
self.options.update( self.options.update(
ssl_insecure=False, ssl_insecure=False,
ssl_verify_upstream_trusted_cadir=tdata.path( ssl_verify_upstream_trusted_confdir=tdata.path(
"mitmproxy/data/servercert/" "mitmproxy/data/servercert/"
), ),
ssl_verify_upstream_trusted_ca=None, ssl_verify_upstream_trusted_ca=None,
@ -322,7 +322,7 @@ class TestHTTPSUpstreamServerVerificationWTrustedCert(tservers.HTTPProxyTest):
def test_verification_w_pemfile(self, tdata): def test_verification_w_pemfile(self, tdata):
self.options.update( self.options.update(
ssl_insecure=False, ssl_insecure=False,
ssl_verify_upstream_trusted_cadir=None, ssl_verify_upstream_trusted_confdir=None,
ssl_verify_upstream_trusted_ca=tdata.path( ssl_verify_upstream_trusted_ca=tdata.path(
"mitmproxy/data/servercert/trusted-root.pem" "mitmproxy/data/servercert/trusted-root.pem"
), ),

View File

@ -70,3 +70,98 @@ def test_remove():
km.remove("key", ["commands"]) km.remove("key", ["commands"])
assert len(km.bindings) == 0 assert len(km.bindings) == 0
def test_load_path(tmpdir):
dst = str(tmpdir.join("conf"))
kmc = keymap.KeymapConfig()
with taddons.context(kmc) as tctx:
km = keymap.Keymap(tctx.master)
tctx.master.keymap = km
with open(dst, 'wb') as f:
f.write(b"\xff\xff\xff")
with pytest.raises(keymap.KeyBindingError, match="expected UTF8"):
kmc.load_path(km, dst)
with open(dst, 'w') as f:
f.write("'''")
with pytest.raises(keymap.KeyBindingError):
kmc.load_path(km, dst)
with open(dst, 'w') as f:
f.write(
"""
- key: key1
ctx: [unknown]
cmd: >
foo bar
foo bar
"""
)
with pytest.raises(keymap.KeyBindingError):
kmc.load_path(km, dst)
with open(dst, 'w') as f:
f.write(
"""
- key: key1
ctx: [chooser]
help: one
cmd: >
foo bar
foo bar
"""
)
kmc.load_path(km, dst)
assert(km.get("chooser", "key1"))
def test_parse():
kmc = keymap.KeymapConfig()
with taddons.context(kmc):
assert kmc.parse("") == []
assert kmc.parse("\n\n\n \n") == []
with pytest.raises(keymap.KeyBindingError, match="expected a list of keys"):
kmc.parse("key: val")
with pytest.raises(keymap.KeyBindingError, match="expected a list of keys"):
kmc.parse("val")
with pytest.raises(keymap.KeyBindingError, match="Unknown key attributes"):
kmc.parse(
"""
- key: key1
nonexistent: bar
"""
)
with pytest.raises(keymap.KeyBindingError, match="Missing required key attributes"):
kmc.parse(
"""
- help: key1
"""
)
with pytest.raises(keymap.KeyBindingError, match="Invalid type for cmd"):
kmc.parse(
"""
- key: key1
cmd: [ cmd ]
"""
)
with pytest.raises(keymap.KeyBindingError, match="Invalid type for ctx"):
kmc.parse(
"""
- key: key1
ctx: foo
cmd: cmd
"""
)
assert kmc.parse(
"""
- key: key1
ctx: [one, two]
help: one
cmd: >
foo bar
foo bar
"""
) == [{"key": "key1", "ctx": ["one", "two"], "help": "one", "cmd": "foo bar foo bar\n"}]

View File

@ -151,7 +151,7 @@ class ProxyTestBase:
def teardown_class(cls): def teardown_class(cls):
# perf: we want to run tests in parallel # perf: we want to run tests in parallel
# should this ever cause an error, travis should catch it. # should this ever cause an error, travis should catch it.
# shutil.rmtree(cls.cadir) # shutil.rmtree(cls.confdir)
cls.proxy.shutdown() cls.proxy.shutdown()
cls.server.shutdown() cls.server.shutdown()
cls.server2.shutdown() cls.server2.shutdown()
@ -175,10 +175,10 @@ class ProxyTestBase:
@classmethod @classmethod
def get_options(cls): def get_options(cls):
cls.cadir = os.path.join(tempfile.gettempdir(), "mitmproxy") cls.confdir = os.path.join(tempfile.gettempdir(), "mitmproxy")
return options.Options( return options.Options(
listen_port=0, listen_port=0,
cadir=cls.cadir, confdir=cls.confdir,
add_upstream_certs_to_client_chain=cls.add_upstream_certs_to_client_chain, add_upstream_certs_to_client_chain=cls.add_upstream_certs_to_client_chain,
ssl_insecure=True, ssl_insecure=True,
) )

View File

@ -10,8 +10,8 @@ from mitmproxy.utils import arg_check
@pytest.mark.parametrize('arg, output', [ @pytest.mark.parametrize('arg, output', [
(["-T"], "-T is deprecated, please use --mode transparent instead"), (["-T"], "-T is deprecated, please use --mode transparent instead"),
(["-U"], "-U is deprecated, please use --mode upstream:SPEC instead"), (["-U"], "-U is deprecated, please use --mode upstream:SPEC instead"),
(["--cadir"], "--cadir is deprecated.\n" (["--confdir"], "--confdir is deprecated.\n"
"Please use `--set cadir=value` instead.\n" "Please use `--set confdir=value` instead.\n"
"To show all options and their default values use --options"), "To show all options and their default values use --options"),
(["--palette"], "--palette is deprecated.\n" (["--palette"], "--palette is deprecated.\n"
"Please use `--set console_palette=value` instead.\n" "Please use `--set console_palette=value` instead.\n"