From c92689dab2cbe2fc106aa3c0d71386ca03c16cf8 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 4 Apr 2026 03:40:44 -0700 Subject: [PATCH] feat: TLS nudge in doctor and service install output When host is set to network access (not 127.0.0.1) and TLS is not configured, doctor shows 'Run: muxplex setup-tls' and service install shows a tip line. Hidden on localhost-only setups since clipboard works without HTTPS there. --- muxplex/cli.py | 11 +++-- muxplex/service.py | 14 +++++++ muxplex/tests/test_cli.py | 54 ++++++++++++++++++++++-- muxplex/tests/test_service.py | 77 +++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 6 deletions(-) diff --git a/muxplex/cli.py b/muxplex/cli.py index 3fc0ba2..4cecf1b 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -369,9 +369,14 @@ def doctor() -> None: else: print(f" {warn_mark} TLS: configured but cert not readable ({tls_cert})") else: - print( - f" {warn_mark} TLS: disabled (clipboard requires HTTPS on non-localhost)" - ) + # Only show TLS warning if host is not localhost + host = cfg.get("host", "127.0.0.1") + if host != "127.0.0.1": + # Network host without TLS: show nudge + print( + f" {warn_mark} TLS: disabled — clipboard won't work on remote devices" + ) + print(" Run: muxplex setup-tls") # Auth status pw_path = get_password_path() diff --git a/muxplex/service.py b/muxplex/service.py index 2e98e64..70436b6 100644 --- a/muxplex/service.py +++ b/muxplex/service.py @@ -116,6 +116,18 @@ def _prompt_host_if_localhost() -> None: # --------------------------------------------------------------------------- + + +def _show_tls_nudge_if_needed() -> None: + """Show TLS setup nudge if host is network and TLS is not configured.""" + from muxplex.settings import load_settings + + settings = load_settings() + host = settings.get("host", "127.0.0.1") + tls_cert = settings.get("tls_cert", "") + + if host != "127.0.0.1" and not tls_cert: + print(" Tip: Enable HTTPS for clipboard support: muxplex setup-tls") def _systemd_install() -> None: muxplex_bin = _resolve_muxplex_bin() safe_path = os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin") @@ -128,6 +140,7 @@ def _systemd_install() -> None: subprocess.run(["systemctl", "--user", "daemon-reload"], check=True) subprocess.run(["systemctl", "--user", "enable", "--now", "muxplex"], check=True) _prompt_host_if_localhost() + _show_tls_nudge_if_needed() def _systemd_uninstall() -> None: @@ -179,6 +192,7 @@ def _launchd_install() -> None: ["launchctl", "bootstrap", f"gui/{uid}", str(_LAUNCHD_PLIST_PATH)], check=True ) _prompt_host_if_localhost() + _show_tls_nudge_if_needed() def _launchd_uninstall() -> None: diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index c53738c..2716096 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -1618,11 +1618,14 @@ def test_doctor_shows_tls_enabled(tmp_path, monkeypatch, capsys): def test_doctor_shows_tls_clipboard_warning(tmp_path, monkeypatch, capsys): - """doctor() mentions clipboard or https when TLS is disabled.""" + """doctor() mentions clipboard or https when TLS is disabled on network host.""" + import json + import muxplex.settings as settings_mod settings_file = tmp_path / "settings.json" - settings_file.write_text("{}") + # Set host to network to trigger the TLS warning (not localhost) + settings_file.write_text(json.dumps({"host": "0.0.0.0"})) monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) from muxplex.cli import doctor @@ -1632,7 +1635,7 @@ def test_doctor_shows_tls_clipboard_warning(tmp_path, monkeypatch, capsys): out = capsys.readouterr().out out_lower = out.lower() assert "clipboard" in out_lower or "https" in out_lower, ( - f"Expected 'clipboard' or 'https' in doctor TLS-disabled output, got: {out!r}" + f"Expected 'clipboard' or 'https' in doctor TLS-disabled output for network host, got: {out!r}" ) @@ -2005,3 +2008,48 @@ def test_setup_tls_regenerates_on_eof(tmp_path, monkeypatch, capsys): assert "keeping" in out_lower, ( f"Expected 'keeping' in output after EOFError (default 'n'), got: {out!r}" ) + + +# --------------------------------------------------------------------------- +# task: TLS nudge hints in doctor and service install +# --------------------------------------------------------------------------- + + +def test_doctor_tls_nudge_shows_run_command_on_network_host(capsys, tmp_path, monkeypatch): + """doctor must show 'Run: muxplex setup-tls' when host is network and TLS disabled.""" + import json + + import muxplex.settings as settings_mod + + settings_file = tmp_path / "settings.json" + settings_file.write_text(json.dumps({"host": "0.0.0.0", "tls_cert": "", "tls_key": ""})) + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + from muxplex.cli import doctor + + doctor() + + out = capsys.readouterr().out + assert "muxplex setup-tls" in out, ( + f"Expected 'muxplex setup-tls' in doctor output when host is 0.0.0.0 and TLS disabled, got: {out!r}" + ) + + +def test_doctor_tls_nudge_hidden_on_localhost(capsys, tmp_path, monkeypatch): + """doctor must NOT show TLS nudge when host is 127.0.0.1.""" + import json + + import muxplex.settings as settings_mod + + settings_file = tmp_path / "settings.json" + settings_file.write_text(json.dumps({"host": "127.0.0.1", "tls_cert": "", "tls_key": ""})) + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + from muxplex.cli import doctor + + doctor() + + out = capsys.readouterr().out + assert "muxplex setup-tls" not in out, ( + f"TLS nudge must NOT appear in doctor output when host is 127.0.0.1, got: {out!r}" + ) diff --git a/muxplex/tests/test_service.py b/muxplex/tests/test_service.py index dcce621..b91b755 100644 --- a/muxplex/tests/test_service.py +++ b/muxplex/tests/test_service.py @@ -586,3 +586,80 @@ def test_launchd_logs_handles_keyboard_interrupt(monkeypatch): monkeypatch.setattr(subprocess, "run", mock_run) # Should not raise svc._launchd_logs() + + +# --------------------------------------------------------------------------- +# task: TLS nudge hints in service install +# --------------------------------------------------------------------------- + + +def test_service_install_shows_tls_tip_on_network_host(capsys, tmp_path, monkeypatch): + """service install must show TLS tip when host is network and TLS disabled.""" + import json + + import muxplex.service as svc + import muxplex.settings as settings_mod + + # Setup paths + unit_dir = tmp_path / "systemd" / "user" + unit_path = unit_dir / "muxplex.service" + settings_file = tmp_path / "settings.json" + + monkeypatch.setattr(svc, "_SYSTEMD_UNIT_DIR", unit_dir) + monkeypatch.setattr(svc, "_SYSTEMD_UNIT_PATH", unit_path) + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + # Setup settings with network host and no TLS + settings_file.write_text(json.dumps({"host": "0.0.0.0", "tls_cert": "", "tls_key": ""})) + + # Mock subprocess to avoid actual systemctl calls + calls = [] + monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(list(cmd))) + + # Mock the prompt function + monkeypatch.setattr(svc, "_prompt_host_if_localhost", lambda: None) + + from muxplex.service import service_install + + service_install() + + out = capsys.readouterr().out + assert "muxplex setup-tls" in out, ( + f"Expected 'muxplex setup-tls' in service install output when host is 0.0.0.0 and TLS disabled, got: {out!r}" + ) + + +def test_service_install_hides_tls_tip_on_localhost(capsys, tmp_path, monkeypatch): + """service install must NOT show TLS tip when host is 127.0.0.1.""" + import json + + import muxplex.service as svc + import muxplex.settings as settings_mod + + # Setup paths + unit_dir = tmp_path / "systemd" / "user" + unit_path = unit_dir / "muxplex.service" + settings_file = tmp_path / "settings.json" + + monkeypatch.setattr(svc, "_SYSTEMD_UNIT_DIR", unit_dir) + monkeypatch.setattr(svc, "_SYSTEMD_UNIT_PATH", unit_path) + monkeypatch.setattr(settings_mod, "SETTINGS_PATH", settings_file) + + # Setup settings with localhost + settings_file.write_text(json.dumps({"host": "127.0.0.1", "tls_cert": "", "tls_key": ""})) + + # Mock subprocess + calls = [] + monkeypatch.setattr(subprocess, "run", lambda cmd, **kw: calls.append(list(cmd))) + + # Mock the prompt function + monkeypatch.setattr(svc, "_prompt_host_if_localhost", lambda: None) + + from muxplex.service import service_install + + service_install() + + out = capsys.readouterr().out + assert "muxplex setup-tls" not in out, ( + f"TLS tip must NOT appear in service install output when host is 127.0.0.1, got: {out!r}" + )