ccpoad / main.go
anyalerob's picture
Upload folder using huggingface_hub
2986042 verified
Raw
History Blame Contribute Delete
6.86 kB
// Package main 是 ccLoad 应用入口
package main
import (
"context"
"errors"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
"time"
"ccLoad/internal/app"
"ccLoad/internal/storage"
"ccLoad/internal/util"
"ccLoad/internal/version"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
// restartRequested 标记是否需要重启(由设置保存触发)
var restartRequested atomic.Bool
// RequestRestart 请求程序重启(由 admin_settings 调用)
func RequestRestart() {
restartRequested.Store(true)
}
// execSelf 使用 syscall.Exec 重新执行自身
func execSelf() {
executable, err := os.Executable()
if err != nil {
log.Printf("[ERROR] 获取可执行文件路径失败: %v", err)
return
}
log.Printf("[INFO] 正在重启程序: %s", executable)
// syscall.Exec 替换当前进程,不会返回
//nolint:gosec // G204: executable 来自 os.Executable(),用于自重启,安全可控
if err := syscall.Exec(executable, os.Args, os.Environ()); err != nil {
log.Printf("[ERROR] 重启失败: %v", err)
}
}
// defaultTrustedProxies 默认可信代理(私有网段 + 共享地址空间)
var defaultTrustedProxies = []string{
"10.0.0.0/8", // Class A 私有 (RFC 1918)
"172.16.0.0/12", // Class B 私有 (RFC 1918)
"192.168.0.0/16", // Class C 私有 (RFC 1918)
"100.64.0.0/10", // 共享地址空间 (RFC 6598, 运营商级NAT/CGNAT)
"127.0.0.0/8", // Loopback
"::1/128", // IPv6 Loopback
}
// getTrustedProxies 获取可信代理配置
// 环境变量 TRUSTED_PROXIES: 逗号分隔的 CIDR,"none" 表示不信任任何代理
// 未设置时返回私有网段默认值
func getTrustedProxies() []string {
v := os.Getenv("TRUSTED_PROXIES")
if v == "" {
return defaultTrustedProxies
}
if v == "none" {
return nil
}
var proxies []string
for p := range strings.SplitSeq(v, ",") {
if p = strings.TrimSpace(p); p != "" {
proxies = append(proxies, p)
}
}
if len(proxies) == 0 {
return nil
}
return proxies
}
func main() {
// 打印启动 Banner
version.PrintBanner()
// 启动后台版本检测(每4小时检查GitHub releases)
version.StartChecker()
// 优先读取.env文件
if err := godotenv.Load(); err != nil {
log.Printf("未找到 .env 文件: %v", err)
}
// 设置Gin运行模式
if os.Getenv("GIN_MODE") == "" {
gin.SetMode(gin.ReleaseMode) // 生产模式
}
// 初始化嵌入的静态资源文件系统
app.SetEmbedFS(WebFS, "web")
// 使用工厂函数创建存储实例(自动识别MySQL/SQLite)
store, err := storage.NewStore()
if err != nil {
log.Fatalf("存储初始化失败: %v", err)
}
// 渠道仅从数据库管理与读取;不再从本地文件初始化。
srv := app.NewServer(store)
// 注入重启函数(避免循环依赖)
// 语义:标记“需要重启”,并发送 SIGTERM 触发优雅关闭;main 在退出前检测标记并 execSelf。
app.RestartFunc = func() {
RequestRestart()
p, err := os.FindProcess(os.Getpid())
if err != nil {
log.Printf("[ERROR] 查找进程失败: %v", err)
return
}
if err := p.Signal(syscall.SIGTERM); err != nil {
log.Printf("[ERROR] 发送 SIGTERM 失败: %v", err)
}
}
// 创建Gin引擎
r := gin.New()
// 配置可信代理,防止 X-Forwarded-For 伪造绕过登录限速
// TRUSTED_PROXIES 环境变量:逗号分隔的 CIDR 列表,设为 "none" 则不信任任何代理
// 未配置时默认信任私有网段(适用于内网反向代理场景)
// [FIX] 2025-12: 检查 SetTrustedProxies 返回值,fail-fast 避免静默的信任链缺口
trustedProxies := getTrustedProxies()
if trustedProxies == nil {
if err := r.SetTrustedProxies(nil); err != nil {
log.Fatalf("[FATAL] 设置可信代理失败: %v", err)
}
log.Printf("[CONFIG] 可信代理: 无 (直接暴露)")
} else {
if err := r.SetTrustedProxies(trustedProxies); err != nil {
log.Fatalf("[FATAL] 设置可信代理失败: %v (配置: %v)", err, trustedProxies)
}
log.Printf("[CONFIG] 可信代理: %v", trustedProxies)
}
// 添加基础中间件
// GIN_LOG 环境变量控制访问日志:false/0/no/off 关闭,默认开启
if util.ParseBoolDefault(os.Getenv("GIN_LOG"), true) {
r.Use(gin.Logger())
}
r.Use(gin.Recovery())
// 注册路由
srv.SetupRoutes(r)
// session清理循环在NewServer中已启动,避免重复启动
addr := ":8080"
if v := os.Getenv("PORT"); v != "" {
if !strings.HasPrefix(v, ":") {
v = ":" + v
}
addr = v
}
// 使用http.Server支持优雅关闭
// WriteTimeout 动态计算:确保 >= nonStreamTimeout,避免传输层截断业务层超时
writeTimeout := srv.GetWriteTimeout()
httpServer := &http.Server{
Addr: addr,
Handler: r,
// ✅ 深度防御:传输层超时保护(抵御slowloris等慢速攻击)
// 即使绕过应用层并发控制,也会在HTTP层被杀死
ReadHeaderTimeout: 5 * time.Second, // 防止慢速发送header(slowloris攻击)
ReadTimeout: 120 * time.Second, // 防止慢速发送body(兼容长请求)
WriteTimeout: writeTimeout, // 动态值,>= nonStreamTimeout
IdleTimeout: 60 * time.Second, // 防止keep-alive连接占用fd
}
log.Printf("[CONFIG] HTTP WriteTimeout: %v", writeTimeout)
// 启动HTTP服务器(在goroutine中)
go func() {
log.Printf("[INFO] HTTP 监听地址: %s", addr)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP服务器启动失败: %v", err)
}
}()
// 监听系统信号,实现优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
// ✅ 停止信号监听,释放signal.Notify创建的后台goroutine
signal.Stop(quit)
close(quit)
log.Println("收到关闭信号,正在优雅关闭服务器...")
// 设置5秒超时用于HTTP服务器关闭
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 关闭HTTP服务器
if err := httpServer.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP服务器关闭超时: %v,强制关闭连接", err)
// 超时后强制关闭,防止streaming连接阻塞退出
_ = httpServer.Close()
}
// 关闭Server后台任务(设置10秒超时)
taskShutdownCtx, taskCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer taskCancel()
if err := srv.Shutdown(taskShutdownCtx); err != nil {
log.Printf("Server后台任务关闭错误: %v", err)
}
log.Println("✅ 服务器已优雅关闭")
// 检查是否需要重启
if restartRequested.Load() {
log.Println("🔄 检测到重启请求,正在重启...")
execSelf()
// execSelf 不会返回,如果到这里说明重启失败
log.Println("[ERROR] 重启失败,程序退出")
}
}