mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 06:34:54 +08:00
新增 bat-api 资源 bootstrap/分发 HTTP 服务、RPC release 发现、CDN path 分发、launcher 资源引导兼容、控制面中间件、OpenAPI 和 systemd 模板。 同步 Go 边界文档,明确 Rust bat 是资源生产者和同步运维入口,Go bat-api 是只读 bootstrap/分发服务,试验 Go CLI 产物为 bin/bat-go。 验证:未运行新命令;本轮已按要求停止重复构建/测试。
94 lines
2.8 KiB
Go
94 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// OfficialHosts are host path prefixes allowed on the CDN surface.
|
|
var OfficialHosts = map[string]struct{}{
|
|
ClientPatchHost: {},
|
|
ServerInfoHost: {},
|
|
}
|
|
|
|
// SplitCDNPath splits a request URL path into host + relative path under that host.
|
|
// Expected form: /prod-clientpatch.bluearchiveyostar.com/r93_xxx/TableBundles/...
|
|
func SplitCDNPath(requestPath string) (host string, rel string, err error) {
|
|
cleaned := filepath.ToSlash(requestPath)
|
|
cleaned = strings.TrimPrefix(cleaned, "/")
|
|
if cleaned == "" {
|
|
return "", "", fmt.Errorf("empty path")
|
|
}
|
|
parts := strings.Split(cleaned, "/")
|
|
if len(parts) < 2 {
|
|
return "", "", fmt.Errorf("path must include host and object path")
|
|
}
|
|
host = parts[0]
|
|
if _, ok := OfficialHosts[host]; !ok {
|
|
return "", "", fmt.Errorf("unsupported host %q", host)
|
|
}
|
|
for _, segment := range parts {
|
|
if segment == "" || segment == "." || segment == ".." {
|
|
return "", "", fmt.Errorf("unsafe path segment")
|
|
}
|
|
if strings.ContainsAny(segment, `?\`) {
|
|
return "", "", fmt.Errorf("unsafe path character")
|
|
}
|
|
}
|
|
rel = strings.Join(parts, "/")
|
|
return host, rel, nil
|
|
}
|
|
|
|
// ResolveUnderRoot joins root with a slash-separated relative path and ensures
|
|
// the result stays within root. Symlink targets that escape root are rejected
|
|
// when evalSymlinks is true and the path exists.
|
|
func ResolveUnderRoot(root, rel string, evalSymlinks bool) (string, error) {
|
|
if root == "" {
|
|
return "", fmt.Errorf("resource root is empty")
|
|
}
|
|
rootAbs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if evalSymlinks {
|
|
if resolved, err := filepath.EvalSymlinks(rootAbs); err == nil {
|
|
rootAbs = resolved
|
|
}
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
rel = strings.TrimPrefix(rel, "/")
|
|
var segments []string
|
|
for _, segment := range strings.Split(rel, "/") {
|
|
if segment == "" || segment == "." {
|
|
continue
|
|
}
|
|
if segment == ".." {
|
|
return "", fmt.Errorf("path escapes resource root")
|
|
}
|
|
if strings.ContainsAny(segment, `?\`) {
|
|
return "", fmt.Errorf("unsafe path segment %q", segment)
|
|
}
|
|
segments = append(segments, segment)
|
|
}
|
|
if len(segments) == 0 {
|
|
return "", fmt.Errorf("empty relative path")
|
|
}
|
|
candidate := filepath.Join(append([]string{rootAbs}, segments...)...)
|
|
// Ensure candidate is under rootAbs even before existence checks.
|
|
relCheck, err := filepath.Rel(rootAbs, candidate)
|
|
if err != nil || strings.HasPrefix(relCheck, "..") || filepath.IsAbs(relCheck) {
|
|
return "", fmt.Errorf("path escapes resource root")
|
|
}
|
|
if evalSymlinks {
|
|
if resolved, err := filepath.EvalSymlinks(candidate); err == nil {
|
|
relCheck, err = filepath.Rel(rootAbs, resolved)
|
|
if err != nil || strings.HasPrefix(relCheck, "..") || filepath.IsAbs(relCheck) {
|
|
return "", fmt.Errorf("symlink escapes resource root")
|
|
}
|
|
candidate = resolved
|
|
}
|
|
}
|
|
return candidate, nil
|
|
}
|