WIP
This commit is contained in:
@@ -1,19 +1,27 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ghodss/yaml"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/notifications"
|
||||
"github.com/safing/portmaster/base/rng"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
@@ -149,6 +157,17 @@ func registerAPIEndpoints() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := api.RegisterEndpoint(api.Endpoint{
|
||||
Name: "Get Resource",
|
||||
Description: "Returns the requested resource from the udpate system",
|
||||
Path: `updates/get/?{artifact_path:[A-Za-z0-9/\.\-_]{1,255}}/{artifact_name:[A-Za-z0-9\.\-_]{1,255}}`,
|
||||
Read: api.PermitUser,
|
||||
ReadMethod: http.MethodGet,
|
||||
HandlerFunc: getUpdateResource,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -170,6 +189,113 @@ func restart(_ *api.Request) (msg string, err error) {
|
||||
return "restart initiated", nil
|
||||
}
|
||||
|
||||
func getUpdateResource(w http.ResponseWriter, r *http.Request) {
|
||||
// Get identifier from URL.
|
||||
var identifier string
|
||||
if ar := api.GetAPIRequest(r); ar != nil {
|
||||
identifier = ar.URLVars["artifact_name"]
|
||||
}
|
||||
if identifier == "" {
|
||||
http.Error(w, "no resource specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get resource.
|
||||
artifact, err := module.instance.BinaryUpdates().GetFile(identifier)
|
||||
if err != nil {
|
||||
intelArtifact, intelErr := module.instance.IntelUpdates().GetFile(identifier)
|
||||
if intelErr == nil {
|
||||
artifact = intelArtifact
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Open file for reading.
|
||||
file, err := os.Open(artifact.Path())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close() //nolint:errcheck,gosec
|
||||
|
||||
// Assign file to reader
|
||||
var reader io.Reader = file
|
||||
|
||||
// Add version and hash to header.
|
||||
if artifact.Version != "" {
|
||||
w.Header().Set("Resource-Version", artifact.Version)
|
||||
}
|
||||
if artifact.SHA256 != "" {
|
||||
w.Header().Set("Resource-SHA256", artifact.SHA256)
|
||||
}
|
||||
|
||||
// Set Content-Type.
|
||||
contentType, _ := utils.MimeTypeByExtension(filepath.Ext(artifact.Path()))
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
|
||||
// Check if the content type may be returned.
|
||||
accept := r.Header.Get("Accept")
|
||||
if accept != "" {
|
||||
mimeTypes := strings.Split(accept, ",")
|
||||
// First, clean mime types.
|
||||
for i, mimeType := range mimeTypes {
|
||||
mimeType = strings.TrimSpace(mimeType)
|
||||
mimeType, _, _ = strings.Cut(mimeType, ";")
|
||||
mimeTypes[i] = mimeType
|
||||
}
|
||||
// Second, check if we may return anything.
|
||||
var acceptsAny bool
|
||||
for _, mimeType := range mimeTypes {
|
||||
switch mimeType {
|
||||
case "*", "*/*":
|
||||
acceptsAny = true
|
||||
}
|
||||
}
|
||||
// Third, check if we can convert.
|
||||
if !acceptsAny {
|
||||
var converted bool
|
||||
sourceType, _, _ := strings.Cut(contentType, ";")
|
||||
findConvertiblePair:
|
||||
for _, mimeType := range mimeTypes {
|
||||
switch {
|
||||
case sourceType == "application/yaml" && mimeType == "application/json":
|
||||
yamlData, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
jsonData, err := yaml.YAMLToJSON(yamlData)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
reader = bytes.NewReader(jsonData)
|
||||
converted = true
|
||||
break findConvertiblePair
|
||||
}
|
||||
}
|
||||
|
||||
// If we could not convert to acceptable format, return an error.
|
||||
if !converted {
|
||||
http.Error(w, "conversion to requested format not supported", http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write file.
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if r.Method != http.MethodHead {
|
||||
_, err = io.Copy(w, reader)
|
||||
if err != nil {
|
||||
log.Errorf("updates: failed to serve resource file: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// debugInfo returns the debugging information for support requests.
|
||||
func debugInfo(ar *api.Request) (data []byte, err error) {
|
||||
// Create debug information helper.
|
||||
@@ -192,7 +318,7 @@ func debugInfo(ar *api.Request) (data []byte, err error) {
|
||||
config.AddToDebugInfo(di)
|
||||
|
||||
// Detailed information.
|
||||
// TODO(vladimir): updates.AddToDebugInfo(di)
|
||||
AddVersionsToDebugInfo(di)
|
||||
compat.AddToDebugInfo(di)
|
||||
module.instance.AddWorkerInfoToDebugInfo(di)
|
||||
di.AddGoroutineStack()
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
package base
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/info"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
// Default Values (changeable for testing).
|
||||
var (
|
||||
DefaultAPIListenAddress = "127.0.0.1:817"
|
||||
|
||||
showVersion bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&showVersion, "version", false, "show version and exit")
|
||||
}
|
||||
|
||||
func prep(instance instance) error {
|
||||
// check if meta info is ok
|
||||
err := info.CheckVersion()
|
||||
if err != nil {
|
||||
return errors.New("compile error: please compile using the provided build script")
|
||||
}
|
||||
|
||||
// print version
|
||||
if showVersion {
|
||||
instance.SetCmdLineOperation(printVersion)
|
||||
return mgr.ErrExecuteCmdLineOp
|
||||
}
|
||||
|
||||
// set api listen address
|
||||
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func printVersion() error {
|
||||
fmt.Println(info.FullVersion())
|
||||
return nil
|
||||
}
|
||||
@@ -4,9 +4,13 @@ import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
// DefaultAPIListenAddress is the default listen address for the API.
|
||||
var DefaultAPIListenAddress = "127.0.0.1:817"
|
||||
|
||||
// Base is the base module.
|
||||
type Base struct {
|
||||
mgr *mgr.Manager
|
||||
@@ -47,9 +51,9 @@ func New(instance instance) (*Base, error) {
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
if err := prep(instance); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Set api listen address.
|
||||
api.SetDefaultAPIListenAddress(DefaultAPIListenAddress)
|
||||
|
||||
if err := registerDatabases(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/metrics"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
@@ -19,6 +21,11 @@ import (
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
var db = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
})
|
||||
|
||||
// Core is the core service module.
|
||||
type Core struct {
|
||||
m *mgr.Manager
|
||||
@@ -56,8 +63,10 @@ func init() {
|
||||
|
||||
func prep() error {
|
||||
// init config
|
||||
err := registerConfig()
|
||||
if err != nil {
|
||||
if err := registerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := registerUpdateConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -77,6 +86,10 @@ func start() error {
|
||||
return fmt.Errorf("failed to start plattform-specific components: %w", err)
|
||||
}
|
||||
|
||||
// Setup update system.
|
||||
initUpdateConfig()
|
||||
initVersionExport()
|
||||
|
||||
// Enable persistent metrics.
|
||||
if err := metrics.EnableMetricPersistence("core:metrics/storage"); err != nil {
|
||||
log.Warningf("core: failed to enable persisted metrics: %s", err)
|
||||
@@ -116,6 +129,7 @@ type instance interface {
|
||||
Shutdown()
|
||||
Restart()
|
||||
AddWorkerInfoToDebugInfo(di *debug.Info)
|
||||
Config() *config.Config
|
||||
BinaryUpdates() *updates.Updater
|
||||
IntelUpdates() *updates.Updater
|
||||
}
|
||||
|
||||
134
service/core/update_config.go
Normal file
134
service/core/update_config.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/service/configure"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
// Release Channel Configuration Keys.
|
||||
const (
|
||||
ReleaseChannelKey = "core/releaseChannel"
|
||||
ReleaseChannelJSONKey = "core.releaseChannel"
|
||||
)
|
||||
|
||||
// Release Channels.
|
||||
const (
|
||||
ReleaseChannelStable = "stable"
|
||||
ReleaseChannelBeta = "beta"
|
||||
ReleaseChannelStaging = "staging"
|
||||
ReleaseChannelSupport = "support"
|
||||
)
|
||||
|
||||
const (
|
||||
enableSoftwareUpdatesKey = "core/automaticUpdates"
|
||||
enableIntelUpdatesKey = "core/automaticIntelUpdates"
|
||||
)
|
||||
|
||||
var (
|
||||
releaseChannel config.StringOption
|
||||
enableSoftwareUpdates config.BoolOption
|
||||
enableIntelUpdates config.BoolOption
|
||||
|
||||
initialReleaseChannel string
|
||||
)
|
||||
|
||||
func registerUpdateConfig() error {
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Release Channel",
|
||||
Key: ReleaseChannelKey,
|
||||
Description: `Use "Stable" for the best experience. The "Beta" channel will have the newest features and fixes, but may also break and cause interruption. Use others only temporarily and when instructed.`,
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: ReleaseChannelStable,
|
||||
PossibleValues: []config.PossibleValue{
|
||||
{
|
||||
Name: "Stable",
|
||||
Description: "Production releases.",
|
||||
Value: ReleaseChannelStable,
|
||||
},
|
||||
{
|
||||
Name: "Beta",
|
||||
Description: "Production releases for testing new features that may break and cause interruption.",
|
||||
Value: ReleaseChannelBeta,
|
||||
},
|
||||
{
|
||||
Name: "Support",
|
||||
Description: "Support releases or version changes for troubleshooting. Only use temporarily and when instructed.",
|
||||
Value: ReleaseChannelSupport,
|
||||
},
|
||||
{
|
||||
Name: "Staging",
|
||||
Description: "Dangerous development releases for testing random things and experimenting. Only use temporarily and when instructed.",
|
||||
Value: ReleaseChannelStaging,
|
||||
},
|
||||
},
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: -4,
|
||||
config.DisplayHintAnnotation: config.DisplayHintOneOf,
|
||||
config.CategoryAnnotation: "Updates",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Automatic Software Updates",
|
||||
Key: enableSoftwareUpdatesKey,
|
||||
Description: "Automatically check for and download software updates. This does not include intelligence data updates.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
RequiresRestart: false,
|
||||
DefaultValue: true,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: -12,
|
||||
config.CategoryAnnotation: "Updates",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Automatic Intelligence Data Updates",
|
||||
Key: enableIntelUpdatesKey,
|
||||
Description: "Automatically check for and download intelligence data updates. This includes filter lists, geo-ip data, and more. Does not include software updates.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
RequiresRestart: false,
|
||||
DefaultValue: true,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: -11,
|
||||
config.CategoryAnnotation: "Updates",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initUpdateConfig() {
|
||||
releaseChannel = config.Concurrent.GetAsString(ReleaseChannelKey, ReleaseChannelStable)
|
||||
enableSoftwareUpdates = config.Concurrent.GetAsBool(enableSoftwareUpdatesKey, true)
|
||||
enableIntelUpdates = config.Concurrent.GetAsBool(enableIntelUpdatesKey, true)
|
||||
|
||||
initialReleaseChannel = releaseChannel()
|
||||
|
||||
module.instance.Config().EventConfigChange.AddCallback("configure updates", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
configureUpdates()
|
||||
return false, nil
|
||||
})
|
||||
configureUpdates()
|
||||
}
|
||||
|
||||
func configureUpdates() {
|
||||
module.instance.BinaryUpdates().Configure(enableSoftwareUpdates(), configure.GetBinaryUpdateURLs(releaseChannel()))
|
||||
module.instance.IntelUpdates().Configure(enableIntelUpdates(), configure.DefaultIntelIndexURLs)
|
||||
}
|
||||
176
service/core/update_versions.go
Normal file
176
service/core/update_versions.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/info"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
const (
|
||||
// versionsDBKey is the database key for update version information.
|
||||
versionsDBKey = "core:status/versions"
|
||||
|
||||
// versionsDBKey is the database key for simple update version information.
|
||||
simpleVersionsDBKey = "core:status/simple-versions"
|
||||
)
|
||||
|
||||
// Versions holds update versions and status information.
|
||||
type Versions struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
Core *info.Info
|
||||
Resources map[string]*updates.Artifact
|
||||
Channel string
|
||||
Beta bool
|
||||
Staging bool
|
||||
}
|
||||
|
||||
// SimpleVersions holds simplified update versions and status information.
|
||||
type SimpleVersions struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
|
||||
Build *info.Info
|
||||
Resources map[string]*SimplifiedResourceVersion
|
||||
Channel string
|
||||
}
|
||||
|
||||
// SimplifiedResourceVersion holds version information about one resource.
|
||||
type SimplifiedResourceVersion struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
// GetVersions returns the update versions and status information.
|
||||
// Resources must be locked when accessed.
|
||||
func GetVersions() *Versions {
|
||||
// Get all artifacts.
|
||||
resources := make(map[string]*updates.Artifact)
|
||||
if artifacts, err := module.instance.BinaryUpdates().GetFiles(); err == nil {
|
||||
for _, artifact := range artifacts {
|
||||
resources[artifact.Filename] = artifact
|
||||
}
|
||||
}
|
||||
if artifacts, err := module.instance.IntelUpdates().GetFiles(); err == nil {
|
||||
for _, artifact := range artifacts {
|
||||
resources[artifact.Filename] = artifact
|
||||
}
|
||||
}
|
||||
|
||||
return &Versions{
|
||||
Core: info.GetInfo(),
|
||||
Resources: resources,
|
||||
Channel: initialReleaseChannel,
|
||||
Beta: initialReleaseChannel == ReleaseChannelBeta,
|
||||
Staging: initialReleaseChannel == ReleaseChannelStaging,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSimpleVersions returns the simplified update versions and status information.
|
||||
func GetSimpleVersions() *SimpleVersions {
|
||||
// Get all artifacts, simply map.
|
||||
resources := make(map[string]*SimplifiedResourceVersion)
|
||||
if artifacts, err := module.instance.BinaryUpdates().GetFiles(); err == nil {
|
||||
for _, artifact := range artifacts {
|
||||
resources[artifact.Filename] = &SimplifiedResourceVersion{
|
||||
Version: artifact.Version,
|
||||
}
|
||||
}
|
||||
}
|
||||
if artifacts, err := module.instance.IntelUpdates().GetFiles(); err == nil {
|
||||
for _, artifact := range artifacts {
|
||||
resources[artifact.Filename] = &SimplifiedResourceVersion{
|
||||
Version: artifact.Version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fill base info.
|
||||
return &SimpleVersions{
|
||||
Build: info.GetInfo(),
|
||||
Resources: resources,
|
||||
Channel: initialReleaseChannel,
|
||||
}
|
||||
}
|
||||
|
||||
func initVersionExport() {
|
||||
module.instance.BinaryUpdates().EventResourcesUpdated.AddCallback("export version status", export)
|
||||
module.instance.IntelUpdates().EventResourcesUpdated.AddCallback("export version status", export)
|
||||
|
||||
_, _ = export(nil, struct{}{})
|
||||
}
|
||||
|
||||
func (v *Versions) save() error {
|
||||
if !v.KeyIsSet() {
|
||||
v.SetKey(versionsDBKey)
|
||||
}
|
||||
return db.Put(v)
|
||||
}
|
||||
|
||||
func (v *SimpleVersions) save() error {
|
||||
if !v.KeyIsSet() {
|
||||
v.SetKey(simpleVersionsDBKey)
|
||||
}
|
||||
return db.Put(v)
|
||||
}
|
||||
|
||||
// export is an event hook.
|
||||
func export(_ *mgr.WorkerCtx, _ struct{}) (cancel bool, err error) {
|
||||
// Export versions.
|
||||
if err := GetVersions().save(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := GetSimpleVersions().save(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AddVersionsToDebugInfo adds the update system status to the given debug.Info.
|
||||
func AddVersionsToDebugInfo(di *debug.Info) {
|
||||
overviewBuf := bytes.NewBuffer(nil)
|
||||
tableBuf := bytes.NewBuffer(nil)
|
||||
tabWriter := tabwriter.NewWriter(tableBuf, 8, 4, 3, ' ', 0)
|
||||
fmt.Fprint(tabWriter, "\nFile\tVersion\tIndex\tSHA256\n")
|
||||
|
||||
// Collect data for debug info.
|
||||
var cnt int
|
||||
if index, err := module.instance.BinaryUpdates().GetIndex(); err == nil {
|
||||
fmt.Fprintf(overviewBuf, "Binaries Index: v%s from %s\n", index.Version, index.Published)
|
||||
for _, artifact := range index.Artifacts {
|
||||
fmt.Fprintf(tabWriter, "\n%s\t%s\t%s\t%s", artifact.Filename, vStr(artifact.Version), "binaries", artifact.SHA256)
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
if index, err := module.instance.IntelUpdates().GetIndex(); err == nil {
|
||||
fmt.Fprintf(overviewBuf, "Intel Index: v%s from %s\n", index.Version, index.Published)
|
||||
for _, artifact := range index.Artifacts {
|
||||
fmt.Fprintf(tabWriter, "\n%s\t%s\t%s\t%s", artifact.Filename, vStr(artifact.Version), "intel", artifact.SHA256)
|
||||
cnt++
|
||||
}
|
||||
}
|
||||
_ = tabWriter.Flush()
|
||||
|
||||
// Add section.
|
||||
di.AddSection(
|
||||
fmt.Sprintf("Updates: %s (%d)", initialReleaseChannel, cnt),
|
||||
debug.UseCodeSection,
|
||||
overviewBuf.String(),
|
||||
tableBuf.String(),
|
||||
)
|
||||
}
|
||||
|
||||
func vStr(v string) string {
|
||||
if v != "" {
|
||||
return v
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
Reference in New Issue
Block a user