feat(auth): add proxy URL override support to auth constructors and executors
- Introduced `WithProxyURL` variants for `CodexAuth`, `ClaudeAuth`, `IFlowAuth`, and `DeviceFlowClient`. - Updated executors to use proxy-aware constructors for improved configurability. - Added unit tests to validate proxy override precedence and functionality. Closes: #2823
This commit is contained in:
@@ -59,10 +59,30 @@ type ClaudeAuth struct {
|
|||||||
// Returns:
|
// Returns:
|
||||||
// - *ClaudeAuth: A new Claude authentication service instance
|
// - *ClaudeAuth: A new Claude authentication service instance
|
||||||
func NewClaudeAuth(cfg *config.Config) *ClaudeAuth {
|
func NewClaudeAuth(cfg *config.Config) *ClaudeAuth {
|
||||||
|
return NewClaudeAuthWithProxyURL(cfg, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClaudeAuthWithProxyURL creates a new Anthropic authentication service with a proxy override.
|
||||||
|
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
|
||||||
|
func NewClaudeAuthWithProxyURL(cfg *config.Config, proxyURL string) *ClaudeAuth {
|
||||||
|
effectiveProxyURL := strings.TrimSpace(proxyURL)
|
||||||
|
var sdkCfg *config.SDKConfig
|
||||||
|
if cfg != nil {
|
||||||
|
sdkCfgCopy := cfg.SDKConfig
|
||||||
|
if effectiveProxyURL == "" {
|
||||||
|
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||||
|
}
|
||||||
|
sdkCfgCopy.ProxyURL = effectiveProxyURL
|
||||||
|
sdkCfg = &sdkCfgCopy
|
||||||
|
} else if effectiveProxyURL != "" {
|
||||||
|
sdkCfgCopy := config.SDKConfig{ProxyURL: effectiveProxyURL}
|
||||||
|
sdkCfg = &sdkCfgCopy
|
||||||
|
}
|
||||||
|
|
||||||
// Use custom HTTP client with Firefox TLS fingerprint to bypass
|
// Use custom HTTP client with Firefox TLS fingerprint to bypass
|
||||||
// Cloudflare's bot detection on Anthropic domains
|
// Cloudflare's bot detection on Anthropic domains
|
||||||
return &ClaudeAuth{
|
return &ClaudeAuth{
|
||||||
httpClient: NewAnthropicHttpClient(&cfg.SDKConfig),
|
httpClient: NewAnthropicHttpClient(sdkCfg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package claude
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewClaudeAuthWithProxyURL_OverrideDirectTakesPrecedence(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "socks5://proxy.example.com:1080"}}
|
||||||
|
auth := NewClaudeAuthWithProxyURL(cfg, "direct")
|
||||||
|
|
||||||
|
transport, ok := auth.httpClient.Transport.(*utlsRoundTripper)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected utlsRoundTripper, got %T", auth.httpClient.Transport)
|
||||||
|
}
|
||||||
|
if transport.dialer != proxy.Direct {
|
||||||
|
t.Fatalf("expected proxy.Direct, got %T", transport.dialer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewClaudeAuthWithProxyURL_OverrideProxyAppliedWithoutConfig(t *testing.T) {
|
||||||
|
auth := NewClaudeAuthWithProxyURL(nil, "socks5://proxy.example.com:1080")
|
||||||
|
|
||||||
|
transport, ok := auth.httpClient.Transport.(*utlsRoundTripper)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected utlsRoundTripper, got %T", auth.httpClient.Transport)
|
||||||
|
}
|
||||||
|
if transport.dialer == proxy.Direct {
|
||||||
|
t.Fatalf("expected proxy dialer, got %T", transport.dialer)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,8 +37,23 @@ type CodexAuth struct {
|
|||||||
// NewCodexAuth creates a new CodexAuth service instance.
|
// NewCodexAuth creates a new CodexAuth service instance.
|
||||||
// It initializes an HTTP client with proxy settings from the provided configuration.
|
// It initializes an HTTP client with proxy settings from the provided configuration.
|
||||||
func NewCodexAuth(cfg *config.Config) *CodexAuth {
|
func NewCodexAuth(cfg *config.Config) *CodexAuth {
|
||||||
|
return NewCodexAuthWithProxyURL(cfg, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCodexAuthWithProxyURL creates a new CodexAuth service instance.
|
||||||
|
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
|
||||||
|
func NewCodexAuthWithProxyURL(cfg *config.Config, proxyURL string) *CodexAuth {
|
||||||
|
effectiveProxyURL := strings.TrimSpace(proxyURL)
|
||||||
|
var sdkCfg config.SDKConfig
|
||||||
|
if cfg != nil {
|
||||||
|
sdkCfg = cfg.SDKConfig
|
||||||
|
if effectiveProxyURL == "" {
|
||||||
|
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sdkCfg.ProxyURL = effectiveProxyURL
|
||||||
return &CodexAuth{
|
return &CodexAuth{
|
||||||
httpClient: util.SetProxy(&cfg.SDKConfig, &http.Client{}),
|
httpClient: util.SetProxy(&sdkCfg, &http.Client{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type roundTripFunc func(*http.Request) (*http.Response, error)
|
type roundTripFunc func(*http.Request) (*http.Response, error)
|
||||||
@@ -42,3 +44,37 @@ func TestRefreshTokensWithRetry_NonRetryableOnlyAttemptsOnce(t *testing.T) {
|
|||||||
t.Fatalf("expected 1 refresh attempt, got %d", got)
|
t.Fatalf("expected 1 refresh attempt, got %d", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNewCodexAuthWithProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://proxy.example.com:8080"}}
|
||||||
|
auth := NewCodexAuthWithProxyURL(cfg, "direct")
|
||||||
|
|
||||||
|
transport, ok := auth.httpClient.Transport.(*http.Transport)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected http.Transport, got %T", auth.httpClient.Transport)
|
||||||
|
}
|
||||||
|
if transport.Proxy != nil {
|
||||||
|
t.Fatal("expected direct transport to disable proxy function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewCodexAuthWithProxyURL_OverrideProxyTakesPrecedence(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://global.example.com:8080"}}
|
||||||
|
auth := NewCodexAuthWithProxyURL(cfg, "http://override.example.com:8081")
|
||||||
|
|
||||||
|
transport, ok := auth.httpClient.Transport.(*http.Transport)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected http.Transport, got %T", auth.httpClient.Transport)
|
||||||
|
}
|
||||||
|
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||||
|
if errReq != nil {
|
||||||
|
t.Fatalf("new request: %v", errReq)
|
||||||
|
}
|
||||||
|
proxyURL, errProxy := transport.Proxy(req)
|
||||||
|
if errProxy != nil {
|
||||||
|
t.Fatalf("proxy func: %v", errProxy)
|
||||||
|
}
|
||||||
|
if proxyURL == nil || proxyURL.String() != "http://override.example.com:8081" {
|
||||||
|
t.Fatalf("proxy URL = %v, want http://override.example.com:8081", proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,8 +48,23 @@ type IFlowAuth struct {
|
|||||||
|
|
||||||
// NewIFlowAuth constructs a new IFlowAuth with proxy-aware transport.
|
// NewIFlowAuth constructs a new IFlowAuth with proxy-aware transport.
|
||||||
func NewIFlowAuth(cfg *config.Config) *IFlowAuth {
|
func NewIFlowAuth(cfg *config.Config) *IFlowAuth {
|
||||||
|
return NewIFlowAuthWithProxyURL(cfg, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewIFlowAuthWithProxyURL constructs a new IFlowAuth with a proxy override.
|
||||||
|
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
|
||||||
|
func NewIFlowAuthWithProxyURL(cfg *config.Config, proxyURL string) *IFlowAuth {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
return &IFlowAuth{httpClient: util.SetProxy(&cfg.SDKConfig, client)}
|
effectiveProxyURL := strings.TrimSpace(proxyURL)
|
||||||
|
var sdkCfg config.SDKConfig
|
||||||
|
if cfg != nil {
|
||||||
|
sdkCfg = cfg.SDKConfig
|
||||||
|
if effectiveProxyURL == "" {
|
||||||
|
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sdkCfg.ProxyURL = effectiveProxyURL
|
||||||
|
return &IFlowAuth{httpClient: util.SetProxy(&sdkCfg, client)}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizationURL builds the authorization URL and matching redirect URI.
|
// AuthorizationURL builds the authorization URL and matching redirect URI.
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package iflow
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewIFlowAuthWithProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://proxy.example.com:8080"}}
|
||||||
|
auth := NewIFlowAuthWithProxyURL(cfg, "direct")
|
||||||
|
|
||||||
|
transport, ok := auth.httpClient.Transport.(*http.Transport)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected http.Transport, got %T", auth.httpClient.Transport)
|
||||||
|
}
|
||||||
|
if transport.Proxy != nil {
|
||||||
|
t.Fatal("expected direct transport to disable proxy function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewIFlowAuthWithProxyURL_OverrideProxyTakesPrecedence(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://global.example.com:8080"}}
|
||||||
|
auth := NewIFlowAuthWithProxyURL(cfg, "http://override.example.com:8081")
|
||||||
|
|
||||||
|
transport, ok := auth.httpClient.Transport.(*http.Transport)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected http.Transport, got %T", auth.httpClient.Transport)
|
||||||
|
}
|
||||||
|
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||||
|
if errReq != nil {
|
||||||
|
t.Fatalf("new request: %v", errReq)
|
||||||
|
}
|
||||||
|
proxyURL, errProxy := transport.Proxy(req)
|
||||||
|
if errProxy != nil {
|
||||||
|
t.Fatalf("proxy func: %v", errProxy)
|
||||||
|
}
|
||||||
|
if proxyURL == nil || proxyURL.String() != "http://override.example.com:8081" {
|
||||||
|
t.Fatalf("proxy URL = %v, want http://override.example.com:8081", proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,10 +102,24 @@ func NewDeviceFlowClient(cfg *config.Config) *DeviceFlowClient {
|
|||||||
|
|
||||||
// NewDeviceFlowClientWithDeviceID creates a new device flow client with the specified device ID.
|
// NewDeviceFlowClientWithDeviceID creates a new device flow client with the specified device ID.
|
||||||
func NewDeviceFlowClientWithDeviceID(cfg *config.Config, deviceID string) *DeviceFlowClient {
|
func NewDeviceFlowClientWithDeviceID(cfg *config.Config, deviceID string) *DeviceFlowClient {
|
||||||
|
return NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg, deviceID, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDeviceFlowClientWithDeviceIDAndProxyURL creates a new device flow client with a proxy override.
|
||||||
|
// proxyURL takes precedence over cfg.ProxyURL when non-empty.
|
||||||
|
func NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg *config.Config, deviceID string, proxyURL string) *DeviceFlowClient {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
effectiveProxyURL := strings.TrimSpace(proxyURL)
|
||||||
|
var sdkCfg config.SDKConfig
|
||||||
if cfg != nil {
|
if cfg != nil {
|
||||||
client = util.SetProxy(&cfg.SDKConfig, client)
|
sdkCfg = cfg.SDKConfig
|
||||||
|
if effectiveProxyURL == "" {
|
||||||
|
effectiveProxyURL = strings.TrimSpace(cfg.ProxyURL)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
sdkCfg.ProxyURL = effectiveProxyURL
|
||||||
|
client = util.SetProxy(&sdkCfg, client)
|
||||||
|
|
||||||
resolvedDeviceID := strings.TrimSpace(deviceID)
|
resolvedDeviceID := strings.TrimSpace(deviceID)
|
||||||
if resolvedDeviceID == "" {
|
if resolvedDeviceID == "" {
|
||||||
resolvedDeviceID = getOrCreateDeviceID()
|
resolvedDeviceID = getOrCreateDeviceID()
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package kimi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideDirectDisablesProxy(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://proxy.example.com:8080"}}
|
||||||
|
client := NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg, "device-1", "direct")
|
||||||
|
|
||||||
|
transport, ok := client.httpClient.Transport.(*http.Transport)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected http.Transport, got %T", client.httpClient.Transport)
|
||||||
|
}
|
||||||
|
if transport.Proxy != nil {
|
||||||
|
t.Fatal("expected direct transport to disable proxy function")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewDeviceFlowClientWithDeviceIDAndProxyURL_OverrideProxyTakesPrecedence(t *testing.T) {
|
||||||
|
cfg := &config.Config{SDKConfig: config.SDKConfig{ProxyURL: "http://global.example.com:8080"}}
|
||||||
|
client := NewDeviceFlowClientWithDeviceIDAndProxyURL(cfg, "device-1", "http://override.example.com:8081")
|
||||||
|
|
||||||
|
transport, ok := client.httpClient.Transport.(*http.Transport)
|
||||||
|
if !ok || transport == nil {
|
||||||
|
t.Fatalf("expected http.Transport, got %T", client.httpClient.Transport)
|
||||||
|
}
|
||||||
|
req, errReq := http.NewRequest(http.MethodGet, "https://example.com", nil)
|
||||||
|
if errReq != nil {
|
||||||
|
t.Fatalf("new request: %v", errReq)
|
||||||
|
}
|
||||||
|
proxyURL, errProxy := transport.Proxy(req)
|
||||||
|
if errProxy != nil {
|
||||||
|
t.Fatalf("proxy func: %v", errProxy)
|
||||||
|
}
|
||||||
|
if proxyURL == nil || proxyURL.String() != "http://override.example.com:8081" {
|
||||||
|
t.Fatalf("proxy URL = %v, want http://override.example.com:8081", proxyURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -659,7 +659,7 @@ func (e *ClaudeExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (
|
|||||||
if refreshToken == "" {
|
if refreshToken == "" {
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
svc := claudeauth.NewClaudeAuth(e.cfg)
|
svc := claudeauth.NewClaudeAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
||||||
td, err := svc.RefreshTokens(ctx, refreshToken)
|
td, err := svc.RefreshTokens(ctx, refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -612,7 +612,7 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
|
|||||||
if refreshToken == "" {
|
if refreshToken == "" {
|
||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
svc := codexauth.NewCodexAuth(e.cfg)
|
svc := codexauth.NewCodexAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
||||||
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3)
|
td, err := svc.RefreshTokensWithRetry(ctx, refreshToken, 3)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ func (e *IFlowExecutor) refreshCookieBased(ctx context.Context, auth *cliproxyau
|
|||||||
|
|
||||||
log.Infof("iflow executor: refreshing cookie-based API key for user: %s", email)
|
log.Infof("iflow executor: refreshing cookie-based API key for user: %s", email)
|
||||||
|
|
||||||
svc := iflowauth.NewIFlowAuth(e.cfg)
|
svc := iflowauth.NewIFlowAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
||||||
keyData, err := svc.RefreshAPIKey(ctx, cookie, email)
|
keyData, err := svc.RefreshAPIKey(ctx, cookie, email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("iflow executor: cookie-based API key refresh failed: %v", err)
|
log.Errorf("iflow executor: cookie-based API key refresh failed: %v", err)
|
||||||
@@ -429,7 +429,7 @@ func (e *IFlowExecutor) refreshOAuthBased(ctx context.Context, auth *cliproxyaut
|
|||||||
log.Debugf("iflow executor: refreshing access token, old: %s", util.HideAPIKey(oldAccessToken))
|
log.Debugf("iflow executor: refreshing access token, old: %s", util.HideAPIKey(oldAccessToken))
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := iflowauth.NewIFlowAuth(e.cfg)
|
svc := iflowauth.NewIFlowAuthWithProxyURL(e.cfg, auth.ProxyURL)
|
||||||
tokenData, err := svc.RefreshTokens(ctx, refreshToken)
|
tokenData, err := svc.RefreshTokens(ctx, refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("iflow executor: token refresh failed: %v", err)
|
log.Errorf("iflow executor: token refresh failed: %v", err)
|
||||||
|
|||||||
@@ -472,7 +472,7 @@ func (e *KimiExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*c
|
|||||||
return auth, nil
|
return auth, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
client := kimiauth.NewDeviceFlowClientWithDeviceID(e.cfg, resolveKimiDeviceID(auth))
|
client := kimiauth.NewDeviceFlowClientWithDeviceIDAndProxyURL(e.cfg, resolveKimiDeviceID(auth), auth.ProxyURL)
|
||||||
td, err := client.RefreshToken(ctx, refreshToken)
|
td, err := client.RefreshToken(ctx, refreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Reference in New Issue
Block a user