srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-29 09:58:00 +0200
committersrdusr <[email protected]>2026-06-29 09:58:00 +0200
commit2ade8c807584bff0b60d6b6f278dbde29b13a5ff (patch)
tree4de78c2face8f9c85f2fe4c5db2fb6ba9d4540eb /cmd
parente01afcbf0de00af52fe90959ba77288679e3303d (diff)
downloadmitmux-2ade8c807584bff0b60d6b6f278dbde29b13a5ff.tar.gz
mitmux-2ade8c807584bff0b60d6b6f278dbde29b13a5ff.zip
Browser-launcher helper: throwaway proxied profile, one flag
Adds -launch-browser=chrome|firefox|auto to the mitmux TUI binary. Resolves an installed browser (PATH first, then common per-OS install locations), spins up a brand new throwaway profile, configures it to proxy through the daemon, and opens straight to http://mitmux.cert/ so installing the CA in that profile is one click. This is the answer to "build an in-house browser": a bundled GUI browser is a different, much larger project and works against this tool's terminal-native positioning - the actually useful part of that idea is zero-friction setup (proxy + CA-install page, no profile pollution), which this delivers by launching the user's own browser in a disposable profile instead of embedding one. Chrome takes --proxy-server as a flag; Firefox has none, so its profile gets a generated user.js instead - the only non-interactive way to configure it. Verified against this machine's real installed Chrome and Firefox: binary discovery resolves both, auto prefers chrome-family when both are present, and the generated Firefox prefs are well-formed. Deliberately did not spawn a live browser window as part of verification - that's a visible GUI action on whoever runs it, left for a user to trigger by hand via the flag.
Diffstat (limited to 'cmd')
-rw-r--r--cmd/mitmux/browser.go162
-rw-r--r--cmd/mitmux/browser_test.go84
-rw-r--r--cmd/mitmux/main.go17
3 files changed, 263 insertions, 0 deletions
diff --git a/cmd/mitmux/browser.go b/cmd/mitmux/browser.go
new file mode 100644
index 0000000..126e858
--- /dev/null
+++ b/cmd/mitmux/browser.go
@@ -0,0 +1,162 @@
+package main
+
+import (
+ "fmt"
+ "net"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+)
+
+// browserCandidate is one browser flavor mitmux knows how to launch, in
+// the order "auto" tries them - chrome-family first (its command-line
+// proxy flag needs no profile-file setup), then firefox.
+type browserCandidate struct {
+ kind string // "chrome" or "firefox" - selects how proxy config is applied
+ names []string
+ macApps []string // .app bundle binaries under /Applications, checked on darwin
+ winPaths []string // common Program Files install paths, checked on windows
+}
+
+var browserCandidates = []browserCandidate{
+ {
+ kind: "chrome",
+ names: []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "brave-browser", "microsoft-edge"},
+ macApps: []string{
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
+ "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
+ },
+ winPaths: []string{
+ `C:\Program Files\Google\Chrome\Application\chrome.exe`,
+ `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
+ `C:\Program Files\Chromium\Application\chrome.exe`,
+ `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`,
+ },
+ },
+ {
+ kind: "firefox",
+ names: []string{"firefox", "firefox-esr"},
+ macApps: []string{
+ "/Applications/Firefox.app/Contents/MacOS/firefox",
+ },
+ winPaths: []string{
+ `C:\Program Files\Mozilla Firefox\firefox.exe`,
+ `C:\Program Files (x86)\Mozilla Firefox\firefox.exe`,
+ },
+ },
+}
+
+// findBrowser locates an installed browser binary matching kind ("chrome",
+// "firefox", or "auto" for the first one found). Checks PATH first - works
+// on every OS covered here, including a Homebrew or otherwise custom-
+// installed browser symlinked onto PATH - then a short list of common
+// per-OS install locations as a fallback for one that isn't on PATH.
+func findBrowser(kind string) (path, resolvedKind string, err error) {
+ if kind != "auto" && kind != "chrome" && kind != "firefox" {
+ return "", "", fmt.Errorf("unsupported browser kind %q (use chrome, firefox, or auto)", kind)
+ }
+ for _, cand := range browserCandidates {
+ if kind != "auto" && kind != cand.kind {
+ continue
+ }
+ for _, name := range cand.names {
+ if p, err := exec.LookPath(name); err == nil {
+ return p, cand.kind, nil
+ }
+ }
+ var extra []string
+ switch runtime.GOOS {
+ case "darwin":
+ extra = cand.macApps
+ case "windows":
+ extra = cand.winPaths
+ }
+ for _, p := range extra {
+ if _, statErr := os.Stat(p); statErr == nil {
+ return p, cand.kind, nil
+ }
+ }
+ }
+ if kind == "auto" {
+ return "", "", fmt.Errorf("no supported browser found (tried chrome/chromium/brave/edge and firefox)")
+ }
+ return "", "", fmt.Errorf("%s not found on PATH or in its usual install location", kind)
+}
+
+// launchBrowser starts kind ("chrome", "firefox", or "auto") in a fresh
+// throwaway profile pre-configured to send all traffic through
+// proxyAddr, opening directly on mitmux's own CA-cert distribution page
+// (see proxy.go's serveCACert) so installing the cert in that profile is
+// one click away - addresses the same "how do other browsers trust
+// mitmux" question CA install already solves for the OS trust store, but
+// scoped to a profile nobody else uses.
+//
+// Returns immediately without waiting for the browser to exit - it's
+// meant to run alongside mitmux, not block it. The profile directory is
+// real and on disk, and deliberately not cleaned up when the browser
+// closes: the point of "throwaway" is a fresh identity every launch
+// (cookies, extensions, cached certificate-trust decisions all reset),
+// not deleting a still-open browser's own profile out from under it. The
+// OS's own temp-directory cleanup handles eventual removal.
+func launchBrowser(kind, proxyAddr string) error {
+ bin, resolvedKind, err := findBrowser(kind)
+ if err != nil {
+ return err
+ }
+
+ profileDir, err := os.MkdirTemp("", "mitmux-browser-*")
+ if err != nil {
+ return fmt.Errorf("create throwaway browser profile: %w", err)
+ }
+
+ var cmd *exec.Cmd
+ switch resolvedKind {
+ case "chrome":
+ cmd = exec.Command(bin,
+ "--user-data-dir="+profileDir,
+ "--proxy-server="+proxyAddr,
+ "--no-first-run",
+ "--no-default-browser-check",
+ "http://mitmux.cert/",
+ )
+ case "firefox":
+ host, port, splitErr := net.SplitHostPort(proxyAddr)
+ if splitErr != nil {
+ return fmt.Errorf("parse proxy address %q: %w", proxyAddr, splitErr)
+ }
+ // Firefox has no proxy command-line flag - network.proxy.* prefs
+ // in the profile are the only way to configure it non-
+ // interactively. user.js is read on every start and applied on
+ // top of the (otherwise empty, since profileDir is brand new)
+ // profile.
+ prefs := firefoxProxyPrefs(host, port)
+ if err := os.WriteFile(filepath.Join(profileDir, "user.js"), []byte(prefs), 0o600); err != nil {
+ return fmt.Errorf("write throwaway profile prefs: %w", err)
+ }
+ cmd = exec.Command(bin, "-profile", profileDir, "-no-remote", "-new-instance", "http://mitmux.cert/")
+ }
+
+ if err := cmd.Start(); err != nil {
+ return fmt.Errorf("launch %s: %w", resolvedKind, err)
+ }
+ go cmd.Wait() // reap the child on exit instead of leaving a zombie
+ return nil
+}
+
+// firefoxProxyPrefs is the user.js content that configures a fresh
+// Firefox profile to send all HTTP and HTTPS traffic through host:port
+// and nothing directly (no proxy exceptions) - see launchBrowser's
+// firefox case for why this is the only way to do it non-interactively.
+func firefoxProxyPrefs(host, port string) string {
+ return fmt.Sprintf(`user_pref("network.proxy.type", 1);
+user_pref("network.proxy.http", %q);
+user_pref("network.proxy.http_port", %s);
+user_pref("network.proxy.ssl", %q);
+user_pref("network.proxy.ssl_port", %s);
+user_pref("network.proxy.share_proxy_settings", true);
+user_pref("network.proxy.no_proxies_on", "");
+`, host, port, host, port)
+}
diff --git a/cmd/mitmux/browser_test.go b/cmd/mitmux/browser_test.go
new file mode 100644
index 0000000..8b46ba8
--- /dev/null
+++ b/cmd/mitmux/browser_test.go
@@ -0,0 +1,84 @@
+package main
+
+import (
+ "os/exec"
+ "strings"
+ "testing"
+)
+
+func TestFindBrowserUnsupportedKind(t *testing.T) {
+ if _, _, err := findBrowser("safari"); err == nil {
+ t.Fatal("expected an error for an unsupported browser kind")
+ } else if !strings.Contains(err.Error(), "unsupported browser kind") {
+ t.Errorf("error = %q, want it to say the kind is unsupported (not just 'not found')", err)
+ }
+}
+
+func TestFindBrowserChrome(t *testing.T) {
+ if _, err := exec.LookPath("google-chrome"); err != nil {
+ if _, err := exec.LookPath("google-chrome-stable"); err != nil {
+ if _, err := exec.LookPath("chromium"); err != nil {
+ t.Skip("no chrome-family browser on PATH")
+ }
+ }
+ }
+ path, kind, err := findBrowser("chrome")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if kind != "chrome" {
+ t.Errorf("kind = %q, want %q", kind, "chrome")
+ }
+ if path == "" {
+ t.Error("expected a non-empty binary path")
+ }
+}
+
+func TestFindBrowserFirefox(t *testing.T) {
+ if _, err := exec.LookPath("firefox"); err != nil {
+ t.Skip("firefox not on PATH")
+ }
+ path, kind, err := findBrowser("firefox")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if kind != "firefox" {
+ t.Errorf("kind = %q, want %q", kind, "firefox")
+ }
+ if path == "" {
+ t.Error("expected a non-empty binary path")
+ }
+}
+
+func TestFindBrowserAutoPrefersChrome(t *testing.T) {
+ if _, err := exec.LookPath("google-chrome"); err != nil {
+ t.Skip("google-chrome not on PATH, can't verify auto's preference order")
+ }
+ _, kind, err := findBrowser("auto")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if kind != "chrome" {
+ t.Errorf("auto resolved to %q, want chrome to win when both are available", kind)
+ }
+}
+
+func TestFirefoxProxyPrefs(t *testing.T) {
+ prefs := firefoxProxyPrefs("127.0.0.1", "8080")
+ for _, want := range []string{
+ `user_pref("network.proxy.type", 1);`,
+ `user_pref("network.proxy.http", "127.0.0.1");`,
+ `user_pref("network.proxy.http_port", 8080);`,
+ `user_pref("network.proxy.ssl", "127.0.0.1");`,
+ `user_pref("network.proxy.ssl_port", 8080);`,
+ } {
+ if !strings.Contains(prefs, want) {
+ t.Errorf("firefoxProxyPrefs output missing %q\ngot:\n%s", want, prefs)
+ }
+ }
+ // The port must NOT be quoted - Firefox's prefs format is JS-ish and
+ // network.proxy.http_port is an integer pref, not a string one.
+ if strings.Contains(prefs, `"8080"`) {
+ t.Error("port should be an unquoted integer literal, not a quoted string")
+ }
+}
diff --git a/cmd/mitmux/main.go b/cmd/mitmux/main.go
index f529681..6e07069 100644
--- a/cmd/mitmux/main.go
+++ b/cmd/mitmux/main.go
@@ -41,6 +41,7 @@ func defaultSocketPath() string {
func main() {
socketPath := flag.String("socket", "", "daemon control socket path (default: $XDG_RUNTIME_DIR/mitmux.sock, else <ca-dir>/mitmux.sock)")
+ launchBrowserFlag := flag.String("launch-browser", "", "launch a browser in a throwaway profile pre-configured to use the daemon's proxy, opening the CA-cert install page - \"chrome\", \"firefox\", or \"auto\" (default: don't launch one)")
showVersion := flag.Bool("version", false, "print version and exit")
flag.Parse()
@@ -61,6 +62,22 @@ func main() {
}
defer client.Close()
+ if *launchBrowserFlag != "" {
+ status, err := client.Status()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "mitmux: get daemon status for browser launch: %v\n", err)
+ os.Exit(1)
+ }
+ // ProxyAddr is every -listen address the daemon is bound to,
+ // comma-joined (see cmd/mitmuxd/main.go) - a browser needs
+ // exactly one to point at, so the first is as good as any.
+ proxyAddr := strings.SplitN(status.ProxyAddr, ", ", 2)[0]
+ if err := launchBrowser(*launchBrowserFlag, proxyAddr); err != nil {
+ fmt.Fprintf(os.Stderr, "mitmux: launch browser: %v\n", err)
+ os.Exit(1)
+ }
+ }
+
subCh, subClose, err := ipc.Subscribe(path)
if err != nil {
fmt.Fprintf(os.Stderr, "mitmux: subscribe to daemon at %s: %v\n", path, err)