Ctrl K
ComunidadGuías…

internal/pluginhost/http_bridge.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package pluginhost

import (
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/MoeForge/moego/base64"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/config"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/httpwire"
	"github.com/router-for-me/CLIProxyAPI/v8/internal/runtime/executor/helps"
	coreauth "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/auth"
	cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v8/sdk/cliproxy/executor"
	"github.com/router-for-me/CLIProxyAPI/v8/sdk/pluginapi"
	"github.com/router-for-me/CLIProxyAPI/v8/sdk/proxyutil"
	log "github.com/sirupsen/logrus"
	"golang.org/x/net/proxy"
)

type hostHTTPClient struct {
	host            *Host
	auth            *coreauth.Auth
	provider        string
	requestProxyURL string
}

func (h *Host) newHTTPClient(auth *coreauth.Auth, providers ...string) pluginapi.HostHTTPClient {
	return h.newHTTPClientWithProxy(auth, "", providers...)
}

func (h *Host) newHTTPClientWithProxy(auth *coreauth.Auth, requestProxyURL string, providers ...string) pluginapi.HostHTTPClient {
	provider := ""
	if len(providers) > 0 {
		provider = providers[0]
	}
	return &hostHTTPClient{
		host:            h,
		auth:            auth,
		provider:        provider,
		requestProxyURL: strings.TrimSpace(requestProxyURL),
	}
}

func (c *hostHTTPClient) proxyContext(ctx context.Context) context.Context {
	if c == nil {
		return cliproxyexecutor.WithoutRequestProxyURL(ctx)
	}
	return cliproxyexecutor.WithRequestProxyURL(ctx, c.requestProxyURL)
}

func (c *hostHTTPClient) Do(ctx context.Context, req pluginapi.HTTPRequest) (pluginapi.HTTPResponse, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	resp, cfg, cleanup, errDo := c.doHTTP(ctx, req)
	if errDo != nil {
		return pluginapi.HTTPResponse{}, errDo
	}
	defer func() {
		if errClose := resp.Body.Close(); errClose != nil {
			log.Warnf("pluginhost: response body close error: %v", errClose)
		}
		if cleanup != nil {
			cleanup()
		}
	}()
	helps.RecordAPIResponseMetadata(ctx, cfg, resp.StatusCode, resp.Header.Clone())
	body, errReadAll := io.ReadAll(resp.Body)
	if len(body) > 0 {
		helps.AppendAPIResponseChunk(ctx, cfg, body)
	}
	if errReadAll != nil {
		helps.RecordAPIResponseError(ctx, cfg, errReadAll)
		return pluginapi.HTTPResponse{}, fmt.Errorf("read host http response: %w", errReadAll)
	}
	return pluginapi.HTTPResponse{
		StatusCode: resp.StatusCode,
		Headers:    cloneHeader(resp.Header),
		Body:       body,
	}, nil
}

func (c *hostHTTPClient) DoStream(ctx context.Context, req pluginapi.HTTPRequest) (pluginapi.HTTPStreamResponse, error) {
	if ctx == nil {
		ctx = context.Background()
	}
	resp, cfg, cleanup, errDo := c.doHTTP(ctx, req)
	if errDo != nil {
		return pluginapi.HTTPStreamResponse{}, errDo
	}
	helps.RecordAPIResponseMetadata(ctx, cfg, resp.StatusCode, resp.Header.Clone())
	chunks := make(chan pluginapi.HTTPStreamChunk)
	go func() {
		defer close(chunks)
		defer func() {
			if errClose := resp.Body.Close(); errClose != nil {
				log.Warnf("pluginhost: stream response body close error: %v", errClose)
			}
			if cleanup != nil {
				cleanup()
			}
		}()
		buf := make([]byte, 32*1024)
		for {
			n, errRead := resp.Body.Read(buf)
			if n > 0 {
				payload := bytes.Clone(buf[:n])
				helps.AppendAPIResponseChunk(ctx, cfg, payload)
				select {
				case <-ctx.Done():
					return
				case chunks <- pluginapi.HTTPStreamChunk{Payload: payload}:
				}
			}
			if errRead != nil {
				if errRead != io.EOF {
					helps.RecordAPIResponseError(ctx, cfg, errRead)
					select {
					case <-ctx.Done():
					case chunks <- pluginapi.HTTPStreamChunk{Err: errRead}:
					}
				}
				return
			}
		}
	}()
	return pluginapi.HTTPStreamResponse{
		StatusCode: resp.StatusCode,
		Headers:    cloneHeader(resp.Header),
		Chunks:     chunks,
	}, nil
}

