eon/lib/main.go

1133 lines
37 KiB
Go
Raw Normal View History

2024-09-01 12:27:25 -07:00
package lib
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
"archive/tar"
"crypto/ed25519"
"encoding/binary"
"encoding/hex"
"encoding/json"
"math/big"
"net/http"
"os/exec"
"path/filepath"
"github.com/Masterminds/semver"
"github.com/cespare/xxhash/v2"
"github.com/dustin/go-humanize"
"github.com/klauspost/compress/zstd"
"modernc.org/sqlite"
)
// RemoteEPK is a struct that contains the metadata of an EPK from a remote repository
type RemoteEPK struct {
Name string
Author string
Description string
Version semver.Version
Architecture string
CompressedSize int64
Dependencies []string
Path string
Arch string
EPKHash uint64
}
// Repository is a struct that contains the repository information
type Repository struct {
Name string
URL string
Owner string
Description string
}
// SpecialFiles is a struct that contains the special files that are not to be deleted or replaced
type SpecialFiles struct {
NoDelete []string
NoReplace []string
}
// Metadata is a struct that contains the metadata of the package
type Metadata struct {
Name string
Description string
LongDescription string
Version semver.Version
Author string
License string
Architecture string
Dependencies []string
SpecialFiles SpecialFiles
Size int64
DecompressedSize big.Int
}
// EPKPreMap is a struct that contains the metadata of the EPK
type EPKPreMap struct {
Name string
Author string
Architecture string
Version semver.Version
Size int64
DecompressedSize big.Int
Dependencies []string
MetadataMap map[string]interface{}
IsLittleEndian bool
IsUpgrade bool
TarOffset int64
}
// PotentiallyNullEPKPreMap is a EPKPreMap that can be nil
type PotentiallyNullEPKPreMap struct {
EPKPreMap *EPKPreMap
Null bool
}
// Log is a struct that contains the log information
type Log struct {
Level string
Content string
Prompt bool
PlaySound bool
Progress *big.Int
Total *big.Int
Overwrite bool
}
// StreamOrBytes is a struct that contains either a stream or bytes, allowing optimising for memory or speed
type StreamOrBytes struct {
Stream *os.File
Bytes []byte
IsStream bool
}
// Logger is a struct that contains the functions and properties of the logger
type Logger struct {
LogFunc func(Log) string
PromptSupported bool
ProgressSupported bool
}
// Epk is a struct that contains the metadata and the tar archive of the EPK
type Epk struct {
Metadata Metadata
TarArchive []byte
}
// interfaceToStringSlice converts an interface slice to a string slice
func interfaceToStringSlice(interfaceSlice []interface{}, interfaceName string) ([]string, error) {
// Yes, it's meant to be empty and not nil: JSON arrays are empty, not nil
//goland:noinspection GoPreferNilSlice
stringSlice := []string{}
for _, interfaceValue := range interfaceSlice {
stringValue, ok := interfaceValue.(string)
if !ok {
return nil, errors.New(interfaceName + " are not strings")
}
stringSlice = append(stringSlice, stringValue)
}
return stringSlice, nil
}
// ByteToFingerprint converts a byte slice to an Eon fingerprint, which is similar to a legacy-style OpenSSH fingerprint
func ByteToFingerprint(input []byte) string {
xxHashWriter := xxhash.New()
_, _ = xxHashWriter.Write(input)
inputString := hex.EncodeToString(xxhash.New().Sum(nil))
var result []string
var previousChar rune
for index, char := range inputString {
if index%2 == 0 && index != 0 {
result = append(result, string(previousChar)+string(char))
} else {
previousChar = char
}
}
return strings.Join(result, ":")
}
// ConstMapEPKMetadataOffset is the offset of the metadata in the EPK: 3 magic bytes, 1 endian byte, 8 offset bytes, 64 signature bytes, and 32 public key bytes
var ConstMapEPKMetadataOffset int64 = 108
var ErrPreMapEPKFailedToReadError = errors.New("failed to read EPK")
var ErrPreMapEPKNotEPKError = errors.New("not an EPK")
var ErrPreMapEPKInvalidEndianError = errors.New("invalid endian")
var ErrPreMapEPKCouldNotParseJSONError = errors.New("error marshaling metadata")
var ErrPreMapEPKCouldNotMapJSONError = errors.New("error mapping metadata")
// PreMapEPK maps enough data to create the display summary of an EPK
func PreMapEPK(epkBytes StreamOrBytes, epkSize int64) (EPKPreMap, error, error) {
// First, we need to check if it even is a EPK by checking the first 3 magic bytes
if epkBytes.IsStream {
var magicBytes = make([]byte, 3)
_, err := epkBytes.Stream.ReadAt(magicBytes, 0)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
}
if string(magicBytes) != "epk" {
return EPKPreMap{}, nil, ErrPreMapEPKNotEPKError
}
} else {
if string(epkBytes.Bytes[0:3]) != "epk" {
return EPKPreMap{}, nil, ErrPreMapEPKNotEPKError
}
}
// Let's determine the endian-ness of the EPK via the 3rd byte
var littleEndian bool
if epkBytes.IsStream {
var littleEndianByte = make([]byte, 1)
_, err := epkBytes.Stream.ReadAt(littleEndianByte, 3)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
}
if littleEndianByte[0] == 0x6C {
littleEndian = true
} else if littleEndianByte[0] == 0x62 {
littleEndian = false
} else {
return EPKPreMap{}, nil, ErrPreMapEPKInvalidEndianError
}
} else {
if epkBytes.Bytes[3] == 0x6C {
littleEndian = true
} else if epkBytes.Bytes[3] == 0x62 {
littleEndian = false
} else {
return EPKPreMap{}, nil, ErrPreMapEPKInvalidEndianError
}
}
// Now we can get the offsets of the tar archive
var tarArchiveOffset int64
if epkBytes.IsStream {
var tarArchiveOffsetBytes = make([]byte, 8)
_, err := epkBytes.Stream.ReadAt(tarArchiveOffsetBytes, 4)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
}
if littleEndian {
tarArchiveOffset = int64(binary.LittleEndian.Uint64(tarArchiveOffsetBytes))
} else {
tarArchiveOffset = int64(binary.BigEndian.Uint64(tarArchiveOffsetBytes))
}
} else {
if littleEndian {
tarArchiveOffset = int64(binary.LittleEndian.Uint64(epkBytes.Bytes[4:12]))
} else {
tarArchiveOffset = int64(binary.BigEndian.Uint64(epkBytes.Bytes[4:12]))
}
}
// We don't need to validate the signature yet. We will do that when we map the full EPK, because it means
// we have to read the entire thing, which is a waste of resources, since we only need the metadata.
// Let's map the display data
var displayDataMap map[string]interface{}
if epkBytes.IsStream {
var metadataBuffer = make([]byte, tarArchiveOffset-ConstMapEPKMetadataOffset)
_, err := epkBytes.Stream.ReadAt(metadataBuffer, ConstMapEPKMetadataOffset)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKFailedToReadError
}
err = json.Unmarshal(metadataBuffer, &displayDataMap)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKCouldNotParseJSONError
}
} else {
err := json.Unmarshal(epkBytes.Bytes[ConstMapEPKMetadataOffset:tarArchiveOffset], &displayDataMap)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKCouldNotParseJSONError
}
}
// Declare the parsedDisplayData object
var parsedDisplayData EPKPreMap
// Add some of our data so that the full EPK can be mapped with less effort
parsedDisplayData.MetadataMap = displayDataMap
parsedDisplayData.IsLittleEndian = littleEndian
parsedDisplayData.Size = epkSize
parsedDisplayData.TarOffset = tarArchiveOffset
// Map the display data
var ok bool
// Set the size
sizeBigInt, ok := displayDataMap["size"].(string)
if !ok {
return EPKPreMap{}, errors.New("size is not a string"), ErrPreMapEPKCouldNotMapJSONError
}
parsedDisplayData.DecompressedSize.SetString(sizeBigInt, 10)
// Set the name, author, version, arch, and dependencies
parsedDisplayData.Name, ok = displayDataMap["name"].(string)
if !ok {
return EPKPreMap{}, errors.New("name is not a string"), ErrPreMapEPKCouldNotMapJSONError
}
parsedDisplayData.Author, ok = displayDataMap["author"].(string)
if !ok {
return EPKPreMap{}, errors.New("author is not a string"), ErrPreMapEPKCouldNotMapJSONError
}
versionString, ok := displayDataMap["version"].(string)
if !ok {
return EPKPreMap{}, errors.New("version is not a string"), ErrPreMapEPKCouldNotMapJSONError
}
versionPointer, err := semver.NewVersion(versionString)
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKCouldNotMapJSONError
}
parsedDisplayData.Version = *versionPointer
parsedDisplayData.Architecture, ok = displayDataMap["arch"].(string)
if !ok {
return EPKPreMap{}, errors.New("arch is not a string"), ErrPreMapEPKCouldNotMapJSONError
}
dependencies, ok := displayDataMap["deps"].([]interface{})
if !ok {
return EPKPreMap{}, errors.New("dependencies is not an array"), ErrPreMapEPKCouldNotMapJSONError
}
parsedDisplayData.Dependencies, err = interfaceToStringSlice(dependencies, "dependencies")
if err != nil {
return EPKPreMap{}, err, ErrPreMapEPKCouldNotMapJSONError
}
return parsedDisplayData, nil, nil
}
func handlePublicKeyCheck(exists bool, matchingAuthor bool, matchingFingerprint bool, publicKey []byte, author string, addFingerprintToDB func([]byte, string, bool) error, logger *Logger) error {
if !exists {
if logger.PromptSupported {
response := logger.LogFunc(Log{
Level: "WARN",
Content: "Public key not found in database.\nthe public key fingerprint is: " + author + " " + ByteToFingerprint(publicKey) +
"\nWould you like to trust this key (y/n)?",
Prompt: true,
})
if strings.ToLower(response) == "y" {
err := addFingerprintToDB(publicKey, author, false)
if err != nil {
return err
} else {
logger.LogFunc(Log{
Level: "INFO",
Content: "Public key added to database.",
})
}
} else {
logger.LogFunc(Log{
Level: "INFO",
Content: "Installation cancelled.",
})
}
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Public key not found in database.\nthe public key fingerprint is:" + author + " " + ByteToFingerprint(publicKey) +
"\nSince non-interactive mode is enabled, the installation will not proceed.",
})
}
} else if !matchingAuthor {
if logger.PromptSupported {
response := logger.LogFunc(Log{
Level: "WARN",
Content: "Public key does not match the author.\nthe public key fingerprint is: " + author + " " + ByteToFingerprint(publicKey) +
"\nWould you like to replace the key (y/n)?",
Prompt: true,
})
if strings.ToLower(response) == "y" {
err := addFingerprintToDB(publicKey, author, true)
if err != nil {
return err
} else {
logger.LogFunc(Log{
Level: "INFO",
Content: "Public key replaced in database.",
})
}
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Installation cancelled.",
})
}
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Public key does not match the author.\nThe public key is :" + author + " " + ByteToFingerprint(publicKey) +
"\nSince non-interactive mode is enabled, the installation will not proceed.",
})
}
} else if !matchingFingerprint {
if logger.PromptSupported {
response := logger.LogFunc(Log{
Level: "WARN",
Content: "Public key fingerprint does not match the author.\nThe public key is :" + author + " " + ByteToFingerprint(publicKey) +
"\nThis may be a security risk. To replace the key, type \"Yes, do as I say!\". Otherwise, type anything else.",
Prompt: true,
})
if response == "Yes, do as I say!" {
err := addFingerprintToDB(publicKey, author, true)
if err != nil {
return err
} else {
logger.LogFunc(Log{
Level: "INFO",
Content: "Public key replaced in database.",
})
}
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Installation cancelled.",
})
}
} else {
logger.LogFunc(Log{
Level: "FATAL",
Content: "Public key fingerprint does not match the author.\nthe public key fingerprint is: " + author + " " + ByteToFingerprint(publicKey) +
"\nSince non-interactive mode is enabled, the installation will not proceed.",
})
}
}
return nil
}
var ErrFullyMapMetadataFailedToJumpError = errors.New("failed to jump to offset")
var ErrFullyMapMetadataFailedToReadError = errors.New("failed to read EPK")
var ErrFullyMapMetadataFailedToAddFingerprintError = errors.New("failed to add fingerprint")
var ErrFullyMapMetadataFailedToGetFingerprintError = errors.New("failed to get fingerprint")
var ErrFullyMapMetadataInvalidSignatureError = errors.New("invalid signature")
var ErrFullyMapMetadataCouldNotMapJSONError = errors.New("error mapping metadata")
// FullyMapMetadata maps an EPK file, but is significantly slower than PreMapEPK. Use PreMapEPK if you only need the display data.
// it pulls data from PreMapEPK to reduce the amount of work needed to map the EPK.
func FullyMapMetadata(epkBytes StreamOrBytes, preMap *EPKPreMap, checkFingerprintInDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, logger *Logger) (*Metadata, error, error) {
// We define the signature and public key bytes here so that we can read them later
signature := make([]byte, 64)
publicKey := make([]byte, 32)
if epkBytes.IsStream {
// Before we continue, check if the signature is valid
// To get the signature, we read from the 12th byte to the 76th byte
_, err := epkBytes.Stream.ReadAt(signature, 12)
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError
}
// To get the public key, we read from the 76th byte to the 108th byte
_, err = epkBytes.Stream.ReadAt(publicKey, 76)
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError
}
} else {
// Make signature and public key the optimised bytes
signature = epkBytes.Bytes[12:76]
publicKey = epkBytes.Bytes[76:108]
}
// Let's check for the public key in the database
exists, matchingAuthor, matchingFingerprint, err := checkFingerprintInDB(publicKey, preMap.Author)
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataFailedToGetFingerprintError
} else {
err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, preMap.Author, addFingerprintToDB, logger)
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataFailedToAddFingerprintError
}
}
// We need to create a new xxHash instance
xxHash := xxhash.New()
if epkBytes.IsStream {
// Now we can verify the signature. First, we need to take the checksum of the metadata
// Seeking is better than using ReadAt because it allows us to not have to load the entire file into memory
_, err = epkBytes.Stream.Seek(ConstMapEPKMetadataOffset, io.SeekStart)
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataFailedToJumpError
}
// Streaming bytes to the hash is more memory efficient
_, err = epkBytes.Stream.WriteTo(xxHash)
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataFailedToReadError
}
// Verify the signature (we verify the hash because it's cheaper than verifying the entire EPK)
if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) {
return &Metadata{}, nil, ErrFullyMapMetadataInvalidSignatureError
}
} else {
// We now verify the signature in one go without streaming
if !ed25519.Verify(publicKey, xxHash.Sum(epkBytes.Bytes[ConstMapEPKMetadataOffset:]), signature) {
return &Metadata{}, nil, ErrFullyMapMetadataInvalidSignatureError
}
}
// Great, the EPK is valid. Let's map the metadata.
// We use the metadata map provided by PreMapEPK to reduce the amount of work needed to map the EPK
// First, map SpecialFiles
var parsedSpecialFiles SpecialFiles
specialFilesMap, ok := preMap.MetadataMap["specialFiles"].(map[string]interface{})
if !ok {
return &Metadata{}, errors.New("specialFiles is not an object"), ErrFullyMapMetadataCouldNotMapJSONError
}
noDelete, ok := specialFilesMap["noDelete"].([]interface{})
if !ok {
return &Metadata{}, errors.New("noDelete is not an array"), ErrFullyMapMetadataCouldNotMapJSONError
}
parsedSpecialFiles.NoDelete, err = interfaceToStringSlice(noDelete, "noDelete")
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSONError
}
noReplace, ok := specialFilesMap["noReplace"].([]interface{})
if !ok {
return &Metadata{}, errors.New("noReplace is not an array"), ErrFullyMapMetadataCouldNotMapJSONError
}
parsedSpecialFiles.NoReplace, err = interfaceToStringSlice(noReplace, "noReplace")
if err != nil {
return &Metadata{}, err, ErrFullyMapMetadataCouldNotMapJSONError
}
// Declare the parsedMetadata object
var parsedMetadata Metadata
// Append parsedSpecialFiles to parsedMetadata
parsedMetadata.SpecialFiles = parsedSpecialFiles
// Steal some data from the PreMapEPK object
parsedMetadata.Name = preMap.Name
parsedMetadata.Version = preMap.Version
parsedMetadata.Architecture = preMap.Architecture
parsedMetadata.Size = preMap.Size
parsedMetadata.Dependencies = preMap.Dependencies
// Map the metadata
parsedMetadata.Description, ok = preMap.MetadataMap["desc"].(string)
if !ok {
return &Metadata{}, errors.New("description is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
}
parsedMetadata.LongDescription, ok = preMap.MetadataMap["longDesc"].(string)
if !ok {
return &Metadata{}, errors.New("longDesc is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
}
parsedMetadata.Author, ok = preMap.MetadataMap["author"].(string)
if !ok {
return &Metadata{}, errors.New("author is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
}
parsedMetadata.License, ok = preMap.MetadataMap["license"].(string)
if !ok {
return &Metadata{}, errors.New("license is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
}
decompressedSizeString, ok := preMap.MetadataMap["size"].(string)
if !ok {
return &Metadata{}, errors.New("size is not a string"), ErrFullyMapMetadataCouldNotMapJSONError
}
parsedMetadata.DecompressedSize.SetString(decompressedSizeString, 10)
return &parsedMetadata, nil, nil
}
// ResolveDependencies resolves the dependencies of an EPK
// func ResolveDependencies(epk Epk) (error, error) {
// TODO: Implement this when I finish up repositories
// return nil, nil
// }
var ErrInstallEPKCouldNotCreateTempDirError = errors.New("could not create temporary directory")
var ErrInstallEPKCouldNotCreateZStandardReaderError = errors.New("could not create ZStandard reader")
var ErrInstallEPKCouldNotDecompressTarArchiveError = errors.New("could not decompress tar archive")
var ErrInstallEPKCouldNotCreateDirError = errors.New("could not create directory")
var ErrInstallEPKCouldNotStatDirError = errors.New("could not stat directory")
var ErrInstallEPKCouldNotStatFileError = errors.New("could not stat file")
var ErrInstallEPKCouldNotCreateFileError = errors.New("could not create file")
var ErrInstallEPKCouldNotCloseTarReaderError = errors.New("could not close tar reader")
var ErrInstallEPKCouldNotStatHookError = errors.New("could not stat hook")
var ErrInstallEPKCouldNotRunHookError = errors.New("could not run hook")
var ErrInstallEPKCouldNotAddEPKToDBError = errors.New("could not add EPK to database")
var ErrInstallEPKCouldNotRemoveTempDirError = errors.New("could not remove temporary directory")
// InstallEPK installs an EPK file
func InstallEPK(epkBytes StreamOrBytes, metadata *Metadata, preMap *EPKPreMap, addEPKToDB func(metadata *Metadata, removeScript []byte, dependency bool, hasRemoveScript bool, size int64) error, logger *Logger) (string, error, error) {
// Create the temporary directory
tempDir, err := os.MkdirTemp("/tmp", "eon-install-")
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCreateTempDirError
}
var zStandardReader *zstd.Decoder
if epkBytes.IsStream {
// Seek to the correct position in the EPK
_, err = epkBytes.Stream.Seek(preMap.TarOffset, io.SeekStart)
if err != nil {
return "", err, nil
}
// Create a ZStandard reader reading from the EPK
zStandardReader, err = zstd.NewReader(epkBytes.Stream)
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError
}
} else {
// Create a ZStandard reader reading from the EPKs in-memory bytes
zStandardReader, err = zstd.NewReader(bytes.NewReader(epkBytes.Bytes[preMap.TarOffset:]))
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCreateZStandardReaderError
}
}
// Create a tar reader reading from the ZStandard reader
tarReader := tar.NewReader(zStandardReader)
// Create a goroutine to see how much of the decompressed size we have decompressed
var written big.Int
stop := make(chan bool)
go func() {
for {
select {
case <-stop:
return
default:
if logger.ProgressSupported {
logger.LogFunc(Log{
Level: "PROGRESS",
Progress: &written,
Total: &metadata.DecompressedSize,
Overwrite: true,
})
} else {
logger.LogFunc(Log{
Level: "INFO",
Content: "Decompressed " + humanize.Bytes(uint64(written.Int64())) + " of " + humanize.Bytes(uint64(metadata.DecompressedSize.Int64())),
Prompt: false,
})
time.Sleep(1 * time.Second)
}
}
}
}()
// Iterate through the tar archive
for {
// Read the next header
header, err := tarReader.Next()
if err != nil {
break
}
switch {
// If we are done, break
case err == io.EOF:
break
// If there was an error, return the error
case err != nil:
return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError
// This should never happen, but if it does, we should just continue
case header == nil:
continue
}
// Get the target path
var target string
if strings.HasPrefix(header.Name, "root") {
target = strings.TrimPrefix(header.Name, "root")
} else if strings.HasPrefix(header.Name, "hooks") {
target = filepath.Join(tempDir, header.Name)
} else {
return tempDir, errors.New("invalid path in EPK: " + header.Name), ErrInstallEPKCouldNotDecompressTarArchiveError
}
switch header.Typeflag {
case tar.TypeDir:
// Check if the directory exists
_, err := os.Stat(target)
if err != nil {
// If the directory does not exist, create it
if os.IsNotExist(err) {
// Make sure to replicate the permissions of the directory
err := os.MkdirAll(target, header.FileInfo().Mode())
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCreateDirError
}
} else {
return tempDir, err, ErrInstallEPKCouldNotStatDirError
}
} else {
// If it does exist, don't touch it
continue
}
case tar.TypeReg:
// Check if the file has anywhere to go
_, err := os.Stat(filepath.Dir(target))
if err != nil {
// No, it doesn't. Create the directory
if os.IsNotExist(err) {
// We assume 0755 for any directories that don't exist
err := os.MkdirAll(filepath.Dir(target), 0755)
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCreateDirError
}
} else {
return tempDir, err, ErrInstallEPKCouldNotStatDirError
}
}
// Check if the file already exists
_, err = os.Stat(target)
if err != nil {
if os.IsNotExist(err) {
// Great, the file does not exist. Let's create it.
file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCreateFileError
}
writtenFile, err := io.Copy(file, tarReader)
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotDecompressTarArchiveError
}
written.Add(&written, big.NewInt(writtenFile))
err = file.Close()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotCloseTarReaderError
}
} else {
return tempDir, err, ErrInstallEPKCouldNotStatFileError
}
} else {
// See if it's an upgrade or not
if preMap.IsUpgrade {
// Check if it's a special file
for _, file := range metadata.SpecialFiles.NoReplace {
if strings.TrimSuffix(target, "/") == strings.TrimSuffix(file, "/") {
// This file is a special file and should not be replaced
continue
}
}
}
}
}
}
zStandardReader.Close()
// Now let's run the hooks
if preMap.IsUpgrade {
_, err := os.Stat(filepath.Join(tempDir, "hooks", "upgrade.sh"))
if err != nil {
if !os.IsNotExist(err) {
return tempDir, err, ErrInstallEPKCouldNotStatHookError
}
} else {
cmd := exec.Command("/bin/sh", filepath.Join(tempDir, "hooks", "upgrade.sh"), metadata.Version.String())
stderr, err := cmd.StderrPipe()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRunHookError
}
err = cmd.Start()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRunHookError
}
scanner := bufio.NewScanner(stderr)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
message := scanner.Text()
fmt.Println(message)
}
err = cmd.Wait()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRunHookError
}
}
} else {
_, err := os.Stat(filepath.Join(tempDir, "hooks", "install.sh"))
if err != nil {
if !os.IsNotExist(err) {
return tempDir, err, ErrInstallEPKCouldNotStatHookError
}
} else {
cmd := exec.Command("/bin/sh", filepath.Join(tempDir, "hooks", "install.sh"), metadata.Version.String())
stderr, err := cmd.StderrPipe()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRunHookError
}
err = cmd.Start()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRunHookError
}
scanner := bufio.NewScanner(stderr)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
message := scanner.Text()
fmt.Println(message)
}
err = cmd.Wait()
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRunHookError
}
}
}
// Finally, add the EPK and remove script to the database
file, err := os.ReadFile(filepath.Join(tempDir, "hooks", "remove.sh"))
if err != nil {
if !os.IsNotExist(err) {
err := addEPKToDB(metadata, []byte{}, false, false, metadata.Size)
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError
}
} else {
return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError
}
} else {
err := addEPKToDB(metadata, file, false, true, metadata.Size)
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotAddEPKToDBError
}
}
// Remove the temporary directory
err = os.RemoveAll(tempDir)
if err != nil {
return tempDir, err, ErrInstallEPKCouldNotRemoveTempDirError
}
stop <- true
logger.LogFunc(Log{
Level: "PROGRESS",
Progress: big.NewInt(1),
Total: big.NewInt(1),
Overwrite: true,
})
return "", nil, nil
}
var ErrAddRepositoryCannotCreateRequestError = errors.New("cannot create request")
var ErrAddRepositoryCannotSendRequestError = errors.New("cannot send request")
var ErrAddRepositoryUnexpectedStatusCodeError = errors.New("unexpected status code")
var ErrAddRepositoryCannotReadResponseError = errors.New("cannot read response")
var ErrAddRepositoryInvalidMagicError = errors.New("invalid magic")
var ErrAddRepositoryCannotHashError = errors.New("cannot write to hash")
var ErrAddRepositoryUnmarshalMetadataError = errors.New("cannot unmarshal metadata")
var ErrAddRepositoryFailedToGetFingerprintError = errors.New("failed to get fingerprint")
var ErrAddRepositoryFailedToAddFingerprintError = errors.New("failed to add fingerprint")
var ErrAddRepositoryInvalidMetadataError = errors.New("invalid metadata")
var ErrAddRepositoryCannotCreateDirError = errors.New("cannot create directory")
var ErrAddRepositoryCannotStatDirError = errors.New("cannot stat directory")
var ErrAddRepositoryFailedToAddPackageError = errors.New("failed to add package to database")
var ErrAddRepositoryRepositoryAlreadyExistsError = errors.New("repository already exists")
var ErrAddRepositoryFailedToAddRepositoryError = errors.New("failed to add repository to database")
func AddRepository(url string, addRepositoryToDB func(repository Repository) error, getFingerprintFromDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, addRemotePackageToDB func(string, RemoteEPK) error, logger *Logger) (error, error) {
// First, fetch range 0-3 of /repository.erf
// Then, check if the first 3 bytes are "eon"
// Create the request
magicRequest, err := http.NewRequest("GET", url+"/repository.erf", nil)
if err != nil {
return err, ErrAddRepositoryCannotCreateRequestError
}
// Add the range header
magicRequest.Header.Add("Range", "bytes=0-3")
// Send the request
magicResponse, err := http.DefaultClient.Do(magicRequest)
if err != nil {
return err, ErrAddRepositoryCannotSendRequestError
}
// Check if the status code is 206
var hasEntireFile bool
if magicResponse.StatusCode != 206 {
if magicResponse.StatusCode == 200 {
// This web server does not support range requests, meaning we now have the entire file.
// Mark it as such.
hasEntireFile = true
} else {
return errors.New("status code " + strconv.Itoa(magicResponse.StatusCode)), ErrAddRepositoryUnexpectedStatusCodeError
}
}
// Check the magic bytes
var magicBytes = make([]byte, 3)
_, err = magicResponse.Body.Read(magicBytes)
if err != nil {
return err, ErrAddRepositoryCannotReadResponseError
}
// Check if the magic bytes are "eon"
if string(magicBytes) != "eon" {
return nil, ErrAddRepositoryInvalidMagicError
}
// Great. We either confirmed the repository is an Eon repository or we have the entire file. Both are good.
var fullFetch *http.Response
if !hasEntireFile {
// Download the rest of the file
var err error
fullFetch, err = http.Get(url + "/repository.erf")
if err != nil {
return err, ErrAddRepositoryCannotSendRequestError
}
} else {
fullFetch = magicResponse
}
// Now we get the contents of the file
contents, err := io.ReadAll(fullFetch.Body)
if err != nil {
return err, ErrAddRepositoryCannotReadResponseError
}
// Verify the file's signature
// Unmarshal the repository metadata, which is NOT the same as the EPK metadata
var repositoryMetadata map[string]interface{}
// We use a decoder instead of unmarshal here because we need to use JSON numbers: float64 is not enough
var jsonDecoder = json.NewDecoder(bytes.NewReader(contents[99:]))
jsonDecoder.UseNumber()
err = jsonDecoder.Decode(&repositoryMetadata)
if err != nil {
return err, ErrAddRepositoryUnmarshalMetadataError
}
// Get the public key and signature
signature := contents[3:67]
publicKey := contents[67:99]
// Look for the public key in the database
exists, matchingAuthor, matchingFingerprint, err := getFingerprintFromDB(publicKey, repositoryMetadata["author"].(string))
if err != nil {
return err, ErrAddRepositoryFailedToGetFingerprintError
} else {
err := handlePublicKeyCheck(exists, matchingAuthor, matchingFingerprint, publicKey, repositoryMetadata["author"].(string), addFingerprintToDB, logger)
if err != nil {
return err, ErrAddRepositoryFailedToAddFingerprintError
}
}
// We need to create a new xxHash instance
xxHash := xxhash.New()
_, err = xxHash.Write(contents[99:])
if err != nil {
return err, ErrAddRepositoryCannotHashError
}
// Verify the signature
if !ed25519.Verify(publicKey, xxHash.Sum(nil), signature) {
return errors.New("invalid signature"), ErrAddRepositoryInvalidMetadataError
}
// Now we can create the repository object
var repository Repository
var ok bool
repository.URL = url
repository.Name, ok = repositoryMetadata["name"].(string)
if !ok {
return errors.New("name is not a string"), ErrAddRepositoryInvalidMetadataError
}
repository.Description, ok = repositoryMetadata["desc"].(string)
if !ok {
return errors.New("desc is not a string"), ErrAddRepositoryInvalidMetadataError
}
repository.Owner, ok = repositoryMetadata["author"].(string)
if !ok {
return errors.New("author is not a string"), ErrAddRepositoryInvalidMetadataError
}
// First check if the directory exists
_, err = os.Stat("/var/lib/eon/repositories")
if err != nil {
if os.IsNotExist(err) {
// If the directory does not exist, create it
err = os.MkdirAll("/var/lib/eon/repositories", 0755)
if err != nil {
return err, ErrAddRepositoryCannotCreateDirError
}
} else {
return err, ErrAddRepositoryCannotStatDirError
}
}
// Write the contents of the repository to the database
packageList, ok := repositoryMetadata["packages"].([]interface{})
if !ok {
return errors.New("packages is not an array"), ErrAddRepositoryInvalidMetadataError
}
var remoteEPKs []RemoteEPK
for _, epk := range packageList {
epk, ok := epk.(map[string]interface{})
if !ok {
return errors.New("package is not an object"), ErrAddRepositoryInvalidMetadataError
}
name, ok := epk["name"].(string)
if !ok {
return errors.New("package name is not a string"), ErrAddRepositoryInvalidMetadataError
}
author, ok := epk["author"].(string)
if !ok {
return errors.New("package author is not a string"), ErrAddRepositoryInvalidMetadataError
}
arch, ok := epk["arch"].(string)
if !ok {
return errors.New("package arch is not a string"), ErrAddRepositoryInvalidMetadataError
}
versionString, ok := epk["version"].(string)
if !ok {
return errors.New("package version is not a string"), ErrAddRepositoryInvalidMetadataError
}
versionPointer, err := semver.NewVersion(versionString)
if err != nil {
return errors.New("package version is not a valid semver version"), ErrAddRepositoryInvalidMetadataError
}
sizeString, ok := epk["size"].(string)
if !ok {
return errors.New("package size is not a string"), ErrAddRepositoryInvalidMetadataError
}
size, err := strconv.ParseInt(sizeString, 10, 64)
if err != nil {
return errors.New("package size is not a number"), ErrAddRepositoryInvalidMetadataError
}
dependenciesInterface, ok := epk["deps"].([]interface{})
if !ok {
return errors.New("package dependencies is not an array"), ErrAddRepositoryInvalidMetadataError
}
dependencies, err := interfaceToStringSlice(dependenciesInterface, "dependencies")
if err != nil {
return err, ErrAddRepositoryInvalidMetadataError
}
hashJsonNumber, ok := epk["hash"].(json.Number)
if !ok {
return errors.New("package hash is not a number"), ErrAddRepositoryInvalidMetadataError
}
var hash uint64
hash, err = strconv.ParseUint(hashJsonNumber.String(), 10, 64)
if err != nil {
return errors.New("package hash is not a valid number"), ErrAddRepositoryInvalidMetadataError
}
fmt.Println(hash)
path, ok := epk["path"].(string)
if !ok {
return errors.New("package path is not a string"), ErrAddRepositoryInvalidMetadataError
}
description, ok := epk["desc"].(string)
if !ok {
return errors.New("package description is not a string"), ErrAddRepositoryInvalidMetadataError
}
remoteEPKs = append(remoteEPKs, RemoteEPK{
Name: name,
Author: author,
Description: description,
Version: *versionPointer,
Architecture: arch,
CompressedSize: size,
Dependencies: dependencies,
Path: path,
Arch: arch,
EPKHash: hash,
})
if err != nil {
return err, ErrAddRepositoryFailedToAddPackageError
}
}
// We add packages afterward so that if there is an error, we don't have to remove the packages
for _, epk := range remoteEPKs {
err := addRemotePackageToDB(repository.Name, epk)
if err != nil {
return err, ErrAddRepositoryFailedToAddPackageError
}
}
// Add the repository to the database
err = addRepositoryToDB(repository)
if err != nil {
if err.(*sqlite.Error).Code() == 2067 {
return nil, ErrAddRepositoryRepositoryAlreadyExistsError
} else {
return err, ErrAddRepositoryFailedToAddRepositoryError
}
}
// Alright, we're done here.
logger.LogFunc(Log{
Level: "INFO",
Content: "Added repository " + repository.Name + " to the database.",
})
return nil, nil
}
var ErrRemoveRepositoryDoesNotExistError = errors.New("repository does not exist")
var ErrRemoveRepositoryCannotStatRepositoryError = errors.New("cannot stat repository")
var ErrRemoveRepositoryCannotRemoveRepositoryError = errors.New("cannot remove repository")
var ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError = errors.New("failed to remove repository from database")
func RemoveRepository(repository string, removeRepositoryFromDB func(repository string) error, logger *Logger) (error, error) {
// First check if the file exists
_, err := os.Stat(filepath.Join("/var/lib/eon/repositories/", repository+".json"))
if err != nil {
if os.IsNotExist(err) {
return nil, ErrRemoveRepositoryDoesNotExistError
} else {
return err, ErrRemoveRepositoryCannotStatRepositoryError
}
}
// Remove the file
err = os.Remove(filepath.Join("/var/lib/eon/repositories/", repository+".json"))
if err != nil {
return err, ErrRemoveRepositoryCannotRemoveRepositoryError
}
// Purge the download cache
err = os.RemoveAll(filepath.Join("/var/cache/eon/repositories/", repository))
if err != nil {
return err, ErrRemoveRepositoryCannotRemoveRepositoryError
}
// Remove the repository from the database
err = removeRepositoryFromDB(repository)
if err != nil {
return err, ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError
}
// Alright, we're done here.
logger.LogFunc(Log{
Level: "INFO",
Content: "Removed repository " + repository + " from the database.",
})
return nil, nil
}