Files
silo-server/internal/plugins/archive_cache.go
T

186 lines
5.5 KiB
Go

package plugins
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
pluginv1 "github.com/Silo-Server/silo-plugin-sdk/pkg/pluginproto/silo/plugin/v1"
)
type archiveStore interface {
GetArchive(ctx context.Context, installationID int) (*InstallationArchive, error)
}
type ArchiveCache struct {
archives archiveStore
}
func NewArchiveCache(archives archiveStore) *ArchiveCache {
if archives == nil {
return nil
}
return &ArchiveCache{archives: archives}
}
func (c *ArchiveCache) Ensure(ctx context.Context, installation *Installation) (*pluginv1.PluginManifest, error) {
if installation == nil {
return nil, fmt.Errorf("plugin installation is required")
}
if manifest, err := LoadManifestFile(InstalledManifestPath(installation.InstallPath)); err == nil {
if err := installedFilesPresent(installation.InstallPath, manifest); err == nil {
return manifest, nil
}
}
archive, err := c.archives.GetArchive(ctx, installation.ID)
if err != nil {
return nil, fmt.Errorf("load stored plugin archive for installation %d: %w", installation.ID, err)
}
reader, manifestBytes, manifest, err := openPluginArchive(archive.Bytes)
if err != nil {
return nil, fmt.Errorf("open stored plugin archive for installation %d: %w", installation.ID, err)
}
if archive.Checksum != manifest.GetChecksum() {
return nil, fmt.Errorf("stored plugin archive checksum mismatch for installation %d", installation.ID)
}
if len(archive.ManifestJSON) > 0 && !bytes.Equal(archive.ManifestJSON, manifestBytes) {
return nil, fmt.Errorf("stored plugin manifest mismatch for installation %d", installation.ID)
}
if installation.PluginID != "" && manifest.GetPluginId() != installation.PluginID {
return nil, fmt.Errorf(
"stored plugin archive plugin_id %q does not match installation %q",
manifest.GetPluginId(),
installation.PluginID,
)
}
if installation.Version != "" && manifest.GetVersion() != installation.Version {
return nil, fmt.Errorf(
"stored plugin archive version %q does not match installation %q",
manifest.GetVersion(),
installation.Version,
)
}
installDir := filepath.Dir(installation.InstallPath)
if err := os.RemoveAll(installDir); err != nil {
return nil, fmt.Errorf("clear plugin cache dir %q: %w", installDir, err)
}
if err := os.MkdirAll(installDir, 0755); err != nil {
return nil, fmt.Errorf("create plugin cache dir %q: %w", installDir, err)
}
if err := extractArchiveFiles(reader, installDir); err != nil {
_ = os.RemoveAll(installDir)
return nil, fmt.Errorf("extract stored plugin archive for installation %d: %w", installation.ID, err)
}
if err := validateInstalledFiles(installation.InstallPath, manifest); err != nil {
_ = os.RemoveAll(installDir)
return nil, fmt.Errorf("validate rehydrated plugin cache for installation %d: %w", installation.ID, err)
}
return manifest, nil
}
func openPluginArchive(data []byte) (*zip.Reader, []byte, *pluginv1.PluginManifest, error) {
reader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, nil, nil, fmt.Errorf("open plugin archive: %w", err)
}
files := make(map[string]*zip.File, len(reader.File))
for _, file := range reader.File {
files[file.Name] = file
}
manifestFile, ok := files["manifest.json"]
if !ok {
return nil, nil, nil, fmt.Errorf("plugin archive is missing manifest.json")
}
binaryFile, ok := files["plugin"]
if !ok {
return nil, nil, nil, fmt.Errorf("plugin archive is missing plugin binary")
}
manifestBytes, err := readZipFile(manifestFile)
if err != nil {
return nil, nil, nil, err
}
manifest, err := LoadManifestBytes(manifestBytes)
if err != nil {
return nil, nil, nil, err
}
binaryBytes, err := readZipFile(binaryFile)
if err != nil {
return nil, nil, nil, err
}
checksum := sha256.Sum256(binaryBytes)
if manifest.GetChecksum() != hex.EncodeToString(checksum[:]) {
return nil, nil, nil, fmt.Errorf("plugin binary checksum does not match manifest")
}
for _, asset := range manifest.GetAssets() {
if _, ok := files[asset.GetPath()]; !ok {
return nil, nil, nil, fmt.Errorf("plugin archive is missing packaged asset %q", asset.GetPath())
}
}
return reader, manifestBytes, manifest, nil
}
func extractArchiveFiles(reader *zip.Reader, root string) error {
for _, file := range reader.File {
if err := extractZipFile(file, root); err != nil {
return err
}
}
return nil
}
func validateInstalledFiles(binaryPath string, manifest *pluginv1.PluginManifest) error {
if err := installedFilesPresent(binaryPath, manifest); err != nil {
return err
}
binaryBytes, err := os.ReadFile(binaryPath)
if err != nil {
return fmt.Errorf("read plugin binary %q: %w", binaryPath, err)
}
checksum := sha256.Sum256(binaryBytes)
if manifest.GetChecksum() != hex.EncodeToString(checksum[:]) {
return fmt.Errorf("plugin binary checksum does not match manifest")
}
return nil
}
func installedFilesPresent(binaryPath string, manifest *pluginv1.PluginManifest) error {
binaryInfo, err := os.Stat(binaryPath)
if err != nil {
return fmt.Errorf("plugin binary %q: %w", binaryPath, err)
}
if binaryInfo.IsDir() {
return fmt.Errorf("plugin binary %q is a directory", binaryPath)
}
for _, asset := range manifest.GetAssets() {
resolved := filepath.Join(filepath.Dir(binaryPath), asset.GetPath())
info, err := os.Stat(resolved)
if err != nil {
return fmt.Errorf("plugin asset %q: %w", asset.GetPath(), err)
}
if info.IsDir() {
return fmt.Errorf("plugin asset %q resolved to a directory", asset.GetPath())
}
}
return nil
}