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 }