优化小程序部分
This commit is contained in:
1 parent
727319a6eb
commit
d4e89788c5
46 files changed
+4906
-63
No files matched your search
@@ -0,0 +1,194 @@
|
||||
// Package verifyfiles 解析「根路径站点验证文件」的请求。
|
||||
//
|
||||
// 背景:nginx 对所有域名都是「全量反代给 Go」,Go 不认识的路径一律回 beego 404,
|
||||
// 于是备案 / 搜索引擎 / 第三方平台的域名所有权验证文件
|
||||
// (如 1XXl8fPHr9.txt、baidu_verify_xxx.html)根本访问不到。
|
||||
// 而这类文件名每次都不同(随机串),不可能逐个注册路由。
|
||||
//
|
||||
// 这里只做纯逻辑:URL 路径 → 磁盘文件 的安全映射。
|
||||
// beego 过滤器胶水放在 routers 包(见 routers/verifyfiles.go)。
|
||||
//
|
||||
// 安全性:
|
||||
// - 只认「根路径下的单段文件名」,不做目录拼接,杜绝 ../ 穿越;
|
||||
// - 文件名走白名单正则,扩展名走允许列表(不放行 .go/.conf/.env 等敏感后缀);
|
||||
// - 目标文件必须真实存在于验证目录内,否则一律放行给正常路由。
|
||||
package verifyfiles
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultDir 默认验证文件目录(相对进程工作目录,即服务目录)。
|
||||
DefaultDir = "verify"
|
||||
|
||||
// DefaultExts 默认允许的扩展名。
|
||||
DefaultExts = ".txt,.html,.htm"
|
||||
)
|
||||
|
||||
// fileNameRe 文件名白名单:字母或数字开头,只允许字母数字与 . _ -
|
||||
var fileNameRe = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
|
||||
|
||||
// Config 验证文件托管配置。
|
||||
type Config struct {
|
||||
// Dir 验证文件目录。留空用 DefaultDir;填 "-" 表示关闭该功能。
|
||||
Dir string
|
||||
// Exts 允许的扩展名,逗号分隔(如 ".txt,.html")。留空用 DefaultExts。
|
||||
Exts string
|
||||
// Hosts 限定生效的 Host(不含端口),逗号分隔。留空表示不限制。
|
||||
Hosts string
|
||||
}
|
||||
|
||||
// Resolver 把 URL 路径解析成验证文件的磁盘路径。
|
||||
type Resolver struct {
|
||||
dir string
|
||||
exts map[string]bool
|
||||
hosts map[string]bool
|
||||
}
|
||||
|
||||
// New 按配置创建 Resolver。
|
||||
// Dir 为 "-" 时表示关闭该功能,返回 (nil, nil)。
|
||||
func New(cfg Config) (*Resolver, error) {
|
||||
dir := strings.TrimSpace(cfg.Dir)
|
||||
if dir == "-" {
|
||||
return nil, nil
|
||||
}
|
||||
if dir == "" {
|
||||
dir = DefaultDir
|
||||
}
|
||||
abs, err := filepath.Abs(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("验证文件目录 %q 解析失败: %w", dir, err)
|
||||
}
|
||||
|
||||
r := &Resolver{
|
||||
dir: abs,
|
||||
exts: map[string]bool{},
|
||||
hosts: map[string]bool{},
|
||||
}
|
||||
|
||||
for _, e := range strings.Split(strings.TrimSpace(cfg.Exts), ",") {
|
||||
e = strings.ToLower(strings.TrimSpace(e))
|
||||
if e == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(e, ".") {
|
||||
e = "." + e
|
||||
}
|
||||
r.exts[e] = true
|
||||
}
|
||||
if len(r.exts) == 0 {
|
||||
for _, e := range strings.Split(DefaultExts, ",") {
|
||||
r.exts[strings.ToLower(strings.TrimSpace(e))] = true
|
||||
}
|
||||
}
|
||||
|
||||
for _, h := range strings.Split(strings.TrimSpace(cfg.Hosts), ",") {
|
||||
h = normalizeHost(h)
|
||||
if h == "" {
|
||||
continue
|
||||
}
|
||||
r.hosts[h] = true
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Dir 返回验证文件目录的绝对路径。
|
||||
func (r *Resolver) Dir() string {
|
||||
if r == nil {
|
||||
return ""
|
||||
}
|
||||
return r.dir
|
||||
}
|
||||
|
||||
// Exts 返回允许的扩展名列表(用于启动日志,便于确认配置是否生效)。
|
||||
func (r *Resolver) Exts() []string {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(r.exts))
|
||||
for e := range r.exts {
|
||||
out = append(out, e)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// AllowHost 判断该 Host 是否允许取验证文件(未配置 hosts 时一律允许)。
|
||||
func (r *Resolver) AllowHost(host string) bool {
|
||||
if r == nil {
|
||||
return false
|
||||
}
|
||||
if len(r.hosts) == 0 {
|
||||
return true
|
||||
}
|
||||
_, ok := r.hosts[normalizeHost(host)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Resolve 把请求路径解析成磁盘文件路径。
|
||||
// 只接受根路径下的单段文件名(如 /1XXl8fPHr9.txt);文件必须真实存在。
|
||||
func (r *Resolver) Resolve(urlPath string) (string, bool) {
|
||||
if r == nil {
|
||||
return "", false
|
||||
}
|
||||
name, ok := r.fileName(urlPath)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
full := filepath.Join(r.dir, name)
|
||||
// 兜底:拼出来的路径必须仍在验证目录内
|
||||
if !strings.HasPrefix(full, r.dir+string(os.PathSeparator)) {
|
||||
return "", false
|
||||
}
|
||||
f, err := os.Stat(full)
|
||||
if err != nil || f.IsDir() {
|
||||
return "", false
|
||||
}
|
||||
return full, true
|
||||
}
|
||||
|
||||
// fileName 从 URL 路径里取出合法的验证文件名。
|
||||
func (r *Resolver) fileName(urlPath string) (string, bool) {
|
||||
p := strings.TrimSpace(urlPath)
|
||||
if p == "" || p == "/" {
|
||||
return "", false
|
||||
}
|
||||
name := strings.TrimPrefix(p, "/")
|
||||
if name == "" {
|
||||
return "", false
|
||||
}
|
||||
// 只允许根目录下的单段文件名,禁止子目录
|
||||
if strings.ContainsAny(name, "/\\") {
|
||||
return "", false
|
||||
}
|
||||
// 禁止路径穿越
|
||||
if strings.Contains(name, "..") {
|
||||
return "", false
|
||||
}
|
||||
if !fileNameRe.MatchString(name) {
|
||||
return "", false
|
||||
}
|
||||
// index.html 会被 http.ServeFile 重定向到 "./",排除
|
||||
if strings.EqualFold(name, "index.html") {
|
||||
return "", false
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(name))
|
||||
if !r.exts[ext] {
|
||||
return "", false
|
||||
}
|
||||
return name, true
|
||||
}
|
||||
|
||||
// normalizeHost 去掉端口并转小写。
|
||||
func normalizeHost(h string) string {
|
||||
h = strings.ToLower(strings.TrimSpace(h))
|
||||
if i := strings.IndexByte(h, ':'); i > 0 {
|
||||
h = h[:i]
|
||||
}
|
||||
return h
|
||||
}
|
||||
Reference in new issue
Block a user