func (c *hostHTTPClient) doHTTP(ctx context.Context, req pluginapi.HTTPRequest) (*http.Response, *config.Config, func(), error) {
	if c == nil || c.host == nil {
		return nil, nil, nil, fmt.Errorf("host http client is unavailable")
	}
	if ctx == nil {
		ctx = context.Background()
	}
	cfg := c.host.currentRuntimeConfig()
	method := req.Method
	if method == "" {
		method = http.MethodGet
	}
	httpReq, errNewRequest := http.NewRequestWithContext(ctx, method, req.URL, bytes.NewReader(bytes.Clone(req.Body)))
	if errNewRequest != nil {
		return nil, cfg, nil, fmt.Errorf("create host http request: %w", errNewRequest)
	}
	httpReq.Header = cloneHeader(req.Headers)
	c.recordHTTPRequest(ctx, cfg, httpReq, req.Body)
	client, cleanup, errClient := c.newHTTPClientForRequest(ctx, cfg, req, httpReq)
	if errClient != nil {
		return nil, cfg, nil, errClient
	}
	if client == nil {
		client = &http.Client{}
	}
	cliproxyexecutor.MarkUpstreamAttempt(ctx)
	resp, errDo := client.Do(httpReq)
	if errDo != nil {
		if cleanup != nil {
			cleanup()
		}
		helps.RecordAPIResponseError(ctx, cfg, errDo)
		return nil, cfg, nil, fmt.Errorf("execute host http request: %w", errDo)
	}
	return resp, cfg, cleanup, nil
}

func (c *hostHTTPClient) recordHTTPRequest(ctx context.Context, cfg *config.Config, req *http.Request, body []byte) {
	if req == nil {
		return
	}
	provider := c.provider
	var authID, authLabel, authType, authValue string
	if c.auth != nil {
		authID = c.auth.ID
		authLabel = c.auth.Label
		authType, authValue = c.auth.AccountInfo()
		if provider == "" {
			provider = c.auth.Provider
		}
	}
	helps.RecordAPIRequest(ctx, cfg, helps.UpstreamRequestLog{
		URL:       req.URL.String(),
		Method:    req.Method,
		Headers:   req.Header.Clone(),
		Body:      bytes.Clone(body),
		Provider:  provider,
		AuthID:    authID,
		AuthLabel: authLabel,
		AuthType:  authType,
		AuthValue: authValue,
	})
}

