118 lines
4.2 KiB
Go
118 lines
4.2 KiB
Go
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())
|
||
}
|