Some checks failed
Go / build (.exe, 386, windows, windows-386) (push) Has been cancelled
Go / build (.exe, amd64, windows, windows-amd64) (push) Has been cancelled
Go / build (.exe, arm64, windows, windows-arm64) (push) Has been cancelled
Go / build (386, freebsd, freebsd-386) (push) Has been cancelled
Go / build (386, linux, linux-386) (push) Has been cancelled
Go / build (386, netbsd, netbsd-386) (push) Has been cancelled
Go / build (386, openbsd, openbsd-386) (push) Has been cancelled
Go / build (386, plan9, plan9-386) (push) Has been cancelled
Go / build (amd64, darwin, darwin-amd64) (push) Has been cancelled
Go / build (amd64, dragonfly, dragonfly-amd64) (push) Has been cancelled
Go / build (amd64, freebsd, freebsd-amd64) (push) Has been cancelled
Go / build (amd64, illumos, illumos-amd64) (push) Has been cancelled
Go / build (amd64, linux, linux-amd64) (push) Has been cancelled
Go / build (amd64, netbsd, netbsd-amd64) (push) Has been cancelled
Go / build (amd64, openbsd, openbsd-amd64) (push) Has been cancelled
Go / build (amd64, plan9, plan9-amd64) (push) Has been cancelled
Go / build (amd64, solaris, solaris-amd64) (push) Has been cancelled
Go / build (arm, 6, linux, linux-armv6) (push) Has been cancelled
Go / build (arm, 7, linux, linux-armv7) (push) Has been cancelled
Go / build (arm, freebsd, freebsd-arm) (push) Has been cancelled
Go / build (arm, netbsd, netbsd-arm) (push) Has been cancelled
Go / build (arm, openbsd, openbsd-arm) (push) Has been cancelled
Go / build (arm, plan9, plan9-arm) (push) Has been cancelled
Go / build (arm64, darwin, darwin-arm64) (push) Has been cancelled
Go / build (arm64, freebsd, freebsd-arm64) (push) Has been cancelled
Go / build (arm64, linux, linux-arm64) (push) Has been cancelled
Go / build (arm64, netbsd, netbsd-arm64) (push) Has been cancelled
Go / build (arm64, openbsd, openbsd-arm64) (push) Has been cancelled
Go / build (loong64, linux, linux-loong64) (push) Has been cancelled
Go / build (mips, linux, linux-mips) (push) Has been cancelled
Go / build (mips64, linux, linux-mips64) (push) Has been cancelled
Go / build (mips64le, linux, linux-mips64le) (push) Has been cancelled
Go / build (mipsle, linux, linux-mipsle) (push) Has been cancelled
Go / build (ppc64, aix, aix-ppc64) (push) Has been cancelled
Go / build (ppc64, linux, linux-ppc64) (push) Has been cancelled
Go / build (ppc64, openbsd, openbsd-ppc64) (push) Has been cancelled
Go / build (ppc64le, linux, linux-ppc64le) (push) Has been cancelled
Go / build (riscv64, freebsd, freebsd-riscv64) (push) Has been cancelled
Go / build (riscv64, linux, linux-riscv64) (push) Has been cancelled
Go / build (riscv64, openbsd, openbsd-riscv64) (push) Has been cancelled
Go / build (s390x, linux, linux-s390x) (push) Has been cancelled
Go / merge-artifacts (push) Has been cancelled
Docker Image / docker (push) Has been cancelled
604 lines
20 KiB
Go
604 lines
20 KiB
Go
package pluginmanager
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var pluginIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,127}$`)
|
|
|
|
type ArtifactStore struct {
|
|
Root string
|
|
MaxPackageBytes int64
|
|
MaxManifestBytes int64
|
|
MaxEntries int
|
|
MaxExtractedBytes int64
|
|
MaxNonRuntimeBytes int64
|
|
now func() time.Time
|
|
}
|
|
|
|
type ArtifactUpload struct {
|
|
SourcePath string
|
|
FileName string
|
|
Actor string
|
|
}
|
|
|
|
func NewArtifactStore(root string) ArtifactStore {
|
|
return ArtifactStore{
|
|
Root: root,
|
|
MaxPackageBytes: DefaultPackageMaxBytes,
|
|
MaxManifestBytes: DefaultManifestMaxBytes,
|
|
MaxEntries: DefaultPackageMaxEntries,
|
|
MaxExtractedBytes: DefaultExtractedMaxBytes,
|
|
MaxNonRuntimeBytes: DefaultNonRuntimeMaxBytes,
|
|
now: time.Now,
|
|
}
|
|
}
|
|
|
|
func (s ArtifactStore) ValidateAndStore(upload ArtifactUpload) (ArtifactRecord, error) {
|
|
return s.validateAndStore(upload, "")
|
|
}
|
|
|
|
func (s ArtifactStore) ValidateAndStoreBinary(upload ArtifactUpload) (ArtifactRecord, error) {
|
|
return s.validateAndStore(upload, ArtifactTypeBinary)
|
|
}
|
|
|
|
func (s ArtifactStore) ValidateAndStoreSource(upload ArtifactUpload) (ArtifactRecord, error) {
|
|
return s.validateAndStore(upload, ArtifactTypeSource)
|
|
}
|
|
|
|
func (s ArtifactStore) StoreBuiltBinary(upload ArtifactUpload, manifest Manifest, pluginBytes []byte, packageSHA string, metadata map[string]any) (ArtifactRecord, error) {
|
|
if s.Root == "" {
|
|
return ArtifactRecord{}, errors.New("plugin artifact root is empty")
|
|
}
|
|
if s.now == nil {
|
|
s.now = time.Now
|
|
}
|
|
manifest.ArtifactType = ArtifactTypeBinary
|
|
manifest.Runtime.Entry = RuntimeEntry
|
|
if err := validateManifest(manifest); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if len(pluginBytes) == 0 {
|
|
return ArtifactRecord{}, errors.New("built runtime entry is empty")
|
|
}
|
|
pluginSum := sha256.Sum256(pluginBytes)
|
|
artifactID := hex.EncodeToString(pluginSum[:])
|
|
artifactDir := filepath.Join(s.Root, manifest.ID, artifactID)
|
|
if err := os.MkdirAll(artifactDir, 0755); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
pluginPath := filepath.Join(artifactDir, RuntimeEntry)
|
|
if err := os.WriteFile(pluginPath, pluginBytes, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(artifactDir, "manifest.json"), manifestBytes, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if len(metadata) > 0 {
|
|
provenance, err := json.MarshalIndent(metadata, "", " ")
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(artifactDir, "provenance.json"), provenance, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
}
|
|
extensionPoints, err := json.Marshal(extensionPointKeys(manifest))
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
capabilities, err := capabilitiesSummaryJSON(manifest.Capabilities)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
now := s.now().Unix()
|
|
return ArtifactRecord{
|
|
ID: artifactID,
|
|
PluginID: manifest.ID,
|
|
Version: manifest.Version,
|
|
FileName: upload.FileName,
|
|
FilePath: pluginPath,
|
|
SHA256: artifactID,
|
|
PackageSHA256: packageSHA,
|
|
SizeBytes: int64(len(pluginBytes)),
|
|
ArtifactType: ArtifactTypeBinary,
|
|
RuntimeType: manifest.Runtime.Type,
|
|
RuntimeEntry: manifest.Runtime.Entry,
|
|
Status: ArtifactStatusLoadable,
|
|
MetadataJSON: string(manifestBytes),
|
|
CapabilitiesSummaryJSON: string(capabilities),
|
|
ExtensionPointsJSON: string(extensionPoints),
|
|
APIVersion: manifest.APIVersion,
|
|
GoVersion: manifest.GoVersion,
|
|
GOOS: manifest.GOOS,
|
|
GOARCH: manifest.GOARCH,
|
|
UploadedBy: upload.Actor,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
func (s ArtifactStore) validateAndStore(upload ArtifactUpload, expectedArtifactType string) (ArtifactRecord, error) {
|
|
if s.Root == "" {
|
|
return ArtifactRecord{}, errors.New("plugin artifact root is empty")
|
|
}
|
|
if s.MaxPackageBytes <= 0 {
|
|
s.MaxPackageBytes = DefaultPackageMaxBytes
|
|
}
|
|
if s.MaxManifestBytes <= 0 {
|
|
s.MaxManifestBytes = DefaultManifestMaxBytes
|
|
}
|
|
if s.MaxEntries <= 0 {
|
|
s.MaxEntries = DefaultPackageMaxEntries
|
|
}
|
|
if s.MaxExtractedBytes <= 0 {
|
|
s.MaxExtractedBytes = DefaultExtractedMaxBytes
|
|
}
|
|
if s.MaxNonRuntimeBytes <= 0 {
|
|
s.MaxNonRuntimeBytes = DefaultNonRuntimeMaxBytes
|
|
}
|
|
if s.now == nil {
|
|
s.now = time.Now
|
|
}
|
|
|
|
info, err := os.Stat(upload.SourcePath)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if info.Size() <= 0 {
|
|
return ArtifactRecord{}, errors.New("plugin package is empty")
|
|
}
|
|
if info.Size() > s.MaxPackageBytes {
|
|
return ArtifactRecord{}, fmt.Errorf("plugin package size %d exceeds limit %d", info.Size(), s.MaxPackageBytes)
|
|
}
|
|
|
|
packageSHA, err := fileSHA256(upload.SourcePath)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
|
|
reader, err := zip.OpenReader(upload.SourcePath)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
defer reader.Close()
|
|
if len(reader.File) > s.MaxEntries {
|
|
return ArtifactRecord{}, fmt.Errorf("plugin package has %d entries, exceeds limit %d", len(reader.File), s.MaxEntries)
|
|
}
|
|
|
|
var manifestFile *zip.File
|
|
entries := make(map[string]*zip.File)
|
|
var extractedSize uint64
|
|
for _, file := range reader.File {
|
|
if file.FileInfo().IsDir() {
|
|
if _, err := cleanZipDirName(file.Name); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
continue
|
|
}
|
|
clean, err := cleanZipName(file.Name)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
mode := file.FileInfo().Mode()
|
|
if !mode.IsRegular() || mode&os.ModeType != 0 {
|
|
return ArtifactRecord{}, fmt.Errorf("unsupported zip entry type %q", file.Name)
|
|
}
|
|
if _, exists := entries[clean]; exists {
|
|
return ArtifactRecord{}, fmt.Errorf("duplicate zip entry %q", clean)
|
|
}
|
|
extractedSize += file.UncompressedSize64
|
|
if extractedSize > uint64(s.MaxExtractedBytes) {
|
|
return ArtifactRecord{}, fmt.Errorf("plugin package extracted size exceeds limit %d", s.MaxExtractedBytes)
|
|
}
|
|
entries[clean] = file
|
|
if clean == "manifest.json" {
|
|
manifestFile = file
|
|
}
|
|
}
|
|
if manifestFile == nil {
|
|
return ArtifactRecord{}, errors.New("manifest.json is required")
|
|
}
|
|
if manifestFile.UncompressedSize64 > uint64(s.MaxManifestBytes) {
|
|
return ArtifactRecord{}, fmt.Errorf("manifest.json size %d exceeds limit %d", manifestFile.UncompressedSize64, s.MaxManifestBytes)
|
|
}
|
|
|
|
manifestBytes, err := readZipFile(manifestFile, s.MaxManifestBytes)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
var manifest Manifest
|
|
if err := json.Unmarshal(manifestBytes, &manifest); err != nil {
|
|
return ArtifactRecord{}, fmt.Errorf("invalid manifest.json: %w", err)
|
|
}
|
|
if err := validateManifest(manifest); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if expectedArtifactType != "" && manifest.ArtifactType != expectedArtifactType {
|
|
return ArtifactRecord{}, fmt.Errorf("artifact_type %q does not match expected %q", manifest.ArtifactType, expectedArtifactType)
|
|
}
|
|
if manifest.ArtifactType == ArtifactTypeSource {
|
|
return s.storeSourcePackage(upload, manifest, manifestBytes, entries, packageSHA)
|
|
}
|
|
|
|
entry := manifest.Runtime.Entry
|
|
pluginFile, ok := entries[entry]
|
|
if !ok {
|
|
return ArtifactRecord{}, fmt.Errorf("runtime entry %q is required", entry)
|
|
}
|
|
if pluginFile.UncompressedSize64 == 0 {
|
|
return ArtifactRecord{}, errors.New("runtime entry is empty")
|
|
}
|
|
if pluginFile.UncompressedSize64 > uint64(s.MaxPackageBytes) {
|
|
return ArtifactRecord{}, fmt.Errorf("runtime entry size %d exceeds limit %d", pluginFile.UncompressedSize64, s.MaxPackageBytes)
|
|
}
|
|
for name, file := range entries {
|
|
if name == "manifest.json" || name == entry {
|
|
continue
|
|
}
|
|
if file.UncompressedSize64 > uint64(s.MaxNonRuntimeBytes) {
|
|
return ArtifactRecord{}, fmt.Errorf("zip entry %q size %d exceeds limit %d", name, file.UncompressedSize64, s.MaxNonRuntimeBytes)
|
|
}
|
|
}
|
|
|
|
pluginBytes, err := readZipFile(pluginFile, s.MaxPackageBytes)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
pluginSum := sha256.Sum256(pluginBytes)
|
|
artifactID := hex.EncodeToString(pluginSum[:])
|
|
artifactDir := filepath.Join(s.Root, manifest.ID, artifactID)
|
|
if err := os.MkdirAll(artifactDir, 0755); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
pluginPath := filepath.Join(artifactDir, RuntimeEntry)
|
|
if err := os.WriteFile(pluginPath, pluginBytes, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(artifactDir, "manifest.json"), manifestBytes, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
|
|
metadataJSON, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
extensionPoints, err := json.Marshal(extensionPointKeys(manifest))
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
capabilities, err := capabilitiesSummaryJSON(manifest.Capabilities)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
now := s.now().Unix()
|
|
return ArtifactRecord{
|
|
ID: artifactID,
|
|
PluginID: manifest.ID,
|
|
Version: manifest.Version,
|
|
FileName: upload.FileName,
|
|
FilePath: pluginPath,
|
|
SHA256: artifactID,
|
|
PackageSHA256: packageSHA,
|
|
SizeBytes: int64(len(pluginBytes)),
|
|
ArtifactType: manifest.ArtifactType,
|
|
RuntimeType: manifest.Runtime.Type,
|
|
RuntimeEntry: manifest.Runtime.Entry,
|
|
Status: ArtifactStatusLoadable,
|
|
MetadataJSON: string(metadataJSON),
|
|
CapabilitiesSummaryJSON: string(capabilities),
|
|
ExtensionPointsJSON: string(extensionPoints),
|
|
APIVersion: manifest.APIVersion,
|
|
GoVersion: manifest.GoVersion,
|
|
GOOS: manifest.GOOS,
|
|
GOARCH: manifest.GOARCH,
|
|
UploadedBy: upload.Actor,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
func (s ArtifactStore) storeSourcePackage(upload ArtifactUpload, manifest Manifest, manifestBytes []byte, entries map[string]*zip.File, packageSHA string) (ArtifactRecord, error) {
|
|
if err := validateSourceEntries(manifest, entries); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
artifactID := packageSHA
|
|
artifactDir := filepath.Join(s.Root, manifest.ID, artifactID)
|
|
sourceDir := filepath.Join(artifactDir, "source")
|
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
if err := copyFile(upload.SourcePath, filepath.Join(artifactDir, "source.mcgp")); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
var sizeBytes int64
|
|
for name, file := range entries {
|
|
if name == "manifest.json" {
|
|
continue
|
|
}
|
|
if file.UncompressedSize64 > uint64(s.MaxNonRuntimeBytes) && !strings.HasPrefix(name, "vendor/") {
|
|
return ArtifactRecord{}, fmt.Errorf("zip entry %q size %d exceeds limit %d", name, file.UncompressedSize64, s.MaxNonRuntimeBytes)
|
|
}
|
|
target := filepath.Join(sourceDir, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
data, err := readZipFile(file, s.MaxExtractedBytes)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
sizeBytes += int64(len(data))
|
|
if err := os.WriteFile(target, data, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(artifactDir, "manifest.json"), manifestBytes, 0644); err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
metadataJSON, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
extensionPoints, err := json.Marshal(extensionPointKeys(manifest))
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
capabilities, err := capabilitiesSummaryJSON(manifest.Capabilities)
|
|
if err != nil {
|
|
return ArtifactRecord{}, err
|
|
}
|
|
now := s.now().Unix()
|
|
return ArtifactRecord{
|
|
ID: artifactID,
|
|
PluginID: manifest.ID,
|
|
Version: manifest.Version,
|
|
FileName: upload.FileName,
|
|
FilePath: sourceDir,
|
|
SHA256: artifactID,
|
|
PackageSHA256: packageSHA,
|
|
SizeBytes: sizeBytes,
|
|
ArtifactType: ArtifactTypeSource,
|
|
RuntimeType: manifest.Runtime.Type,
|
|
RuntimeEntry: sourceBuildEntry(manifest),
|
|
Status: ArtifactStatusValidated,
|
|
MetadataJSON: string(metadataJSON),
|
|
CapabilitiesSummaryJSON: string(capabilities),
|
|
ExtensionPointsJSON: string(extensionPoints),
|
|
APIVersion: manifest.APIVersion,
|
|
GoVersion: manifest.GoVersion,
|
|
GOOS: manifest.GOOS,
|
|
GOARCH: manifest.GOARCH,
|
|
UploadedBy: upload.Actor,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}, nil
|
|
}
|
|
|
|
func capabilitiesSummaryJSON(raw json.RawMessage) ([]byte, error) {
|
|
summary := CapabilitySummary{
|
|
UpstreamConnect: UpstreamConnectCapability{Mode: UpstreamModeDialer},
|
|
}
|
|
if len(raw) == 0 {
|
|
return json.Marshal(summary)
|
|
}
|
|
summary.Raw = append(json.RawMessage(nil), raw...)
|
|
var caps struct {
|
|
UpstreamConnect UpstreamConnectCapability `json:"upstream_connect"`
|
|
Minecraft *MinecraftCapability `json:"minecraft"`
|
|
}
|
|
if err := json.Unmarshal(raw, &caps); err != nil {
|
|
return nil, fmt.Errorf("invalid capabilities: %w", err)
|
|
}
|
|
if caps.UpstreamConnect.Mode != "" {
|
|
summary.UpstreamConnect.Mode = caps.UpstreamConnect.Mode
|
|
}
|
|
if caps.Minecraft != nil {
|
|
summary.Minecraft = caps.Minecraft
|
|
if summary.Minecraft.UnsupportedPolicy == "" {
|
|
summary.Minecraft.UnsupportedPolicy = summary.Minecraft.ProtocolVersions.UnsupportedPolicy
|
|
}
|
|
}
|
|
switch summary.UpstreamConnect.Mode {
|
|
case UpstreamModeDialer, UpstreamModeProtocolProxy:
|
|
default:
|
|
return nil, fmt.Errorf("unsupported upstream_connect.mode %q", summary.UpstreamConnect.Mode)
|
|
}
|
|
return json.Marshal(summary)
|
|
}
|
|
|
|
func validateManifest(manifest Manifest) error {
|
|
switch {
|
|
case manifest.SchemaVersion != SchemaVersion:
|
|
return fmt.Errorf("unsupported schema_version %q", manifest.SchemaVersion)
|
|
case !pluginIDPattern.MatchString(manifest.ID):
|
|
return fmt.Errorf("invalid plugin id %q", manifest.ID)
|
|
case strings.TrimSpace(manifest.Version) == "":
|
|
return errors.New("version is required")
|
|
case manifest.ArtifactType != ArtifactTypeBinary && manifest.ArtifactType != ArtifactTypeSource:
|
|
return fmt.Errorf("unsupported artifact_type %q", manifest.ArtifactType)
|
|
case manifest.Runtime.Type != RuntimeGoPlugin:
|
|
return fmt.Errorf("unsupported runtime.type %q", manifest.Runtime.Type)
|
|
case manifest.ArtifactType == ArtifactTypeBinary && manifest.Runtime.Entry != RuntimeEntry:
|
|
return fmt.Errorf("unsupported runtime.entry %q", manifest.Runtime.Entry)
|
|
case manifest.ArtifactType == ArtifactTypeSource && rawSourceBuildEntry(manifest) == "":
|
|
return errors.New("build.entry is required for source artifacts")
|
|
case manifest.APIVersion != APIVersion:
|
|
return fmt.Errorf("unsupported api_version %q", manifest.APIVersion)
|
|
case manifest.ArtifactType == ArtifactTypeBinary && manifest.GoVersion == "":
|
|
return errors.New("go_version is required")
|
|
case manifest.ArtifactType == ArtifactTypeBinary && manifest.GOOS == "":
|
|
return errors.New("go_os is required")
|
|
case manifest.ArtifactType == ArtifactTypeBinary && manifest.GOARCH == "":
|
|
return errors.New("go_arch is required")
|
|
}
|
|
if manifest.ArtifactType == ArtifactTypeBinary && manifest.GOOS != "" && manifest.GOOS != runtime.GOOS {
|
|
return fmt.Errorf("go_os %q does not match gateway %q", manifest.GOOS, runtime.GOOS)
|
|
}
|
|
if manifest.ArtifactType == ArtifactTypeBinary && manifest.GOARCH != "" && manifest.GOARCH != runtime.GOARCH {
|
|
return fmt.Errorf("go_arch %q does not match gateway %q", manifest.GOARCH, runtime.GOARCH)
|
|
}
|
|
found := false
|
|
for _, ep := range manifest.ExtensionPoints {
|
|
if ep.Type == "hook" && ep.Key == ExtensionUpstreamConnect {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("extension point %q is required", ExtensionUpstreamConnect)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateSourceEntries(manifest Manifest, entries map[string]*zip.File) error {
|
|
if _, ok := entries["go.mod"]; !ok {
|
|
return errors.New("source package requires go.mod")
|
|
}
|
|
buildEntry := sourceBuildEntry(manifest)
|
|
if buildEntry == "" || buildEntry == "." {
|
|
buildEntry = "."
|
|
}
|
|
cleanBuildEntry, err := cleanZipName(buildEntry)
|
|
if buildEntry == "." {
|
|
cleanBuildEntry = "."
|
|
err = nil
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("invalid runtime.build_entry: %w", err)
|
|
}
|
|
hasBuildSource := false
|
|
hasAnySource := false
|
|
for name := range entries {
|
|
switch {
|
|
case name == "manifest.json" || name == "go.mod" || name == "go.sum":
|
|
case strings.HasPrefix(name, "vendor/"):
|
|
case strings.EqualFold(path.Base(name), "README.md"), strings.EqualFold(path.Base(name), "LICENSE"), strings.Contains(strings.ToLower(path.Base(name)), "sbom"):
|
|
case strings.HasSuffix(name, ".go"):
|
|
default:
|
|
return fmt.Errorf("unsupported source package entry %q", name)
|
|
}
|
|
if strings.HasSuffix(name, ".go") {
|
|
hasAnySource = true
|
|
if cleanBuildEntry == "." || strings.HasPrefix(name, cleanBuildEntry+"/") || path.Dir(name) == cleanBuildEntry {
|
|
hasBuildSource = true
|
|
}
|
|
}
|
|
}
|
|
if !hasAnySource {
|
|
return errors.New("source package requires at least one Go source file")
|
|
}
|
|
if !hasBuildSource {
|
|
return fmt.Errorf("source package build entry %q has no Go source files", manifest.Runtime.BuildEntry)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sourceBuildEntry(manifest Manifest) string {
|
|
entry := rawSourceBuildEntry(manifest)
|
|
if entry == "" {
|
|
return SourceBuildEntry
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func rawSourceBuildEntry(manifest Manifest) string {
|
|
entry := strings.Trim(strings.TrimSpace(manifest.Build.Entry), "/")
|
|
if entry == "" {
|
|
entry = strings.Trim(strings.TrimSpace(manifest.Runtime.BuildEntry), "/")
|
|
}
|
|
return entry
|
|
}
|
|
|
|
func extensionPointKeys(manifest Manifest) []string {
|
|
keys := make([]string, 0, len(manifest.ExtensionPoints))
|
|
for _, ep := range manifest.ExtensionPoints {
|
|
keys = append(keys, ep.Key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func cleanZipName(name string) (string, error) {
|
|
if name == "" || strings.Contains(name, `\`) || strings.HasPrefix(name, "/") {
|
|
return "", fmt.Errorf("unsafe zip entry %q", name)
|
|
}
|
|
clean := path.Clean(name)
|
|
if clean == "." || clean != name || strings.HasPrefix(clean, "../") || clean == ".." || path.IsAbs(clean) {
|
|
return "", fmt.Errorf("unsafe zip entry %q", name)
|
|
}
|
|
return clean, nil
|
|
}
|
|
|
|
func cleanZipDirName(name string) (string, error) {
|
|
name = strings.TrimSuffix(name, "/")
|
|
if name == "" {
|
|
return "", fmt.Errorf("unsafe zip entry %q", name)
|
|
}
|
|
return cleanZipName(name)
|
|
}
|
|
|
|
func readZipFile(file *zip.File, maxBytes int64) ([]byte, error) {
|
|
rc, err := file.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.CopyN(&buf, rc, maxBytes+1); err != nil && !errors.Is(err, io.EOF) {
|
|
return nil, err
|
|
}
|
|
if int64(buf.Len()) > maxBytes {
|
|
return nil, fmt.Errorf("zip entry %q exceeds limit %d", file.Name, maxBytes)
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func fileSHA256(path string) (string, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer file.Close()
|
|
|
|
hash := sha256.New()
|
|
if _, err := io.Copy(hash, file); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
|
}
|
|
|
|
func copyFile(src, dst string) error {
|
|
input, err := os.Open(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer input.Close()
|
|
|
|
output, err := os.Create(dst)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer output.Close()
|
|
if _, err := io.Copy(output, input); err != nil {
|
|
return err
|
|
}
|
|
return output.Close()
|
|
}
|