func (h *Host) currentRuntimeConfig() *config.Config {
	if h == nil {
		return nil
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	return h.runtimeConfig
}

func (c *hostHTTPClient) newHTTPClientForRequest(ctx context.Context, cfg *config.Config, req pluginapi.HTTPRequest, httpReq *http.Request) (*http.Client, func(), error) {
	profile := req.WireProfile
	if profile == nil || (!profile.HTTP1Only && !profile.DisableAutoCompression && len(profile.HeaderProfile) == 0) {
		client := helps.NewProxyAwareHTTPClient(c.proxyContext(ctx), cfg, c.auth, 0)
		if client == nil {
			client = &http.Client{}
		}
		installPluginRedirectPolicy(client)
		return client, nil, nil
	}

	// Priority: request override, then auth proxy, then config proxy.
	proxyStr := strings.TrimSpace(c.requestProxyURL)
	if proxyStr == "" && c.auth != nil {
		proxyStr = strings.TrimSpace(c.auth.ProxyURL)
	}
	if proxyStr == "" && cfg != nil {
		proxyStr = strings.TrimSpace(cfg.ProxyURL)
	}

	var baseTransport *http.Transport
	var explicitDirect bool
	var isConfiguredSOCKS bool
	var builtByProxyutil bool
	if proxyStr != "" {
		setting, errParse := proxyutil.Parse(proxyStr)
		if errParse != nil {
			return nil, nil, fmt.Errorf("pluginhost: parse proxy %s: %w", proxyutil.Redact(proxyStr), errParse)
		}
		switch setting.Mode {
		case proxyutil.ModeDirect:
			explicitDirect = true
			baseTransport = proxyutil.NewDirectTransport()
			proxyStr = ""
		case proxyutil.ModeProxy:
			builtTransport, _, errBuild := proxyutil.BuildHTTPTransport(proxyStr)
			if errBuild != nil {
				return nil, nil, fmt.Errorf("pluginhost: build proxy transport for %s: %w", proxyutil.Redact(proxyStr), errBuild)
			}
			baseTransport = builtTransport
			builtByProxyutil = true
			if setting.URL != nil && (strings.EqualFold(setting.URL.Scheme, "socks5") || strings.EqualFold(setting.URL.Scheme, "socks5h")) {
				isConfiguredSOCKS = true
			}
		default: // ModeInherit
			proxyStr = ""
		}
	}

	var ctxTransport *http.Transport
	if ctx != nil {
		if ctxRoundTripper, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && ctxRoundTripper != nil {
			if t, ok := ctxRoundTripper.(*http.Transport); ok && t != nil {
				ctxTransport = t
			} else if baseTransport == nil {
				return nil, nil, fmt.Errorf("pluginhost: wire profile is not supported with custom context RoundTripper %T", ctxRoundTripper)
			}
		}
	}

	// Priority 3 & 4: Context RoundTripper or Default transport
	if baseTransport == nil {
		if ctxTransport != nil {
			baseTransport = ctxTransport.Clone()
		} else if !explicitDirect {
			if def, ok := http.DefaultTransport.(*http.Transport); ok && def != nil {
				baseTransport = def.Clone()
			} else {
				return nil, nil, fmt.Errorf("pluginhost: wire profile is not supported with custom default RoundTripper %T", http.DefaultTransport)
			}
		} else {
			baseTransport = proxyutil.NewDirectTransport()
		}
	} else if ctxTransport != nil && ctxTransport.TLSClientConfig != nil {
		baseTransport.TLSClientConfig = ctxTransport.TLSClientConfig.Clone()
	}

	if profile.DisableAutoCompression {
		baseTransport.DisableCompression = true
	}

	headerProfile := append([]string(nil), profile.HeaderProfile...)
	hasHeaderProfile := len(headerProfile) > 0
	forceHTTP1 := profile.HTTP1Only || hasHeaderProfile

	if forceHTTP1 {
		if baseTransport.Protocols != nil {
			protocols := &http.Protocols{}
			protocols.SetHTTP1(true)
			protocols.SetHTTP2(false)
			protocols.SetUnencryptedHTTP2(false)
			baseTransport.Protocols = protocols
		}
		baseTransport.ForceAttemptHTTP2 = false
		baseTransport.TLSNextProto = make(map[string]func(string, *tls.Conn) http.RoundTripper)
		if baseTransport.TLSClientConfig == nil {
			baseTransport.TLSClientConfig = &tls.Config{}
		} else {
			baseTransport.TLSClientConfig = baseTransport.TLSClientConfig.Clone()
		}
		baseTransport.TLSClientConfig.NextProtos = []string{"http/1.1"}
	}

	var callerTLSDialer func(context.Context, string, string) (net.Conn, error)
	if ctxTransport != nil {
		if ctxTransport.DialTLSContext != nil {
			callerTLSDialer = ctxTransport.DialTLSContext
		} else if ctxTransport.DialTLS != nil {
			callerTLSDialer = func(dialCtx context.Context, network, addr string) (net.Conn, error) {
				return ctxTransport.DialTLS(network, addr)
			}
		}
	} else if baseTransport != nil && !builtByProxyutil {
		if baseTransport.DialTLSContext != nil {
			callerTLSDialer = baseTransport.DialTLSContext
		} else if baseTransport.DialTLS != nil {
			callerTLSDialer = func(dialCtx context.Context, network, addr string) (net.Conn, error) {
				return baseTransport.DialTLS(network, addr)
			}
		}
	}

	if forceHTTP1 && !hasHeaderProfile {
		if callerTLSDialer != nil {
			baseTransport.DialTLSContext = func(dialCtx context.Context, network, addr string) (net.Conn, error) {
				tlsConn, errTLS := callerTLSDialer(dialCtx, network, addr)
				if errTLS != nil {
					return nil, errTLS
				}
				if tc, ok := tlsConn.(*tls.Conn); ok && tc != nil {
					if errHandshake := tc.HandshakeContext(dialCtx); errHandshake != nil {
						_ = tlsConn.Close()
						return nil, fmt.Errorf("pluginhost: custom TLS handshake: %w", errHandshake)
					}
					cs := tc.ConnectionState()
					if cs.NegotiatedProtocol != "" && cs.NegotiatedProtocol != "http/1.1" {
						_ = tlsConn.Close()
						return nil, fmt.Errorf("pluginhost: custom TLS dialer negotiated unsupported protocol %q; wire profile requires HTTP/1.1", cs.NegotiatedProtocol)
					}
				}
				return tlsConn, nil
			}
		}
	}

	reqHolder := &requestHolder{req: httpReq}

	if hasHeaderProfile {
		headerOrderFunc := func(_, _ string) []string {
			return headerProfile
		}

		origDialContext := baseTransport.DialContext
		if origDialContext == nil && baseTransport.Dial != nil {
			origDialContext = func(dialCtx context.Context, network, addr string) (net.Conn, error) {
				return baseTransport.Dial(network, addr)
			}
		}
		if origDialContext == nil {
			origDialContext = (&net.Dialer{
				Timeout:   30 * time.Second,
				KeepAlive: 30 * time.Second,
			}).DialContext
		}
		origProxyFunc := baseTransport.Proxy

		// For plain HTTP requests, wrapping DialContext intercepts the plaintext HTTP request bytes.
		// Handshake bytes (like SOCKS5 or direct writes) are bypassed cleanly by NewOrderedRequestConn.
		baseTransport.DialContext = func(dialCtx context.Context, network, addr string) (net.Conn, error) {
			conn, errDialContext := origDialContext(dialCtx, network, addr)
			if errDialContext != nil {
				return nil, errDialContext
			}
			return httpwire.NewOrderedRequestConn(conn, headerOrderFunc), nil
		}

		// Configure dynamic proxy function:
		// For plain HTTP, keep origProxyFunc so standard library forwards HTTP requests to the proxy.
		// For HTTPS, return nil so that DialTLSContext is always called to perform TLS wrapping.
		baseTransport.Proxy = func(r *http.Request) (*url.URL, error) {
			if r != nil && r.URL != nil && r.URL.Scheme == "http" {
				if origProxyFunc != nil {
					return origProxyFunc(r)
				}
				return nil, nil
			}
			return nil, nil
		}

		baseTransport.DialTLSContext = func(dialCtx context.Context, network, addr string) (net.Conn, error) {
			currentReq := reqHolder.get()
			targetProxy, errProxy := resolveProxyForRequest(currentReq, c.auth, cfg, origProxyFunc)
			if errProxy != nil {
				return nil, errProxy
			}

			isTargetHTTPS := currentReq != nil && currentReq.URL != nil && currentReq.URL.Scheme == "https"

			var rawConn net.Conn
			var errDial error
			if !isTargetHTTPS {
				// Target is plain HTTP; standard library called DialTLSContext to connect to an HTTPS proxy itself
				if callerTLSDialer != nil {
					tlsConn, errTLS := callerTLSDialer(dialCtx, network, addr)
					if errTLS != nil {
						return nil, errTLS
					}
					if tc, ok := tlsConn.(*tls.Conn); ok && tc != nil {
						if errHandshake := tc.HandshakeContext(dialCtx); errHandshake != nil {
							_ = tlsConn.Close()
							return nil, fmt.Errorf("pluginhost: custom TLS proxy handshake: %w", errHandshake)
						}
						cs := tc.ConnectionState()
						if cs.NegotiatedProtocol != "" && cs.NegotiatedProtocol != "http/1.1" {
							_ = tlsConn.Close()
							return nil, fmt.Errorf("pluginhost: custom TLS proxy dialer negotiated unsupported protocol %q; wire profile requires HTTP/1.1", cs.NegotiatedProtocol)
						}
					}
					return httpwire.NewOrderedRequestConn(tlsConn, headerOrderFunc), nil
				}
				rawConn, errDial = origDialContext(dialCtx, network, addr)
				if errDial != nil {
					return nil, errDial
				}
				pHost, _, errSplitHost := net.SplitHostPort(addr)
				if errSplitHost != nil {
					pHost = addr
				}
				tlsCfg := baseTransport.TLSClientConfig.Clone()
				if tlsCfg.ServerName == "" {
					tlsCfg.ServerName = pHost
				}
				tlsCfg.NextProtos = []string{"http/1.1"}
				tlsConn := tls.Client(rawConn, tlsCfg)
				if errHandshake := tlsConn.HandshakeContext(dialCtx); errHandshake != nil {
					_ = rawConn.Close()
					return nil, fmt.Errorf("HTTPS proxy TLS handshake: %w", errHandshake)
				}
				return httpwire.NewOrderedRequestConn(tlsConn, headerOrderFunc), nil
			} else if isConfiguredSOCKS {
				// Configured SOCKS5 proxy: origDialContext is already the SOCKS5 dialer from BuildHTTPTransport
				rawConn, errDial = origDialContext(dialCtx, network, addr)
			} else if targetProxy != nil {
				// Target is HTTPS; tunnel through the proxy (HTTP/HTTPS/SOCKS) to addr
				rawConn, errDial = dialProxyTunnel(dialCtx, origDialContext, callerTLSDialer, targetProxy, addr, baseTransport)
			} else {
				if callerTLSDialer != nil {
					tlsConn, errOrigTLS := callerTLSDialer(dialCtx, network, addr)
					if errOrigTLS != nil {
						return nil, errOrigTLS
					}
					if tc, ok := tlsConn.(*tls.Conn); ok && tc != nil {
						if errHandshake := tc.HandshakeContext(dialCtx); errHandshake != nil {
							_ = tlsConn.Close()
							return nil, fmt.Errorf("pluginhost: custom TLS handshake: %w", errHandshake)
						}
						cs := tc.ConnectionState()
						if cs.NegotiatedProtocol != "" && cs.NegotiatedProtocol != "http/1.1" {
							_ = tlsConn.Close()
							return nil, fmt.Errorf("pluginhost: custom TLS dialer negotiated unsupported protocol %q; wire profile requires HTTP/1.1", cs.NegotiatedProtocol)
						}
					}
					return httpwire.NewOrderedRequestConn(tlsConn, headerOrderFunc), nil
				}
				rawConn, errDial = origDialContext(dialCtx, network, addr)
			}
			if errDial != nil {
				return nil, errDial
			}

			host, _, errSplit := net.SplitHostPort(addr)
			if errSplit != nil {
				host = addr
			}
			tlsCfg := baseTransport.TLSClientConfig.Clone()
			if tlsCfg.ServerName == "" {
				tlsCfg.ServerName = host
			}
			tlsCfg.NextProtos = []string{"http/1.1"}
			tlsConn := tls.Client(rawConn, tlsCfg)
			if errHandshake := tlsConn.HandshakeContext(dialCtx); errHandshake != nil {
				if errClose := rawConn.Close(); errClose != nil {
					log.Debugf("pluginhost: close connection after TLS handshake failure: %v", errClose)
				}
				return nil, fmt.Errorf("pluginhost TLS handshake: %w", errHandshake)
			}
			return httpwire.NewOrderedRequestConn(tlsConn, headerOrderFunc), nil
		}
	}

	cleanup := func() {
		baseTransport.CloseIdleConnections()
	}

	baseTransport.DisableKeepAlives = true

	client := &http.Client{
		Transport: baseTransport,
		CheckRedirect: func(redirectReq *http.Request, via []*http.Request) error {
			reqHolder.set(redirectReq)
			baseTransport.CloseIdleConnections()
			return checkPluginRedirect(redirectReq, via, nil)
		},
	}

	return client, cleanup, nil
}

func installPluginRedirectPolicy(client *http.Client) {
	if client == nil {
		return
	}
	previous := client.CheckRedirect
	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
		return checkPluginRedirect(req, via, previous)
	}
}

