feat: configure HTTP/HTTPS public entry and managed certificates
This commit is contained in:
1 parent
35596cda9d
commit
88ea6b7854
21 files changed
+1309
-149
No files matched your search
@@ -40,9 +40,13 @@ func Stage() string {
|
||||
if err != nil {
|
||||
return "none"
|
||||
}
|
||||
if strings.Contains(string(data), "# Add only after the certificate exists.") {
|
||||
body := string(data)
|
||||
if strings.Contains(body, "# Add only after the certificate exists.") {
|
||||
return "https"
|
||||
}
|
||||
if strings.Contains(body, "# Plain HTTP public entry") || strings.Contains(body, "server_name ") {
|
||||
return "http"
|
||||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
@@ -59,23 +63,46 @@ func AuthRouteReady(port int) (bool, bool) {
|
||||
// RefreshAuth updates only the managed site after validation; a bad config is
|
||||
// rolled back before returning. It never provisions a certificate or domain.
|
||||
func RefreshAuth(domain string, routes gin.RoutesInfo, authPort int) error {
|
||||
status := tlsservice.Status()
|
||||
scheme := status.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
return RefreshAuthEntry(nginxconfig.RenderOptions{Domain: domain, Scheme: scheme, Port: status.Port}, routes, authPort)
|
||||
}
|
||||
|
||||
func RefreshAuthEntry(opts nginxconfig.RenderOptions, routes gin.RoutesInfo, authPort int) error {
|
||||
if Running() {
|
||||
return errors.New("公网入口操作正在进行")
|
||||
}
|
||||
if err := checkSites(); err != nil {
|
||||
scheme := strings.ToLower(strings.TrimSpace(opts.Scheme))
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
opts.Scheme = scheme
|
||||
if scheme == "http" {
|
||||
if err := checkSitesHTTP(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := checkSites(); err != nil {
|
||||
return err
|
||||
}
|
||||
if Stage() != "https" {
|
||||
stage := Stage()
|
||||
if scheme == "https" && stage != "https" {
|
||||
return errors.New("请先启用 HTTPS 站点")
|
||||
}
|
||||
if scheme == "http" && stage == "none" {
|
||||
return errors.New("请先启用公网入口站点")
|
||||
}
|
||||
previous, err := os.ReadFile(sitePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
domain := strings.ToLower(strings.TrimSpace(opts.Domain))
|
||||
if !strings.Contains(string(previous), "server_name "+domain+";") {
|
||||
return errors.New("现有站点域名与配置不匹配")
|
||||
}
|
||||
httpSite, httpsSite, err := nginxconfig.RenderWithAuth(domain, routes, authPort)
|
||||
httpSite, httpsSite, err := nginxconfig.RenderWithOptions(opts, routes, authPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -137,6 +164,33 @@ func writeSite(content string) error {
|
||||
return os.Rename(f.Name(), sitePath)
|
||||
}
|
||||
|
||||
func checkSitesHTTP() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("配置入口需要 root 权限")
|
||||
}
|
||||
state := nginxsetup.Inspect()
|
||||
if !state.Nginx {
|
||||
return errors.New("请先安装 Nginx")
|
||||
}
|
||||
entries, err := os.ReadDir(siteDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Name() != "default" && e.Name() != "opsx-vpn.conf" {
|
||||
return fmt.Errorf("发现其他启用站点 %s,请先由管理员检查冲突", e.Name())
|
||||
}
|
||||
}
|
||||
if entries, err := os.ReadDir("/etc/nginx/conf.d"); err == nil {
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".conf") {
|
||||
return fmt.Errorf("发现其他 Nginx 配置 %s,请先由管理员检查冲突", e.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSites() error {
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("配置入口需要 root 权限")
|
||||
@@ -179,21 +233,39 @@ func checkSites() error {
|
||||
}
|
||||
|
||||
func certificateExists(domain string) bool {
|
||||
path := filepath.Join("/etc/letsencrypt/live", domain, "fullchain.pem")
|
||||
_, err := os.Stat(path)
|
||||
fullchain, _, _ := tlsservice.CertificateFiles(domain)
|
||||
_, err := os.Stat(fullchain)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Run performs a reversible HTTP bootstrap, certificate issuance, then HTTPS
|
||||
// activation. On issuance errors the HTTP challenge site remains available for retry.
|
||||
func Run(domain string, routes gin.RoutesInfo, emit func(string)) error {
|
||||
return RunEntry(nginxconfig.RenderOptions{Domain: domain, Scheme: "https", Port: 0}, routes, emit)
|
||||
}
|
||||
|
||||
// RunEntry activates the public entry for the given scheme/port. HTTP skips ACME.
|
||||
func RunEntry(opts nginxconfig.RenderOptions, routes gin.RoutesInfo, emit func(string)) error {
|
||||
domain := strings.ToLower(strings.TrimSpace(opts.Domain))
|
||||
scheme := strings.ToLower(strings.TrimSpace(opts.Scheme))
|
||||
if scheme == "" {
|
||||
scheme = "https"
|
||||
}
|
||||
opts.Domain, opts.Scheme = domain, scheme
|
||||
if !tlsservice.ValidDomain(domain) {
|
||||
return errors.New("请先保存有效域名")
|
||||
}
|
||||
if err := checkSites(); err != nil {
|
||||
if !tlsservice.ValidScheme(scheme) {
|
||||
return errors.New("协议仅支持 http 或 https")
|
||||
}
|
||||
if scheme == "http" {
|
||||
if err := checkSitesHTTP(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err := checkSites(); err != nil {
|
||||
return err
|
||||
}
|
||||
httpSite, httpsSite, err := nginxconfig.Render(domain, routes)
|
||||
httpSite, httpsSite, err := nginxconfig.RenderWithOptions(opts, routes, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -209,7 +281,16 @@ func Run(domain string, routes gin.RoutesInfo, emit func(string)) error {
|
||||
return errors.New("已有其他域名的入口配置,请人工迁移证书与站点")
|
||||
}
|
||||
if readErr != nil {
|
||||
for _, port := range []string{"80", "443"} {
|
||||
ports := []string{fmt.Sprintf("%d", tlsservice.EffectivePort(scheme, opts.Port))}
|
||||
if scheme == "https" && tlsservice.EffectivePort(scheme, opts.Port) != 80 {
|
||||
ports = append([]string{"80"}, ports...)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, port := range ports {
|
||||
if seen[port] {
|
||||
continue
|
||||
}
|
||||
seen[port] = true
|
||||
listener, e := net.Listen("tcp", "0.0.0.0:"+port)
|
||||
if e != nil {
|
||||
return fmt.Errorf("端口 %s 已占用,请先检查监听服务: %w", port, e)
|
||||
@@ -251,6 +332,10 @@ func Run(domain string, routes gin.RoutesInfo, emit func(string)) error {
|
||||
rollback()
|
||||
return err
|
||||
}
|
||||
if scheme == "http" {
|
||||
emit("HTTP 公网入口已启用(未申请证书)。\n")
|
||||
return nil
|
||||
}
|
||||
emit("HTTP-01 验证站点已启用,开始签发证书。\n")
|
||||
if !certificateExists(domain) {
|
||||
if err := command(emit, 4*time.Minute, "certbot", "certonly", "--webroot", "-w", nginxsetup.Webroot,
|
||||
@@ -286,6 +371,9 @@ func Run(domain string, routes gin.RoutesInfo, emit func(string)) error {
|
||||
if err := os.WriteFile(hookPath, []byte(hook), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, _, source := tlsservice.CertificateFiles(domain); source == tlsservice.CertSourceHTTP01 {
|
||||
_ = tlsservice.MarkCertSource(tlsservice.DataDir(), tlsservice.CertSourceHTTP01)
|
||||
}
|
||||
emit("HTTPS 入口已启用;Certbot 续签成功后将自动重载 Nginx。\n")
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user