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 }