func checkPluginRedirect(req *http.Request, via []*http.Request, previous func(*http.Request, []*http.Request) error) error {
	// net/http rebuilds each redirect request from the initial request's headers,
	// so strip credential headers whenever the target or any intermediate hop
	// leaves the initial request's origin (scheme + host:port).
	if crossOriginRedirect(req, via) {
		for name := range req.Header {
			if isPluginCredentialHeader(name) {
				// Delete the exact map key because Header.Del canonicalizes and
				// would miss non-canonical keys such as "x-api-key".
				delete(req.Header, name)
			}
		}
	}
	if previous != nil {
		return previous(req, via)
	}
	if len(via) >= 10 {
		return errors.New("stopped after 10 redirects")
	}
	return nil
}

func crossOriginRedirect(req *http.Request, via []*http.Request) bool {
	if req == nil || len(via) == 0 || via[0] == nil {
		return false
	}
	origin := via[0].URL
	if !sameRedirectOrigin(origin, req.URL) {
		return true
	}
	for _, hop := range via[1:] {
		if hop == nil || !sameRedirectOrigin(origin, hop.URL) {
			return true
		}
	}
	return false
}

func sameRedirectOrigin(a, b *url.URL) bool {
	if a == nil || b == nil || a.Hostname() == "" || b.Hostname() == "" {
		return false
	}
	return strings.EqualFold(a.Scheme, b.Scheme) &&
		strings.EqualFold(a.Hostname(), b.Hostname()) &&
		redirectOriginPort(a) == redirectOriginPort(b)
}

