diff options
| author | srdusr <[email protected]> | 2024-01-16 16:26:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-01-16 16:26:00 +0200 |
| commit | 1125afc47b9d6e68d95d0ffdbb74514f4618e624 (patch) | |
| tree | 63d5150b7dbc74685ebaec35ee0e74ec30c721d5 /internal/proxy/proxy.go | |
| download | mitmux-1125afc47b9d6e68d95d0ffdbb74514f4618e624.tar.gz mitmux-1125afc47b9d6e68d95d0ffdbb74514f4618e624.zip | |
Scaffold mitmux: proxy daemon, CA generation, HTTP/CONNECT passthrough
Implements build-order step 1: headless proxy daemon (mitmuxd) with
plaintext HTTP passthrough and raw CONNECT tunneling, plus root CA
generation/persistence for later TLS interception. Verified live
against real HTTP and HTTPS requests through the proxy.
Diffstat (limited to 'internal/proxy/proxy.go')
| -rw-r--r-- | internal/proxy/proxy.go | 153 |
1 files changed, 153 insertions, 0 deletions
diff --git a/internal/proxy/proxy.go b/internal/proxy/proxy.go new file mode 100644 index 0000000..c9b6195 --- /dev/null +++ b/internal/proxy/proxy.go @@ -0,0 +1,153 @@ +// Package proxy is the mitmux proxy engine: a forward HTTP proxy that, +// at this build stage, passes traffic through unmodified. CONNECT +// requests (HTTPS) are tunneled raw rather than intercepted - TLS +// interception is a later build step. +package proxy + +import ( + "context" + "io" + "log" + "net" + "net/http" + "time" +) + +// hopByHopHeaders are stripped before forwarding a request or response, +// per RFC 7230 6.1 - they are meaningful only between a client and its +// immediate next hop, not end-to-end. +var hopByHopHeaders = []string{ + "Connection", + "Proxy-Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "TE", + "Trailers", + "Transfer-Encoding", + "Upgrade", +} + +// Server is a forward proxy listener. +type Server struct { + Addr string + + transport *http.Transport + server *http.Server +} + +// New creates a proxy Server bound to addr (e.g. "127.0.0.1:8080"). +func New(addr string) *Server { + s := &Server{ + Addr: addr, + transport: &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + }).DialContext, + ForceAttemptHTTP2: false, + MaxIdleConns: 100, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + } + s.server = &http.Server{ + Addr: addr, + Handler: http.HandlerFunc(s.handle), + } + return s +} + +// ListenAndServe starts the proxy and blocks until it stops. +func (s *Server) ListenAndServe() error { + log.Printf("proxy listening on %s", s.Addr) + return s.server.ListenAndServe() +} + +// Shutdown gracefully stops the proxy. +func (s *Server) Shutdown(ctx context.Context) error { + return s.server.Shutdown(ctx) +} + +func (s *Server) handle(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + s.handleConnect(w, r) + return + } + s.handleHTTP(w, r) +} + +// handleConnect tunnels a CONNECT request raw, byte for byte, without +// terminating TLS. This is the passthrough behavior for HTTPS traffic +// until TLS interception is implemented. +func (s *Server) handleConnect(w http.ResponseWriter, r *http.Request) { + dst, err := net.DialTimeout("tcp", r.Host, 10*time.Second) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + defer dst.Close() + + hijacker, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "hijacking not supported", http.StatusInternalServerError) + return + } + src, _, err := hijacker.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer src.Close() + + if _, err := src.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil { + return + } + + done := make(chan struct{}, 2) + go func() { + io.Copy(dst, src) + done <- struct{}{} + }() + go func() { + io.Copy(src, dst) + done <- struct{}{} + }() + <-done +} + +// handleHTTP forwards a plain (non-CONNECT) proxy request and copies the +// response back unmodified. +func (s *Server) handleHTTP(w http.ResponseWriter, r *http.Request) { + if !r.URL.IsAbs() { + http.Error(w, "mitmux: request must use absolute-form URI (configure as a proxy, not a target)", http.StatusBadRequest) + return + } + + outReq := r.Clone(r.Context()) + outReq.RequestURI = "" + stripHopByHop(outReq.Header) + + resp, err := s.transport.RoundTrip(outReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + stripHopByHop(resp.Header) + for k, vv := range resp.Header { + for _, v := range vv { + w.Header().Add(k, v) + } + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + +func stripHopByHop(h http.Header) { + for _, k := range hopByHopHeaders { + h.Del(k) + } +} |