package api import ( "encoding/json" "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "vpn-platform/backend/internal/nginxconfig" "vpn-platform/backend/internal/nginxgateway" "vpn-platform/backend/internal/nginxsetup" "vpn-platform/backend/internal/service" "vpn-platform/backend/internal/store" "vpn-platform/backend/internal/tlsservice" ) func (s *Server) nginxSetupStatus(c *gin.Context) { status := nginxsetup.Inspect() c.JSON(http.StatusOK, gin.H{"os": status.OS, "version": status.Version, "package_manager": status.PackageManager, "supported": status.Supported, "root": status.Root, "nginx_installed": status.Nginx, "certbot_installed": status.Certbot, "webroot_ready": status.Webroot, "running": status.Running || nginxgateway.Running(), "site_stage": nginxgateway.Stage()}) } func (s *Server) activateNginx(c *gin.Context) { status := tlsservice.Status() if status.Mode != "nginx" || status.ConfiguredMode != "nginx" || status.ConfiguredDomain == "" { c.JSON(http.StatusPreconditionFailed, gin.H{"error": "请先启用 Nginx 模式并保存公网入口;切换入口模式需重启 opsx-vpn"}) return } scheme := status.ConfiguredScheme if scheme == "" { scheme = "https" } authPort, err := authGatewayPort(status.ConfiguredPublicBase) if err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } if !nginxgateway.Begin() { c.JSON(http.StatusConflict, gin.H{"error": "入口配置正在运行,请等待完成"}) return } defer nginxgateway.End() c.Header("Content-Type", "application/x-ndjson; charset=utf-8") c.Header("Cache-Control", "no-store") c.Header("X-Accel-Buffering", "no") c.Status(http.StatusOK) write := func(kind, message string) { payload, _ := json.Marshal(gin.H{"type": kind, "message": message}) fmt.Fprintf(c.Writer, "%s\n", payload) c.Writer.Flush() } if scheme == "http" { write("info", "启用 HTTP 公网入口(不申请证书)。\n") } else { write("info", "检查现有站点并启用 HTTP-01 验证。\n") } opts := nginxconfig.RenderOptions{Domain: status.ConfiguredDomain, Scheme: scheme, Port: status.ConfiguredPort} if err := nginxgateway.RunEntryWithAuth(opts, s.router.Routes(), authPort, func(message string) { write("output", message) }); err != nil { store.Audit(c.GetString("username"), "nginx_activate", "failed", err.Error()) write("error", err.Error()) return } if err := tlsservice.AdoptNginxConfiguration(service.BaseDir()); err != nil { write("error", "站点已更新,但状态同步失败:"+err.Error()) return } store.Audit(c.GetString("username"), "nginx_activate", "completed", status.ConfiguredPublicBase) write("done", "站点和入口已就绪。") } func (s *Server) initializeNginx(c *gin.Context) { status := nginxsetup.Inspect() if !status.Supported || !status.Root { c.JSON(http.StatusPreconditionFailed, gin.H{"error": "仅支持 root 运行的 Ubuntu / TencentOS Server 4"}) return } if !nginxsetup.Begin() { c.JSON(http.StatusConflict, gin.H{"error": "初始化正在运行,请等待完成"}) return } defer nginxsetup.End() c.Header("Content-Type", "application/x-ndjson; charset=utf-8") c.Header("Cache-Control", "no-store") c.Header("X-Accel-Buffering", "no") c.Status(http.StatusOK) write := func(kind, message string) { payload, _ := json.Marshal(gin.H{"type": kind, "message": message}) fmt.Fprintf(c.Writer, "%s\n", payload) c.Writer.Flush() } write("info", "开始初始化 Nginx / Certbot;此步骤不会启用 80/443 或签发证书。\n") err := nginxsetup.Run(func(message string) { write("output", message) }) if err != nil { store.Audit(c.GetString("username"), "nginx_setup", "failed", err.Error()) write("error", err.Error()) return } store.Audit(c.GetString("username"), "nginx_setup", "completed", "software and webroot ready") write("done", "初始化完成;继续配置 HTTP-01 站点与证书。") } func (s *Server) tlsStatus(c *gin.Context) { c.JSON(http.StatusOK, tlsservice.Status()) } func (s *Server) nginxPreview(c *gin.Context) { status := tlsservice.Status() if status.ConfiguredDomain == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "请先在页面保存公网入口"}) return } scheme := status.ConfiguredScheme if scheme == "" { scheme = "https" } httpSite, httpsSite, err := nginxconfig.RenderWithOptions(nginxconfig.RenderOptions{ Domain: status.ConfiguredDomain, Scheme: scheme, Port: status.ConfiguredPort, }, s.router.Routes(), 0) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{ "http_site": httpSite, "https_site": httpsSite, "public_base": status.ConfiguredPublicBase, "warning": "仅预览:保存并重启后按协议启用入口;认证域名须单独配置。", }) } func (s *Server) setTLSMode(c *gin.Context) { var req struct { Mode string `json:"mode"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请输入入口模式"}) return } if err := tlsservice.ConfigureMode(service.BaseDir(), req.Mode); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "tls_mode_configured", req.Mode, "需检查外部入口后重启") status := tlsservice.Status() c.JSON(http.StatusOK, gin.H{"status": status, "restart_required": status.RestartRequired}) } func (s *Server) setTLSDomain(c *gin.Context) { var req struct { Domain string `json:"domain"` Scheme string `json:"scheme"` Port *int `json:"port"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请输入公网入口配置"}) return } scheme := strings.ToLower(strings.TrimSpace(req.Scheme)) if scheme == "" { scheme = "https" } port := 0 if req.Port != nil { port = *req.Port } status := tlsservice.Status() newBase := tlsservice.PublicBaseURL(scheme, strings.ToLower(strings.TrimSpace(req.Domain)), port) if newBase == "" || port < 0 || port > 65535 { c.JSON(http.StatusBadRequest, gin.H{"error": "公网域名、协议或端口无效"}) return } // The Go listener cannot be rebound in place; Nginx owns its own sockets. if status.Mode == "nginx" && status.ConfiguredMode == "nginx" && nginxgateway.Stage() != "none" { s.gatewayMu.Lock() defer s.gatewayMu.Unlock() if err := checkAuthGatewayChange(status.PublicBase, newBase); err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } authPort, err := authGatewayPort(newBase) if err != nil { c.JSON(http.StatusConflict, gin.H{"error": err.Error()}) return } opts := nginxconfig.RenderOptions{Domain: req.Domain, Scheme: scheme, Port: port} err = nginxgateway.UpdateEntry(opts, status.ConfiguredDomain, s.router.Routes(), authPort, func() error { if err := tlsservice.ConfigurePublicEntry(service.BaseDir(), scheme, req.Domain, port); err != nil { return err } return tlsservice.AdoptNginxConfiguration(service.BaseDir()) }) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "tls_domain_applied", newBase, "Nginx 已重载") c.JSON(http.StatusOK, gin.H{"status": tlsservice.Status(), "restart_required": false, "applied": true}) return } if err := tlsservice.ConfigurePublicEntry(service.BaseDir(), scheme, req.Domain, port); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "tls_domain_configured", tlsservice.PublicBaseURL(scheme, strings.ToLower(strings.TrimSpace(req.Domain)), port), "需重启后生效") status = tlsservice.Status() c.JSON(http.StatusOK, gin.H{"status": status, "restart_required": status.RestartRequired, "site_activation_required": status.Mode == "nginx" && status.ConfiguredMode == "nginx" && nginxgateway.Stage() == "none"}) } func (s *Server) uploadTLSCertificate(c *gin.Context) { var req struct { CertificatePEM string `json:"certificate_pem"` PrivateKeyPEM string `json:"private_key_pem"` Host string `json:"host"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "请提交证书与私钥 PEM"}) return } status := tlsservice.Status() domain := strings.ToLower(strings.TrimSpace(req.Host)) if domain == "" { domain = status.ConfiguredDomain } if domain == "" { domain = status.Domain } if domain == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "请先填写公网域名"}) return } if domain != status.ConfiguredDomain && domain != status.Domain { c.JSON(http.StatusBadRequest, gin.H{"error": "证书域名必须与公网入口一致"}) return } apply := func() error { return nil } if status.Mode == "nginx" && status.ConfiguredMode == "nginx" && !status.RestartRequired && (status.ConfiguredScheme == "https" || status.Scheme == "https" || status.ConfiguredScheme == "") && nginxgateway.Stage() == "https" && s.router != nil { apply = func() error { s.gatewayMu.Lock() defer s.gatewayMu.Unlock() port := 0 if pb := tlsservice.Status().PublicBase; pb != "" { port, _ = authGatewayPort(pb) } return nginxgateway.RefreshAuthEntry(nginxconfig.RenderOptions{ Domain: domain, Scheme: "https", Port: status.ConfiguredPort, }, s.router.Routes(), port) } } if err := tlsservice.InstallUploadedCertificateWithApply(service.BaseDir(), domain, req.CertificatePEM, req.PrivateKeyPEM, apply); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } store.Audit(c.GetString("username"), "tls_certificate_uploaded", domain, "upload") c.JSON(http.StatusOK, gin.H{"ok": true, "status": tlsservice.Status()}) }