exit for all tools on startup error, fix #4544 (#5187)

This commit is contained in:
Maximilian Hils 2022-03-16 14:23:32 +01:00 committed by GitHub
parent 148429c0b3
commit e2f42ddb30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 92 additions and 47 deletions

View File

@ -35,6 +35,7 @@
* Console Improvements on Windows (@mhils)
* Fix processing of `--set` options (#5067, @marwinxxii)
* Lowercase user-added header names and emit a log message to notify the user when using HTTP/2 (#4746, @mhils)
* Exit early if there are errors on startup (#4544, @mhils)
## 28 September 2021: mitmproxy 7.0.4

View File

@ -0,0 +1,20 @@
import asyncio
import sys
class ErrorCheck:
"""Monitor startup for error log entries, and terminate immediately if there are some."""
def __init__(self):
self.has_errored = False
def add_log(self, e):
if e.level == "error":
self.has_errored = True
async def running(self):
# don't run immediately, wait for all logging tasks to finish.
asyncio.create_task(self._shutdown_if_errored())
async def _shutdown_if_errored(self):
if self.has_errored:
sys.exit(1)

View File

@ -99,11 +99,11 @@ class Proxyserver:
""",
)
def running(self):
async def running(self):
self.master = ctx.master
self.options = ctx.options
self.is_running = True
self.configure(["listen_port"])
await self.refresh_server()
def configure(self, updated):
if "stream_large_bodies" in updated:
@ -120,9 +120,7 @@ class Proxyserver:
f"{ctx.options.body_size_limit}")
if "mode" in updated and ctx.options.mode == "transparent": # pragma: no cover
platform.init_transparent_mode()
if not self.is_running:
return
if any(x in updated for x in ["server", "listen_host", "listen_port"]):
if self.is_running and any(x in updated for x in ["server", "listen_host", "listen_port"]):
asyncio.create_task(self.refresh_server())
async def refresh_server(self):
@ -133,11 +131,15 @@ class Proxyserver:
if ctx.options.server:
if not ctx.master.addons.get("nextlayer"):
ctx.log.warn("Warning: Running proxyserver without nextlayer addon!")
try:
self.server = await asyncio.start_server(
self.handle_connection,
self.options.listen_host,
self.options.listen_port,
)
except OSError as e:
ctx.log.error(str(e))
return
addrs = {f"http://{human.format_address(s.getsockname())}" for s in self.server.sockets}
ctx.log.info(f"Proxy server listening at {' and '.join(addrs)}")

View File

@ -72,8 +72,7 @@ class DoneHook(Hook):
class RunningHook(Hook):
"""
Called when the proxy is completely up and running. At this point,
you can expect the proxy to be bound to a port, and all addons to be
loaded.
you can expect all addons to be loaded and all options to be set.
"""

View File

@ -19,7 +19,7 @@ import urwid
from mitmproxy import addons
from mitmproxy import master
from mitmproxy import log
from mitmproxy.addons import intercept
from mitmproxy.addons import errorcheck, intercept
from mitmproxy.addons import eventstore
from mitmproxy.addons import readfile
from mitmproxy.addons import view
@ -56,6 +56,7 @@ class ConsoleMaster(master.Master):
readfile.ReadFile(),
consoleaddons.ConsoleAddon(self),
keymap.KeymapConfig(),
errorcheck.ErrorCheck(),
)
self.window = None

View File

@ -1,20 +1,10 @@
from mitmproxy import addons
from mitmproxy import options
from mitmproxy import master
from mitmproxy.addons import dumper, termlog, keepserving, readfile
class ErrorCheck:
def __init__(self):
self.has_errored = False
def add_log(self, e):
if e.level == "error":
self.has_errored = True
from mitmproxy import options
from mitmproxy.addons import dumper, errorcheck, keepserving, readfile, termlog
class DumpMaster(master.Master):
def __init__(
self,
options: options.Options,
@ -22,7 +12,6 @@ class DumpMaster(master.Master):
with_dumper=True,
) -> None:
super().__init__(options)
self.errorcheck = ErrorCheck()
if with_termlog:
self.addons.add(termlog.TermLog())
self.addons.add(*addons.default_addons())
@ -31,5 +20,5 @@ class DumpMaster(master.Master):
self.addons.add(
keepserving.KeepServing(),
readfile.ReadFileStdin(),
self.errorcheck
errorcheck.ErrorCheck(),
)

View File

@ -48,17 +48,20 @@ def process_options(parser, opts, args):
opts.merge(adict)
T = typing.TypeVar("T", bound=master.Master)
def run(
master_cls: typing.Type[master.Master],
master_cls: typing.Type[T],
make_parser: typing.Callable[[options.Options], argparse.ArgumentParser],
arguments: typing.Sequence[str],
extra: typing.Callable[[typing.Any], dict] = None
) -> master.Master: # pragma: no cover
) -> T: # pragma: no cover
"""
extra: Extra argument processing callable which returns a dict of
options.
"""
async def main() -> master.Master:
async def main() -> T:
debug.register_info_dumpers()
opts = options.Options()
@ -143,9 +146,7 @@ def mitmdump(args=None) -> typing.Optional[int]: # pragma: no cover
)
return {}
m = run(dump.DumpMaster, cmdline.mitmdump, args, extra)
if m and m.errorcheck.has_errored: # type: ignore
return 1
run(dump.DumpMaster, cmdline.mitmdump, args, extra)
return None

View File

@ -5,7 +5,7 @@ from mitmproxy import addons
from mitmproxy import log
from mitmproxy import master
from mitmproxy import optmanager
from mitmproxy.addons import eventstore
from mitmproxy.addons import errorcheck, eventstore
from mitmproxy.addons import intercept
from mitmproxy.addons import readfile
from mitmproxy.addons import termlog
@ -29,6 +29,8 @@ class WebMaster(master.Master):
self.options.changed.connect(self._sig_options_update)
if with_termlog:
self.addons.add(termlog.TermLog())
self.addons.add(*addons.default_addons())
self.addons.add(
webaddons.WebAddon(),
@ -37,9 +39,8 @@ class WebMaster(master.Master):
static_viewer.StaticViewer(),
self.view,
self.events,
errorcheck.ErrorCheck(),
)
if with_termlog:
self.addons.add(termlog.TermLog())
self.app = app.Application(
self, self.options.web_debug
)

View File

@ -54,7 +54,7 @@ async def test_asgi_full():
with taddons.context(ps, *addons) as tctx:
tctx.master.addons.add(next_layer.NextLayer())
tctx.configure(ps, listen_host="127.0.0.1", listen_port=0)
ps.running()
await ps.running()
await tctx.master.await_log("Proxy server listening", level="info")
proxy_addr = ps.server.sockets[0].getsockname()[:2]

View File

@ -0,0 +1,25 @@
import asyncio
import pytest
from mitmproxy import log
from mitmproxy.addons.errorcheck import ErrorCheck
def test_errorcheck():
async def run():
# suppress error that task exception was not retrieved.
asyncio.get_running_loop().set_exception_handler(lambda *_: 0)
e = ErrorCheck()
e.add_log(log.LogEntry("fatal", "error"))
await e.running()
await asyncio.sleep(0)
with pytest.raises(SystemExit):
asyncio.run(run())
async def test_no_error():
e = ErrorCheck()
await e.running()
await asyncio.sleep(0)

View File

@ -55,7 +55,7 @@ async def test_start_stop():
async with tcp_server(server_handler) as addr:
tctx.configure(ps, listen_host="127.0.0.1", listen_port=0)
assert not ps.server
ps.running()
await ps.running()
await tctx.master.await_log("Proxy server listening", level="info")
assert ps.server
@ -99,7 +99,7 @@ async def test_inject() -> None:
tctx.master.addons.add(state)
async with tcp_server(server_handler) as addr:
tctx.configure(ps, listen_host="127.0.0.1", listen_port=0)
ps.running()
await ps.running()
await tctx.master.await_log("Proxy server listening", level="info")
proxy_addr = ps.server.sockets[0].getsockname()[:2]
reader, writer = await asyncio.open_connection(*proxy_addr)
@ -154,7 +154,7 @@ async def test_warn_no_nextlayer():
ps = Proxyserver()
with taddons.context(ps) as tctx:
tctx.configure(ps, listen_host="127.0.0.1", listen_port=0)
ps.running()
await ps.running()
await tctx.master.await_log("Proxy server listening at", level="info")
assert tctx.master.has_log("Warning: Running proxyserver without nextlayer addon!", level="warn")
await ps.shutdown_server()
@ -184,3 +184,15 @@ def test_options():
with pytest.raises(exceptions.OptionsError):
tctx.configure(ps, stream_large_bodies="invalid")
tctx.configure(ps, stream_large_bodies="1m")
async def test_startup_err(monkeypatch) -> None:
async def _raise(*_):
raise OSError("cannot bind")
monkeypatch.setattr(asyncio, "start_server", _raise)
ps = Proxyserver()
with taddons.context(ps) as tctx:
await ps.running()
await tctx.master.await_log("cannot bind", level="error")

View File

@ -39,6 +39,7 @@ def console(monkeypatch) -> ConsoleTestMaster:
async def make_master():
opts = mitmproxy.options.Options()
m = ConsoleTestMaster(opts)
opts.server = False
await m.running()
return m
return asyncio.run(make_master())

View File

@ -2,7 +2,6 @@ from unittest import mock
import pytest
from mitmproxy import log
from mitmproxy import options
from mitmproxy.tools import dump
@ -13,12 +12,6 @@ class TestDumpMaster:
m = dump.DumpMaster(o, with_termlog=False, with_dumper=False)
return m
async def test_has_error(self):
m = self.mkmaster()
ent = log.LogEntry("foo", "error")
m.addons.trigger(log.AddLogHook(ent))
assert m.errorcheck.has_errored
@pytest.mark.parametrize("termlog", [False, True])
async def test_addons_termlog(self, termlog):
with mock.patch('sys.stdout'):