package mitm import ( "context" "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "errors" "fmt" "io" stdlog "log" "net" "net/http" "net/url" "strings" "sync" "time" "cursor/internal/certs" "cursor/internal/logger" "cursor/internal/netproxy" "github.com/elazarl/goproxy" ) const ( // HeaderServerUpstreamURL 表示转发给 backend server 时携带的原始上游地址。 HeaderServerUpstreamURL = "X-Server-Upstream-URL" ) // ProxyServer 定义了当前模块中的 ProxyServer 类型。 type ProxyServer struct { // addr 表示当前声明中的 addr。 addr string // baseURL 表示当前声明中的 baseURL。 baseURL string // certManager 表示当前声明中的 certManager。 certManager *certs.Manager // baseEndpoint 表示当前声明中的 baseEndpoint。 baseEndpoint *url.URL // baseMu 表示当前声明中的 baseMu。 baseMu sync.RWMutex // upstreamClient 表示当前声明中的 upstreamClient。 upstreamClient *http.Client // proxy 表示当前声明中的 proxy。 proxy *goproxy.ProxyHttpServer // runMu 表示当前声明中的 runMu。 runMu sync.RWMutex // httpServer 表示当前声明中的 httpServer。 httpServer *http.Server // serveErrCh 表示当前声明中的 serveErrCh。 serveErrCh chan error } // Snapshot 定义了当前模块中的 Snapshot 类型。 type Snapshot struct { // ListenAddr 表示当前声明中的 ListenAddr。 ListenAddr string `json:"listenAddr"` // BaseURL 表示当前声明中的 BaseURL。 BaseURL string `json:"baseUrl"` // Running 表示当前声明中的 Running。 Running bool `json:"running"` } // hopByHopHeaders 表示当前模块中的 hopByHopHeaders 状态值。 var hopByHopHeaders = map[string]struct{}{ "Connection": {}, "Proxy-Connection": {}, "Keep-Alive": {}, "Proxy-Authenticate": {}, "Proxy-Authorization": {}, "Te": {}, "Trailer": {}, "Transfer-Encoding": {}, "Upgrade": {}, } const ( proxyLogRateLimitWindow = 30 * time.Second proxyLogRateLimitTTL = 5 * time.Minute proxyLogRateLimitMaxKeys = 1024 ) var proxyLogLimiter = newLogLimiter(proxyLogRateLimitWindow) type logLimiter struct { mu sync.Mutex window time.Duration entries map[string]*logLimitEntry } type logLimitEntry struct { nextAllowed time.Time lastSeen time.Time suppressed int } func newLogLimiter(window time.Duration) *logLimiter { if window <= 0 { window = 30 * time.Second } return &logLimiter{ window: window, entries: make(map[string]*logLimitEntry), } } func (limiter *logLimiter) ShouldLog(key string) (suppressed int, ok bool) { if limiter == nil { return 0, true } key = strings.TrimSpace(key) if key == "" { return 0, true } now := time.Now() limiter.mu.Lock() defer limiter.mu.Unlock() limiter.pruneLocked(now) entry := limiter.entries[key] if entry == nil { limiter.entries[key] = &logLimitEntry{nextAllowed: now.Add(limiter.window), lastSeen: now} return 0, true } entry.lastSeen = now if now.Before(entry.nextAllowed) { entry.suppressed++ return 0, false } suppressed = entry.suppressed entry.suppressed = 0 entry.nextAllowed = now.Add(limiter.window) return suppressed, true } func (limiter *logLimiter) pruneLocked(now time.Time) { if len(limiter.entries) == 0 { return } cutoff := now.Add(-proxyLogRateLimitTTL) for key, entry := range limiter.entries { if entry == nil || entry.lastSeen.Before(cutoff) { delete(limiter.entries, key) } } for len(limiter.entries) >= proxyLogRateLimitMaxKeys { oldestKey := "" var oldestSeen time.Time for key, entry := range limiter.entries { if entry == nil { oldestKey = key break } if oldestKey == "" || entry.lastSeen.Before(oldestSeen) { oldestKey = key oldestSeen = entry.lastSeen } } if oldestKey == "" { return } delete(limiter.entries, oldestKey) } } func logSuppressedProxyMessages(prefix string, suppressed int) { if suppressed <= 0 { return } logger.Infof("%s: suppressed %d repeated messages in last %s", prefix, suppressed, proxyLogRateLimitWindow) } // NewProxyServer 用于处理与 NewProxyServer 相关的逻辑。 func NewProxyServer(addr, baseURL, _ string, _ string, certManager *certs.Manager) (*ProxyServer, error) { u, normalizedBaseURL, err := parseBaseURL(baseURL) if err != nil { return nil, err } tlsConfig := &tls.Config{MinVersion: tls.VersionTLS12} if certManager != nil { caCertificate, err := certManager.CATLSCertificate() if err != nil { return nil, fmt.Errorf("load proxy backend CA: %w", err) } roots := x509.NewCertPool() if caCertificate.Leaf != nil { roots.AddCert(caCertificate.Leaf) } tlsConfig.RootCAs = roots } s := &ProxyServer{ addr: addr, baseURL: normalizedBaseURL, certManager: certManager, baseEndpoint: u, upstreamClient: &http.Client{ Transport: &http.Transport{ DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 200, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, ResponseHeaderTimeout: 60 * time.Second, TLSClientConfig: tlsConfig, }, }, } s.proxy = s.newGoproxyHandler() return s, nil } // parseBaseURL 用于处理与 parseBaseURL 相关的逻辑。 func parseBaseURL(baseURL string) (*url.URL, string, error) { baseURL = strings.TrimSpace(baseURL) if baseURL == "" { return nil, "", errors.New("base URL is empty") } u, err := url.Parse(baseURL) if err != nil { return nil, "", fmt.Errorf("parse base URL: %w", err) } if u.Scheme != "http" && u.Scheme != "https" { return nil, "", fmt.Errorf("unsupported base URL scheme %q", u.Scheme) } if strings.TrimSpace(u.Host) == "" { return nil, "", errors.New("base URL host is empty") } base := *u base.Path = "" base.RawPath = "" base.RawQuery = "" base.Fragment = "" base.RawFragment = "" normalizedBaseURL := strings.TrimRight(base.String(), "/") if normalizedBaseURL == "" { return nil, "", errors.New("base URL is empty") } return &base, normalizedBaseURL, nil } // UpdateBaseURL 用于处理与 UpdateBaseURL 相关的逻辑。 func (s *ProxyServer) UpdateBaseURL(baseURL string) error { u, normalizedBaseURL, err := parseBaseURL(baseURL) if err != nil { return err } s.baseMu.Lock() s.baseURL = normalizedBaseURL s.baseEndpoint = u s.baseMu.Unlock() return nil } // ListenAndServe 用于处理与 ListenAndServe 相关的逻辑。 func (s *ProxyServer) ListenAndServe() error { if err := s.Start(); err != nil { return err } s.runMu.RLock() errCh := s.serveErrCh s.runMu.RUnlock() if errCh == nil { return nil } return <-errCh } // Start 用于处理与 Start 相关的逻辑。 func (s *ProxyServer) Start() error { s.runMu.Lock() defer s.runMu.Unlock() if s.httpServer != nil { return errors.New("proxy is already running") } httpServer := &http.Server{ Addr: s.addr, Handler: s.proxy, ErrorLog: stdlog.New(&httpErrorFilterWriter{}, "", stdlog.LstdFlags), } ln, err := net.Listen("tcp", s.addr) if err != nil { return err } s.httpServer = httpServer s.serveErrCh = make(chan error, 1) go func() { var serveErr error err := httpServer.Serve(ln) if err != nil && !errors.Is(err, http.ErrServerClosed) { serveErr = err } s.runMu.Lock() if s.serveErrCh != nil { s.serveErrCh <- serveErr close(s.serveErrCh) s.serveErrCh = nil } if s.httpServer == httpServer { s.httpServer = nil } s.runMu.Unlock() }() return nil } // Stop 用于处理与 Stop 相关的逻辑。 func (s *ProxyServer) Stop(ctx context.Context) error { s.runMu.Lock() httpServer := s.httpServer if httpServer == nil { s.runMu.Unlock() return nil } s.httpServer = nil s.runMu.Unlock() return httpServer.Shutdown(ctx) } // IsRunning 用于处理与 IsRunning 相关的逻辑。 func (s *ProxyServer) IsRunning() bool { s.runMu.RLock() running := s.httpServer != nil s.runMu.RUnlock() return running } // Snapshot 用于处理与 Snapshot 相关的逻辑。 func (s *ProxyServer) Snapshot() Snapshot { baseURL := s.currentBaseURL() return Snapshot{ ListenAddr: s.addr, BaseURL: baseURL, Running: s.IsRunning(), } } // currentBaseURL 用于处理与 currentBaseURL 相关的逻辑。 func (s *ProxyServer) currentBaseURL() string { s.baseMu.RLock() baseURL := s.baseURL s.baseMu.RUnlock() return baseURL } // currentBaseEndpoint 用于处理与 currentBaseEndpoint 相关的逻辑。 func (s *ProxyServer) currentBaseEndpoint() *url.URL { s.baseMu.RLock() endpoint := s.baseEndpoint s.baseMu.RUnlock() if endpoint == nil { return nil } clone := *endpoint return &clone } // newGoproxyHandler 用于处理与 newGoproxyHandler 相关的逻辑。 func (s *ProxyServer) newGoproxyHandler() *goproxy.ProxyHttpServer { proxy := goproxy.NewProxyHttpServer() proxy.Verbose = false proxy.AllowHTTP2 = true proxy.Logger = &goproxyLogAdapter{} proxy.CertStore = newMITMCertStore() proxy.Tr = netproxy.NewTransport(&http.Transport{ DialContext: (&net.Dialer{Timeout: 10 * time.Second, KeepAlive: 30 * time.Second}).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 200, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, ResponseHeaderTimeout: 60 * time.Second, }) proxy.ConnectionErrHandler = func(conn io.Writer, ctx *goproxy.ProxyCtx, err error) { host := requestHost(ctx) remoteAddr := requestRemoteAddr(ctx) ua := trimForLog(requestUserAgent(ctx), 160) msg := fmt.Sprintf("proxy connect error: host=%s remote=%s ua=%q err=%v", host, remoteAddr, ua, err) key := "proxy-connect-error|" + strings.ToLower(strings.TrimSpace(host)) + "|" + errorRateLimitKey(err) if suppressed, ok := proxyLogLimiter.ShouldLog(key); ok { logSuppressedProxyMessages("proxy connect error", suppressed) logger.Errorf("%s", msg) } } var mitmAction *goproxy.ConnectAction if s.certManager != nil { caCert, err := s.certManager.CATLSCertificate() if err != nil { logger.Errorf("MITM disabled: invalid CA config err=%v", err) } else { logMITMCAInfo(caCert) baseTLSConfigFromCA := goproxy.TLSConfigFromCA(caCert) mitmAction = &goproxy.ConnectAction{ Action: goproxy.ConnectMitm, TLSConfig: func(host string, ctx *goproxy.ProxyCtx) (*tls.Config, error) { cfg, err := baseTLSConfigFromCA(host, ctx) connectHost := normalizeConnectHost(host) remoteAddr := requestRemoteAddr(ctx) ua := trimForLog(requestUserAgent(ctx), 160) if err != nil { logger.Errorf( "MITM tls config failed: connect_host=%s remote=%s ua=%q err=%v", connectHost, remoteAddr, ua, err, ) return nil, err } return cfg, nil }, } } } proxy.OnRequest().HandleConnect(goproxy.FuncHttpsHandler(func(host string, ctx *goproxy.ProxyCtx) (*goproxy.ConnectAction, string) { if mitmAction == nil || !isWhitelistedRelayHost(host) { return goproxy.OkConnect, host } return mitmAction, host })) // MITM 解密后:Cursor 白名单域名转发到 backend server,其余请求由 goproxy 直连回源。 proxy.OnRequest().DoFunc(func(req *http.Request, _ *goproxy.ProxyCtx) (*http.Request, *http.Response) { req.Header.Del(HeaderServerUpstreamURL) host := hostFromHTTPRequest(req) if isWhitelistedRelayHost(host) { if shouldHandleLocalCORSPreflight(req) { return req, buildLocalCORSPreflightResponse(req) } raw := requestURL(req) if parsedRaw, rawErr := rawURLForRelay(req); rawErr == nil { raw = parsedRaw } resp, err := s.forwardToServer(req) if err != nil { logger.Errorf("转发失败: %s %s %v", req.Method, raw, err) return req, goproxy.NewResponse(req, goproxy.ContentTypeText, http.StatusBadGateway, "bad gateway") } return req, resp } return req, nil }) return proxy } // forwardToServer 用于处理与 forwardToServer 相关的逻辑。 func (s *ProxyServer) forwardToServer(incoming *http.Request) (*http.Response, error) { if incoming == nil { return nil, errors.New("nil request") } rawURL, err := rawURLForRelay(incoming) if err != nil { return nil, err } endpoint := s.currentBaseEndpoint() if endpoint == nil { return nil, errors.New("server endpoint is not configured") } forwardURL := *endpoint if incoming.URL != nil { forwardURL.Path = incoming.URL.Path forwardURL.RawPath = incoming.URL.RawPath forwardURL.RawQuery = incoming.URL.RawQuery } serverReq, err := http.NewRequestWithContext(incoming.Context(), incoming.Method, forwardURL.String(), incoming.Body) if err != nil { return nil, err } serverReq.ContentLength = incoming.ContentLength copyHeaders(serverReq.Header, incoming.Header) serverReq.Header.Set(HeaderServerUpstreamURL, rawURL) removeHopByHop(serverReq.Header) resp, err := s.upstreamClient.Do(serverReq) if err != nil { return nil, fmt.Errorf("forward to backend server: %w", err) } removeHopByHop(resp.Header) return resp, nil } // requestHost 用于处理与 requestHost 相关的逻辑。 func requestHost(ctx *goproxy.ProxyCtx) string { if ctx == nil || ctx.Req == nil { return "" } if strings.TrimSpace(ctx.Req.Host) != "" { return ctx.Req.Host } if ctx.Req.URL != nil { return ctx.Req.URL.Host } return "" } // requestRemoteAddr 用于处理与 requestRemoteAddr 相关的逻辑。 func requestRemoteAddr(ctx *goproxy.ProxyCtx) string { if ctx == nil || ctx.Req == nil { return "-" } remoteAddr := strings.TrimSpace(ctx.Req.RemoteAddr) if remoteAddr == "" { return "-" } return remoteAddr } // requestUserAgent 用于处理与 requestUserAgent 相关的逻辑。 func requestUserAgent(ctx *goproxy.ProxyCtx) string { if ctx == nil || ctx.Req == nil { return "-" } ua := strings.TrimSpace(ctx.Req.UserAgent()) if ua == "" { return "-" } return ua } // requestURL 用于处理与 requestURL 相关的逻辑。 func requestURL(req *http.Request) string { if req == nil || req.URL == nil { return "" } u := req.URL.String() if strings.TrimSpace(u) == "" { return "/" } return u } // rawURLForRelay 用于处理与 rawURLForRelay 相关的逻辑。 func rawURLForRelay(r *http.Request) (string, error) { if r == nil { return "", errors.New("nil request") } if r.URL != nil && r.URL.IsAbs() { return r.URL.String(), nil } host := strings.TrimSpace(r.Host) if host == "" && r.URL != nil { host = strings.TrimSpace(r.URL.Host) } if host == "" { return "", errors.New("missing host") } scheme := "https" if r.TLS == nil { scheme = "http" } path := "/" if r.URL != nil { if strings.TrimSpace(r.URL.RequestURI()) != "" { path = r.URL.RequestURI() } } return scheme + "://" + host + path, nil } // copyHeaders 用于处理与 copyHeaders 相关的逻辑。 func copyHeaders(dst, src http.Header) { removeHopByHop(src) for k, vv := range src { for _, v := range vv { dst.Add(k, v) } } } // removeHopByHop 用于处理与 removeHopByHop 相关的逻辑。 func removeHopByHop(h http.Header) { if h == nil { return } connectionVals := h.Values("Connection") for _, line := range connectionVals { for _, token := range strings.Split(line, ",") { name := http.CanonicalHeaderKey(strings.TrimSpace(token)) if name != "" { h.Del(name) } } } for k := range hopByHopHeaders { h.Del(k) } } // trimForLog 用于处理与 trimForLog 相关的逻辑。 func trimForLog(text string, limit int) string { text = strings.TrimSpace(text) if limit <= 0 { limit = 160 } runes := []rune(text) if len(runes) <= limit { return text } return string(runes[:limit]) + "..." } // normalizeConnectHost 用于处理与 normalizeConnectHost 相关的逻辑。 func normalizeConnectHost(host string) string { host = strings.TrimSpace(host) if host == "" { return "-" } return host } // hostFromHTTPRequest 用于处理与 hostFromHTTPRequest 相关的逻辑。 func hostFromHTTPRequest(req *http.Request) string { if req == nil { return "" } if strings.TrimSpace(req.Host) != "" { return req.Host } if req.URL != nil { return req.URL.Host } return "" } // isWhitelistedRelayHost 用于处理与 isWhitelistedRelayHost 相关的逻辑。 func isWhitelistedRelayHost(host string) bool { host = strings.TrimSpace(strings.ToLower(host)) if host == "" { return false } if strings.HasPrefix(host, "[") { host = strings.TrimPrefix(host, "[") host = strings.TrimSuffix(host, "]") } if h, _, err := net.SplitHostPort(host); err == nil { host = strings.ToLower(strings.TrimSpace(h)) } else if strings.Count(host, ":") > 1 { // IPv6 without port host = strings.Trim(host, "[]") } if host == "api2.cursor.sh" || host == "api3.cursor.sh" { return true } if strings.HasSuffix(host, ".cursor.sh") { return true } return false } // mitmCertStore 缓存 goproxy 为站点动态签发的证书,避免同一 host 重复执行 RSA/x509 签发。 type mitmCertStore struct { mu sync.Mutex certs map[string]*tls.Certificate } func newMITMCertStore() *mitmCertStore { return &mitmCertStore{certs: make(map[string]*tls.Certificate)} } func (store *mitmCertStore) Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) { hostname = strings.TrimSpace(strings.ToLower(hostname)) if hostname == "" { return gen() } store.mu.Lock() defer store.mu.Unlock() if cert, ok := store.certs[hostname]; ok { return cert, nil } cert, err := gen() if err != nil { return nil, err } store.certs[hostname] = cert return cert, nil } // shouldHandleLocalCORSPreflight 用于处理与 shouldHandleLocalCORSPreflight 相关的逻辑。 func shouldHandleLocalCORSPreflight(req *http.Request) bool { if req == nil { return false } if !strings.EqualFold(strings.TrimSpace(req.Method), http.MethodOptions) { return false } if strings.TrimSpace(req.Header.Get("Origin")) == "" { return false } if strings.TrimSpace(req.Header.Get("Access-Control-Request-Method")) == "" { return false } return true } // buildLocalCORSPreflightResponse 用于处理与 buildLocalCORSPreflightResponse 相关的逻辑。 func buildLocalCORSPreflightResponse(req *http.Request) *http.Response { allowOrigin := "*" if req != nil { origin := strings.TrimSpace(req.Header.Get("Origin")) if origin != "" { allowOrigin = origin } } headers := make(http.Header) headers.Set("Access-Control-Allow-Origin", allowOrigin) headers.Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS") headers.Set("Access-Control-Allow-Headers", "*") headers.Set("Access-Control-Allow-Credentials", "true") headers.Set("Access-Control-Max-Age", "86400") if allowOrigin != "*" { headers.Set("Vary", "Origin") } body := io.NopCloser(strings.NewReader("")) return &http.Response{ StatusCode: http.StatusNoContent, Status: "204 No Content", Header: headers, Body: body, Request: req, } } // httpErrorFilterWriter 定义了当前模块中的 httpErrorFilterWriter 类型。 type httpErrorFilterWriter struct{} // Write 用于处理与 Write 相关的逻辑。 func (w *httpErrorFilterWriter) Write(p []byte) (int, error) { msg := strings.TrimSpace(string(p)) if msg == "" { return len(p), nil } lower := strings.ToLower(msg) if strings.Contains(lower, "tls: first record does not look like a tls handshake") { logger.Infof("proxy ignore handshake mismatch: %s", msg) return len(p), nil } if strings.Contains(lower, "tls handshake error") { logger.Errorf("proxy tls handshake error: %s", msg) return len(p), nil } logger.Infof("proxy http server: %s", msg) return len(p), nil } // goproxyLogAdapter 定义了当前模块中的 goproxyLogAdapter 类型。 type goproxyLogAdapter struct{} // Printf 用于处理与 Printf 相关的逻辑。 func (l *goproxyLogAdapter) Printf(format string, args ...interface{}) { msg := strings.TrimSpace(fmt.Sprintf(format, args...)) if msg == "" { return } lower := strings.ToLower(msg) if strings.Contains(lower, "tls: first record does not look like a tls handshake") { // logger.Infof("goproxy ignore handshake mismatch: %s", msg) return } if strings.Contains(lower, "broken pipe") || strings.Contains(lower, "connection reset by peer") { // logger.Infof("goproxy transient network error: %s", msg) return } if suppressed, ok := proxyLogLimiter.ShouldLog("goproxy|" + goproxyMessageRateLimitKey(msg)); ok { logSuppressedProxyMessages("goproxy", suppressed) logger.Infof("goproxy: %s", msg) } } func errorRateLimitKey(err error) string { if err == nil { return "" } return normalizeRateLimitKey(err.Error()) } func goproxyMessageRateLimitKey(msg string) string { return normalizeRateLimitKey(msg) } func normalizeRateLimitKey(value string) string { value = strings.ToLower(strings.TrimSpace(value)) if value == "" { return "" } value = normalizeAddrPortsForLogKey(value) value = normalizeGoproxyRequestIDForLogKey(value) return value } func normalizeAddrPortsForLogKey(value string) string { parts := strings.Fields(value) for index, part := range parts { parts[index] = normalizeAddrPortToken(part) } return strings.Join(parts, " ") } func normalizeAddrPortToken(token string) string { trimmedRight := strings.TrimRight(token, ",;)]}") suffix := strings.TrimPrefix(token[len(trimmedRight):], "") host, port, err := net.SplitHostPort(trimmedRight) if err != nil || host == "" || port == "" { return token } return net.JoinHostPort(host, "*") + suffix } func normalizeGoproxyRequestIDForLogKey(value string) string { start := strings.Index(value, "[") end := strings.Index(value, "]") if start < 0 || end <= start { return value } prefix := strings.TrimSpace(value[:start]) if prefix != "" { return value } return "[*]" + value[end+1:] } // logMITMCAInfo 用于处理与 logMITMCAInfo 相关的逻辑。 func logMITMCAInfo(caTLS *tls.Certificate) { if caTLS == nil || len(caTLS.Certificate) == 0 { logger.Errorf("MITM CA info unavailable: empty certificate chain") return } leaf, err := x509.ParseCertificate(caTLS.Certificate[0]) if err != nil { logger.Errorf("MITM CA parse failed: %v", err) return } sum := sha256.Sum256(leaf.Raw) logger.Infof( "MITM enabled: sha256=%s subject=%s valid=%s~%s", strings.ToUpper(hex.EncodeToString(sum[:])), leaf.Subject.String(), leaf.NotBefore.Format(time.RFC3339), leaf.NotAfter.Format(time.RFC3339), ) }