package syncer import ( "crypto/tls" "crypto/x509" "fmt" "log" "net/http" "os" "time" ) // userAgent 出站标识。HF 与中转网关都会记录它,便于排障时区分是谁在同步。 const userAgent = "workbuddy2api-sync/1.0" // newHTTPClient 构造同步用 HTTP 客户端。 // // insecureSkipVerify 仅供本地抓包代理/自签证书排障(默认关闭);一旦开启会打一条 // 醒目警告——这条链路上流动的是账号 refreshToken,不能悄悄降级 TLS。 // // caBundle 是额外的 PEM 证书包:某些抓包代理(DevSidecar / Reqable / ProxyPin) // 会在系统信任库里装根证书,Go 在 Windows 上本来就吃系统库;但显式给一份也支持, // 便于非 Windows 或证书没装进系统库的场景。 func newHTTPClient(timeout time.Duration, insecureSkipVerify bool, caBundle string) *http.Client { if timeout <= 0 { timeout = 60 * time.Second } tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConns: 8, MaxIdleConnsPerHost: 4, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 20 * time.Second, ExpectContinueTimeout: 5 * time.Second, ForceAttemptHTTP2: true, } tlsCfg := &tls.Config{MinVersion: tls.VersionTLS12} if caBundle != "" { pool, err := loadCABundle(caBundle) if err != nil { log.Printf("[syncer] 警告: 加载 ca_bundle 失败(%v),改用系统信任库", err) } else { tlsCfg.RootCAs = pool } } if insecureSkipVerify { log.Printf("[syncer] 警告: insecure_skip_verify 已开启,同步链路不再校验 TLS 证书") tlsCfg.InsecureSkipVerify = true // #nosec G402 —— 显式开关,默认关闭 } tr.TLSClientConfig = tlsCfg return &http.Client{Timeout: timeout, Transport: tr} } // loadCABundle 读取一个 PEM 证书包,并在系统信任库之上追加它。 // // 为什么要在系统库之上"追加"而不是替换:抓包代理的根证书只是多一个信任锚, // 不该把公共 CA 全部丢掉——否则一旦代理临时停了,同步会连带访问不了任何 HTTPS。 func loadCABundle(path string) (*x509.CertPool, error) { pem, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("读取 %s: %w", path, err) } pool, err := x509.SystemCertPool() if err != nil || pool == nil { pool = x509.NewCertPool() } if !pool.AppendCertsFromPEM(pem) { return nil, fmt.Errorf("%s 里没有可解析的 PEM 证书", path) } return pool, nil }