// Package nginxconfig renders an opt-in Nginx gateway for the existing Gin routes. // Rendering is read-only: it never installs packages or writes system configuration. package nginxconfig import ( "fmt" "regexp" "sort" "strings" "github.com/gin-gonic/gin" "vpn-platform/backend/internal/tlsservice" ) var spaPages = []string{ "/", "/login", "/mfa-verify", "/dashboard", "/tunnels", "/tunnels/create", "/sessions", "/logs", "/ipsec-peer", "/users", "/vpn-users", "/settings", "/identity", "/plugins", "/nodes", "/favicon.svg", } const upstream = " proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto https;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_pass http://127.0.0.1:18080;\n" func validRoute(path string) bool { return strings.HasPrefix(path, "/api/") && !strings.ContainsAny(path, "\n\r{};\\") } func routePattern(path string) string { segments := strings.Split(path, "/") for i, segment := range segments { if strings.HasPrefix(segment, ":") { segments[i] = "[^/]+" } else { segments[i] = regexp.QuoteMeta(segment) } } return "^" + strings.Join(segments, "/") + "$" } // Render returns the HTTP-01 bootstrap site and the HTTPS site separately. // Enable the second site only after Certbot has issued the certificate. func Render(domain string, routes gin.RoutesInfo) (httpSite, httpsSite string, err error) { return RenderWithAuth(domain, routes, 0) } // RenderWithAuth optionally exposes a single local OpenVPN browser-auth service. // All other unknown paths retain the existing 444 catch-all. func RenderWithAuth(domain string, routes gin.RoutesInfo, authPort int) (httpSite, httpsSite string, err error) { domain = strings.ToLower(strings.TrimSpace(domain)) if !tlsservice.ValidDomain(domain) { return "", "", fmt.Errorf("请输入有效的单个完整域名") } if authPort != 0 && (authPort < 1024 || authPort > 65535) { return "", "", fmt.Errorf("认证端口无效") } httpSite = fmt.Sprintf(`# Install this HTTP site first. Certbot webroot: /var/lib/opsx-vpn/acme server { listen 80 default_server; server_name _; return 444; } server { listen 80; server_name %s; location ^~ /.well-known/acme-challenge/ { root /var/lib/opsx-vpn/acme; default_type text/plain; try_files $uri =404; } location / { return 444; } } `, domain) var b strings.Builder fmt.Fprintf(&b, `# Add only after the certificate exists. Keep the HTTP site for renewals. server { listen 443 ssl default_server; server_name _; ssl_reject_handshake on; return 444; } server { listen 443 ssl; server_name %s; ssl_certificate /etc/letsencrypt/live/%s/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/%s/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; server_tokens off; if ($host != %s) { return 444; } `, domain, domain, domain, domain) for _, path := range spaPages { fmt.Fprintf(&b, " location = %s {\n%s }\n", path, upstream) } b.WriteString(" location ~ ^/tunnels/[0-9]+/edit$ {\n" + upstream + " }\n") b.WriteString(" location ~ ^/assets/[A-Za-z0-9_.-]+\\.(js|css|svg|png|woff2)$ {\n" + upstream + " }\n") // Public issuer endpoints are fixed: unknown /id/* requests still get 444. for _, path := range []string{"/id/.well-known/openid-configuration", "/id/jwks", "/id/authorize", "/id/feishu/callback", "/id/token", "/id/userinfo"} { fmt.Fprintf(&b, " location = %s {\n%s }\n", path, upstream) } if authPort != 0 { fmt.Fprintf(&b, " location ^~ /oauth2/ {\n proxy_set_header Host $host;\n proxy_set_header X-Forwarded-Proto https;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_pass http://127.0.0.1:%d;\n }\n", authPort) } seen := make(map[string]bool) var paths []string for _, route := range routes { // The issuer locations above are deliberately enumerated. Gin also // reports them as routes; emitting them twice makes nginx -t fail. if strings.HasPrefix(route.Path, "/id/") { continue } if !validRoute(route.Path) || seen[route.Path] { continue } seen[route.Path] = true paths = append(paths, route.Path) } sort.Strings(paths) for _, path := range paths { if strings.Contains(path, ":") { fmt.Fprintf(&b, " location ~ %s {\n%s }\n", routePattern(path), upstream) } else { fmt.Fprintf(&b, " location = %s {\n%s }\n", path, upstream) } } b.WriteString(" location / { return 444; }\n}\n") return httpSite, b.String(), nil }