Files
opsx-vpn/backend/main.go
T
opsx-tools 7de1f70905 fix: 静态资源 Cache-Control 防止浏览器缓存旧 index.html 导致白屏
- assets/* 设为 immutable 长期缓存(文件名含 hash,内容变更即换名)
- index.html 和 SPA fallback 设为 no-cache(每次重新验证)
- 彻底解决 MIME type 错误白屏问题
2026-07-23 12:59:11 +08:00

118 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// VPN 管理平台 —— 单二进制交付(前端静态资源内嵌) // Logan.Li
package main
import (
"embed"
"io/fs"
"log"
"net/http"
"os"
"strings"
"time"
"github.com/gin-gonic/gin"
"vpn-platform/backend/internal/api"
"vpn-platform/backend/internal/core"
"vpn-platform/backend/internal/monitor"
"vpn-platform/backend/internal/service"
"vpn-platform/backend/internal/service/custom"
"vpn-platform/backend/internal/service/frp"
"vpn-platform/backend/internal/service/ikev2"
"vpn-platform/backend/internal/service/ipsec"
"vpn-platform/backend/internal/service/l2tp"
"vpn-platform/backend/internal/service/openvpn"
"vpn-platform/backend/internal/service/wireguard"
"vpn-platform/backend/internal/store"
)
//go:embed all:dist
var webFS embed.FS
func main() {
gin.SetMode(gin.ReleaseMode)
// 数据目录:默认 ./data,可用 VPN_DATA_DIR 覆盖
dataDir := service.BaseDir()
if err := store.Init(dataDir); err != nil {
log.Fatalf("初始化存储失败: %v", err)
}
// OpenVPN invokes this same, already-installed binary from its
// auth-user-pass-verify hook. Keep it before HTTP startup so the hook has a
// short-lived process and a meaningful exit status.
if len(os.Args) > 1 && os.Args[1] == "openvpn-auth" {
if err := openvpn.RunAuthHelper(os.Args[2:]); err != nil {
log.Printf("OpenVPN 用户认证失败: %v", err)
os.Exit(1)
}
return
}
// 注册内置协议
mgr := core.NewManager()
mgr.Register(wireguard.New())
mgr.Register(ipsec.New())
mgr.Register(ikev2.New())
mgr.Register(l2tp.New())
mgr.Register(frp.New())
mgr.Register(openvpn.New())
// 加载数据库中的自定义协议插件
custom.LoadAll(mgr)
r := gin.New()
r.Use(gin.Recovery())
srv := api.NewServer(mgr)
srv.RegisterRoutes(r)
// 内嵌前端(SPA 回退到 index.html)
dist, err := fs.Sub(webFS, "dist")
if err != nil {
log.Fatalf("内嵌资源损坏: %v", err)
}
fileServer := http.FileServer(http.FS(dist))
r.NoRoute(func(c *gin.Context) {
path := strings.TrimPrefix(c.Request.URL.Path, "/")
if path == "" {
path = "index.html"
}
if f, err := dist.Open(path); err == nil {
f.Close()
// 带 hash 的静态资源(assets/*)可长期缓存;index.html 必须每次重新验证
if strings.HasPrefix(path, "assets/") {
c.Header("Cache-Control", "public, max-age=31536000, immutable")
} else {
c.Header("Cache-Control", "no-cache")
}
fileServer.ServeHTTP(c.Writer, c.Request)
return
}
// SPA fallback — 必须 no-cache,否则浏览器缓存旧 index.html 会白屏
c.Header("Cache-Control", "no-cache")
c.Request.URL.Path = "/"
fileServer.ServeHTTP(c.Writer, c.Request)
})
// 状态采集 + 自启恢复
monitor.Start(mgr, 15*time.Second)
monitor.AutoStartAll(mgr)
addr := os.Getenv("VPN_LISTEN_ADDR")
if addr == "" {
addr = ":8080"
}
log.Printf("VPN 管理平台已启动: http://localhost%s (默认账号 admin / admin123)", addr)
log.Printf("协议依赖检测:")
for _, p := range mgr.List() {
ok, dep := p.Available()
mark := "✓"
if !ok {
mark = "✗"
}
log.Printf(" [%s] %-10s %s", mark, p.Name(), dep)
}
if err := r.Run(addr); err != nil {
log.Fatal(err)
}
}