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) }