198 lines
6.0 KiB
Go
198 lines
6.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"browser.local/run/protocol"
|
|
)
|
|
|
|
const maxExecutionContentBytes = 64 * 1024
|
|
|
|
type WorkspaceResolver struct {
|
|
root string
|
|
}
|
|
|
|
func NewWorkspaceResolver(root string) WorkspaceResolver {
|
|
if strings.TrimSpace(root) == "" {
|
|
root = filepath.Join(".", ".run-workspace")
|
|
}
|
|
return WorkspaceResolver{root: root}
|
|
}
|
|
|
|
func (resolver WorkspaceResolver) Scope(serverInstanceID string, profileKey string) (string, error) {
|
|
if err := validateWorkspaceComponent(serverInstanceID, "serverInstanceId"); err != nil {
|
|
return "", err
|
|
}
|
|
if err := validateWorkspaceComponent(profileKey, "profileKey"); err != nil {
|
|
return "", err
|
|
}
|
|
root, err := filepath.Abs(resolver.root)
|
|
if err != nil {
|
|
return "", fmt.Errorf("resolve workspace root: %w", err)
|
|
}
|
|
if err := ensureDirectory(root); err != nil {
|
|
return "", fmt.Errorf("secure workspace root: %w", err)
|
|
}
|
|
instances := filepath.Join(root, "instances")
|
|
if err := ensureDirectory(instances); err != nil {
|
|
return "", fmt.Errorf("secure workspace instances: %w", err)
|
|
}
|
|
serverDir := filepath.Join(instances, serverInstanceID)
|
|
if err := ensureDirectory(serverDir); err != nil {
|
|
return "", fmt.Errorf("secure server workspace: %w", err)
|
|
}
|
|
scope := filepath.Join(serverDir, profileKey)
|
|
if err := ensureDirectory(scope); err != nil {
|
|
return "", fmt.Errorf("secure profile workspace: %w", err)
|
|
}
|
|
return scope, nil
|
|
}
|
|
|
|
func (resolver WorkspaceResolver) ExistingTarget(scope string, key string) (string, error) {
|
|
path, err := resolver.target(scope, key, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
|
|
return "", fmt.Errorf("target must be a regular file")
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (resolver WorkspaceResolver) ExistingDirectory(scope string, key string) (string, error) {
|
|
if strings.TrimSpace(scope) == "" || !protocol.ValidLogicalFileKey(key) || filepath.IsAbs(key) || strings.Contains(key, string(rune(92))) {
|
|
return "", fmt.Errorf("logical directory is unsafe")
|
|
}
|
|
cleanScope, err := filepath.Abs(scope)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
root, err := filepath.Abs(resolver.root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel, err := filepath.Rel(root, cleanScope)
|
|
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
|
return "", fmt.Errorf("workspace scope escapes root")
|
|
}
|
|
current := cleanScope
|
|
for _, part := range strings.Split(filepath.ToSlash(key), "/") {
|
|
if part == "" || part == "." || part == ".." {
|
|
return "", fmt.Errorf("logical directory contains unsafe component")
|
|
}
|
|
current = filepath.Join(current, part)
|
|
info, statErr := os.Lstat(current)
|
|
if statErr != nil {
|
|
return "", statErr
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return "", fmt.Errorf("logical directory is not a real directory")
|
|
}
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func (resolver WorkspaceResolver) WritableTarget(scope string, key string) (string, string, error) {
|
|
if strings.HasPrefix(key, "actions/") || strings.HasPrefix(key, "state/") || key == "actions" || key == "state" {
|
|
return "", "", fmt.Errorf("target is reserved")
|
|
}
|
|
parts := strings.Split(filepath.ToSlash(key), "/")
|
|
parent := scope
|
|
for _, part := range parts[:len(parts)-1] {
|
|
parent = filepath.Join(parent, part)
|
|
if err := ensureDirectory(parent); err != nil {
|
|
return "", "", err
|
|
}
|
|
}
|
|
path, err := resolver.target(scope, key, true)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return path, filepath.Dir(path), nil
|
|
}
|
|
|
|
func (resolver WorkspaceResolver) target(scope string, key string, allowMissingFinal bool) (string, error) {
|
|
if strings.TrimSpace(scope) == "" {
|
|
return "", fmt.Errorf("workspace scope is invalid")
|
|
}
|
|
if !protocol.ValidLogicalFileKey(key) || filepath.IsAbs(key) || strings.Contains(key, `\`) {
|
|
return "", fmt.Errorf("logical key is unsafe")
|
|
}
|
|
cleanScope, err := filepath.Abs(scope)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
root, err := filepath.Abs(resolver.root)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel, err := filepath.Rel(root, cleanScope)
|
|
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
|
return "", fmt.Errorf("workspace scope escapes root")
|
|
}
|
|
parts := strings.Split(filepath.ToSlash(key), "/")
|
|
current := cleanScope
|
|
for index, part := range parts {
|
|
if part == "" || part == "." || part == ".." {
|
|
return "", fmt.Errorf("logical key contains unsafe component")
|
|
}
|
|
current = filepath.Join(current, part)
|
|
info, statErr := os.Lstat(current)
|
|
if statErr != nil {
|
|
if allowMissingFinal && index == len(parts)-1 && os.IsNotExist(statErr) {
|
|
return current, nil
|
|
}
|
|
return "", statErr
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return "", fmt.Errorf("logical key contains a symlink")
|
|
}
|
|
if index < len(parts)-1 && !info.IsDir() {
|
|
return "", fmt.Errorf("logical key parent is not a directory")
|
|
}
|
|
if index == len(parts)-1 && info.Mode()&os.ModeType != 0 {
|
|
return "", fmt.Errorf("target is a special file")
|
|
}
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func ensureDirectory(path string) error {
|
|
if info, err := os.Lstat(path); err == nil {
|
|
if info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return fmt.Errorf("path is not a real directory")
|
|
}
|
|
return os.Chmod(path, 0o700)
|
|
} else if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
parent := filepath.Dir(path)
|
|
if parent != path {
|
|
if err := ensureDirectory(parent); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := os.Mkdir(path, 0o700); err != nil && !os.IsExist(err) {
|
|
return err
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
|
|
return fmt.Errorf("created path is not a real directory")
|
|
}
|
|
return os.Chmod(path, 0o700)
|
|
}
|
|
|
|
func validateWorkspaceComponent(value string, field string) error {
|
|
if strings.TrimSpace(value) == "" || value != strings.TrimSpace(value) || value == "." || value == ".." || strings.ContainsAny(value, `/\`) || !protocol.ValidLogicalFileKey(value) {
|
|
return fmt.Errorf("%s is unsafe", field)
|
|
}
|
|
return nil
|
|
}
|