Files
portal_page/main.go
T
kevin 0116489dea fix: 信任反向代理头,修复 unix socket 模式下获取不到真实 IP
Caddy 传 X-Real-IP / X-Forwarded-For,但 Gin 默认不信任代理头。
配置 SetTrustedProxies 全信任(unix socket 无来源 IP,无法按 IP 过滤),
安全边界由前端 Caddy 把控。
2026-05-28 16:31:17 +08:00

213 lines
5.6 KiB
Go

package main
import (
"html/template"
"log"
"net"
"os"
"path/filepath"
"strings"
"sync"
"simple_portal/config"
"simple_portal/database"
"simple_portal/handlers"
"simple_portal/middleware"
"simple_portal/session"
"github.com/gin-gonic/gin"
)
// loadTemplates 加载 templates/ 目录下所有 HTML 模板
// 自定义实现,因为 Go 的 ParseGlob 在 Windows 下有路径问题
func loadTemplates() *template.Template {
funcMap := template.FuncMap{
"hasPrefix": strings.HasPrefix,
"sub": func(a, b int) int { return a - b },
"add": func(a, b int) int { return a + b },
}
t := template.New("").Funcs(funcMap)
var files []string
filepath.Walk("templates", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".html" {
files = append(files, path)
}
return nil
})
if len(files) == 0 {
log.Fatal("No template files found in templates/")
}
for i, f := range files {
files[i] = filepath.ToSlash(f)
}
var terr error
t, terr = t.ParseFiles(files...)
if terr != nil {
log.Fatalf("Failed to parse templates: %v", terr)
}
return t
}
func main() {
// 加载配置文件(自动生成 + 补全缺失项)
if err := config.Load(); err != nil {
log.Fatalf("加载配置失败: %v", err)
}
log.Printf("配置加载成功,数据目录: %s,数据库: %s", config.Cfg.Data.Dir, config.Cfg.Database.Type)
// 初始化数据库
if err := database.InitDB(); err != nil {
log.Fatalf("初始化数据库失败: %v", err)
}
defer database.CloseDB()
// 创建上传目录
if err := os.MkdirAll(config.GetUploadDir(), 0755); err != nil {
log.Fatalf("创建上传目录失败: %v", err)
}
// 创建 session 存储
sessionStore := session.NewSessionStore()
// 创建 IP 封禁守护
ipBanGuard := middleware.NewIPBanGuard()
// 设置 Gin 模式
ginMode := os.Getenv("GIN_MODE")
if ginMode == "" {
gin.SetMode(gin.DebugMode)
}
r := gin.Default()
// 信任反向代理,从 X-Real-IP / X-Forwarded-For 获取真实客户端 IP
// Unix socket 下无法按来源 IP 判断信任,全信任(安全边界由 Caddy 把控)
_ = r.SetTrustedProxies([]string{"0.0.0.0/0", "::/0"})
r.RemoteIPHeaders = []string{"X-Real-IP", "X-Forwarded-For"}
// 加载 HTML 模板
r.SetHTMLTemplate(loadTemplates())
// 静态文件
r.Static("/static", "./static")
// 注入 session 和 IP 封禁守护
r.Use(func(c *gin.Context) {
c.Set("sessionStore", sessionStore)
c.Set("ipBanGuard", ipBanGuard)
c.Next()
})
// 公开路由
r.GET("/", handlers.HomeHandler)
r.GET("/click/:id", handlers.CardClickHandler)
r.GET("/search", handlers.SearchHandler)
r.GET("/uploads/:filename", handlers.ServeUploadHandler)
// 后台路由(IP 白名单)
adminGroup := r.Group("/admin")
adminGroup.Use(middleware.IPWhitelistRequired(func(sessionID string) bool {
return sessionStore.Get(sessionID) != nil
}))
{
// 登录(无需认证,受 IP 白名单限制)
adminGroup.GET("/login", handlers.LoginGet)
adminGroup.POST("/login", handlers.LoginPost)
// 需要认证的后台路由
protected := adminGroup.Group("")
protected.Use(middleware.AuthRequired(sessionStore))
{
protected.POST("/logout", handlers.Logout)
protected.GET("/", handlers.AdminIndex)
// 卡片管理
protected.GET("/cards", handlers.CardsList)
protected.GET("/cards/new", handlers.CardCreateGet)
protected.POST("/cards", handlers.CardCreatePost)
protected.GET("/cards/:id/edit", handlers.CardEditGet)
protected.POST("/cards/:id", handlers.CardEditPost)
protected.POST("/cards/:id/delete", handlers.CardDelete)
protected.POST("/cards/:id/toggle", handlers.CardToggle)
protected.POST("/cards/:id/move-up", handlers.CardMoveUp)
protected.POST("/cards/:id/move-down", handlers.CardMoveDown)
// 图片上传
protected.POST("/upload", handlers.UploadHandler)
// 设置
protected.GET("/settings", handlers.SettingsGet)
protected.POST("/settings", handlers.SettingsPost)
// 安全:登录日志
protected.GET("/logs", handlers.LoginLogsGet)
protected.POST("/logs/unban/:id", handlers.UnbanIP)
// 安全:修改密码
protected.GET("/password", handlers.ChangePasswordGet)
protected.POST("/password", handlers.ChangePasswordPost)
// 安全:IP 白名单
protected.GET("/ip-whitelist", handlers.IPWhitelistGet)
protected.POST("/ip-whitelist/add", handlers.IPWhitelistAdd)
protected.POST("/ip-whitelist/:id/delete", handlers.IPWhitelistDelete)
// 分析:访问日志
protected.GET("/access-logs", handlers.AccessLogsGet)
}
}
// 启动服务器:TCP 和 Unix socket 按配置监听
addr := config.Cfg.Server.Addr
unix := config.Cfg.Server.Unix
if addr == "" && unix == "" {
log.Fatal("未配置任何监听地址,请设置 server.addr 或 server.unix")
}
var wg sync.WaitGroup
started := 0
if unix != "" {
wg.Add(1)
started++
go func() {
defer wg.Done()
// 清理残留的 socket 文件
os.Remove(unix)
listener, err := net.Listen("unix", unix)
if err != nil {
log.Fatalf("监听 Unix socket 失败: %v", err)
}
// 设置 socket 文件权限,允许 nginx 等其他进程访问
os.Chmod(unix, 0666)
log.Printf("监听 Unix socket: %s", unix)
if err := r.RunListener(listener); err != nil {
log.Fatalf("Unix socket 服务启动失败: %v", err)
}
}()
}
if addr != "" && addr != ":0" {
wg.Add(1)
started++
go func() {
defer wg.Done()
log.Printf("监听 TCP: %s", addr)
if err := r.Run(addr); err != nil {
log.Fatalf("TCP 服务启动失败: %v", err)
}
}()
}
if started == 0 {
log.Fatal("未配置任何有效监听地址")
}
wg.Wait()
}