Files
MyGoNavi/internal/app/global_proxy.go
2026-04-05 11:39:54 +08:00

331 lines
8.4 KiB
Go

package app
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"GoNavi-Wails/internal/connection"
"GoNavi-Wails/internal/logger"
proxytunnel "GoNavi-Wails/internal/proxy"
)
type globalProxySnapshot struct {
Enabled bool `json:"enabled"`
Proxy connection.ProxyConfig `json:"proxy"`
}
var globalProxyRuntime = struct {
mu sync.RWMutex
enabled bool
proxy connection.ProxyConfig
}{}
type localProxyTLSFallbackTransport struct {
primary *http.Transport
fallback *http.Transport
proxyEndpoint string
}
func currentGlobalProxyConfig() globalProxySnapshot {
globalProxyRuntime.mu.RLock()
defer globalProxyRuntime.mu.RUnlock()
if !globalProxyRuntime.enabled {
return globalProxySnapshot{
Enabled: false,
Proxy: connection.ProxyConfig{},
}
}
return globalProxySnapshot{
Enabled: true,
Proxy: globalProxyRuntime.proxy,
}
}
func setGlobalProxyConfig(enabled bool, proxyConfig connection.ProxyConfig) (globalProxySnapshot, error) {
if !enabled {
globalProxyRuntime.mu.Lock()
globalProxyRuntime.enabled = false
globalProxyRuntime.proxy = connection.ProxyConfig{}
globalProxyRuntime.mu.Unlock()
return currentGlobalProxyConfig(), nil
}
normalizedProxy, err := proxytunnel.NormalizeConfig(proxyConfig)
if err != nil {
return globalProxySnapshot{}, err
}
globalProxyRuntime.mu.Lock()
globalProxyRuntime.enabled = true
globalProxyRuntime.proxy = normalizedProxy
globalProxyRuntime.mu.Unlock()
return currentGlobalProxyConfig(), nil
}
func (a *App) ConfigureGlobalProxy(enabled bool, proxyConfig connection.ProxyConfig) connection.QueryResult {
before := currentGlobalProxyConfig()
snapshot, err := setGlobalProxyConfig(enabled, proxyConfig)
if err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
// 前端可能在同一配置下重复触发同步(例如严格模式或状态回放),
// 这里做幂等日志,避免重复刷屏。
if !globalProxySnapshotEqual(before, snapshot) {
if snapshot.Enabled {
authState := ""
if strings.TrimSpace(snapshot.Proxy.User) != "" {
authState = "(认证:已配置)"
}
logger.Infof(
"全局代理已启用:%s://%s:%d%s",
strings.ToLower(strings.TrimSpace(snapshot.Proxy.Type)),
strings.TrimSpace(snapshot.Proxy.Host),
snapshot.Proxy.Port,
authState,
)
} else {
logger.Infof("全局代理已关闭")
}
}
return connection.QueryResult{
Success: true,
Message: "全局代理配置已生效",
Data: snapshot,
}
}
func globalProxySnapshotEqual(a, b globalProxySnapshot) bool {
if a.Enabled != b.Enabled {
return false
}
if !a.Enabled {
return true
}
return proxyConfigEqual(a.Proxy, b.Proxy)
}
func proxyConfigEqual(a, b connection.ProxyConfig) bool {
return strings.EqualFold(strings.TrimSpace(a.Type), strings.TrimSpace(b.Type)) &&
strings.TrimSpace(a.Host) == strings.TrimSpace(b.Host) &&
a.Port == b.Port &&
strings.TrimSpace(a.User) == strings.TrimSpace(b.User) &&
a.Password == b.Password
}
func currentGlobalProxyView() connection.GlobalProxyView {
snapshot := currentGlobalProxyConfig()
if !snapshot.Enabled {
return connection.GlobalProxyView{Enabled: false}
}
return connection.GlobalProxyView{
Enabled: true,
Type: snapshot.Proxy.Type,
Host: snapshot.Proxy.Host,
Port: snapshot.Proxy.Port,
User: snapshot.Proxy.User,
HasPassword: strings.TrimSpace(snapshot.Proxy.Password) != "",
}
}
func (a *App) GetGlobalProxyConfig() connection.QueryResult {
return connection.QueryResult{
Success: true,
Message: "OK",
Data: currentGlobalProxyView(),
}
}
func applyGlobalProxyToConnection(config connection.ConnectionConfig) connection.ConnectionConfig {
effective := config
if effective.UseProxy || effective.UseHTTPTunnel {
return effective
}
if isFileDatabaseType(effective.Type) {
effective.Proxy = connection.ProxyConfig{}
return effective
}
snapshot := currentGlobalProxyConfig()
if !snapshot.Enabled {
effective.Proxy = connection.ProxyConfig{}
return effective
}
effective.UseProxy = true
effective.Proxy = snapshot.Proxy
return effective
}
func isFileDatabaseType(driverType string) bool {
switch strings.ToLower(strings.TrimSpace(driverType)) {
case "sqlite", "duckdb":
return true
default:
return false
}
}
func newHTTPClientWithGlobalProxy(timeout time.Duration) *http.Client {
client := &http.Client{
Timeout: timeout,
}
if transport := buildHTTPTransportWithGlobalProxy(); transport != nil {
client.Transport = transport
}
return client
}
func buildHTTPTransportWithGlobalProxy() http.RoundTripper {
baseTransport, ok := http.DefaultTransport.(*http.Transport)
if !ok || baseTransport == nil {
return nil
}
transport := baseTransport.Clone()
snapshot := currentGlobalProxyConfig()
if !snapshot.Enabled {
transport.Proxy = http.ProxyFromEnvironment
return transport
}
proxyURL, err := buildProxyURLFromConfig(snapshot.Proxy)
if err != nil {
logger.Warnf("全局代理配置无效,回退系统代理:%v", err)
transport.Proxy = http.ProxyFromEnvironment
return transport
}
transport.Proxy = http.ProxyURL(proxyURL)
if !isLoopbackProxyHost(snapshot.Proxy.Host) {
return transport
}
fallbackTransport := transport.Clone()
fallbackTransport.TLSClientConfig = cloneTLSConfigWithInsecureSkipVerify(fallbackTransport.TLSClientConfig)
return &localProxyTLSFallbackTransport{
primary: transport,
fallback: fallbackTransport,
proxyEndpoint: proxyURL.Redacted(),
}
}
func (t *localProxyTLSFallbackTransport) RoundTrip(req *http.Request) (*http.Response, error) {
resp, err := t.primary.RoundTrip(req)
if err == nil {
return resp, nil
}
if !isTLSFallbackCandidate(req.Method, err) {
return nil, err
}
retryReq, cloneErr := cloneRequestForRetry(req)
if cloneErr != nil {
return nil, err
}
logger.Warnf("检测到本地代理 TLS 证书不受信任,启用兼容回退:代理=%s 目标=%s 错误=%v", t.proxyEndpoint, req.URL.String(), err)
return t.fallback.RoundTrip(retryReq)
}
func isTLSFallbackCandidate(method string, err error) bool {
if !isIdempotentRequestMethod(method) {
return false
}
return isUnknownAuthorityError(err)
}
func isIdempotentRequestMethod(method string) bool {
switch strings.ToUpper(strings.TrimSpace(method)) {
case http.MethodGet, http.MethodHead:
return true
default:
return false
}
}
func cloneRequestForRetry(req *http.Request) (*http.Request, error) {
cloned := req.Clone(req.Context())
if req.Body == nil || req.Body == http.NoBody {
return cloned, nil
}
if req.GetBody == nil {
return nil, fmt.Errorf("request body not replayable")
}
body, err := req.GetBody()
if err != nil {
return nil, err
}
cloned.Body = body
return cloned, nil
}
func isUnknownAuthorityError(err error) bool {
var unknownErr x509.UnknownAuthorityError
if errors.As(err, &unknownErr) {
return true
}
return strings.Contains(strings.ToLower(err.Error()), "x509: certificate signed by unknown authority")
}
func cloneTLSConfigWithInsecureSkipVerify(base *tls.Config) *tls.Config {
if base == nil {
return &tls.Config{InsecureSkipVerify: true}
}
cloned := base.Clone()
cloned.InsecureSkipVerify = true
return cloned
}
func isLoopbackProxyHost(host string) bool {
trimmed := strings.TrimSpace(host)
if trimmed == "" {
return false
}
if strings.EqualFold(trimmed, "localhost") {
return true
}
ip := net.ParseIP(trimmed)
if ip == nil {
return false
}
return ip.IsLoopback()
}
func buildProxyURLFromConfig(proxyConfig connection.ProxyConfig) (*url.URL, error) {
normalizedProxy, err := proxytunnel.NormalizeConfig(proxyConfig)
if err != nil {
return nil, err
}
proxyType := strings.ToLower(strings.TrimSpace(normalizedProxy.Type))
if proxyType != "http" && proxyType != "socks5" {
return nil, fmt.Errorf("不支持的代理类型:%s", normalizedProxy.Type)
}
if strings.TrimSpace(normalizedProxy.Host) == "" {
return nil, fmt.Errorf("代理地址不能为空")
}
if normalizedProxy.Port <= 0 || normalizedProxy.Port > 65535 {
return nil, fmt.Errorf("代理端口无效:%d", normalizedProxy.Port)
}
proxyURL := &url.URL{
Scheme: proxyType,
Host: net.JoinHostPort(strings.TrimSpace(normalizedProxy.Host), strconv.Itoa(normalizedProxy.Port)),
}
if strings.TrimSpace(normalizedProxy.User) != "" {
proxyURL.User = url.UserPassword(strings.TrimSpace(normalizedProxy.User), normalizedProxy.Password)
}
return proxyURL, nil
}