package api import ( "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "vpn-platform/backend/internal/model" "vpn-platform/backend/internal/nginxconfig" "vpn-platform/backend/internal/nginxgateway" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/store" "vpn-platform/backend/internal/tlsservice" ) // Only a single OpenVPN WebAuth instance may own /oauth2/ on the management // hostname. Never route an arbitrary user-supplied address to a local port. func authGatewayPort(publicBase string) (int, error) { publicBase = strings.TrimRight(strings.TrimSpace(publicBase), "/") var tunnels []model.Tunnel if err := store.DB.Where("protocol = ? AND state = ?", "openvpn", "running").Find(&tunnels).Error; err != nil { return 0, err } port := 0 for _, tunnel := range tunnels { cfg, err := store.TunnelConfig(&tunnel) if err != nil { return 0, err } if service.Str(cfg, "auth_mode") != "oidc" || !strings.EqualFold(strings.TrimRight(service.Str(cfg, "oidc_base_url"), "/"), publicBase) { continue } if port != 0 { return 0, &gatewayConflict{} } port = int(service.Num(cfg, "oidc_http_port", 0)) } return port, nil } func checkAuthGatewayChange(oldBase, newBase string) error { if oldBase == "" || oldBase == newBase { return nil } port, err := authGatewayPort(oldBase) if err != nil { return err } if port != 0 { return &activeAuthEntry{} } return nil } type activeAuthEntry struct{} func (*activeAuthEntry) Error() string { return "运行中的 OIDC 专线仍使用旧公网地址,请先停止或迁移专线" } type gatewayConflict struct{} func (*gatewayConflict) Error() string { return "当前域名存在多条运行中的 OIDC 专线,请先保留一条" } // RefreshManagedGatewayOnStart keeps the installed route allowlist in sync // with the binary after upgrades. It only touches this application's managed // Nginx site; RefreshAuth validates it and rolls back on failed reloads. func (s *Server) RefreshManagedGatewayOnStart() error { s.gatewayMu.Lock() defer s.gatewayMu.Unlock() status := tlsservice.Status() if status.Mode != "nginx" || status.RestartRequired { return nil } scheme := status.Scheme if scheme == "" { scheme = "https" } stage := nginxgateway.Stage() if stage != "none" { actual, err := nginxgateway.ManagedEntry() if err != nil { return err } actualBase := tlsservice.PublicBaseURL(actual.Scheme, actual.Domain, actual.Port) if actualBase != status.ConfiguredPublicBase { if err := tlsservice.UseManagedNginxEntry(service.BaseDir(), actual.Scheme, actual.Domain, actual.Port); err != nil { return err } return fmt.Errorf("已保存的公网入口 %s 与运行中的 Nginx %s 不同;保留当前站点,请在设置页确认后保存", status.ConfiguredPublicBase, actualBase) } } if status.Domain == "" || status.PublicBase == "" { return nil } if scheme == "https" && (!status.Valid || stage != "https") { return nil } if scheme == "http" && stage == "none" { return nil } port, err := authGatewayPort(status.PublicBase) if err != nil { return err } return nginxgateway.RefreshAuthEntry(nginxconfig.RenderOptions{ Domain: status.Domain, Scheme: scheme, Port: status.Port, }, s.router.Routes(), port) } func (s *Server) syncAuthGatewayForTunnel(t *model.Tunnel) error { if t.Protocol != "openvpn" || s.router == nil { return nil } return s.RefreshManagedGatewayOnStart() } func (s *Server) identityGateway(c *gin.Context) { s.gatewayMu.Lock() defer s.gatewayMu.Unlock() status := tlsservice.Status() scheme := status.Scheme if scheme == "" { scheme = "https" } if status.Mode != "nginx" || status.RestartRequired || status.Domain == "" || status.PublicBase == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "请先完成公网入口配置"}) return } stage := nginxgateway.Stage() if scheme == "https" && (!status.Valid || stage != "https") { c.JSON(http.StatusBadRequest, gin.H{"error": "请先完成 Nginx HTTPS 入口配置"}) return } if scheme == "http" && stage == "none" { c.JSON(http.StatusBadRequest, gin.H{"error": "请先启用 HTTP 公网入口"}) return } port, err := authGatewayPort(status.PublicBase) if err != nil { c.JSON(400, gin.H{"error": err.Error()}) return } if err := nginxgateway.RefreshAuthEntry(nginxconfig.RenderOptions{ Domain: status.Domain, Scheme: scheme, Port: status.Port, }, s.router.Routes(), port); err != nil { c.JSON(500, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "identity_gateway_updated", status.PublicBase, "") c.JSON(200, gin.H{"ok": true, "identity_route": true, "auth_route": port != 0, "auth_port": port, "public_base": status.PublicBase}) }