Files
opsx-vpn/backend/internal/nginxconfig/nginxconfig.go
T

129 lines
4.4 KiB
Go

// 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", "/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
}