mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-19 04:40:52 +08:00
Merge pull request #641 from router-for-me/url-OAuth-add-ter
OAuth and management
This commit is contained in:
@@ -197,6 +197,19 @@ func stopCallbackForwarder(port int) {
|
|||||||
stopForwarderInstance(port, forwarder)
|
stopForwarderInstance(port, forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stopCallbackForwarderInstance(port int, forwarder *callbackForwarder) {
|
||||||
|
if forwarder == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callbackForwardersMu.Lock()
|
||||||
|
if current := callbackForwarders[port]; current == forwarder {
|
||||||
|
delete(callbackForwarders, port)
|
||||||
|
}
|
||||||
|
callbackForwardersMu.Unlock()
|
||||||
|
|
||||||
|
stopForwarderInstance(port, forwarder)
|
||||||
|
}
|
||||||
|
|
||||||
func stopForwarderInstance(port int, forwarder *callbackForwarder) {
|
func stopForwarderInstance(port int, forwarder *callbackForwarder) {
|
||||||
if forwarder == nil || forwarder.server == nil {
|
if forwarder == nil || forwarder.server == nil {
|
||||||
return
|
return
|
||||||
@@ -785,6 +798,7 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
RegisterOAuthSession(state, "anthropic")
|
RegisterOAuthSession(state, "anthropic")
|
||||||
|
|
||||||
isWebUI := isWebUIRequest(c)
|
isWebUI := isWebUIRequest(c)
|
||||||
|
var forwarder *callbackForwarder
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
targetURL, errTarget := h.managementCallbackURL("/anthropic/callback")
|
targetURL, errTarget := h.managementCallbackURL("/anthropic/callback")
|
||||||
if errTarget != nil {
|
if errTarget != nil {
|
||||||
@@ -792,7 +806,8 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, errStart := startCallbackForwarder(anthropicCallbackPort, "anthropic", targetURL); errStart != nil {
|
var errStart error
|
||||||
|
if forwarder, errStart = startCallbackForwarder(anthropicCallbackPort, "anthropic", targetURL); errStart != nil {
|
||||||
log.WithError(errStart).Error("failed to start anthropic callback forwarder")
|
log.WithError(errStart).Error("failed to start anthropic callback forwarder")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
||||||
return
|
return
|
||||||
@@ -801,7 +816,7 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
defer stopCallbackForwarder(anthropicCallbackPort)
|
defer stopCallbackForwarderInstance(anthropicCallbackPort, forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper: wait for callback file
|
// Helper: wait for callback file
|
||||||
@@ -809,6 +824,9 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
waitForFile := func(path string, timeout time.Duration) (map[string]string, error) {
|
waitForFile := func(path string, timeout time.Duration) (map[string]string, error) {
|
||||||
deadline := time.Now().Add(timeout)
|
deadline := time.Now().Add(timeout)
|
||||||
for {
|
for {
|
||||||
|
if !IsOAuthSessionPending(state, "anthropic") {
|
||||||
|
return nil, errOAuthSessionNotPending
|
||||||
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
SetOAuthSessionError(state, "Timeout waiting for OAuth callback")
|
SetOAuthSessionError(state, "Timeout waiting for OAuth callback")
|
||||||
return nil, fmt.Errorf("timeout waiting for OAuth callback")
|
return nil, fmt.Errorf("timeout waiting for OAuth callback")
|
||||||
@@ -828,6 +846,9 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
// Wait up to 5 minutes
|
// Wait up to 5 minutes
|
||||||
resultMap, errWait := waitForFile(waitFile, 5*time.Minute)
|
resultMap, errWait := waitForFile(waitFile, 5*time.Minute)
|
||||||
if errWait != nil {
|
if errWait != nil {
|
||||||
|
if errors.Is(errWait, errOAuthSessionNotPending) {
|
||||||
|
return
|
||||||
|
}
|
||||||
authErr := claude.NewAuthenticationError(claude.ErrCallbackTimeout, errWait)
|
authErr := claude.NewAuthenticationError(claude.ErrCallbackTimeout, errWait)
|
||||||
log.Error(claude.GetUserFriendlyMessage(authErr))
|
log.Error(claude.GetUserFriendlyMessage(authErr))
|
||||||
return
|
return
|
||||||
@@ -933,6 +954,7 @@ func (h *Handler) RequestAnthropicToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
fmt.Println("You can now use Claude services through this CLI")
|
fmt.Println("You can now use Claude services through this CLI")
|
||||||
CompleteOAuthSession(state)
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("anthropic")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
||||||
@@ -968,6 +990,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
RegisterOAuthSession(state, "gemini")
|
RegisterOAuthSession(state, "gemini")
|
||||||
|
|
||||||
isWebUI := isWebUIRequest(c)
|
isWebUI := isWebUIRequest(c)
|
||||||
|
var forwarder *callbackForwarder
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
targetURL, errTarget := h.managementCallbackURL("/google/callback")
|
targetURL, errTarget := h.managementCallbackURL("/google/callback")
|
||||||
if errTarget != nil {
|
if errTarget != nil {
|
||||||
@@ -975,7 +998,8 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, errStart := startCallbackForwarder(geminiCallbackPort, "gemini", targetURL); errStart != nil {
|
var errStart error
|
||||||
|
if forwarder, errStart = startCallbackForwarder(geminiCallbackPort, "gemini", targetURL); errStart != nil {
|
||||||
log.WithError(errStart).Error("failed to start gemini callback forwarder")
|
log.WithError(errStart).Error("failed to start gemini callback forwarder")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
||||||
return
|
return
|
||||||
@@ -984,7 +1008,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
defer stopCallbackForwarder(geminiCallbackPort)
|
defer stopCallbackForwarderInstance(geminiCallbackPort, forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for callback file written by server route
|
// Wait for callback file written by server route
|
||||||
@@ -993,6 +1017,9 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
deadline := time.Now().Add(5 * time.Minute)
|
deadline := time.Now().Add(5 * time.Minute)
|
||||||
var authCode string
|
var authCode string
|
||||||
for {
|
for {
|
||||||
|
if !IsOAuthSessionPending(state, "gemini") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
log.Error("oauth flow timed out")
|
log.Error("oauth flow timed out")
|
||||||
SetOAuthSessionError(state, "OAuth flow timed out")
|
SetOAuthSessionError(state, "OAuth flow timed out")
|
||||||
@@ -1093,7 +1120,9 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
|
|
||||||
// Initialize authenticated HTTP client via GeminiAuth to honor proxy settings
|
// Initialize authenticated HTTP client via GeminiAuth to honor proxy settings
|
||||||
gemAuth := geminiAuth.NewGeminiAuth()
|
gemAuth := geminiAuth.NewGeminiAuth()
|
||||||
gemClient, errGetClient := gemAuth.GetAuthenticatedClient(ctx, &ts, h.cfg, true)
|
gemClient, errGetClient := gemAuth.GetAuthenticatedClient(ctx, &ts, h.cfg, &geminiAuth.WebLoginOptions{
|
||||||
|
NoBrowser: true,
|
||||||
|
})
|
||||||
if errGetClient != nil {
|
if errGetClient != nil {
|
||||||
log.Errorf("failed to get authenticated client: %v", errGetClient)
|
log.Errorf("failed to get authenticated client: %v", errGetClient)
|
||||||
SetOAuthSessionError(state, "Failed to get authenticated client")
|
SetOAuthSessionError(state, "Failed to get authenticated client")
|
||||||
@@ -1166,6 +1195,7 @@ func (h *Handler) RequestGeminiCLIToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompleteOAuthSession(state)
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("gemini")
|
||||||
fmt.Printf("You can now use Gemini CLI services through this CLI; token saved to %s\n", savedPath)
|
fmt.Printf("You can now use Gemini CLI services through this CLI; token saved to %s\n", savedPath)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -1207,6 +1237,7 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
|||||||
RegisterOAuthSession(state, "codex")
|
RegisterOAuthSession(state, "codex")
|
||||||
|
|
||||||
isWebUI := isWebUIRequest(c)
|
isWebUI := isWebUIRequest(c)
|
||||||
|
var forwarder *callbackForwarder
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
targetURL, errTarget := h.managementCallbackURL("/codex/callback")
|
targetURL, errTarget := h.managementCallbackURL("/codex/callback")
|
||||||
if errTarget != nil {
|
if errTarget != nil {
|
||||||
@@ -1214,7 +1245,8 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, errStart := startCallbackForwarder(codexCallbackPort, "codex", targetURL); errStart != nil {
|
var errStart error
|
||||||
|
if forwarder, errStart = startCallbackForwarder(codexCallbackPort, "codex", targetURL); errStart != nil {
|
||||||
log.WithError(errStart).Error("failed to start codex callback forwarder")
|
log.WithError(errStart).Error("failed to start codex callback forwarder")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
||||||
return
|
return
|
||||||
@@ -1223,7 +1255,7 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
defer stopCallbackForwarder(codexCallbackPort)
|
defer stopCallbackForwarderInstance(codexCallbackPort, forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for callback file
|
// Wait for callback file
|
||||||
@@ -1231,6 +1263,9 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
|||||||
deadline := time.Now().Add(5 * time.Minute)
|
deadline := time.Now().Add(5 * time.Minute)
|
||||||
var code string
|
var code string
|
||||||
for {
|
for {
|
||||||
|
if !IsOAuthSessionPending(state, "codex") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
authErr := codex.NewAuthenticationError(codex.ErrCallbackTimeout, fmt.Errorf("timeout waiting for OAuth callback"))
|
authErr := codex.NewAuthenticationError(codex.ErrCallbackTimeout, fmt.Errorf("timeout waiting for OAuth callback"))
|
||||||
log.Error(codex.GetUserFriendlyMessage(authErr))
|
log.Error(codex.GetUserFriendlyMessage(authErr))
|
||||||
@@ -1346,6 +1381,7 @@ func (h *Handler) RequestCodexToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
fmt.Println("You can now use Codex services through this CLI")
|
fmt.Println("You can now use Codex services through this CLI")
|
||||||
CompleteOAuthSession(state)
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("codex")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
c.JSON(200, gin.H{"status": "ok", "url": authURL, "state": state})
|
||||||
@@ -1391,6 +1427,7 @@ func (h *Handler) RequestAntigravityToken(c *gin.Context) {
|
|||||||
RegisterOAuthSession(state, "antigravity")
|
RegisterOAuthSession(state, "antigravity")
|
||||||
|
|
||||||
isWebUI := isWebUIRequest(c)
|
isWebUI := isWebUIRequest(c)
|
||||||
|
var forwarder *callbackForwarder
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
targetURL, errTarget := h.managementCallbackURL("/antigravity/callback")
|
targetURL, errTarget := h.managementCallbackURL("/antigravity/callback")
|
||||||
if errTarget != nil {
|
if errTarget != nil {
|
||||||
@@ -1398,7 +1435,8 @@ func (h *Handler) RequestAntigravityToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "callback server unavailable"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, errStart := startCallbackForwarder(antigravityCallbackPort, "antigravity", targetURL); errStart != nil {
|
var errStart error
|
||||||
|
if forwarder, errStart = startCallbackForwarder(antigravityCallbackPort, "antigravity", targetURL); errStart != nil {
|
||||||
log.WithError(errStart).Error("failed to start antigravity callback forwarder")
|
log.WithError(errStart).Error("failed to start antigravity callback forwarder")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to start callback server"})
|
||||||
return
|
return
|
||||||
@@ -1407,13 +1445,16 @@ func (h *Handler) RequestAntigravityToken(c *gin.Context) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
defer stopCallbackForwarder(antigravityCallbackPort)
|
defer stopCallbackForwarderInstance(antigravityCallbackPort, forwarder)
|
||||||
}
|
}
|
||||||
|
|
||||||
waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-antigravity-%s.oauth", state))
|
waitFile := filepath.Join(h.cfg.AuthDir, fmt.Sprintf(".oauth-antigravity-%s.oauth", state))
|
||||||
deadline := time.Now().Add(5 * time.Minute)
|
deadline := time.Now().Add(5 * time.Minute)
|
||||||
var authCode string
|
var authCode string
|
||||||
for {
|
for {
|
||||||
|
if !IsOAuthSessionPending(state, "antigravity") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
log.Error("oauth flow timed out")
|
log.Error("oauth flow timed out")
|
||||||
SetOAuthSessionError(state, "OAuth flow timed out")
|
SetOAuthSessionError(state, "OAuth flow timed out")
|
||||||
@@ -1576,6 +1617,7 @@ func (h *Handler) RequestAntigravityToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CompleteOAuthSession(state)
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("antigravity")
|
||||||
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
fmt.Printf("Authentication successful! Token saved to %s\n", savedPath)
|
||||||
if projectID != "" {
|
if projectID != "" {
|
||||||
fmt.Printf("Using GCP project: %s\n", projectID)
|
fmt.Printf("Using GCP project: %s\n", projectID)
|
||||||
@@ -1653,6 +1695,7 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
RegisterOAuthSession(state, "iflow")
|
RegisterOAuthSession(state, "iflow")
|
||||||
|
|
||||||
isWebUI := isWebUIRequest(c)
|
isWebUI := isWebUIRequest(c)
|
||||||
|
var forwarder *callbackForwarder
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
targetURL, errTarget := h.managementCallbackURL("/iflow/callback")
|
targetURL, errTarget := h.managementCallbackURL("/iflow/callback")
|
||||||
if errTarget != nil {
|
if errTarget != nil {
|
||||||
@@ -1660,7 +1703,8 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "callback server unavailable"})
|
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "callback server unavailable"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if _, errStart := startCallbackForwarder(iflowauth.CallbackPort, "iflow", targetURL); errStart != nil {
|
var errStart error
|
||||||
|
if forwarder, errStart = startCallbackForwarder(iflowauth.CallbackPort, "iflow", targetURL); errStart != nil {
|
||||||
log.WithError(errStart).Error("failed to start iflow callback forwarder")
|
log.WithError(errStart).Error("failed to start iflow callback forwarder")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to start callback server"})
|
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to start callback server"})
|
||||||
return
|
return
|
||||||
@@ -1669,7 +1713,7 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
if isWebUI {
|
if isWebUI {
|
||||||
defer stopCallbackForwarder(iflowauth.CallbackPort)
|
defer stopCallbackForwarderInstance(iflowauth.CallbackPort, forwarder)
|
||||||
}
|
}
|
||||||
fmt.Println("Waiting for authentication...")
|
fmt.Println("Waiting for authentication...")
|
||||||
|
|
||||||
@@ -1677,6 +1721,9 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
deadline := time.Now().Add(5 * time.Minute)
|
deadline := time.Now().Add(5 * time.Minute)
|
||||||
var resultMap map[string]string
|
var resultMap map[string]string
|
||||||
for {
|
for {
|
||||||
|
if !IsOAuthSessionPending(state, "iflow") {
|
||||||
|
return
|
||||||
|
}
|
||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
SetOAuthSessionError(state, "Authentication failed")
|
SetOAuthSessionError(state, "Authentication failed")
|
||||||
fmt.Println("Authentication failed: timeout waiting for callback")
|
fmt.Println("Authentication failed: timeout waiting for callback")
|
||||||
@@ -1743,6 +1790,7 @@ func (h *Handler) RequestIFlowToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
fmt.Println("You can now use iFlow services through this CLI")
|
fmt.Println("You can now use iFlow services through this CLI")
|
||||||
CompleteOAuthSession(state)
|
CompleteOAuthSession(state)
|
||||||
|
CompleteOAuthSessionsByProvider("iflow")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"status": "ok", "url": authURL, "state": state})
|
c.JSON(http.StatusOK, gin.H{"status": "ok", "url": authURL, "state": state})
|
||||||
|
|||||||
@@ -145,71 +145,74 @@ func (h *Handler) PutGeminiKeys(c *gin.Context) {
|
|||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
func (h *Handler) PatchGeminiKey(c *gin.Context) {
|
||||||
|
type geminiKeyPatch struct {
|
||||||
|
APIKey *string `json:"api-key"`
|
||||||
|
Prefix *string `json:"prefix"`
|
||||||
|
BaseURL *string `json:"base-url"`
|
||||||
|
ProxyURL *string `json:"proxy-url"`
|
||||||
|
Headers *map[string]string `json:"headers"`
|
||||||
|
ExcludedModels *[]string `json:"excluded-models"`
|
||||||
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Index *int `json:"index"`
|
Index *int `json:"index"`
|
||||||
Match *string `json:"match"`
|
Match *string `json:"match"`
|
||||||
Value *config.GeminiKey `json:"value"`
|
Value *geminiKeyPatch `json:"value"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
value := *body.Value
|
targetIndex := -1
|
||||||
value.APIKey = strings.TrimSpace(value.APIKey)
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
||||||
value.BaseURL = strings.TrimSpace(value.BaseURL)
|
targetIndex = *body.Index
|
||||||
value.ProxyURL = strings.TrimSpace(value.ProxyURL)
|
}
|
||||||
value.ExcludedModels = config.NormalizeExcludedModels(value.ExcludedModels)
|
if targetIndex == -1 && body.Match != nil {
|
||||||
if value.APIKey == "" {
|
match := strings.TrimSpace(*body.Match)
|
||||||
// Treat empty API key as delete.
|
if match != "" {
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
for i := range h.cfg.GeminiKey {
|
||||||
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:*body.Index], h.cfg.GeminiKey[*body.Index+1:]...)
|
if h.cfg.GeminiKey[i].APIKey == match {
|
||||||
h.cfg.SanitizeGeminiKeys()
|
targetIndex = i
|
||||||
h.persist(c)
|
break
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Match != nil {
|
|
||||||
match := strings.TrimSpace(*body.Match)
|
|
||||||
if match != "" {
|
|
||||||
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
|
||||||
removed := false
|
|
||||||
for i := range h.cfg.GeminiKey {
|
|
||||||
if !removed && h.cfg.GeminiKey[i].APIKey == match {
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, h.cfg.GeminiKey[i])
|
|
||||||
}
|
|
||||||
if removed {
|
|
||||||
h.cfg.GeminiKey = out
|
|
||||||
h.cfg.SanitizeGeminiKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if targetIndex == -1 {
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.GeminiKey) {
|
entry := h.cfg.GeminiKey[targetIndex]
|
||||||
h.cfg.GeminiKey[*body.Index] = value
|
if body.Value.APIKey != nil {
|
||||||
h.cfg.SanitizeGeminiKeys()
|
trimmed := strings.TrimSpace(*body.Value.APIKey)
|
||||||
h.persist(c)
|
if trimmed == "" {
|
||||||
return
|
h.cfg.GeminiKey = append(h.cfg.GeminiKey[:targetIndex], h.cfg.GeminiKey[targetIndex+1:]...)
|
||||||
}
|
h.cfg.SanitizeGeminiKeys()
|
||||||
if body.Match != nil {
|
h.persist(c)
|
||||||
match := strings.TrimSpace(*body.Match)
|
return
|
||||||
for i := range h.cfg.GeminiKey {
|
|
||||||
if h.cfg.GeminiKey[i].APIKey == match {
|
|
||||||
h.cfg.GeminiKey[i] = value
|
|
||||||
h.cfg.SanitizeGeminiKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
entry.APIKey = trimmed
|
||||||
}
|
}
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
if body.Value.Prefix != nil {
|
||||||
|
entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
|
||||||
|
}
|
||||||
|
if body.Value.BaseURL != nil {
|
||||||
|
entry.BaseURL = strings.TrimSpace(*body.Value.BaseURL)
|
||||||
|
}
|
||||||
|
if body.Value.ProxyURL != nil {
|
||||||
|
entry.ProxyURL = strings.TrimSpace(*body.Value.ProxyURL)
|
||||||
|
}
|
||||||
|
if body.Value.Headers != nil {
|
||||||
|
entry.Headers = config.NormalizeHeaders(*body.Value.Headers)
|
||||||
|
}
|
||||||
|
if body.Value.ExcludedModels != nil {
|
||||||
|
entry.ExcludedModels = config.NormalizeExcludedModels(*body.Value.ExcludedModels)
|
||||||
|
}
|
||||||
|
h.cfg.GeminiKey[targetIndex] = entry
|
||||||
|
h.cfg.SanitizeGeminiKeys()
|
||||||
|
h.persist(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
func (h *Handler) DeleteGeminiKey(c *gin.Context) {
|
||||||
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
if val := strings.TrimSpace(c.Query("api-key")); val != "" {
|
||||||
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
out := make([]config.GeminiKey, 0, len(h.cfg.GeminiKey))
|
||||||
@@ -268,35 +271,70 @@ func (h *Handler) PutClaudeKeys(c *gin.Context) {
|
|||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
func (h *Handler) PatchClaudeKey(c *gin.Context) {
|
||||||
|
type claudeKeyPatch struct {
|
||||||
|
APIKey *string `json:"api-key"`
|
||||||
|
Prefix *string `json:"prefix"`
|
||||||
|
BaseURL *string `json:"base-url"`
|
||||||
|
ProxyURL *string `json:"proxy-url"`
|
||||||
|
Models *[]config.ClaudeModel `json:"models"`
|
||||||
|
Headers *map[string]string `json:"headers"`
|
||||||
|
ExcludedModels *[]string `json:"excluded-models"`
|
||||||
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Index *int `json:"index"`
|
Index *int `json:"index"`
|
||||||
Match *string `json:"match"`
|
Match *string `json:"match"`
|
||||||
Value *config.ClaudeKey `json:"value"`
|
Value *claudeKeyPatch `json:"value"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
value := *body.Value
|
targetIndex := -1
|
||||||
normalizeClaudeKey(&value)
|
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) {
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.ClaudeKey) {
|
||||||
h.cfg.ClaudeKey[*body.Index] = value
|
targetIndex = *body.Index
|
||||||
h.cfg.SanitizeClaudeKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if body.Match != nil {
|
if targetIndex == -1 && body.Match != nil {
|
||||||
|
match := strings.TrimSpace(*body.Match)
|
||||||
for i := range h.cfg.ClaudeKey {
|
for i := range h.cfg.ClaudeKey {
|
||||||
if h.cfg.ClaudeKey[i].APIKey == *body.Match {
|
if h.cfg.ClaudeKey[i].APIKey == match {
|
||||||
h.cfg.ClaudeKey[i] = value
|
targetIndex = i
|
||||||
h.cfg.SanitizeClaudeKeys()
|
break
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
if targetIndex == -1 {
|
||||||
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := h.cfg.ClaudeKey[targetIndex]
|
||||||
|
if body.Value.APIKey != nil {
|
||||||
|
entry.APIKey = strings.TrimSpace(*body.Value.APIKey)
|
||||||
|
}
|
||||||
|
if body.Value.Prefix != nil {
|
||||||
|
entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
|
||||||
|
}
|
||||||
|
if body.Value.BaseURL != nil {
|
||||||
|
entry.BaseURL = strings.TrimSpace(*body.Value.BaseURL)
|
||||||
|
}
|
||||||
|
if body.Value.ProxyURL != nil {
|
||||||
|
entry.ProxyURL = strings.TrimSpace(*body.Value.ProxyURL)
|
||||||
|
}
|
||||||
|
if body.Value.Models != nil {
|
||||||
|
entry.Models = append([]config.ClaudeModel(nil), (*body.Value.Models)...)
|
||||||
|
}
|
||||||
|
if body.Value.Headers != nil {
|
||||||
|
entry.Headers = config.NormalizeHeaders(*body.Value.Headers)
|
||||||
|
}
|
||||||
|
if body.Value.ExcludedModels != nil {
|
||||||
|
entry.ExcludedModels = config.NormalizeExcludedModels(*body.Value.ExcludedModels)
|
||||||
|
}
|
||||||
|
normalizeClaudeKey(&entry)
|
||||||
|
h.cfg.ClaudeKey[targetIndex] = entry
|
||||||
|
h.cfg.SanitizeClaudeKeys()
|
||||||
|
h.persist(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
func (h *Handler) DeleteClaudeKey(c *gin.Context) {
|
||||||
if val := c.Query("api-key"); val != "" {
|
if val := c.Query("api-key"); val != "" {
|
||||||
out := make([]config.ClaudeKey, 0, len(h.cfg.ClaudeKey))
|
out := make([]config.ClaudeKey, 0, len(h.cfg.ClaudeKey))
|
||||||
@@ -356,62 +394,73 @@ func (h *Handler) PutOpenAICompat(c *gin.Context) {
|
|||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
func (h *Handler) PatchOpenAICompat(c *gin.Context) {
|
||||||
|
type openAICompatPatch struct {
|
||||||
|
Name *string `json:"name"`
|
||||||
|
Prefix *string `json:"prefix"`
|
||||||
|
BaseURL *string `json:"base-url"`
|
||||||
|
APIKeyEntries *[]config.OpenAICompatibilityAPIKey `json:"api-key-entries"`
|
||||||
|
Models *[]config.OpenAICompatibilityModel `json:"models"`
|
||||||
|
Headers *map[string]string `json:"headers"`
|
||||||
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Name *string `json:"name"`
|
Name *string `json:"name"`
|
||||||
Index *int `json:"index"`
|
Index *int `json:"index"`
|
||||||
Value *config.OpenAICompatibility `json:"value"`
|
Value *openAICompatPatch `json:"value"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
normalizeOpenAICompatibilityEntry(body.Value)
|
targetIndex := -1
|
||||||
// If base-url becomes empty, delete the provider instead of updating
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
|
||||||
if strings.TrimSpace(body.Value.BaseURL) == "" {
|
targetIndex = *body.Index
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
|
}
|
||||||
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:*body.Index], h.cfg.OpenAICompatibility[*body.Index+1:]...)
|
if targetIndex == -1 && body.Name != nil {
|
||||||
|
match := strings.TrimSpace(*body.Name)
|
||||||
|
for i := range h.cfg.OpenAICompatibility {
|
||||||
|
if h.cfg.OpenAICompatibility[i].Name == match {
|
||||||
|
targetIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if targetIndex == -1 {
|
||||||
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := h.cfg.OpenAICompatibility[targetIndex]
|
||||||
|
if body.Value.Name != nil {
|
||||||
|
entry.Name = strings.TrimSpace(*body.Value.Name)
|
||||||
|
}
|
||||||
|
if body.Value.Prefix != nil {
|
||||||
|
entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
|
||||||
|
}
|
||||||
|
if body.Value.BaseURL != nil {
|
||||||
|
trimmed := strings.TrimSpace(*body.Value.BaseURL)
|
||||||
|
if trimmed == "" {
|
||||||
|
h.cfg.OpenAICompatibility = append(h.cfg.OpenAICompatibility[:targetIndex], h.cfg.OpenAICompatibility[targetIndex+1:]...)
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
h.persist(c)
|
h.persist(c)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.Name != nil {
|
entry.BaseURL = trimmed
|
||||||
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
|
|
||||||
removed := false
|
|
||||||
for i := range h.cfg.OpenAICompatibility {
|
|
||||||
if !removed && h.cfg.OpenAICompatibility[i].Name == *body.Name {
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, h.cfg.OpenAICompatibility[i])
|
|
||||||
}
|
|
||||||
if removed {
|
|
||||||
h.cfg.OpenAICompatibility = out
|
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.OpenAICompatibility) {
|
if body.Value.APIKeyEntries != nil {
|
||||||
h.cfg.OpenAICompatibility[*body.Index] = *body.Value
|
entry.APIKeyEntries = append([]config.OpenAICompatibilityAPIKey(nil), (*body.Value.APIKeyEntries)...)
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if body.Name != nil {
|
if body.Value.Models != nil {
|
||||||
for i := range h.cfg.OpenAICompatibility {
|
entry.Models = append([]config.OpenAICompatibilityModel(nil), (*body.Value.Models)...)
|
||||||
if h.cfg.OpenAICompatibility[i].Name == *body.Name {
|
|
||||||
h.cfg.OpenAICompatibility[i] = *body.Value
|
|
||||||
h.cfg.SanitizeOpenAICompatibility()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
if body.Value.Headers != nil {
|
||||||
|
entry.Headers = config.NormalizeHeaders(*body.Value.Headers)
|
||||||
|
}
|
||||||
|
normalizeOpenAICompatibilityEntry(&entry)
|
||||||
|
h.cfg.OpenAICompatibility[targetIndex] = entry
|
||||||
|
h.cfg.SanitizeOpenAICompatibility()
|
||||||
|
h.persist(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
func (h *Handler) DeleteOpenAICompat(c *gin.Context) {
|
||||||
if name := c.Query("name"); name != "" {
|
if name := c.Query("name"); name != "" {
|
||||||
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
|
out := make([]config.OpenAICompatibility, 0, len(h.cfg.OpenAICompatibility))
|
||||||
@@ -563,66 +612,72 @@ func (h *Handler) PutCodexKeys(c *gin.Context) {
|
|||||||
h.persist(c)
|
h.persist(c)
|
||||||
}
|
}
|
||||||
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
func (h *Handler) PatchCodexKey(c *gin.Context) {
|
||||||
|
type codexKeyPatch struct {
|
||||||
|
APIKey *string `json:"api-key"`
|
||||||
|
Prefix *string `json:"prefix"`
|
||||||
|
BaseURL *string `json:"base-url"`
|
||||||
|
ProxyURL *string `json:"proxy-url"`
|
||||||
|
Headers *map[string]string `json:"headers"`
|
||||||
|
ExcludedModels *[]string `json:"excluded-models"`
|
||||||
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
Index *int `json:"index"`
|
Index *int `json:"index"`
|
||||||
Match *string `json:"match"`
|
Match *string `json:"match"`
|
||||||
Value *config.CodexKey `json:"value"`
|
Value *codexKeyPatch `json:"value"`
|
||||||
}
|
}
|
||||||
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
if err := c.ShouldBindJSON(&body); err != nil || body.Value == nil {
|
||||||
c.JSON(400, gin.H{"error": "invalid body"})
|
c.JSON(400, gin.H{"error": "invalid body"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
value := *body.Value
|
targetIndex := -1
|
||||||
value.APIKey = strings.TrimSpace(value.APIKey)
|
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) {
|
||||||
value.BaseURL = strings.TrimSpace(value.BaseURL)
|
targetIndex = *body.Index
|
||||||
value.ProxyURL = strings.TrimSpace(value.ProxyURL)
|
}
|
||||||
value.Headers = config.NormalizeHeaders(value.Headers)
|
if targetIndex == -1 && body.Match != nil {
|
||||||
value.ExcludedModels = config.NormalizeExcludedModels(value.ExcludedModels)
|
match := strings.TrimSpace(*body.Match)
|
||||||
// If base-url becomes empty, delete instead of update
|
for i := range h.cfg.CodexKey {
|
||||||
if value.BaseURL == "" {
|
if h.cfg.CodexKey[i].APIKey == match {
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) {
|
targetIndex = i
|
||||||
h.cfg.CodexKey = append(h.cfg.CodexKey[:*body.Index], h.cfg.CodexKey[*body.Index+1:]...)
|
break
|
||||||
h.cfg.SanitizeCodexKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Match != nil {
|
|
||||||
out := make([]config.CodexKey, 0, len(h.cfg.CodexKey))
|
|
||||||
removed := false
|
|
||||||
for i := range h.cfg.CodexKey {
|
|
||||||
if !removed && h.cfg.CodexKey[i].APIKey == *body.Match {
|
|
||||||
removed = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out = append(out, h.cfg.CodexKey[i])
|
|
||||||
}
|
|
||||||
if removed {
|
|
||||||
h.cfg.CodexKey = out
|
|
||||||
h.cfg.SanitizeCodexKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if body.Index != nil && *body.Index >= 0 && *body.Index < len(h.cfg.CodexKey) {
|
|
||||||
h.cfg.CodexKey[*body.Index] = value
|
|
||||||
h.cfg.SanitizeCodexKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if body.Match != nil {
|
|
||||||
for i := range h.cfg.CodexKey {
|
|
||||||
if h.cfg.CodexKey[i].APIKey == *body.Match {
|
|
||||||
h.cfg.CodexKey[i] = value
|
|
||||||
h.cfg.SanitizeCodexKeys()
|
|
||||||
h.persist(c)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.JSON(404, gin.H{"error": "item not found"})
|
if targetIndex == -1 {
|
||||||
|
c.JSON(404, gin.H{"error": "item not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := h.cfg.CodexKey[targetIndex]
|
||||||
|
if body.Value.APIKey != nil {
|
||||||
|
entry.APIKey = strings.TrimSpace(*body.Value.APIKey)
|
||||||
|
}
|
||||||
|
if body.Value.Prefix != nil {
|
||||||
|
entry.Prefix = strings.TrimSpace(*body.Value.Prefix)
|
||||||
|
}
|
||||||
|
if body.Value.BaseURL != nil {
|
||||||
|
trimmed := strings.TrimSpace(*body.Value.BaseURL)
|
||||||
|
if trimmed == "" {
|
||||||
|
h.cfg.CodexKey = append(h.cfg.CodexKey[:targetIndex], h.cfg.CodexKey[targetIndex+1:]...)
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
|
h.persist(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
entry.BaseURL = trimmed
|
||||||
|
}
|
||||||
|
if body.Value.ProxyURL != nil {
|
||||||
|
entry.ProxyURL = strings.TrimSpace(*body.Value.ProxyURL)
|
||||||
|
}
|
||||||
|
if body.Value.Headers != nil {
|
||||||
|
entry.Headers = config.NormalizeHeaders(*body.Value.Headers)
|
||||||
|
}
|
||||||
|
if body.Value.ExcludedModels != nil {
|
||||||
|
entry.ExcludedModels = config.NormalizeExcludedModels(*body.Value.ExcludedModels)
|
||||||
|
}
|
||||||
|
h.cfg.CodexKey[targetIndex] = entry
|
||||||
|
h.cfg.SanitizeCodexKeys()
|
||||||
|
h.persist(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
func (h *Handler) DeleteCodexKey(c *gin.Context) {
|
||||||
if val := c.Query("api-key"); val != "" {
|
if val := c.Query("api-key"); val != "" {
|
||||||
out := make([]config.CodexKey, 0, len(h.cfg.CodexKey))
|
out := make([]config.CodexKey, 0, len(h.cfg.CodexKey))
|
||||||
|
|||||||
@@ -111,6 +111,27 @@ func (s *oauthSessionStore) Complete(state string) {
|
|||||||
delete(s.sessions, state)
|
delete(s.sessions, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *oauthSessionStore) CompleteProvider(provider string) int {
|
||||||
|
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||||
|
if provider == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
s.purgeExpiredLocked(now)
|
||||||
|
removed := 0
|
||||||
|
for state, session := range s.sessions {
|
||||||
|
if strings.EqualFold(session.Provider, provider) {
|
||||||
|
delete(s.sessions, state)
|
||||||
|
removed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed
|
||||||
|
}
|
||||||
|
|
||||||
func (s *oauthSessionStore) Get(state string) (oauthSession, bool) {
|
func (s *oauthSessionStore) Get(state string) (oauthSession, bool) {
|
||||||
state = strings.TrimSpace(state)
|
state = strings.TrimSpace(state)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -153,6 +174,10 @@ func SetOAuthSessionError(state, message string) { oauthSessions.SetError(state,
|
|||||||
|
|
||||||
func CompleteOAuthSession(state string) { oauthSessions.Complete(state) }
|
func CompleteOAuthSession(state string) { oauthSessions.Complete(state) }
|
||||||
|
|
||||||
|
func CompleteOAuthSessionsByProvider(provider string) int {
|
||||||
|
return oauthSessions.CompleteProvider(provider)
|
||||||
|
}
|
||||||
|
|
||||||
func GetOAuthSession(state string) (provider string, status string, ok bool) {
|
func GetOAuthSession(state string) (provider string, status string, ok bool) {
|
||||||
session, ok := oauthSessions.Get(state)
|
session, ok := oauthSessions.Get(state)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/browser"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
@@ -46,6 +47,12 @@ var (
|
|||||||
type GeminiAuth struct {
|
type GeminiAuth struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebLoginOptions customizes the interactive OAuth flow.
|
||||||
|
type WebLoginOptions struct {
|
||||||
|
NoBrowser bool
|
||||||
|
Prompt func(string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
// NewGeminiAuth creates a new instance of GeminiAuth.
|
// NewGeminiAuth creates a new instance of GeminiAuth.
|
||||||
func NewGeminiAuth() *GeminiAuth {
|
func NewGeminiAuth() *GeminiAuth {
|
||||||
return &GeminiAuth{}
|
return &GeminiAuth{}
|
||||||
@@ -59,12 +66,12 @@ func NewGeminiAuth() *GeminiAuth {
|
|||||||
// - ctx: The context for the HTTP client
|
// - ctx: The context for the HTTP client
|
||||||
// - ts: The Gemini token storage containing authentication tokens
|
// - ts: The Gemini token storage containing authentication tokens
|
||||||
// - cfg: The configuration containing proxy settings
|
// - cfg: The configuration containing proxy settings
|
||||||
// - noBrowser: Optional parameter to disable browser opening
|
// - opts: Optional parameters to customize browser and prompt behavior
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *http.Client: An HTTP client configured with authentication
|
// - *http.Client: An HTTP client configured with authentication
|
||||||
// - error: An error if the client configuration fails, nil otherwise
|
// - error: An error if the client configuration fails, nil otherwise
|
||||||
func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiTokenStorage, cfg *config.Config, noBrowser ...bool) (*http.Client, error) {
|
func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiTokenStorage, cfg *config.Config, opts *WebLoginOptions) (*http.Client, error) {
|
||||||
// Configure proxy settings for the HTTP client if a proxy URL is provided.
|
// Configure proxy settings for the HTTP client if a proxy URL is provided.
|
||||||
proxyURL, err := url.Parse(cfg.ProxyURL)
|
proxyURL, err := url.Parse(cfg.ProxyURL)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -109,7 +116,7 @@ func (g *GeminiAuth) GetAuthenticatedClient(ctx context.Context, ts *GeminiToken
|
|||||||
// If no token is found in storage, initiate the web-based OAuth flow.
|
// If no token is found in storage, initiate the web-based OAuth flow.
|
||||||
if ts.Token == nil {
|
if ts.Token == nil {
|
||||||
fmt.Printf("Could not load token from file, starting OAuth flow.\n")
|
fmt.Printf("Could not load token from file, starting OAuth flow.\n")
|
||||||
token, err = g.getTokenFromWeb(ctx, conf, noBrowser...)
|
token, err = g.getTokenFromWeb(ctx, conf, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get token from web: %w", err)
|
return nil, fmt.Errorf("failed to get token from web: %w", err)
|
||||||
}
|
}
|
||||||
@@ -205,15 +212,15 @@ func (g *GeminiAuth) createTokenStorage(ctx context.Context, config *oauth2.Conf
|
|||||||
// Parameters:
|
// Parameters:
|
||||||
// - ctx: The context for the HTTP client
|
// - ctx: The context for the HTTP client
|
||||||
// - config: The OAuth2 configuration
|
// - config: The OAuth2 configuration
|
||||||
// - noBrowser: Optional parameter to disable browser opening
|
// - opts: Optional parameters to customize browser and prompt behavior
|
||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *oauth2.Token: The OAuth2 token obtained from the authorization flow
|
// - *oauth2.Token: The OAuth2 token obtained from the authorization flow
|
||||||
// - error: An error if the token acquisition fails, nil otherwise
|
// - error: An error if the token acquisition fails, nil otherwise
|
||||||
func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config, noBrowser ...bool) (*oauth2.Token, error) {
|
func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config, opts *WebLoginOptions) (*oauth2.Token, error) {
|
||||||
// Use a channel to pass the authorization code from the HTTP handler to the main function.
|
// Use a channel to pass the authorization code from the HTTP handler to the main function.
|
||||||
codeChan := make(chan string)
|
codeChan := make(chan string, 1)
|
||||||
errChan := make(chan error)
|
errChan := make(chan error, 1)
|
||||||
|
|
||||||
// Create a new HTTP server with its own multiplexer.
|
// Create a new HTTP server with its own multiplexer.
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -223,17 +230,26 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
|||||||
mux.HandleFunc("/oauth2callback", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/oauth2callback", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := r.URL.Query().Get("error"); err != "" {
|
if err := r.URL.Query().Get("error"); err != "" {
|
||||||
_, _ = fmt.Fprintf(w, "Authentication failed: %s", err)
|
_, _ = fmt.Fprintf(w, "Authentication failed: %s", err)
|
||||||
errChan <- fmt.Errorf("authentication failed via callback: %s", err)
|
select {
|
||||||
|
case errChan <- fmt.Errorf("authentication failed via callback: %s", err):
|
||||||
|
default:
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
code := r.URL.Query().Get("code")
|
code := r.URL.Query().Get("code")
|
||||||
if code == "" {
|
if code == "" {
|
||||||
_, _ = fmt.Fprint(w, "Authentication failed: code not found.")
|
_, _ = fmt.Fprint(w, "Authentication failed: code not found.")
|
||||||
errChan <- fmt.Errorf("code not found in callback")
|
select {
|
||||||
|
case errChan <- fmt.Errorf("code not found in callback"):
|
||||||
|
default:
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = fmt.Fprint(w, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
|
_, _ = fmt.Fprint(w, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
|
||||||
codeChan <- code
|
select {
|
||||||
|
case codeChan <- code:
|
||||||
|
default:
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Start the server in a goroutine.
|
// Start the server in a goroutine.
|
||||||
@@ -250,7 +266,12 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
|||||||
// Open the authorization URL in the user's browser.
|
// Open the authorization URL in the user's browser.
|
||||||
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
|
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
|
||||||
|
|
||||||
if len(noBrowser) == 1 && !noBrowser[0] {
|
noBrowser := false
|
||||||
|
if opts != nil {
|
||||||
|
noBrowser = opts.NoBrowser
|
||||||
|
}
|
||||||
|
|
||||||
|
if !noBrowser {
|
||||||
fmt.Println("Opening browser for authentication...")
|
fmt.Println("Opening browser for authentication...")
|
||||||
|
|
||||||
// Check if browser is available
|
// Check if browser is available
|
||||||
@@ -281,13 +302,60 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config,
|
|||||||
|
|
||||||
// Wait for the authorization code or an error.
|
// Wait for the authorization code or an error.
|
||||||
var authCode string
|
var authCode string
|
||||||
select {
|
timeoutTimer := time.NewTimer(5 * time.Minute)
|
||||||
case code := <-codeChan:
|
defer timeoutTimer.Stop()
|
||||||
authCode = code
|
|
||||||
case err := <-errChan:
|
var manualPromptTimer *time.Timer
|
||||||
return nil, err
|
var manualPromptC <-chan time.Time
|
||||||
case <-time.After(5 * time.Minute): // Timeout
|
if opts != nil && opts.Prompt != nil {
|
||||||
return nil, fmt.Errorf("oauth flow timed out")
|
manualPromptTimer = time.NewTimer(15 * time.Second)
|
||||||
|
manualPromptC = manualPromptTimer.C
|
||||||
|
defer manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForCallback:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case code := <-codeChan:
|
||||||
|
authCode = code
|
||||||
|
break waitForCallback
|
||||||
|
case err := <-errChan:
|
||||||
|
return nil, err
|
||||||
|
case <-manualPromptC:
|
||||||
|
manualPromptC = nil
|
||||||
|
if manualPromptTimer != nil {
|
||||||
|
manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case code := <-codeChan:
|
||||||
|
authCode = code
|
||||||
|
break waitForCallback
|
||||||
|
case err := <-errChan:
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
input, err := opts.Prompt("Paste the Gemini callback URL (or press Enter to keep waiting): ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed, err := misc.ParseOAuthCallback(input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if parsed == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if parsed.Error != "" {
|
||||||
|
return nil, fmt.Errorf("authentication failed via callback: %s", parsed.Error)
|
||||||
|
}
|
||||||
|
if parsed.Code == "" {
|
||||||
|
return nil, fmt.Errorf("code not found in callback")
|
||||||
|
}
|
||||||
|
authCode = parsed.Code
|
||||||
|
break waitForCallback
|
||||||
|
case <-timeoutTimer.C:
|
||||||
|
return nil, fmt.Errorf("oauth flow timed out")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shutdown the server.
|
// Shutdown the server.
|
||||||
|
|||||||
@@ -24,12 +24,17 @@ func DoClaudeLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
options = &LoginOptions{}
|
options = &LoginOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
promptFn = defaultProjectPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
manager := newAuthManager()
|
manager := newAuthManager()
|
||||||
|
|
||||||
authOpts := &sdkAuth.LoginOptions{
|
authOpts := &sdkAuth.LoginOptions{
|
||||||
NoBrowser: options.NoBrowser,
|
NoBrowser: options.NoBrowser,
|
||||||
Metadata: map[string]string{},
|
Metadata: map[string]string{},
|
||||||
Prompt: options.Prompt,
|
Prompt: promptFn,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, savedPath, err := manager.Login(context.Background(), "claude", cfg, authOpts)
|
_, savedPath, err := manager.Login(context.Background(), "claude", cfg, authOpts)
|
||||||
|
|||||||
@@ -15,11 +15,16 @@ func DoAntigravityLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
options = &LoginOptions{}
|
options = &LoginOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
promptFn = defaultProjectPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
manager := newAuthManager()
|
manager := newAuthManager()
|
||||||
authOpts := &sdkAuth.LoginOptions{
|
authOpts := &sdkAuth.LoginOptions{
|
||||||
NoBrowser: options.NoBrowser,
|
NoBrowser: options.NoBrowser,
|
||||||
Metadata: map[string]string{},
|
Metadata: map[string]string{},
|
||||||
Prompt: options.Prompt,
|
Prompt: promptFn,
|
||||||
}
|
}
|
||||||
|
|
||||||
record, savedPath, err := manager.Login(context.Background(), "antigravity", cfg, authOpts)
|
record, savedPath, err := manager.Login(context.Background(), "antigravity", cfg, authOpts)
|
||||||
|
|||||||
@@ -20,13 +20,7 @@ func DoIFlowLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
|
|
||||||
promptFn := options.Prompt
|
promptFn := options.Prompt
|
||||||
if promptFn == nil {
|
if promptFn == nil {
|
||||||
promptFn = func(prompt string) (string, error) {
|
promptFn = defaultProjectPrompt()
|
||||||
fmt.Println()
|
|
||||||
fmt.Println(prompt)
|
|
||||||
var value string
|
|
||||||
_, err := fmt.Scanln(&value)
|
|
||||||
return value, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authOpts := &sdkAuth.LoginOptions{
|
authOpts := &sdkAuth.LoginOptions{
|
||||||
|
|||||||
@@ -55,11 +55,22 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
promptFn = defaultProjectPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedProjectID := strings.TrimSpace(projectID)
|
||||||
|
callbackPrompt := promptFn
|
||||||
|
if trimmedProjectID == "" {
|
||||||
|
callbackPrompt = nil
|
||||||
|
}
|
||||||
|
|
||||||
loginOpts := &sdkAuth.LoginOptions{
|
loginOpts := &sdkAuth.LoginOptions{
|
||||||
NoBrowser: options.NoBrowser,
|
NoBrowser: options.NoBrowser,
|
||||||
ProjectID: strings.TrimSpace(projectID),
|
ProjectID: trimmedProjectID,
|
||||||
Metadata: map[string]string{},
|
Metadata: map[string]string{},
|
||||||
Prompt: options.Prompt,
|
Prompt: callbackPrompt,
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticator := sdkAuth.NewGeminiAuthenticator()
|
authenticator := sdkAuth.NewGeminiAuthenticator()
|
||||||
@@ -76,7 +87,10 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
geminiAuth := gemini.NewGeminiAuth()
|
geminiAuth := gemini.NewGeminiAuth()
|
||||||
httpClient, errClient := geminiAuth.GetAuthenticatedClient(ctx, storage, cfg, options.NoBrowser)
|
httpClient, errClient := geminiAuth.GetAuthenticatedClient(ctx, storage, cfg, &gemini.WebLoginOptions{
|
||||||
|
NoBrowser: options.NoBrowser,
|
||||||
|
Prompt: callbackPrompt,
|
||||||
|
})
|
||||||
if errClient != nil {
|
if errClient != nil {
|
||||||
log.Errorf("Gemini authentication failed: %v", errClient)
|
log.Errorf("Gemini authentication failed: %v", errClient)
|
||||||
return
|
return
|
||||||
@@ -90,12 +104,7 @@ func DoLogin(cfg *config.Config, projectID string, options *LoginOptions) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
promptFn := options.Prompt
|
selectedProjectID := promptForProjectSelection(projects, trimmedProjectID, promptFn)
|
||||||
if promptFn == nil {
|
|
||||||
promptFn = defaultProjectPrompt()
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedProjectID := promptForProjectSelection(projects, strings.TrimSpace(projectID), promptFn)
|
|
||||||
projectSelections, errSelection := resolveProjectSelections(selectedProjectID, projects)
|
projectSelections, errSelection := resolveProjectSelections(selectedProjectID, projects)
|
||||||
if errSelection != nil {
|
if errSelection != nil {
|
||||||
log.Errorf("Invalid project selection: %v", errSelection)
|
log.Errorf("Invalid project selection: %v", errSelection)
|
||||||
|
|||||||
@@ -35,12 +35,17 @@ func DoCodexLogin(cfg *config.Config, options *LoginOptions) {
|
|||||||
options = &LoginOptions{}
|
options = &LoginOptions{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promptFn := options.Prompt
|
||||||
|
if promptFn == nil {
|
||||||
|
promptFn = defaultProjectPrompt()
|
||||||
|
}
|
||||||
|
|
||||||
manager := newAuthManager()
|
manager := newAuthManager()
|
||||||
|
|
||||||
authOpts := &sdkAuth.LoginOptions{
|
authOpts := &sdkAuth.LoginOptions{
|
||||||
NoBrowser: options.NoBrowser,
|
NoBrowser: options.NoBrowser,
|
||||||
Metadata: map[string]string{},
|
Metadata: map[string]string{},
|
||||||
Prompt: options.Prompt,
|
Prompt: promptFn,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, savedPath, err := manager.Login(context.Background(), "codex", cfg, authOpts)
|
_, savedPath, err := manager.Login(context.Background(), "codex", cfg, authOpts)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GenerateRandomState generates a cryptographically secure random state parameter
|
// GenerateRandomState generates a cryptographically secure random state parameter
|
||||||
@@ -19,3 +21,83 @@ func GenerateRandomState() (string, error) {
|
|||||||
}
|
}
|
||||||
return hex.EncodeToString(bytes), nil
|
return hex.EncodeToString(bytes), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OAuthCallback captures the parsed OAuth callback parameters.
|
||||||
|
type OAuthCallback struct {
|
||||||
|
Code string
|
||||||
|
State string
|
||||||
|
Error string
|
||||||
|
ErrorDescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseOAuthCallback extracts OAuth parameters from a callback URL.
|
||||||
|
// It returns nil when the input is empty.
|
||||||
|
func ParseOAuthCallback(input string) (*OAuthCallback, error) {
|
||||||
|
trimmed := strings.TrimSpace(input)
|
||||||
|
if trimmed == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
candidate := trimmed
|
||||||
|
if !strings.Contains(candidate, "://") {
|
||||||
|
if strings.HasPrefix(candidate, "?") {
|
||||||
|
candidate = "http://localhost" + candidate
|
||||||
|
} else if strings.ContainsAny(candidate, "/?#") || strings.Contains(candidate, ":") {
|
||||||
|
candidate = "http://" + candidate
|
||||||
|
} else if strings.Contains(candidate, "=") {
|
||||||
|
candidate = "http://localhost/?" + candidate
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("invalid callback URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedURL, err := url.Parse(candidate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
query := parsedURL.Query()
|
||||||
|
code := strings.TrimSpace(query.Get("code"))
|
||||||
|
state := strings.TrimSpace(query.Get("state"))
|
||||||
|
errCode := strings.TrimSpace(query.Get("error"))
|
||||||
|
errDesc := strings.TrimSpace(query.Get("error_description"))
|
||||||
|
|
||||||
|
if parsedURL.Fragment != "" {
|
||||||
|
if fragQuery, errFrag := url.ParseQuery(parsedURL.Fragment); errFrag == nil {
|
||||||
|
if code == "" {
|
||||||
|
code = strings.TrimSpace(fragQuery.Get("code"))
|
||||||
|
}
|
||||||
|
if state == "" {
|
||||||
|
state = strings.TrimSpace(fragQuery.Get("state"))
|
||||||
|
}
|
||||||
|
if errCode == "" {
|
||||||
|
errCode = strings.TrimSpace(fragQuery.Get("error"))
|
||||||
|
}
|
||||||
|
if errDesc == "" {
|
||||||
|
errDesc = strings.TrimSpace(fragQuery.Get("error_description"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if code != "" && state == "" && strings.Contains(code, "#") {
|
||||||
|
parts := strings.SplitN(code, "#", 2)
|
||||||
|
code = parts[0]
|
||||||
|
state = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if errCode == "" && errDesc != "" {
|
||||||
|
errCode = errDesc
|
||||||
|
errDesc = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if code == "" && errCode == "" {
|
||||||
|
return nil, fmt.Errorf("callback URL missing code")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &OAuthCallback{
|
||||||
|
Code: code,
|
||||||
|
State: state,
|
||||||
|
Error: errCode,
|
||||||
|
ErrorDescription: errDesc,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,11 +99,54 @@ func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, o
|
|||||||
fmt.Println("Waiting for antigravity authentication callback...")
|
fmt.Println("Waiting for antigravity authentication callback...")
|
||||||
|
|
||||||
var cbRes callbackResult
|
var cbRes callbackResult
|
||||||
select {
|
timeoutTimer := time.NewTimer(5 * time.Minute)
|
||||||
case res := <-cbChan:
|
defer timeoutTimer.Stop()
|
||||||
cbRes = res
|
|
||||||
case <-time.After(5 * time.Minute):
|
var manualPromptTimer *time.Timer
|
||||||
return nil, fmt.Errorf("antigravity: authentication timed out")
|
var manualPromptC <-chan time.Time
|
||||||
|
if opts.Prompt != nil {
|
||||||
|
manualPromptTimer = time.NewTimer(15 * time.Second)
|
||||||
|
manualPromptC = manualPromptTimer.C
|
||||||
|
defer manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForCallback:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case res := <-cbChan:
|
||||||
|
cbRes = res
|
||||||
|
break waitForCallback
|
||||||
|
case <-manualPromptC:
|
||||||
|
manualPromptC = nil
|
||||||
|
if manualPromptTimer != nil {
|
||||||
|
manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case res := <-cbChan:
|
||||||
|
cbRes = res
|
||||||
|
break waitForCallback
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ")
|
||||||
|
if errPrompt != nil {
|
||||||
|
return nil, errPrompt
|
||||||
|
}
|
||||||
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
|
if errParse != nil {
|
||||||
|
return nil, errParse
|
||||||
|
}
|
||||||
|
if parsed == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cbRes = callbackResult{
|
||||||
|
Code: parsed.Code,
|
||||||
|
State: parsed.State,
|
||||||
|
Error: parsed.Error,
|
||||||
|
}
|
||||||
|
break waitForCallback
|
||||||
|
case <-timeoutTimer.C:
|
||||||
|
return nil, fmt.Errorf("antigravity: authentication timed out")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cbRes.Error != "" {
|
if cbRes.Error != "" {
|
||||||
|
|||||||
@@ -98,16 +98,76 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
|||||||
|
|
||||||
fmt.Println("Waiting for Claude authentication callback...")
|
fmt.Println("Waiting for Claude authentication callback...")
|
||||||
|
|
||||||
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
callbackCh := make(chan *claude.OAuthResult, 1)
|
||||||
if err != nil {
|
callbackErrCh := make(chan error, 1)
|
||||||
if strings.Contains(err.Error(), "timeout") {
|
manualDescription := ""
|
||||||
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
|
|
||||||
|
go func() {
|
||||||
|
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
|
||||||
|
if errWait != nil {
|
||||||
|
callbackErrCh <- errWait
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callbackCh <- result
|
||||||
|
}()
|
||||||
|
|
||||||
|
var result *claude.OAuthResult
|
||||||
|
var manualPromptTimer *time.Timer
|
||||||
|
var manualPromptC <-chan time.Time
|
||||||
|
if opts.Prompt != nil {
|
||||||
|
manualPromptTimer = time.NewTimer(15 * time.Second)
|
||||||
|
manualPromptC = manualPromptTimer.C
|
||||||
|
defer manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForCallback:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case result = <-callbackCh:
|
||||||
|
break waitForCallback
|
||||||
|
case err = <-callbackErrCh:
|
||||||
|
if strings.Contains(err.Error(), "timeout") {
|
||||||
|
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
case <-manualPromptC:
|
||||||
|
manualPromptC = nil
|
||||||
|
if manualPromptTimer != nil {
|
||||||
|
manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case result = <-callbackCh:
|
||||||
|
break waitForCallback
|
||||||
|
case err = <-callbackErrCh:
|
||||||
|
if strings.Contains(err.Error(), "timeout") {
|
||||||
|
return nil, claude.NewAuthenticationError(claude.ErrCallbackTimeout, err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ")
|
||||||
|
if errPrompt != nil {
|
||||||
|
return nil, errPrompt
|
||||||
|
}
|
||||||
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
|
if errParse != nil {
|
||||||
|
return nil, errParse
|
||||||
|
}
|
||||||
|
if parsed == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
manualDescription = parsed.ErrorDescription
|
||||||
|
result = &claude.OAuthResult{
|
||||||
|
Code: parsed.Code,
|
||||||
|
State: parsed.State,
|
||||||
|
Error: parsed.Error,
|
||||||
|
}
|
||||||
|
break waitForCallback
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Error != "" {
|
if result.Error != "" {
|
||||||
return nil, claude.NewOAuthError(result.Error, "", http.StatusBadRequest)
|
return nil, claude.NewOAuthError(result.Error, manualDescription, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.State != state {
|
if result.State != state {
|
||||||
|
|||||||
@@ -97,16 +97,76 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
|
|
||||||
fmt.Println("Waiting for Codex authentication callback...")
|
fmt.Println("Waiting for Codex authentication callback...")
|
||||||
|
|
||||||
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
callbackCh := make(chan *codex.OAuthResult, 1)
|
||||||
if err != nil {
|
callbackErrCh := make(chan error, 1)
|
||||||
if strings.Contains(err.Error(), "timeout") {
|
manualDescription := ""
|
||||||
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
|
|
||||||
|
go func() {
|
||||||
|
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
|
||||||
|
if errWait != nil {
|
||||||
|
callbackErrCh <- errWait
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callbackCh <- result
|
||||||
|
}()
|
||||||
|
|
||||||
|
var result *codex.OAuthResult
|
||||||
|
var manualPromptTimer *time.Timer
|
||||||
|
var manualPromptC <-chan time.Time
|
||||||
|
if opts.Prompt != nil {
|
||||||
|
manualPromptTimer = time.NewTimer(15 * time.Second)
|
||||||
|
manualPromptC = manualPromptTimer.C
|
||||||
|
defer manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForCallback:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case result = <-callbackCh:
|
||||||
|
break waitForCallback
|
||||||
|
case err = <-callbackErrCh:
|
||||||
|
if strings.Contains(err.Error(), "timeout") {
|
||||||
|
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
case <-manualPromptC:
|
||||||
|
manualPromptC = nil
|
||||||
|
if manualPromptTimer != nil {
|
||||||
|
manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case result = <-callbackCh:
|
||||||
|
break waitForCallback
|
||||||
|
case err = <-callbackErrCh:
|
||||||
|
if strings.Contains(err.Error(), "timeout") {
|
||||||
|
return nil, codex.NewAuthenticationError(codex.ErrCallbackTimeout, err)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ")
|
||||||
|
if errPrompt != nil {
|
||||||
|
return nil, errPrompt
|
||||||
|
}
|
||||||
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
|
if errParse != nil {
|
||||||
|
return nil, errParse
|
||||||
|
}
|
||||||
|
if parsed == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
manualDescription = parsed.ErrorDescription
|
||||||
|
result = &codex.OAuthResult{
|
||||||
|
Code: parsed.Code,
|
||||||
|
State: parsed.State,
|
||||||
|
Error: parsed.Error,
|
||||||
|
}
|
||||||
|
break waitForCallback
|
||||||
}
|
}
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.Error != "" {
|
if result.Error != "" {
|
||||||
return nil, codex.NewOAuthError(result.Error, "", http.StatusBadRequest)
|
return nil, codex.NewOAuthError(result.Error, manualDescription, http.StatusBadRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.State != state {
|
if result.State != state {
|
||||||
|
|||||||
@@ -44,7 +44,10 @@ func (a *GeminiAuthenticator) Login(ctx context.Context, cfg *config.Config, opt
|
|||||||
}
|
}
|
||||||
|
|
||||||
geminiAuth := gemini.NewGeminiAuth()
|
geminiAuth := gemini.NewGeminiAuth()
|
||||||
_, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, opts.NoBrowser)
|
_, err := geminiAuth.GetAuthenticatedClient(ctx, &ts, cfg, &gemini.WebLoginOptions{
|
||||||
|
NoBrowser: opts.NoBrowser,
|
||||||
|
Prompt: opts.Prompt,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("gemini authentication failed: %w", err)
|
return nil, fmt.Errorf("gemini authentication failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,9 +84,64 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts
|
|||||||
|
|
||||||
fmt.Println("Waiting for iFlow authentication callback...")
|
fmt.Println("Waiting for iFlow authentication callback...")
|
||||||
|
|
||||||
result, err := oauthServer.WaitForCallback(5 * time.Minute)
|
callbackCh := make(chan *iflow.OAuthResult, 1)
|
||||||
if err != nil {
|
callbackErrCh := make(chan error, 1)
|
||||||
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
|
|
||||||
|
go func() {
|
||||||
|
result, errWait := oauthServer.WaitForCallback(5 * time.Minute)
|
||||||
|
if errWait != nil {
|
||||||
|
callbackErrCh <- errWait
|
||||||
|
return
|
||||||
|
}
|
||||||
|
callbackCh <- result
|
||||||
|
}()
|
||||||
|
|
||||||
|
var result *iflow.OAuthResult
|
||||||
|
var manualPromptTimer *time.Timer
|
||||||
|
var manualPromptC <-chan time.Time
|
||||||
|
if opts.Prompt != nil {
|
||||||
|
manualPromptTimer = time.NewTimer(15 * time.Second)
|
||||||
|
manualPromptC = manualPromptTimer.C
|
||||||
|
defer manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForCallback:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case result = <-callbackCh:
|
||||||
|
break waitForCallback
|
||||||
|
case err = <-callbackErrCh:
|
||||||
|
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
|
||||||
|
case <-manualPromptC:
|
||||||
|
manualPromptC = nil
|
||||||
|
if manualPromptTimer != nil {
|
||||||
|
manualPromptTimer.Stop()
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case result = <-callbackCh:
|
||||||
|
break waitForCallback
|
||||||
|
case err = <-callbackErrCh:
|
||||||
|
return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err)
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ")
|
||||||
|
if errPrompt != nil {
|
||||||
|
return nil, errPrompt
|
||||||
|
}
|
||||||
|
parsed, errParse := misc.ParseOAuthCallback(input)
|
||||||
|
if errParse != nil {
|
||||||
|
return nil, errParse
|
||||||
|
}
|
||||||
|
if parsed == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = &iflow.OAuthResult{
|
||||||
|
Code: parsed.Code,
|
||||||
|
State: parsed.State,
|
||||||
|
Error: parsed.Error,
|
||||||
|
}
|
||||||
|
break waitForCallback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if result.Error != "" {
|
if result.Error != "" {
|
||||||
return nil, fmt.Errorf("iflow auth: provider returned error %s", result.Error)
|
return nil, fmt.Errorf("iflow auth: provider returned error %s", result.Error)
|
||||||
|
|||||||
Reference in New Issue
Block a user