Files
navidrome-meilisearch/plugins/package.go
Dongho Kim c251f174ed
Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
update
2025-12-08 16:16:23 +01:00

178 lines
4.8 KiB
Go

package plugins
import (
"archive/zip"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/navidrome/navidrome/plugins/schema"
)
// PluginPackage represents a Navidrome Plugin Package (.ndp file)
type PluginPackage struct {
ManifestJSON []byte
Manifest *schema.PluginManifest
WasmBytes []byte
Docs map[string][]byte
}
// ExtractPackage extracts a .ndp file to the target directory
func ExtractPackage(ndpPath, targetDir string) error {
r, err := zip.OpenReader(ndpPath)
if err != nil {
return fmt.Errorf("error opening .ndp file: %w", err)
}
defer r.Close()
// Create target directory if it doesn't exist
if err := os.MkdirAll(targetDir, 0755); err != nil {
return fmt.Errorf("error creating plugin directory: %w", err)
}
// Define a reasonable size limit for plugin files to prevent decompression bombs
const maxFileSize = 10 * 1024 * 1024 // 10 MB limit
// Extract all files from the zip
for _, f := range r.File {
// Skip directories (they will be created as needed)
if f.FileInfo().IsDir() {
continue
}
// Create the file path for extraction
// Validate the file name to prevent directory traversal or absolute paths
if strings.Contains(f.Name, "..") || filepath.IsAbs(f.Name) {
return fmt.Errorf("illegal file path in plugin package: %s", f.Name)
}
// Create the file path for extraction
targetPath := filepath.Join(targetDir, f.Name) // #nosec G305
// Clean the path to prevent directory traversal.
cleanedPath := filepath.Clean(targetPath)
// Ensure the cleaned path is still within the target directory.
// We resolve both paths to absolute paths to be sure.
absTargetDir, err := filepath.Abs(targetDir)
if err != nil {
return fmt.Errorf("failed to resolve target directory path: %w", err)
}
absTargetPath, err := filepath.Abs(cleanedPath)
if err != nil {
return fmt.Errorf("failed to resolve extracted file path: %w", err)
}
if !strings.HasPrefix(absTargetPath, absTargetDir+string(os.PathSeparator)) && absTargetPath != absTargetDir {
return fmt.Errorf("illegal file path in plugin package: %s", f.Name)
}
// Open the file inside the zip
rc, err := f.Open()
if err != nil {
return fmt.Errorf("error opening file in plugin package: %w", err)
}
// Create parent directories if they don't exist
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
rc.Close()
return fmt.Errorf("error creating directory structure: %w", err)
}
// Create the file
outFile, err := os.Create(targetPath)
if err != nil {
rc.Close()
return fmt.Errorf("error creating extracted file: %w", err)
}
// Copy the file contents with size limit
if _, err := io.CopyN(outFile, rc, maxFileSize); err != nil && !errors.Is(err, io.EOF) {
outFile.Close()
rc.Close()
if errors.Is(err, io.ErrUnexpectedEOF) { // File size exceeds limit
return fmt.Errorf("error extracting file: size exceeds limit (%d bytes) for %s", maxFileSize, f.Name)
}
return fmt.Errorf("error writing extracted file: %w", err)
}
outFile.Close()
rc.Close()
// Set appropriate file permissions (0600 - readable only by owner)
if err := os.Chmod(targetPath, 0600); err != nil {
return fmt.Errorf("error setting permissions on extracted file: %w", err)
}
}
return nil
}
// LoadPackage loads and validates an .ndp file without extracting it
func LoadPackage(ndpPath string) (*PluginPackage, error) {
r, err := zip.OpenReader(ndpPath)
if err != nil {
return nil, fmt.Errorf("error opening .ndp file: %w", err)
}
defer r.Close()
pkg := &PluginPackage{
Docs: make(map[string][]byte),
}
// Required files
var hasManifest, hasWasm bool
// Read all files in the zip
for _, f := range r.File {
// Skip directories
if f.FileInfo().IsDir() {
continue
}
// Get file content
rc, err := f.Open()
if err != nil {
return nil, fmt.Errorf("error opening file in plugin package: %w", err)
}
content, err := io.ReadAll(rc)
rc.Close()
if err != nil {
return nil, fmt.Errorf("error reading file in plugin package: %w", err)
}
// Process based on file name
switch strings.ToLower(f.Name) {
case "manifest.json":
pkg.ManifestJSON = content
hasManifest = true
case "plugin.wasm":
pkg.WasmBytes = content
hasWasm = true
default:
// Store other files as documentation
pkg.Docs[f.Name] = content
}
}
// Ensure required files exist
if !hasManifest {
return nil, fmt.Errorf("plugin package missing required manifest.json")
}
if !hasWasm {
return nil, fmt.Errorf("plugin package missing required plugin.wasm")
}
// Parse and validate the manifest
var manifest schema.PluginManifest
if err := json.Unmarshal(pkg.ManifestJSON, &manifest); err != nil {
return nil, fmt.Errorf("invalid manifest: %w", err)
}
pkg.Manifest = &manifest
return pkg, nil
}