vibe-proxy/backend/internal/api/handlers/management/auth_files_oauth_callback.go
2026-08-24 00:10:41 +02:00

220 lines
5.7 KiB
Go

package management
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
)
const (
anthropicCallbackPort = 54545
codexCallbackPort = 1455
)
type callbackForwarder struct {
provider string
server *http.Server
done chan struct{}
}
func isWebUIRequest(c *gin.Context) bool {
raw := strings.TrimSpace(c.Query("is_webui"))
if raw == "" {
return false
}
switch strings.ToLower(raw) {
case "1", "true", "yes", "on":
return true
default:
return false
}
}
func startCallbackForwarder(port int, provider, targetBase string) (*callbackForwarder, error) {
callbackForwardersMu.Lock()
prev := callbackForwarders[port]
if prev != nil {
delete(callbackForwarders, port)
}
callbackForwardersMu.Unlock()
if prev != nil {
stopForwarderInstance(port, prev)
}
addr := fmt.Sprintf("0.0.0.0:%d", port)
ln, err := net.Listen("tcp", addr)
if err != nil {
return nil, fmt.Errorf("failed to listen on %s: %w", addr, err)
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
target := targetBase
if raw := r.URL.RawQuery; raw != "" {
if strings.Contains(target, "?") {
target = target + "&" + raw
} else {
target = target + "?" + raw
}
}
w.Header().Set("Cache-Control", "no-store")
http.Redirect(w, r, target, http.StatusFound)
})
srv := &http.Server{
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
}
done := make(chan struct{})
go func() {
if errServe := srv.Serve(ln); errServe != nil && !errors.Is(errServe, http.ErrServerClosed) {
log.WithError(errServe).Warnf("callback forwarder for %s stopped unexpectedly", provider)
}
close(done)
}()
forwarder := &callbackForwarder{
provider: provider,
server: srv,
done: done,
}
callbackForwardersMu.Lock()
callbackForwarders[port] = forwarder
callbackForwardersMu.Unlock()
log.Infof("callback forwarder for %s listening on %s", provider, addr)
return forwarder, nil
}
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) {
if forwarder == nil || forwarder.server == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := forwarder.server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.WithError(err).Warnf("failed to shut down callback forwarder on port %d", port)
}
select {
case <-forwarder.done:
case <-time.After(2 * time.Second):
}
log.Infof("callback forwarder on port %d stopped", port)
}
func (h *Handler) managementCallbackURL(path string) (string, error) {
if h == nil || h.cfg == nil || h.cfg.Port <= 0 {
return "", fmt.Errorf("server port is not configured")
}
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
scheme := "http"
if h.cfg.TLS.Enable {
scheme = "https"
}
return fmt.Sprintf("%s://127.0.0.1:%d%s", scheme, h.cfg.Port, path), nil
}
func pluginAuthProviderFromPath(path string) (string, bool) {
path = strings.TrimSpace(path)
const prefix = "/v0/management/"
const suffix = "-auth-url"
if !strings.HasPrefix(path, prefix) || !strings.HasSuffix(path, suffix) {
return "", false
}
provider := strings.TrimSuffix(strings.TrimPrefix(path, prefix), suffix)
provider = strings.ToLower(strings.TrimSpace(provider))
if provider == "" {
return "", false
}
for _, r := range provider {
switch {
case r >= 'a' && r <= 'z':
case r >= '0' && r <= '9':
case r == '-':
default:
return "", false
}
}
return provider, true
}
func (h *Handler) ServePluginAuthURL(c *gin.Context) bool {
if h == nil || c == nil || c.Request == nil || c.Request.URL == nil {
return false
}
h.mu.Lock()
host := h.pluginHost
h.mu.Unlock()
if host == nil {
return false
}
provider, ok := pluginAuthProviderFromPath(c.Request.URL.Path)
if !ok || !host.HasAuthProvider(provider) {
return false
}
ctx := PopulateAuthContext(context.Background(), c)
baseURL, errBaseURL := h.managementCallbackURL("/v0/management/oauth-callback")
if errBaseURL != nil {
log.WithError(errBaseURL).Error("failed to compute plugin auth callback URL")
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization url"})
return true
}
resp, handled, errStart := host.StartLogin(ctx, provider, baseURL)
if !handled {
return false
}
if errStart != nil {
log.WithError(errStart).Error("failed to start plugin auth login")
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate authorization url"})
return true
}
state := strings.TrimSpace(resp.State)
if state == "" {
log.WithField("provider", provider).Error("plugin auth provider returned empty state")
c.JSON(http.StatusBadGateway, gin.H{"error": "invalid oauth state"})
return true
}
if errState := ValidateOAuthState(state); errState != nil {
log.WithError(errState).WithField("provider", provider).Error("plugin auth provider returned invalid state")
c.JSON(http.StatusBadGateway, gin.H{"error": "invalid oauth state"})
return true
}
if errRegister := RegisterPluginOAuthSession(state, provider, resp.Metadata); errRegister != nil {
log.WithError(errRegister).WithField("provider", provider).Error("failed to register plugin oauth session")
c.JSON(http.StatusBadGateway, gin.H{"error": "failed to generate authorization url"})
return true
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "url": resp.URL, "state": state})
return true
}