Add support for new index format

This commit is contained in:
Daniel 2022-08-16 13:43:54 +02:00
parent 0e5eb4b6de
commit f6fc67ad46
5 changed files with 165 additions and 14 deletions

View file

@ -1,13 +1,97 @@
package updater
import (
"encoding/json"
"errors"
"fmt"
"time"
)
// Index describes an index file pulled by the updater.
type Index struct {
// Path is the path to the index file
// on the update server.
Path string
// Channel holds the release channel name of the index.
// It must match the filename without extension.
Channel string
// PreRelease signifies that all versions of this index should be marked as
// pre-releases, no matter if the versions actually have a pre-release tag or
// not.
PreRelease bool
// LastRelease holds the time of the last seen release of this index.
LastRelease time.Time
}
// IndexFile represents an index file.
type IndexFile struct {
Channel string
Published time.Time
Releases map[string]string
}
var (
// ErrIndexFromFuture is returned when a signed index is parsed with a
// Published timestamp that lies in the future.
ErrIndexFromFuture = errors.New("index is from the future")
// ErrIndexIsOlder is returned when a signed index is parsed with an older
// Published timestamp than the current Published timestamp.
ErrIndexIsOlder = errors.New("index is older than the current one")
// ErrIndexChannelMismatch is returned when a signed index is parsed with a
// different channel that the expected one.
ErrIndexChannelMismatch = errors.New("index does not match the expected channel")
)
// ParseIndexFile parses an index file and checks if it is valid.
func ParseIndexFile(indexData []byte, channel string, currentPublished time.Time) (*IndexFile, error) {
// Load into struct.
indexFile := &IndexFile{}
err := json.Unmarshal(indexData, indexFile)
if err != nil {
return nil, fmt.Errorf("failed to parse signed index data: %w", err)
}
// Fallback to old format if there are no releases and no channel is defined.
// TODO: Remove in v0.10
if len(indexFile.Releases) == 0 && indexFile.Channel == "" {
return loadOldIndexFormat(indexData, channel)
}
// Check the index metadata.
switch {
case !indexFile.Published.IsZero() && time.Now().Before(indexFile.Published):
return indexFile, ErrIndexFromFuture
case !indexFile.Published.IsZero() &&
!currentPublished.IsZero() &&
currentPublished.After(indexFile.Published):
return indexFile, ErrIndexIsOlder
case channel != "" &&
indexFile.Channel != "" &&
channel != indexFile.Channel:
return indexFile, ErrIndexChannelMismatch
}
return indexFile, nil
}
func loadOldIndexFormat(indexData []byte, channel string) (*IndexFile, error) {
releases := make(map[string]string)
err := json.Unmarshal(indexData, &releases)
if err != nil {
return nil, err
}
return &IndexFile{
Channel: channel,
Published: time.Now(),
Releases: releases,
}, nil
}

55
updater/indexes_test.go Normal file
View file

@ -0,0 +1,55 @@
package updater
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
var (
oldFormat = `{
"all/ui/modules/assets.zip": "0.3.0",
"all/ui/modules/portmaster.zip": "0.2.4",
"linux_amd64/core/portmaster-core": "0.8.13"
}`
newFormat = `{
"Channel": "stable",
"Published": "2022-01-02T00:00:00Z",
"Releases": {
"all/ui/modules/assets.zip": "0.3.0",
"all/ui/modules/portmaster.zip": "0.2.4",
"linux_amd64/core/portmaster-core": "0.8.13"
}
}`
formatTestChannel = "stable"
formatTestReleases = map[string]string{
"all/ui/modules/assets.zip": "0.3.0",
"all/ui/modules/portmaster.zip": "0.2.4",
"linux_amd64/core/portmaster-core": "0.8.13",
}
)
func TestIndexParsing(t *testing.T) {
lastRelease, err := time.Parse(time.RFC3339, "2022-01-01T00:00:00Z")
if err != nil {
t.Fatal(err)
}
oldIndexFile, err := ParseIndexFile([]byte(oldFormat), formatTestChannel, lastRelease)
if err != nil {
t.Fatal(err)
}
newIndexFile, err := ParseIndexFile([]byte(newFormat), formatTestChannel, lastRelease)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, formatTestChannel, oldIndexFile.Channel, "channel should be the same")
assert.Equal(t, formatTestChannel, newIndexFile.Channel, "channel should be the same")
assert.Equal(t, formatTestReleases, oldIndexFile.Releases, "releases should be the same")
assert.Equal(t, formatTestReleases, newIndexFile.Releases, "releases should be the same")
}

View file

@ -3,7 +3,9 @@ package updater
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/safing/portbase/log"
@ -50,6 +52,11 @@ func (reg *ResourceRegistry) AddIndex(idx Index) {
reg.Lock()
defer reg.Unlock()
// Get channel name from path.
idx.Channel = strings.TrimSuffix(
filepath.Base(idx.Path), filepath.Ext(idx.Path),
)
reg.indexes = append(reg.indexes, idx)
}

View file

@ -2,7 +2,6 @@ package updater
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
@ -131,18 +130,26 @@ func (reg *ResourceRegistry) loadIndexFile(idx Index) error {
return err
}
releases := make(map[string]string)
err = json.Unmarshal(data, &releases)
// Parse the index file.
indexFile, err := ParseIndexFile(data, idx.Channel, idx.LastRelease)
if err != nil {
return err
}
if len(releases) == 0 {
log.Debugf("%s: index %s is empty", reg.Name, idx.Path)
// Update last seen release.
// TODO: We are working with a copy here, so this has no effect.
// This does not break to current implementation, but make the
// protection ineffective.
idx.LastRelease = indexFile.Published
// Warn if there aren't any releases in the index.
if len(indexFile.Releases) == 0 {
log.Debugf("%s: index %s has no releases", reg.Name, idx.Path)
return nil
}
err = reg.AddResources(releases, false, true, idx.PreRelease)
// Add index releases to available resources.
err = reg.AddResources(indexFile.Releases, false, true, idx.PreRelease)
if err != nil {
log.Warningf("%s: failed to add resource: %s", reg.Name, err)
}

View file

@ -2,7 +2,6 @@ package updater
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
@ -52,23 +51,22 @@ func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Cli
return fmt.Errorf("failed to download index %s: %w", idx.Path, err)
}
// parse
newIndexData := make(map[string]string)
err = json.Unmarshal(data, &newIndexData)
// Parse the index file.
indexFile, err := ParseIndexFile(data, idx.Channel, idx.LastRelease)
if err != nil {
return fmt.Errorf("failed to parse index %s: %w", idx.Path, err)
}
// Add index data to registry.
if len(newIndexData) > 0 {
if len(indexFile.Releases) > 0 {
// Check if all resources are within the indexes' authority.
authoritativePath := path.Dir(idx.Path) + "/"
if authoritativePath == "./" {
// Fix path for indexes at the storage root.
authoritativePath = ""
}
cleanedData := make(map[string]string, len(newIndexData))
for key, version := range newIndexData {
cleanedData := make(map[string]string, len(indexFile.Releases))
for key, version := range indexFile.Releases {
if strings.HasPrefix(key, authoritativePath) {
cleanedData[key] = version
} else {
@ -100,7 +98,7 @@ func (reg *ResourceRegistry) downloadIndex(ctx context.Context, client *http.Cli
log.Warningf("%s: failed to save updated index %s: %s", reg.Name, idx.Path, err)
}
log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(newIndexData))
log.Infof("%s: updated index %s with %d entries", reg.Name, idx.Path, len(indexFile.Releases))
return nil
}