1018 lines
29 KiB
Go
1018 lines
29 KiB
Go
package plugin
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"mime"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"seanime/internal/extension"
|
|
util "seanime/internal/util"
|
|
goja_util "seanime/internal/util/goja"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/bmatcuk/doublestar/v4"
|
|
"github.com/dop251/goja"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
var (
|
|
ErrPathNotAuthorized = errors.New("path not authorized")
|
|
)
|
|
|
|
const (
|
|
AllowPathRead = 0
|
|
AllowPathWrite = 1
|
|
)
|
|
|
|
type AsyncCmd struct {
|
|
cmd *exec.Cmd
|
|
|
|
appContext *AppContextImpl
|
|
scheduler *goja_util.Scheduler
|
|
vm *goja.Runtime
|
|
}
|
|
|
|
type CmdHelper struct {
|
|
cmd *exec.Cmd
|
|
|
|
stdout io.ReadCloser
|
|
stderr io.ReadCloser
|
|
|
|
appContext *AppContextImpl
|
|
scheduler *goja_util.Scheduler
|
|
vm *goja.Runtime
|
|
}
|
|
|
|
// BindSystem binds the system module to the Goja runtime.
|
|
// Permissions needed: system + allowlist
|
|
func (a *AppContextImpl) BindSystem(vm *goja.Runtime, logger *zerolog.Logger, ext *extension.Extension, scheduler *goja_util.Scheduler) {
|
|
|
|
//////////////////////////////////////
|
|
// OS
|
|
//////////////////////////////////////
|
|
|
|
osObj := vm.NewObject()
|
|
|
|
// _ = osObj.Set("args", os.Args) // NOT INCLUDED
|
|
// _ = osObj.Set("exit", os.Exit) // NOT INCLUDED
|
|
// _ = osObj.Set("getenv", os.Getenv) // NOT INCLUDED
|
|
// _ = osObj.Set("dirFS", os.DirFS) // NOT INCLUDED
|
|
// _ = osObj.Set("getwd", os.Getwd) // NOT INCLUDED
|
|
// _ = osObj.Set("chown", os.Chown) // NOT INCLUDED
|
|
|
|
// e.g. $os.platform // "windows"
|
|
_ = osObj.Set("platform", runtime.GOOS)
|
|
|
|
// e.g. $os.arch // "amd64"
|
|
_ = osObj.Set("arch", runtime.GOARCH)
|
|
|
|
_ = osObj.Set("cmd", func(name string, arg ...string) (*exec.Cmd, error) {
|
|
if !a.isAllowedCommand(ext, name, arg...) {
|
|
return nil, fmt.Errorf("command (%s) not authorized", fmt.Sprintf("%s %s", name, strings.Join(arg, " ")))
|
|
}
|
|
|
|
return util.NewCmdCtx(context.Background(), name, arg...), nil
|
|
})
|
|
|
|
_ = osObj.Set("Interrupt", os.Interrupt)
|
|
_ = osObj.Set("Kill", os.Kill)
|
|
|
|
_ = osObj.Set("readFile", func(path string) ([]byte, error) {
|
|
if !a.isAllowedPath(ext, path, AllowPathRead) {
|
|
return nil, fmt.Errorf("$os.readFile: path (%s) not authorized for read", path)
|
|
}
|
|
|
|
return os.ReadFile(path)
|
|
})
|
|
_ = osObj.Set("writeFile", func(path string, data []byte, perm fs.FileMode) error {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return fmt.Errorf("$os.writeFile: path (%s) not authorized for write", path)
|
|
}
|
|
return os.WriteFile(path, data, perm)
|
|
})
|
|
_ = osObj.Set("readDir", func(path string) ([]fs.DirEntry, error) {
|
|
if !a.isAllowedPath(ext, path, AllowPathRead) {
|
|
return nil, fmt.Errorf("$os.readDir: path (%s) not authorized for read", path)
|
|
}
|
|
return os.ReadDir(path)
|
|
})
|
|
_ = osObj.Set("tempDir", func() (string, error) {
|
|
if !a.isAllowedPath(ext, os.TempDir(), AllowPathRead) {
|
|
return "", fmt.Errorf("$os.tempDir: path (%s) not authorized for read", os.TempDir())
|
|
}
|
|
return os.TempDir(), nil
|
|
})
|
|
_ = osObj.Set("configDir", func() (string, error) {
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !a.isAllowedPath(ext, configDir, AllowPathRead) {
|
|
return "", fmt.Errorf("$os.configDir: path (%s) not authorized for read", configDir)
|
|
}
|
|
return configDir, nil
|
|
})
|
|
_ = osObj.Set("homeDir", func() (string, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !a.isAllowedPath(ext, homeDir, AllowPathRead) {
|
|
return "", fmt.Errorf("$os.homeDir: path (%s) not authorized for read", homeDir)
|
|
}
|
|
return homeDir, nil
|
|
})
|
|
_ = osObj.Set("cacheDir", func() (string, error) {
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !a.isAllowedPath(ext, cacheDir, AllowPathRead) {
|
|
return "", fmt.Errorf("$os.cacheDir: path (%s) not authorized for read", cacheDir)
|
|
}
|
|
return cacheDir, nil
|
|
})
|
|
_ = osObj.Set("truncate", func(path string, size int64) error {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return fmt.Errorf("$os.truncate: path (%s) not authorized for write", path)
|
|
}
|
|
return os.Truncate(path, size)
|
|
})
|
|
_ = osObj.Set("mkdir", func(path string, perm fs.FileMode) error {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return fmt.Errorf("$os.mkdir: path (%s) not authorized for write", path)
|
|
}
|
|
return os.Mkdir(path, perm)
|
|
})
|
|
_ = osObj.Set("mkdirAll", func(path string, perm fs.FileMode) error {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return fmt.Errorf("$os.mkdirAll: path (%s) not authorized for write", path)
|
|
}
|
|
return os.MkdirAll(path, perm)
|
|
})
|
|
_ = osObj.Set("rename", func(oldpath, newpath string) error {
|
|
if !a.isAllowedPath(ext, oldpath, AllowPathWrite) || !a.isAllowedPath(ext, newpath, AllowPathWrite) {
|
|
return fmt.Errorf("$os.rename: path (%s) not authorized for write", oldpath)
|
|
}
|
|
return os.Rename(oldpath, newpath)
|
|
})
|
|
_ = osObj.Set("remove", func(path string) error {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return fmt.Errorf("$os.remove: path (%s) not authorized for write", path)
|
|
}
|
|
return os.Remove(path)
|
|
})
|
|
_ = osObj.Set("removeAll", func(path string) error {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return fmt.Errorf("$os.removeAll: path (%s) not authorized for write", path)
|
|
}
|
|
return os.RemoveAll(path)
|
|
})
|
|
_ = osObj.Set("stat", func(path string) (fs.FileInfo, error) {
|
|
if !a.isAllowedPath(ext, path, AllowPathRead) {
|
|
return nil, fmt.Errorf("$os.stat: path (%s) not authorized for read", path)
|
|
}
|
|
return os.Stat(path)
|
|
})
|
|
|
|
_ = osObj.Set("O_RDONLY", os.O_RDONLY)
|
|
_ = osObj.Set("O_WRONLY", os.O_WRONLY)
|
|
_ = osObj.Set("O_RDWR", os.O_RDWR)
|
|
_ = osObj.Set("O_APPEND", os.O_APPEND)
|
|
_ = osObj.Set("O_CREATE", os.O_CREATE)
|
|
_ = osObj.Set("O_EXCL", os.O_EXCL)
|
|
_ = osObj.Set("O_SYNC", os.O_SYNC)
|
|
_ = osObj.Set("O_TRUNC", os.O_TRUNC)
|
|
|
|
// Example:
|
|
// const file = $os.openFile("path/to/file.txt", $os.O_RDWR|$os.O_CREATE, 0644)
|
|
// file.writeString("Hello, world!")
|
|
// file.close()
|
|
_ = osObj.Set("openFile", func(path string, flag int, perm fs.FileMode) (*os.File, error) {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return nil, fmt.Errorf("$os.openFile: path (%s) not authorized for write", path)
|
|
}
|
|
return os.OpenFile(path, flag, perm)
|
|
})
|
|
|
|
// Example:
|
|
// const file = $os.create("path/to/file.txt")
|
|
// file.writeString("Hello, world!")
|
|
// file.close()
|
|
_ = osObj.Set("create", func(path string) (*os.File, error) {
|
|
if !a.isAllowedPath(ext, path, AllowPathWrite) {
|
|
return nil, fmt.Errorf("$os.create: path (%s) not authorized for write", path)
|
|
}
|
|
return os.Create(path)
|
|
})
|
|
|
|
fileModeObj := vm.NewObject()
|
|
|
|
fileModeObj.Set("ModeDir", os.ModeDir)
|
|
fileModeObj.Set("ModeAppend", os.ModeAppend)
|
|
fileModeObj.Set("ModeExclusive", os.ModeExclusive)
|
|
fileModeObj.Set("ModeTemporary", os.ModeTemporary)
|
|
fileModeObj.Set("ModeSymlink", os.ModeSymlink)
|
|
fileModeObj.Set("ModeDevice", os.ModeDevice)
|
|
fileModeObj.Set("ModeNamedPipe", os.ModeNamedPipe)
|
|
fileModeObj.Set("ModeSocket", os.ModeSocket)
|
|
fileModeObj.Set("ModeSetuid", os.ModeSetuid)
|
|
fileModeObj.Set("ModeSetgid", os.ModeSetgid)
|
|
fileModeObj.Set("ModeCharDevice", os.ModeCharDevice)
|
|
fileModeObj.Set("ModeSticky", os.ModeSticky)
|
|
fileModeObj.Set("ModeIrregular", os.ModeIrregular)
|
|
fileModeObj.Set("ModeType", os.ModeType)
|
|
fileModeObj.Set("ModePerm", os.ModePerm)
|
|
_ = osObj.Set("FileMode", fileModeObj)
|
|
|
|
_ = vm.Set("$os", osObj)
|
|
|
|
//////////////////////////////////////
|
|
// IO
|
|
//////////////////////////////////////
|
|
|
|
ioObj := vm.NewObject()
|
|
|
|
ioObj.Set("copy", func(dst io.Writer, src io.Reader) (int64, error) {
|
|
return io.Copy(dst, src)
|
|
})
|
|
|
|
ioObj.Set("readAll", func(r io.Reader) ([]byte, error) {
|
|
return io.ReadAll(r)
|
|
})
|
|
|
|
ioObj.Set("writeString", func(w io.Writer, s string) (int, error) {
|
|
return io.WriteString(w, s)
|
|
})
|
|
|
|
ioObj.Set("readAtLeast", func(r io.Reader, buf []byte, min int) (int, error) {
|
|
return io.ReadAtLeast(r, buf, min)
|
|
})
|
|
|
|
ioObj.Set("readFull", func(r io.Reader, buf []byte) (int, error) {
|
|
return io.ReadFull(r, buf)
|
|
})
|
|
|
|
ioObj.Set("copyN", func(dst io.Writer, src io.Reader, n int64) (int64, error) {
|
|
return io.CopyN(dst, src, n)
|
|
})
|
|
|
|
ioObj.Set("copyBuffer", func(dst io.Writer, src io.Reader, buf []byte) (int64, error) {
|
|
return io.CopyBuffer(dst, src, buf)
|
|
})
|
|
|
|
ioObj.Set("limitReader", func(r io.Reader, n int64) io.Reader {
|
|
return io.LimitReader(r, n)
|
|
})
|
|
|
|
ioObj.Set("newSectionReader", func(r io.ReaderAt, off int64, n int64) io.Reader {
|
|
return io.NewSectionReader(r, off, n)
|
|
})
|
|
|
|
ioObj.Set("nopCloser", func(r io.Reader) io.ReadCloser {
|
|
return io.NopCloser(r)
|
|
})
|
|
|
|
_ = vm.Set("$io", ioObj)
|
|
|
|
//////////////////////////////////////
|
|
// bufio
|
|
//////////////////////////////////////
|
|
|
|
bufioObj := vm.NewObject()
|
|
|
|
bufioObj.Set("newReader", func(r io.Reader) *bufio.Reader {
|
|
return bufio.NewReader(r)
|
|
})
|
|
|
|
bufioObj.Set("newReaderSize", func(r io.Reader, size int) *bufio.Reader {
|
|
return bufio.NewReaderSize(r, size)
|
|
})
|
|
|
|
bufioObj.Set("newWriter", func(w io.Writer) *bufio.Writer {
|
|
return bufio.NewWriter(w)
|
|
})
|
|
|
|
bufioObj.Set("newWriterSize", func(w io.Writer, size int) *bufio.Writer {
|
|
return bufio.NewWriterSize(w, size)
|
|
})
|
|
|
|
bufioObj.Set("newScanner", func(r io.Reader) *bufio.Scanner {
|
|
return bufio.NewScanner(r)
|
|
})
|
|
|
|
bufioObj.Set("scanLines", func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
return bufio.ScanLines(data, atEOF)
|
|
})
|
|
|
|
bufioObj.Set("scanWords", func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
return bufio.ScanWords(data, atEOF)
|
|
})
|
|
|
|
bufioObj.Set("scanRunes", func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
return bufio.ScanRunes(data, atEOF)
|
|
})
|
|
|
|
bufioObj.Set("scanBytes", func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
|
return bufio.ScanBytes(data, atEOF)
|
|
})
|
|
|
|
_ = vm.Set("$bufio", bufioObj)
|
|
|
|
//////////////////////////////////////
|
|
// bytes
|
|
//////////////////////////////////////
|
|
|
|
bytesObj := vm.NewObject()
|
|
|
|
bytesObj.Set("newBuffer", func(buf []byte) *bytes.Buffer {
|
|
return bytes.NewBuffer(buf)
|
|
})
|
|
|
|
bytesObj.Set("newBufferString", func(s string) *bytes.Buffer {
|
|
return bytes.NewBufferString(s)
|
|
})
|
|
|
|
bytesObj.Set("newReader", func(b []byte) *bytes.Reader {
|
|
return bytes.NewReader(b)
|
|
})
|
|
|
|
_ = vm.Set("$bytes", bytesObj)
|
|
|
|
//////////////////////////////////////
|
|
// Filepath
|
|
//////////////////////////////////////
|
|
|
|
filepathObj := vm.NewObject()
|
|
|
|
filepathObj.Set("base", filepath.Base)
|
|
filepathObj.Set("clean", filepath.Clean)
|
|
filepathObj.Set("dir", filepath.Dir)
|
|
filepathObj.Set("ext", filepath.Ext)
|
|
filepathObj.Set("fromSlash", filepath.FromSlash)
|
|
|
|
filepathObj.Set("glob", func(basePath string, pattern string) ([]string, error) {
|
|
if !a.isAllowedPath(ext, basePath, AllowPathRead) {
|
|
return nil, fmt.Errorf("$filepath.glob: path (%s) not authorized for read", basePath)
|
|
}
|
|
return doublestar.Glob(os.DirFS(basePath), pattern)
|
|
})
|
|
filepathObj.Set("isAbs", filepath.IsAbs)
|
|
filepathObj.Set("join", filepath.Join)
|
|
filepathObj.Set("match", doublestar.Match)
|
|
filepathObj.Set("rel", filepath.Rel)
|
|
filepathObj.Set("split", filepath.Split)
|
|
filepathObj.Set("splitList", filepath.SplitList)
|
|
filepathObj.Set("toSlash", filepath.ToSlash)
|
|
filepathObj.Set("walk", func(root string, walkFn filepath.WalkFunc) error {
|
|
if !a.isAllowedPath(ext, root, AllowPathRead) {
|
|
return fmt.Errorf("$filepath.walk: path (%s) not authorized for read", root)
|
|
}
|
|
return filepath.Walk(root, walkFn)
|
|
})
|
|
filepathObj.Set("walkDir", func(root string, walkFn fs.WalkDirFunc) error {
|
|
if !a.isAllowedPath(ext, root, AllowPathRead) {
|
|
return fmt.Errorf("$filepath.walkDir: path (%s) not authorized for read", root)
|
|
}
|
|
return filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return walkFn(path, d, err)
|
|
})
|
|
})
|
|
|
|
skipDir := filepath.SkipDir
|
|
filepathObj.Set("skipDir", skipDir)
|
|
|
|
_ = vm.Set("$filepath", filepathObj)
|
|
|
|
//////////////////////////////////////
|
|
// osextra
|
|
//////////////////////////////////////
|
|
|
|
osExtraObj := vm.NewObject()
|
|
|
|
osExtraObj.Set("unwrapAndMove", func(src string, dest string) error {
|
|
if !a.isAllowedPath(ext, src, AllowPathWrite) || !a.isAllowedPath(ext, dest, AllowPathWrite) {
|
|
return fmt.Errorf("$osExtra.unwrapAndMove: path (%s) not authorized for write", src)
|
|
}
|
|
return util.UnwrapAndMove(src, dest)
|
|
})
|
|
|
|
osExtraObj.Set("unzip", func(src string, dest string) error {
|
|
if !a.isAllowedPath(ext, src, AllowPathWrite) || !a.isAllowedPath(ext, dest, AllowPathWrite) {
|
|
return fmt.Errorf("$osExtra.unzip: path (%s) not authorized for write", src)
|
|
}
|
|
return util.UnzipFile(src, dest)
|
|
})
|
|
|
|
osExtraObj.Set("unrar", func(src string, dest string) error {
|
|
if !a.isAllowedPath(ext, src, AllowPathWrite) || !a.isAllowedPath(ext, dest, AllowPathWrite) {
|
|
return fmt.Errorf("$osExtra.unrar: path (%s) not authorized for write", src)
|
|
}
|
|
return util.UnrarFile(src, dest)
|
|
})
|
|
|
|
osExtraObj.Set("downloadDir", func() (string, error) {
|
|
donwloadDir, err := util.DownloadDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !a.isAllowedPath(ext, donwloadDir, AllowPathRead) {
|
|
return "", fmt.Errorf("$osExtra.downloadDir: path not authorized for read")
|
|
}
|
|
return donwloadDir, nil
|
|
})
|
|
|
|
osExtraObj.Set("desktopDir", func() (string, error) {
|
|
desktopDir, err := util.DesktopDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !a.isAllowedPath(ext, desktopDir, AllowPathRead) {
|
|
return "", fmt.Errorf("$osExtra.desktopDir: path not authorized for read")
|
|
}
|
|
return desktopDir, nil
|
|
})
|
|
|
|
osExtraObj.Set("documentsDir", func() (string, error) {
|
|
documentsDir, err := util.DocumentsDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !a.isAllowedPath(ext, documentsDir, AllowPathRead) {
|
|
return "", fmt.Errorf("$osExtra.documentsDir: path not authorized for read")
|
|
}
|
|
return documentsDir, nil
|
|
})
|
|
|
|
osExtraObj.Set("libraryDirs", func() ([]string, error) {
|
|
libraryDirs := []string{}
|
|
if animeLibraryPaths, ok := a.animeLibraryPaths.Get(); ok && len(animeLibraryPaths) > 0 {
|
|
for _, path := range animeLibraryPaths {
|
|
if !a.isAllowedPath(ext, path, AllowPathRead) {
|
|
return nil, fmt.Errorf("$osExtra.libraryDirs: path not authorized for read")
|
|
}
|
|
libraryDirs = append(libraryDirs, path)
|
|
}
|
|
}
|
|
return libraryDirs, nil
|
|
})
|
|
|
|
_ = osExtraObj.Set("asyncCmd", func(name string, arg ...string) *AsyncCmd {
|
|
return &AsyncCmd{
|
|
cmd: util.NewCmdCtx(context.Background(), name, arg...),
|
|
appContext: a,
|
|
scheduler: scheduler,
|
|
vm: vm,
|
|
}
|
|
})
|
|
|
|
_ = vm.Set("$osExtra", osExtraObj)
|
|
|
|
//////////////////////////////////////
|
|
// mime
|
|
//////////////////////////////////////
|
|
|
|
mimeObj := vm.NewObject()
|
|
|
|
mimeObj.Set("parse", func(contentType string) (map[string]interface{}, error) {
|
|
res, params, err := mime.ParseMediaType(contentType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]interface{}{
|
|
"mediaType": res,
|
|
"parameters": params,
|
|
}, nil
|
|
})
|
|
mimeObj.Set("format", mime.FormatMediaType)
|
|
_ = vm.Set("$mime", mimeObj)
|
|
|
|
}
|
|
|
|
// resolveEnvironmentPaths resolves environment paths in the form of $NAME
|
|
// to the actual path.
|
|
//
|
|
// e.g. $SEANIME_LIBRARY_PATH -> /home/user/anime
|
|
func (a *AppContextImpl) resolveEnvironmentPaths(name string) []string {
|
|
|
|
switch name {
|
|
case "SEANIME_ANIME_LIBRARY":
|
|
if animeLibraryPaths, ok := a.animeLibraryPaths.Get(); ok && len(animeLibraryPaths) > 0 {
|
|
return animeLibraryPaths
|
|
}
|
|
return []string{}
|
|
case "HOME": // %USERPROFILE% on Windows
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
return []string{homeDir}
|
|
case "CACHE": // LocalAppData on Windows
|
|
cacheDir, err := os.UserCacheDir()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
return []string{cacheDir}
|
|
case "TEMP": // %TMP%, %TEMP% or %USERPROFILE% on Windows
|
|
tempDir := os.TempDir()
|
|
if tempDir == "" {
|
|
return []string{}
|
|
}
|
|
return []string{tempDir}
|
|
case "CONFIG": // AppData on Windows
|
|
configDir, err := os.UserConfigDir()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
return []string{configDir}
|
|
case "DOWNLOAD":
|
|
downloadDir, err := util.DownloadDir()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
return []string{downloadDir}
|
|
case "DESKTOP":
|
|
desktopDir, err := util.DesktopDir()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
return []string{desktopDir}
|
|
case "DOCUMENT":
|
|
documentsDir, err := util.DocumentsDir()
|
|
if err != nil {
|
|
return []string{}
|
|
}
|
|
return []string{documentsDir}
|
|
}
|
|
|
|
return []string{}
|
|
}
|
|
|
|
func (a *AppContextImpl) isAllowedPath(ext *extension.Extension, path string, mode int) bool {
|
|
// If the extension doesn't have a plugin manifest or system allowlist, deny access
|
|
if ext == nil || ext.Plugin == nil {
|
|
return false
|
|
}
|
|
|
|
// Get the appropriate patterns based on the mode
|
|
var patterns []string
|
|
if mode == AllowPathRead {
|
|
patterns = ext.Plugin.Permissions.Allow.ReadPaths
|
|
} else if mode == AllowPathWrite {
|
|
patterns = ext.Plugin.Permissions.Allow.WritePaths
|
|
} else {
|
|
// Unknown mode
|
|
return false
|
|
}
|
|
|
|
// If no patterns are defined, deny access
|
|
if len(patterns) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Normalize the path to use forward slashes and absolute path
|
|
normalizedPath := path
|
|
if !filepath.IsAbs(normalizedPath) {
|
|
absPath, err := filepath.Abs(normalizedPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
normalizedPath = absPath
|
|
}
|
|
normalizedPath = filepath.ToSlash(normalizedPath)
|
|
|
|
// Check if the path is a directory
|
|
isDir := false
|
|
if stat, err := os.Stat(path); err == nil && stat.IsDir() {
|
|
isDir = true
|
|
// Ensure directory paths end with a slash for proper matching
|
|
if !strings.HasSuffix(normalizedPath, "/") {
|
|
normalizedPath += "/"
|
|
}
|
|
}
|
|
|
|
// Check if the path matches any of the allowed patterns
|
|
for _, pattern := range patterns {
|
|
// Resolve environment variables in the pattern, which may result in multiple patterns
|
|
resolvedPatterns := a.resolvePattern(pattern)
|
|
|
|
for _, resolvedPattern := range resolvedPatterns {
|
|
// Convert to absolute path if needed
|
|
if !filepath.IsAbs(resolvedPattern) && !strings.HasPrefix(resolvedPattern, "*") {
|
|
resolvedPattern = filepath.Join(filepath.Dir(normalizedPath), resolvedPattern)
|
|
}
|
|
|
|
// Direct match attempt
|
|
matched, err := doublestar.Match(resolvedPattern, normalizedPath)
|
|
if err == nil && matched {
|
|
return true
|
|
}
|
|
|
|
// For directories, we need special handling
|
|
if isDir {
|
|
// Case 1: Check if this directory is explicitly allowed by a pattern ending with "/"
|
|
if !strings.HasSuffix(resolvedPattern, "/") {
|
|
dirPattern := resolvedPattern
|
|
if !strings.HasSuffix(dirPattern, "/") {
|
|
dirPattern += "/"
|
|
}
|
|
matched, err = doublestar.Match(dirPattern, normalizedPath)
|
|
if err == nil && matched {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Case 2: Check if this directory is covered by a wildcard pattern
|
|
// Strip trailing wildcards to get the base directory pattern
|
|
basePattern := resolvedPattern
|
|
basePattern = strings.TrimSuffix(basePattern, "/**/*")
|
|
basePattern = strings.TrimSuffix(basePattern, "/**")
|
|
basePattern = strings.TrimSuffix(basePattern, "/*")
|
|
|
|
// Ensure the base pattern ends with a slash for directory comparison
|
|
if !strings.HasSuffix(basePattern, "/") {
|
|
basePattern += "/"
|
|
}
|
|
|
|
// If the path is exactly the base directory or a subdirectory of it
|
|
// AND the original pattern had a wildcard
|
|
if (normalizedPath == basePattern || strings.HasPrefix(normalizedPath, basePattern)) &&
|
|
(strings.HasSuffix(resolvedPattern, "/**") ||
|
|
strings.HasSuffix(resolvedPattern, "/**/*") ||
|
|
strings.HasSuffix(resolvedPattern, "/*")) {
|
|
return true
|
|
}
|
|
|
|
// Case 3: Check if the pattern is for a subdirectory of this directory
|
|
// This handles the case where we're checking access to a parent directory
|
|
// when a subdirectory is explicitly allowed
|
|
if strings.HasPrefix(basePattern, normalizedPath) &&
|
|
(strings.HasSuffix(resolvedPattern, "/**") ||
|
|
strings.HasSuffix(resolvedPattern, "/**/*") ||
|
|
strings.HasSuffix(resolvedPattern, "/*")) {
|
|
return true
|
|
}
|
|
} else {
|
|
// For files, check if any parent directory is allowed with wildcards
|
|
parentDir := filepath.Dir(normalizedPath)
|
|
if !strings.HasSuffix(parentDir, "/") {
|
|
parentDir += "/"
|
|
}
|
|
|
|
// Check if the file's parent directory matches a directory wildcard pattern
|
|
for _, suffix := range []string{"/**/*", "/**", "/*"} {
|
|
if strings.HasSuffix(resolvedPattern, suffix) {
|
|
basePattern := strings.TrimSuffix(resolvedPattern, suffix)
|
|
if !strings.HasSuffix(basePattern, "/") {
|
|
basePattern += "/"
|
|
}
|
|
|
|
if strings.HasPrefix(parentDir, basePattern) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// No matching pattern found, deny access
|
|
return false
|
|
}
|
|
|
|
// resolvePattern resolves environment variables and special placeholders in a pattern
|
|
// Returns a slice of resolved patterns to account for placeholders that can expand to multiple paths
|
|
func (a *AppContextImpl) resolvePattern(pattern string) []string {
|
|
// Start with the original pattern
|
|
patterns := []string{pattern}
|
|
|
|
// Replace special placeholders with their actual values
|
|
placeholders := []string{"$SEANIME_ANIME_LIBRARY", "$HOME", "$CACHE", "$TEMP", "$CONFIG"}
|
|
|
|
for _, placeholder := range placeholders {
|
|
// Extract the placeholder name without the $ prefix
|
|
name := strings.TrimPrefix(placeholder, "$")
|
|
paths := a.resolveEnvironmentPaths(name)
|
|
|
|
if len(paths) == 0 {
|
|
continue
|
|
}
|
|
|
|
// If the placeholder exists in the pattern and expands to multiple paths,
|
|
// we need to create multiple patterns
|
|
if strings.Contains(pattern, placeholder) && len(paths) > 1 {
|
|
// Create a new set of patterns for each path
|
|
newPatterns := []string{}
|
|
|
|
for _, existingPattern := range patterns {
|
|
for _, path := range paths {
|
|
// Ensure proper path separator handling
|
|
cleanPath := filepath.ToSlash(path)
|
|
// Replace the placeholder with the path, ensuring no double slashes
|
|
newPattern := strings.ReplaceAll(existingPattern, placeholder, cleanPath)
|
|
newPatterns = append(newPatterns, newPattern)
|
|
}
|
|
}
|
|
|
|
// Replace the old patterns with the new ones
|
|
patterns = newPatterns
|
|
} else if len(paths) > 0 {
|
|
// If there's only one path or the placeholder doesn't exist,
|
|
// just replace it in all existing patterns
|
|
for i := range patterns {
|
|
// Ensure proper path separator handling
|
|
cleanPath := filepath.ToSlash(paths[0])
|
|
// Replace the placeholder with the path, ensuring no double slashes
|
|
patterns[i] = strings.ReplaceAll(patterns[i], placeholder, cleanPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace environment variables in all patterns
|
|
for i := range patterns {
|
|
patterns[i] = os.ExpandEnv(patterns[i])
|
|
}
|
|
|
|
// Clean up any potential double slashes that might have been introduced
|
|
for i := range patterns {
|
|
// Replace any double slashes with single slashes
|
|
for strings.Contains(patterns[i], "//") {
|
|
patterns[i] = strings.ReplaceAll(patterns[i], "//", "/")
|
|
}
|
|
}
|
|
|
|
return patterns
|
|
}
|
|
|
|
func (a *AppContextImpl) isAllowedCommand(ext *extension.Extension, name string, arg ...string) bool {
|
|
// If the extension doesn't have a plugin manifest or system allowlist, deny access
|
|
if ext == nil || ext.Plugin == nil {
|
|
return false
|
|
}
|
|
|
|
// Get the system allowlist
|
|
allowlist := ext.Plugin.Permissions.Allow
|
|
|
|
// Check if the command is allowed in any of the command scopes
|
|
for _, scope := range allowlist.CommandScopes {
|
|
// Check if the command name matches
|
|
if scope.Command != name {
|
|
continue
|
|
}
|
|
|
|
// If no args are defined in the scope but args are provided, deny access
|
|
if len(scope.Args) == 0 && len(arg) > 0 {
|
|
continue
|
|
}
|
|
|
|
// Check if the arguments match the allowed pattern
|
|
if !a.validateCommandArgs(ext, scope.Args, arg) {
|
|
continue
|
|
}
|
|
|
|
// Command and args match the scope, allow access
|
|
return true
|
|
}
|
|
|
|
// No matching command scope found, deny access
|
|
return false
|
|
}
|
|
|
|
// validateCommandArgs checks if the provided arguments match the allowed pattern
|
|
func (a *AppContextImpl) validateCommandArgs(ext *extension.Extension, allowedArgs []extension.CommandArg, providedArgs []string) bool {
|
|
// If more args are provided than allowed, deny access
|
|
if len(providedArgs) > len(allowedArgs) {
|
|
if len(allowedArgs) > 0 && allowedArgs[len(allowedArgs)-1].Validator == "$ARGS" {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check each argument
|
|
for i, allowedArg := range allowedArgs {
|
|
// If we've reached the end of the provided args, deny access
|
|
if i >= len(providedArgs) {
|
|
return false
|
|
}
|
|
|
|
// If the argument has a fixed value, check if it matches exactly
|
|
if allowedArg.Value != "" {
|
|
if allowedArg.Value != providedArgs[i] {
|
|
return false
|
|
}
|
|
continue
|
|
}
|
|
|
|
// If the argument has a validator, check if it matches
|
|
if allowedArg.Validator != "" {
|
|
// Special case: $ARGS allows any value for the rest of the arguments
|
|
if allowedArg.Validator == "$ARGS" {
|
|
return true
|
|
}
|
|
|
|
// Special case: $PATH allows any valid file path
|
|
if allowedArg.Validator == "$PATH" {
|
|
// Simple path validation - could be enhanced
|
|
if providedArgs[i] == "" {
|
|
return false
|
|
}
|
|
|
|
// Check if the path is allowed
|
|
if !a.isAllowedPath(ext, providedArgs[i], AllowPathWrite) {
|
|
return false
|
|
}
|
|
|
|
// Check if the path exists
|
|
if _, err := os.Stat(providedArgs[i]); os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// Use regex validation for other validators
|
|
matched, err := regexp.MatchString(allowedArg.Validator, providedArgs[i])
|
|
if err != nil || !matched {
|
|
return false
|
|
}
|
|
continue
|
|
}
|
|
|
|
// If neither value nor validator is specified, deny access
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// GetCommand returns the underlying exec.Cmd
|
|
func (c *AsyncCmd) GetCommand() *exec.Cmd {
|
|
return c.cmd
|
|
}
|
|
|
|
func (c *AsyncCmd) Run(callback goja.Callable) error {
|
|
|
|
stdout, err := c.cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stderr, err := c.cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start the command
|
|
err = c.cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use WaitGroup to ensure all goroutines complete before Wait() is called
|
|
var wg sync.WaitGroup
|
|
wg.Add(2) // One for stdout, one for stderr
|
|
|
|
// Handle stdout
|
|
go func() {
|
|
defer wg.Done()
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
data := scanner.Bytes()
|
|
dataBytes := make([]byte, len(data))
|
|
copy(dataBytes, data)
|
|
|
|
c.scheduler.ScheduleAsync(func() error {
|
|
_, err := callback(goja.Undefined(), c.vm.ToValue(dataBytes), goja.Undefined(), goja.Undefined(), goja.Undefined())
|
|
return err
|
|
})
|
|
}
|
|
}()
|
|
|
|
// Handle stderr
|
|
go func() {
|
|
defer wg.Done()
|
|
scanner := bufio.NewScanner(stderr)
|
|
for scanner.Scan() {
|
|
data := scanner.Bytes()
|
|
dataBytes := make([]byte, len(data))
|
|
copy(dataBytes, data)
|
|
|
|
c.scheduler.ScheduleAsync(func() error {
|
|
_, err := callback(goja.Undefined(), goja.Undefined(), c.vm.ToValue(dataBytes), goja.Undefined(), goja.Undefined())
|
|
return err
|
|
})
|
|
}
|
|
}()
|
|
|
|
// Wait for both stdout and stderr to be fully processed in a separate goroutine
|
|
go func() {
|
|
// Wait for stdout and stderr goroutines to complete
|
|
wg.Wait()
|
|
|
|
// Now wait for the command to finish
|
|
err := c.cmd.Wait()
|
|
|
|
_ = stdout.Close()
|
|
_ = stderr.Close()
|
|
|
|
// Process exit code and signal
|
|
exitCode := 0
|
|
signal := ""
|
|
|
|
var exitErr *exec.ExitError
|
|
if errors.As(err, &exitErr) {
|
|
exitCode = exitErr.ExitCode()
|
|
signal = exitErr.String()
|
|
} else if c.cmd.ProcessState != nil {
|
|
exitCode = c.cmd.ProcessState.ExitCode()
|
|
signal = c.cmd.ProcessState.String()
|
|
}
|
|
|
|
// Call callback with exit code and signal
|
|
c.scheduler.ScheduleAsync(func() error {
|
|
_, err = callback(goja.Undefined(), goja.Undefined(), goja.Undefined(), c.vm.ToValue(exitCode), c.vm.ToValue(signal))
|
|
return err
|
|
})
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
func (c *CmdHelper) Run(callback goja.Callable) error {
|
|
return nil
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// // OnData registers a callback to be called when data is available from the command's stdout
|
|
// func (c *AsyncCmd) OnData(callback func(data []byte)) error {
|
|
// stdout, err := c.cmd.StdoutPipe()
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// go func() {
|
|
// scanner := bufio.NewScanner(stdout)
|
|
// for scanner.Scan() {
|
|
// c.scheduler.ScheduleAsync(func() error {
|
|
// callback(scanner.Bytes())
|
|
// return nil
|
|
// })
|
|
// }
|
|
// }()
|
|
|
|
// return nil
|
|
// }
|
|
|
|
// // OnError registers a callback to be called when data is available from the command's stderr
|
|
// func (c *AsyncCmd) OnError(callback func(data []byte)) error {
|
|
// stderr, err := c.cmd.StderrPipe()
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
|
|
// go func() {
|
|
// scanner := bufio.NewScanner(stderr)
|
|
// for scanner.Scan() {
|
|
// c.scheduler.ScheduleAsync(func() error {
|
|
// callback(scanner.Bytes())
|
|
// return nil
|
|
// })
|
|
// }
|
|
// }()
|
|
|
|
// return nil
|
|
// }
|
|
|
|
// // OnExit registers a callback to be called when the command exits
|
|
// func (c *AsyncCmd) OnExit(callback func(code int, signal string)) error {
|
|
// go func() {
|
|
// err := c.cmd.Wait()
|
|
// if err != nil {
|
|
// return
|
|
// }
|
|
// c.scheduler.ScheduleAsync(func() error {
|
|
// callback(c.cmd.ProcessState.ExitCode(), c.cmd.ProcessState.String())
|
|
// return nil
|
|
// })
|
|
// }()
|
|
// return nil
|
|
// }
|