feat: configure HTTP/HTTPS public entry and managed certificates

This commit is contained in:
Codex committed 2026-10-26 14:06:41 +08:00
1 parent 35596cda9d
commit 88ea6b7854
21 files changed
+1309 -149

No files matched your search

+88 -11
View File
@@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
@@ -26,9 +27,13 @@ func (s *Server) nginxSetupStatus(c *gin.Context) {
func (s *Server) activateNginx(c *gin.Context) {
status := tlsservice.Status()
if status.Mode != "nginx" || status.ConfiguredMode != "nginx" || status.RestartRequired || status.ConfiguredDomain == "" {
c.JSON(http.StatusPreconditionFailed, gin.H{"error": "请先保存 Nginx 模式和域名,重启 opsx-vpn 后再启用站点"})
c.JSON(http.StatusPreconditionFailed, gin.H{"error": "请先保存 Nginx 模式和公网入口,重启 opsx-vpn 后再启用站点"})
return
}
scheme := status.ConfiguredScheme
if scheme == "" {
scheme = "https"
}
if !nginxgateway.Begin() {
c.JSON(http.StatusConflict, gin.H{"error": "入口配置正在运行,请等待完成"})
return
@@ -43,14 +48,19 @@ func (s *Server) activateNginx(c *gin.Context) {
fmt.Fprintf(c.Writer, "%s\n", payload)
c.Writer.Flush()
}
write("info", "检查现有站点并启用 HTTP-01 验证。\n")
if err := nginxgateway.Run(status.ConfiguredDomain, s.router.Routes(), func(message string) { write("output", message) }); err != nil {
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.RunEntry(opts, s.router.Routes(), func(message string) { write("output", message) }); err != nil {
store.Audit(c.GetString("username"), "nginx_activate", "failed", err.Error())
write("error", err.Error())
return
}
store.Audit(c.GetString("username"), "nginx_activate", "completed", status.ConfiguredDomain)
write("done", "站点和证书已就绪。")
store.Audit(c.GetString("username"), "nginx_activate", "completed", status.ConfiguredPublicBase)
write("done", "站点和入口已就绪。")
}
func (s *Server) initializeNginx(c *gin.Context) {
@@ -91,17 +101,24 @@ func (s *Server) tlsStatus(c *gin.Context) {
func (s *Server) nginxPreview(c *gin.Context) {
status := tlsservice.Status()
if status.ConfiguredDomain == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "请先在页面保存域名"})
c.JSON(http.StatusBadRequest, gin.H{"error": "请先在页面保存公网入口"})
return
}
httpSite, httpsSite, err := nginxconfig.Render(status.ConfiguredDomain, s.router.Routes())
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,
"warning": "仅预览:先安装 Nginx/Certbot,停用 Go 的 80/443 监听并签出证书;认证域名须单独配置。",
"public_base": status.ConfiguredPublicBase,
"warning": "仅预览:保存并重启后按协议启用入口;认证域名须单独配置。",
})
}
@@ -125,16 +142,76 @@ func (s *Server) setTLSMode(c *gin.Context) {
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": "请输入域名"})
c.JSON(http.StatusBadRequest, gin.H{"error": "请输入公网入口配置"})
return
}
if err := tlsservice.ConfigureDomain(service.BaseDir(), req.Domain); err != nil {
scheme := strings.ToLower(strings.TrimSpace(req.Scheme))
if scheme == "" {
scheme = "https"
}
port := 0
if req.Port != nil {
port = *req.Port
}
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", req.Domain, "需重启后生效")
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})
}
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()})
}