Skip to content

Commit 1112ef1

Browse files
committed
feat(auth): add per-auth use_global_proxy configuration
- Add use_global_proxy field to Auth struct and auth files - Prompt users for proxy preference during login flows - Use provider-specific defaults (iFlow/Qwen: false, others: true) - Maintain backward compatibility by defaulting to true for existing files - Implement GlobalProxySetter interface for token storage - Update proxy selection logic to respect use_global_proxy flag This allows users to control proxy usage on a per-auth basis while maintaining backward compatibility with existing auth files. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: HustCoderHu <[email protected]>
1 parent b6ad243 commit 1112ef1

File tree

11 files changed

+122
-33
lines changed

11 files changed

+122
-33
lines changed

internal/api/handlers/management/auth_files.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1565,8 +1565,8 @@ func (h *Handler) RequestQwenToken(c *gin.Context) {
15651565
return
15661566
}
15671567

1568-
// Create token storage
1569-
tokenStorage := qwenAuth.CreateTokenStorage(tokenData)
1568+
// Create token storage - default to false for use_global_proxy for qwen
1569+
tokenStorage := qwenAuth.CreateTokenStorage(tokenData, false)
15701570

15711571
tokenStorage.Email = fmt.Sprintf("qwen-%d", time.Now().UnixMilli())
15721572
record := &coreauth.Auth{
@@ -1742,7 +1742,8 @@ func (h *Handler) RequestIFlowCookieToken(c *gin.Context) {
17421742

17431743
tokenData.Cookie = cookieValue
17441744

1745-
tokenStorage := authSvc.CreateCookieTokenStorage(tokenData)
1745+
// For management API, default to false for use_global_proxy for iFlow
1746+
tokenStorage := authSvc.CreateCookieTokenStorage(tokenData, false)
17461747
email := strings.TrimSpace(tokenStorage.Email)
17471748
if email == "" {
17481749
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "failed to extract email from token"})

internal/auth/iflow/iflow_auth.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ func ShouldRefreshAPIKey(expireTime string) (bool, time.Duration, error) {
489489
}
490490

491491
// CreateCookieTokenStorage converts cookie-based token data into persistence storage
492-
func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenStorage {
492+
func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData, useGlobalProxy bool) *IFlowTokenStorage {
493493
if data == nil {
494494
return nil
495495
}
@@ -502,12 +502,13 @@ func (ia *IFlowAuth) CreateCookieTokenStorage(data *IFlowTokenData) *IFlowTokenS
502502
}
503503

504504
return &IFlowTokenStorage{
505-
APIKey: data.APIKey,
506-
Email: data.Email,
507-
Expire: data.Expire,
508-
Cookie: cookieToSave,
509-
LastRefresh: time.Now().Format(time.RFC3339),
510-
Type: "iflow",
505+
APIKey: data.APIKey,
506+
Email: data.Email,
507+
Expire: data.Expire,
508+
Cookie: cookieToSave,
509+
LastRefresh: time.Now().Format(time.RFC3339),
510+
Type: "iflow",
511+
UseGlobalProxy: useGlobalProxy,
511512
}
512513
}
513514

internal/auth/iflow/iflow_token.go

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@ import (
1111

1212
// IFlowTokenStorage persists iFlow OAuth credentials alongside the derived API key.
1313
type IFlowTokenStorage struct {
14-
AccessToken string `json:"access_token"`
15-
RefreshToken string `json:"refresh_token"`
16-
LastRefresh string `json:"last_refresh"`
17-
Expire string `json:"expired"`
18-
APIKey string `json:"api_key"`
19-
Email string `json:"email"`
20-
TokenType string `json:"token_type"`
21-
Scope string `json:"scope"`
22-
Cookie string `json:"cookie"`
23-
Type string `json:"type"`
14+
AccessToken string `json:"access_token"`
15+
RefreshToken string `json:"refresh_token"`
16+
LastRefresh string `json:"last_refresh"`
17+
Expire string `json:"expired"`
18+
APIKey string `json:"api_key"`
19+
Email string `json:"email"`
20+
TokenType string `json:"token_type"`
21+
Scope string `json:"scope"`
22+
Cookie string `json:"cookie"`
23+
Type string `json:"type"`
24+
UseGlobalProxy bool `json:"use_global_proxy"`
2425
}
2526

2627
// SaveTokenToFile serialises the token storage to disk.
@@ -42,3 +43,8 @@ func (ts *IFlowTokenStorage) SaveTokenToFile(authFilePath string) error {
4243
}
4344
return nil
4445
}
46+
47+
// SetUseGlobalProxy implements the GlobalProxySetter interface
48+
func (ts *IFlowTokenStorage) SetUseGlobalProxy(value bool) {
49+
ts.UseGlobalProxy = value
50+
}

