Files
SpotiFLAC-Mobile/go_backend/extension_manager_package.go
T

213 lines
6.1 KiB
Go

package gobackend
import (
"archive/zip"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strconv"
"strings"
)
func compareVersions(v1, v2 string) int {
parts1 := strings.Split(strings.TrimPrefix(v1, "v"), ".")
parts2 := strings.Split(strings.TrimPrefix(v2, "v"), ".")
maxLen := len(parts1)
if len(parts2) > maxLen {
maxLen = len(parts2)
}
for i := 0; i < maxLen; i++ {
var n1, n2 int
if i < len(parts1) {
n1, _ = strconv.Atoi(parts1[i])
}
if i < len(parts2) {
n2, _ = strconv.Atoi(parts2[i])
}
if n1 < n2 {
return -1
}
if n1 > n2 {
return 1
}
}
return 0
}
func isExtensionPackagePath(filePath string) bool {
lowerPath := strings.ToLower(filePath)
return strings.HasSuffix(lowerPath, ".spotiflac-ext") || strings.HasSuffix(lowerPath, ".sflx")
}
func managedExtensionPath(root, extensionID string) (string, error) {
if root == "" {
return "", fmt.Errorf("extension directory is not configured")
}
if !extensionIDPattern.MatchString(extensionID) {
return "", fmt.Errorf("invalid extension ID %q", extensionID)
}
fullPath := filepath.Join(root, extensionID)
if !isPathWithinBase(root, fullPath) {
return "", fmt.Errorf("extension path escapes its managed directory")
}
return fullPath, nil
}
func safeExtensionAssetPath(root, assetPath string) (string, bool) {
if root == "" || assetPath == "" || filepath.IsAbs(assetPath) || strings.Contains(assetPath, `\`) {
return "", false
}
cleaned := path.Clean(assetPath)
if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, "../") {
return "", false
}
fullPath := filepath.Join(root, filepath.FromSlash(cleaned))
return fullPath, isPathWithinBase(root, fullPath)
}
const (
maxExtensionArchiveEntries = 2048
maxExtensionArchiveUncompressedBytes = 256 * 1024 * 1024
maxExtensionManifestBytes = 1024 * 1024
)
func validateExtensionArchive(files []*zip.File) error {
if len(files) > maxExtensionArchiveEntries {
return fmt.Errorf(
"extension archive contains too many entries (maximum %d)",
maxExtensionArchiveEntries,
)
}
seenPaths := make(map[string]struct{}, len(files))
var totalUncompressed uint64
for _, file := range files {
if file.FileInfo().Mode()&os.ModeSymlink != 0 || strings.Contains(file.Name, `\`) {
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
}
relPath := path.Clean(file.Name)
if relPath == "." || relPath == ".." || strings.HasPrefix(relPath, "../") || path.IsAbs(relPath) {
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
}
pathKey := strings.ToLower(relPath)
if _, exists := seenPaths[pathKey]; exists {
return fmt.Errorf("duplicate path in extension archive: %s", file.Name)
}
seenPaths[pathKey] = struct{}{}
if file.FileInfo().IsDir() {
continue
}
if file.UncompressedSize64 > maxExtensionArchiveUncompressedBytes-totalUncompressed {
return fmt.Errorf(
"extension archive exceeds the %d MiB extracted size limit",
maxExtensionArchiveUncompressedBytes/(1024*1024),
)
}
totalUncompressed += file.UncompressedSize64
}
return nil
}
func inspectExtensionPackage(files []*zip.File) (*ExtensionManifest, error) {
if err := validateExtensionArchive(files); err != nil {
return nil, err
}
var manifestFile *zip.File
hasIndexJS := false
for _, file := range files {
switch path.Clean(file.Name) {
case "manifest.json":
manifestFile = file
case "index.js":
hasIndexJS = !file.FileInfo().IsDir()
}
}
if manifestFile == nil || manifestFile.FileInfo().IsDir() {
return nil, fmt.Errorf("invalid extension package: root manifest.json not found")
}
if !hasIndexJS {
return nil, fmt.Errorf("invalid extension package: root index.js not found")
}
if manifestFile.UncompressedSize64 > maxExtensionManifestBytes {
return nil, fmt.Errorf("invalid extension package: manifest.json is too large")
}
rc, err := manifestFile.Open()
if err != nil {
return nil, fmt.Errorf("failed to open manifest.json: %w", err)
}
manifestData, readErr := io.ReadAll(io.LimitReader(rc, maxExtensionManifestBytes+1))
closeErr := rc.Close()
if readErr != nil {
return nil, fmt.Errorf("failed to read manifest.json: %w", readErr)
}
if closeErr != nil {
return nil, fmt.Errorf("failed to close manifest.json: %w", closeErr)
}
if len(manifestData) > maxExtensionManifestBytes {
return nil, fmt.Errorf("invalid extension package: manifest.json is too large")
}
manifest, err := ParseManifest(manifestData)
if err != nil {
return nil, fmt.Errorf("invalid extension manifest: %w", err)
}
return manifest, nil
}
func extractExtensionArchive(zipReader *zip.ReadCloser, destination string) error {
if err := validateExtensionArchive(zipReader.File); err != nil {
return err
}
for _, file := range zipReader.File {
if file.FileInfo().IsDir() {
continue
}
if file.FileInfo().Mode()&os.ModeSymlink != 0 || strings.Contains(file.Name, `\`) {
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
}
relPath := path.Clean(file.Name)
if relPath == "." || relPath == ".." || strings.HasPrefix(relPath, "../") || path.IsAbs(relPath) {
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
}
destPath := filepath.Join(destination, filepath.FromSlash(relPath))
if !isPathWithinBase(destination, destPath) {
return fmt.Errorf("unsafe path in extension archive: %s", file.Name)
}
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return fmt.Errorf("failed to create extension directory: %w", err)
}
destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to create extension file: %w", err)
}
srcFile, err := file.Open()
if err != nil {
destFile.Close()
return fmt.Errorf("failed to open file in archive: %w", err)
}
_, copyErr := io.Copy(destFile, srcFile)
closeSrcErr := srcFile.Close()
closeDestErr := destFile.Close()
if copyErr != nil {
return fmt.Errorf("failed to extract extension file: %w", copyErr)
}
if closeSrcErr != nil || closeDestErr != nil {
return fmt.Errorf("failed to close extracted extension file")
}
}
return nil
}