Files
Local-Cursor/internal/mitm/service.go
FuDie0915 db73cf5a39 chore: import Local-Cursor v1.1.8 source snapshot
Includes the current working directory changes. Historical commits and release binaries are excluded to limit Gitea storage.
Source base: d7515973fbfd6dd2001fa156c483455b8081c7cb
2026-11-10 01:24:28 +08:00

859 lines
22 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
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,
},
},
}
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),
)
}