mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
Introduce a centralized OAuth session store with TTL-based expiration to replace the previous simple map-based status tracking. Add a new /api/oauth/callback endpoint that allows remote clients to relay OAuth callback data back to the CLI proxy, enabling OAuth flows when the callback cannot reach the local machine directly. - Add oauth_sessions.go with thread-safe session store and validation - Add oauth_callback.go with POST handler for remote callback relay - Refactor auth_files.go to use new session management APIs - Register new callback route in server.go
101 lines
2.8 KiB
Go
101 lines
2.8 KiB
Go
package management
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type oauthCallbackRequest struct {
|
|
Provider string `json:"provider"`
|
|
RedirectURL string `json:"redirect_url"`
|
|
Code string `json:"code"`
|
|
State string `json:"state"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
func (h *Handler) PostOAuthCallback(c *gin.Context) {
|
|
if h == nil || h.cfg == nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "handler not initialized"})
|
|
return
|
|
}
|
|
|
|
var req oauthCallbackRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "invalid body"})
|
|
return
|
|
}
|
|
|
|
canonicalProvider, err := NormalizeOAuthProvider(req.Provider)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "unsupported provider"})
|
|
return
|
|
}
|
|
|
|
state := strings.TrimSpace(req.State)
|
|
code := strings.TrimSpace(req.Code)
|
|
errMsg := strings.TrimSpace(req.Error)
|
|
|
|
if rawRedirect := strings.TrimSpace(req.RedirectURL); rawRedirect != "" {
|
|
u, errParse := url.Parse(rawRedirect)
|
|
if errParse != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "invalid redirect_url"})
|
|
return
|
|
}
|
|
q := u.Query()
|
|
if state == "" {
|
|
state = strings.TrimSpace(q.Get("state"))
|
|
}
|
|
if code == "" {
|
|
code = strings.TrimSpace(q.Get("code"))
|
|
}
|
|
if errMsg == "" {
|
|
errMsg = strings.TrimSpace(q.Get("error"))
|
|
if errMsg == "" {
|
|
errMsg = strings.TrimSpace(q.Get("error_description"))
|
|
}
|
|
}
|
|
}
|
|
|
|
if state == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "state is required"})
|
|
return
|
|
}
|
|
if err := ValidateOAuthState(state); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "invalid state"})
|
|
return
|
|
}
|
|
if code == "" && errMsg == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "code or error is required"})
|
|
return
|
|
}
|
|
|
|
sessionProvider, sessionStatus, ok := GetOAuthSession(state)
|
|
if !ok {
|
|
c.JSON(http.StatusNotFound, gin.H{"status": "error", "error": "unknown or expired state"})
|
|
return
|
|
}
|
|
if sessionStatus != "" {
|
|
c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "oauth flow is not pending"})
|
|
return
|
|
}
|
|
if !strings.EqualFold(sessionProvider, canonicalProvider) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"status": "error", "error": "provider does not match state"})
|
|
return
|
|
}
|
|
|
|
if _, errWrite := WriteOAuthCallbackFileForPendingSession(h.cfg.AuthDir, canonicalProvider, state, code, errMsg); errWrite != nil {
|
|
if errors.Is(errWrite, errOAuthSessionNotPending) {
|
|
c.JSON(http.StatusConflict, gin.H{"status": "error", "error": "oauth flow is not pending"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusInternalServerError, gin.H{"status": "error", "error": "failed to persist oauth callback"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
|
}
|