package pluginhost import ( "bufio" "bytes" "context" "crypto/tls" "encoding/base64" "errors" "fmt" "io" "net" "net/http" "net/url" "strings" "sync" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/httpwire" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "golang.org/x/net/proxy") type hostHTTPClient struct { host *Host auth *coreauth.Auth provider string} func (h *Host) newHTTPClient(auth *coreauth.Auth, providers ...string) pluginapi.HostHTTPClient { provider := "" if len(providers) > 0 { provider = providers[0] } return &hostHTTPClient{host: h, auth: auth, provider: provider}} 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(ctx, cfg, c.auth, 0) if client == nil { client = &http.Client{} } return client, nil, nil } // Priority 1: Auth proxy var proxyStr string if c.auth != nil { proxyStr = strings.TrimSpace(c.auth.ProxyURL) } // Priority 2: Config proxy 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()Lines 1–500 of 743. Load more
package pluginhost import ( "bufio" "bytes" "context" "crypto/tls" "encoding/base64" "errors" "fmt" "io" "net" "net/http" "net/url" "strings" "sync" "time" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/httpwire" "github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps" coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor" "github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "golang.org/x/net/proxy" ) type hostHTTPClient struct { host *Host auth *coreauth.Auth provider string } func (h *Host) newHTTPClient(auth *coreauth.Auth, providers ...string) pluginapi.HostHTTPClient { provider := "" if len(providers) > 0 { provider = providers[0] } return &hostHTTPClient{host: h, auth: auth, provider: provider} } 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(ctx, cfg, c.auth, 0) if client == nil { client = &http.Client{} } return client, nil, nil } // Priority 1: Auth proxy var proxyStr string if c.auth != nil { proxyStr = strings.TrimSpace(c.auth.ProxyURL) } // Priority 2: Config proxy 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()