220 lines
5.7 KiB
Go
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
|
|
}
|