Files
nyaKazuha 3f78f8f880 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。

验证:未运行新命令;本轮已按要求停止重复构建/测试。
2026-07-31 00:41:48 +08:00

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
}