From 9f4e921609e65f4f2a08127249f013691075f7ba Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jun 2022 16:55:19 +0200 Subject: [PATCH 1/4] Simplify and improve update version export --- updates/export.go | 179 +++++++++++++++++++++++----------------------- updates/main.go | 12 +++- 2 files changed, 98 insertions(+), 93 deletions(-) diff --git a/updates/export.go b/updates/export.go index e00dee33..0c03b365 100644 --- a/updates/export.go +++ b/updates/export.go @@ -2,11 +2,8 @@ package updates import ( "context" - "errors" "sync" - "github.com/safing/portbase/database" - "github.com/safing/portbase/database/query" "github.com/safing/portbase/database/record" "github.com/safing/portbase/info" "github.com/safing/portbase/log" @@ -14,48 +11,95 @@ import ( "github.com/safing/portmaster/updates/helper" ) -// Database key for update information. const ( + // versionsDBKey is the database key for update version information. versionsDBKey = "core:status/versions" + + // versionsDBKey is the database key for simple update version information. + simpleVersionsDBKey = "core:status/simple-versions" ) -var ( - versionExport *versions - versionExportDB = database.NewInterface(&database.Options{ - Local: true, - Internal: true, - }) - versionExportHook *database.RegisteredHook -) - -// versions holds updates status information. -type versions struct { +// Versions holds update versions and status information. +type Versions struct { record.Base - lock sync.Mutex + sync.Mutex Core *info.Info Resources map[string]*updater.Resource Channel string Beta bool Staging bool +} - internalSave bool +// SimpleVersions holds simplified update versions and status information. +type SimpleVersions struct { + record.Base + sync.Mutex + + Build *info.Info + Resources map[string]*SimplifiedResourceVersion + Channel string +} + +// SimplifiedResourceVersion holds version information about one resource. +type SimplifiedResourceVersion struct { + Version string +} + +// GetVersions returns the update versions and status information. +// Resources must be locked when accessed. +func GetVersions() *Versions { + return &Versions{ + Core: info.GetInfo(), + Resources: registry.Export(), + Channel: initialReleaseChannel, + Beta: initialReleaseChannel == helper.ReleaseChannelBeta, + Staging: initialReleaseChannel == helper.ReleaseChannelStaging, + } +} + +// GetSimpleVersions returns the simplified update versions and status information. +func GetSimpleVersions() *SimpleVersions { + // Fill base info. + v := &SimpleVersions{ + Build: info.GetInfo(), + Resources: make(map[string]*SimplifiedResourceVersion), + Channel: initialReleaseChannel, + } + + // Iterate through all versions and add version info. + for id, resource := range registry.Export() { + func() { + resource.Lock() + defer resource.Unlock() + + // Get current in-used or selected version. + var rv *updater.ResourceVersion + switch { + case resource.ActiveVersion != nil: + rv = resource.ActiveVersion + case resource.SelectedVersion != nil: + rv = resource.SelectedVersion + } + + // Get information from resource. + if rv != nil { + v.Resources[id] = &SimplifiedResourceVersion{ + Version: rv.VersionNumber, + } + } + }() + } + + return v } func initVersionExport() (err error) { - // init export struct - versionExport = &versions{ - internalSave: true, - Channel: initialReleaseChannel, - Beta: initialReleaseChannel == helper.ReleaseChannelBeta, - Staging: initialReleaseChannel == helper.ReleaseChannelStaging, + if err := GetVersions().save(); err != nil { + log.Warningf("updates: failed to export version information: %s", err) } - versionExport.SetKey(versionsDBKey) - - // attach hook to database - versionExportHook, err = database.RegisterHook(query.New(versionsDBKey), &exportHook{}) - if err != nil { - return err + if err := GetSimpleVersions().save(); err != nil { + log.Warningf("updates: failed to export version information: %s", err) } return module.RegisterEventHook( @@ -66,71 +110,24 @@ func initVersionExport() (err error) { ) } -func stopVersionExport() error { - return versionExportHook.Cancel() +func (v *Versions) save() error { + if !v.KeyIsSet() { + v.SetKey(versionsDBKey) + } + return db.Put(v) +} + +func (v *SimpleVersions) save() error { + if !v.KeyIsSet() { + v.SetKey(simpleVersionsDBKey) + } + return db.Put(v) } // export is an event hook. func export(_ context.Context, _ interface{}) error { - // populate - versionExport.lock.Lock() - versionExport.Core = info.GetInfo() - versionExport.Resources = registry.Export() - versionExport.lock.Unlock() - - // save - err := versionExportDB.Put(versionExport) - if err != nil { - log.Warningf("updates: failed to export versions: %s", err) + if err := GetVersions().save(); err != nil { + return err } - - return nil -} - -// Lock locks the versionExport and all associated resources. -func (v *versions) Lock() { - // lock self - v.lock.Lock() - - // lock all resources - for _, res := range v.Resources { - res.Lock() - } -} - -// Lock unlocks the versionExport and all associated resources. -func (v *versions) Unlock() { - // unlock all resources - for _, res := range v.Resources { - res.Unlock() - } - - // unlock self - v.lock.Unlock() -} - -type exportHook struct { - database.HookBase -} - -// UsesPrePut implements the Hook interface. -func (eh *exportHook) UsesPrePut() bool { - return true -} - -var errInternalRecord = errors.New("may not modify internal record") - -// PrePut implements the Hook interface. -func (eh *exportHook) PrePut(r record.Record) (record.Record, error) { - if r.IsWrapped() { - return nil, errInternalRecord - } - ve, ok := r.(*versions) - if !ok { - return nil, errInternalRecord - } - if !ve.internalSave { - return nil, errInternalRecord - } - return r, nil + return GetSimpleVersions().save() } diff --git a/updates/main.go b/updates/main.go index 550e23ac..bee808a0 100644 --- a/updates/main.go +++ b/updates/main.go @@ -7,6 +7,7 @@ import ( "runtime" "time" + "github.com/safing/portbase/database" "github.com/safing/portbase/dataroot" "github.com/safing/portbase/log" "github.com/safing/portbase/modules" @@ -48,6 +49,11 @@ var ( updateASAP bool disableTaskSchedule bool + db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, + }) + // UserAgent is an HTTP User-Agent that is used to add // more context to requests made by the registry when // fetching resources from the update server. @@ -55,6 +61,8 @@ var ( ) const ( + updatesDirName = "updates" + updateFailed = "updates:failed" updateSuccess = "updates:success" ) @@ -108,7 +116,7 @@ func start() error { registry.UserAgent = userAgentFromFlag } // initialize - err := registry.Initialize(dataroot.Root().ChildDir("updates", 0o0755)) + err := registry.Initialize(dataroot.Root().ChildDir(updatesDirName, 0o0755)) if err != nil { return err } @@ -275,7 +283,7 @@ func stop() error { } } - return stopVersionExport() + return nil } // RootPath returns the root path used for storing updates. From 959bb012b87b7b8e05053ac1eb6141dd234e6015 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 21 Jun 2022 16:56:00 +0200 Subject: [PATCH 2/4] Add broadcasts module --- broadcasts/api.go | 116 +++++++++++++++ broadcasts/data.go | 102 +++++++++++++ broadcasts/install_info.go | 175 +++++++++++++++++++++++ broadcasts/module.go | 46 ++++++ broadcasts/notify.go | 285 +++++++++++++++++++++++++++++++++++++ broadcasts/state.go | 64 +++++++++ core/core.go | 3 +- 7 files changed, 790 insertions(+), 1 deletion(-) create mode 100644 broadcasts/api.go create mode 100644 broadcasts/data.go create mode 100644 broadcasts/install_info.go create mode 100644 broadcasts/module.go create mode 100644 broadcasts/notify.go create mode 100644 broadcasts/state.go diff --git a/broadcasts/api.go b/broadcasts/api.go new file mode 100644 index 00000000..f855ddfc --- /dev/null +++ b/broadcasts/api.go @@ -0,0 +1,116 @@ +package broadcasts + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/safing/portbase/api" + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/accessor" +) + +func registerAPIEndpoints() error { + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `broadcasts/matching-data`, + Read: api.PermitAdmin, + BelongsTo: module, + StructFunc: handleMatchingData, + Name: "Get Broadcast Notifications Matching Data", + Description: "Returns the data used by the broadcast notifications to match the instance.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `broadcasts/reset-state`, + Write: api.PermitAdmin, + WriteMethod: http.MethodPost, + BelongsTo: module, + ActionFunc: handleResetState, + Name: "Resets the Broadcast Notification States", + Description: "Delete the cache of Broadcast Notifications, making them appear again.", + }); err != nil { + return err + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Path: `broadcasts/simulate`, + Write: api.PermitAdmin, + WriteMethod: http.MethodPost, + BelongsTo: module, + ActionFunc: handleSimulate, + Name: "Simulate Broadcast Notifications", + Description: "Test broadcast notifications by sending a valid source file in the body.", + Parameters: []api.Parameter{ + { + Method: http.MethodPost, + Field: "state", + Value: "true", + Description: "Check against state when deciding to display a broadcast notification. Acknowledgements are always saved.", + }, + }, + }); err != nil { + return err + } + + return nil +} + +func handleMatchingData(ar *api.Request) (i interface{}, err error) { + return collectData(), nil +} + +func handleResetState(ar *api.Request) (msg string, err error) { + err = db.Delete(broadcastStatesDBKey) + if err != nil { + return "", err + } + return "Reset complete.", nil +} + +func handleSimulate(ar *api.Request) (msg string, err error) { + // Parse broadcast notification data. + broadcasts, err := parseBroadcastSource(ar.InputData) + if err != nil { + return "", fmt.Errorf("failed to parse broadcast notifications update: %w", err) + } + + // Get and marshal matching data. + matchingData := collectData() + matchingJSON, err := json.Marshal(matchingData) + if err != nil { + return "", fmt.Errorf("failed to marshal broadcast notifications matching data: %w", err) + } + matchingDataAccessor := accessor.NewJSONBytesAccessor(&matchingJSON) + + var bss *BroadcastStates + if ar.URL.Query().Get("state") == "true" { + // Get broadcast notification states. + bss, err = getBroadcastStates() + if err != nil { + if !errors.Is(err, database.ErrNotFound) { + return "", fmt.Errorf("failed to get broadcast notifications states: %w", err) + } + bss = newBroadcastStates() + } + } + + // Go through all broadcast nofications and check if they match. + var results []string + for _, bn := range broadcasts.Notifications { + err := handleBroadcast(bn, matchingDataAccessor, bss) + switch { + case err == nil: + results = append(results, fmt.Sprintf("%30s: displayed", bn.id)) + case errors.Is(err, ErrSkip): + results = append(results, fmt.Sprintf("%30s: %s", bn.id, err)) + default: + results = append(results, fmt.Sprintf("FAILED %23s: %s", bn.id, err)) + } + } + + return strings.Join(results, "\n"), nil +} diff --git a/broadcasts/data.go b/broadcasts/data.go new file mode 100644 index 00000000..73da1bb9 --- /dev/null +++ b/broadcasts/data.go @@ -0,0 +1,102 @@ +package broadcasts + +import ( + "time" + + "github.com/safing/portbase/config" + "github.com/safing/portmaster/intel/geoip" + "github.com/safing/portmaster/netenv" + "github.com/safing/portmaster/updates" + "github.com/safing/spn/access" + "github.com/safing/spn/captain" +) + +var portmasterStarted = time.Now() + +func collectData() interface{} { + data := make(map[string]interface{}) + + // Get data about versions. + versions := updates.GetSimpleVersions() + data["Updates"] = versions + data["Version"] = versions.Build.Version + numericVersion, err := MakeNumericVersion(versions.Build.Version) + if err != nil { + data["NumericVersion"] = &DataError{ + Error: err, + } + } else { + data["NumericVersion"] = numericVersion + } + + // Get data about install. + installInfo, err := GetInstallInfo() + if err != nil { + data["Install"] = &DataError{ + Error: err, + } + } else { + data["Install"] = installInfo + } + + // Get global configuration. + data["Config"] = config.GetActiveConfigValues() + + // Get data about device location. + locs, ok := netenv.GetInternetLocation() + if ok && locs.Best().LocationOrNil() != nil { + loc := locs.Best() + data["Location"] = &Location{ + Country: loc.Location.Country.ISOCode, + Coordinates: loc.Location.Coordinates, + ASN: loc.Location.AutonomousSystemNumber, + ASOrg: loc.Location.AutonomousSystemOrganization, + Source: loc.Source, + SourceAccuracy: loc.SourceAccuracy, + } + } + + // Get data about SPN status. + data["SPN"] = captain.GetSPNStatus() + + // Get data about account. + userRecord, err := access.GetUser() + if err != nil { + data["Account"] = &DataError{ + Error: err, + } + } else { + data["Account"] = &Account{ + UserRecord: userRecord, + UpToDate: userRecord.Meta().Modified > time.Now().Add(-7*24*time.Hour).Unix(), + MayUseUSP: userRecord.MayUseSPN(), + } + } + + // Time running. + data["UptimeHours"] = int(time.Since(portmasterStarted).Hours()) + + return data +} + +// Location holds location matching data. +type Location struct { + Country string + Coordinates geoip.Coordinates + ASN uint + ASOrg string + Source netenv.DeviceLocationSource + SourceAccuracy int +} + +// Account holds SPN account matching data. +type Account struct { + *access.UserRecord + UpToDate bool + MayUseUSP bool +} + +// DataError represents an error getting some matching data. +type DataError struct { + Error error +} diff --git a/broadcasts/install_info.go b/broadcasts/install_info.go new file mode 100644 index 00000000..2f667a17 --- /dev/null +++ b/broadcasts/install_info.go @@ -0,0 +1,175 @@ +package broadcasts + +import ( + "errors" + "fmt" + "strconv" + "sync" + "time" + + semver "github.com/hashicorp/go-version" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/database/record" + "github.com/safing/portbase/info" + "github.com/safing/portbase/log" +) + +const installInfoDBKey = "core:status/install-info" + +// InstallInfo holds generic info about the install. +type InstallInfo struct { + record.Base + sync.Mutex + + Version string + NumericVersion int64 + + Time time.Time + NumericDate int64 + DaysSinceInstall int64 + UnixTimestamp int64 +} + +// GetInstallInfo returns the install info from the database. +func GetInstallInfo() (*InstallInfo, error) { + r, err := db.Get(installInfoDBKey) + if err != nil { + return nil, err + } + + // Unwrap. + if r.IsWrapped() { + // Only allocate a new struct, if we need it. + newRecord := &InstallInfo{} + err = record.Unwrap(r, newRecord) + if err != nil { + return nil, err + } + return newRecord, nil + } + + // or adjust type + newRecord, ok := r.(*InstallInfo) + if !ok { + return nil, fmt.Errorf("record not of type *InstallInfo, but %T", r) + } + return newRecord, nil +} + +func ensureInstallInfo() { + // Get current install info from database. + installInfo, err := GetInstallInfo() + if err != nil { + installInfo = &InstallInfo{} + if !errors.Is(err, database.ErrNotFound) { + log.Warningf("updates: failed to load install info: %s", err) + } + } + + // Fill in missing data and save. + installInfo.checkAll() + if err := installInfo.save(); err != nil { + log.Warningf("updates: failed to save install info: %s", err) + } +} + +func (ii *InstallInfo) save() error { + if !ii.KeyIsSet() { + ii.SetKey(installInfoDBKey) + } + return db.Put(ii) +} + +func (ii *InstallInfo) checkAll() { + ii.checkVersion() + ii.checkInstallDate() +} + +func (ii *InstallInfo) checkVersion() { + // Check if everything is present. + if ii.Version != "" && ii.NumericVersion > 0 { + return + } + + // Update version information. + versionInfo := info.GetInfo() + ii.Version = versionInfo.Version + + // Update numeric version. + if versionInfo.Version != "" { + numericVersion, err := MakeNumericVersion(versionInfo.Version) + if err != nil { + log.Warningf("updates: failed to make numeric version: %s", err) + } else { + ii.NumericVersion = numericVersion + } + } +} + +// MakeNumericVersion makes a numeric version with the first three version +// segment always using three digits. +func MakeNumericVersion(version string) (numericVersion int64, err error) { + // Parse version string. + ver, err := semver.NewVersion(version) + if err != nil { + return 0, fmt.Errorf("failed to parse core version: %w", err) + } + + // Transform version for numeric representation. + segments := ver.Segments() + for i := 0; i < 3 && i < len(segments); i++ { + segmentNumber := int64(segments[i]) + if segmentNumber > 999 { + segmentNumber = 999 + } + switch i { + case 0: + numericVersion += segmentNumber * 1000000 + case 1: + numericVersion += segmentNumber * 1000 + case 2: + numericVersion += segmentNumber + } + } + + return numericVersion, nil +} + +func (ii *InstallInfo) checkInstallDate() { + // Check if everything is present. + if ii.UnixTimestamp > 0 && + ii.NumericDate > 0 && + ii.DaysSinceInstall > 0 && + !ii.Time.IsZero() { + return + } + + // Find oldest created database entry and use it as install time. + oldest := time.Now().Unix() + it, err := db.Query(query.New("core")) + if err != nil { + log.Warningf("updates: failed to create iterator for searching DB for install time: %s", err) + return + } + defer it.Cancel() + for r := range it.Next { + if oldest > r.Meta().Created { + oldest = r.Meta().Created + } + } + + // Set data. + ii.UnixTimestamp = oldest + ii.Time = time.Unix(oldest, 0) + ii.DaysSinceInstall = int64(time.Since(ii.Time).Hours()) / 24 + + // Transform date for numeric representation. + numericDate, err := strconv.ParseInt(ii.Time.Format("20060102"), 10, 64) + if err != nil { + log.Warningf("updates: failed to make numeric date from %s: %s", ii.Time, err) + } else { + ii.NumericDate = numericDate + } +} diff --git a/broadcasts/module.go b/broadcasts/module.go new file mode 100644 index 00000000..360bc912 --- /dev/null +++ b/broadcasts/module.go @@ -0,0 +1,46 @@ +package broadcasts + +import ( + "sync" + "time" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/modules" +) + +var ( + module *modules.Module + + db = database.NewInterface(&database.Options{ + Local: true, + Internal: true, + }) + + startOnce sync.Once +) + +func init() { + module = modules.Register("broadcasts", prep, start, nil, "updates", "netenv", "notifications") +} + +func prep() error { + // Register API endpoints. + if err := registerAPIEndpoints(); err != nil { + return err + } + + return nil +} + +func start() error { + // Ensure the install info is up to date. + ensureInstallInfo() + + // Start broadcast notifier task. + startOnce.Do(func() { + module.NewTask("broadcast notifier", broadcastNotify). + Repeat(10 * time.Minute).Queue() + }) + + return nil +} diff --git a/broadcasts/notify.go b/broadcasts/notify.go new file mode 100644 index 00000000..c7b7d661 --- /dev/null +++ b/broadcasts/notify.go @@ -0,0 +1,285 @@ +package broadcasts + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "strings" + "sync" + "time" + + "github.com/ghodss/yaml" + + "github.com/safing/portbase/database" + "github.com/safing/portbase/database/accessor" + "github.com/safing/portbase/database/query" + "github.com/safing/portbase/log" + "github.com/safing/portbase/modules" + "github.com/safing/portbase/notifications" + "github.com/safing/portmaster/updates" +) + +const ( + broadcastsResourcePath = "intel/portmaster/notifications.yaml" + + broadcastNotificationIDPrefix = "broadcasts:" + + minRepeatDuration = 1 * time.Hour +) + +// Errors. +var ( + ErrSkip = errors.New("broadcast skipped") + ErrSkipDoesNotMatch = fmt.Errorf("%w: does not match", ErrSkip) + ErrSkipAlreadyActive = fmt.Errorf("%w: already active", ErrSkip) + ErrSkipAlreadyShown = fmt.Errorf("%w: already shown", ErrSkip) + ErrSkipRemovedByMismatch = fmt.Errorf("%w: removed due to mismatch", ErrSkip) + ErrSkipRemovedBySource = fmt.Errorf("%w: removed by source", ErrSkip) +) + +// BroadcastNotifications holds the data structure of the broadcast +// notifications update file. +type BroadcastNotifications struct { + Notifications map[string]*BroadcastNotification +} + +// BroadcastNotification is a single broadcast notification. +type BroadcastNotification struct { + *notifications.Notification + id string + + // Match holds a query string that needs to match the local matching data in + // order for the broadcast to be displayed. + Match string + matchingQuery *query.Query + // AttachToModule signifies if the broadcast notification should be attached to the module. + AttachToModule bool + // Remove signifies that the broadcast should be canceled and its state removed. + Remove bool + // Permanent signifies that the broadcast cannot be acknowledge by the user + // and remains in the UI indefinitely. + Permanent bool + // Repeat specifies a duration after which the broadcast should be shown again. + Repeat string + repeatDuration time.Duration +} + +func broadcastNotify(ctx context.Context, t *modules.Task) error { + // Get broadcast notifications file, load it from disk and parse it. + broadcastsResource, err := updates.GetFile(broadcastsResourcePath) + if err != nil { + return fmt.Errorf("failed to get broadcast notifications update: %w", err) + } + broadcastsData, err := ioutil.ReadFile(broadcastsResource.Path()) + if err != nil { + return fmt.Errorf("failed to load broadcast notifications update: %w", err) + } + broadcasts, err := parseBroadcastSource(broadcastsData) + if err != nil { + return fmt.Errorf("failed to parse broadcast notifications update: %w", err) + } + + // Get and marshal matching data. + matchingData := collectData() + matchingJSON, err := json.Marshal(matchingData) + if err != nil { + return fmt.Errorf("failed to marshal broadcast notifications matching data: %w", err) + } + matchingDataAccessor := accessor.NewJSONBytesAccessor(&matchingJSON) + + // Get broadcast notification states. + bss, err := getBroadcastStates() + if err != nil { + if !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to get broadcast notifications states: %w", err) + } + bss = newBroadcastStates() + } + + // Go through all broadcast nofications and check if they match. + for _, bn := range broadcasts.Notifications { + err := handleBroadcast(bn, matchingDataAccessor, bss) + switch { + case err == nil: + log.Infof("broadcasts: displaying broadcast %s", bn.id) + case errors.Is(err, ErrSkip): + log.Tracef("broadcasts: skipped displaying broadcast %s: %s", bn.id, err) + default: + log.Warningf("broadcasts: failed to handle broadcast %s: %s", bn.id, err) + } + } + + return nil +} + +func parseBroadcastSource(yamlData []byte) (*BroadcastNotifications, error) { + // Parse data. + broadcasts := &BroadcastNotifications{} + err := yaml.Unmarshal(yamlData, broadcasts) + if err != nil { + return nil, err + } + + // Add IDs to struct for easier handling. + for id, bn := range broadcasts.Notifications { + bn.id = id + + // Parse matching query. + if bn.Match != "" { + q, err := query.ParseQuery("query / where " + bn.Match) + if err != nil { + return nil, fmt.Errorf("failed to parse query of broadcast notification %s: %w", bn.id, err) + } + bn.matchingQuery = q + } + + // Parse the repeat duration. + if bn.Repeat != "" { + duration, err := time.ParseDuration(bn.Repeat) + if err != nil { + return nil, fmt.Errorf("failed to parse repeat duration of broadcast notification %s: %w", bn.id, err) + } + bn.repeatDuration = duration + // Raise duration to minimum. + if bn.repeatDuration < minRepeatDuration { + bn.repeatDuration = minRepeatDuration + } + } + } + + return broadcasts, nil +} + +func handleBroadcast(bn *BroadcastNotification, matchingDataAccessor accessor.Accessor, bss *BroadcastStates) error { + // Check if broadcast was already shown. + if bss != nil { + state, ok := bss.States[bn.id] + switch { + case !ok || state.Read.IsZero(): + // Was never shown, continue. + case bn.repeatDuration == 0 && !state.Read.IsZero(): + // Was already shown and is not repeated, skip. + return ErrSkipAlreadyShown + case bn.repeatDuration > 0 && time.Now().Add(-bn.repeatDuration).After(state.Read): + // Was already shown, but should be repeated now, continue. + } + } + + // Check if broadcast should be removed. + if bn.Remove { + removeBroadcast(bn, bss) + return ErrSkipRemovedBySource + } + + // Skip if broadcast does not match. + if bn.matchingQuery != nil && !bn.matchingQuery.MatchesAccessor(matchingDataAccessor) { + removed := removeBroadcast(bn, bss) + if removed { + return ErrSkipRemovedByMismatch + } + return ErrSkipDoesNotMatch + } + + // Check if there is already an active notification for this. + eventID := broadcastNotificationIDPrefix + bn.id + n := notifications.Get(eventID) + if n != nil { + // Already active! + return ErrSkipAlreadyActive + } + + // Prepare notification for displaying. + n = bn.Notification + n.EventID = eventID + n.GUID = "" + n.State = "" + n.SelectedActionID = "" + + // It is okay to edit the notification, as they are loaded from the file every time. + // Add dismiss button if the notification is not permanent. + if !bn.Permanent { + n.AvailableActions = append(n.AvailableActions, ¬ifications.Action{ + ID: "ack", + Text: "Got it!", + }) + } + n.SetActionFunction(markBroadcastAsRead) + + // Display notification. + n.Save() + + // Attach to module to raise more awareness. + if bn.AttachToModule { + n.AttachToModule(module) + } + + return nil +} + +func removeBroadcast(bn *BroadcastNotification, bss *BroadcastStates) (removed bool) { + // Remove any active notification. + n := notifications.Get(broadcastNotificationIDPrefix + bn.id) + if n != nil { + removed = true + n.Delete() + } + + // Remove any state. + if bss != nil { + delete(bss.States, bn.id) + } + + return +} + +var savingBroadcastStateLock sync.Mutex + +func markBroadcastAsRead(ctx context.Context, n *notifications.Notification) error { + // Lock persisting broadcast state. + savingBroadcastStateLock.Lock() + defer savingBroadcastStateLock.Unlock() + + // Get notification data. + var broadcastID, actionID string + func() { + n.Lock() + defer n.Unlock() + broadcastID = strings.TrimPrefix(n.EventID, broadcastNotificationIDPrefix) + actionID = n.SelectedActionID + }() + + // Check response. + switch actionID { + case "ack": + case "": + return fmt.Errorf("no action ID for %s", broadcastID) + default: + return fmt.Errorf("unexpected action ID for %s: %s", broadcastID, actionID) + } + + // Get broadcast notification states. + bss, err := getBroadcastStates() + if err != nil { + if !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("failed to get broadcast notifications states: %w", err) + } + bss = newBroadcastStates() + } + + // Get state for this notification. + bs, ok := bss.States[broadcastID] + if !ok { + bs = &BroadcastState{} + bss.States[broadcastID] = bs + } + + // Delete to allow for timely repeats. + n.Delete() + + // Mark as read and save to DB. + log.Infof("broadcasts: user acknowledged broadcast %s", broadcastID) + bs.Read = time.Now() + return bss.save() +} diff --git a/broadcasts/state.go b/broadcasts/state.go new file mode 100644 index 00000000..afe8994c --- /dev/null +++ b/broadcasts/state.go @@ -0,0 +1,64 @@ +package broadcasts + +import ( + "fmt" + "sync" + "time" + + "github.com/safing/portbase/database/record" +) + +const broadcastStatesDBKey = "core:broadcasts/state" + +// BroadcastStates holds states for broadcast notifications. +type BroadcastStates struct { + record.Base + sync.Mutex + + States map[string]*BroadcastState +} + +// BroadcastState holds state for a single broadcast notifications. +type BroadcastState struct { + Read time.Time +} + +func (bss *BroadcastStates) save() error { + return db.Put(bss) +} + +// getbroadcastStates returns the broadcast states from the database. +func getBroadcastStates() (*BroadcastStates, error) { + r, err := db.Get(broadcastStatesDBKey) + if err != nil { + return nil, err + } + + // Unwrap. + if r.IsWrapped() { + // Only allocate a new struct, if we need it. + newRecord := &BroadcastStates{} + err = record.Unwrap(r, newRecord) + if err != nil { + return nil, err + } + return newRecord, nil + } + + // or adjust type + newRecord, ok := r.(*BroadcastStates) + if !ok { + return nil, fmt.Errorf("record not of type *BroadcastStates, but %T", r) + } + return newRecord, nil +} + +// newBroadcastStates returns a new BroadcastStates. +func newBroadcastStates() *BroadcastStates { + bss := &BroadcastStates{ + States: make(map[string]*BroadcastState), + } + bss.SetKey(broadcastStatesDBKey) + + return bss +} diff --git a/core/core.go b/core/core.go index 80bd6343..8b70cba6 100644 --- a/core/core.go +++ b/core/core.go @@ -7,6 +7,7 @@ import ( "github.com/safing/portbase/modules" "github.com/safing/portbase/modules/subsystems" + _ "github.com/safing/portmaster/broadcasts" _ "github.com/safing/portmaster/netenv" _ "github.com/safing/portmaster/status" _ "github.com/safing/portmaster/ui" @@ -25,7 +26,7 @@ var ( ) func init() { - module = modules.Register("core", prep, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui", "netenv", "network", "interception", "compat") + module = modules.Register("core", prep, start, nil, "base", "subsystems", "status", "updates", "api", "notifications", "ui", "netenv", "network", "interception", "compat", "broadcasts") subsystems.Register( "core", "Core", From 92cc733aca318f45c8bef06b66be0cadb915039d Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Jun 2022 16:30:55 +0200 Subject: [PATCH 3/4] Add broadcast notifications test data --- broadcasts/testdata/README.md | 9 +++++++++ broadcasts/testdata/notifications.yaml | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 broadcasts/testdata/README.md create mode 100644 broadcasts/testdata/notifications.yaml diff --git a/broadcasts/testdata/README.md b/broadcasts/testdata/README.md new file mode 100644 index 00000000..1752a7f6 --- /dev/null +++ b/broadcasts/testdata/README.md @@ -0,0 +1,9 @@ +# Testing Broadcast Notifications + +``` +# Reset state +curl -X POST http://127.0.0.1:817/api/v1/broadcasts/reset-state + +# Simulate notifications +curl --upload-file notifications.yaml http://127.0.0.1:817/api/v1/broadcasts/simulate +``` diff --git a/broadcasts/testdata/notifications.yaml b/broadcasts/testdata/notifications.yaml new file mode 100644 index 00000000..6c60d6b5 --- /dev/null +++ b/broadcasts/testdata/notifications.yaml @@ -0,0 +1,22 @@ +notifications: + test1: + title: "[TEST] Normal Broadcast" + message: "This is a normal broadcast without matching. (#1)" + test2: + title: "[TEST] Permanent Broadcast" + message: "This is a permanent broadcast without matching. (#2)" + type: 1 # Warning + permanent: true + test3: + title: "[TEST] Repeating Broadcast" + message: "This is a repeating broadcast without matching. (#3)" + repeat: "1m" + test4: + title: "[TEST] Matching Broadcast: PM version" + message: "This is a normal broadcast that matches the PM version. (#4)" + match: "NumericVersion > 8000" + test5: + title: "[TEST] Important Update" + message: "A criticial update has been released, please update immediately. (#5)" + type: 3 # Error + attachToModule: true From 492afc6dcfa2882749cc5ecbe5ddff19c676c612 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 23 Jun 2022 16:31:04 +0200 Subject: [PATCH 4/4] Update dependencies --- go.mod | 25 ++++++++++++------------- go.sum | 45 ++++++++++++++++++++++++++++----------------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/go.mod b/go.mod index 997bdcb0..ab267425 100644 --- a/go.mod +++ b/go.mod @@ -7,23 +7,24 @@ require ( github.com/cookieo9/resources-go v0.0.0-20150225115733-d27c04069d0d github.com/coreos/go-iptables v0.6.0 github.com/florianl/go-nfqueue v1.3.1 + github.com/ghodss/yaml v1.0.0 github.com/godbus/dbus/v5 v5.1.0 github.com/google/gopacket v1.1.19 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/go-version v1.4.0 - github.com/miekg/dns v1.1.49 + github.com/hashicorp/go-version v1.5.0 + github.com/miekg/dns v1.1.50 github.com/oschwald/maxminddb-golang v1.9.0 - github.com/safing/portbase v0.14.4 - github.com/safing/spn v0.4.11 + github.com/safing/portbase v0.14.5 + github.com/safing/spn v0.4.12 github.com/shirou/gopsutil v3.21.11+incompatible - github.com/spf13/cobra v1.4.0 + github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.7.1 github.com/tannerryan/ring v1.1.2 github.com/tevino/abool v1.2.0 github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 - golang.org/x/net v0.0.0-20220513224357-95641704303c - golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 - golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a + golang.org/x/net v0.0.0-20220622184535-263ec571b305 + golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f + golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 ) require ( @@ -35,7 +36,6 @@ require ( github.com/bluele/gcache v0.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect - github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/gofrs/uuid v4.2.0+incompatible // indirect github.com/google/go-cmp v0.5.8 // indirect @@ -71,10 +71,9 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect go.etcd.io/bbolt v1.3.6 // indirect - golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 // indirect - golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect + golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect + golang.org/x/tools v0.1.11 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 7fe99705..40b1435d 100644 --- a/go.sum +++ b/go.sum @@ -250,6 +250,7 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -530,8 +531,9 @@ github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.5.0 h1:O293SZ2Eg+AAYijkVK3jR786Am1bhDEh2GHT0tIVE5E= +github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -688,8 +690,9 @@ github.com/miekg/dns v1.1.45/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7Xn github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.49 h1:qe0mQU3Z/XpFeE+AEBo2rqaS1IPBJ3anmqZ4XiZJVG8= github.com/miekg/dns v1.1.49/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= +github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -818,8 +821,9 @@ github.com/safing/portbase v0.14.0/go.mod h1:z9sRR/vqohAGdYSSx2B+o8tND4WVvcxPL6X github.com/safing/portbase v0.14.1/go.mod h1:z9sRR/vqohAGdYSSx2B+o8tND4WVvcxPL6XBBtN3bDI= github.com/safing/portbase v0.14.2/go.mod h1:z9sRR/vqohAGdYSSx2B+o8tND4WVvcxPL6XBBtN3bDI= github.com/safing/portbase v0.14.3/go.mod h1:z9sRR/vqohAGdYSSx2B+o8tND4WVvcxPL6XBBtN3bDI= -github.com/safing/portbase v0.14.4 h1:yEWlvh2w0gVdjrA9oo5QPFV5oluJ6YDoKRPtmFb9B4c= github.com/safing/portbase v0.14.4/go.mod h1:z9sRR/vqohAGdYSSx2B+o8tND4WVvcxPL6XBBtN3bDI= +github.com/safing/portbase v0.14.5 h1:+8H+mQ7AFjA04M7UPq0490pj3/+nvJj3pEUP1PYTMYc= +github.com/safing/portbase v0.14.5/go.mod h1:z9sRR/vqohAGdYSSx2B+o8tND4WVvcxPL6XBBtN3bDI= github.com/safing/portmaster v0.7.3/go.mod h1:o//kZ8eE+5vT1V22mgnxHIAdlEz42sArsK5OF2Lf/+s= github.com/safing/portmaster v0.7.4/go.mod h1:Q93BWdF1oAL0oUMukshl8W1aPZhmrlTGi6tFTFc3pTw= github.com/safing/portmaster v0.7.6/go.mod h1:qOs9hQtvAzTVICRbwLg3vddqOaqJHeWBjWQ0C+TJ/Bw= @@ -837,6 +841,7 @@ github.com/safing/portmaster v0.8.5/go.mod h1:MqOlFwHcIx/109Ugutz/CG23znuuXCRVHc github.com/safing/portmaster v0.8.7/go.mod h1:RUgCWt5v22jDUOtJfOwApi//Kt8RTZQhlREcBc+L4z8= github.com/safing/portmaster v0.8.9-interdep/go.mod h1:1hK7QpvFVlb/sglkc3SKj+RXMGBuk0wqO2s3pvMg1Xs= github.com/safing/portmaster v0.8.9/go.mod h1:tv0rxO76hrpBLdArN7YTypOaseH6zgQ2gLI2zCknk9Q= +github.com/safing/portmaster v0.8.14-interdep/go.mod h1:HIkaE8wCXr8ULyZSWFkQNNY9obpMufxizXZugnjHLK0= github.com/safing/spn v0.3.4/go.mod h1:TfzNsZCbnlWv0UFDILFOUSudVKJZlnBVoR1fDXrjOK0= github.com/safing/spn v0.3.5/go.mod h1:jHkFF2Yu1fnjFu4KXjVA+iagMr/z4eB4p3jiwikvKj8= github.com/safing/spn v0.3.6/go.mod h1:RSeFb/h5Wt3yDVezXj3lhXJ/Iwd7FbtsGf5E+p5J2YQ= @@ -854,14 +859,10 @@ github.com/safing/spn v0.4.3/go.mod h1:YHtg3FkZviN8T7db4BdRffbYO1pO7w9SydQatLmvW github.com/safing/spn v0.4.5/go.mod h1:mkQA5pYM1SUd4JkTyuwXFycFMGQXLTd9RUJuY2vqccM= github.com/safing/spn v0.4.6/go.mod h1:AmZ+rore+6DQp0GSchIAXPn8ij0Knyw7uy4PbMLljXg= github.com/safing/spn v0.4.7/go.mod h1:NoSG9K0OK9hrPC76yqWFS6RtvbqZdIc/KGOsC4T3hV8= -github.com/safing/spn v0.4.8 h1:C5QOl03hypDaC4qvFtWjAKmSJIOjfqYVGT+sTnUIO/E= -github.com/safing/spn v0.4.8/go.mod h1:nro/I6b2JnafeeqoMsQRqf6TaQeL9uLLZkUREtxLVDE= -github.com/safing/spn v0.4.9 h1:p+TRS2y1yAY4CYyHiVuy+V3NcA9p826NtYksuBaSl64= -github.com/safing/spn v0.4.9/go.mod h1:nro/I6b2JnafeeqoMsQRqf6TaQeL9uLLZkUREtxLVDE= -github.com/safing/spn v0.4.10 h1:nZuWPBn1z0aBUDnnpG9pKZCmPAH2hgNE4cqiSB7JGU8= -github.com/safing/spn v0.4.10/go.mod h1:nro/I6b2JnafeeqoMsQRqf6TaQeL9uLLZkUREtxLVDE= github.com/safing/spn v0.4.11 h1:Er3ZtBCqKaf0G6lmRKpm1LenlrWeaWUp8612bpeG+aM= github.com/safing/spn v0.4.11/go.mod h1:nro/I6b2JnafeeqoMsQRqf6TaQeL9uLLZkUREtxLVDE= +github.com/safing/spn v0.4.12 h1:Tw7TUZEZR4yZy7L+ICRCketDk5L5x0s0pvrSUHFaKs4= +github.com/safing/spn v0.4.12/go.mod h1:AUNgBrRwCcspC98ljptDnrPuHLn/BHSG+rSprV/5Wlc= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -905,8 +906,9 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/cobra v1.1.3/go.mod h1:pGADOWyqRD/YMrPZigI/zbliZ2wVD/23d+is3pSWzOo= github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= +github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1094,8 +1096,10 @@ golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1133,8 +1137,9 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1215,8 +1220,10 @@ golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0= golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220621193019-9d032be2e588/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220622184535-263ec571b305 h1:dAgbJ2SP4jD6XYfMNLVj0BF21jo2PjChrtGaAvF5M3I= +golang.org/x/net v0.0.0-20220622184535-263ec571b305/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1248,8 +1255,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f h1:Ax0t5p6N38Ga0dThY21weqDEyz2oklo4IvDkpigvkD8= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1374,8 +1382,11 @@ golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220325203850-36772127a21f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1471,13 +1482,13 @@ golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= +golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=