func redirectOriginPort(u *url.URL) string {
	if u == nil {
		return ""
	}
	if port := u.Port(); port != "" {
		return port
	}
	switch strings.ToLower(u.Scheme) {
	case "http":
		return "80"
	case "https":
		return "443"
	default:
		return ""
	}
}

func isPluginCredentialHeader(name string) bool {
	lower := strings.ToLower(strings.TrimSpace(name))
	switch lower {
	case "authorization", "proxy-authorization", "cookie", "cookie2", "www-authenticate":
		return true
	}
	return strings.Contains(lower, "api-key") ||
		strings.Contains(lower, "apikey") ||
		strings.Contains(lower, "api_key") ||
		strings.Contains(lower, "token") ||
		strings.Contains(lower, "secret") ||
		strings.Contains(lower, "credential")
}

type requestHolder struct {
	mu  sync.Mutex
	req *http.Request
}

func (h *requestHolder) get() *http.Request {
	if h == nil {
		return nil
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	return h.req
}

func (h *requestHolder) set(req *http.Request) {
	if h == nil {
		return
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	h.req = req
}

func resolveProxyForRequest(r *http.Request, auth *coreauth.Auth, cfg *config.Config, proxyFunc func(*http.Request) (*url.URL, error)) (*url.URL, error) {
	if auth != nil && strings.TrimSpace(auth.ProxyURL) != "" {
		setting, errParse := proxyutil.Parse(strings.TrimSpace(auth.ProxyURL))
		if errParse != nil {
			return nil, fmt.Errorf("pluginhost: parse auth proxy: %w", errParse)
		}
		if setting.Mode == proxyutil.ModeDirect {
			return nil, nil
		}
		if setting.Mode == proxyutil.ModeProxy {
			return setting.URL, nil
		}
	}
	if cfg != nil && strings.TrimSpace(cfg.ProxyURL) != "" {
		setting, errParse := proxyutil.Parse(strings.TrimSpace(cfg.ProxyURL))
		if errParse != nil {
			return nil, fmt.Errorf("pluginhost: parse config proxy: %w", errParse)
		}
		if setting.Mode == proxyutil.ModeDirect {
			return nil, nil
		}
		if setting.Mode == proxyutil.ModeProxy {
			return setting.URL, nil
		}
	}
	if proxyFunc != nil && r != nil {
		return proxyFunc(r)
	}
	return nil, nil
}

func dialProxyTunnel(
	ctx context.Context,
	baseDial func(context.Context, string, string) (net.Conn, error),
	baseDialTLS func(context.Context, string, string) (net.Conn, error),
	proxyURL *url.URL,
	targetAddr string,
	transport *http.Transport,
) (net.Conn, error) {
	if proxyURL == nil {
		return baseDial(ctx, "tcp", targetAddr)
	}

	pHost := proxyURL.Hostname()
	pPort := proxyURL.Port()
	if pPort == "" {
		switch proxyURL.Scheme {
		case "https":
			pPort = "443"
		case "socks5", "socks5h":
			pPort = "1080"
		default:
			pPort = "80"
		}
	}
	proxyAddr := net.JoinHostPort(pHost, pPort)

	dialerToUse := baseDial
	if dialerToUse == nil {
		dialerToUse = (&net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,
		}).DialContext
	}

	if proxyURL.Scheme == "socks5" || proxyURL.Scheme == "socks5h" {
		var auth *proxy.Auth
		if proxyURL.User != nil {
			pwd, _ := proxyURL.User.Password()
			auth = &proxy.Auth{
				User:     proxyURL.User.Username(),
				Password: pwd,
			}
		}
		socksDialer, errSOCKS := proxy.SOCKS5("tcp", proxyAddr, auth, &contextDialerWrapper{dial: dialerToUse})
		if errSOCKS != nil {
			return nil, fmt.Errorf("create SOCKS5 dialer: %w", errSOCKS)
		}
		if cd, ok := socksDialer.(proxy.ContextDialer); ok {
			return cd.DialContext(ctx, "tcp", targetAddr)
		}
		return socksDialer.Dial("tcp", targetAddr)
	}

	var conn net.Conn
	if proxyURL.Scheme == "https" && baseDialTLS != nil {
		proxyTLS, errTLS := baseDialTLS(ctx, "tcp", proxyAddr)
		if errTLS != nil {
			return nil, fmt.Errorf("HTTPS proxy custom TLS dial failed: %w", errTLS)
		}
		if tc, ok := proxyTLS.(*tls.Conn); ok && tc != nil {
			if errHandshake := tc.HandshakeContext(ctx); errHandshake != nil {
				_ = proxyTLS.Close()
				return nil, fmt.Errorf("HTTPS proxy custom TLS handshake: %w", errHandshake)
			}
			cs := tc.ConnectionState()
			if cs.NegotiatedProtocol != "" && cs.NegotiatedProtocol != "http/1.1" {
				_ = proxyTLS.Close()
				return nil, fmt.Errorf("HTTPS proxy custom TLS dialer negotiated unsupported protocol %q; wire profile requires HTTP/1.1", cs.NegotiatedProtocol)
			}
		}
		conn = proxyTLS
	} else {
		rawConn, errDial := dialerToUse(ctx, "tcp", proxyAddr)
		if errDial != nil {
			return nil, fmt.Errorf("dial proxy %s: %w", proxyutil.Redact(proxyURL.String()), errDial)
		}
		conn = rawConn

		if proxyURL.Scheme == "https" {
			var tlsCfg *tls.Config
			if transport != nil && transport.TLSClientConfig != nil {
				tlsCfg = transport.TLSClientConfig.Clone()
			} else {
				tlsCfg = &tls.Config{}
			}
			if tlsCfg.ServerName == "" {
				tlsCfg.ServerName = proxyURL.Hostname()
			}
			tlsCfg.NextProtos = []string{"http/1.1"}
			proxyTLS := tls.Client(rawConn, tlsCfg)
			if errHandshake := proxyTLS.HandshakeContext(ctx); errHandshake != nil {
				_ = rawConn.Close()
				return nil, fmt.Errorf("HTTPS proxy TLS handshake: %w", errHandshake)
			}
			conn = proxyTLS
		}
	}

	cancelDone := make(chan struct{})
	stopCancel := context.AfterFunc(ctx, func() {
		_ = conn.Close()
		close(cancelDone)
	})
	defer func() {
		if !stopCancel() {
			<-cancelDone
		}
	}()

	connectReq := (&http.Request{
		Method: http.MethodConnect,
		URL:    &url.URL{Opaque: targetAddr},
		Host:   targetAddr,
		Header: make(http.Header),
	}).WithContext(ctx)

	if transport != nil {
		if transport.GetProxyConnectHeader != nil {
			hdr, errHdr := transport.GetProxyConnectHeader(ctx, proxyURL, targetAddr)
			if errHdr != nil {
				_ = conn.Close()
				return nil, fmt.Errorf("get proxy connect header: %w", errHdr)
			}
			for k, vv := range hdr {
				for _, v := range vv {
					connectReq.Header.Add(k, v)
				}
			}
		} else if transport.ProxyConnectHeader != nil {
			for k, vv := range transport.ProxyConnectHeader {
				for _, v := range vv {
					connectReq.Header.Add(k, v)
				}
			}
		}
	}

	if proxyURL.User != nil {
		pwd, _ := proxyURL.User.Password()
		token := base64.StdEncoding.EncodeToString([]byte(proxyURL.User.Username() + ":" + pwd))
		connectReq.Header.Set("Proxy-Authorization", "Basic "+token)
	}

	if errWrite := connectReq.Write(conn); errWrite != nil {
		_ = conn.Close()
		return nil, fmt.Errorf("write CONNECT request: %w", errWrite)
	}

	br := bufio.NewReader(conn)
	resp, errResp := http.ReadResponse(br, connectReq)
	if errResp != nil {
		_ = conn.Close()
		return nil, fmt.Errorf("read CONNECT response: %w", errResp)
	}
	if transport != nil && transport.OnProxyConnectResponse != nil {
		if errOn := transport.OnProxyConnectResponse(ctx, proxyURL, connectReq, resp); errOn != nil {
			_ = conn.Close()
			return nil, fmt.Errorf("on proxy connect response: %w", errOn)
		}
	}
	if resp.StatusCode != http.StatusOK {
		_ = conn.Close()
		return nil, fmt.Errorf("proxy CONNECT failed with status %d %s", resp.StatusCode, resp.Status)
	}

	return conn, nil
}

type contextDialerWrapper struct {
	dial func(context.Context, string, string) (net.Conn, error)
}

func (w *contextDialerWrapper) Dial(network, addr string) (net.Conn, error) {
	return w.dial(context.Background(), network, addr)
}

func (w *contextDialerWrapper) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
	return w.dial(ctx, network, addr)
}