safing-portmaster/base/config/persistence.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

234 lines
5.8 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"path"
"strings"
"sync"
"github.com/safing/portmaster/base/log"
)
var (
configFilePath string
loadedConfigValidationErrors []*ValidationError
loadedConfigValidationErrorsLock sync.Mutex
)
// GetLoadedConfigValidationErrors returns the encountered validation errors
// from the last time loading config from disk.
func GetLoadedConfigValidationErrors() []*ValidationError {
loadedConfigValidationErrorsLock.Lock()
defer loadedConfigValidationErrorsLock.Unlock()
return loadedConfigValidationErrors
}
func loadConfig(requireValidConfig bool) error {
// check if persistence is configured
if configFilePath == "" {
return nil
}
// read config file
data, err := os.ReadFile(configFilePath)
if err != nil {
return err
}
// convert to map
newValues, err := JSONToMap(data)
if err != nil {
return err
}
validationErrors, _ := ReplaceConfig(newValues)
if requireValidConfig && len(validationErrors) > 0 {
return fmt.Errorf("encountered %d validation errors during config loading", len(validationErrors))
}
// Save validation errors.
loadedConfigValidationErrorsLock.Lock()
defer loadedConfigValidationErrorsLock.Unlock()
loadedConfigValidationErrors = validationErrors
return nil
}
// SaveConfig saves the current configuration to file.
// It will acquire a read-lock on the global options registry
// lock and must lock each option!
func SaveConfig() error {
optionsLock.RLock()
defer optionsLock.RUnlock()
// check if persistence is configured
if configFilePath == "" {
return nil
}
// extract values
activeValues := make(map[string]interface{})
for key, option := range options {
// we cannot immedately unlock the option afger
// getData() because someone could lock and change it
// while we are marshaling the value (i.e. for string slices).
// We NEED to keep the option locks until we finsihed.
option.Lock()
defer option.Unlock()
if option.activeValue != nil {
activeValues[key] = option.activeValue.getData(option)
}
}
// convert to JSON
data, err := MapToJSON(activeValues)
if err != nil {
log.Errorf("config: failed to save config: %s", err)
return err
}
// write file
return os.WriteFile(configFilePath, data, 0o0600)
}
// JSONToMap parses and flattens a hierarchical json object.
func JSONToMap(jsonData []byte) (map[string]interface{}, error) {
loaded := make(map[string]interface{})
err := json.Unmarshal(jsonData, &loaded)
if err != nil {
return nil, err
}
return Flatten(loaded), nil
}
// Flatten returns a flattened copy of the given hierarchical config.
func Flatten(config map[string]interface{}) (flattenedConfig map[string]interface{}) {
flattenedConfig = make(map[string]interface{})
flattenMap(flattenedConfig, config, "")
return flattenedConfig
}
func flattenMap(rootMap, subMap map[string]interface{}, subKey string) {
for key, entry := range subMap {
// get next level key
subbedKey := path.Join(subKey, key)
// check for next subMap
nextSub, ok := entry.(map[string]interface{})
if ok {
flattenMap(rootMap, nextSub, subbedKey)
} else {
// only set if not on root level
rootMap[subbedKey] = entry
}
}
}
// MapToJSON expands a flattened map and returns it as json.
func MapToJSON(config map[string]interface{}) ([]byte, error) {
return json.MarshalIndent(Expand(config), "", " ")
}
// Expand returns a hierarchical copy of the given flattened config.
func Expand(flattenedConfig map[string]interface{}) (config map[string]interface{}) {
config = make(map[string]interface{})
for key, entry := range flattenedConfig {
PutValueIntoHierarchicalConfig(config, key, entry)
}
return config
}
// PutValueIntoHierarchicalConfig injects a configuration entry into an hierarchical config map. Conflicting entries will be replaced.
func PutValueIntoHierarchicalConfig(config map[string]interface{}, key string, value interface{}) {
parts := strings.Split(key, "/")
// create/check maps for all parts except the last one
subMap := config
for i, part := range parts {
if i == len(parts)-1 {
// do not process the last part,
// which is not a map, but the value key itself
break
}
var nextSubMap map[string]interface{}
// get value
value, ok := subMap[part]
if !ok {
// create new map and assign it
nextSubMap = make(map[string]interface{})
subMap[part] = nextSubMap
} else {
nextSubMap, ok = value.(map[string]interface{})
if !ok {
// create new map and assign it
nextSubMap = make(map[string]interface{})
subMap[part] = nextSubMap
}
}
// assign for next parts loop
subMap = nextSubMap
}
// assign value to last submap
subMap[parts[len(parts)-1]] = value
}
// CleanFlattenedConfig removes all inexistent configuration options from the given flattened config map.
func CleanFlattenedConfig(flattenedConfig map[string]interface{}) {
optionsLock.RLock()
defer optionsLock.RUnlock()
for key := range flattenedConfig {
_, ok := options[key]
if !ok {
delete(flattenedConfig, key)
}
}
}
// CleanHierarchicalConfig removes all inexistent configuration options from the given hierarchical config map.
func CleanHierarchicalConfig(config map[string]interface{}) {
optionsLock.RLock()
defer optionsLock.RUnlock()
cleanSubMap(config, "")
}
func cleanSubMap(subMap map[string]interface{}, subKey string) (empty bool) {
var foundValid int
for key, value := range subMap {
value, ok := value.(map[string]interface{})
if ok {
// we found another section
isEmpty := cleanSubMap(value, path.Join(subKey, key))
if isEmpty {
delete(subMap, key)
} else {
foundValid++
}
continue
}
// we found an option value
if strings.Contains(key, "/") {
delete(subMap, key)
} else {
_, ok := options[path.Join(subKey, key)]
if ok {
foundValid++
} else {
delete(subMap, key)
}
}
}
return foundValid == 0
}