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 }