safing-portmaster/base/config/option.go
Daniel Hååvi 80664d1a27
Restructure modules ()
* 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>
2024-08-09 18:15:48 +03:00

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] }