safing-portmaster/service/sync/setting_single.go
Daniel Hååvi 80664d1a27
Restructure modules (#1572)
* 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

268 lines
7.3 KiB
Go

package sync
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/safing/portmaster/base/api"
"github.com/safing/portmaster/base/config"
"github.com/safing/portmaster/service/profile"
"github.com/safing/structures/dsd"
)
// SingleSettingExport holds an export of a single setting.
type SingleSettingExport struct {
Type Type `json:"type" yaml:"type"` // Must be TypeSingleSetting
ID string `json:"id" yaml:"id"` // Settings Key
Value any `json:"value" yaml:"value"`
}
// SingleSettingImportRequest is a request to import a single setting.
type SingleSettingImportRequest struct {
ImportRequest `json:",inline"`
Export *SingleSettingExport `json:"export"`
}
func registerSingleSettingAPI() error {
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Export Single Setting",
Description: "Exports a single setting in a share-able format.",
Path: "sync/single-setting/export",
Read: api.PermitAdmin,
Write: api.PermitAdmin,
Parameters: []api.Parameter{{
Method: http.MethodGet,
Field: "from",
Description: "Specify where to export from.",
}, {
Method: http.MethodGet,
Field: "key",
Description: "Specify which settings key to export.",
}},
DataFunc: handleExportSingleSetting,
}); err != nil {
return err
}
if err := api.RegisterEndpoint(api.Endpoint{
Name: "Import Single Setting",
Description: "Imports a single setting from the share-able format.",
Path: "sync/single-setting/import",
Read: api.PermitAdmin,
Write: api.PermitAdmin,
Parameters: []api.Parameter{{
Method: http.MethodPost,
Field: "to",
Description: "Specify where to import to.",
}, {
Method: http.MethodPost,
Field: "key",
Description: "Specify which setting key to import.",
}, {
Method: http.MethodPost,
Field: "validate",
Description: "Validate only.",
}},
StructFunc: handleImportSingleSetting,
}); err != nil {
return err
}
return nil
}
func handleExportSingleSetting(ar *api.Request) (data []byte, err error) {
var request *ExportRequest
// Get parameters.
q := ar.URL.Query()
if len(q) > 0 {
request = &ExportRequest{
From: q.Get("from"),
Keys: q["key"], // Get []string by direct map access.
}
} else {
request = &ExportRequest{}
if err := json.Unmarshal(ar.InputData, request); err != nil {
return nil, fmt.Errorf("%w: failed to parse export request: %w", ErrExportFailed, err)
}
}
// Check parameters.
if request.From == "" || len(request.Keys) != 1 {
return nil, errors.New("missing or malformed parameters")
}
// Export.
export, err := ExportSingleSetting(request.Keys[0], request.From)
if err != nil {
return nil, err
}
return serializeExport(export, ar)
}
func handleImportSingleSetting(ar *api.Request) (any, error) {
var request *SingleSettingImportRequest
// Get parameters.
q := ar.URL.Query()
if len(q) > 0 {
request = &SingleSettingImportRequest{
ImportRequest: ImportRequest{
Target: q.Get("to"),
ValidateOnly: q.Has("validate"),
RawExport: string(ar.InputData),
RawMime: ar.Header.Get("Content-Type"),
},
}
} else {
request = &SingleSettingImportRequest{}
if _, err := dsd.MimeLoad(ar.InputData, ar.Header.Get("Accept"), request); err != nil {
return nil, fmt.Errorf("%w: failed to parse import request: %w", ErrInvalidImportRequest, err)
}
}
// Check if we need to parse the export.
switch {
case request.Export != nil && request.RawExport != "":
return nil, fmt.Errorf("%w: both Export and RawExport are defined", ErrInvalidImportRequest)
case request.RawExport != "":
// Parse export.
export := &SingleSettingExport{}
if err := parseExport(&request.ImportRequest, export); err != nil {
return nil, err
}
request.Export = export
case request.Export != nil:
// Export is aleady parsed.
default:
return nil, ErrInvalidImportRequest
}
// Optional check if the setting key matches.
if len(q) > 0 && q.Has("key") && q.Get("key") != request.Export.ID {
return nil, ErrMismatch
}
// Import.
return ImportSingeSetting(request)
}
// ExportSingleSetting export a single setting.
func ExportSingleSetting(key, from string) (*SingleSettingExport, error) {
option, err := config.GetOption(key)
if err != nil {
return nil, fmt.Errorf("%w: configuration %w", ErrSettingNotFound, err)
}
var value any
if from == ExportTargetGlobal {
value = option.UserValue()
if value == nil {
return nil, ErrUnchanged
}
} else {
// Check if the setting is settable per app.
if !option.AnnotationEquals(config.SettablePerAppAnnotation, true) {
return nil, ErrNotSettablePerApp
}
// Get and load profile.
r, err := db.Get(profile.ProfilesDBPath + from)
if err != nil {
return nil, fmt.Errorf("%w: failed to find profile: %w", ErrTargetNotFound, err)
}
p, err := profile.EnsureProfile(r)
if err != nil {
return nil, fmt.Errorf("%w: failed to load profile: %w", ErrExportFailed, err)
}
// Flatten config and get key we are looking for.
flattened := config.Flatten(p.Config)
value = flattened[key]
if value == nil {
return nil, ErrUnchanged
}
}
return &SingleSettingExport{
Type: TypeSingleSetting,
ID: key,
Value: value,
}, nil
}
// ImportSingeSetting imports a single setting.
func ImportSingeSetting(r *SingleSettingImportRequest) (*ImportResult, error) {
// Check import.
if r.Export.Type != TypeSingleSetting {
return nil, ErrMismatch
}
// Get option and validate value.
option, err := config.GetOption(r.Export.ID)
if err != nil {
return nil, fmt.Errorf("%w: configuration %w", ErrSettingNotFound, err)
}
if err := option.ValidateValue(r.Export.Value); err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidSettingValue, err)
}
// Import single global setting.
if r.Target == ExportTargetGlobal {
// Stop here if we are only validating.
if r.ValidateOnly {
return &ImportResult{
RestartRequired: option.RequiresRestart,
ReplacesExisting: option.IsSetByUser(),
}, nil
}
// Actually import the setting.
if err := config.SetConfigOption(r.Export.ID, r.Export.Value); err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidSettingValue, err)
}
} else {
// Check if the setting is settable per app.
if !option.AnnotationEquals(config.SettablePerAppAnnotation, true) {
return nil, ErrNotSettablePerApp
}
// Import single setting into profile.
rec, err := db.Get(profile.ProfilesDBPath + r.Target)
if err != nil {
return nil, fmt.Errorf("%w: failed to find profile: %w", ErrTargetNotFound, err)
}
p, err := profile.EnsureProfile(rec)
if err != nil {
return nil, fmt.Errorf("%w: failed to load profile: %w", ErrImportFailed, err)
}
// Stop here if we are only validating.
if r.ValidateOnly {
return &ImportResult{
RestartRequired: option.RequiresRestart,
ReplacesExisting: option.IsSetByUser(),
}, nil
}
// Set imported setting on profile.
config.PutValueIntoHierarchicalConfig(p.Config, r.Export.ID, r.Export.Value)
// Mark profile as edited by user.
p.LastEdited = time.Now().Unix()
// Save profile back to db.
if err := p.Save(); err != nil {
return nil, fmt.Errorf("%w: failed to save profile: %w", ErrImportFailed, err)
}
}
return &ImportResult{
RestartRequired: option.RequiresRestart,
ReplacesExisting: option.IsSetByUser(),
}, nil
}