84 lines
2.7 KiB
Go
84 lines
2.7 KiB
Go
package transcodenode
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// InputPathAuthorizer approves a local media input before a node passes it to
|
|
// FFmpeg. Implementations must reject protocol URLs and paths outside the
|
|
// authoritative media catalog.
|
|
type InputPathAuthorizer interface {
|
|
Allowed(ctx context.Context, path string) (bool, error)
|
|
}
|
|
|
|
type catalogPathSource interface {
|
|
IsActivePath(ctx context.Context, path string) (bool, error)
|
|
}
|
|
|
|
// CatalogPathAuthorizer permits only existing regular files whose exact
|
|
// logical path is active in the media catalog. The scanner deliberately keeps
|
|
// logical paths for readable symlinks, so catalog membership is the correct
|
|
// authority: resolving the target and requiring it to remain under the logical
|
|
// library root would reject media layouts the scanner explicitly supports.
|
|
type CatalogPathAuthorizer struct {
|
|
paths catalogPathSource
|
|
}
|
|
|
|
// NewCatalogPathAuthorizer creates an FFmpeg input authorizer backed by the
|
|
// authoritative media-file catalog.
|
|
func NewCatalogPathAuthorizer(paths catalogPathSource) *CatalogPathAuthorizer {
|
|
return &CatalogPathAuthorizer{paths: paths}
|
|
}
|
|
|
|
// Allowed reports whether path is an active catalog entry that resolves to a
|
|
// regular file on this node. os.Stat follows scanner-approved symlinks while
|
|
// rejecting dangling links, directories, and other non-regular inputs.
|
|
func (a *CatalogPathAuthorizer) Allowed(ctx context.Context, path string) (bool, error) {
|
|
if a == nil || a.paths == nil || !plainAbsolutePath(path) {
|
|
return false, nil
|
|
}
|
|
active, err := a.paths.IsActivePath(ctx, path)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !active {
|
|
return false, nil
|
|
}
|
|
info, err := os.Stat(path)
|
|
return err == nil && info.Mode().IsRegular(), nil
|
|
}
|
|
|
|
func plainAbsolutePath(path string) bool {
|
|
path = strings.TrimSpace(path)
|
|
return path != "" && !strings.ContainsRune(path, '\x00') && filepath.IsAbs(path)
|
|
}
|
|
|
|
// pathWithinRoot validates a not-yet-created output by resolving the root and
|
|
// target parent. The caller creates the basename only after this check.
|
|
func pathWithinRoot(root, target string) bool {
|
|
if !plainAbsolutePath(root) || !plainAbsolutePath(target) {
|
|
return false
|
|
}
|
|
resolvedRoot, err := filepath.EvalSymlinks(filepath.Clean(root))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
resolvedParent, err := filepath.EvalSymlinks(filepath.Dir(filepath.Clean(target)))
|
|
if err != nil {
|
|
return false
|
|
}
|
|
resolvedTarget := filepath.Join(resolvedParent, filepath.Base(target))
|
|
return resolvedPathContained(resolvedRoot, resolvedTarget)
|
|
}
|
|
|
|
func resolvedPathContained(root, target string) bool {
|
|
rel, err := filepath.Rel(root, target)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
}
|