2024-09-01 12:27:25 -07:00
package lib
import (
"bufio"
"bytes"
"errors"
"io"
2024-09-03 11:58:53 -07:00
"net/url"
2024-09-01 12:27:25 -07:00
"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 {
2024-09-03 11:58:53 -07:00
Repository Repository
2024-09-01 12:27:25 -07:00
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
2024-09-03 11:58:53 -07:00
DecompressedSize * big . Int
2024-09-01 12:27:25 -07:00
}
// 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
2024-09-03 11:58:53 -07:00
DecompressedSize * big . Int
2024-09-01 12:27:25 -07:00
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 {
2024-09-03 11:58:53 -07:00
FileStream * os . File
RepositoryName string
URL string
Bytes [ ] byte
IsURL bool
IsFileStream bool
2024-09-01 12:27:25 -07:00
}
// 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 , ":" )
}
2024-09-03 11:58:53 -07:00
func preMapEpkFromBytes ( metaDataBytes [ ] byte , littleEndian bool , size int64 , offset int64 ) ( EPKPreMap , error ) {
// Unmarshal the JSON
var displayDataMap map [ string ] interface { }
err := json . Unmarshal ( metaDataBytes , & displayDataMap )
if err != nil {
return EPKPreMap { } , errors . New ( "metadata is not valid JSON" )
}
// 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 . TarOffset = offset
parsedDisplayData . Size = size
// 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" )
}
parsedDisplayData . DecompressedSize = new ( big . Int )
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" )
}
parsedDisplayData . Author , ok = displayDataMap [ "author" ] . ( string )
if ! ok {
return EPKPreMap { } , errors . New ( "author is not a string" )
}
versionString , ok := displayDataMap [ "version" ] . ( string )
if ! ok {
return EPKPreMap { } , errors . New ( "version is not a string" )
}
versionPointer , err := semver . NewVersion ( versionString )
if err != nil {
return EPKPreMap { } , err
}
parsedDisplayData . Version = * versionPointer
parsedDisplayData . Architecture , ok = displayDataMap [ "arch" ] . ( string )
if ! ok {
return EPKPreMap { } , errors . New ( "arch is not a string" )
}
dependencies , ok := displayDataMap [ "deps" ] . ( [ ] interface { } )
if ! ok {
return EPKPreMap { } , errors . New ( "dependencies is not an array" )
}
parsedDisplayData . Dependencies , err = interfaceToStringSlice ( dependencies , "dependencies" )
if err != nil {
return EPKPreMap { } , err
}
return parsedDisplayData , nil
}
2024-09-01 12:27:25 -07:00
// 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" )
2024-09-03 11:58:53 -07:00
var ErrPreMapEPKNetworkStreamError = errors . New ( "network streams are not supported" )
2024-09-01 12:27:25 -07:00
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 ) {
2024-09-03 11:58:53 -07:00
// Say that we don't support network streams
if epkBytes . IsURL {
return EPKPreMap { } , nil , ErrPreMapEPKNetworkStreamError
}
2024-09-01 12:27:25 -07:00
// First, we need to check if it even is a EPK by checking the first 3 magic bytes
2024-09-03 11:58:53 -07:00
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
var magicBytes = make ( [ ] byte , 3 )
2024-09-03 11:58:53 -07:00
_ , err := epkBytes . FileStream . ReadAt ( magicBytes , 0 )
2024-09-01 12:27:25 -07:00
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
2024-09-03 11:58:53 -07:00
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
var littleEndianByte = make ( [ ] byte , 1 )
2024-09-03 11:58:53 -07:00
_ , err := epkBytes . FileStream . ReadAt ( littleEndianByte , 3 )
2024-09-01 12:27:25 -07:00
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
2024-09-03 11:58:53 -07:00
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
var tarArchiveOffsetBytes = make ( [ ] byte , 8 )
2024-09-03 11:58:53 -07:00
_ , err := epkBytes . FileStream . ReadAt ( tarArchiveOffsetBytes , 4 )
2024-09-01 12:27:25 -07:00
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.
2024-09-03 11:58:53 -07:00
var preMapEpk EPKPreMap
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
var metadataBuffer = make ( [ ] byte , tarArchiveOffset - ConstMapEPKMetadataOffset )
2024-09-03 11:58:53 -07:00
_ , err := epkBytes . FileStream . ReadAt ( metadataBuffer , ConstMapEPKMetadataOffset )
2024-09-01 12:27:25 -07:00
if err != nil {
return EPKPreMap { } , err , ErrPreMapEPKFailedToReadError
}
2024-09-03 11:58:53 -07:00
preMapEpk , err = preMapEpkFromBytes ( metadataBuffer , littleEndian , epkSize , tarArchiveOffset )
2024-09-01 12:27:25 -07:00
} else {
2024-09-03 11:58:53 -07:00
var err error
preMapEpk , err = preMapEpkFromBytes ( epkBytes . Bytes [ ConstMapEPKMetadataOffset : tarArchiveOffset ] , littleEndian , epkSize , tarArchiveOffset )
2024-09-01 12:27:25 -07:00
if err != nil {
2024-09-03 11:58:53 -07:00
return EPKPreMap { } , err , ErrPreMapEPKCouldNotMapJSONError
2024-09-01 12:27:25 -07:00
}
}
2024-09-03 11:58:53 -07:00
return preMapEpk , nil , nil
}
2024-09-01 12:27:25 -07:00
2024-09-03 11:58:53 -07:00
var ErrPreMapRemoteEPKCannotCreateURLError = errors . New ( "could not create URL" )
var ErrPreMapRemoteEPKCannotCreateRequestError = errors . New ( "could not create request" )
var ErrPreMapRemoteEPKFailedToSendRequestError = errors . New ( "failed to send request" )
var ErrPreMapRemoteEPKFailedToReadError = errors . New ( "failed to read EPK" )
var ErrPreMapRemoteEPKFailedToCloseConnectionError = errors . New ( "failed to close connection" )
var ErrPreMapRemoteEPKUnexpectedStatusCodeError = errors . New ( "unexpected status code" )
var ErrPreMapRemoteEPKNotEPKError = errors . New ( "not an EPK" )
var ErrPreMapRemoteEPKInvalidEndianError = errors . New ( "invalid endian" )
var ErrPreMapRemoteEPKCouldNotMapJSONError = errors . New ( "error mapping metadata" )
func PreMapRemoteEPK ( remoteEPK RemoteEPK , logger * Logger ) ( EPKPreMap , error , error ) {
// Fetch the first 12 bytes of the EPK - this contains the magic, endian, and offset
// We use the range header to only fetch the first 12 bytes
packageUrl , err := url . JoinPath ( remoteEPK . Repository . URL , remoteEPK . Path )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKCannotCreateURLError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
req , err := http . NewRequest ( "GET" , packageUrl , nil )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKCannotCreateRequestError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
req . Header . Set ( "Range" , "bytes=0-12" )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToSendRequestError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
// Check if the status code is 206 (partial content)
var epkHeaderBytes = make ( [ ] byte , 12 )
var rangeSupported bool
if resp . StatusCode == 200 {
// We have the entire file. Not great, not terrible.
// We'll have to cut off the connection early later. To optimise things slightly, we'll reuse this connection
// to read the metadata later.
// I'm deadly serious about the radiation. It could cause a bit flip causing the Range header to be malformed.
// It's amazing how many times I error handled for this, and I hope I can save someone from cancer one day.
logger . LogFunc ( Log {
Level : "INFO" ,
Content : "The server does not support range requests. The installation process will be significantly slower." +
"Is the repository owner using python3's SimpleHTTPServer or similar? If so, please use a proper web " +
"server like Nginx, Ailur HTTP Server, or Apache. If not, please report this to the repository owner or " +
"check for sources of radiation around your computer." ,
} )
_ , err := resp . Body . Read ( epkHeaderBytes )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToReadError
}
rangeSupported = false
} else if resp . StatusCode == 206 {
// Great, everything is working as expected.
_ , err := io . ReadFull ( resp . Body , epkHeaderBytes )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToReadError
}
rangeSupported = true
// Close the connection
err = resp . Body . Close ( )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToCloseConnectionError
}
} else {
// Something went wrong
return EPKPreMap { } , errors . New ( "unexpected status code: " + strconv . Itoa ( resp . StatusCode ) ) , ErrPreMapRemoteEPKUnexpectedStatusCodeError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
// Now we verify the magic bytes
if string ( epkHeaderBytes [ 0 : 3 ] ) != "epk" {
return EPKPreMap { } , nil , ErrPreMapRemoteEPKNotEPKError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
// Let's determine the endian-ness of the EPK via the 3rd byte
var littleEndian bool
if epkHeaderBytes [ 3 ] == 0x6C {
littleEndian = true
} else if epkHeaderBytes [ 3 ] == 0x62 {
littleEndian = false
} else {
return EPKPreMap { } , nil , ErrPreMapRemoteEPKInvalidEndianError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
// Now we can get the offsets of the tar archive
var tarArchiveOffset int64
if littleEndian {
tarArchiveOffset = int64 ( binary . LittleEndian . Uint64 ( epkHeaderBytes [ 4 : 12 ] ) )
} else {
tarArchiveOffset = int64 ( binary . BigEndian . Uint64 ( epkHeaderBytes [ 4 : 12 ] ) )
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
// No signature verification for you
// Let's fetch the display data bytes
displayDataBytes := make ( [ ] byte , tarArchiveOffset - ConstMapEPKMetadataOffset )
if rangeSupported {
// Send another request to fetch the display data
req . Header . Set ( "Range" , "bytes=108-" + strconv . FormatInt ( tarArchiveOffset - 1 , 10 ) )
resp , err = http . DefaultClient . Do ( req )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToSendRequestError
}
// Read the display data
_ , err = io . ReadFull ( resp . Body , displayDataBytes )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToReadError
}
// Close the connection
err = resp . Body . Close ( )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToCloseConnectionError
}
} else {
// Re-use the connection to read the display data
// The offset will move automatically because we are reading from the same connection, therefore
// meaning that the web server will have already iterated past the header
_ , err = io . ReadFull ( resp . Body , displayDataBytes )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToReadError
}
// You didn't have to cut me off, make out like it never happened and that we were nothing
// All I wanted was a header part, but you just had to go and give me the whole thing
// Now you're just some obscure web server that I used to know
err = resp . Body . Close ( )
if err != nil {
return EPKPreMap { } , err , ErrPreMapRemoteEPKFailedToCloseConnectionError
}
}
// Now we can map the display data
var preMapEpk EPKPreMap
preMapEpk , err = preMapEpkFromBytes ( displayDataBytes , littleEndian , remoteEPK . CompressedSize , tarArchiveOffset )
2024-09-01 12:27:25 -07:00
if err != nil {
2024-09-03 11:58:53 -07:00
return EPKPreMap { } , err , ErrPreMapRemoteEPKCouldNotMapJSONError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
return preMapEpk , nil , nil
2024-09-01 12:27:25 -07:00
}
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 )
2024-09-03 11:58:53 -07:00
var connection io . ReadCloser
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
// Before we continue, check if the signature is valid
// To get the signature, we read from the 12th byte to the 76th byte
2024-09-03 11:58:53 -07:00
_ , err := epkBytes . FileStream . ReadAt ( signature , 12 )
2024-09-01 12:27:25 -07:00
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
// To get the public key, we read from the 76th byte to the 108th byte
2024-09-03 11:58:53 -07:00
_ , err = epkBytes . FileStream . ReadAt ( publicKey , 76 )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
} else if epkBytes . IsURL {
// Before we continue, check if the signature is valid
// Fetch range 12 - EOF and read them in
req , err := http . NewRequest ( "GET" , epkBytes . URL , nil )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
req . Header . Set ( "Range" , "bytes=12-" )
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
// Set the connection
connection = resp . Body
// Check the status code
if resp . StatusCode == 200 {
// Not great, not terrible.
// We'll have to cut off the connection early later.
// Warn the user
logger . LogFunc ( Log {
Level : "INFO" ,
Content : "The server does not support range requests. It is recommended to use the -O flag to download " +
"the entire file to memory rather than streaming it, which in this case, will be significantly slower, " +
"as we have to read then immediately discard bytes in order to reach an offset. You've likely already " +
"been warned by the repository refresh command, so I won't prompt you again." ,
} )
// Discard the first 12 bytes
_ , err := io . CopyN ( io . Discard , connection , 12 )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
} else if resp . StatusCode != 206 {
return & Metadata { } , errors . New ( "unexpected status code: " + strconv . Itoa ( resp . StatusCode ) ) , ErrFullyMapMetadataFailedToReadError
}
// Read the signature
_ , err = io . ReadFull ( connection , signature )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
// Read the public key
_ , err = io . ReadFull ( connection , publicKey )
2024-09-01 12:27:25 -07:00
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 ( )
2024-09-03 11:58:53 -07:00
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
// 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
2024-09-03 11:58:53 -07:00
_ , err = epkBytes . FileStream . Seek ( ConstMapEPKMetadataOffset , io . SeekStart )
2024-09-01 12:27:25 -07:00
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToJumpError
}
// Streaming bytes to the hash is more memory efficient
2024-09-03 11:58:53 -07:00
_ , err = epkBytes . FileStream . 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 if epkBytes . IsURL {
// Now we can verify the signature. We can just stream the rest of the EPK to the hash
_ , err = io . Copy ( xxHash , connection )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
// You didn't have to cut me off...
// Don't worry, we are reading to EOF anyway, no matter if we do have a non-range supported server, so we
// (probably) won't upset the server owner.
err = connection . Close ( )
2024-09-01 12:27:25 -07:00
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
2024-09-03 11:58:53 -07:00
2024-09-01 12:27:25 -07:00
// 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
2024-09-03 11:58:53 -07:00
_ , err := xxHash . Write ( epkBytes . Bytes [ ConstMapEPKMetadataOffset : ] )
if err != nil {
return & Metadata { } , err , ErrFullyMapMetadataFailedToReadError
}
if ! ed25519 . Verify ( publicKey , xxHash . Sum ( nil ) , signature ) {
2024-09-01 12:27:25 -07:00
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
}
2024-09-03 11:58:53 -07:00
parsedMetadata . DecompressedSize = new ( big . Int )
2024-09-01 12:27:25 -07:00
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" )
2024-09-03 11:58:53 -07:00
// ProgressWriter implements a writer that intercepts writes in order to log progress
type ProgressWriter struct {
Logger * Logger
Total * big . Int
Writer io . Writer
}
// Write writes to the ProgressWriter
func ( writer * ProgressWriter ) Write ( p [ ] byte ) ( n int , err error ) {
byteCount := new ( big . Int )
for range p {
byteCount . Add ( byteCount , big . NewInt ( 1 ) )
}
if writer . Logger . ProgressSupported {
writer . Logger . LogFunc ( Log {
Level : "PROGRESS" ,
Progress : byteCount ,
Total : writer . Total ,
Overwrite : true ,
} )
} else {
writer . Logger . LogFunc ( Log {
Level : "INFO" ,
Content : "Written " + humanize . BigIBytes ( byteCount ) + " out of " + humanize . BigIBytes ( writer . Total ) ,
Prompt : false ,
} )
}
written , err := writer . Writer . Write ( p )
if err != nil {
return written , err
}
return written , nil
}
2024-09-01 12:27:25 -07:00
// InstallEPK installs an EPK file
2024-09-03 11:58:53 -07:00
func InstallEPK ( epkBytes StreamOrBytes , metadata * Metadata , preMap * EPKPreMap , addEPKToDB func ( * Metadata , [ ] byte , bool , bool , int64 , ... string ) error , logger * Logger ) ( string , error , error ) {
2024-09-01 12:27:25 -07:00
// Create the temporary directory
tempDir , err := os . MkdirTemp ( "/tmp" , "eon-install-" )
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotCreateTempDirError
}
var zStandardReader * zstd . Decoder
2024-09-03 11:58:53 -07:00
var connection io . ReadCloser
if epkBytes . IsFileStream {
2024-09-01 12:27:25 -07:00
// Seek to the correct position in the EPK
2024-09-03 11:58:53 -07:00
_ , err = epkBytes . FileStream . Seek ( preMap . TarOffset , io . SeekStart )
2024-09-01 12:27:25 -07:00
if err != nil {
return "" , err , nil
}
// Create a ZStandard reader reading from the EPK
2024-09-03 11:58:53 -07:00
zStandardReader , err = zstd . NewReader ( epkBytes . FileStream )
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotCreateZStandardReaderError
}
} else if epkBytes . IsURL {
// Range header to the tar offset
req , err := http . NewRequest ( "GET" , epkBytes . URL , nil )
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotCreateZStandardReaderError
}
// Set the range header
req . Header . Set ( "Range" , "bytes=" + strconv . FormatInt ( preMap . TarOffset , 10 ) + "-" )
// Send the request
resp , err := http . DefaultClient . Do ( req )
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotCreateZStandardReaderError
}
// Set connection to the response body
connection = resp . Body
// Check the status code
if resp . StatusCode == 200 {
// Not great, is terrible in this case, we have to keep reading bytes and discarding them until we reach the offset
// The user will have already been warned about 300 times, so we don't need to warn them again
// God this is painful. Let's give the user a progress bar to make it less painful
_ , err := io . CopyN ( & ProgressWriter {
Logger : logger ,
Total : big . NewInt ( preMap . TarOffset ) ,
Writer : io . Discard ,
} , connection , preMap . TarOffset )
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotDecompressTarArchiveError
}
} else if resp . StatusCode != 206 {
// Something went wrong
return tempDir , errors . New ( "unexpected status code: " + strconv . Itoa ( resp . StatusCode ) ) , ErrInstallEPKCouldNotCreateZStandardReaderError
}
// Create a ZStandard reader reading from the EPK
zStandardReader , err = zstd . NewReader ( connection )
2024-09-01 12:27:25 -07:00
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
2024-09-03 11:58:53 -07:00
written := new ( big . Int )
2024-09-01 12:27:25 -07:00
stop := make ( chan bool )
go func ( ) {
for {
select {
case <- stop :
return
default :
if logger . ProgressSupported {
logger . LogFunc ( Log {
Level : "PROGRESS" ,
2024-09-03 11:58:53 -07:00
Progress : written ,
Total : metadata . DecompressedSize ,
2024-09-01 12:27:25 -07:00
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
}
2024-09-03 11:58:53 -07:00
written . Add ( written , big . NewInt ( writtenFile ) )
2024-09-01 12:27:25 -07:00
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 ( )
2024-09-03 11:58:53 -07:00
// Close the connection if it's a URL
if epkBytes . IsURL {
err = connection . Close ( )
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotCloseTarReaderError
}
}
2024-09-01 12:27:25 -07:00
// 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 ( )
2024-09-03 11:58:53 -07:00
logger . LogFunc ( Log {
Level : "INFO" ,
Content : message ,
} )
2024-09-01 12:27:25 -07:00
}
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 ( )
2024-09-03 11:58:53 -07:00
logger . LogFunc ( Log {
Level : "INFO" ,
Content : message ,
} )
2024-09-01 12:27:25 -07:00
}
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 ) {
2024-09-03 11:58:53 -07:00
var err error
if ! epkBytes . IsURL {
err = addEPKToDB ( metadata , [ ] byte { } , false , false , metadata . Size )
} else {
err = addEPKToDB ( metadata , [ ] byte { } , false , false , metadata . Size , epkBytes . RepositoryName )
}
2024-09-01 12:27:25 -07:00
if err != nil {
return tempDir , err , ErrInstallEPKCouldNotAddEPKToDBError
}
} else {
return tempDir , err , ErrInstallEPKCouldNotAddEPKToDBError
}
} else {
2024-09-03 11:58:53 -07:00
var err error
if ! epkBytes . IsURL {
err = addEPKToDB ( metadata , file , false , true , metadata . Size )
} else {
err = addEPKToDB ( metadata , file , false , true , metadata . Size , epkBytes . RepositoryName )
}
2024-09-01 12:27:25 -07:00
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" )
2024-09-03 11:58:53 -07:00
// AddRepository adds a repository to the database
func AddRepository ( url string , addRepositoryToDB func ( Repository , bool ) error , getFingerprintFromDB func ( [ ] byte , string ) ( bool , bool , bool , error ) , addFingerprintToDB func ( [ ] byte , string , bool ) error , addRemotePackageToDB func ( RemoteEPK ) error , checkRepositoryInDB func ( string ) ( bool , error ) , forceReplace bool , logger * Logger ) ( string , error , error ) {
2024-09-01 12:27:25 -07:00
// 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 {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryCannotCreateRequestError
2024-09-01 12:27:25 -07:00
}
// Add the range header
magicRequest . Header . Add ( "Range" , "bytes=0-3" )
// Send the request
magicResponse , err := http . DefaultClient . Do ( magicRequest )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryCannotSendRequestError
2024-09-01 12:27:25 -07:00
}
// 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 {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "status code " + strconv . Itoa ( magicResponse . StatusCode ) ) , ErrAddRepositoryUnexpectedStatusCodeError
2024-09-01 12:27:25 -07:00
}
}
// Check the magic bytes
var magicBytes = make ( [ ] byte , 3 )
_ , err = magicResponse . Body . Read ( magicBytes )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryCannotReadResponseError
2024-09-01 12:27:25 -07:00
}
// Check if the magic bytes are "eon"
if string ( magicBytes ) != "eon" {
2024-09-03 11:58:53 -07:00
return "" , nil , ErrAddRepositoryInvalidMagicError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
// Great. We either confirmed the repository is an Eon repository or we have the entire file.
2024-09-01 12:27:25 -07:00
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 {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryCannotSendRequestError
2024-09-01 12:27:25 -07:00
}
} else {
fullFetch = magicResponse
}
// Now we get the contents of the file
contents , err := io . ReadAll ( fullFetch . Body )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryCannotReadResponseError
2024-09-01 12:27:25 -07:00
}
// 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 {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryUnmarshalMetadataError
2024-09-01 12:27:25 -07:00
}
// 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 {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryFailedToGetFingerprintError
2024-09-01 12:27:25 -07:00
} else {
err := handlePublicKeyCheck ( exists , matchingAuthor , matchingFingerprint , publicKey , repositoryMetadata [ "author" ] . ( string ) , addFingerprintToDB , logger )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryFailedToAddFingerprintError
2024-09-01 12:27:25 -07:00
}
}
// We need to create a new xxHash instance
xxHash := xxhash . New ( )
_ , err = xxHash . Write ( contents [ 99 : ] )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryCannotHashError
2024-09-01 12:27:25 -07:00
}
// Verify the signature
if ! ed25519 . Verify ( publicKey , xxHash . Sum ( nil ) , signature ) {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "invalid signature" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
// Now we can create the repository object
var repository Repository
var ok bool
repository . URL = url
repository . Name , ok = repositoryMetadata [ "name" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "name is not a string" ) , ErrAddRepositoryInvalidMetadataError
}
// In force replace mode, we don't check if the repository already exists and just replace it
if ! forceReplace {
// Side quest: check if the repository already exists
repoExists , err := checkRepositoryInDB ( repository . Name )
if err != nil {
return "" , err , ErrAddRepositoryFailedToAddRepositoryError
} else if repoExists {
return "" , nil , ErrAddRepositoryRepositoryAlreadyExistsError
}
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
2024-09-01 12:27:25 -07:00
repository . Description , ok = repositoryMetadata [ "desc" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "desc is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
repository . Owner , ok = repositoryMetadata [ "author" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "author is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
// Write the contents of the repository to the database
packageList , ok := repositoryMetadata [ "packages" ] . ( [ ] interface { } )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "packages is not an array" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
var remoteEPKs [ ] RemoteEPK
for _ , epk := range packageList {
epk , ok := epk . ( map [ string ] interface { } )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package is not an object" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
name , ok := epk [ "name" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package name is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
author , ok := epk [ "author" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package author is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
arch , ok := epk [ "arch" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package arch is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
versionString , ok := epk [ "version" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package version is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
versionPointer , err := semver . NewVersion ( versionString )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package version is not a valid semver version" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
sizeString , ok := epk [ "size" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package size is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
size , err := strconv . ParseInt ( sizeString , 10 , 64 )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package size is not a number" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
dependenciesInterface , ok := epk [ "deps" ] . ( [ ] interface { } )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package dependencies is not an array" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
dependencies , err := interfaceToStringSlice ( dependenciesInterface , "dependencies" )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
hashJsonNumber , ok := epk [ "hash" ] . ( json . Number )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package hash is not a number" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
var hash uint64
hash , err = strconv . ParseUint ( hashJsonNumber . String ( ) , 10 , 64 )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package hash is not a valid number" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
path , ok := epk [ "path" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package path is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
description , ok := epk [ "desc" ] . ( string )
if ! ok {
2024-09-03 11:58:53 -07:00
return "" , errors . New ( "package description is not a string" ) , ErrAddRepositoryInvalidMetadataError
2024-09-01 12:27:25 -07:00
}
remoteEPKs = append ( remoteEPKs , RemoteEPK {
Name : name ,
Author : author ,
Description : description ,
Version : * versionPointer ,
Architecture : arch ,
CompressedSize : size ,
Dependencies : dependencies ,
Path : path ,
Arch : arch ,
EPKHash : hash ,
2024-09-03 11:58:53 -07:00
Repository : repository ,
2024-09-01 12:27:25 -07:00
} )
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryFailedToAddPackageError
2024-09-01 12:27:25 -07:00
}
}
// We add packages afterward so that if there is an error, we don't have to remove the packages
for _ , epk := range remoteEPKs {
2024-09-03 11:58:53 -07:00
err := addRemotePackageToDB ( epk )
2024-09-01 12:27:25 -07:00
if err != nil {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryFailedToAddPackageError
2024-09-01 12:27:25 -07:00
}
}
// Add the repository to the database
2024-09-03 11:58:53 -07:00
err = addRepositoryToDB ( repository , forceReplace )
2024-09-01 12:27:25 -07:00
if err != nil {
if err . ( * sqlite . Error ) . Code ( ) == 2067 {
2024-09-03 11:58:53 -07:00
return "" , nil , ErrAddRepositoryRepositoryAlreadyExistsError
2024-09-01 12:27:25 -07:00
} else {
2024-09-03 11:58:53 -07:00
return "" , err , ErrAddRepositoryFailedToAddRepositoryError
2024-09-01 12:27:25 -07:00
}
}
2024-09-03 11:58:53 -07:00
return repository . Name , nil , nil
2024-09-01 12:27:25 -07:00
}
var ErrRemoveRepositoryDoesNotExistError = errors . New ( "repository does not exist" )
2024-09-03 11:58:53 -07:00
var ErrRemoveRepositoryCannotFindRepositoryError = errors . New ( "cannot check for repository" )
2024-09-01 12:27:25 -07:00
var ErrRemoveRepositoryCannotRemoveRepositoryError = errors . New ( "cannot remove repository" )
var ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError = errors . New ( "failed to remove repository from database" )
2024-09-03 11:58:53 -07:00
func RemoveRepository ( repository string , removeRepositoryFromDB func ( repository string ) error , checkRepositoryInDB func ( repository string ) ( bool , error ) , logger * Logger ) ( error , error ) {
// First check if the repository exists
exists , err := checkRepositoryInDB ( repository )
2024-09-01 12:27:25 -07:00
if err != nil {
2024-09-03 11:58:53 -07:00
return err , ErrRemoveRepositoryCannotFindRepositoryError
2024-09-01 12:27:25 -07:00
}
2024-09-03 11:58:53 -07:00
if ! exists {
return nil , ErrRemoveRepositoryDoesNotExistError
2024-09-01 12:27:25 -07:00
}
// 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
}