153 lines
4.9 KiB
Go
153 lines
4.9 KiB
Go
|
|
package updatepackage
|
||
|
|
|
||
|
|
import (
|
||
|
|
"archive/zip"
|
||
|
|
"crypto/sha256"
|
||
|
|
"encoding/hex"
|
||
|
|
"errors"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"strings"
|
||
|
|
|
||
|
|
"yms-daemon/internal/filestore"
|
||
|
|
)
|
||
|
|
|
||
|
|
// DirectNativeJAR is one JAR supplied directly by Jenkins or an operator.
|
||
|
|
// FileName is treated as opaque text; no version is parsed from it.
|
||
|
|
type DirectNativeJAR struct {
|
||
|
|
Path string
|
||
|
|
SHA256 string
|
||
|
|
FileName string
|
||
|
|
Identity filestore.Identity
|
||
|
|
}
|
||
|
|
|
||
|
|
// OpenDirectNativeJAR validates the direct file, verifies every ZIP entry and
|
||
|
|
// records the immutable identity used by the transaction.
|
||
|
|
func OpenDirectNativeJAR(jarPath string) (DirectNativeJAR, error) {
|
||
|
|
if err := validateAbsoluteRegularFile(jarPath, "native backend JAR"); err != nil {
|
||
|
|
return DirectNativeJAR{}, err
|
||
|
|
}
|
||
|
|
fileName := filepath.Base(jarPath)
|
||
|
|
if filepath.Ext(fileName) != ".jar" {
|
||
|
|
return DirectNativeJAR{}, fmt.Errorf("native backend JAR file name must end with .jar: %s", fileName)
|
||
|
|
}
|
||
|
|
if err := verifyJARArchive(jarPath); err != nil {
|
||
|
|
return DirectNativeJAR{}, err
|
||
|
|
}
|
||
|
|
identity, err := identifyFile(jarPath)
|
||
|
|
if err != nil {
|
||
|
|
return DirectNativeJAR{}, err
|
||
|
|
}
|
||
|
|
return DirectNativeJAR{
|
||
|
|
Path: jarPath,
|
||
|
|
SHA256: identity.SHA256,
|
||
|
|
FileName: fileName,
|
||
|
|
Identity: identity,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// CopyArtifact copies the exact JAR into transaction storage and rejects a
|
||
|
|
// source file that changes after OpenDirectNativeJAR returns.
|
||
|
|
func (j DirectNativeJAR) CopyArtifact(destination string) error {
|
||
|
|
if !filepath.IsAbs(destination) || filepath.Clean(destination) != destination {
|
||
|
|
return errors.New("native backend JAR destination must be a clean absolute path")
|
||
|
|
}
|
||
|
|
parent := filepath.Dir(destination)
|
||
|
|
if err := os.MkdirAll(parent, 0o750); err != nil {
|
||
|
|
return fmt.Errorf("create native backend JAR destination directory: %w", err)
|
||
|
|
}
|
||
|
|
parentInfo, err := os.Lstat(parent)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("inspect native backend JAR destination directory: %w", err)
|
||
|
|
}
|
||
|
|
if !parentInfo.IsDir() || parentInfo.Mode()&os.ModeSymlink != 0 {
|
||
|
|
return fmt.Errorf("native backend JAR destination parent is not a direct directory: %s", parent)
|
||
|
|
}
|
||
|
|
|
||
|
|
source, err := os.Open(j.Path)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("open native backend JAR: %w", err)
|
||
|
|
}
|
||
|
|
temporary, err := os.CreateTemp(parent, ".backend-jar-*")
|
||
|
|
if err != nil {
|
||
|
|
_ = source.Close()
|
||
|
|
return fmt.Errorf("create native backend JAR transaction file: %w", err)
|
||
|
|
}
|
||
|
|
temporaryPath := temporary.Name()
|
||
|
|
defer os.Remove(temporaryPath)
|
||
|
|
if err := temporary.Chmod(0o640); err != nil {
|
||
|
|
_ = source.Close()
|
||
|
|
_ = temporary.Close()
|
||
|
|
return fmt.Errorf("set native backend JAR transaction file permissions: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
digest := sha256.New()
|
||
|
|
written, copyErr := io.Copy(io.MultiWriter(temporary, digest), source)
|
||
|
|
closeSourceErr := source.Close()
|
||
|
|
if err := errors.Join(copyErr, closeSourceErr); err != nil {
|
||
|
|
_ = temporary.Close()
|
||
|
|
return fmt.Errorf("copy native backend JAR: %w", err)
|
||
|
|
}
|
||
|
|
actualDigest := hex.EncodeToString(digest.Sum(nil))
|
||
|
|
if written != j.Identity.Size || !strings.EqualFold(actualDigest, j.Identity.SHA256) {
|
||
|
|
_ = temporary.Close()
|
||
|
|
return errors.New("native backend JAR changed while entering the transaction")
|
||
|
|
}
|
||
|
|
if err := temporary.Sync(); err != nil {
|
||
|
|
_ = temporary.Close()
|
||
|
|
return fmt.Errorf("flush native backend JAR transaction file: %w", err)
|
||
|
|
}
|
||
|
|
if err := temporary.Close(); err != nil {
|
||
|
|
return fmt.Errorf("close native backend JAR transaction file: %w", err)
|
||
|
|
}
|
||
|
|
if err := verifyJARArchive(temporaryPath); err != nil {
|
||
|
|
return fmt.Errorf("verify copied native backend JAR: %w", err)
|
||
|
|
}
|
||
|
|
if err := os.Rename(temporaryPath, destination); err != nil {
|
||
|
|
return fmt.Errorf("publish native backend JAR transaction file: %w", err)
|
||
|
|
}
|
||
|
|
return syncDirectory(parent)
|
||
|
|
}
|
||
|
|
|
||
|
|
func verifyJARArchive(jarPath string) error {
|
||
|
|
archive, err := zip.OpenReader(jarPath)
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("open native backend JAR %s: %w", jarPath, err)
|
||
|
|
}
|
||
|
|
defer archive.Close()
|
||
|
|
if len(archive.File) == 0 {
|
||
|
|
return errors.New("native backend JAR contains no ZIP entries")
|
||
|
|
}
|
||
|
|
for _, entry := range archive.File {
|
||
|
|
if entry.FileInfo().IsDir() {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
reader, err := entry.Open()
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("open native backend JAR entry %s: %w", entry.Name, err)
|
||
|
|
}
|
||
|
|
_, readErr := io.Copy(io.Discard, reader)
|
||
|
|
closeErr := reader.Close()
|
||
|
|
if err := errors.Join(readErr, closeErr); err != nil {
|
||
|
|
return fmt.Errorf("verify native backend JAR entry %s: %w", entry.Name, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func identifyFile(filePath string) (filestore.Identity, error) {
|
||
|
|
file, err := os.Open(filePath)
|
||
|
|
if err != nil {
|
||
|
|
return filestore.Identity{}, fmt.Errorf("open native backend JAR for identity: %w", err)
|
||
|
|
}
|
||
|
|
digest := sha256.New()
|
||
|
|
size, copyErr := io.Copy(digest, file)
|
||
|
|
closeErr := file.Close()
|
||
|
|
if err := errors.Join(copyErr, closeErr); err != nil {
|
||
|
|
return filestore.Identity{}, fmt.Errorf("identify native backend JAR: %w", err)
|
||
|
|
}
|
||
|
|
return filestore.Identity{Size: size, SHA256: hex.EncodeToString(digest.Sum(nil))}, nil
|
||
|
|
}
|