internal/auth/qwen/qwen_auth.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -337,13 +337,14 @@ func (o *QwenAuth) RefreshTokensWithRetry(ctx context.Context, refreshToken stri
337337
}
338338

339339
// CreateTokenStorage creates a QwenTokenStorage object from a QwenTokenData object.
340-
func (o *QwenAuth) CreateTokenStorage(tokenData *QwenTokenData) *QwenTokenStorage {
340+
func (o *QwenAuth) CreateTokenStorage(tokenData *QwenTokenData, useGlobalProxy bool) *QwenTokenStorage {
341341
storage := &QwenTokenStorage{
342-
AccessToken: tokenData.AccessToken,
343-
RefreshToken: tokenData.RefreshToken,
344-
LastRefresh: time.Now().Format(time.RFC3339),
345-
ResourceURL: tokenData.ResourceURL,
346-
Expire: tokenData.Expire,
342+
AccessToken: tokenData.AccessToken,
343+
RefreshToken: tokenData.RefreshToken,
344+
LastRefresh: time.Now().Format(time.RFC3339),
345+
ResourceURL: tokenData.ResourceURL,
346+
Expire: tokenData.Expire,
347+
UseGlobalProxy: useGlobalProxy,
347348
}
348349

349350
return storage

internal/auth/qwen/qwen_token.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ type QwenTokenStorage struct {
3030
Type string `json:"type"`
3131
// Expire is the timestamp when the current access token expires.
3232
Expire string `json:"expired"`
33+
// UseGlobalProxy indicates whether to use the global proxy from config.yaml.
34+
UseGlobalProxy bool `json:"use_global_proxy"`
3335
}
3436

3537
// SaveTokenToFile serializes the Qwen token storage to a JSON file.
@@ -61,3 +63,8 @@ func (ts *QwenTokenStorage) SaveTokenToFile(authFilePath string) error {
6163
}
6264
return nil
6365
}
66+
67+
// SetUseGlobalProxy implements the GlobalProxySetter interface
68+
func (ts *QwenTokenStorage) SetUseGlobalProxy(value bool) {
69+
ts.UseGlobalProxy = value
70+
}

internal/cmd/iflow_cookie.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) {
4949
return
5050
}
5151

52+
// Ask if user wants to use global proxy
53+
useGlobalProxy := false // Default to false for iFlow
54+
answer, err := promptFn("Would you like to use the global proxy from config.yaml for this authentication? (yes/no, default: no) ")
55+
if err == nil {
56+
answer = strings.TrimSpace(strings.ToLower(answer))
57+
useGlobalProxy = answer == "y" || answer == "yes"
58+
}
59+
5260
// Authenticate with cookie
5361
auth := iflow.NewIFlowAuth(cfg)
5462
ctx := context.Background()
@@ -60,7 +68,7 @@ func DoIFlowCookieAuth(cfg *config.Config, options *LoginOptions) {
6068
}
6169

6270
// Create token storage
63-
tokenStorage := auth.CreateCookieTokenStorage(tokenData)
71+
tokenStorage := auth.CreateCookieTokenStorage(tokenData, useGlobalProxy)
6472

6573
// Get auth file path using email in filename
6674
authFilePath := getAuthFilePath(cfg, "iflow", tokenData.Email)

internal/runtime/executor/proxy_helpers.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import (
1616

1717
// newProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority:
1818
// 1. Use auth.ProxyURL if configured (highest priority)
19-
// 2. Use cfg.ProxyURL if auth proxy is not configured
20-
// 3. Use RoundTripper from context if neither are configured
19+
// 2. Use cfg.ProxyURL if auth.UseGlobalProxy is true and auth.ProxyURL is not configured
20+
// 3. Use RoundTripper from context if no proxy is configured
2121
//
2222
// Parameters:
2323
// - ctx: The context containing optional RoundTripper
@@ -33,14 +33,14 @@ func newProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *clip
3333
httpClient.Timeout = timeout
3434
}
3535

36-
// Priority 1: Use auth.ProxyURL if configured
36+
// Priority 1: Use auth.ProxyURL if configured (highest priority)
3737
var proxyURL string
3838
if auth != nil {
3939
proxyURL = strings.TrimSpace(auth.ProxyURL)
4040
}
4141

