mirror of
https://github.com/safing/portmaster
synced 2025-09-01 18:19:12 +00:00
266 lines
7.2 KiB
Go
266 lines
7.2 KiB
Go
package sync
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/safing/portbase/api"
|
|
"github.com/safing/portbase/config"
|
|
"github.com/safing/portbase/formats/dsd"
|
|
"github.com/safing/portmaster/profile"
|
|
)
|
|
|
|
// SingleSettingExport holds an export of a single setting.
|
|
type SingleSettingExport struct {
|
|
Type Type `json:"type"` // Must be TypeSingleSetting
|
|
ID string `json:"id"` // Settings Key
|
|
|
|
Value any `json:"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.",
|
|
}},
|
|
BelongsTo: module,
|
|
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.",
|
|
}},
|
|
BelongsTo: module,
|
|
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"),
|
|
Key: q.Get("key"),
|
|
}
|
|
} 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 == "" || request.Key == "" {
|
|
return nil, errors.New("missing parameters")
|
|
}
|
|
|
|
// Export.
|
|
export, err := ExportSingleSetting(request.Key, 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)
|
|
|
|
// 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
|
|
}
|