package routers import ( "log" "net/http" "os" "path/filepath" "strings" "server/pkg/verifyfiles" beego "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/server/web/context" ) // appRoot 定位服务根目录(conf/app.conf 所在目录的上一级)。 // // beego 找配置的顺序是:当前工作目录 → 可执行文件所在目录。 // 这里沿用同样的顺序,保证不管是用 `go run main.go`(systemd 的 WorkingDirectory) // 还是编译后的二进制、不管从哪个目录启动,验证文件目录都稳定落在服务根目录下, // 不会出现「换个目录启动就找不到验证文件」的情况。 // // 线上即 /www/wwwroot/api.yunzer.cn(conf/app.conf 在 /www/wwwroot/api.yunzer.cn/conf/)。 func appRoot() string { candidates := make([]string, 0, 2) if wd, err := os.Getwd(); err == nil { candidates = append(candidates, wd) } if beego.AppPath != "" { candidates = append(candidates, beego.AppPath) } return appRootFrom(candidates) } // appRootFrom 在候选目录里找第一个含 conf/app.conf 的目录(即服务根目录)。 func appRootFrom(candidates []string) string { for _, base := range candidates { if base == "" { continue } if _, err := os.Stat(filepath.Join(base, "conf", "app.conf")); err == nil { return base } } return "" } // RegisterVerifyFiles 让 Go 直接吐「根路径站点验证文件」。 // // 背景:nginx 对所有域名都是全量反代给 Go(location / → proxy_pass), // Go 不认识的路径一律回 beego 404 页,于是这些文件根本访问不到: // - 备案 / 域名所有权验证:1XXl8fPHr9.txt(阿里云、腾讯云) // - 搜索引擎站点验证:googleXXXX.html、msXXXXXXXX.txt、Bing 的 .xml // - 第三方平台域名校验:各平台给的 .txt / .html // // 这些文件名每次都不同(随机串),逐个注册路由不现实;改 nginx 又会因为 // 宝塔重生成 vhost 被冲掉(同 routers/spa.go 里遇到的问题)。 // 所以由 Go 统一托管:只要把验证文件丢进 verify 目录, // https://api.yunzer.cn/1XXl8fPHr9.txt 就能直接取到。 // // 规则: // - 只处理 GET / HEAD; // - 只认「根路径下的单段文件名」,如 /1XXl8fPHr9.txt; // - 文件名过白名单正则,扩展名在允许列表内(默认 .txt/.html/.htm); // - 文件必须真实存在于验证目录,否则放行给正常路由(最终仍是 beego 404)。 // // 验证文件目录由 app.conf 的 verify_dir 指定(默认相对进程工作目录的 verify, // 即线上服务目录 /www/wwwroot/api.yunzer.cn/verify);verify_hosts 可限定生效域名, // 留空表示所有走本服务的域名都生效(备案常常同时要过 api.yunzer.cn 与官网主域名)。 func RegisterVerifyFiles(dir string, exts string, hosts ...string) { dir = strings.TrimSpace(dir) if dir == "-" { // 显式关闭 return } if dir == "" { // 默认:服务根目录下的 verify/(app.conf 所在目录的上一级) dir = filepath.Join(appRoot(), verifyfiles.DefaultDir) } resolver, err := verifyfiles.New(verifyfiles.Config{ Dir: dir, Exts: exts, Hosts: strings.Join(hosts, ","), }) if err != nil { log.Printf("[verify] 根路径验证文件功能未启用: %v", err) return } if resolver == nil { // dir = "-":显式关闭 return } // 目录不存在就建一个,方便直接往里丢验证文件 if err := os.MkdirAll(resolver.Dir(), 0o755); err != nil { log.Printf("[verify] 创建验证文件目录失败: %v", err) } beego.InsertFilter("*", beego.BeforeStatic, func(ctx *context.Context) { if ctx.Request.Method != http.MethodGet && ctx.Request.Method != http.MethodHead { return } if !resolver.AllowHost(ctx.Request.Host) { return } full, ok := resolver.Resolve(ctx.Request.URL.Path) if !ok { return } // 文件内容不变,但可能被替换(换 token、换文件名),短缓存即可 ctx.Output.Header("Cache-Control", "public, max-age=300") http.ServeFile(ctx.ResponseWriter, ctx.Request, full) }) log.Printf("[verify] 根路径验证文件目录: %s,允许扩展名: %v", resolver.Dir(), resolver.Exts()) }