Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add export and import for profiles #1363

Merged
merged 5 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ require (
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.0 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,8 @@ github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/vmihailenco/msgpack/v5 v5.4.0 h1:hRM0digJwyR6vll33NNAwCFguy5JuBD6jxDmQP3l608=
github.com/vmihailenco/msgpack/v5 v5.4.0/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
Expand Down
97 changes: 97 additions & 0 deletions profile/api.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package profile

import (
"errors"
"fmt"
"net/http"
"path/filepath"
"strings"

"github.com/safing/portbase/api"
"github.com/safing/portbase/formats/dsd"
"github.com/safing/portbase/utils"
)

func registerAPIEndpoints() error {
Expand All @@ -19,6 +24,28 @@ func registerAPIEndpoints() error {
return err
}

if err := api.RegisterEndpoint(api.Endpoint{
Name: "Get Profile Icon",
Description: "Returns the requested profile icon.",
Path: "profile/icon/{id:[a-f0-9]*\\.[a-z]{3,4}}",
Read: api.PermitUser,
BelongsTo: module,
DataFunc: handleGetProfileIcon,
}); err != nil {
return err
}

if err := api.RegisterEndpoint(api.Endpoint{
Name: "Update Profile Icon",
Description: "Updates a profile icon.",
Path: "profile/icon",
Write: api.PermitUser,
BelongsTo: module,
StructFunc: handleUpdateProfileIcon,
}); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -64,3 +91,73 @@ func handleMergeProfiles(ar *api.Request) (i interface{}, err error) {
New: newProfile.ScopedID(),
}, nil
}

func handleGetProfileIcon(ar *api.Request) (data []byte, err error) {
name := ar.URLVars["id"]

ext := filepath.Ext(name)

// Get profile icon.
data, err = GetProfileIcon(name)
if err != nil {
return nil, err
}

// Set content type for icon.
contentType, ok := utils.MimeTypeByExtension(ext)
if ok {
ar.ResponseHeader.Set("Content-Type", contentType)
}

return data, nil
}

type updateProfileIconResponse struct {
Filename string `json:"filename"`
}

//nolint:goconst
func handleUpdateProfileIcon(ar *api.Request) (any, error) {
// Check input.
if len(ar.InputData) == 0 {
return nil, api.ErrorWithStatus(errors.New("no content"), http.StatusBadRequest)
}
mimeType := ar.Header.Get("Content-Type")
if mimeType == "" {
return nil, api.ErrorWithStatus(errors.New("no content type"), http.StatusBadRequest)
}

// Derive image format from content type.
mimeType = strings.TrimSpace(mimeType)
mimeType = strings.ToLower(mimeType)
mimeType, _, _ = strings.Cut(mimeType, ";")
var ext string
switch mimeType {
case "image/gif":
ext = "gif"
case "image/jpeg":
ext = "jpg"
case "image/jpg":
ext = "jpg"
case "image/png":
ext = "png"
case "image/svg+xml":
ext = "svg"
case "image/tiff":
ext = "tiff"
case "image/webp":
ext = "webp"
default:
return "", api.ErrorWithStatus(errors.New("unsupported image format"), http.StatusBadRequest)
}

// Update profile icon.
filename, err := UpdateProfileIcon(ar.InputData, ext)
if err != nil {
return nil, err
}

return &updateProfileIconResponse{
Filename: filename,
}, nil
}
2 changes: 1 addition & 1 deletion profile/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ Current Features:
- Disable Firefox' internal DNS-over-HTTPs resolver
- Block direct access to public DNS resolvers

Please note that DNS bypass attempts might be additionally blocked in the Sytem D there too.`,
Please note that DNS bypass attempts might be additionally blocked in the System DNS Client App.`,
OptType: config.OptTypeInt,
ExpertiseLevel: config.ExpertiseLevelUser,
ReleaseLevel: config.ReleaseLevelStable,
Expand Down
6 changes: 4 additions & 2 deletions profile/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ var profileDB = database.NewInterface(&database.Options{
Internal: true,
})

func makeScopedID(source profileSource, id string) string {
// MakeScopedID returns a scoped profile ID.
func MakeScopedID(source ProfileSource, id string) string {
return string(source) + "/" + id
}

func makeProfileKey(source profileSource, id string) string {
// MakeProfileKey returns a profile key.
func MakeProfileKey(source ProfileSource, id string) string {
return ProfilesDBPath + string(source) + "/" + id
}

Expand Down
19 changes: 11 additions & 8 deletions profile/fingerprint.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ type (
// merged from. The merged profile should create a new profile ID derived
// from the new fingerprints and add all fingerprints with this field set
// to the originating profile ID
MergedFrom string
MergedFrom string // `json:"mergedFrom,omitempty"`
}

// Tag represents a simple key/value kind of tag used in process metadata
Expand Down Expand Up @@ -163,15 +163,17 @@ func (fp fingerprintRegex) Match(value string) (score int) {
return 0
}

type parsedFingerprints struct {
// ParsedFingerprints holds parsed fingerprints for fast usage.
type ParsedFingerprints struct {
tagPrints []matchingFingerprint
envPrints []matchingFingerprint
pathPrints []matchingFingerprint
cmdlinePrints []matchingFingerprint
}

func parseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *parsedFingerprints, firstErr error) {
parsed = &parsedFingerprints{}
// ParseFingerprints parses the fingerprints to make them ready for matching.
func ParseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *ParsedFingerprints, firstErr error) {
parsed = &ParsedFingerprints{}

// Add deprecated LinkedPath to fingerprints, if they are empty.
// TODO: Remove in v1.5
Expand Down Expand Up @@ -230,15 +232,15 @@ func parseFingerprints(raw []Fingerprint, deprecatedLinkedPath string) (parsed *

default:
if firstErr == nil {
firstErr = fmt.Errorf("unknown fingerprint operation: %q", entry.Type)
firstErr = fmt.Errorf("unknown fingerprint operation: %q", entry.Operation)
}
}
}

return parsed, firstErr
}

func (parsed *parsedFingerprints) addMatchingFingerprint(fp Fingerprint, matchingPrint matchingFingerprint) {
func (parsed *ParsedFingerprints) addMatchingFingerprint(fp Fingerprint, matchingPrint matchingFingerprint) {
switch fp.Type {
case FingerprintTypeTagID:
parsed.tagPrints = append(parsed.tagPrints, matchingPrint)
Expand All @@ -256,7 +258,7 @@ func (parsed *parsedFingerprints) addMatchingFingerprint(fp Fingerprint, matchin

// MatchFingerprints returns the highest matching score of the given
// fingerprints and matching data.
func MatchFingerprints(prints *parsedFingerprints, md MatchingData) (highestScore int) {
func MatchFingerprints(prints *ParsedFingerprints, md MatchingData) (highestScore int) {
// Check tags.
tags := md.Tags()
if len(tags) > 0 {
Expand Down Expand Up @@ -367,7 +369,8 @@ const (
deriveFPKeyIDForValue
)

func deriveProfileID(fps []Fingerprint) string {
// DeriveProfileID derives a profile ID from the given fingerprints.
func DeriveProfileID(fps []Fingerprint) string {
// Sort the fingerprints.
sortAndCompactFingerprints(fps)

Expand Down
2 changes: 1 addition & 1 deletion profile/fingerprint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestDeriveProfileID(t *testing.T) {
})

// Check if fingerprint matches.
id := deriveProfileID(fps)
id := DeriveProfileID(fps)
assert.Equal(t, "PTSRP7rdCnmvdjRoPMTrtjj7qk7PxR1a9YdBWUGwnZXJh2", id)
}
}
16 changes: 8 additions & 8 deletions profile/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *P
// Get active profile based on the ID, if available.
if id != "" {
// Check if there already is an active profile.
profile = getActiveProfile(makeScopedID(SourceLocal, id))
profile = getActiveProfile(MakeScopedID(SourceLocal, id))
if profile != nil {
// Mark active and return if not outdated.
if profile.outdated.IsNotSet() {
Expand All @@ -57,9 +57,9 @@ func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *P
return nil, errors.New("cannot get local profiles without ID and matching data")
}

profile, err = getProfile(makeScopedID(SourceLocal, id))
profile, err = getProfile(MakeScopedID(SourceLocal, id))
if err != nil {
return nil, fmt.Errorf("failed to load profile %s by ID: %w", makeScopedID(SourceLocal, id), err)
return nil, fmt.Errorf("failed to load profile %s by ID: %w", MakeScopedID(SourceLocal, id), err)
}
}

Expand All @@ -70,7 +70,7 @@ func GetLocalProfile(id string, md MatchingData, createProfileCallback func() *P

// Get special profile from DB.
if profile == nil {
profile, err = getProfile(makeScopedID(SourceLocal, id))
profile, err = getProfile(MakeScopedID(SourceLocal, id))
if err != nil && !errors.Is(err, database.ErrNotFound) {
log.Warningf("profile: failed to get special profile %s: %s", id, err)
}
Expand Down Expand Up @@ -188,12 +188,12 @@ func getProfile(scopedID string) (profile *Profile, err error) {

// findProfile searches for a profile with the given linked path. If it cannot
// find one, it will create a new profile for the given linked path.
func findProfile(source profileSource, md MatchingData) (profile *Profile, err error) {
func findProfile(source ProfileSource, md MatchingData) (profile *Profile, err error) {
// TODO: Loading every profile from database and parsing it for every new
// process might be quite expensive. Measure impact and possibly improve.

// Get iterator over all profiles.
it, err := profileDB.Query(query.New(ProfilesDBPath + makeScopedID(source, "")))
it, err := profileDB.Query(query.New(ProfilesDBPath + MakeScopedID(source, "")))
if err != nil {
return nil, fmt.Errorf("failed to query for profiles: %w", err)
}
Expand Down Expand Up @@ -257,15 +257,15 @@ profileFeed:
return profile, nil
}

func loadProfileFingerprints(r record.Record) (parsed *parsedFingerprints, err error) {
func loadProfileFingerprints(r record.Record) (parsed *ParsedFingerprints, err error) {
// Ensure it's a profile.
profile, err := EnsureProfile(r)
if err != nil {
return nil, err
}

// Parse and return fingerprints.
return parseFingerprints(profile.Fingerprints, profile.LinkedPath)
return ParseFingerprints(profile.Fingerprints, profile.LinkedPath)
}

func loadProfile(r record.Record) (*Profile, error) {
Expand Down
Loading
Loading