승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
113 lines
3 KiB
Go
113 lines
3 KiB
Go
package workspace
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
errInvalidPath = errors.New("workspace path is invalid")
|
|
errReservedPath = errors.New("workspace path is reserved")
|
|
errUnsafePath = errors.New("workspace path is unsafe")
|
|
errNotFound = errors.New("workspace path not found")
|
|
)
|
|
|
|
type targetIdentity struct {
|
|
exists bool
|
|
device uint64
|
|
inode uint64
|
|
mode fs.FileMode
|
|
}
|
|
|
|
// userPath accepts a canonical relative path only. .iop is private runtime
|
|
// state: no caller-facing operation can name it or a child beneath it.
|
|
func userPath(value string) (string, error) {
|
|
if value == "" || strings.Contains(value, "\\") || path.IsAbs(value) || path.Clean(value) != value {
|
|
return "", errInvalidPath
|
|
}
|
|
if value == "." {
|
|
return value, nil
|
|
}
|
|
if strings.HasPrefix(value, "../") || value == ".." {
|
|
return "", errInvalidPath
|
|
}
|
|
first := strings.Split(value, "/")[0]
|
|
if first == ".iop" {
|
|
return "", errReservedPath
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// internalPath is deliberately unexported. It is available only to future
|
|
// request-owned runtime artifacts and cannot name sibling request namespaces.
|
|
func (r *Request) internalPath(value string) (string, error) {
|
|
if value == "" || path.IsAbs(value) || path.Clean(value) != value || value == "." || strings.HasPrefix(value, "../") || value == ".." {
|
|
return "", errInvalidPath
|
|
}
|
|
prefix := r.internalPrefix + "/"
|
|
if value != r.internalPrefix && !strings.HasPrefix(value, prefix) {
|
|
return "", errReservedPath
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func checkedExisting(entry *catalogEntry, name string, wantDirectory bool, allowSymlinkTarget bool) (fs.FileInfo, error) {
|
|
if name != "." {
|
|
parts := strings.Split(name, "/")
|
|
for index := range parts {
|
|
partial := strings.Join(parts[:index+1], "/")
|
|
info, err := entry.root.Lstat(partial)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, errNotFound
|
|
}
|
|
return nil, errUnsafePath
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 && (!allowSymlinkTarget || index != len(parts)-1) {
|
|
return nil, errUnsafePath
|
|
}
|
|
if index != len(parts)-1 && !info.IsDir() {
|
|
return nil, errUnsafePath
|
|
}
|
|
if info.Mode()&os.ModeSymlink == 0 {
|
|
if err := sameFilesystem(entry, partial); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
info, err := entry.root.Lstat(name)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, errNotFound
|
|
}
|
|
return nil, errUnsafePath
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 && !allowSymlinkTarget {
|
|
return nil, errUnsafePath
|
|
}
|
|
if !allowSymlinkTarget || info.Mode()&os.ModeSymlink == 0 {
|
|
if err := sameFilesystem(entry, name); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if wantDirectory && !info.IsDir() {
|
|
return nil, errUnsafePath
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func sameFilesystem(entry *catalogEntry, name string) error {
|
|
info, err := entry.root.Stat(name)
|
|
if err != nil {
|
|
return errUnsafePath
|
|
}
|
|
device, _, ok := fileIdentity(info)
|
|
if !ok || device != entry.device {
|
|
return errUnsafePath
|
|
}
|
|
return nil
|
|
}
|