diff --git a/internal/config/config_types.go b/internal/config/config_types.go index 783f666..df24892 100644 --- a/internal/config/config_types.go +++ b/internal/config/config_types.go @@ -655,6 +655,9 @@ type OpenAICompatibility struct { // BaseURL is the base URL for the external OpenAI-compatible API endpoint. BaseURL string `yaml:"base-url" json:"base-url"` + // Insecure disables TLS certificate verification for this provider. + Insecure bool `yaml:"insecure,omitempty" json:"insecure,omitempty"` + // APIKeyEntries defines API keys with optional per-key proxy configuration. APIKeyEntries []OpenAICompatibilityAPIKey `yaml:"api-key-entries,omitempty" json:"api-key-entries,omitempty"` diff --git a/internal/runtime/executor/helps/proxy_helpers.go b/internal/runtime/executor/helps/proxy_helpers.go index 572f87c..dcf3700 100644 --- a/internal/runtime/executor/helps/proxy_helpers.go +++ b/internal/runtime/executor/helps/proxy_helpers.go @@ -2,6 +2,7 @@ package helps import ( "context" + "crypto/tls" "net/http" "strings" "time" @@ -33,8 +34,10 @@ func NewProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip // Priority 1: Use auth.ProxyURL if configured var proxyURL string + insecure := false if auth != nil { proxyURL = strings.TrimSpace(auth.ProxyURL) + insecure = strings.EqualFold(strings.TrimSpace(auth.Attributes["insecure"]), "true") } // Priority 2: Use cfg.ProxyURL if auth proxy is not configured @@ -46,7 +49,7 @@ func NewProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip if proxyURL != "" { transport := buildProxyTransport(proxyURL) if transport != nil { - httpClient.Transport = transport + httpClient.Transport = configureInsecureTLS(transport, insecure) return httpClient } // If proxy setup failed, log and fall through to context RoundTripper @@ -55,12 +58,32 @@ func NewProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip // Priority 3: Use RoundTripper from context (typically from RoundTripperFor) if rt, ok := ctx.Value("cliproxy.roundtripper").(http.RoundTripper); ok && rt != nil { - httpClient.Transport = rt + httpClient.Transport = configureInsecureTLS(rt, insecure) + } else if insecure { + httpClient.Transport = configureInsecureTLS(http.DefaultTransport, true) } return httpClient } +func configureInsecureTLS(rt http.RoundTripper, insecure bool) http.RoundTripper { + if !insecure { + return rt + } + transport, ok := rt.(*http.Transport) + if !ok || transport == nil { + return rt + } + transport = transport.Clone() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } else { + transport.TLSClientConfig = transport.TLSClientConfig.Clone() + } + transport.TLSClientConfig.InsecureSkipVerify = true + return transport +} + // buildProxyTransport creates an HTTP transport configured for the given proxy URL. // It supports SOCKS5, HTTP, and HTTPS proxy protocols. // diff --git a/internal/runtime/executor/helps/proxy_helpers_test.go b/internal/runtime/executor/helps/proxy_helpers_test.go index fb57b6b..9586cee 100644 --- a/internal/runtime/executor/helps/proxy_helpers_test.go +++ b/internal/runtime/executor/helps/proxy_helpers_test.go @@ -2,6 +2,7 @@ package helps import ( "context" + "crypto/tls" "net/http" "testing" @@ -28,3 +29,49 @@ func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) { t.Fatal("expected direct transport to disable proxy function") } } + +func TestNewProxyAwareHTTPClientInsecureTLS(t *testing.T) { + t.Parallel() + + insecureClient := NewProxyAwareHTTPClient(context.Background(), nil, &cliproxyauth.Auth{ + Attributes: map[string]string{"insecure": "true"}, + }, 0) + transport, ok := insecureClient.Transport.(*http.Transport) + if !ok { + t.Fatalf("transport type = %T, want *http.Transport", insecureClient.Transport) + } + if transport.TLSClientConfig == nil || !transport.TLSClientConfig.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify = false, want true") + } +} + +func TestNewProxyAwareHTTPClientClonesTransportForInsecureTLS(t *testing.T) { + t.Parallel() + + original := http.DefaultTransport.(*http.Transport).Clone() + original.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12} + ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", http.RoundTripper(original)) + + client := NewProxyAwareHTTPClient(ctx, nil, &cliproxyauth.Auth{ + Attributes: map[string]string{"insecure": "true"}, + }, 0) + configured, ok := client.Transport.(*http.Transport) + if !ok { + t.Fatalf("transport type = %T, want *http.Transport", client.Transport) + } + if configured == original { + t.Fatal("insecure TLS mutated the original transport") + } + if original.TLSClientConfig.InsecureSkipVerify { + t.Fatal("insecure TLS mutated the original TLS config") + } + if configured.TLSClientConfig == original.TLSClientConfig { + t.Fatal("insecure TLS reused the original TLS config") + } + if !configured.TLSClientConfig.InsecureSkipVerify { + t.Fatal("InsecureSkipVerify = false, want true") + } + if configured.TLSClientConfig.MinVersion != tls.VersionTLS12 { + t.Fatalf("MinVersion = %d, want %d", configured.TLSClientConfig.MinVersion, tls.VersionTLS12) + } +} diff --git a/internal/watcher/synthesizer/config.go b/internal/watcher/synthesizer/config.go index c5f5722..447fdfd 100644 --- a/internal/watcher/synthesizer/config.go +++ b/internal/watcher/synthesizer/config.go @@ -305,6 +305,9 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor "provider_key": internalProviderKey, "config_index": strconv.Itoa(i), } + if compat.Insecure { + attrs["insecure"] = "true" + } metadata := map[string]any{} if disableCooling != nil { metadata["disable_cooling"] = *disableCooling @@ -351,6 +354,9 @@ func (s *ConfigSynthesizer) synthesizeOpenAICompat(ctx *SynthesisContext) []*cor "provider_key": internalProviderKey, "config_index": strconv.Itoa(i), } + if compat.Insecure { + attrs["insecure"] = "true" + } metadata := map[string]any{} if disableCooling != nil { metadata["disable_cooling"] = *disableCooling diff --git a/internal/watcher/synthesizer/config_test.go b/internal/watcher/synthesizer/config_test.go index e4a9abc..f79babe 100644 --- a/internal/watcher/synthesizer/config_test.go +++ b/internal/watcher/synthesizer/config_test.go @@ -524,6 +524,42 @@ func TestConfigSynthesizer_OpenAICompat(t *testing.T) { } } +func TestConfigSynthesizer_OpenAICompat_Insecure(t *testing.T) { + synth := NewConfigSynthesizer() + ctx := &SynthesisContext{ + Config: &config.Config{ + OpenAICompatibility: []config.OpenAICompatibility{ + { + Name: "with-key", + BaseURL: "https://with-key.example.com", + Insecure: true, + APIKeyEntries: []config.OpenAICompatibilityAPIKey{{APIKey: "key"}}, + }, + { + Name: "without-key", + BaseURL: "https://without-key.example.com", + Insecure: true, + }, + }, + }, + Now: time.Now(), + IDGenerator: NewStableIDGenerator(), + } + + auths, errSynthesize := synth.Synthesize(ctx) + if errSynthesize != nil { + t.Fatalf("Synthesize() error = %v", errSynthesize) + } + if len(auths) != 2 { + t.Fatalf("auth count = %d, want 2", len(auths)) + } + for i := range auths { + if got := auths[i].Attributes["insecure"]; got != "true" { + t.Fatalf("auth[%d] insecure = %q, want true", i, got) + } + } +} + func TestConfigSynthesizer_OpenAICompat_UsesNamespacedProviderKey(t *testing.T) { synth := NewConfigSynthesizer() ctx := &SynthesisContext{