mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-09-18 10:00:40 +08:00
feat(api): 补齐资源分发服务入口
新增 bat-api 资源 bootstrap/分发 HTTP 服务、RPC release 发现、CDN path 分发、launcher 资源引导兼容、控制面中间件、OpenAPI 和 systemd 模板。 同步 Go 边界文档,明确 Rust bat 是资源生产者和同步运维入口,Go bat-api 是只读 bootstrap/分发服务,试验 Go CLI 产物为 bin/bat-go。 验证:未运行新命令;本轮已按要求停止重复构建/测试。
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user