srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-06-05 23:30:00 +0200
committersrdusr <[email protected]>2026-06-05 23:30:00 +0200
commitf3558f18ecabba983aba001468b06394471e6d8b (patch)
tree49bc1c3843945e4d98a85e9129f34b4739a081b9 /internal
parentec42c6652eec8030b3710a804d02ff954d253720 (diff)
downloadmitmux-f3558f18ecabba983aba001468b06394471e6d8b.tar.gz
mitmux-f3558f18ecabba983aba001468b06394471e6d8b.zip
Serve the CA certificate for browsers/mobile at http://mitmux.cert/
Browser/mobile setup previously meant "find ca.pem on disk and import it manually" - awkward on a phone or tablet especially, which has no convenient way to get a file onto the device at all short of emailing it to yourself or similar. Any client already configured to proxy through mitmux can now just visit http://mitmux.cert/ and get the cert directly, with Content-Type: application/x-x509-ca-cert triggering iOS/Android's native "install this certificate" prompt. Same idea as mitmproxy's own http://mitm.it/, arrived at independently rather than reusing their domain - mitmux.cert isn't a registered TLD, so it can never collide with a real site someone meant to visit. Deliberately HTTP-only: fetching it over HTTPS would require the client to already trust mitmux's CA to MITM that very connection, the exact chicken-and-egg problem this exists to solve, so it's not attempted on the CONNECT/TLS path at all. internal/proxy/proxy.go: isCertDownloadHost matches the hostname case-insensitively regardless of port; handleHTTP checks it before ever dialing upstream and answers directly via serveCACert, using the CA's own CertPEM bytes already held in memory. Short-circuits before record() is ever reached, so the download itself never pollutes history. Verified live: fetched http://mitmux.cert/ through a real running proxy and confirmed the downloaded bytes are byte-identical to the actual ca.pem on disk (diff, not just "the request succeeded"); confirmed a request with an explicit port and a path still matches; confirmed normal proxying to an unrelated host is completely unaffected; confirmed via direct SQLite query that the cert-download requests never appear in history while a normal request in the same session does. Also documented (no code needed): any standard proxy-switcher extension (FoxyProxy, etc.) or a phone/tablet's own Wi-Fi proxy setting already works with mitmux exactly like it would with Burp/ZAP/Caido, since it's a normal forward proxy speaking the standard protocol. This was true before but never actually spelled out in the README for the phone/ tablet case specifically, which is a real, common daily workflow. go build/vet/gofmt/test/mod tidy all clean.
Diffstat (limited to 'internal')
-rw-r--r--internal/proxy/certdownload_test.go24
-rw-r--r--internal/proxy/proxy.go39
2 files changed, 63 insertions, 0 deletions
diff --git a/internal/proxy/certdownload_test.go b/internal/proxy/certdownload_test.go
new file mode 100644
index 0000000..aae5f48
--- /dev/null
+++ b/internal/proxy/certdownload_test.go
@@ -0,0 +1,24 @@
+package proxy
+
+import "testing"
+
+func TestIsCertDownloadHost(t *testing.T) {
+ tests := []struct {
+ host string
+ want bool
+ }{
+ {"mitmux.cert", true},
+ {"mitmux.cert:80", true},
+ {"MITMUX.CERT", true},
+ {"Mitmux.Cert:8080", true},
+ {"example.com", false},
+ {"notmitmux.cert", false},
+ {"mitmux.cert.evil.com", false},
+ {"", false},
+ }
+ for _, tt := range tests {
+ if got := isCertDownloadHost(tt.host); got != tt.want {
+ t.Errorf("isCertDownloadHost(%q) = %v, want %v", tt.host, got, tt.want)
+ }
+ }
+}
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go
index 2e80ed2..523ca01 100644
--- a/internal/proxy/proxy.go
+++ b/internal/proxy/proxy.go
@@ -31,6 +31,7 @@ import (
"net"
"net/http"
"net/url"
+ "strings"
"sync"
"time"
@@ -611,6 +612,10 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
http.Error(w, "mitmux: request must use absolute-form URI (configure as a proxy, not a target)", http.StatusBadRequest)
return
}
+ if isCertDownloadHost(r.URL.Host) {
+ s.serveCACert(w)
+ return
+ }
host := r.URL.Host
dial := func(ctx context.Context) (net.Conn, string, error) {
return dialUpstreamPlain(ctx, host, s.UpstreamProxy)
@@ -618,6 +623,40 @@ func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) {
s.forward(dial, r.URL.Scheme, r.URL.Host, w, r)
}
+// certDownloadHost is a magic hostname mitmux intercepts and answers
+// itself, serving its own CA certificate - reachable over plain HTTP
+// from any client configured to use mitmux as its proxy, including a
+// mobile browser, which otherwise has no easy way to get a file onto
+// the device to trust as a CA at all. Deliberately not a real,
+// resolvable domain (".cert" isn't a registered TLD) so it can never
+// collide with an actual site someone meant to visit - the same idea
+// as mitmproxy's own http://mitm.it/, arrived at independently rather
+// than reusing their domain. HTTP only, on purpose: fetching this over
+// HTTPS would require the client to already trust mitmux's CA to MITM
+// that very connection - exactly the chicken-and-egg problem this page
+// exists to solve, so intercepting it on the CONNECT/TLS path wouldn't
+// make sense and isn't attempted.
+const certDownloadHost = "mitmux.cert"
+
+func isCertDownloadHost(hostPort string) bool {
+ host := hostPort
+ if h, _, err := net.SplitHostPort(hostPort); err == nil {
+ host = h
+ }
+ return strings.EqualFold(host, certDownloadHost)
+}
+
+// serveCACert answers with the CA certificate as a download. The
+// content type (application/x-x509-ca-cert) is what makes iOS and
+// Android offer to install it as a trusted certificate directly from
+// the browser's download prompt, rather than just saving a plain file.
+func (s *Server) serveCACert(w http.ResponseWriter) {
+ w.Header().Set("Content-Type", "application/x-x509-ca-cert")
+ w.Header().Set("Content-Disposition", `attachment; filename="mitmux-ca.pem"`)
+ w.WriteHeader(http.StatusOK)
+ w.Write(s.ca.CertPEM)
+}
+
func stripHopByHop(h http.Header) {
for _, k := range hopByHopHeaders {
h.Del(k)