mirror of
https://github.com/safing/portmaster
synced 2025-04-08 13:09:11 +00:00
* Move portbase into monorepo * Add new simple module mgr * [WIP] Switch to new simple module mgr * Add StateMgr and more worker variants * [WIP] Switch more modules * [WIP] Switch more modules * [WIP] swtich more modules * [WIP] switch all SPN modules * [WIP] switch all service modules * [WIP] Convert all workers to the new module system * [WIP] add new task system to module manager * [WIP] Add second take for scheduling workers * [WIP] Add FIXME for bugs in new scheduler * [WIP] Add minor improvements to scheduler * [WIP] Add new worker scheduler * [WIP] Fix more bug related to new module system * [WIP] Fix start handing of the new module system * [WIP] Improve startup process * [WIP] Fix minor issues * [WIP] Fix missing subsystem in settings * [WIP] Initialize managers in constructor * [WIP] Move module event initialization to constrictors * [WIP] Fix setting for enabling and disabling the SPN module * [WIP] Move API registeration into module construction * [WIP] Update states mgr for all modules * [WIP] Add CmdLine operation support * Add state helper methods to module group and instance * Add notification and module status handling to status package * Fix starting issues * Remove pilot widget and update security lock to new status data * Remove debug logs * Improve http server shutdown * Add workaround for cleanly shutting down firewall+netquery * Improve logging * Add syncing states with notifications for new module system * Improve starting, stopping, shutdown; resolve FIXMEs/TODOs * [WIP] Fix most unit tests * Review new module system and fix minor issues * Push shutdown and restart events again via API * Set sleep mode via interface * Update example/template module * [WIP] Fix spn/cabin unit test * Remove deprecated UI elements * Make log output more similar for the logging transition phase * Switch spn hub and observer cmds to new module system * Fix log sources * Make worker mgr less error prone * Fix tests and minor issues * Fix observation hub * Improve shutdown and restart handling * Split up big connection.go source file * Move varint and dsd packages to structures repo * Improve expansion test * Fix linter warnings * Fix interception module on windows * Fix linter errors --------- Co-authored-by: Vladimir Stoilov <vladimir@safing.io>
418 lines
14 KiB
Go
418 lines
14 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"regexp"
|
|
"sync"
|
|
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/tidwall/sjson"
|
|
|
|
"github.com/safing/portmaster/base/database/record"
|
|
"github.com/safing/structures/dsd"
|
|
)
|
|
|
|
// OptionType defines the value type of an option.
|
|
type OptionType uint8
|
|
|
|
// Various attribute options. Use ExternalOptType for extended types in the frontend.
|
|
const (
|
|
optTypeAny OptionType = 0
|
|
OptTypeString OptionType = 1
|
|
OptTypeStringArray OptionType = 2
|
|
OptTypeInt OptionType = 3
|
|
OptTypeBool OptionType = 4
|
|
)
|
|
|
|
func getTypeName(t OptionType) string {
|
|
switch t {
|
|
case optTypeAny:
|
|
return "any"
|
|
case OptTypeString:
|
|
return "string"
|
|
case OptTypeStringArray:
|
|
return "[]string"
|
|
case OptTypeInt:
|
|
return "int"
|
|
case OptTypeBool:
|
|
return "bool"
|
|
default:
|
|
return "unknown"
|
|
}
|
|
}
|
|
|
|
// PossibleValue defines a value that is possible for
|
|
// a configuration setting.
|
|
type PossibleValue struct {
|
|
// Name is a human readable name of the option.
|
|
Name string
|
|
// Description is a human readable description of
|
|
// this value.
|
|
Description string
|
|
// Value is the actual value of the option. The type
|
|
// must match the option's value type.
|
|
Value interface{}
|
|
}
|
|
|
|
// Annotations can be attached to configuration options to
|
|
// provide hints for user interfaces or other systems working
|
|
// or setting configuration options.
|
|
// Annotation keys should follow the below format to ensure
|
|
// future well-known annotation additions do not conflict
|
|
// with vendor/product/package specific annoations.
|
|
//
|
|
// Format: <vendor/package>:<scope>:<identifier> //.
|
|
type Annotations map[string]interface{}
|
|
|
|
// MigrationFunc is a function that migrates a config option value.
|
|
type MigrationFunc func(option *Option, value any) any
|
|
|
|
// Well known annotations defined by this package.
|
|
const (
|
|
// DisplayHintAnnotation provides a hint for the user
|
|
// interface on how to render an option.
|
|
// The value of DisplayHintAnnotation is expected to
|
|
// be a string. See DisplayHintXXXX constants below
|
|
// for a list of well-known display hint annotations.
|
|
DisplayHintAnnotation = "safing/portbase:ui:display-hint"
|
|
// DisplayOrderAnnotation provides a hint for the user
|
|
// interface in which order settings should be displayed.
|
|
// The value of DisplayOrderAnnotations is expected to be
|
|
// an number (int).
|
|
DisplayOrderAnnotation = "safing/portbase:ui:order"
|
|
// UnitAnnotations defines the SI unit of an option (if any).
|
|
UnitAnnotation = "safing/portbase:ui:unit"
|
|
// CategoryAnnotations can provide an additional category
|
|
// to each settings. This category can be used by a user
|
|
// interface to group certain options together.
|
|
// User interfaces should treat a CategoryAnnotation, if
|
|
// supported, with higher priority as a DisplayOrderAnnotation.
|
|
CategoryAnnotation = "safing/portbase:ui:category"
|
|
// SubsystemAnnotation can be used to mark an option as part
|
|
// of a module subsystem.
|
|
SubsystemAnnotation = "safing/portbase:module:subsystem"
|
|
// StackableAnnotation can be set on configuration options that
|
|
// stack on top of the default (or otherwise related) options.
|
|
// The value of StackableAnnotaiton is expected to be a boolean but
|
|
// may be extended to hold references to other options in the
|
|
// future.
|
|
StackableAnnotation = "safing/portbase:options:stackable"
|
|
// RestartPendingAnnotation is automatically set on a configuration option
|
|
// that requires a restart and has been changed.
|
|
// The value must always be a boolean with value "true".
|
|
RestartPendingAnnotation = "safing/portbase:options:restart-pending"
|
|
// QuickSettingAnnotation can be used to add quick settings to
|
|
// a configuration option. A quick setting can support the user
|
|
// by switching between pre-configured values.
|
|
// The type of a quick-setting annotation is []QuickSetting or QuickSetting.
|
|
QuickSettingsAnnotation = "safing/portbase:ui:quick-setting"
|
|
// RequiresAnnotation can be used to mark another option as a
|
|
// requirement. The type of RequiresAnnotation is []ValueRequirement
|
|
// or ValueRequirement.
|
|
RequiresAnnotation = "safing/portbase:config:requires"
|
|
// RequiresFeatureIDAnnotation can be used to mark a setting as only available
|
|
// when the user has a certain feature ID in the subscription plan.
|
|
// The type is []string or string.
|
|
RequiresFeatureIDAnnotation = "safing/portmaster:ui:config:requires-feature"
|
|
// SettablePerAppAnnotation can be used to mark a setting as settable per-app and
|
|
// is a boolean.
|
|
SettablePerAppAnnotation = "safing/portmaster:settable-per-app"
|
|
// RequiresUIReloadAnnotation can be used to inform the UI that changing the value
|
|
// of the annotated setting requires a full reload of the user interface.
|
|
// The value of this annotation does not matter as the sole presence of
|
|
// the annotation key is enough. Though, users are advised to set the value
|
|
// of this annotation to true.
|
|
RequiresUIReloadAnnotation = "safing/portmaster:ui:requires-reload"
|
|
)
|
|
|
|
// QuickSettingsAction defines the action of a quick setting.
|
|
type QuickSettingsAction string
|
|
|
|
const (
|
|
// QuickReplace replaces the current setting with the one from
|
|
// the quick setting.
|
|
QuickReplace = QuickSettingsAction("replace")
|
|
// QuickMergeTop merges the value of the quick setting with the
|
|
// already configured one adding new values on the top. Merging
|
|
// is only supported for OptTypeStringArray.
|
|
QuickMergeTop = QuickSettingsAction("merge-top")
|
|
// QuickMergeBottom merges the value of the quick setting with the
|
|
// already configured one adding new values at the bottom. Merging
|
|
// is only supported for OptTypeStringArray.
|
|
QuickMergeBottom = QuickSettingsAction("merge-bottom")
|
|
)
|
|
|
|
// QuickSetting defines a quick setting for a configuration option and
|
|
// should be used together with the QuickSettingsAnnotation.
|
|
type QuickSetting struct {
|
|
// Name is the name of the quick setting.
|
|
Name string
|
|
|
|
// Value is the value that the quick-setting configures. It must match
|
|
// the expected value type of the annotated option.
|
|
Value interface{}
|
|
|
|
// Action defines the action of the quick setting.
|
|
Action QuickSettingsAction
|
|
}
|
|
|
|
// ValueRequirement defines a requirement on another configuration option.
|
|
type ValueRequirement struct {
|
|
// Key is the key of the configuration option that is required.
|
|
Key string
|
|
|
|
// Value that is required.
|
|
Value interface{}
|
|
}
|
|
|
|
// Values for the DisplayHintAnnotation.
|
|
const (
|
|
// DisplayHintOneOf is used to mark an option
|
|
// as a "select"-style option. That is, only one of
|
|
// the supported values may be set. This option makes
|
|
// only sense together with the PossibleValues property
|
|
// of Option.
|
|
DisplayHintOneOf = "one-of"
|
|
// DisplayHintOrdered is used to mark a list option as ordered.
|
|
// That is, the order of items is important and a user interface
|
|
// is encouraged to provide the user with re-ordering support
|
|
// (like drag'n'drop).
|
|
DisplayHintOrdered = "ordered"
|
|
// DisplayHintFilePicker is used to mark the option as being a file, which
|
|
// should give the option to use a file picker to select a local file from disk.
|
|
DisplayHintFilePicker = "file-picker"
|
|
)
|
|
|
|
// Option describes a configuration option.
|
|
type Option struct {
|
|
sync.Mutex
|
|
// Name holds the name of the configuration options.
|
|
// It should be human readable and is mainly used for
|
|
// presentation purposes.
|
|
// Name is considered immutable after the option has
|
|
// been created.
|
|
Name string
|
|
// Key holds the database path for the option. It should
|
|
// follow the path format `category/sub/key`.
|
|
// Key is considered immutable after the option has
|
|
// been created.
|
|
Key string
|
|
// Description holds a human readable description of the
|
|
// option and what is does. The description should be short.
|
|
// Use the Help property for a longer support text.
|
|
// Description is considered immutable after the option has
|
|
// been created.
|
|
Description string
|
|
// Help may hold a long version of the description providing
|
|
// assistance with the configuration option.
|
|
// Help is considered immutable after the option has
|
|
// been created.
|
|
Help string
|
|
// Sensitive signifies that the configuration values may contain sensitive
|
|
// content, such as authentication keys.
|
|
Sensitive bool
|
|
// OptType defines the type of the option.
|
|
// OptType is considered immutable after the option has
|
|
// been created.
|
|
OptType OptionType
|
|
// ExpertiseLevel can be used to set the required expertise
|
|
// level for the option to be displayed to a user.
|
|
// ExpertiseLevel is considered immutable after the option has
|
|
// been created.
|
|
ExpertiseLevel ExpertiseLevel
|
|
// ReleaseLevel is used to mark the stability of the option.
|
|
// ReleaseLevel is considered immutable after the option has
|
|
// been created.
|
|
ReleaseLevel ReleaseLevel
|
|
// RequiresRestart should be set to true if a modification of
|
|
// the options value requires a restart of the whole application
|
|
// to take effect.
|
|
// RequiresRestart is considered immutable after the option has
|
|
// been created.
|
|
RequiresRestart bool
|
|
// DefaultValue holds the default value of the option. Note that
|
|
// this value can be overwritten during runtime (see activeDefaultValue
|
|
// and activeFallbackValue).
|
|
// DefaultValue is considered immutable after the option has
|
|
// been created.
|
|
DefaultValue interface{}
|
|
// ValidationRegex may contain a regular expression used to validate
|
|
// the value of option. If the option type is set to OptTypeStringArray
|
|
// the validation regex is applied to all entries of the string slice.
|
|
// Note that it is recommended to keep the validation regex simple so
|
|
// it can also be used in other languages (mainly JavaScript) to provide
|
|
// a better user-experience by pre-validating the expression.
|
|
// ValidationRegex is considered immutable after the option has
|
|
// been created.
|
|
ValidationRegex string
|
|
// ValidationFunc may contain a function to validate more complex values.
|
|
// The error is returned beyond the scope of this package and may be
|
|
// displayed to a user.
|
|
ValidationFunc func(value interface{}) error `json:"-"`
|
|
// PossibleValues may be set to a slice of values that are allowed
|
|
// for this configuration setting. Note that PossibleValues makes most
|
|
// sense when ExternalOptType is set to HintOneOf
|
|
// PossibleValues is considered immutable after the option has
|
|
// been created.
|
|
PossibleValues []PossibleValue `json:",omitempty"`
|
|
// Annotations adds additional annotations to the configuration options.
|
|
// See documentation of Annotations for more information.
|
|
// Annotations is considered mutable and setting/reading annotation keys
|
|
// must be performed while the option is locked.
|
|
Annotations Annotations
|
|
// Migrations holds migration functions that are given the raw option value
|
|
// before any validation is run. The returned value is then used.
|
|
Migrations []MigrationFunc `json:"-"`
|
|
|
|
activeValue *valueCache // runtime value (loaded from config file or set by user)
|
|
activeDefaultValue *valueCache // runtime default value (may be set internally)
|
|
activeFallbackValue *valueCache // default value from option registration
|
|
compiledRegex *regexp.Regexp
|
|
}
|
|
|
|
// AddAnnotation adds the annotation key to option if it's not already set.
|
|
func (option *Option) AddAnnotation(key string, value interface{}) {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
if option.Annotations == nil {
|
|
option.Annotations = make(Annotations)
|
|
}
|
|
|
|
if _, ok := option.Annotations[key]; ok {
|
|
return
|
|
}
|
|
option.Annotations[key] = value
|
|
}
|
|
|
|
// SetAnnotation sets the value of the annotation key overwritting an
|
|
// existing value if required.
|
|
func (option *Option) SetAnnotation(key string, value interface{}) {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
option.setAnnotation(key, value)
|
|
}
|
|
|
|
// setAnnotation sets the value of the annotation key overwritting an
|
|
// existing value if required. Does not lock the Option.
|
|
func (option *Option) setAnnotation(key string, value interface{}) {
|
|
if option.Annotations == nil {
|
|
option.Annotations = make(Annotations)
|
|
}
|
|
option.Annotations[key] = value
|
|
}
|
|
|
|
// GetAnnotation returns the value of the annotation key.
|
|
func (option *Option) GetAnnotation(key string) (interface{}, bool) {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
if option.Annotations == nil {
|
|
return nil, false
|
|
}
|
|
val, ok := option.Annotations[key]
|
|
return val, ok
|
|
}
|
|
|
|
// AnnotationEquals returns whether the annotation of the given key matches the
|
|
// given value.
|
|
func (option *Option) AnnotationEquals(key string, value any) bool {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
if option.Annotations == nil {
|
|
return false
|
|
}
|
|
setValue, ok := option.Annotations[key]
|
|
if !ok {
|
|
return false
|
|
}
|
|
return reflect.DeepEqual(value, setValue)
|
|
}
|
|
|
|
// copyOrNil returns a copy of the option, or nil if copying failed.
|
|
func (option *Option) copyOrNil() *Option {
|
|
copied, err := copystructure.Copy(option)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return copied.(*Option) //nolint:forcetypeassert
|
|
}
|
|
|
|
// IsSetByUser returns whether the option has been set by the user.
|
|
func (option *Option) IsSetByUser() bool {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
return option.activeValue != nil
|
|
}
|
|
|
|
// UserValue returns the value set by the user or nil if the value has not
|
|
// been changed from the default.
|
|
func (option *Option) UserValue() any {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
if option.activeValue == nil {
|
|
return nil
|
|
}
|
|
return option.activeValue.getData(option)
|
|
}
|
|
|
|
// ValidateValue checks if the given value is valid for the option.
|
|
func (option *Option) ValidateValue(value any) error {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
value = migrateValue(option, value)
|
|
if _, err := validateValue(option, value); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Export expors an option to a Record.
|
|
func (option *Option) Export() (record.Record, error) {
|
|
option.Lock()
|
|
defer option.Unlock()
|
|
|
|
return option.export()
|
|
}
|
|
|
|
func (option *Option) export() (record.Record, error) {
|
|
data, err := json.Marshal(option)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if option.activeValue != nil {
|
|
data, err = sjson.SetBytes(data, "Value", option.activeValue.getData(option))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if option.activeDefaultValue != nil {
|
|
data, err = sjson.SetBytes(data, "DefaultValue", option.activeDefaultValue.getData(option))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
r, err := record.NewWrapper(fmt.Sprintf("config:%s", option.Key), nil, dsd.JSON, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.SetMeta(&record.Meta{})
|
|
|
|
return r, nil
|
|
}
|
|
|
|
type sortByKey []*Option
|
|
|
|
func (opts sortByKey) Len() int { return len(opts) }
|
|
func (opts sortByKey) Less(i, j int) bool { return opts[i].Key < opts[j].Key }
|
|
func (opts sortByKey) Swap(i, j int) { opts[i], opts[j] = opts[j], opts[i] }
|