42-
// Priority 2: Use cfg.ProxyURL if auth proxy is not configured
43-
if proxyURL == "" && cfg != nil {
42+
// Priority 2: Use cfg.ProxyURL if auth.UseGlobalProxy is true and no auth proxy is set
43+
if proxyURL == "" && auth != nil && auth.UseGlobalProxy && cfg != nil {
4444
proxyURL = strings.TrimSpace(cfg.ProxyURL)
4545
}
4646

sdk/auth/filestore.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ func (s *FileTokenStore) readAuthFile(path, baseDir string) (*cliproxyauth.Auth,
197197
if email, ok := metadata["email"].(string); ok && email != "" {
198198
auth.Attributes["email"] = email
199199
}
200+
// Load use_global_proxy from metadata, default to true if not set
201+
// This maintains backward compatibility for existing auth files
202+
auth.UseGlobalProxy = true // Default to true
203+
if useGlobalProxy, ok := metadata["use_global_proxy"].(bool); ok {
204+
auth.UseGlobalProxy = useGlobalProxy
205+
}
200206
return auth, nil
201207
}
202208

sdk/auth/manager.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package auth
33
import (
44
"context"
55
"fmt"
6+
"strings"
67

78
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
89
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
@@ -43,6 +44,11 @@ func (m *Manager) SetStore(store coreauth.Store) {
4344
m.store = store
4445
}
4546

47+
// GlobalProxySetter is an interface for token storage to set the use_global_proxy value
48+
type GlobalProxySetter interface {
49+
SetUseGlobalProxy(bool)
50+
}
51+
4652
// Login executes the provider login flow and persists the resulting auth record.
4753
func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config, opts *LoginOptions) (*coreauth.Auth, string, error) {
4854
auth, ok := m.authenticators[provider]
@@ -58,6 +64,23 @@ func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config
5864
return nil, "", fmt.Errorf("cliproxy auth: authenticator %s returned nil record", provider)
5965
}
6066

67+
// Always ask about using global proxy if auth doesn't have proxy URL configured
68+
// This allows the setting to work even if user adds proxy to config.yaml later
69+
if record.ProxyURL == "" {
70+
// Determine default value based on provider
71+
defaultValue := true
72+
if provider == "qwen" || provider == "iflow" {
73+
defaultValue = false
74+
}
75+
shouldUseGlobalProxy := askUseGlobalProxy(opts, defaultValue)
76+
record.UseGlobalProxy = shouldUseGlobalProxy
77+
78+
// Update the storage if it implements GlobalProxySetter
79+
if storage, ok := record.Storage.(GlobalProxySetter); ok {
80+
storage.SetUseGlobalProxy(shouldUseGlobalProxy)
81+
}
82+
}
83+
6184
if m.store == nil {
6285
return record, "", nil
6386
}
@@ -74,3 +97,35 @@ func (m *Manager) Login(ctx context.Context, provider string, cfg *config.Config
7497
}
7598
return record, savedPath, nil
7699
}
100+
101+
// askUseGlobalProxy asks the user whether to use the global proxy from config.yaml.
102+
func askUseGlobalProxy(opts *LoginOptions, defaultValue bool) bool {
103+
if opts == nil || opts.Prompt == nil {
104+
// If no prompt function available, return the default value
105+
return defaultValue
106+
}
107+
108+
fmt.Println()
109+
fmt.Println("Would you like to use the global proxy from config.yaml for this authentication?")
110+
fmt.Println("(This allows the proxy to be applied automatically if you add it to config.yaml later)")
111+
if defaultValue {
112+
fmt.Println("yes/no, default: yes")
113+
} else {
114+
fmt.Println("yes/no, default: no")
115+
}
116+
117+
answer, err := opts.Prompt("Use global proxy? ")
118+
if err != nil {
119+
// If we can't get user input, return the default value
120+
return defaultValue
121+
}
122+
123+
answer = strings.TrimSpace(strings.ToLower(answer))
124+
if defaultValue {
125+
// If default is true, only turn off if user says no
126+
return answer != "n" && answer != "no"
127+
} else {
128+
// If default is false, only turn on if user says yes
129+
return answer == "y" || answer == "yes"
130+
}
131+
}

sdk/auth/qwen.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ func (a *QwenAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
7171
return nil, fmt.Errorf("qwen authentication failed: %w", err)
7272
}
7373

74-
tokenStorage := authSvc.CreateTokenStorage(tokenData)
74+
// Note: use_global_proxy will be set by manager.go's Login method
75+
// Default to false for qwen
76+
tokenStorage := authSvc.CreateTokenStorage(tokenData, false)
7577

7678
email := ""
7779
if opts.Metadata != nil {

0 commit comments

Comments
 (0)