mirror of
https://github.com/safing/portmaster
synced 2025-04-24 04:49:10 +00:00
Compare commits
71 commits
Author | SHA1 | Date | |
---|---|---|---|
|
eccda20802 | ||
|
7b05ed82b2 | ||
|
b9a9129e81 | ||
|
be133b8856 | ||
|
32c7f6e7d1 | ||
|
4f3ebc7156 | ||
|
9b12dfffc2 | ||
|
2c8ab54104 | ||
|
c0d8d0c2f0 | ||
|
67cfefde9b | ||
|
b8ab348095 | ||
|
782c07d867 | ||
|
a13d52b68f | ||
|
71f6f09384 | ||
|
c04213219b | ||
|
7373b8868b | ||
|
b68646c689 | ||
|
130c4a427c | ||
|
76c352da5a | ||
|
83292d761c | ||
|
4721e58727 | ||
|
90ead7d5e5 | ||
|
c742c7dfd1 | ||
|
fe8a560f9e | ||
|
fdca991166 | ||
|
5e7ad95a44 | ||
|
40b443282f | ||
|
d8108bff0e | ||
|
98137ca4b6 | ||
|
857df4086f | ||
|
32d6e1cb04 | ||
|
0f28af66cd | ||
|
726159427b | ||
|
88b92dcc93 | ||
|
f021ec2444 | ||
|
3478622eb8 | ||
|
b4fda1bdce | ||
|
0937bedd6c | ||
|
241bf20c7a | ||
|
3ee214abaf | ||
|
96209c28cf | ||
|
ef7b129ced | ||
|
1e9e6263d4 | ||
|
c7f3475382 | ||
|
6c014d227c | ||
|
df70c70ab5 | ||
|
692838b696 | ||
|
475d69f8a2 | ||
|
9d874daed2 | ||
|
05a5d5e350 | ||
|
22253c4e9e | ||
|
6e173e3b96 | ||
|
ed2338fdb9 | ||
|
2a9d75433f | ||
|
ef0995b1f7 | ||
|
df4106fd70 | ||
|
614d8972a2 | ||
|
acaf98144a | ||
|
473f05becc | ||
|
1a1bc14804 | ||
|
943b9b7859 | ||
|
ca88c6d8ad | ||
|
fe070b4f56 | ||
|
a4b76843e5 | ||
|
38e9e342f7 | ||
|
ee58324aee | ||
|
f4b96e1ce7 | ||
|
07acb9befa | ||
|
a99b68ec91 | ||
|
af3bb804bf | ||
|
145f5e67de |
148 changed files with 11443 additions and 6468 deletions
.github/workflows
.gitignore.golangci.ymlEarthfilebase
cmds
desktop
angular
tauri/src-tauri
service
compat
core/base
firewall
bypassing.godns.gomodule.gopacket_handler.go
instance.gointerception
dnsmonitor
etwlink_windows.goeventlistener.goeventlistener_linux.goeventlistener_windows.gomodule.govarlinktypes.go
nfq
windowskext
windowskext2
integration
intel
mgr
nameserver
netquery
network
process
profile
4
.github/workflows/go.yml
vendored
4
.github/workflows/go.yml
vendored
|
@ -38,9 +38,9 @@ jobs:
|
|||
cache: false
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
uses: golangci/golangci-lint-action@v6
|
||||
with:
|
||||
version: v1.60.3
|
||||
version: v1.64.6
|
||||
only-new-issues: true
|
||||
args: -c ./.golangci.yml --timeout 15m
|
||||
|
||||
|
|
41
.github/workflows/windows-dll.yml
vendored
Normal file
41
.github/workflows/windows-dll.yml
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
name: Windows Portmaster Core DLL
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'windows_core_dll/**'
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
pull_request:
|
||||
paths:
|
||||
- 'windows_core_dll/**'
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Add msbuild to PATH
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
- name: Build DLL
|
||||
run: msbuild windows_core_dll\windows_core_dll.sln -t:rebuild -property:Configuration=Release
|
||||
- name: Verify DLL
|
||||
shell: powershell
|
||||
run: |
|
||||
if (!(Test-Path "windows_core_dll/x64/Release/portmaster-core.dll")) {
|
||||
Write-Error "DLL build failed: portmaster-core.dll not found"
|
||||
exit 1
|
||||
}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: portmaster-core-dll
|
||||
path: windows_core_dll/x64/Release/portmaster-core.dll
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -12,8 +12,7 @@ go.mod.*
|
|||
vendor
|
||||
|
||||
# testing
|
||||
testing
|
||||
spn/testing/simple/testdata
|
||||
testdata
|
||||
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.a
|
||||
|
@ -52,3 +51,4 @@ go.work.sum
|
|||
|
||||
# Kext releases
|
||||
windows_kext/release/kext_release_*.zip
|
||||
windows_core_dll/.vs/windows_core_dll
|
||||
|
|
|
@ -8,7 +8,6 @@ linters:
|
|||
- contextcheck
|
||||
- cyclop
|
||||
- depguard
|
||||
- exhaustivestruct
|
||||
- exhaustruct
|
||||
- forbidigo
|
||||
- funlen
|
||||
|
@ -16,12 +15,8 @@ linters:
|
|||
- gochecknoinits
|
||||
- gocognit
|
||||
- gocyclo
|
||||
- goerr113
|
||||
- gomnd
|
||||
- gomoddirectives
|
||||
- ifshort
|
||||
- interfacebloat
|
||||
- interfacer
|
||||
- ireturn
|
||||
- lll
|
||||
- mnd
|
||||
|
@ -32,7 +27,6 @@ linters:
|
|||
- noctx
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosnakecase
|
||||
- perfsprint # TODO(ppacher): we should re-enanble this one to avoid costly fmt.* calls in the hot-path
|
||||
- revive
|
||||
- tagliatelle
|
||||
|
@ -42,15 +36,14 @@ linters:
|
|||
- whitespace
|
||||
- wrapcheck
|
||||
- wsl
|
||||
- gci
|
||||
- tenv # Deprecated
|
||||
|
||||
linters-settings:
|
||||
revive:
|
||||
# See https://github.com/mgechev/revive#available-rules for details.
|
||||
enable-all-rules: true
|
||||
gci:
|
||||
# put imports beginning with prefix after 3rd-party packages;
|
||||
# only support one prefix
|
||||
# if not set, use goimports.local-prefixes
|
||||
goimports:
|
||||
local-prefixes: github.com/safing
|
||||
godox:
|
||||
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
|
||||
|
|
11
Earthfile
11
Earthfile
|
@ -1,9 +1,9 @@
|
|||
VERSION --arg-scope-and-set --global-cache 0.8
|
||||
|
||||
ARG --global go_version = 1.22
|
||||
ARG --global go_version = 1.24
|
||||
ARG --global node_version = 18
|
||||
ARG --global rust_version = 1.79
|
||||
ARG --global golangci_lint_version = 1.57.1
|
||||
ARG --global golangci_lint_version = 1.64.6
|
||||
|
||||
ARG --global go_builder_image = "golang:${go_version}-alpine"
|
||||
ARG --global node_builder_image = "node:${node_version}"
|
||||
|
@ -70,6 +70,11 @@ build:
|
|||
# ./dist/all/assets.zip
|
||||
BUILD +assets
|
||||
|
||||
build-spn:
|
||||
BUILD +go-build --CMDS="hub" --GOOS="linux" --GOARCH="amd64"
|
||||
BUILD +go-build --CMDS="hub" --GOOS="linux" --GOARCH="arm64"
|
||||
# TODO: Add other platforms
|
||||
|
||||
go-ci:
|
||||
BUILD +go-build --GOOS="linux" --GOARCH="amd64"
|
||||
BUILD +go-build --GOOS="linux" --GOARCH="arm64"
|
||||
|
@ -420,7 +425,7 @@ rust-base:
|
|||
DO rust+INIT --keep_fingerprints=true
|
||||
|
||||
# For now we need tauri-cli 2.0.0 for bulding
|
||||
DO rust+CARGO --args="install tauri-cli --version ^2.0.0-beta"
|
||||
DO rust+CARGO --args="install tauri-cli --version 2.1.0 --locked"
|
||||
|
||||
# Explicitly cache here.
|
||||
SAVE IMAGE --cache-hint
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"github.com/safing/portmaster/base/database/dbmodule"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/base/utils/debug"
|
||||
|
@ -144,12 +145,13 @@ func InitializeUnitTestDataroot(testName string) (string, error) {
|
|||
return "", fmt.Errorf("failed to make tmp dir: %w", err)
|
||||
}
|
||||
|
||||
ds := utils.NewDirStructure(basePath, 0o0755)
|
||||
ds := utils.NewDirStructure(basePath, utils.PublicReadPermission)
|
||||
SetDataRoot(ds)
|
||||
err = dataroot.Initialize(basePath, 0o0755)
|
||||
err = dataroot.Initialize(basePath, utils.PublicReadPermission)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to initialize dataroot: %w", err)
|
||||
}
|
||||
dbmodule.SetDatabaseLocation(dataroot.Root())
|
||||
|
||||
return basePath, nil
|
||||
}
|
||||
|
|
|
@ -264,6 +264,20 @@ func (c *Controller) Purge(ctx context.Context, q *query.Query, local, internal
|
|||
return 0, ErrNotImplemented
|
||||
}
|
||||
|
||||
// PurgeOlderThan deletes all records last updated before the given time.
|
||||
// It returns the number of successful deletes and an error.
|
||||
func (c *Controller) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal bool) (int, error) {
|
||||
if shuttingDown.IsSet() {
|
||||
return 0, ErrShuttingDown
|
||||
}
|
||||
|
||||
if purger, ok := c.storage.(storage.PurgeOlderThan); ok {
|
||||
return purger.PurgeOlderThan(ctx, prefix, purgeBefore, local, internal, c.shadowDelete)
|
||||
}
|
||||
|
||||
return 0, ErrNotImplemented
|
||||
}
|
||||
|
||||
// Shutdown shuts down the storage.
|
||||
func (c *Controller) Shutdown() error {
|
||||
return c.storage.Shutdown()
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
@ -37,7 +36,6 @@ func SetDatabaseLocation(dirStructureRoot *utils.DirStructure) {
|
|||
}
|
||||
|
||||
func prep() error {
|
||||
SetDatabaseLocation(dataroot.Root())
|
||||
if databaseStructureRoot == nil {
|
||||
return errors.New("database location not specified")
|
||||
}
|
||||
|
|
|
@ -562,6 +562,27 @@ func (i *Interface) Purge(ctx context.Context, q *query.Query) (int, error) {
|
|||
return db.Purge(ctx, q, i.options.Local, i.options.Internal)
|
||||
}
|
||||
|
||||
// PurgeOlderThan deletes all records last updated before the given time.
|
||||
// It returns the number of successful deletes and an error.
|
||||
func (i *Interface) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time) (int, error) {
|
||||
dbName, dbKeyPrefix := record.ParseKey(prefix)
|
||||
if dbName == "" {
|
||||
return 0, errors.New("unknown database")
|
||||
}
|
||||
|
||||
db, err := getController(dbName)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Check if database is read only before we add to the cache.
|
||||
if db.ReadOnly() {
|
||||
return 0, ErrReadOnly
|
||||
}
|
||||
|
||||
return db.PurgeOlderThan(ctx, dbKeyPrefix, purgeBefore, i.options.Local, i.options.Internal)
|
||||
}
|
||||
|
||||
// Subscribe subscribes to updates matching the given query.
|
||||
func (i *Interface) Subscribe(q *query.Query) (*Subscription, error) {
|
||||
_, err := q.Check()
|
||||
|
|
|
@ -9,9 +9,8 @@ import (
|
|||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
databasesSubDir = "databases"
|
||||
)
|
||||
// DatabasesSubDir defines the sub directory where the databases are stored.
|
||||
const DatabasesSubDir = "databases"
|
||||
|
||||
var (
|
||||
initialized = abool.NewBool(false)
|
||||
|
@ -25,7 +24,7 @@ var (
|
|||
|
||||
// InitializeWithPath initializes the database at the specified location using a path.
|
||||
func InitializeWithPath(dirPath string) error {
|
||||
return Initialize(utils.NewDirStructure(dirPath, 0o0755))
|
||||
return Initialize(utils.NewDirStructure(dirPath, utils.PublicReadPermission))
|
||||
}
|
||||
|
||||
// Initialize initializes the database at the specified location using a dir structure.
|
||||
|
@ -34,7 +33,7 @@ func Initialize(dirStructureRoot *utils.DirStructure) error {
|
|||
rootStructure = dirStructureRoot
|
||||
|
||||
// ensure root and databases dirs
|
||||
databasesStructure = rootStructure.ChildDir(databasesSubDir, 0o0700)
|
||||
databasesStructure = rootStructure.ChildDir(DatabasesSubDir, utils.AdminOnlyPermission)
|
||||
err := databasesStructure.Ensure()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create/open database directory (%s): %w", rootStructure.Path, err)
|
||||
|
@ -67,7 +66,7 @@ func Shutdown() (err error) {
|
|||
|
||||
// getLocation returns the storage location for the given name and type.
|
||||
func getLocation(name, storageType string) (string, error) {
|
||||
location := databasesStructure.ChildDir(name, 0o0700).ChildDir(storageType, 0o0700)
|
||||
location := databasesStructure.ChildDir(name, utils.AdminOnlyPermission).ChildDir(storageType, utils.AdminOnlyPermission)
|
||||
// check location
|
||||
err := location.Ensure()
|
||||
if err != nil {
|
||||
|
|
|
@ -99,6 +99,11 @@ func (q *Query) MatchesKey(dbKey string) bool {
|
|||
return strings.HasPrefix(dbKey, q.dbKeyPrefix)
|
||||
}
|
||||
|
||||
// HasWhereCondition returns whether the query has a "where" condition set.
|
||||
func (q *Query) HasWhereCondition() bool {
|
||||
return q.where != nil
|
||||
}
|
||||
|
||||
// MatchesRecord checks whether the query matches the supplied database record (value only).
|
||||
func (q *Query) MatchesRecord(r record.Record) bool {
|
||||
if q.where == nil {
|
||||
|
|
|
@ -102,7 +102,7 @@ func (b *Base) SetMeta(meta *Meta) {
|
|||
b.meta = meta
|
||||
}
|
||||
|
||||
// Marshal marshals the object, without the database key or metadata. It returns nil if the record is deleted.
|
||||
// Marshal marshals the format and data.
|
||||
func (b *Base) Marshal(self Record, format uint8) ([]byte, error) {
|
||||
if b.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
|
@ -119,7 +119,20 @@ func (b *Base) Marshal(self Record, format uint8) ([]byte, error) {
|
|||
return dumped, nil
|
||||
}
|
||||
|
||||
// MarshalRecord packs the object, including metadata, into a byte array for saving in a database.
|
||||
// MarshalDataOnly marshals the data only.
|
||||
func (b *Base) MarshalDataOnly(self Record, format uint8) ([]byte, error) {
|
||||
if b.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
}
|
||||
|
||||
if b.Meta().Deleted > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return dsd.DumpWithoutIdentifier(self, format, "")
|
||||
}
|
||||
|
||||
// MarshalRecord marshals the data, format and metadata.
|
||||
func (b *Base) MarshalRecord(self Record) ([]byte, error) {
|
||||
if b.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
|
|
|
@ -49,11 +49,21 @@ func (m *Meta) MakeCrownJewel() {
|
|||
m.cronjewel = true
|
||||
}
|
||||
|
||||
// IsCrownJewel returns whether the database record is marked as a crownjewel.
|
||||
func (m *Meta) IsCrownJewel() bool {
|
||||
return m.cronjewel
|
||||
}
|
||||
|
||||
// MakeSecret sets the database record as secret, meaning that it may only be used internally, and not by interfacing processes, such as the UI.
|
||||
func (m *Meta) MakeSecret() {
|
||||
m.secret = true
|
||||
}
|
||||
|
||||
// IsSecret returns whether the database record is marked as a secret.
|
||||
func (m *Meta) IsSecret() bool {
|
||||
return m.secret
|
||||
}
|
||||
|
||||
// Update updates the internal meta states and should be called before writing the record to the database.
|
||||
func (m *Meta) Update() {
|
||||
now := time.Now().Unix()
|
||||
|
|
|
@ -20,6 +20,7 @@ type Record interface {
|
|||
|
||||
// Serialization.
|
||||
Marshal(self Record, format uint8) ([]byte, error)
|
||||
MarshalDataOnly(self Record, format uint8) ([]byte, error)
|
||||
MarshalRecord(self Record) ([]byte, error)
|
||||
GetAccessor(self Record) accessor.Accessor
|
||||
|
||||
|
|
|
@ -79,7 +79,21 @@ func NewWrapper(key string, meta *Meta, format uint8, data []byte) (*Wrapper, er
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Marshal marshals the object, without the database key or metadata.
|
||||
// NewWrapperFromDatabase returns a new record wrapper for the given data.
|
||||
func NewWrapperFromDatabase(dbName, dbKey string, meta *Meta, format uint8, data []byte) (*Wrapper, error) {
|
||||
return &Wrapper{
|
||||
Base{
|
||||
dbName: dbName,
|
||||
dbKey: dbKey,
|
||||
meta: meta,
|
||||
},
|
||||
sync.Mutex{},
|
||||
format,
|
||||
data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Marshal marshals the format and data.
|
||||
func (w *Wrapper) Marshal(r Record, format uint8) ([]byte, error) {
|
||||
if w.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
|
@ -100,7 +114,24 @@ func (w *Wrapper) Marshal(r Record, format uint8) ([]byte, error) {
|
|||
return data, nil
|
||||
}
|
||||
|
||||
// MarshalRecord packs the object, including metadata, into a byte array for saving in a database.
|
||||
// MarshalDataOnly marshals the data only.
|
||||
func (w *Wrapper) MarshalDataOnly(self Record, format uint8) ([]byte, error) {
|
||||
if w.Meta() == nil {
|
||||
return nil, errors.New("missing meta")
|
||||
}
|
||||
|
||||
if w.Meta().Deleted > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if format != dsd.AUTO && format != w.Format {
|
||||
return nil, errors.New("could not dump model, wrapped object format mismatch")
|
||||
}
|
||||
|
||||
return w.Data, nil
|
||||
}
|
||||
|
||||
// MarshalRecord marshals the data, format and metadata.
|
||||
func (w *Wrapper) MarshalRecord(r Record) ([]byte, error) {
|
||||
// Duplication necessary, as the version from Base would call Base.Marshal instead of Wrapper.Marshal
|
||||
|
||||
|
|
|
@ -288,10 +288,10 @@ func writeFile(filename string, data []byte, perm os.FileMode) error {
|
|||
defer t.Cleanup() //nolint:errcheck
|
||||
|
||||
// Set permissions before writing data, in case the data is sensitive.
|
||||
if !onWindows {
|
||||
if err := t.Chmod(perm); err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(vladimir): to set permissions on windows we need the full path of the file.
|
||||
err = t.Chmod(perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := t.Write(data); err != nil {
|
||||
|
|
|
@ -46,3 +46,8 @@ type Batcher interface {
|
|||
type Purger interface {
|
||||
Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error)
|
||||
}
|
||||
|
||||
// PurgeOlderThan defines the database storage API for backends that support the PurgeOlderThan operation.
|
||||
type PurgeOlderThan interface {
|
||||
PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal, shadowDelete bool) (int, error)
|
||||
}
|
||||
|
|
6
base/database/storage/sqlite/bobgen.yaml
Normal file
6
base/database/storage/sqlite/bobgen.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
sqlite:
|
||||
dsn: "testdata/schema.db"
|
||||
except:
|
||||
migrations:
|
||||
|
||||
no_factory: true
|
7
base/database/storage/sqlite/migrations/0_settings.sql
Normal file
7
base/database/storage/sqlite/migrations/0_settings.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- +migrate Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
PRAGMA auto_vacuum = INCREMENTAL; -- https://sqlite.org/pragma.html#pragma_auto_vacuum
|
||||
|
||||
-- +migrate Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
PRAGMA auto_vacuum = NONE; -- https://sqlite.org/pragma.html#pragma_auto_vacuum
|
19
base/database/storage/sqlite/migrations/1_initial.sql
Normal file
19
base/database/storage/sqlite/migrations/1_initial.sql
Normal file
|
@ -0,0 +1,19 @@
|
|||
-- +migrate Up
|
||||
-- SQL in section 'Up' is executed when this migration is applied
|
||||
CREATE TABLE records (
|
||||
key TEXT PRIMARY KEY,
|
||||
|
||||
format SMALLINT,
|
||||
value BLOB,
|
||||
|
||||
created BIGINT NOT NULL,
|
||||
modified BIGINT NOT NULL,
|
||||
expires BIGINT DEFAULT 0 NOT NULL,
|
||||
deleted BIGINT DEFAULT 0 NOT NULL,
|
||||
secret BOOLEAN DEFAULT FALSE NOT NULL,
|
||||
crownjewel BOOLEAN DEFAULT FALSE NOT NULL
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
-- SQL section 'Down' is executed when this migration is rolled back
|
||||
DROP TABLE records;
|
5
base/database/storage/sqlite/migrations_config.yml
Normal file
5
base/database/storage/sqlite/migrations_config.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
development:
|
||||
dialect: sqlite3
|
||||
datasource: testdata/schema.db
|
||||
dir: migrations
|
||||
table: migrations
|
131
base/database/storage/sqlite/models/bob_main.bob.go
Normal file
131
base/database/storage/sqlite/models/bob_main.bob.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT.
|
||||
// This file is meant to be re-generated in place and/or deleted at any time.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"hash/maphash"
|
||||
"strings"
|
||||
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/clause"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
||||
sqliteDriver "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
var TableNames = struct {
|
||||
Records string
|
||||
}{
|
||||
Records: "records",
|
||||
}
|
||||
|
||||
var ColumnNames = struct {
|
||||
Records recordColumnNames
|
||||
}{
|
||||
Records: recordColumnNames{
|
||||
Key: "key",
|
||||
Format: "format",
|
||||
Value: "value",
|
||||
Created: "created",
|
||||
Modified: "modified",
|
||||
Expires: "expires",
|
||||
Deleted: "deleted",
|
||||
Secret: "secret",
|
||||
Crownjewel: "crownjewel",
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
SelectWhere = Where[*dialect.SelectQuery]()
|
||||
InsertWhere = Where[*dialect.InsertQuery]()
|
||||
UpdateWhere = Where[*dialect.UpdateQuery]()
|
||||
DeleteWhere = Where[*dialect.DeleteQuery]()
|
||||
)
|
||||
|
||||
func Where[Q sqlite.Filterable]() struct {
|
||||
Records recordWhere[Q]
|
||||
} {
|
||||
return struct {
|
||||
Records recordWhere[Q]
|
||||
}{
|
||||
Records: buildRecordWhere[Q](RecordColumns),
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
SelectJoins = getJoins[*dialect.SelectQuery]
|
||||
UpdateJoins = getJoins[*dialect.UpdateQuery]
|
||||
)
|
||||
|
||||
type joinSet[Q interface{ aliasedAs(string) Q }] struct {
|
||||
InnerJoin Q
|
||||
LeftJoin Q
|
||||
RightJoin Q
|
||||
}
|
||||
|
||||
func (j joinSet[Q]) AliasedAs(alias string) joinSet[Q] {
|
||||
return joinSet[Q]{
|
||||
InnerJoin: j.InnerJoin.aliasedAs(alias),
|
||||
LeftJoin: j.LeftJoin.aliasedAs(alias),
|
||||
RightJoin: j.RightJoin.aliasedAs(alias),
|
||||
}
|
||||
}
|
||||
|
||||
type joins[Q dialect.Joinable] struct{}
|
||||
|
||||
func buildJoinSet[Q interface{ aliasedAs(string) Q }, C any, F func(C, string) Q](c C, f F) joinSet[Q] {
|
||||
return joinSet[Q]{
|
||||
InnerJoin: f(c, clause.InnerJoin),
|
||||
LeftJoin: f(c, clause.LeftJoin),
|
||||
RightJoin: f(c, clause.RightJoin),
|
||||
}
|
||||
}
|
||||
|
||||
func getJoins[Q dialect.Joinable]() joins[Q] {
|
||||
return joins[Q]{}
|
||||
}
|
||||
|
||||
type modAs[Q any, C interface{ AliasedAs(string) C }] struct {
|
||||
c C
|
||||
f func(C) bob.Mod[Q]
|
||||
}
|
||||
|
||||
func (m modAs[Q, C]) Apply(q Q) {
|
||||
m.f(m.c).Apply(q)
|
||||
}
|
||||
|
||||
func (m modAs[Q, C]) AliasedAs(alias string) bob.Mod[Q] {
|
||||
m.c = m.c.AliasedAs(alias)
|
||||
return m
|
||||
}
|
||||
|
||||
func randInt() int64 {
|
||||
out := int64(new(maphash.Hash).Sum64())
|
||||
|
||||
if out < 0 {
|
||||
return -out % 10000
|
||||
}
|
||||
|
||||
return out % 10000
|
||||
}
|
||||
|
||||
// ErrUniqueConstraint captures all unique constraint errors by explicitly leaving `s` empty.
|
||||
var ErrUniqueConstraint = &UniqueConstraintError{s: ""}
|
||||
|
||||
type UniqueConstraintError struct {
|
||||
// s is a string uniquely identifying the constraint in the raw error message returned from the database.
|
||||
s string
|
||||
}
|
||||
|
||||
func (e *UniqueConstraintError) Error() string {
|
||||
return e.s
|
||||
}
|
||||
|
||||
func (e *UniqueConstraintError) Is(target error) bool {
|
||||
err, ok := target.(*sqliteDriver.Error)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return err.Code() == 2067 && strings.Contains(err.Error(), e.s)
|
||||
}
|
9
base/database/storage/sqlite/models/bob_main_test.bob.go
Normal file
9
base/database/storage/sqlite/models/bob_main_test.bob.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT.
|
||||
// This file is meant to be re-generated in place and/or deleted at any time.
|
||||
|
||||
package models
|
||||
|
||||
import "github.com/stephenafamo/bob"
|
||||
|
||||
// Make sure the type Record runs hooks after queries
|
||||
var _ bob.HookableType = &Record{}
|
555
base/database/storage/sqlite/models/records.bob.go
Normal file
555
base/database/storage/sqlite/models/records.bob.go
Normal file
|
@ -0,0 +1,555 @@
|
|||
// Code generated by BobGen sqlite v0.30.0. DO NOT EDIT.
|
||||
// This file is meant to be re-generated in place and/or deleted at any time.
|
||||
|
||||
package models
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/aarondl/opt/null"
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/dialect"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/dm"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/sm"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/um"
|
||||
"github.com/stephenafamo/bob/expr"
|
||||
)
|
||||
|
||||
// Record is an object representing the database table.
|
||||
type Record struct {
|
||||
Key string `db:"key,pk" `
|
||||
Format null.Val[int16] `db:"format" `
|
||||
Value null.Val[[]byte] `db:"value" `
|
||||
Created int64 `db:"created" `
|
||||
Modified int64 `db:"modified" `
|
||||
Expires int64 `db:"expires" `
|
||||
Deleted int64 `db:"deleted" `
|
||||
Secret bool `db:"secret" `
|
||||
Crownjewel bool `db:"crownjewel" `
|
||||
}
|
||||
|
||||
// RecordSlice is an alias for a slice of pointers to Record.
|
||||
// This should almost always be used instead of []*Record.
|
||||
type RecordSlice []*Record
|
||||
|
||||
// Records contains methods to work with the records table
|
||||
var Records = sqlite.NewTablex[*Record, RecordSlice, *RecordSetter]("", "records")
|
||||
|
||||
// RecordsQuery is a query on the records table
|
||||
type RecordsQuery = *sqlite.ViewQuery[*Record, RecordSlice]
|
||||
|
||||
type recordColumnNames struct {
|
||||
Key string
|
||||
Format string
|
||||
Value string
|
||||
Created string
|
||||
Modified string
|
||||
Expires string
|
||||
Deleted string
|
||||
Secret string
|
||||
Crownjewel string
|
||||
}
|
||||
|
||||
var RecordColumns = buildRecordColumns("records")
|
||||
|
||||
type recordColumns struct {
|
||||
tableAlias string
|
||||
Key sqlite.Expression
|
||||
Format sqlite.Expression
|
||||
Value sqlite.Expression
|
||||
Created sqlite.Expression
|
||||
Modified sqlite.Expression
|
||||
Expires sqlite.Expression
|
||||
Deleted sqlite.Expression
|
||||
Secret sqlite.Expression
|
||||
Crownjewel sqlite.Expression
|
||||
}
|
||||
|
||||
func (c recordColumns) Alias() string {
|
||||
return c.tableAlias
|
||||
}
|
||||
|
||||
func (recordColumns) AliasedAs(alias string) recordColumns {
|
||||
return buildRecordColumns(alias)
|
||||
}
|
||||
|
||||
func buildRecordColumns(alias string) recordColumns {
|
||||
return recordColumns{
|
||||
tableAlias: alias,
|
||||
Key: sqlite.Quote(alias, "key"),
|
||||
Format: sqlite.Quote(alias, "format"),
|
||||
Value: sqlite.Quote(alias, "value"),
|
||||
Created: sqlite.Quote(alias, "created"),
|
||||
Modified: sqlite.Quote(alias, "modified"),
|
||||
Expires: sqlite.Quote(alias, "expires"),
|
||||
Deleted: sqlite.Quote(alias, "deleted"),
|
||||
Secret: sqlite.Quote(alias, "secret"),
|
||||
Crownjewel: sqlite.Quote(alias, "crownjewel"),
|
||||
}
|
||||
}
|
||||
|
||||
type recordWhere[Q sqlite.Filterable] struct {
|
||||
Key sqlite.WhereMod[Q, string]
|
||||
Format sqlite.WhereNullMod[Q, int16]
|
||||
Value sqlite.WhereNullMod[Q, []byte]
|
||||
Created sqlite.WhereMod[Q, int64]
|
||||
Modified sqlite.WhereMod[Q, int64]
|
||||
Expires sqlite.WhereMod[Q, int64]
|
||||
Deleted sqlite.WhereMod[Q, int64]
|
||||
Secret sqlite.WhereMod[Q, bool]
|
||||
Crownjewel sqlite.WhereMod[Q, bool]
|
||||
}
|
||||
|
||||
func (recordWhere[Q]) AliasedAs(alias string) recordWhere[Q] {
|
||||
return buildRecordWhere[Q](buildRecordColumns(alias))
|
||||
}
|
||||
|
||||
func buildRecordWhere[Q sqlite.Filterable](cols recordColumns) recordWhere[Q] {
|
||||
return recordWhere[Q]{
|
||||
Key: sqlite.Where[Q, string](cols.Key),
|
||||
Format: sqlite.WhereNull[Q, int16](cols.Format),
|
||||
Value: sqlite.WhereNull[Q, []byte](cols.Value),
|
||||
Created: sqlite.Where[Q, int64](cols.Created),
|
||||
Modified: sqlite.Where[Q, int64](cols.Modified),
|
||||
Expires: sqlite.Where[Q, int64](cols.Expires),
|
||||
Deleted: sqlite.Where[Q, int64](cols.Deleted),
|
||||
Secret: sqlite.Where[Q, bool](cols.Secret),
|
||||
Crownjewel: sqlite.Where[Q, bool](cols.Crownjewel),
|
||||
}
|
||||
}
|
||||
|
||||
// RecordSetter is used for insert/upsert/update operations
|
||||
// All values are optional, and do not have to be set
|
||||
// Generated columns are not included
|
||||
type RecordSetter struct {
|
||||
Key omit.Val[string] `db:"key,pk" `
|
||||
Format omitnull.Val[int16] `db:"format" `
|
||||
Value omitnull.Val[[]byte] `db:"value" `
|
||||
Created omit.Val[int64] `db:"created" `
|
||||
Modified omit.Val[int64] `db:"modified" `
|
||||
Expires omit.Val[int64] `db:"expires" `
|
||||
Deleted omit.Val[int64] `db:"deleted" `
|
||||
Secret omit.Val[bool] `db:"secret" `
|
||||
Crownjewel omit.Val[bool] `db:"crownjewel" `
|
||||
}
|
||||
|
||||
func (s RecordSetter) SetColumns() []string {
|
||||
vals := make([]string, 0, 9)
|
||||
if !s.Key.IsUnset() {
|
||||
vals = append(vals, "key")
|
||||
}
|
||||
|
||||
if !s.Format.IsUnset() {
|
||||
vals = append(vals, "format")
|
||||
}
|
||||
|
||||
if !s.Value.IsUnset() {
|
||||
vals = append(vals, "value")
|
||||
}
|
||||
|
||||
if !s.Created.IsUnset() {
|
||||
vals = append(vals, "created")
|
||||
}
|
||||
|
||||
if !s.Modified.IsUnset() {
|
||||
vals = append(vals, "modified")
|
||||
}
|
||||
|
||||
if !s.Expires.IsUnset() {
|
||||
vals = append(vals, "expires")
|
||||
}
|
||||
|
||||
if !s.Deleted.IsUnset() {
|
||||
vals = append(vals, "deleted")
|
||||
}
|
||||
|
||||
if !s.Secret.IsUnset() {
|
||||
vals = append(vals, "secret")
|
||||
}
|
||||
|
||||
if !s.Crownjewel.IsUnset() {
|
||||
vals = append(vals, "crownjewel")
|
||||
}
|
||||
|
||||
return vals
|
||||
}
|
||||
|
||||
func (s RecordSetter) Overwrite(t *Record) {
|
||||
if !s.Key.IsUnset() {
|
||||
t.Key, _ = s.Key.Get()
|
||||
}
|
||||
if !s.Format.IsUnset() {
|
||||
t.Format, _ = s.Format.GetNull()
|
||||
}
|
||||
if !s.Value.IsUnset() {
|
||||
t.Value, _ = s.Value.GetNull()
|
||||
}
|
||||
if !s.Created.IsUnset() {
|
||||
t.Created, _ = s.Created.Get()
|
||||
}
|
||||
if !s.Modified.IsUnset() {
|
||||
t.Modified, _ = s.Modified.Get()
|
||||
}
|
||||
if !s.Expires.IsUnset() {
|
||||
t.Expires, _ = s.Expires.Get()
|
||||
}
|
||||
if !s.Deleted.IsUnset() {
|
||||
t.Deleted, _ = s.Deleted.Get()
|
||||
}
|
||||
if !s.Secret.IsUnset() {
|
||||
t.Secret, _ = s.Secret.Get()
|
||||
}
|
||||
if !s.Crownjewel.IsUnset() {
|
||||
t.Crownjewel, _ = s.Crownjewel.Get()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *RecordSetter) Apply(q *dialect.InsertQuery) {
|
||||
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
|
||||
return Records.BeforeInsertHooks.RunHooks(ctx, exec, s)
|
||||
})
|
||||
|
||||
if len(q.Table.Columns) == 0 {
|
||||
q.Table.Columns = s.SetColumns()
|
||||
}
|
||||
|
||||
q.AppendValues(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
|
||||
vals := make([]bob.Expression, 0, 9)
|
||||
if !s.Key.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Key))
|
||||
}
|
||||
|
||||
if !s.Format.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Format))
|
||||
}
|
||||
|
||||
if !s.Value.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Value))
|
||||
}
|
||||
|
||||
if !s.Created.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Created))
|
||||
}
|
||||
|
||||
if !s.Modified.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Modified))
|
||||
}
|
||||
|
||||
if !s.Expires.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Expires))
|
||||
}
|
||||
|
||||
if !s.Deleted.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Deleted))
|
||||
}
|
||||
|
||||
if !s.Secret.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Secret))
|
||||
}
|
||||
|
||||
if !s.Crownjewel.IsUnset() {
|
||||
vals = append(vals, sqlite.Arg(s.Crownjewel))
|
||||
}
|
||||
|
||||
return bob.ExpressSlice(ctx, w, d, start, vals, "", ", ", "")
|
||||
}))
|
||||
}
|
||||
|
||||
func (s RecordSetter) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
|
||||
return um.Set(s.Expressions()...)
|
||||
}
|
||||
|
||||
func (s RecordSetter) Expressions(prefix ...string) []bob.Expression {
|
||||
exprs := make([]bob.Expression, 0, 9)
|
||||
|
||||
if !s.Key.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "key")...),
|
||||
sqlite.Arg(s.Key),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Format.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "format")...),
|
||||
sqlite.Arg(s.Format),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Value.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "value")...),
|
||||
sqlite.Arg(s.Value),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Created.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "created")...),
|
||||
sqlite.Arg(s.Created),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Modified.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "modified")...),
|
||||
sqlite.Arg(s.Modified),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Expires.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "expires")...),
|
||||
sqlite.Arg(s.Expires),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Deleted.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "deleted")...),
|
||||
sqlite.Arg(s.Deleted),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Secret.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "secret")...),
|
||||
sqlite.Arg(s.Secret),
|
||||
}})
|
||||
}
|
||||
|
||||
if !s.Crownjewel.IsUnset() {
|
||||
exprs = append(exprs, expr.Join{Sep: " = ", Exprs: []bob.Expression{
|
||||
sqlite.Quote(append(prefix, "crownjewel")...),
|
||||
sqlite.Arg(s.Crownjewel),
|
||||
}})
|
||||
}
|
||||
|
||||
return exprs
|
||||
}
|
||||
|
||||
// FindRecord retrieves a single record by primary key
|
||||
// If cols is empty Find will return all columns.
|
||||
func FindRecord(ctx context.Context, exec bob.Executor, KeyPK string, cols ...string) (*Record, error) {
|
||||
if len(cols) == 0 {
|
||||
return Records.Query(
|
||||
SelectWhere.Records.Key.EQ(KeyPK),
|
||||
).One(ctx, exec)
|
||||
}
|
||||
|
||||
return Records.Query(
|
||||
SelectWhere.Records.Key.EQ(KeyPK),
|
||||
sm.Columns(Records.Columns().Only(cols...)),
|
||||
).One(ctx, exec)
|
||||
}
|
||||
|
||||
// RecordExists checks the presence of a single record by primary key
|
||||
func RecordExists(ctx context.Context, exec bob.Executor, KeyPK string) (bool, error) {
|
||||
return Records.Query(
|
||||
SelectWhere.Records.Key.EQ(KeyPK),
|
||||
).Exists(ctx, exec)
|
||||
}
|
||||
|
||||
// AfterQueryHook is called after Record is retrieved from the database
|
||||
func (o *Record) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
|
||||
var err error
|
||||
|
||||
switch queryType {
|
||||
case bob.QueryTypeSelect:
|
||||
ctx, err = Records.AfterSelectHooks.RunHooks(ctx, exec, RecordSlice{o})
|
||||
case bob.QueryTypeInsert:
|
||||
ctx, err = Records.AfterInsertHooks.RunHooks(ctx, exec, RecordSlice{o})
|
||||
case bob.QueryTypeUpdate:
|
||||
ctx, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, RecordSlice{o})
|
||||
case bob.QueryTypeDelete:
|
||||
ctx, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, RecordSlice{o})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// PrimaryKeyVals returns the primary key values of the Record
|
||||
func (o *Record) PrimaryKeyVals() bob.Expression {
|
||||
return sqlite.Arg(o.Key)
|
||||
}
|
||||
|
||||
func (o *Record) pkEQ() dialect.Expression {
|
||||
return sqlite.Quote("records", "key").EQ(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
|
||||
return o.PrimaryKeyVals().WriteSQL(ctx, w, d, start)
|
||||
}))
|
||||
}
|
||||
|
||||
// Update uses an executor to update the Record
|
||||
func (o *Record) Update(ctx context.Context, exec bob.Executor, s *RecordSetter) error {
|
||||
v, err := Records.Update(s.UpdateMod(), um.Where(o.pkEQ())).One(ctx, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = *v
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes a single Record record with an executor
|
||||
func (o *Record) Delete(ctx context.Context, exec bob.Executor) error {
|
||||
_, err := Records.Delete(dm.Where(o.pkEQ())).Exec(ctx, exec)
|
||||
return err
|
||||
}
|
||||
|
||||
// Reload refreshes the Record using the executor
|
||||
func (o *Record) Reload(ctx context.Context, exec bob.Executor) error {
|
||||
o2, err := Records.Query(
|
||||
SelectWhere.Records.Key.EQ(o.Key),
|
||||
).One(ctx, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*o = *o2
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AfterQueryHook is called after RecordSlice is retrieved from the database
|
||||
func (o RecordSlice) AfterQueryHook(ctx context.Context, exec bob.Executor, queryType bob.QueryType) error {
|
||||
var err error
|
||||
|
||||
switch queryType {
|
||||
case bob.QueryTypeSelect:
|
||||
ctx, err = Records.AfterSelectHooks.RunHooks(ctx, exec, o)
|
||||
case bob.QueryTypeInsert:
|
||||
ctx, err = Records.AfterInsertHooks.RunHooks(ctx, exec, o)
|
||||
case bob.QueryTypeUpdate:
|
||||
ctx, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, o)
|
||||
case bob.QueryTypeDelete:
|
||||
ctx, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, o)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (o RecordSlice) pkIN() dialect.Expression {
|
||||
if len(o) == 0 {
|
||||
return sqlite.Raw("NULL")
|
||||
}
|
||||
|
||||
return sqlite.Quote("records", "key").In(bob.ExpressionFunc(func(ctx context.Context, w io.Writer, d bob.Dialect, start int) ([]any, error) {
|
||||
pkPairs := make([]bob.Expression, len(o))
|
||||
for i, row := range o {
|
||||
pkPairs[i] = row.PrimaryKeyVals()
|
||||
}
|
||||
return bob.ExpressSlice(ctx, w, d, start, pkPairs, "", ", ", "")
|
||||
}))
|
||||
}
|
||||
|
||||
// copyMatchingRows finds models in the given slice that have the same primary key
|
||||
// then it first copies the existing relationships from the old model to the new model
|
||||
// and then replaces the old model in the slice with the new model
|
||||
func (o RecordSlice) copyMatchingRows(from ...*Record) {
|
||||
for i, old := range o {
|
||||
for _, new := range from {
|
||||
if new.Key != old.Key {
|
||||
continue
|
||||
}
|
||||
|
||||
o[i] = new
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateMod modifies an update query with "WHERE primary_key IN (o...)"
|
||||
func (o RecordSlice) UpdateMod() bob.Mod[*dialect.UpdateQuery] {
|
||||
return bob.ModFunc[*dialect.UpdateQuery](func(q *dialect.UpdateQuery) {
|
||||
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
|
||||
return Records.BeforeUpdateHooks.RunHooks(ctx, exec, o)
|
||||
})
|
||||
|
||||
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
|
||||
var err error
|
||||
switch retrieved := retrieved.(type) {
|
||||
case *Record:
|
||||
o.copyMatchingRows(retrieved)
|
||||
case []*Record:
|
||||
o.copyMatchingRows(retrieved...)
|
||||
case RecordSlice:
|
||||
o.copyMatchingRows(retrieved...)
|
||||
default:
|
||||
// If the retrieved value is not a Record or a slice of Record
|
||||
// then run the AfterUpdateHooks on the slice
|
||||
_, err = Records.AfterUpdateHooks.RunHooks(ctx, exec, o)
|
||||
}
|
||||
|
||||
return err
|
||||
}))
|
||||
|
||||
q.AppendWhere(o.pkIN())
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteMod modifies an delete query with "WHERE primary_key IN (o...)"
|
||||
func (o RecordSlice) DeleteMod() bob.Mod[*dialect.DeleteQuery] {
|
||||
return bob.ModFunc[*dialect.DeleteQuery](func(q *dialect.DeleteQuery) {
|
||||
q.AppendHooks(func(ctx context.Context, exec bob.Executor) (context.Context, error) {
|
||||
return Records.BeforeDeleteHooks.RunHooks(ctx, exec, o)
|
||||
})
|
||||
|
||||
q.AppendLoader(bob.LoaderFunc(func(ctx context.Context, exec bob.Executor, retrieved any) error {
|
||||
var err error
|
||||
switch retrieved := retrieved.(type) {
|
||||
case *Record:
|
||||
o.copyMatchingRows(retrieved)
|
||||
case []*Record:
|
||||
o.copyMatchingRows(retrieved...)
|
||||
case RecordSlice:
|
||||
o.copyMatchingRows(retrieved...)
|
||||
default:
|
||||
// If the retrieved value is not a Record or a slice of Record
|
||||
// then run the AfterDeleteHooks on the slice
|
||||
_, err = Records.AfterDeleteHooks.RunHooks(ctx, exec, o)
|
||||
}
|
||||
|
||||
return err
|
||||
}))
|
||||
|
||||
q.AppendWhere(o.pkIN())
|
||||
})
|
||||
}
|
||||
|
||||
func (o RecordSlice) UpdateAll(ctx context.Context, exec bob.Executor, vals RecordSetter) error {
|
||||
if len(o) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := Records.Update(vals.UpdateMod(), o.UpdateMod()).All(ctx, exec)
|
||||
return err
|
||||
}
|
||||
|
||||
func (o RecordSlice) DeleteAll(ctx context.Context, exec bob.Executor) error {
|
||||
if len(o) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err := Records.Delete(o.DeleteMod()).Exec(ctx, exec)
|
||||
return err
|
||||
}
|
||||
|
||||
func (o RecordSlice) ReloadAll(ctx context.Context, exec bob.Executor) error {
|
||||
if len(o) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
o2, err := Records.Query(sm.Where(o.pkIN())).All(ctx, exec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
o.copyMatchingRows(o2...)
|
||||
|
||||
return nil
|
||||
}
|
125
base/database/storage/sqlite/prepared.go
Normal file
125
base/database/storage/sqlite/prepared.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/im"
|
||||
"github.com/stephenafamo/bob/expr"
|
||||
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/database/storage/sqlite/models"
|
||||
"github.com/safing/structures/dsd"
|
||||
)
|
||||
|
||||
var UsePreparedStatements bool = true
|
||||
|
||||
// PutMany stores many records in the database.
|
||||
func (db *SQLite) putManyWithPreparedStmts(shadowDelete bool) (chan<- record.Record, <-chan error) {
|
||||
batch := make(chan record.Record, 100)
|
||||
errs := make(chan error, 1)
|
||||
|
||||
// Simulate upsert with custom selection on conflict.
|
||||
rawQuery, _, err := models.Records.Insert(
|
||||
im.Into("records", "key", "format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"),
|
||||
im.Values(expr.Arg("key"), expr.Arg("format"), expr.Arg("value"), expr.Arg("created"), expr.Arg("modified"), expr.Arg("expires"), expr.Arg("deleted"), expr.Arg("secret"), expr.Arg("crownjewel")),
|
||||
im.OnConflict("key").DoUpdate(
|
||||
im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"),
|
||||
),
|
||||
).Build(db.ctx)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return batch, errs
|
||||
}
|
||||
|
||||
// Start transaction.
|
||||
tx, err := db.bob.BeginTx(db.ctx, nil)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return batch, errs
|
||||
}
|
||||
|
||||
// Create prepared statement WITHIN TRANSACTION.
|
||||
preparedStmt, err := tx.PrepareContext(db.ctx, rawQuery)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return batch, errs
|
||||
}
|
||||
|
||||
// start handler
|
||||
go func() {
|
||||
// Read all put records.
|
||||
writeBatch:
|
||||
for {
|
||||
select {
|
||||
case r := <-batch:
|
||||
if r != nil {
|
||||
// Write record.
|
||||
err := writeWithPreparedStatement(db.ctx, &preparedStmt, r)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
break writeBatch
|
||||
}
|
||||
} else {
|
||||
// Finalize transcation.
|
||||
errs <- tx.Commit()
|
||||
return
|
||||
}
|
||||
|
||||
case <-db.ctx.Done():
|
||||
break writeBatch
|
||||
}
|
||||
}
|
||||
|
||||
// Rollback transaction.
|
||||
errs <- tx.Rollback()
|
||||
}()
|
||||
|
||||
return batch, errs
|
||||
}
|
||||
|
||||
func writeWithPreparedStatement(ctx context.Context, pStmt *bob.StdPrepared, r record.Record) error {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
// Serialize to JSON.
|
||||
data, err := r.MarshalDataOnly(r, dsd.JSON)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get Meta.
|
||||
m := r.Meta()
|
||||
|
||||
// Insert.
|
||||
if len(data) > 0 {
|
||||
format := strconv.Itoa(dsd.JSON)
|
||||
_, err = pStmt.ExecContext(
|
||||
ctx,
|
||||
r.DatabaseKey(),
|
||||
format,
|
||||
data,
|
||||
m.Created,
|
||||
m.Modified,
|
||||
m.Expires,
|
||||
m.Deleted,
|
||||
m.IsSecret(),
|
||||
m.IsCrownJewel(),
|
||||
)
|
||||
} else {
|
||||
_, err = pStmt.ExecContext(
|
||||
ctx,
|
||||
r.DatabaseKey(),
|
||||
nil,
|
||||
nil,
|
||||
m.Created,
|
||||
m.Modified,
|
||||
m.Expires,
|
||||
m.Deleted,
|
||||
m.IsSecret(),
|
||||
m.IsCrownJewel(),
|
||||
)
|
||||
}
|
||||
return err
|
||||
}
|
86
base/database/storage/sqlite/prepared_test.go
Normal file
86
base/database/storage/sqlite/prepared_test.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkPutMany(b *testing.B) {
|
||||
// Configure prepared statement usage.
|
||||
origSetting := UsePreparedStatements
|
||||
UsePreparedStatements = false
|
||||
defer func() {
|
||||
UsePreparedStatements = origSetting
|
||||
}()
|
||||
|
||||
// Run benchmark.
|
||||
benchPutMany(b)
|
||||
}
|
||||
|
||||
func BenchmarkPutManyPreparedStmt(b *testing.B) {
|
||||
// Configure prepared statement usage.
|
||||
origSetting := UsePreparedStatements
|
||||
UsePreparedStatements = true
|
||||
defer func() {
|
||||
UsePreparedStatements = origSetting
|
||||
}()
|
||||
|
||||
// Run benchmark.
|
||||
benchPutMany(b)
|
||||
}
|
||||
|
||||
func benchPutMany(b *testing.B) { //nolint:thelper
|
||||
// Start database.
|
||||
testDir := b.TempDir()
|
||||
db, err := openSQLite("test", testDir, false)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
// shutdown
|
||||
err = db.Shutdown()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start benchmarking.
|
||||
b.ResetTimer()
|
||||
|
||||
// Benchmark PutMany.
|
||||
records, errs := db.PutMany(false)
|
||||
for i := range b.N {
|
||||
// Create test record.
|
||||
newTestRecord := &TestRecord{
|
||||
S: "banana",
|
||||
I: 42,
|
||||
I8: 42,
|
||||
I16: 42,
|
||||
I32: 42,
|
||||
I64: 42,
|
||||
UI: 42,
|
||||
UI8: 42,
|
||||
UI16: 42,
|
||||
UI32: 42,
|
||||
UI64: 42,
|
||||
F32: 42.42,
|
||||
F64: 42.42,
|
||||
B: true,
|
||||
}
|
||||
newTestRecord.UpdateMeta()
|
||||
newTestRecord.SetKey("test:" + strconv.Itoa(i))
|
||||
|
||||
select {
|
||||
case records <- newTestRecord:
|
||||
case err := <-errs:
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize.
|
||||
close(records)
|
||||
err = <-errs
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
51
base/database/storage/sqlite/schema.go
Normal file
51
base/database/storage/sqlite/schema.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package sqlite
|
||||
|
||||
// Base command for sql-migrate:
|
||||
//go:generate -command migrate go tool github.com/rubenv/sql-migrate/sql-migrate
|
||||
|
||||
// Run missing migrations:
|
||||
//go:generate migrate up --config=migrations_config.yml
|
||||
|
||||
// Redo last migration:
|
||||
// x go:generate migrate redo --config=migrations_config.yml
|
||||
|
||||
// Undo all migrations:
|
||||
// x go:generate migrate down --config=migrations_config.yml
|
||||
|
||||
// Generate models with bob:
|
||||
//go:generate go tool github.com/stephenafamo/bob/gen/bobgen-sqlite
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/database/storage/sqlite/models"
|
||||
)
|
||||
|
||||
//go:embed migrations/*
|
||||
var dbMigrations embed.FS
|
||||
|
||||
func getMigrations() migrate.EmbedFileSystemMigrationSource {
|
||||
return migrate.EmbedFileSystemMigrationSource{
|
||||
FileSystem: dbMigrations,
|
||||
Root: "migrations",
|
||||
}
|
||||
}
|
||||
|
||||
func getMeta(r *models.Record) *record.Meta {
|
||||
meta := &record.Meta{
|
||||
Created: r.Created,
|
||||
Modified: r.Modified,
|
||||
Expires: r.Expires,
|
||||
Deleted: r.Deleted,
|
||||
}
|
||||
if r.Secret {
|
||||
meta.MakeSecret()
|
||||
}
|
||||
if r.Crownjewel {
|
||||
meta.MakeCrownJewel()
|
||||
}
|
||||
return meta
|
||||
}
|
566
base/database/storage/sqlite/sqlite.go
Normal file
566
base/database/storage/sqlite/sqlite.go
Normal file
|
@ -0,0 +1,566 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aarondl/opt/omit"
|
||||
"github.com/aarondl/opt/omitnull"
|
||||
migrate "github.com/rubenv/sql-migrate"
|
||||
sqldblogger "github.com/simukti/sqldb-logger"
|
||||
"github.com/stephenafamo/bob"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/im"
|
||||
"github.com/stephenafamo/bob/dialect/sqlite/um"
|
||||
_ "modernc.org/sqlite"
|
||||
|
||||
"github.com/safing/portmaster/base/database/accessor"
|
||||
"github.com/safing/portmaster/base/database/iterator"
|
||||
"github.com/safing/portmaster/base/database/query"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/database/storage"
|
||||
"github.com/safing/portmaster/base/database/storage/sqlite/models"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/structures/dsd"
|
||||
)
|
||||
|
||||
// Errors.
|
||||
var (
|
||||
ErrQueryTimeout = errors.New("query timeout")
|
||||
)
|
||||
|
||||
// SQLite storage.
|
||||
type SQLite struct {
|
||||
name string
|
||||
|
||||
db *sql.DB
|
||||
bob bob.DB
|
||||
wg sync.WaitGroup
|
||||
|
||||
ctx context.Context
|
||||
cancelCtx context.CancelFunc
|
||||
}
|
||||
|
||||
func init() {
|
||||
_ = storage.Register("sqlite", func(name, location string) (storage.Interface, error) {
|
||||
return NewSQLite(name, location)
|
||||
})
|
||||
}
|
||||
|
||||
// NewSQLite creates a sqlite database.
|
||||
func NewSQLite(name, location string) (*SQLite, error) {
|
||||
return openSQLite(name, location, false)
|
||||
}
|
||||
|
||||
// openSQLite creates a sqlite database.
|
||||
func openSQLite(name, location string, printStmts bool) (*SQLite, error) {
|
||||
dbFile := filepath.Join(location, "db.sqlite")
|
||||
|
||||
// Open database file.
|
||||
// Default settings:
|
||||
// _time_format = YYYY-MM-DDTHH:MM:SS.SSS
|
||||
// _txlock = deferred
|
||||
db, err := sql.Open("sqlite", dbFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||
}
|
||||
|
||||
// Enable statement printing.
|
||||
if printStmts {
|
||||
db = sqldblogger.OpenDriver(dbFile, db.Driver(), &statementLogger{})
|
||||
}
|
||||
|
||||
// Set other settings.
|
||||
pragmas := []string{
|
||||
"PRAGMA journal_mode=WAL;", // Corruption safe write ahead log for txs.
|
||||
"PRAGMA synchronous=NORMAL;", // Best for WAL.
|
||||
"PRAGMA cache_size=-10000;", // 10MB Cache.
|
||||
"PRAGMA busy_timeout=3000;", // 3s (3000ms) timeout for locked tables.
|
||||
}
|
||||
for _, pragma := range pragmas {
|
||||
_, err := db.Exec(pragma)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init sqlite with %s: %w", pragma, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Run migrations on database.
|
||||
n, err := migrate.Exec(db, "sqlite3", getMigrations(), migrate.Up)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("migrate sqlite: %w", err)
|
||||
}
|
||||
log.Debugf("database/sqlite: ran %d migrations on %s database", n, name)
|
||||
|
||||
// Return as bob database.
|
||||
ctx, cancelCtx := context.WithCancel(context.Background())
|
||||
return &SQLite{
|
||||
name: name,
|
||||
db: db,
|
||||
bob: bob.NewDB(db),
|
||||
ctx: ctx,
|
||||
cancelCtx: cancelCtx,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get returns a database record.
|
||||
func (db *SQLite) Get(key string) (record.Record, error) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Get record from database.
|
||||
r, err := models.FindRecord(db.ctx, db.bob, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", storage.ErrNotFound, err)
|
||||
}
|
||||
|
||||
// Return data in wrapper.
|
||||
return record.NewWrapperFromDatabase(
|
||||
db.name,
|
||||
key,
|
||||
getMeta(r),
|
||||
uint8(r.Format.GetOrZero()), //nolint:gosec // Values are within uint8.
|
||||
r.Value.GetOrZero(),
|
||||
)
|
||||
}
|
||||
|
||||
// GetMeta returns the metadata of a database record.
|
||||
func (db *SQLite) GetMeta(key string) (*record.Meta, error) {
|
||||
r, err := db.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r.Meta(), nil
|
||||
}
|
||||
|
||||
// Put stores a record in the database.
|
||||
func (db *SQLite) Put(r record.Record) (record.Record, error) {
|
||||
return db.putRecord(r, nil)
|
||||
}
|
||||
|
||||
func (db *SQLite) putRecord(r record.Record, tx *bob.Tx) (record.Record, error) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Lock record if in a transaction.
|
||||
if tx != nil {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
}
|
||||
|
||||
// Serialize to JSON.
|
||||
data, err := r.MarshalDataOnly(r, dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Prepare for setter.
|
||||
setFormat := omitnull.From(int16(dsd.JSON))
|
||||
setData := omitnull.From(data)
|
||||
if len(data) == 0 {
|
||||
setFormat.Null()
|
||||
setData.Null()
|
||||
}
|
||||
|
||||
// Create structure for insert.
|
||||
m := r.Meta()
|
||||
setter := models.RecordSetter{
|
||||
Key: omit.From(r.DatabaseKey()),
|
||||
Format: setFormat,
|
||||
Value: setData,
|
||||
Created: omit.From(m.Created),
|
||||
Modified: omit.From(m.Modified),
|
||||
Expires: omit.From(m.Expires),
|
||||
Deleted: omit.From(m.Deleted),
|
||||
Secret: omit.From(m.IsSecret()),
|
||||
Crownjewel: omit.From(m.IsCrownJewel()),
|
||||
}
|
||||
|
||||
// Simulate upsert with custom selection on conflict.
|
||||
dbQuery := models.Records.Insert(
|
||||
&setter,
|
||||
im.OnConflict("key").DoUpdate(
|
||||
im.SetExcluded("format", "value", "created", "modified", "expires", "deleted", "secret", "crownjewel"),
|
||||
),
|
||||
)
|
||||
|
||||
// Execute in transaction or directly.
|
||||
if tx != nil {
|
||||
_, err = dbQuery.Exec(db.ctx, tx)
|
||||
} else {
|
||||
_, err = dbQuery.Exec(db.ctx, db.bob)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// PutMany stores many records in the database.
|
||||
func (db *SQLite) PutMany(shadowDelete bool) (chan<- record.Record, <-chan error) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Check if we should use prepared statement optimized inserting.
|
||||
if UsePreparedStatements {
|
||||
return db.putManyWithPreparedStmts(shadowDelete)
|
||||
}
|
||||
|
||||
batch := make(chan record.Record, 100)
|
||||
errs := make(chan error, 1)
|
||||
|
||||
tx, err := db.bob.BeginTx(db.ctx, nil)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return batch, errs
|
||||
}
|
||||
|
||||
// start handler
|
||||
go func() {
|
||||
// Read all put records.
|
||||
writeBatch:
|
||||
for {
|
||||
select {
|
||||
case r := <-batch:
|
||||
if r != nil {
|
||||
// Write record.
|
||||
_, err := db.putRecord(r, &tx)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
break writeBatch
|
||||
}
|
||||
} else {
|
||||
// Finalize transcation.
|
||||
errs <- tx.Commit()
|
||||
return
|
||||
}
|
||||
|
||||
case <-db.ctx.Done():
|
||||
break writeBatch
|
||||
}
|
||||
}
|
||||
|
||||
// Rollback transaction.
|
||||
errs <- tx.Rollback()
|
||||
}()
|
||||
|
||||
return batch, errs
|
||||
}
|
||||
|
||||
// Delete deletes a record from the database.
|
||||
func (db *SQLite) Delete(key string) error {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
toDelete := &models.Record{Key: key}
|
||||
return toDelete.Delete(db.ctx, db.bob)
|
||||
}
|
||||
|
||||
// Query returns a an iterator for the supplied query.
|
||||
func (db *SQLite) Query(q *query.Query, local, internal bool) (*iterator.Iterator, error) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
_, err := q.Check()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid query: %w", err)
|
||||
}
|
||||
|
||||
queryIter := iterator.New()
|
||||
|
||||
go db.queryExecutor(queryIter, q, local, internal)
|
||||
return queryIter, nil
|
||||
}
|
||||
|
||||
func (db *SQLite) queryExecutor(queryIter *iterator.Iterator, q *query.Query, local, internal bool) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Build query.
|
||||
var recordQuery *sqlite.ViewQuery[*models.Record, models.RecordSlice]
|
||||
if q.DatabaseKeyPrefix() != "" {
|
||||
recordQuery = models.Records.View.Query(
|
||||
models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix() + "%"),
|
||||
)
|
||||
} else {
|
||||
recordQuery = models.Records.View.Query()
|
||||
}
|
||||
|
||||
// Get cursor to go over all records in the query.
|
||||
cursor, err := models.RecordsQuery.Cursor(recordQuery, db.ctx, db.bob)
|
||||
if err != nil {
|
||||
queryIter.Finish(err)
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
_ = cursor.Close()
|
||||
}()
|
||||
|
||||
recordsLoop:
|
||||
for cursor.Next() {
|
||||
// Get next record
|
||||
r, cErr := cursor.Get()
|
||||
if cErr != nil {
|
||||
err = fmt.Errorf("cursor error: %w", cErr)
|
||||
break recordsLoop
|
||||
}
|
||||
|
||||
// Check if key matches.
|
||||
if !q.MatchesKey(r.Key) {
|
||||
continue recordsLoop
|
||||
}
|
||||
|
||||
// Check Meta.
|
||||
m := getMeta(r)
|
||||
if !m.CheckValidity() ||
|
||||
!m.CheckPermission(local, internal) {
|
||||
continue recordsLoop
|
||||
}
|
||||
|
||||
// Check Data.
|
||||
if q.HasWhereCondition() {
|
||||
if r.Format.IsNull() || r.Value.IsNull() {
|
||||
continue recordsLoop
|
||||
}
|
||||
|
||||
jsonData := string(r.Value.GetOrZero())
|
||||
jsonAccess := accessor.NewJSONAccessor(&jsonData)
|
||||
if !q.MatchesAccessor(jsonAccess) {
|
||||
continue recordsLoop
|
||||
}
|
||||
}
|
||||
|
||||
// Build database record.
|
||||
matched, _ := record.NewWrapperFromDatabase(
|
||||
db.name,
|
||||
r.Key,
|
||||
m,
|
||||
uint8(r.Format.GetOrZero()), //nolint:gosec // Values are within uint8.
|
||||
r.Value.GetOrZero(),
|
||||
)
|
||||
|
||||
select {
|
||||
case <-queryIter.Done:
|
||||
break recordsLoop
|
||||
case queryIter.Next <- matched:
|
||||
default:
|
||||
select {
|
||||
case <-queryIter.Done:
|
||||
break recordsLoop
|
||||
case queryIter.Next <- matched:
|
||||
case <-time.After(1 * time.Second):
|
||||
err = ErrQueryTimeout
|
||||
break recordsLoop
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
queryIter.Finish(err)
|
||||
}
|
||||
|
||||
// Purge deletes all records that match the given query. It returns the number of successful deletes and an error.
|
||||
func (db *SQLite) Purge(ctx context.Context, q *query.Query, local, internal, shadowDelete bool) (int, error) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Optimize for local and internal queries without where clause and without shadow delete.
|
||||
if local && internal && !shadowDelete && !q.HasWhereCondition() {
|
||||
// First count entries (SQLite does not support affected rows)
|
||||
n, err := models.Records.Query(
|
||||
models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
|
||||
).Count(db.ctx, db.bob)
|
||||
if err != nil || n == 0 {
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Delete entries.
|
||||
_, err = models.Records.Delete(
|
||||
models.DeleteWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
|
||||
).Exec(db.ctx, db.bob)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Optimize for local and internal queries without where clause, but with shadow delete.
|
||||
if local && internal && shadowDelete && !q.HasWhereCondition() {
|
||||
// First count entries (SQLite does not support affected rows)
|
||||
n, err := models.Records.Query(
|
||||
models.SelectWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
|
||||
).Count(db.ctx, db.bob)
|
||||
if err != nil || n == 0 {
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Mark purged records as deleted.
|
||||
now := time.Now().Unix()
|
||||
_, err = models.Records.Update(
|
||||
um.SetCol("format").ToArg(nil),
|
||||
um.SetCol("value").ToArg(nil),
|
||||
um.SetCol("deleted").ToArg(now),
|
||||
models.UpdateWhere.Records.Key.Like(q.DatabaseKeyPrefix()+"%"),
|
||||
).Exec(db.ctx, db.bob)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Otherwise, iterate over all entries and delete matching ones.
|
||||
|
||||
// TODO: Non-local, non-internal or content matching queries are not supported at the moment.
|
||||
return 0, storage.ErrNotImplemented
|
||||
}
|
||||
|
||||
// PurgeOlderThan deletes all records last updated before the given time. It returns the number of successful deletes and an error.
|
||||
func (db *SQLite) PurgeOlderThan(ctx context.Context, prefix string, purgeBefore time.Time, local, internal, shadowDelete bool) (int, error) {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
purgeBeforeInt := purgeBefore.Unix()
|
||||
|
||||
// Optimize for local and internal queries without where clause and without shadow delete.
|
||||
if local && internal && !shadowDelete {
|
||||
// First count entries (SQLite does not support affected rows)
|
||||
n, err := models.Records.Query(
|
||||
models.SelectWhere.Records.Key.Like(prefix+"%"),
|
||||
models.SelectWhere.Records.Modified.LT(purgeBeforeInt),
|
||||
).Count(db.ctx, db.bob)
|
||||
if err != nil || n == 0 {
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Delete entries.
|
||||
_, err = models.Records.Delete(
|
||||
models.DeleteWhere.Records.Key.Like(prefix+"%"),
|
||||
models.DeleteWhere.Records.Modified.LT(purgeBeforeInt),
|
||||
).Exec(db.ctx, db.bob)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Optimize for local and internal queries without where clause, but with shadow delete.
|
||||
if local && internal && shadowDelete {
|
||||
// First count entries (SQLite does not support affected rows)
|
||||
n, err := models.Records.Query(
|
||||
models.SelectWhere.Records.Key.Like(prefix+"%"),
|
||||
models.SelectWhere.Records.Modified.LT(purgeBeforeInt),
|
||||
).Count(db.ctx, db.bob)
|
||||
if err != nil || n == 0 {
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// Mark purged records as deleted.
|
||||
now := time.Now().Unix()
|
||||
_, err = models.Records.Update(
|
||||
um.SetCol("format").ToArg(nil),
|
||||
um.SetCol("value").ToArg(nil),
|
||||
um.SetCol("deleted").ToArg(now),
|
||||
models.UpdateWhere.Records.Key.Like(prefix+"%"),
|
||||
models.UpdateWhere.Records.Modified.LT(purgeBeforeInt),
|
||||
).Exec(db.ctx, db.bob)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// TODO: Non-local or non-internal queries are not supported at the moment.
|
||||
return 0, storage.ErrNotImplemented
|
||||
}
|
||||
|
||||
// ReadOnly returns whether the database is read only.
|
||||
func (db *SQLite) ReadOnly() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Injected returns whether the database is injected.
|
||||
func (db *SQLite) Injected() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// MaintainRecordStates maintains records states in the database.
|
||||
func (db *SQLite) MaintainRecordStates(ctx context.Context, purgeDeletedBefore time.Time, shadowDelete bool) error {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
now := time.Now().Unix()
|
||||
purgeThreshold := purgeDeletedBefore.Unix()
|
||||
|
||||
// Option 1: Using shadow delete.
|
||||
if shadowDelete {
|
||||
// Mark expired records as deleted.
|
||||
_, err := models.Records.Update(
|
||||
um.SetCol("format").ToArg(nil),
|
||||
um.SetCol("value").ToArg(nil),
|
||||
um.SetCol("deleted").ToArg(now),
|
||||
models.UpdateWhere.Records.Deleted.EQ(0),
|
||||
models.UpdateWhere.Records.Expires.GT(0),
|
||||
models.UpdateWhere.Records.Expires.LT(now),
|
||||
).Exec(db.ctx, db.bob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to shadow delete expired records: %w", err)
|
||||
}
|
||||
|
||||
// Purge deleted records before threshold.
|
||||
_, err = models.Records.Delete(
|
||||
models.DeleteWhere.Records.Deleted.GT(0),
|
||||
models.DeleteWhere.Records.Deleted.LT(purgeThreshold),
|
||||
).Exec(db.ctx, db.bob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to purge deleted records (before threshold): %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Option 2: Immediate delete.
|
||||
|
||||
// Delete expired record.
|
||||
_, err := models.Records.Delete(
|
||||
models.DeleteWhere.Records.Expires.GT(0),
|
||||
models.DeleteWhere.Records.Expires.LT(now),
|
||||
).Exec(db.ctx, db.bob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete expired records: %w", err)
|
||||
}
|
||||
|
||||
// Delete shadow deleted records.
|
||||
_, err = models.Records.Delete(
|
||||
models.DeleteWhere.Records.Deleted.GT(0),
|
||||
).Exec(db.ctx, db.bob)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to purge deleted records: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *SQLite) Maintain(ctx context.Context) error {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Remove up to about 100KB of SQLite pages from the freelist on every run.
|
||||
// (Assuming 4KB page size.)
|
||||
_, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum(25);")
|
||||
return err
|
||||
}
|
||||
|
||||
func (db *SQLite) MaintainThorough(ctx context.Context) error {
|
||||
db.wg.Add(1)
|
||||
defer db.wg.Done()
|
||||
|
||||
// Remove all pages from the freelist.
|
||||
_, err := db.db.ExecContext(ctx, "PRAGMA incremental_vacuum;")
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutdown shuts down the database.
|
||||
func (db *SQLite) Shutdown() error {
|
||||
db.wg.Wait()
|
||||
db.cancelCtx()
|
||||
|
||||
return db.bob.Close()
|
||||
}
|
||||
|
||||
type statementLogger struct{}
|
||||
|
||||
func (sl statementLogger) Log(ctx context.Context, level sqldblogger.Level, msg string, data map[string]interface{}) {
|
||||
fmt.Printf("SQL: %s --- %+v\n", msg, data)
|
||||
}
|
216
base/database/storage/sqlite/sqlite_test.go
Normal file
216
base/database/storage/sqlite/sqlite_test.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package sqlite
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safing/portmaster/base/database/query"
|
||||
"github.com/safing/portmaster/base/database/record"
|
||||
"github.com/safing/portmaster/base/database/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
// Compile time interface checks.
|
||||
_ storage.Interface = &SQLite{}
|
||||
_ storage.Batcher = &SQLite{}
|
||||
_ storage.Purger = &SQLite{}
|
||||
)
|
||||
|
||||
type TestRecord struct { //nolint:maligned
|
||||
record.Base
|
||||
sync.Mutex
|
||||
S string
|
||||
I int
|
||||
I8 int8
|
||||
I16 int16
|
||||
I32 int32
|
||||
I64 int64
|
||||
UI uint
|
||||
UI8 uint8
|
||||
UI16 uint16
|
||||
UI32 uint32
|
||||
UI64 uint64
|
||||
F32 float32
|
||||
F64 float64
|
||||
B bool
|
||||
}
|
||||
|
||||
func TestSQLite(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// start
|
||||
testDir := t.TempDir()
|
||||
db, err := openSQLite("test", testDir, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
// shutdown
|
||||
err = db.Shutdown()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
|
||||
a := &TestRecord{
|
||||
S: "banana",
|
||||
I: 42,
|
||||
I8: 42,
|
||||
I16: 42,
|
||||
I32: 42,
|
||||
I64: 42,
|
||||
UI: 42,
|
||||
UI8: 42,
|
||||
UI16: 42,
|
||||
UI32: 42,
|
||||
UI64: 42,
|
||||
F32: 42.42,
|
||||
F64: 42.42,
|
||||
B: true,
|
||||
}
|
||||
a.SetMeta(&record.Meta{})
|
||||
a.Meta().Update()
|
||||
a.SetKey("test:A")
|
||||
|
||||
// put record
|
||||
_, err = db.Put(a)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// get and compare
|
||||
r1, err := db.Get("A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
a1 := &TestRecord{}
|
||||
err = record.Unwrap(r1, a1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, a, a1, "struct must match")
|
||||
|
||||
// setup query test records
|
||||
qA := &TestRecord{}
|
||||
qA.SetKey("test:path/to/A")
|
||||
qA.UpdateMeta()
|
||||
|
||||
qB := &TestRecord{}
|
||||
qB.SetKey("test:path/to/B")
|
||||
qB.UpdateMeta()
|
||||
// Set creation/modification in the past.
|
||||
qB.Meta().Created = time.Now().Add(-time.Hour).Unix()
|
||||
qB.Meta().Modified = time.Now().Add(-time.Hour).Unix()
|
||||
|
||||
qC := &TestRecord{}
|
||||
qC.SetKey("test:path/to/C")
|
||||
qC.UpdateMeta()
|
||||
// Set expiry in the past.
|
||||
qC.Meta().Expires = time.Now().Add(-time.Hour).Unix()
|
||||
|
||||
qZ := &TestRecord{}
|
||||
qZ.SetKey("test:z")
|
||||
qZ.UpdateMeta()
|
||||
|
||||
put, errs := db.PutMany(false)
|
||||
put <- qA
|
||||
put <- qB
|
||||
put <- qC
|
||||
put <- qZ
|
||||
close(put)
|
||||
err = <-errs
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// test query
|
||||
q := query.New("test:path/to/").MustBeValid()
|
||||
it, err := db.Query(q, true, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cnt := 0
|
||||
for range it.Next {
|
||||
cnt++
|
||||
}
|
||||
if it.Err() != nil {
|
||||
t.Fatal(it.Err())
|
||||
}
|
||||
if cnt != 2 {
|
||||
// Note: One is expired.
|
||||
t.Fatalf("unexpected query result count: %d", cnt)
|
||||
}
|
||||
|
||||
// delete
|
||||
err = db.Delete("A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// check if its gone
|
||||
_, err = db.Get("A")
|
||||
if err == nil {
|
||||
t.Fatal("should fail")
|
||||
}
|
||||
|
||||
// purge older than
|
||||
n, err := db.PurgeOlderThan(t.Context(), "path/to/", time.Now().Add(-30*time.Minute), true, true, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("unexpected purge older than delete count: %d", n)
|
||||
}
|
||||
|
||||
// maintenance
|
||||
err = db.MaintainRecordStates(t.Context(), time.Now().Add(-time.Minute), true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// maintenance
|
||||
err = db.MaintainRecordStates(t.Context(), time.Now(), false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// purge
|
||||
n, err = db.Purge(t.Context(), query.New("test:path/to/").MustBeValid(), true, true, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("unexpected purge delete count: %d", n)
|
||||
}
|
||||
|
||||
// Maintenance
|
||||
err = db.Maintain(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("Maintain: %s", err)
|
||||
}
|
||||
err = db.MaintainThorough(t.Context())
|
||||
if err != nil {
|
||||
t.Fatalf("MaintainThorough: %s", err)
|
||||
}
|
||||
|
||||
// test query
|
||||
q = query.New("test").MustBeValid()
|
||||
it, err = db.Query(q, true, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cnt = 0
|
||||
for range it.Next {
|
||||
cnt++
|
||||
}
|
||||
if it.Err() != nil {
|
||||
t.Fatal(it.Err())
|
||||
}
|
||||
if cnt != 1 {
|
||||
t.Fatalf("unexpected query result count: %d", cnt)
|
||||
}
|
||||
}
|
0
base/database/storage/sqlite/testdata/.gitkeep
vendored
Normal file
0
base/database/storage/sqlite/testdata/.gitkeep
vendored
Normal file
|
@ -2,7 +2,6 @@ package dataroot
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
@ -10,7 +9,7 @@ import (
|
|||
var root *utils.DirStructure
|
||||
|
||||
// Initialize initializes the data root directory.
|
||||
func Initialize(rootDir string, perm os.FileMode) error {
|
||||
func Initialize(rootDir string, perm utils.FSPermission) error {
|
||||
if root != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ func Set(setName string, setVersion string, setLicenseName string) {
|
|||
|
||||
if setVersion != "" {
|
||||
version = setVersion
|
||||
versionNumber = setVersion
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/safing/jess/filesig"
|
||||
"github.com/safing/jess/lhash"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/base/utils/renameio"
|
||||
)
|
||||
|
||||
|
@ -136,12 +137,10 @@ func (reg *ResourceRegistry) fetchFile(ctx context.Context, client *http.Client,
|
|||
return fmt.Errorf("%s: failed to finalize file %s: %w", reg.Name, rv.storagePath(), err)
|
||||
}
|
||||
// set permissions
|
||||
if !onWindows {
|
||||
// TODO: only set executable files to 0755, set other to 0644
|
||||
err = os.Chmod(rv.storagePath(), 0o0755) //nolint:gosec // See TODO above.
|
||||
if err != nil {
|
||||
log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
|
||||
}
|
||||
// TODO: distinguish between executable and non executable files.
|
||||
err = utils.SetExecPermission(rv.storagePath(), utils.PublicReadPermission)
|
||||
if err != nil {
|
||||
log.Warningf("%s: failed to set permissions on downloaded file %s: %s", reg.Name, rv.storagePath(), err)
|
||||
}
|
||||
|
||||
log.Debugf("%s: fetched %s and stored to %s", reg.Name, downloadURL, rv.storagePath())
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
@ -13,10 +12,6 @@ import (
|
|||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
onWindows = runtime.GOOS == "windows"
|
||||
)
|
||||
|
||||
// ResourceRegistry is a registry for managing update resources.
|
||||
type ResourceRegistry struct {
|
||||
sync.RWMutex
|
||||
|
@ -98,7 +93,7 @@ func (reg *ResourceRegistry) Initialize(storageDir *utils.DirStructure) error {
|
|||
|
||||
// initialize private attributes
|
||||
reg.storageDir = storageDir
|
||||
reg.tmpDir = storageDir.ChildDir("tmp", 0o0700)
|
||||
reg.tmpDir = storageDir.ChildDir("tmp", utils.AdminOnlyPermission)
|
||||
reg.resources = make(map[string]*Resource)
|
||||
if reg.state == nil {
|
||||
reg.state = &RegistryState{}
|
||||
|
|
|
@ -20,7 +20,7 @@ func TestMain(m *testing.M) {
|
|||
DevMode: true,
|
||||
Online: true,
|
||||
}
|
||||
err = registry.Initialize(utils.NewDirStructure(tmpDir, 0o0777))
|
||||
err = registry.Initialize(utils.NewDirStructure(tmpDir, utils.PublicWritePermission))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ const isWindows = runtime.GOOS == "windows"
|
|||
|
||||
// EnsureDirectory ensures that the given directory exists and that is has the given permissions set.
|
||||
// If path is a file, it is deleted and a directory created.
|
||||
func EnsureDirectory(path string, perm os.FileMode) error {
|
||||
func EnsureDirectory(path string, perm FSPermission) error {
|
||||
// open path
|
||||
f, err := os.Stat(path)
|
||||
if err == nil {
|
||||
|
@ -20,10 +20,11 @@ func EnsureDirectory(path string, perm os.FileMode) error {
|
|||
if f.IsDir() {
|
||||
// directory exists, check permissions
|
||||
if isWindows {
|
||||
// TODO: set correct permission on windows
|
||||
// acl.Chmod(path, perm)
|
||||
} else if f.Mode().Perm() != perm {
|
||||
return os.Chmod(path, perm)
|
||||
// Ignore windows permission error. For none admin users it will always fail.
|
||||
_ = SetDirPermission(path, perm)
|
||||
return nil
|
||||
} else if f.Mode().Perm() != perm.AsUnixDirExecPermission() {
|
||||
return SetDirPermission(path, perm)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -34,11 +35,17 @@ func EnsureDirectory(path string, perm os.FileMode) error {
|
|||
}
|
||||
// file does not exist (or has been deleted)
|
||||
if err == nil || errors.Is(err, fs.ErrNotExist) {
|
||||
err = os.Mkdir(path, perm)
|
||||
err = os.Mkdir(path, perm.AsUnixDirExecPermission())
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not create dir %s: %w", path, err)
|
||||
}
|
||||
return os.Chmod(path, perm)
|
||||
// Set permissions.
|
||||
err = SetDirPermission(path, perm)
|
||||
// Ignore windows permission error. For none admin users it will always fail.
|
||||
if !isWindows {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// other error opening path
|
||||
return fmt.Errorf("failed to access %s: %w", path, err)
|
||||
|
|
20
base/utils/permissions.go
Normal file
20
base/utils/permissions.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
//go:build !windows
|
||||
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
// SetDirPermission sets the permission of a directory.
|
||||
func SetDirPermission(path string, perm FSPermission) error {
|
||||
return os.Chmod(path, perm.AsUnixDirExecPermission())
|
||||
}
|
||||
|
||||
// SetExecPermission sets the permission of an executable file.
|
||||
func SetExecPermission(path string, perm FSPermission) error {
|
||||
return SetDirPermission(path, perm)
|
||||
}
|
||||
|
||||
// SetFilePermission sets the permission of a non executable file.
|
||||
func SetFilePermission(path string, perm FSPermission) error {
|
||||
return os.Chmod(path, perm.AsUnixFilePermission())
|
||||
}
|
79
base/utils/permissions_windows.go
Normal file
79
base/utils/permissions_windows.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
//go:build windows
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/hectane/go-acl"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
systemSID *windows.SID
|
||||
adminsSID *windows.SID
|
||||
usersSID *windows.SID
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Initialize Security ID for all need groups.
|
||||
// Reference: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
|
||||
var err error
|
||||
systemSID, err = windows.StringToSid("S-1-5-18") // SYSTEM (Local System)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
adminsSID, err = windows.StringToSid("S-1-5-32-544") // Administrators
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
usersSID, err = windows.StringToSid("S-1-5-32-545") // Users
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// SetDirPermission sets the permission of a directory.
|
||||
func SetDirPermission(path string, perm FSPermission) error {
|
||||
SetFilePermission(path, perm)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetExecPermission sets the permission of an executable file.
|
||||
func SetExecPermission(path string, perm FSPermission) error {
|
||||
SetFilePermission(path, perm)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFilePermission sets the permission of a non executable file.
|
||||
func SetFilePermission(path string, perm FSPermission) {
|
||||
switch perm {
|
||||
case AdminOnlyPermission:
|
||||
// Set only admin rights, remove all others.
|
||||
acl.Apply(
|
||||
path,
|
||||
true,
|
||||
false,
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, systemSID),
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, adminsSID),
|
||||
)
|
||||
case PublicReadPermission:
|
||||
// Set admin rights and read/execute rights for users, remove all others.
|
||||
acl.Apply(
|
||||
path,
|
||||
true,
|
||||
false,
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, systemSID),
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, adminsSID),
|
||||
acl.GrantSid(windows.GENERIC_READ|windows.GENERIC_EXECUTE, usersSID),
|
||||
)
|
||||
case PublicWritePermission:
|
||||
// Set full control to admin and regular users. Guest users will not have access.
|
||||
acl.Apply(
|
||||
path,
|
||||
true,
|
||||
false,
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, systemSID),
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, adminsSID),
|
||||
acl.GrantSid(windows.GENERIC_ALL|windows.STANDARD_RIGHTS_ALL, usersSID),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,11 @@
|
|||
package renameio
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/hectane/go-acl"
|
||||
)
|
||||
|
||||
// WriteFile mirrors os.WriteFile, replacing an existing file with the same
|
||||
// name atomically.
|
||||
|
@ -14,7 +19,12 @@ func WriteFile(filename string, data []byte, perm os.FileMode) error {
|
|||
}()
|
||||
|
||||
// Set permissions before writing data, in case the data is sensitive.
|
||||
if err := t.Chmod(perm); err != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
err = acl.Chmod(t.path, perm)
|
||||
} else {
|
||||
err = t.Chmod(perm)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -2,25 +2,61 @@ package utils
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type FSPermission uint8
|
||||
|
||||
const (
|
||||
AdminOnlyPermission FSPermission = iota
|
||||
PublicReadPermission
|
||||
PublicWritePermission
|
||||
)
|
||||
|
||||
// AsUnixDirExecPermission return the corresponding unix permission for a directory or executable.
|
||||
func (perm FSPermission) AsUnixDirExecPermission() fs.FileMode {
|
||||
switch perm {
|
||||
case AdminOnlyPermission:
|
||||
return 0o700
|
||||
case PublicReadPermission:
|
||||
return 0o755
|
||||
case PublicWritePermission:
|
||||
return 0o777
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// AsUnixFilePermission return the corresponding unix permission for a regular file.
|
||||
func (perm FSPermission) AsUnixFilePermission() fs.FileMode {
|
||||
switch perm {
|
||||
case AdminOnlyPermission:
|
||||
return 0o600
|
||||
case PublicReadPermission:
|
||||
return 0o644
|
||||
case PublicWritePermission:
|
||||
return 0o666
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// DirStructure represents a directory structure with permissions that should be enforced.
|
||||
type DirStructure struct {
|
||||
sync.Mutex
|
||||
|
||||
Path string
|
||||
Dir string
|
||||
Perm os.FileMode
|
||||
Perm FSPermission
|
||||
Parent *DirStructure
|
||||
Children map[string]*DirStructure
|
||||
}
|
||||
|
||||
// NewDirStructure returns a new DirStructure.
|
||||
func NewDirStructure(path string, perm os.FileMode) *DirStructure {
|
||||
func NewDirStructure(path string, perm FSPermission) *DirStructure {
|
||||
return &DirStructure{
|
||||
Path: path,
|
||||
Perm: perm,
|
||||
|
@ -29,7 +65,7 @@ func NewDirStructure(path string, perm os.FileMode) *DirStructure {
|
|||
}
|
||||
|
||||
// ChildDir adds a new child DirStructure and returns it. Should the child already exist, the existing child is returned and the permissions are updated.
|
||||
func (ds *DirStructure) ChildDir(dirName string, perm os.FileMode) (child *DirStructure) {
|
||||
func (ds *DirStructure) ChildDir(dirName string, perm FSPermission) (child *DirStructure) {
|
||||
ds.Lock()
|
||||
defer ds.Unlock()
|
||||
|
||||
|
|
|
@ -13,13 +13,13 @@ func ExampleDirStructure() {
|
|||
// output:
|
||||
// / [755]
|
||||
// /repo [777]
|
||||
// /repo/b [707]
|
||||
// /repo/b/c [750]
|
||||
// /repo/b/d [707]
|
||||
// /repo/b/d/e [707]
|
||||
// /repo/b/d/f [707]
|
||||
// /repo/b/d/f/g [707]
|
||||
// /repo/b/d/f/g/h [707]
|
||||
// /repo/b [755]
|
||||
// /repo/b/c [777]
|
||||
// /repo/b/d [755]
|
||||
// /repo/b/d/e [755]
|
||||
// /repo/b/d/f [755]
|
||||
// /repo/b/d/f/g [755]
|
||||
// /repo/b/d/f/g/h [755]
|
||||
// /secret [700]
|
||||
|
||||
basePath, err := os.MkdirTemp("", "")
|
||||
|
@ -28,12 +28,12 @@ func ExampleDirStructure() {
|
|||
return
|
||||
}
|
||||
|
||||
ds := NewDirStructure(basePath, 0o0755)
|
||||
secret := ds.ChildDir("secret", 0o0700)
|
||||
repo := ds.ChildDir("repo", 0o0777)
|
||||
_ = repo.ChildDir("a", 0o0700)
|
||||
b := repo.ChildDir("b", 0o0707)
|
||||
c := b.ChildDir("c", 0o0750)
|
||||
ds := NewDirStructure(basePath, PublicReadPermission)
|
||||
secret := ds.ChildDir("secret", AdminOnlyPermission)
|
||||
repo := ds.ChildDir("repo", PublicWritePermission)
|
||||
_ = repo.ChildDir("a", AdminOnlyPermission)
|
||||
b := repo.ChildDir("b", PublicReadPermission)
|
||||
c := b.ChildDir("c", PublicWritePermission)
|
||||
|
||||
err = ds.Ensure()
|
||||
if err != nil {
|
||||
|
|
|
@ -33,7 +33,7 @@ func main() {
|
|||
flag.Parse()
|
||||
|
||||
// Set name and license.
|
||||
info.Set("SPN Hub", "", "GPLv3")
|
||||
info.Set("SPN Hub", "0.7.8", "GPLv3")
|
||||
|
||||
// Configure metrics.
|
||||
_ = metrics.SetNamespace("hub")
|
||||
|
@ -45,10 +45,6 @@ func main() {
|
|||
// Set SPN public hub mode.
|
||||
conf.EnablePublicHub(true)
|
||||
|
||||
// Set default log level.
|
||||
log.SetLogLevel(log.WarningLevel)
|
||||
_ = log.Start()
|
||||
|
||||
// Create instance.
|
||||
var execCmdLine bool
|
||||
instance, err := spn.New()
|
||||
|
@ -79,6 +75,10 @@ func main() {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Set default log level.
|
||||
log.SetLogLevel(log.WarningLevel)
|
||||
_ = log.Start()
|
||||
|
||||
// Start
|
||||
go func() {
|
||||
err = instance.Start()
|
||||
|
|
|
@ -225,14 +225,14 @@ func configureRegistry(mustLoadIndex bool) error {
|
|||
// Remove left over quotes.
|
||||
dataDir = strings.Trim(dataDir, `\"`)
|
||||
// Initialize data root.
|
||||
err := dataroot.Initialize(dataDir, 0o0755)
|
||||
err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize data root: %w", err)
|
||||
}
|
||||
dataRoot = dataroot.Root()
|
||||
|
||||
// Initialize registry.
|
||||
err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
|
||||
err = registry.Initialize(dataRoot.ChildDir("updates", utils.PublicReadPermission))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -24,7 +25,7 @@ var cleanStructureCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func cleanAndEnsureExecDir() error {
|
||||
execDir := dataRoot.ChildDir("exec", 0o777)
|
||||
execDir := dataRoot.ChildDir("exec", utils.PublicWritePermission)
|
||||
|
||||
// Clean up and remove exec dir.
|
||||
err := os.RemoveAll(execDir.Path)
|
||||
|
|
|
@ -179,14 +179,14 @@ func configureRegistry(mustLoadIndex bool) error {
|
|||
// Remove left over quotes.
|
||||
dataDir = strings.Trim(dataDir, `\"`)
|
||||
// Initialize data root.
|
||||
err := dataroot.Initialize(dataDir, 0o0755)
|
||||
err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize data root: %w", err)
|
||||
}
|
||||
dataRoot = dataroot.Root()
|
||||
|
||||
// Initialize registry.
|
||||
err = registry.Initialize(dataRoot.ChildDir("updates", 0o0755))
|
||||
err = registry.Initialize(dataRoot.ChildDir("updates", utils.PublicReadPermission))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ func configureRegistry(mustLoadIndex bool) error {
|
|||
|
||||
func ensureLoggingDir() error {
|
||||
// set up logs root
|
||||
logsRoot = dataRoot.ChildDir("logs", 0o0777)
|
||||
logsRoot = dataRoot.ChildDir("logs", utils.PublicWritePermission)
|
||||
err := logsRoot.Ensure()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize logs root (%q): %w", logsRoot.Path, err)
|
||||
|
|
|
@ -272,7 +272,7 @@ func fixExecPerm(path string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec // Set execution rights.
|
||||
if err := os.Chmod(path, 0o0755); err != nil { //nolint:gosec
|
||||
return fmt.Errorf("failed to chmod %s: %w", path, err)
|
||||
}
|
||||
|
||||
|
|
|
@ -31,7 +31,7 @@ var rootCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
registry = &updater.ResourceRegistry{}
|
||||
err = registry.Initialize(utils.NewDirStructure(absDistPath, 0o0755))
|
||||
err = registry.Initialize(utils.NewDirStructure(absDistPath, utils.PublicReadPermission))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safing/portmaster/base/updater"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -63,7 +64,7 @@ func release(cmd *cobra.Command, args []string) error {
|
|||
fmt.Println("aborted...")
|
||||
return nil
|
||||
}
|
||||
symlinksDir := registry.StorageDir().ChildDir("latest", 0o755)
|
||||
symlinksDir := registry.StorageDir().ChildDir("latest", utils.PublicReadPermission)
|
||||
err = registry.CreateSymlinks(symlinksDir)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
134
desktop/angular/package-lock.json
generated
134
desktop/angular/package-lock.json
generated
|
@ -23,13 +23,13 @@
|
|||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@tauri-apps/api": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-cli": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-dialog": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-notification": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc",
|
||||
"@tauri-apps/api": ">=2.1.1",
|
||||
"@tauri-apps/plugin-cli": ">=2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
||||
"@tauri-apps/plugin-dialog": ">=2.0.0",
|
||||
"@tauri-apps/plugin-notification": ">=2.0.0",
|
||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"d3": "^7.8.4",
|
||||
"data-urls": "^5.0.0",
|
||||
|
@ -4406,9 +4406,9 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/@tauri-apps/api": {
|
||||
"version": "2.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz",
|
||||
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A==",
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
|
@ -4416,57 +4416,57 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-cli": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-EcSTRfEU3zzlNbgwVtZVzqB19z3PNjyXD9H+YXuuLpV+Hwuh6Oi1fhUdCI0mp5zr9HSMWE+HzHkpBI7sVP1RyA==",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0.tgz",
|
||||
"integrity": "sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-clipboard-manager": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
|
||||
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-dialog": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz",
|
||||
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-notification": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-ddDj7xM8XR7Zv2vdpofNXlLjcp49p/VjlL0D+/eBcMuyooaLNMor3jz/+H6s23iHerdxMWA50mzy26BRN1BySA==",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0.tgz",
|
||||
"integrity": "sha512-6qEDYJS7mgXZWLXA0EFL+DVCJh8sJlzSoyw6B50pxhLPVFjc5Vr5DVzl5W3mUHaYhod5wsC984eQnlCCGqxYDA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-os": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
|
||||
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-shell": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
|
||||
"license": "MIT or APACHE-2.0",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.1.tgz",
|
||||
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
|
@ -21067,56 +21067,56 @@
|
|||
"peer": true
|
||||
},
|
||||
"@tauri-apps/api": {
|
||||
"version": "2.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.0.0-rc.4.tgz",
|
||||
"integrity": "sha512-UNiIhhKG08j4ooss2oEEVexffmWkgkYlC2M3GcX3VPtNsqFgVNL8Mcw/4Y7rO9M9S+ffAMnLOF5ypzyuyb8tyg=="
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.1.1.tgz",
|
||||
"integrity": "sha512-fzUfFFKo4lknXGJq8qrCidkUcKcH2UHhfaaCNt4GzgzGaW2iS26uFOg4tS3H4P8D6ZEeUxtiD5z0nwFF0UN30A=="
|
||||
},
|
||||
"@tauri-apps/plugin-cli": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-EcSTRfEU3zzlNbgwVtZVzqB19z3PNjyXD9H+YXuuLpV+Hwuh6Oi1fhUdCI0mp5zr9HSMWE+HzHkpBI7sVP1RyA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-cli/-/plugin-cli-2.0.0.tgz",
|
||||
"integrity": "sha512-glQmlL1IiCGEa1FHYa/PTPSeYhfu56omLRgHXWlJECDt6DbJyRuJWVgtkQfUxtqnVdYnnU+DGIGeiInoEqtjLw==",
|
||||
"requires": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tauri-apps/plugin-clipboard-manager": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-hFgUABMmQuVGKwHb8PR9fuqfk0WRkedbWUt/ZV5sL4Q6kLrsp3JYJvtzVPeMYdeBvMqHl8WXNxAc/zwSld2h9w==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-clipboard-manager/-/plugin-clipboard-manager-2.0.0.tgz",
|
||||
"integrity": "sha512-V1sXmbjnwfXt/r48RJMwfUmDMSaP/8/YbH4CLNxt+/sf1eHlIP8PRFdFDQwLN0cNQKu2rqQVbG/Wc/Ps6cDUhw==",
|
||||
"requires": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tauri-apps/plugin-dialog": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-H28gh6BfZtjflHQ+HrmWwunDriBI3AQLAKnMs50GA6zeNUULqbQr7VXbAAKeJL/0CmWcecID4PKXVoSlaWRhEg==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.0.1.tgz",
|
||||
"integrity": "sha512-fnUrNr6EfvTqdls/ufusU7h6UbNFzLKvHk/zTuOiBq01R3dTODqwctZlzakdbfSp/7pNwTKvgKTAgl/NAP/Z0Q==",
|
||||
"requires": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tauri-apps/plugin-notification": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-ddDj7xM8XR7Zv2vdpofNXlLjcp49p/VjlL0D+/eBcMuyooaLNMor3jz/+H6s23iHerdxMWA50mzy26BRN1BySA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.0.0.tgz",
|
||||
"integrity": "sha512-6qEDYJS7mgXZWLXA0EFL+DVCJh8sJlzSoyw6B50pxhLPVFjc5Vr5DVzl5W3mUHaYhod5wsC984eQnlCCGqxYDA==",
|
||||
"requires": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tauri-apps/plugin-os": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-PV8zlSTmYfiN2xzILUmlDSEycS7UYbH2yXk/ZqF+qQU6/s+OVQvmSth4EhllFjcpvPbtqELvpzfjw+2qEouchA==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.0.0.tgz",
|
||||
"integrity": "sha512-M7hG/nNyQYTJxVG/UhTKhp9mpXriwWzrs9mqDreB8mIgqA3ek5nHLdwRZJWhkKjZrnDT4v9CpA9BhYeplTlAiA==",
|
||||
"requires": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tauri-apps/plugin-shell": {
|
||||
"version": "2.0.0-rc.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0-rc.1.tgz",
|
||||
"integrity": "sha512-JtNROc0rqEwN/g93ig5pK4cl1vUo2yn+osCpY9de64cy/d9hRzof7AuYOgvt/Xcd5VPQmlgo2AGvUh5sQRSR1A==",
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.1.tgz",
|
||||
"integrity": "sha512-akU1b77sw3qHiynrK0s930y8zKmcdrSD60htjH+mFZqv5WaakZA/XxHR3/sF1nNv9Mgmt/Shls37HwnOr00aSw==",
|
||||
"requires": {
|
||||
"@tauri-apps/api": "^2.0.0-rc.4"
|
||||
"@tauri-apps/api": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
|
|
|
@ -37,13 +37,13 @@
|
|||
"@fortawesome/free-brands-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@tauri-apps/api": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-cli": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-dialog": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-notification": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": ">=2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0-rc",
|
||||
"@tauri-apps/api": ">=2.1.1",
|
||||
"@tauri-apps/plugin-cli": ">=2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": ">=2.0.0",
|
||||
"@tauri-apps/plugin-dialog": ">=2.0.0",
|
||||
"@tauri-apps/plugin-notification": ">=2.0.0",
|
||||
"@tauri-apps/plugin-os": ">=2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"d3": "^7.8.4",
|
||||
"data-urls": "^5.0.0",
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<div class="flex justify-between items-center p-4 px-12 text-xxs">
|
||||
<!-- Breadcrumbs -->
|
||||
<div class="flex items-center">
|
||||
<a class="text-secondary hover:text-primary" [routerLink]="['/app/overview']">Apps</a>
|
||||
<div class="cursor-pointer text-secondary hover:text-primary" [routerLink]="['/app/overview']">Apps</div>
|
||||
<svg viewBox="0 0 24 24" class="inline-block w-4 h-4 text-secondary">
|
||||
<g fill="none" class="inner" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.2" d="M10 16l4-4-4-4" />
|
||||
|
|
|
@ -27,9 +27,15 @@ if (typeof (CSS as any)['registerProperty'] === 'function') {
|
|||
}
|
||||
|
||||
function handleExternalResources(e: Event) {
|
||||
// TODO:
|
||||
// This code executes "openExternal()" when any "<a />" element in the app is clicked.
|
||||
// This could potentially be a security issue.
|
||||
// We should consider restricting this to only external links that belong to a certain domain (e.g., https://safing.io).
|
||||
|
||||
// get click target
|
||||
let target: HTMLElement | null = e.target as HTMLElement;
|
||||
// traverse until we reach an a tag
|
||||
|
||||
// traverse until we reach element "<a />"
|
||||
while (!!target && target.tagName !== "A") {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
|
4280
desktop/tauri/src-tauri/Cargo.lock
generated
4280
desktop/tauri/src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -12,21 +12,21 @@ rust-version = "1.60"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-rc.7", features = [] }
|
||||
tauri-build = { version = "2.0.3", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Tauri
|
||||
tauri = { version = "2.0.0-rc.8", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
|
||||
tauri-plugin-shell = "2.0.0-rc"
|
||||
tauri-plugin-dialog = "2.0.0-rc"
|
||||
tauri-plugin-clipboard-manager = "2.0.0-rc"
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-single-instance = "2.0.0-rc"
|
||||
tauri-plugin-notification = "2.0.0-rc"
|
||||
tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-window-state = "2.0.0-rc"
|
||||
tauri = { version = "2.1.1", features = ["tray-icon", "image-png", "config-json5", "devtools"] }
|
||||
tauri-plugin-shell = "2.0.2"
|
||||
tauri-plugin-dialog = "2.0.3"
|
||||
tauri-plugin-clipboard-manager = "2.0.2"
|
||||
tauri-plugin-os = "2.0.1"
|
||||
tauri-plugin-single-instance = "2.0.1"
|
||||
tauri-plugin-notification = "2.0.1"
|
||||
tauri-plugin-log = "2.0.2"
|
||||
tauri-plugin-window-state = "2.0.2"
|
||||
|
||||
tauri-cli = "2.0.0-rc.8"
|
||||
tauri-cli = "2.1.0"
|
||||
clap = { version = "4" }
|
||||
|
||||
# General
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -29,7 +29,7 @@ use crate::{
|
|||
portmaster::PortmasterExt,
|
||||
window::{create_main_window, may_navigate_to_ui, open_window},
|
||||
};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
||||
|
||||
pub type AppIcon = TrayIcon<Wry>;
|
||||
|
||||
|
@ -199,8 +199,10 @@ pub fn setup_tray_menu(
|
|||
app.dialog()
|
||||
.message("This does not stop the Portmaster system service")
|
||||
.title("Do you really want to quit the user interface?")
|
||||
.ok_button_label("Yes, exit")
|
||||
.cancel_button_label("No")
|
||||
.buttons(MessageDialogButtons::OkCancelCustom(
|
||||
"Yes, exit".to_owned(),
|
||||
"No".to_owned(),
|
||||
))
|
||||
.show(move |answer| {
|
||||
if answer {
|
||||
// let _ = handle.emit("exit-requested", "");
|
||||
|
|
|
@ -139,7 +139,7 @@
|
|||
{{/each~}}
|
||||
</Component>
|
||||
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
|
||||
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
|
||||
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
|
||||
{{#each file_associations as |association| ~}}
|
||||
{{#each association.ext as |ext| ~}}
|
||||
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
|
||||
|
|
|
@ -139,7 +139,7 @@
|
|||
{{/each~}}
|
||||
</Component>
|
||||
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
|
||||
<File Id="Path" Source="{{app_exe_source}}" KeyPath="yes" Checksum="yes"/>
|
||||
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
|
||||
{{#each file_associations as |association| ~}}
|
||||
{{#each association.ext as |ext| ~}}
|
||||
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
|
||||
|
|
97
go.mod
97
go.mod
|
@ -1,6 +1,8 @@
|
|||
module github.com/safing/portmaster
|
||||
|
||||
go 1.22.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.24.0
|
||||
|
||||
// TODO: Remove when https://github.com/tc-hib/winres/pull/4 is released.
|
||||
replace github.com/tc-hib/winres => github.com/dhaavi/winres v0.2.2
|
||||
|
@ -9,14 +11,15 @@ require (
|
|||
fyne.io/systray v1.11.0
|
||||
github.com/VictoriaMetrics/metrics v1.35.1
|
||||
github.com/Xuanwo/go-locale v1.1.1
|
||||
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0
|
||||
github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6
|
||||
github.com/agext/levenshtein v1.2.3
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/awalterschulze/gographviz v2.0.3+incompatible
|
||||
github.com/bluele/gcache v0.0.2
|
||||
github.com/brianvoe/gofakeit v3.18.0+incompatible
|
||||
github.com/cilium/ebpf v0.16.0
|
||||
github.com/coreos/go-iptables v0.7.0
|
||||
github.com/cilium/ebpf v0.17.1
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dgraph-io/badger v1.6.2
|
||||
github.com/dhaavi/go-notify v0.0.0-20190209221809-c404b1f22435
|
||||
|
@ -32,10 +35,12 @@ require (
|
|||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/hashicorp/go-version v1.7.0
|
||||
github.com/jackc/puddle/v2 v2.2.1
|
||||
github.com/lmittmann/tint v1.0.5
|
||||
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb
|
||||
github.com/jackc/puddle/v2 v2.2.2
|
||||
github.com/lmittmann/tint v1.0.6
|
||||
github.com/maruel/panicparse/v2 v2.4.0
|
||||
github.com/mat/besticon v3.12.0+incompatible
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-colorable v0.1.14
|
||||
github.com/mattn/go-isatty v0.0.20
|
||||
github.com/miekg/dns v1.1.62
|
||||
github.com/mitchellh/copystructure v1.2.0
|
||||
|
@ -44,84 +49,138 @@ require (
|
|||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/r3labs/diff/v3 v3.0.1
|
||||
github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5
|
||||
github.com/safing/jess v0.3.4
|
||||
github.com/safing/structures v1.1.0
|
||||
github.com/rubenv/sql-migrate v1.7.1
|
||||
github.com/safing/jess v0.3.5
|
||||
github.com/safing/structures v1.2.0
|
||||
github.com/seehuhn/fortuna v1.0.1
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spkg/zipfs v0.7.1
|
||||
github.com/stephenafamo/bob v0.30.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/tannerryan/ring v1.1.2
|
||||
github.com/tc-hib/winres v0.3.1
|
||||
github.com/tevino/abool v1.2.0
|
||||
github.com/tidwall/gjson v1.17.3
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26
|
||||
github.com/varlink/go v0.4.0
|
||||
github.com/vincent-petithory/dataurl v1.0.0
|
||||
go.etcd.io/bbolt v1.3.10
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/net v0.28.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.24.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.29.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.32.0
|
||||
zombiezen.com/go/sqlite v1.3.0
|
||||
)
|
||||
|
||||
require (
|
||||
al.essio.dev/pkg/shellescape v1.5.1 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect
|
||||
github.com/aead/ecdh v0.2.0 // indirect
|
||||
github.com/alessio/shellescape v1.4.2 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/danieljoos/wincred v1.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.9.0 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
|
||||
github.com/godbus/dbus v4.1.0+incompatible // indirect
|
||||
github.com/godror/godror v0.40.4 // indirect
|
||||
github.com/godror/knownpb v0.1.1 // indirect
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect
|
||||
github.com/golang/glog v1.2.1 // indirect
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huandu/xstrings v1.4.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/josharian/native v1.1.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/maruel/panicparse/v2 v2.3.1 // indirect
|
||||
github.com/knadh/koanf/maps v0.1.1 // indirect
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0 // indirect
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0 // indirect
|
||||
github.com/knadh/koanf/providers/env v0.1.0 // indirect
|
||||
github.com/knadh/koanf/providers/file v0.1.0 // indirect
|
||||
github.com/knadh/koanf/v2 v2.1.0 // indirect
|
||||
github.com/lib/pq v1.10.7 // indirect
|
||||
github.com/mattn/go-oci8 v0.1.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.19 // indirect
|
||||
github.com/mdlayher/netlink v1.7.2 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/cli v1.1.5 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/posener/complete v1.2.3 // indirect
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.0 // indirect
|
||||
github.com/seehuhn/sha256d v1.0.0 // indirect
|
||||
github.com/shopspring/decimal v1.3.1 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stephenafamo/scan v0.6.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/urfave/cli/v2 v2.23.7 // indirect
|
||||
github.com/valyala/fastrand v1.1.0 // indirect
|
||||
github.com/valyala/histogram v1.2.0 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
github.com/volatiletech/inflect v0.0.1 // indirect
|
||||
github.com/volatiletech/strmangle v0.0.6 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zalando/go-keyring v0.2.5 // indirect
|
||||
github.com/zalando/go-keyring v0.2.6 // indirect
|
||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/crypto v0.31.0 // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||
modernc.org/libc v1.59.9 // indirect
|
||||
modernc.org/mathutil v1.6.0 // indirect
|
||||
modernc.org/memory v1.8.0 // indirect
|
||||
modernc.org/sqlite v1.32.0 // indirect
|
||||
modernc.org/strutil v1.2.0 // indirect
|
||||
modernc.org/token v1.1.0 // indirect
|
||||
mvdan.cc/gofumpt v0.5.0 // indirect
|
||||
)
|
||||
|
||||
tool (
|
||||
github.com/rubenv/sql-migrate/sql-migrate
|
||||
github.com/stephenafamo/bob/gen/bobgen-sqlite
|
||||
)
|
||||
|
|
242
go.sum
242
go.sum
|
@ -1,28 +1,49 @@
|
|||
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
|
||||
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
|
||||
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
|
||||
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
|
||||
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||
github.com/Masterminds/sprig/v3 v3.2.1/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
|
||||
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/UNO-SOFT/zlog v0.8.1 h1:TEFkGJHtUfTRgMkLZiAjLSHALjwSBdw6/zByMC5GJt4=
|
||||
github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc=
|
||||
github.com/VictoriaMetrics/metrics v1.35.1 h1:o84wtBKQbzLdDy14XeskkCZih6anG+veZ1SwJHFGwrU=
|
||||
github.com/VictoriaMetrics/metrics v1.35.1/go.mod h1:r7hveu6xMdUACXvB8TYdAj8WEsKzWB0EkpJN+RDtOf8=
|
||||
github.com/Xuanwo/go-locale v1.1.1 h1:nhvzo1phY4LRwdrwVwKWXn5iZ0pMwwsa3o29yiDRuZc=
|
||||
github.com/Xuanwo/go-locale v1.1.1/go.mod h1:ldC3FzZeMYALkL3YYpwhr4iVYdOIUx42kORcnAHdKUo=
|
||||
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE=
|
||||
github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k=
|
||||
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0 h1:vLrhbOWVPxtHao/QthU8pcpI4DbtSGnWgH7qIJf8F6k=
|
||||
github.com/aarondl/opt v0.0.0-20230114172057-b91f370c41f0/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk=
|
||||
github.com/aead/ecdh v0.2.0 h1:pYop54xVaq/CEREFEcukHRZfTdjiWvYIsZDXXrBapQQ=
|
||||
github.com/aead/ecdh v0.2.0/go.mod h1:a9HHtXuSo8J1Js1MwLQx2mBhkXMT6YwUmVVEY4tTB8U=
|
||||
github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpYk2gxGJnDjsYuboNTcRmbtGKGs=
|
||||
github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0=
|
||||
github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
|
||||
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
|
||||
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
|
||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
|
||||
|
@ -34,20 +55,23 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
|||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
|
||||
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
|
||||
github.com/cilium/ebpf v0.16.0 h1:+BiEnHL6Z7lXnlGUsXQPPAE7+kenAd4ES8MQ5min0Ok=
|
||||
github.com/cilium/ebpf v0.16.0/go.mod h1:L7u2Blt2jMM/vLAVgjxluxtBKlz3/GWjB0dMOEngfwE=
|
||||
github.com/cilium/ebpf v0.17.1 h1:G8mzU81R2JA1nE5/8SRubzqvBMmAmri2VL8BIZPWvV0=
|
||||
github.com/cilium/ebpf v0.17.1/go.mod h1:vay2FaYSmIlv3r8dNACd4mW/OCaZLJKJOo+IHBvCIO8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/coreos/go-iptables v0.8.0 h1:MPc2P89IhuVpLI7ETL/2tx3XZ61VeICZjYqDEgNsPRc=
|
||||
github.com/coreos/go-iptables v0.8.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
|
||||
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
|
||||
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
|
||||
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
||||
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=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk=
|
||||
github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8=
|
||||
github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
|
@ -62,6 +86,9 @@ github.com/dhaavi/winres v0.2.2/go.mod h1:1NTs+/DtKP1BplIL1+XQSoq4X1PUfLczexS7gf
|
|||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
|
@ -72,33 +99,54 @@ github.com/florianl/go-nfqueue v1.3.2/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uD
|
|||
github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
|
||||
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
|
||||
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
|
||||
github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY=
|
||||
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk=
|
||||
github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI=
|
||||
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d h1:QQP1nE4qh5aHTGvI1LgOFxZYVxYoGeMfbNHikogPyoA=
|
||||
github.com/go-sql-driver/mysql v1.7.2-0.20231213112541-0004702b931d/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c=
|
||||
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/godbus/dbus v4.1.0+incompatible h1:WqqLRTsQic3apZUK9qC5sGNfXthmPXzUZ7nQPrNITa4=
|
||||
github.com/godbus/dbus v4.1.0+incompatible/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/godror/godror v0.40.4 h1:X1e7hUd02GDaLWKZj40Z7L0CP0W9TrGgmPQZw6+anBg=
|
||||
github.com/godror/godror v0.40.4/go.mod h1:i8YtVTHUJKfFT3wTat4A9UoqScUtZXiYB9Rf3SVARgc=
|
||||
github.com/godror/knownpb v0.1.1 h1:A4J7jdx7jWBhJm18NntafzSC//iZDHkDi1+juwQ5pTI=
|
||||
github.com/godror/knownpb v0.1.1/go.mod h1:4nRFbQo1dDuwKnblRXDxrfCFYeT4hjg3GjMqef58eRE=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
|
||||
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
|
||||
github.com/golang/gddo v0.0.0-20180823221919-9d8ff1c67be5/go.mod h1:xEhNfoBDX1hzLm2Nf80qUvZ2sVwoMZ8d6IE2SrsQfh4=
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
|
||||
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4=
|
||||
github.com/golang/glog v1.2.1/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
|
@ -118,8 +166,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
|
||||
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
|
@ -131,18 +183,31 @@ github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:Fecb
|
|||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
|
||||
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb h1:PGufWXXDq9yaev6xX1YQauaO1MV90e6Mpoq1I7Lz/VM=
|
||||
github.com/hectane/go-acl v0.0.0-20230122075934-ca0b05cb1adb/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E=
|
||||
github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
|
||||
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
|
@ -161,6 +226,18 @@ github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCX
|
|||
github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs=
|
||||
github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0 h1:ZZ8/iGfRLvKSaMEECEBPM1HQslrZADk8fP1XFUxVI5w=
|
||||
github.com/knadh/koanf/parsers/yaml v0.1.0/go.mod h1:cvbUDC7AL23pImuQP0oRw/hPuccrNBS2bps8asS0CwY=
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0 h1:gOkxhHkemwG4LezxxN8DMOFopOPghxRVp7JbIvdvqzU=
|
||||
github.com/knadh/koanf/providers/confmap v0.1.0/go.mod h1:2uLhxQzJnyHKfxG927awZC7+fyHFdQkd697K4MdLnIU=
|
||||
github.com/knadh/koanf/providers/env v0.1.0 h1:LqKteXqfOWyx5Ab9VfGHmjY9BvRXi+clwyZozgVRiKg=
|
||||
github.com/knadh/koanf/providers/env v0.1.0/go.mod h1:RE8K9GbACJkeEnkl8L/Qcj8p4ZyPXZIQ191HJi44ZaQ=
|
||||
github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c=
|
||||
github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA=
|
||||
github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8=
|
||||
github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
@ -169,23 +246,33 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
|
||||
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
|
||||
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lmittmann/tint v1.0.6 h1:vkkuDAZXc0EFGNzYjWcV0h7eEX+uujH48f/ifSkJWgc=
|
||||
github.com/lmittmann/tint v1.0.6/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
|
||||
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/maruel/panicparse/v2 v2.3.1 h1:NtJavmbMn0DyzmmSStE8yUsmPZrZmudPH7kplxBinOA=
|
||||
github.com/maruel/panicparse/v2 v2.3.1/go.mod h1:s3UmQB9Fm/n7n/prcD2xBGDkwXD6y2LeZnhbEXvs9Dg=
|
||||
github.com/maruel/panicparse/v2 v2.4.0 h1:yQKMIbQ0DKfinzVkTkcUzQyQ60UCiNnYfR7PWwTs2VI=
|
||||
github.com/maruel/panicparse/v2 v2.4.0/go.mod h1:nOY2OKe8csO3F3SA5+hsxot05JLgukrF54B9x88fVp4=
|
||||
github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg=
|
||||
github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-oci8 v0.1.1 h1:aEUDxNAyDG0tv8CA3TArnDQNyc4EhnWlsfxRgDHABHM=
|
||||
github.com/mattn/go-oci8 v0.1.1/go.mod h1:wjDx6Xm9q7dFtHJvIlrI99JytznLw5wQ4R+9mNXJwGI=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
|
||||
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
github.com/mdlayher/ethtool v0.0.0-20211028163843-288d040e9d60/go.mod h1:aYbhishWc4Ai3I2U4Gaa2n3kHWSwzme6EsG/46HRQbE=
|
||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||
|
@ -209,9 +296,11 @@ github.com/mdlayher/socket v0.1.0/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5A
|
|||
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||
github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng=
|
||||
github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
|
@ -219,6 +308,7 @@ github.com/mitchellh/go-server-timing v1.0.1 h1:f00/aIe8T3MrnLhQHu3tSWvnwc5GV/p5
|
|||
github.com/mitchellh/go-server-timing v1.0.1/go.mod h1:Mo6GKi9FSLwWFAMn3bqVPWe20y5ri5QGQuO9D9MCOxk=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||
|
@ -227,6 +317,12 @@ github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdh
|
|||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=
|
||||
github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=
|
||||
github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
|
||||
github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
|
@ -236,6 +332,13 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
|
||||
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
|
||||
github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=
|
||||
github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494 h1:wSmWgpuccqS2IOfmYrbRiUgv+g37W5suLLLxwwniTSc=
|
||||
github.com/qdm12/reprint v0.0.0-20200326205758-722754a53494/go.mod h1:yipyliwI08eQ6XwDm1fEwKPdF/xdbkiHtrU+1Hg+vc4=
|
||||
github.com/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg=
|
||||
github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
|
@ -244,12 +347,15 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
|
|||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5 h1:R/qQ2Hw5/BgVQS87pg/mRTep8RxqDY0rcj4pbpBvNF8=
|
||||
github.com/rot256/pblind v0.0.0-20240730113005-f3275049ead5/go.mod h1:NTdpGnZ/E2cKXTiAz824w1p6OIm0mBbXcyuiYPCi/Ps=
|
||||
github.com/rubenv/sql-migrate v1.7.1 h1:f/o0WgfO/GqNuVg+6801K/KW3WdDSupzSjDYODmiUq4=
|
||||
github.com/rubenv/sql-migrate v1.7.1/go.mod h1:Ob2Psprc0/3ggbM6wCzyYVFFuc6FyZrb2AS+ezLDFb4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/safing/jess v0.3.4 h1:/p6ensqEUn2jI/z1EB9JUdwH4MJQirh/C9jEwNBzxw8=
|
||||
github.com/safing/jess v0.3.4/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI=
|
||||
github.com/safing/structures v1.1.0 h1:QzHBQBjaZSLzw2f6PM4ibSmPcfBHAOB5CKJ+k4FYkhQ=
|
||||
github.com/safing/structures v1.1.0/go.mod h1:QUrB74FcU41ahQ5oy3YNFCoSq+twE/n3+vNZc2K35II=
|
||||
github.com/safing/jess v0.3.5 h1:KS5elTKfWcDUow8SUoCj5QdyyGJNoExJNySerNkbxUU=
|
||||
github.com/safing/jess v0.3.5/go.mod h1:+B6UJnXVxi406Wk08SDnoC5NNBL7t3N0vZGokEbkVQI=
|
||||
github.com/safing/structures v1.2.0 h1:S6EzKxxGYTO6P9P3Dkab9gisLOrfAyvy7JzFOUSkOUk=
|
||||
github.com/safing/structures v1.2.0/go.mod h1:zIun7mz3xV0dJ3vXRZuU71ATzT8D/0hGJO+u+bk5Kvs=
|
||||
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0=
|
||||
|
@ -258,12 +364,20 @@ github.com/seehuhn/sha256d v1.0.0 h1:TXTsAuEWr02QjRm153Fnvvb6fXXDo7Bmy1FizxarGYw
|
|||
github.com/seehuhn/sha256d v1.0.0/go.mod h1:PEuxg9faClSveVuFXacQmi+NtDI/PX8bpKjtNzf2+s4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
|
||||
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551 h1:+EXKKt7RC4HyE/iE8zSeFL+7YBL8Z7vpBaEE3c7lCnk=
|
||||
github.com/simukti/sqldb-logger v0.0.0-20230108155151-646c1a075551/go.mod h1:ztTX0ctjRZ1wn9OXrzhonvNmv43yjFUXJYJR95JQAJE=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
|
||||
github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
|
||||
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
|
||||
|
@ -277,12 +391,21 @@ github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7Sr
|
|||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spkg/zipfs v0.7.1 h1:+2X5lvNHTybnDMQZAIHgedRXZK1WXdc+94R/P5v2XWE=
|
||||
github.com/spkg/zipfs v0.7.1/go.mod h1:48LW+/Rh1G7aAav1ew1PdlYn52T+LM+ARmSHfDNJvg8=
|
||||
github.com/stephenafamo/bob v0.30.0 h1:pGCS3iMh1xr2xlAP20eBV6gmqun6CCH/05l7uJJfvJI=
|
||||
github.com/stephenafamo/bob v0.30.0/go.mod h1:0z9AeWTOTJmGsokEtQReTEJry4iI9J+SCyKMcr40mOo=
|
||||
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97 h1:XItoZNmhOih06TC02jK7l3wlpZ0XT/sPQYutDcGOQjg=
|
||||
github.com/stephenafamo/fakedb v0.0.0-20221230081958-0b86f816ed97/go.mod h1:bM3Vmw1IakoaXocHmMIGgJFYob0vuK+CFWiJHQvz0jQ=
|
||||
github.com/stephenafamo/scan v0.6.1 h1:nXokGCQwYazMuyvdNAoK0T8Z76FWcpMvDdtengpz6PU=
|
||||
github.com/stephenafamo/scan v0.6.1/go.mod h1:FhIUJ8pLNyex36xGFiazDJJ5Xry0UkAi+RkWRrEcRMg=
|
||||
github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c h1:JFga++XBnZG2xlnvQyHJkeBWZ9G9mGdtgvLeSRbp/BA=
|
||||
github.com/stephenafamo/sqlparser v0.0.0-20241111104950-b04fa8a26c9c/go.mod h1:4iveRk8mkzQZxDuK/W0MGLrGmu/igyDYWNDD4a6v0r0=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
|
@ -291,8 +414,8 @@ github.com/tannerryan/ring v1.1.2/go.mod h1:DkELJEjbZhJBtFKR9Xziwj3HKZnb/knRgljN
|
|||
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.3 h1:bwWLZU7icoKRG+C+0PNwIKC6FCJO/Q3p2pZvuP0jN94=
|
||||
github.com/tidwall/gjson v1.17.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
|
@ -307,10 +430,14 @@ github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8t
|
|||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 h1:UFHFmFfixpmfRBcxuu+LA9l8MdURWVdVNUHxO5n1d2w=
|
||||
github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26/go.mod h1:IGhd0qMDsUa9acVjsbsT7bu3ktadtGOHI79+idTew/M=
|
||||
github.com/urfave/cli/v2 v2.23.7 h1:YHDQ46s3VghFHFf1DdF+Sh7H4RqhcM+t0TmZRJx4oJY=
|
||||
github.com/urfave/cli/v2 v2.23.7/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
|
||||
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/varlink/go v0.4.0 h1:+/BQoUO9eJK/+MTSHwFcJch7TMsb6N6Dqp6g0qaXXRo=
|
||||
github.com/varlink/go v0.4.0/go.mod h1:DKg9Y2ctoNkesREGAEak58l+jOC6JU2aqZvUYs5DynU=
|
||||
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.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
|
||||
|
@ -318,15 +445,22 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
|
|||
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
|
||||
github.com/volatiletech/inflect v0.0.1 h1:2a6FcMQyhmPZcLa+uet3VJ8gLn/9svWhJxJYwvE8KsU=
|
||||
github.com/volatiletech/inflect v0.0.1/go.mod h1:IBti31tG6phkHitLlr5j7shC5SOo//x0AjDzaJU1PLA=
|
||||
github.com/volatiletech/strmangle v0.0.6 h1:AdOYE3B2ygRDq4rXDij/MMwq6KVK/pWAYxpC7CLrkKQ=
|
||||
github.com/volatiletech/strmangle v0.0.6/go.mod h1:ycDvbDkjDvhC0NUU8w3fWwl5JEMTV56vTKXzR3GeR+0=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zalando/go-keyring v0.2.5 h1:Bc2HHpjALryKD62ppdEzaFG6VxL6Bc+5v0LYpN8Lba8=
|
||||
github.com/zalando/go-keyring v0.2.5/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s=
|
||||
github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI=
|
||||
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||
|
@ -338,10 +472,15 @@ go.etcd.io/bbolt v1.3.10/go.mod h1:bK3UQLPJZly7IlNmV7uVHJDxfe5aK9Ll93e/74Y9oEQ=
|
|||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ=
|
||||
|
@ -351,6 +490,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
|||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
|
@ -374,6 +514,8 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx
|
|||
golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
|
@ -381,18 +523,22 @@ golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/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.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190529164535-6a60838ec259/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -415,35 +561,44 @@ golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
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-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
|
@ -451,12 +606,15 @@ google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7ya
|
|||
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
|
||||
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
|
@ -469,6 +627,8 @@ modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
|
|||
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
|
||||
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
|
||||
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
|
||||
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
|
||||
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||
|
@ -485,5 +645,7 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
|||
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
mvdan.cc/gofumpt v0.5.0 h1:0EQ+Z56k8tXjj/6TQD25BFNKQXpCvT0rnansIc7Ug5E=
|
||||
mvdan.cc/gofumpt v0.5.0/go.mod h1:HBeVDtMKRZpXyxFciAirzdKklDlGu8aAy1wEbH5Y9js=
|
||||
zombiezen.com/go/sqlite v1.3.0 h1:98g1gnCm+CNz6AuQHu0gqyw7gR2WU3O3PJufDOStpUs=
|
||||
zombiezen.com/go/sqlite v1.3.0/go.mod h1:yRl27//s/9aXU3RWs8uFQwjkTG9gYNGEls6+6SvrclY=
|
||||
|
|
|
@ -3,6 +3,7 @@ package compat
|
|||
import (
|
||||
"net"
|
||||
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
)
|
||||
|
@ -31,10 +32,16 @@ func SubmitDNSCheckDomain(subdomain string) (respondWith net.IP) {
|
|||
|
||||
// ReportSecureDNSBypassIssue reports a DNS bypassing issue for the given process.
|
||||
func ReportSecureDNSBypassIssue(p *process.Process) {
|
||||
secureDNSBypassIssue.notify(p)
|
||||
module.mgr.Go("report secure dns bypass issue", func(w *mgr.WorkerCtx) error {
|
||||
secureDNSBypassIssue.notify(p)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// ReportMultiPeerUDPTunnelIssue reports a multi-peer UDP tunnel for the given process.
|
||||
func ReportMultiPeerUDPTunnelIssue(p *process.Process) {
|
||||
multiPeerUDPTunnelIssue.notify(p)
|
||||
module.mgr.Go("report multi-peer udp tunnel issue", func(w *mgr.WorkerCtx) error {
|
||||
multiPeerUDPTunnelIssue.notify(p)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
@ -181,4 +181,5 @@ func New(instance instance) (*Compat, error) {
|
|||
|
||||
type instance interface {
|
||||
NetEnv() *netenv.NetEnv
|
||||
Resolver() *resolver.ResolverModule
|
||||
}
|
||||
|
|
|
@ -158,6 +158,12 @@ func selfcheck(ctx context.Context) (issue *systemIssue, err error) {
|
|||
|
||||
// Step 3: Have the nameserver respond with random data in the answer section.
|
||||
|
||||
// Check if the resolver is enabled
|
||||
if module.instance.Resolver().IsDisabled() {
|
||||
// There is no control over the response, there is nothing more that can be checked.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Wait for the reply from the resolver.
|
||||
select {
|
||||
case err := <-dnsCheckLookupError:
|
||||
|
|
|
@ -1,43 +1,52 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/safing/portmaster/base/database"
|
||||
_ "github.com/safing/portmaster/base/database/dbmodule"
|
||||
_ "github.com/safing/portmaster/base/database/storage/bbolt"
|
||||
_ "github.com/safing/portmaster/base/database/storage/sqlite"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
)
|
||||
|
||||
// Default Values (changeable for testing).
|
||||
var (
|
||||
DefaultDatabaseStorageType = "bbolt"
|
||||
DefaultDatabaseStorageType = "sqlite"
|
||||
)
|
||||
|
||||
func registerDatabases() error {
|
||||
// If there is an existing bbolt core database, use it instead.
|
||||
coreStorageType := DefaultDatabaseStorageType
|
||||
if utils.PathExists(filepath.Join(dataroot.Root().Path, database.DatabasesSubDir, "core", "bbolt")) {
|
||||
coreStorageType = "bbolt"
|
||||
}
|
||||
|
||||
// Register core database.
|
||||
_, err := database.Register(&database.Database{
|
||||
Name: "core",
|
||||
Description: "Holds core data, such as settings and profiles",
|
||||
StorageType: DefaultDatabaseStorageType,
|
||||
StorageType: coreStorageType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If there is an existing cache bbolt database, use it instead.
|
||||
cacheStorageType := DefaultDatabaseStorageType
|
||||
if utils.PathExists(filepath.Join(dataroot.Root().Path, database.DatabasesSubDir, "cache", "bbolt")) {
|
||||
cacheStorageType = "bbolt"
|
||||
}
|
||||
|
||||
// Register cache database.
|
||||
_, err = database.Register(&database.Database{
|
||||
Name: "cache",
|
||||
Description: "Cached data, such as Intelligence and DNS Records",
|
||||
StorageType: DefaultDatabaseStorageType,
|
||||
StorageType: cacheStorageType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// _, err = database.Register(&database.Database{
|
||||
// Name: "history",
|
||||
// Description: "Historic event data",
|
||||
// StorageType: DefaultDatabaseStorageType,
|
||||
// })
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/safing/portmaster/base/api"
|
||||
"github.com/safing/portmaster/base/database/dbmodule"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/info"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
)
|
||||
|
||||
|
@ -54,10 +56,13 @@ func prep(instance instance) error {
|
|||
}
|
||||
|
||||
// initialize structure
|
||||
err := dataroot.Initialize(dataDir, 0o0755)
|
||||
err := dataroot.Initialize(dataDir, utils.PublicReadPermission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set root dir for the databases.
|
||||
dbmodule.SetDatabaseLocation(dataroot.Root())
|
||||
}
|
||||
|
||||
// set api listen address
|
||||
|
|
|
@ -43,8 +43,24 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
|
|||
return endpoints.NoMatch, "", nil
|
||||
}
|
||||
|
||||
// If Portmaster resolver is disabled allow requests going to system dns resolver.
|
||||
// And allow all connections out of the System Resolver.
|
||||
if module.instance.Resolver().IsDisabled() {
|
||||
// TODO(vladimir): Is there a more specific check that can be done?
|
||||
if conn.Process().IsSystemResolver() {
|
||||
return endpoints.NoMatch, "", nil
|
||||
}
|
||||
if conn.Entity.Port == 53 && conn.Entity.IPScope.IsLocalhost() {
|
||||
return endpoints.NoMatch, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// Block bypass attempts using an (encrypted) DNS server.
|
||||
switch {
|
||||
case looksLikeOutgoingDNSRequest(conn) && module.instance.Resolver().IsDisabled():
|
||||
// Allow. Packet will be analyzed and blocked if its not a dns request, before sent.
|
||||
conn.Inspecting = true
|
||||
return endpoints.NoMatch, "", nil
|
||||
case conn.Entity.Port == 53:
|
||||
return endpoints.Denied,
|
||||
"blocked DNS query, manual dns setup required",
|
||||
|
@ -62,3 +78,17 @@ func PreventBypassing(ctx context.Context, conn *network.Connection) (endpoints.
|
|||
|
||||
return endpoints.NoMatch, "", nil
|
||||
}
|
||||
|
||||
func looksLikeOutgoingDNSRequest(conn *network.Connection) bool {
|
||||
// Outbound on remote port 53, UDP.
|
||||
if conn.Inbound {
|
||||
return false
|
||||
}
|
||||
if conn.Entity.Port != 53 {
|
||||
return false
|
||||
}
|
||||
if conn.IPProtocol != packet.UDP {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -287,6 +287,30 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
|
|||
}
|
||||
}
|
||||
|
||||
// Create new record for this IP.
|
||||
record := resolver.ResolvedDomain{
|
||||
Domain: q.FQDN,
|
||||
Resolver: rrCache.Resolver,
|
||||
DNSRequestContext: rrCache.ToDNSRequestContext(),
|
||||
Expires: rrCache.Expires,
|
||||
}
|
||||
// Process CNAMEs
|
||||
record.AddCNAMEs(cnames)
|
||||
// Link connection with cnames.
|
||||
if conn.Type == network.DNSRequest {
|
||||
conn.Entity.CNAME = record.CNAMEs
|
||||
}
|
||||
|
||||
SaveIPsInCache(ips, profileID, record)
|
||||
}
|
||||
|
||||
// formatRR is a friendlier alternative to miekg/dns.RR.String().
|
||||
func formatRR(rr dns.RR) string {
|
||||
return strings.ReplaceAll(rr.String(), "\t", " ")
|
||||
}
|
||||
|
||||
// SaveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
|
||||
func SaveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
|
||||
// Package IPs and CNAMEs into IPInfo structs.
|
||||
for _, ip := range ips {
|
||||
// Never save domain attributions for localhost IPs.
|
||||
|
@ -294,31 +318,6 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
|
|||
continue
|
||||
}
|
||||
|
||||
// Create new record for this IP.
|
||||
record := resolver.ResolvedDomain{
|
||||
Domain: q.FQDN,
|
||||
Resolver: rrCache.Resolver,
|
||||
DNSRequestContext: rrCache.ToDNSRequestContext(),
|
||||
Expires: rrCache.Expires,
|
||||
}
|
||||
|
||||
// Resolve all CNAMEs in the correct order and add the to the record.
|
||||
domain := q.FQDN
|
||||
for {
|
||||
nextDomain, isCNAME := cnames[domain]
|
||||
if !isCNAME {
|
||||
break
|
||||
}
|
||||
|
||||
record.CNAMEs = append(record.CNAMEs, nextDomain)
|
||||
domain = nextDomain
|
||||
}
|
||||
|
||||
// Update the entity to include the CNAMEs of the query response.
|
||||
conn.Entity.CNAME = record.CNAMEs
|
||||
|
||||
// Check if there is an existing record for this DNS response.
|
||||
// Else create a new one.
|
||||
ipString := ip.String()
|
||||
info, err := resolver.GetIPInfo(profileID, ipString)
|
||||
if err != nil {
|
||||
|
@ -341,8 +340,3 @@ func UpdateIPsAndCNAMEs(q *resolver.Query, rrCache *resolver.RRCache, conn *netw
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// formatRR is a friendlier alternative to miekg/dns.RR.String().
|
||||
func formatRR(rr dns.RR) string {
|
||||
return strings.ReplaceAll(rr.String(), "\t", " ")
|
||||
}
|
||||
|
|
109
service/firewall/interception/dnsmonitor/etwlink_windows.go
Normal file
109
service/firewall/interception/dnsmonitor/etwlink_windows.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dnsmonitor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/service/integration"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type ETWSession struct {
|
||||
i *integration.ETWFunctions
|
||||
|
||||
shutdownGuard atomic.Bool
|
||||
shutdownMutex sync.Mutex
|
||||
|
||||
state uintptr
|
||||
}
|
||||
|
||||
// NewSession creates new ETW event listener and initializes it. This is a low level interface, make sure to call DestroySession when you are done using it.
|
||||
func NewSession(etwInterface *integration.ETWFunctions, callback func(domain string, pid uint32, result string)) (*ETWSession, error) {
|
||||
if etwInterface == nil {
|
||||
return nil, fmt.Errorf("etw interface was nil")
|
||||
}
|
||||
etwSession := &ETWSession{
|
||||
i: etwInterface,
|
||||
}
|
||||
|
||||
// Make sure session from previous instances are not running.
|
||||
_ = etwSession.i.StopOldSession()
|
||||
|
||||
// Initialize notification activated callback
|
||||
win32Callback := windows.NewCallback(func(domain *uint16, pid uint32, result *uint16) uintptr {
|
||||
callback(windows.UTF16PtrToString(domain), pid, windows.UTF16PtrToString(result))
|
||||
return 0
|
||||
})
|
||||
// The function only allocates memory it will not fail.
|
||||
etwSession.state = etwSession.i.CreateState(win32Callback)
|
||||
|
||||
// Make sure DestroySession is called even if caller forgets to call it.
|
||||
runtime.SetFinalizer(etwSession, func(s *ETWSession) {
|
||||
_ = s.i.DestroySession(s.state)
|
||||
})
|
||||
|
||||
// Initialize session.
|
||||
err := etwSession.i.InitializeSession(etwSession.state)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize session: %q", err)
|
||||
}
|
||||
|
||||
return etwSession, nil
|
||||
}
|
||||
|
||||
// StartTrace starts the tracing session of dns events. This is a blocking call. It will not return until the trace is stopped.
|
||||
func (l *ETWSession) StartTrace() error {
|
||||
return l.i.StartTrace(l.state)
|
||||
}
|
||||
|
||||
// IsRunning returns true if DestroySession has NOT been called.
|
||||
func (l *ETWSession) IsRunning() bool {
|
||||
return !l.shutdownGuard.Load()
|
||||
}
|
||||
|
||||
// FlushTrace flushes the trace buffer.
|
||||
func (l *ETWSession) FlushTrace() error {
|
||||
if l.i == nil {
|
||||
return fmt.Errorf("session not initialized")
|
||||
}
|
||||
|
||||
l.shutdownMutex.Lock()
|
||||
defer l.shutdownMutex.Unlock()
|
||||
|
||||
// Make sure session is still running.
|
||||
if l.shutdownGuard.Load() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return l.i.FlushTrace(l.state)
|
||||
}
|
||||
|
||||
// StopTrace stops the trace. This will cause StartTrace to return.
|
||||
func (l *ETWSession) StopTrace() error {
|
||||
return l.i.StopTrace(l.state)
|
||||
}
|
||||
|
||||
// DestroySession closes the session and frees the allocated memory. Listener cannot be used after this function is called.
|
||||
func (l *ETWSession) DestroySession() error {
|
||||
if l.i == nil {
|
||||
return fmt.Errorf("session not initialized")
|
||||
}
|
||||
l.shutdownMutex.Lock()
|
||||
defer l.shutdownMutex.Unlock()
|
||||
|
||||
if l.shutdownGuard.Swap(true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := l.i.DestroySession(l.state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
l.state = 0
|
||||
return nil
|
||||
}
|
19
service/firewall/interception/dnsmonitor/eventlistener.go
Normal file
19
service/firewall/interception/dnsmonitor/eventlistener.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
//go:build !linux && !windows
|
||||
// +build !linux,!windows
|
||||
|
||||
package dnsmonitor
|
||||
|
||||
type Listener struct{}
|
||||
|
||||
func newListener(_ *DNSMonitor) (*Listener, error) {
|
||||
return &Listener{}, nil
|
||||
}
|
||||
|
||||
func (l *Listener) flush() error {
|
||||
// Nothing to flush
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) stop() error {
|
||||
return nil
|
||||
}
|
145
service/firewall/interception/dnsmonitor/eventlistener_linux.go
Normal file
145
service/firewall/interception/dnsmonitor/eventlistener_linux.go
Normal file
|
@ -0,0 +1,145 @@
|
|||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package dnsmonitor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/varlink/go/varlink"
|
||||
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
varlinkConn *varlink.Connection
|
||||
}
|
||||
|
||||
func newListener(module *DNSMonitor) (*Listener, error) {
|
||||
// Set source of the resolver.
|
||||
ResolverInfo.Source = resolver.ServerSourceSystemd
|
||||
|
||||
// Check if the system has systemd-resolver.
|
||||
_, err := os.Stat("/run/systemd/resolve/io.systemd.Resolve.Monitor")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system does not support systemd resolver monitor")
|
||||
}
|
||||
|
||||
listener := &Listener{}
|
||||
|
||||
restartAttempts := 0
|
||||
|
||||
module.mgr.Go("systemd-resolver-event-listener", func(w *mgr.WorkerCtx) error {
|
||||
// Abort initialization if the connection failed after too many tries.
|
||||
if restartAttempts > 10 {
|
||||
return nil
|
||||
}
|
||||
restartAttempts += 1
|
||||
|
||||
// Initialize varlink connection
|
||||
varlinkConn, err := varlink.NewConnection(module.mgr.Ctx(), "unix:/run/systemd/resolve/io.systemd.Resolve.Monitor")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to systemd-resolver varlink service: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if varlinkConn != nil {
|
||||
err = varlinkConn.Close()
|
||||
if err != nil {
|
||||
log.Errorf("dnsmonitor: failed to close varlink connection: %s", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
listener.varlinkConn = varlinkConn
|
||||
// Subscribe to the dns query events
|
||||
receive, err := listener.varlinkConn.Send(w.Ctx(), "io.systemd.Resolve.Monitor.SubscribeQueryResults", nil, varlink.More)
|
||||
if err != nil {
|
||||
var varlinkErr *varlink.Error
|
||||
if errors.As(err, &varlinkErr) {
|
||||
return fmt.Errorf("failed to issue Varlink call: %+v", varlinkErr.Parameters)
|
||||
} else {
|
||||
return fmt.Errorf("failed to issue Varlink call: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
queryResult := QueryResult{}
|
||||
// Receive the next event from the resolver.
|
||||
flags, err := receive(w.Ctx(), &queryResult)
|
||||
if err != nil {
|
||||
var varlinkErr *varlink.Error
|
||||
if errors.As(err, &varlinkErr) {
|
||||
return fmt.Errorf("failed to receive Varlink reply: %+v", varlinkErr.Parameters)
|
||||
} else {
|
||||
return fmt.Errorf("failed to receive Varlink reply: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the reply indicates the end of the stream
|
||||
if flags&varlink.Continues == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Ignore if there is no question.
|
||||
if queryResult.Question == nil || len(*queryResult.Question) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Protmaster self check
|
||||
domain := (*queryResult.Question)[0].Name
|
||||
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
|
||||
// Not need to process result.
|
||||
continue
|
||||
}
|
||||
|
||||
if queryResult.Rcode != nil {
|
||||
continue // Ignore DNS errors
|
||||
}
|
||||
|
||||
listener.processAnswer(domain, &queryResult)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (l *Listener) flush() error {
|
||||
// Nothing to flush
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) processAnswer(domain string, queryResult *QueryResult) {
|
||||
// Allocated data struct for the parsed result.
|
||||
cnames := make(map[string]string)
|
||||
ips := make([]net.IP, 0, 5)
|
||||
|
||||
// Check if the query is valid
|
||||
if queryResult.Answer == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Go trough each answer entry.
|
||||
for _, a := range *queryResult.Answer {
|
||||
if a.RR.Address != nil {
|
||||
ip := net.IP(*a.RR.Address)
|
||||
// Answer contains ip address.
|
||||
ips = append(ips, ip)
|
||||
|
||||
} else if a.RR.Name != nil {
|
||||
// Answer is a CNAME.
|
||||
cnames[domain] = *a.RR.Name
|
||||
}
|
||||
}
|
||||
|
||||
saveDomain(domain, ips, cnames, resolver.IPInfoProfileScopeGlobal)
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dnsmonitor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
etw *ETWSession
|
||||
}
|
||||
|
||||
func newListener(module *DNSMonitor) (*Listener, error) {
|
||||
// Set source of the resolver.
|
||||
ResolverInfo.Source = resolver.ServerSourceETW
|
||||
|
||||
listener := &Listener{}
|
||||
// Initialize new dns event session.
|
||||
err := initializeSessions(module, listener)
|
||||
if err != nil {
|
||||
// Listen for event if the dll has been loaded
|
||||
module.instance.OSIntegration().OnInitializedEvent.AddCallback("loader-listener", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
err = initializeSessions(module, listener)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
})
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func initializeSessions(module *DNSMonitor, listener *Listener) error {
|
||||
var err error
|
||||
listener.etw, err = NewSession(module.instance.OSIntegration().GetETWInterface(), listener.processEvent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Start listener
|
||||
module.mgr.Go("etw-dns-event-listener", func(w *mgr.WorkerCtx) error {
|
||||
return listener.etw.StartTrace()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) flush() error {
|
||||
if l.etw == nil {
|
||||
return fmt.Errorf("etw not initialized")
|
||||
}
|
||||
return l.etw.FlushTrace()
|
||||
}
|
||||
|
||||
func (l *Listener) stop() error {
|
||||
if l == nil {
|
||||
return fmt.Errorf("listener is nil")
|
||||
}
|
||||
if l.etw == nil {
|
||||
return fmt.Errorf("invalid etw session")
|
||||
}
|
||||
// Stop and destroy trace. Destroy should be called even if stop fails for some reason.
|
||||
err := l.etw.StopTrace()
|
||||
err2 := l.etw.DestroySession()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("StopTrace failed: %w", err)
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
return fmt.Errorf("DestroySession failed: %w", err2)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) processEvent(domain string, pid uint32, result string) {
|
||||
if processIfSelfCheckDomain(dns.Fqdn(domain)) {
|
||||
// Not need to process result.
|
||||
return
|
||||
}
|
||||
|
||||
// Ignore empty results
|
||||
if len(result) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
profileScope := resolver.IPInfoProfileScopeGlobal
|
||||
// Get the profile ID if the process can be found
|
||||
if proc, err := process.GetOrFindProcess(context.Background(), int(pid)); err == nil {
|
||||
if profile := proc.Profile(); profile != nil {
|
||||
if localProfile := profile.LocalProfile(); localProfile != nil {
|
||||
profileScope = localProfile.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
cnames := make(map[string]string)
|
||||
ips := []net.IP{}
|
||||
|
||||
resultArray := strings.Split(result, ";")
|
||||
for _, r := range resultArray {
|
||||
// For results other than IP addresses, the string starts with "type:"
|
||||
if strings.HasPrefix(r, "type:") {
|
||||
dnsValueArray := strings.Split(r, " ")
|
||||
if len(dnsValueArray) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Ignore everything except CNAME records
|
||||
if value, err := strconv.ParseInt(dnsValueArray[1], 10, 16); err == nil && value == int64(dns.TypeCNAME) {
|
||||
cnames[domain] = dnsValueArray[2]
|
||||
}
|
||||
|
||||
} else {
|
||||
// If the event doesn't start with "type:", it's an IP address
|
||||
ip := net.ParseIP(r)
|
||||
if ip != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
saveDomain(domain, ips, cnames, profileScope)
|
||||
}
|
139
service/firewall/interception/dnsmonitor/module.go
Normal file
139
service/firewall/interception/dnsmonitor/module.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package dnsmonitor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/compat"
|
||||
"github.com/safing/portmaster/service/integration"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
)
|
||||
|
||||
var ResolverInfo = resolver.ResolverInfo{
|
||||
Name: "SystemResolver",
|
||||
Type: resolver.ServerTypeMonitor,
|
||||
}
|
||||
|
||||
type DNSMonitor struct {
|
||||
instance instance
|
||||
mgr *mgr.Manager
|
||||
|
||||
listener *Listener
|
||||
}
|
||||
|
||||
// Manager returns the module manager.
|
||||
func (dl *DNSMonitor) Manager() *mgr.Manager {
|
||||
return dl.mgr
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (dl *DNSMonitor) Start() error {
|
||||
// Initialize dns event listener
|
||||
var err error
|
||||
dl.listener, err = newListener(dl)
|
||||
if err != nil {
|
||||
log.Warningf("dnsmonitor: failed to start dns listener: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (dl *DNSMonitor) Stop() error {
|
||||
if dl.listener != nil {
|
||||
err := dl.listener.stop()
|
||||
if err != nil {
|
||||
log.Errorf("dnsmonitor: failed to close listener: %s", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush flushes the buffer forcing all events to be processed.
|
||||
func (dl *DNSMonitor) Flush() error {
|
||||
return dl.listener.flush()
|
||||
}
|
||||
|
||||
func saveDomain(domain string, ips []net.IP, cnames map[string]string, profileScope string) {
|
||||
fqdn := dns.Fqdn(domain)
|
||||
// Create new record for this IP.
|
||||
record := resolver.ResolvedDomain{
|
||||
Domain: fqdn,
|
||||
Resolver: &ResolverInfo,
|
||||
DNSRequestContext: &resolver.DNSRequestContext{},
|
||||
Expires: 0,
|
||||
}
|
||||
|
||||
// Process cnames
|
||||
record.AddCNAMEs(cnames)
|
||||
|
||||
// Add to cache
|
||||
saveIPsInCache(ips, profileScope, record)
|
||||
}
|
||||
|
||||
func New(instance instance) (*DNSMonitor, error) {
|
||||
// Initialize module
|
||||
m := mgr.New("DNSMonitor")
|
||||
module := &DNSMonitor{
|
||||
mgr: m,
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
OSIntegration() *integration.OSIntegration
|
||||
}
|
||||
|
||||
func processIfSelfCheckDomain(fqdn string) bool {
|
||||
// Check for compat check dns request.
|
||||
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
|
||||
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
|
||||
_ = compat.SubmitDNSCheckDomain(subdomain)
|
||||
log.Infof("dnsmonitor: self-check domain received")
|
||||
// No need to parse the answer.
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// saveIPsInCache saves the provided ips in the dns cashe assoseted with the record Domain and CNAMEs.
|
||||
func saveIPsInCache(ips []net.IP, profileID string, record resolver.ResolvedDomain) {
|
||||
// Package IPs and CNAMEs into IPInfo structs.
|
||||
for _, ip := range ips {
|
||||
// Never save domain attributions for localhost IPs.
|
||||
if netutils.GetIPScope(ip) == netutils.HostLocal {
|
||||
continue
|
||||
}
|
||||
|
||||
ipString := ip.String()
|
||||
info, err := resolver.GetIPInfo(profileID, ipString)
|
||||
if err != nil {
|
||||
if !errors.Is(err, database.ErrNotFound) {
|
||||
log.Errorf("dnsmonitor: failed to search for IP info record: %s", err)
|
||||
}
|
||||
|
||||
info = &resolver.IPInfo{
|
||||
IP: ipString,
|
||||
ProfileID: profileID,
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new record to the resolved domains for this IP and scope.
|
||||
info.AddDomain(record)
|
||||
|
||||
// Save if the record is new or has been updated.
|
||||
if err := info.Save(); err != nil {
|
||||
log.Errorf("dnsmonitor: failed to save IP info record: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
83
service/firewall/interception/dnsmonitor/varlinktypes.go
Normal file
83
service/firewall/interception/dnsmonitor/varlinktypes.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package dnsmonitor
|
||||
|
||||
// List of struct that define the systemd-resolver varlink dns event protocol.
|
||||
// Source: `sudo varlinkctl introspect /run/systemd/resolve/io.systemd.Resolve.Monitor io.systemd.Resolve.Monitor`
|
||||
|
||||
type ResourceKey struct {
|
||||
Class int `json:"class"`
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type ResourceRecord struct {
|
||||
Key ResourceKey `json:"key"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Address *[]byte `json:"address,omitempty"`
|
||||
// Rest of the fields are not used.
|
||||
// Priority *int `json:"priority,omitempty"`
|
||||
// Weight *int `json:"weight,omitempty"`
|
||||
// Port *int `json:"port,omitempty"`
|
||||
// CPU *string `json:"cpu,omitempty"`
|
||||
// OS *string `json:"os,omitempty"`
|
||||
// Items *[]string `json:"items,omitempty"`
|
||||
// MName *string `json:"mname,omitempty"`
|
||||
// RName *string `json:"rname,omitempty"`
|
||||
// Serial *int `json:"serial,omitempty"`
|
||||
// Refresh *int `json:"refresh,omitempty"`
|
||||
// Expire *int `json:"expire,omitempty"`
|
||||
// Minimum *int `json:"minimum,omitempty"`
|
||||
// Exchange *string `json:"exchange,omitempty"`
|
||||
// Version *int `json:"version,omitempty"`
|
||||
// Size *int `json:"size,omitempty"`
|
||||
// HorizPre *int `json:"horiz_pre,omitempty"`
|
||||
// VertPre *int `json:"vert_pre,omitempty"`
|
||||
// Latitude *int `json:"latitude,omitempty"`
|
||||
// Longitude *int `json:"longitude,omitempty"`
|
||||
// Altitude *int `json:"altitude,omitempty"`
|
||||
// KeyTag *int `json:"key_tag,omitempty"`
|
||||
// Algorithm *int `json:"algorithm,omitempty"`
|
||||
// DigestType *int `json:"digest_type,omitempty"`
|
||||
// Digest *string `json:"digest,omitempty"`
|
||||
// FPType *int `json:"fptype,omitempty"`
|
||||
// Fingerprint *string `json:"fingerprint,omitempty"`
|
||||
// Flags *int `json:"flags,omitempty"`
|
||||
// Protocol *int `json:"protocol,omitempty"`
|
||||
// DNSKey *string `json:"dnskey,omitempty"`
|
||||
// Signer *string `json:"signer,omitempty"`
|
||||
// TypeCovered *int `json:"type_covered,omitempty"`
|
||||
// Labels *int `json:"labels,omitempty"`
|
||||
// OriginalTTL *int `json:"original_ttl,omitempty"`
|
||||
// Expiration *int `json:"expiration,omitempty"`
|
||||
// Inception *int `json:"inception,omitempty"`
|
||||
// Signature *string `json:"signature,omitempty"`
|
||||
// NextDomain *string `json:"next_domain,omitempty"`
|
||||
// Types *[]int `json:"types,omitempty"`
|
||||
// Iterations *int `json:"iterations,omitempty"`
|
||||
// Salt *string `json:"salt,omitempty"`
|
||||
// Hash *string `json:"hash,omitempty"`
|
||||
// CertUsage *int `json:"cert_usage,omitempty"`
|
||||
// Selector *int `json:"selector,omitempty"`
|
||||
// MatchingType *int `json:"matching_type,omitempty"`
|
||||
// Data *string `json:"data,omitempty"`
|
||||
// Tag *string `json:"tag,omitempty"`
|
||||
// Value *string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
type Answer struct {
|
||||
RR *ResourceRecord `json:"rr,omitempty"`
|
||||
Raw string `json:"raw"`
|
||||
IfIndex *int `json:"ifindex,omitempty"`
|
||||
}
|
||||
|
||||
type QueryResult struct {
|
||||
Ready *bool `json:"ready,omitempty"`
|
||||
State *string `json:"state,omitempty"`
|
||||
Rcode *int `json:"rcode,omitempty"`
|
||||
Errno *int `json:"errno,omitempty"`
|
||||
Question *[]ResourceKey `json:"question,omitempty"`
|
||||
CollectedQuestions *[]ResourceKey `json:"collectedQuestions,omitempty"`
|
||||
Answer *[]Answer `json:"answer,omitempty"`
|
||||
}
|
|
@ -188,7 +188,7 @@ func (q *Queue) packetHandler(ctx context.Context) func(nfqueue.Attribute) int {
|
|||
return 0
|
||||
}
|
||||
|
||||
if err := pmpacket.Parse(*attrs.Payload, &pkt.Base); err != nil {
|
||||
if err := pmpacket.ParseLayer3(*attrs.Payload, &pkt.Base); err != nil {
|
||||
log.Warningf("nfqueue: failed to parse payload: %s", err)
|
||||
_ = pkt.Drop()
|
||||
return 0
|
||||
|
|
|
@ -59,7 +59,7 @@ func (pkt *Packet) LoadPacketData() error {
|
|||
return packet.ErrFailedToLoadPayload
|
||||
}
|
||||
|
||||
err = packet.Parse(payload, &pkt.Base)
|
||||
err = packet.ParseLayer3(payload, &pkt.Base)
|
||||
if err != nil {
|
||||
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
|
||||
return packet.ErrFailedToLoadPayload
|
||||
|
|
|
@ -55,6 +55,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch
|
|||
newPacket := &Packet{
|
||||
verdictRequest: conn.ID,
|
||||
payload: conn.Payload,
|
||||
payloadLayer: conn.PayloadLayer,
|
||||
verdictSet: abool.NewBool(false),
|
||||
}
|
||||
info := newPacket.Info()
|
||||
|
@ -106,6 +107,7 @@ func Handler(ctx context.Context, packets chan packet.Packet, bandwidthUpdate ch
|
|||
newPacket := &Packet{
|
||||
verdictRequest: conn.ID,
|
||||
payload: conn.Payload,
|
||||
payloadLayer: conn.PayloadLayer,
|
||||
verdictSet: abool.NewBool(false),
|
||||
}
|
||||
info := newPacket.Info()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package windowskext
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
@ -19,6 +20,7 @@ type Packet struct {
|
|||
|
||||
verdictRequest uint64
|
||||
payload []byte
|
||||
payloadLayer uint8
|
||||
verdictSet *abool.AtomicBool
|
||||
|
||||
payloadLoaded bool
|
||||
|
@ -51,7 +53,15 @@ func (pkt *Packet) LoadPacketData() error {
|
|||
pkt.payloadLoaded = true
|
||||
|
||||
if len(pkt.payload) > 0 {
|
||||
err := packet.Parse(pkt.payload, &pkt.Base)
|
||||
var err error
|
||||
switch pkt.payloadLayer {
|
||||
case 3:
|
||||
err = packet.ParseLayer3(pkt.payload, &pkt.Base)
|
||||
case 4:
|
||||
err = packet.ParseLayer4(pkt.payload, &pkt.Base)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported payload layer: %d", pkt.payloadLayer)
|
||||
}
|
||||
if err != nil {
|
||||
log.Tracef("payload: %#v", pkt.payload)
|
||||
log.Tracer(pkt.Ctx()).Warningf("windowskext: failed to parse payload: %s", err)
|
||||
|
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/safing/portmaster/service/netquery"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
"github.com/safing/portmaster/spn/access"
|
||||
"github.com/safing/portmaster/spn/captain"
|
||||
)
|
||||
|
@ -34,8 +35,7 @@ func (ss *stringSliceFlag) Set(value string) error {
|
|||
var allowedClients stringSliceFlag
|
||||
|
||||
type Firewall struct {
|
||||
mgr *mgr.Manager
|
||||
|
||||
mgr *mgr.Manager
|
||||
instance instance
|
||||
}
|
||||
|
||||
|
@ -165,4 +165,5 @@ type instance interface {
|
|||
Access() *access.Access
|
||||
Network() *network.Network
|
||||
NetQuery() *netquery.NetQuery
|
||||
Resolver() *resolver.ResolverModule
|
||||
}
|
||||
|
|
|
@ -6,10 +6,12 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portmaster/base/log"
|
||||
|
@ -23,6 +25,7 @@ import (
|
|||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
"github.com/safing/portmaster/spn/access"
|
||||
)
|
||||
|
||||
|
@ -444,8 +447,9 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
|
|||
filterConnection = false
|
||||
log.Tracer(pkt.Ctx()).Infof("filter: granting own pre-authenticated connection %s", conn)
|
||||
|
||||
// Redirect outbound DNS packets if enabled,
|
||||
// Redirect outbound DNS packets if enabled,
|
||||
case dnsQueryInterception() &&
|
||||
!module.instance.Resolver().IsDisabled() &&
|
||||
pkt.IsOutbound() &&
|
||||
pkt.Info().DstPort == 53 &&
|
||||
// that don't match the address of our nameserver,
|
||||
|
@ -478,11 +482,13 @@ func filterHandler(conn *network.Connection, pkt packet.Packet) {
|
|||
|
||||
// Decide how to continue handling connection.
|
||||
switch {
|
||||
case conn.Inspecting && looksLikeOutgoingDNSRequest(conn):
|
||||
inspectDNSPacket(conn, pkt)
|
||||
conn.UpdateFirewallHandler(inspectDNSPacket)
|
||||
case conn.Inspecting:
|
||||
log.Tracer(pkt.Ctx()).Trace("filter: start inspecting")
|
||||
conn.UpdateFirewallHandler(inspectAndVerdictHandler)
|
||||
inspectAndVerdictHandler(conn, pkt)
|
||||
|
||||
default:
|
||||
conn.StopFirewallHandler()
|
||||
verdictHandler(conn, pkt)
|
||||
|
@ -506,7 +512,7 @@ func FilterConnection(ctx context.Context, conn *network.Connection, pkt packet.
|
|||
}
|
||||
|
||||
// TODO: Enable inspection framework again.
|
||||
conn.Inspecting = false
|
||||
// conn.Inspecting = false
|
||||
|
||||
// TODO: Quick fix for the SPN.
|
||||
// Use inspection framework for proper encryption detection.
|
||||
|
@ -580,6 +586,98 @@ func inspectAndVerdictHandler(conn *network.Connection, pkt packet.Packet) {
|
|||
issueVerdict(conn, pkt, 0, true)
|
||||
}
|
||||
|
||||
func inspectDNSPacket(conn *network.Connection, pkt packet.Packet) {
|
||||
// Ignore info-only packets in this handler.
|
||||
if pkt.InfoOnly() {
|
||||
return
|
||||
}
|
||||
|
||||
dnsPacket := new(dns.Msg)
|
||||
err := pkt.LoadPacketData()
|
||||
if err != nil {
|
||||
_ = pkt.Block()
|
||||
log.Errorf("filter: failed to load packet payload: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse and block invalid packets.
|
||||
err = dnsPacket.Unpack(pkt.Payload())
|
||||
if err != nil {
|
||||
err = pkt.PermanentBlock()
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to block packet: %s", err)
|
||||
}
|
||||
_ = conn.SetVerdict(network.VerdictBlock, "none DNS data on DNS port", "", nil)
|
||||
conn.VerdictPermanent = true
|
||||
conn.Save()
|
||||
return
|
||||
}
|
||||
|
||||
// Packet was parsed.
|
||||
// Allow it but only after the answer was added to the cache.
|
||||
defer func() {
|
||||
err = pkt.Accept()
|
||||
if err != nil {
|
||||
log.Errorf("filter: failed to accept dns packet: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Check if packet has a question.
|
||||
if len(dnsPacket.Question) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Read create structs with the needed data.
|
||||
question := dnsPacket.Question[0]
|
||||
fqdn := dns.Fqdn(question.Name)
|
||||
|
||||
// Check for compat check dns request.
|
||||
if strings.HasSuffix(fqdn, compat.DNSCheckInternalDomainScope) {
|
||||
subdomain := strings.TrimSuffix(fqdn, compat.DNSCheckInternalDomainScope)
|
||||
_ = compat.SubmitDNSCheckDomain(subdomain)
|
||||
log.Infof("packet_handler: self-check domain received")
|
||||
// No need to parse the answer.
|
||||
return
|
||||
}
|
||||
|
||||
// Check if there is an answer.
|
||||
if len(dnsPacket.Answer) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
resolverInfo := &resolver.ResolverInfo{
|
||||
Name: "DNSRequestObserver",
|
||||
Type: resolver.ServerTypeFirewall,
|
||||
Source: resolver.ServerSourceFirewall,
|
||||
IP: conn.Entity.IP,
|
||||
Domain: conn.Entity.Domain,
|
||||
IPScope: conn.Entity.IPScope,
|
||||
}
|
||||
|
||||
rrCache := &resolver.RRCache{
|
||||
Domain: fqdn,
|
||||
Question: dns.Type(question.Qtype),
|
||||
RCode: dnsPacket.Rcode,
|
||||
Answer: dnsPacket.Answer,
|
||||
Ns: dnsPacket.Ns,
|
||||
Extra: dnsPacket.Extra,
|
||||
Resolver: resolverInfo,
|
||||
}
|
||||
|
||||
query := &resolver.Query{
|
||||
FQDN: fqdn,
|
||||
QType: dns.Type(question.Qtype),
|
||||
NoCaching: false,
|
||||
IgnoreFailing: false,
|
||||
LocalResolversOnly: false,
|
||||
ICANNSpace: false,
|
||||
DomainRoot: "",
|
||||
}
|
||||
|
||||
// Save to cache
|
||||
UpdateIPsAndCNAMEs(query, rrCache, conn)
|
||||
}
|
||||
|
||||
func icmpFilterHandler(conn *network.Connection, pkt packet.Packet) {
|
||||
// Load packet data.
|
||||
err := pkt.LoadPacketData()
|
||||
|
|
|
@ -19,6 +19,8 @@ import (
|
|||
"github.com/safing/portmaster/service/core/base"
|
||||
"github.com/safing/portmaster/service/firewall"
|
||||
"github.com/safing/portmaster/service/firewall/interception"
|
||||
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
|
||||
"github.com/safing/portmaster/service/integration"
|
||||
"github.com/safing/portmaster/service/intel/customlists"
|
||||
"github.com/safing/portmaster/service/intel/filterlists"
|
||||
"github.com/safing/portmaster/service/intel/geoip"
|
||||
|
@ -65,6 +67,7 @@ type Instance struct {
|
|||
|
||||
core *core.Core
|
||||
updates *updates.Updates
|
||||
integration *integration.OSIntegration
|
||||
geoip *geoip.GeoIP
|
||||
netenv *netenv.NetEnv
|
||||
ui *ui.UI
|
||||
|
@ -74,6 +77,7 @@ type Instance struct {
|
|||
firewall *firewall.Firewall
|
||||
filterLists *filterlists.FilterLists
|
||||
interception *interception.Interception
|
||||
dnsmonitor *dnsmonitor.DNSMonitor
|
||||
customlist *customlists.CustomList
|
||||
status *status.Status
|
||||
broadcasts *broadcasts.Broadcasts
|
||||
|
@ -107,7 +111,6 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
|||
instance.ctx, instance.cancelCtx = context.WithCancel(context.Background())
|
||||
|
||||
var err error
|
||||
|
||||
// Base modules
|
||||
instance.base, err = base.New(instance)
|
||||
if err != nil {
|
||||
|
@ -151,6 +154,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
|||
if err != nil {
|
||||
return instance, fmt.Errorf("create updates module: %w", err)
|
||||
}
|
||||
instance.integration, err = integration.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create integration module: %w", err)
|
||||
}
|
||||
instance.geoip, err = geoip.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create customlist module: %w", err)
|
||||
|
@ -187,6 +194,10 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
|||
if err != nil {
|
||||
return instance, fmt.Errorf("create interception module: %w", err)
|
||||
}
|
||||
instance.dnsmonitor, err = dnsmonitor.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create dns-listener module: %w", err)
|
||||
}
|
||||
instance.customlist, err = customlists.New(instance)
|
||||
if err != nil {
|
||||
return instance, fmt.Errorf("create customlist module: %w", err)
|
||||
|
@ -275,6 +286,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
|||
|
||||
instance.core,
|
||||
instance.updates,
|
||||
instance.integration,
|
||||
instance.geoip,
|
||||
instance.netenv,
|
||||
|
||||
|
@ -288,6 +300,7 @@ func New(svcCfg *ServiceConfig) (*Instance, error) { //nolint:maintidx
|
|||
instance.filterLists,
|
||||
instance.customlist,
|
||||
instance.interception,
|
||||
instance.dnsmonitor,
|
||||
|
||||
instance.compat,
|
||||
instance.status,
|
||||
|
@ -378,6 +391,11 @@ func (i *Instance) Updates() *updates.Updates {
|
|||
return i.updates
|
||||
}
|
||||
|
||||
// OSIntegration returns the integration module.
|
||||
func (i *Instance) OSIntegration() *integration.OSIntegration {
|
||||
return i.integration
|
||||
}
|
||||
|
||||
// GeoIP returns the geoip module.
|
||||
func (i *Instance) GeoIP() *geoip.GeoIP {
|
||||
return i.geoip
|
||||
|
@ -463,6 +481,11 @@ func (i *Instance) Interception() *interception.Interception {
|
|||
return i.interception
|
||||
}
|
||||
|
||||
// DNSMonitor returns the dns-listener module.
|
||||
func (i *Instance) DNSMonitor() *dnsmonitor.DNSMonitor {
|
||||
return i.dnsmonitor
|
||||
}
|
||||
|
||||
// CustomList returns the customlist module.
|
||||
func (i *Instance) CustomList() *customlists.CustomList {
|
||||
return i.customlist
|
||||
|
|
114
service/integration/etw_windows.go
Normal file
114
service/integration/etw_windows.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type ETWFunctions struct {
|
||||
createState *windows.Proc
|
||||
initializeSession *windows.Proc
|
||||
startTrace *windows.Proc
|
||||
flushTrace *windows.Proc
|
||||
stopTrace *windows.Proc
|
||||
destroySession *windows.Proc
|
||||
stopOldSession *windows.Proc
|
||||
}
|
||||
|
||||
func initializeETW(dll *windows.DLL) (*ETWFunctions, error) {
|
||||
functions := &ETWFunctions{}
|
||||
var err error
|
||||
functions.createState, err = dll.FindProc("PM_ETWCreateState")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWCreateState: %q", err)
|
||||
}
|
||||
functions.initializeSession, err = dll.FindProc("PM_ETWInitializeSession")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWInitializeSession: %q", err)
|
||||
}
|
||||
functions.startTrace, err = dll.FindProc("PM_ETWStartTrace")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWStartTrace: %q", err)
|
||||
}
|
||||
functions.flushTrace, err = dll.FindProc("PM_ETWFlushTrace")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWFlushTrace: %q", err)
|
||||
}
|
||||
functions.stopTrace, err = dll.FindProc("PM_ETWStopTrace")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWStopTrace: %q", err)
|
||||
}
|
||||
functions.destroySession, err = dll.FindProc("PM_ETWDestroySession")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
|
||||
}
|
||||
functions.stopOldSession, err = dll.FindProc("PM_ETWStopOldSession")
|
||||
if err != nil {
|
||||
return functions, fmt.Errorf("failed to load function PM_ETWDestroySession: %q", err)
|
||||
}
|
||||
return functions, nil
|
||||
}
|
||||
|
||||
// CreateState calls the dll createState C function.
|
||||
func (etw ETWFunctions) CreateState(callback uintptr) uintptr {
|
||||
state, _, _ := etw.createState.Call(callback)
|
||||
return state
|
||||
}
|
||||
|
||||
// InitializeSession calls the dll initializeSession C function.
|
||||
func (etw ETWFunctions) InitializeSession(state uintptr) error {
|
||||
rc, _, _ := etw.initializeSession.Call(state)
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", rc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartTrace calls the dll startTrace C function.
|
||||
func (etw ETWFunctions) StartTrace(state uintptr) error {
|
||||
rc, _, _ := etw.startTrace.Call(state)
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", rc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FlushTrace calls the dll flushTrace C function.
|
||||
func (etw ETWFunctions) FlushTrace(state uintptr) error {
|
||||
rc, _, _ := etw.flushTrace.Call(state)
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", rc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopTrace calls the dll stopTrace C function.
|
||||
func (etw ETWFunctions) StopTrace(state uintptr) error {
|
||||
rc, _, _ := etw.stopTrace.Call(state)
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", rc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DestroySession calls the dll destroySession C function.
|
||||
func (etw ETWFunctions) DestroySession(state uintptr) error {
|
||||
rc, _, _ := etw.destroySession.Call(state)
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", rc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopOldSession calls the dll stopOldSession C function.
|
||||
func (etw ETWFunctions) StopOldSession() error {
|
||||
rc, _, _ := etw.stopOldSession.Call()
|
||||
if rc != 0 {
|
||||
return fmt.Errorf("failed with status code: %d", rc)
|
||||
}
|
||||
return nil
|
||||
}
|
16
service/integration/integration.go
Normal file
16
service/integration/integration.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package integration
|
||||
|
||||
type OSSpecific struct{}
|
||||
|
||||
// Initialize is empty on any OS different then Windows.
|
||||
func (i *OSIntegration) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp releases any resourses allocated during initializaion.
|
||||
func (i *OSIntegration) CleanUp() error {
|
||||
return nil
|
||||
}
|
86
service/integration/integration_windows.go
Normal file
86
service/integration/integration_windows.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type OSSpecific struct {
|
||||
dll *windows.DLL
|
||||
etwFunctions *ETWFunctions
|
||||
}
|
||||
|
||||
// Initialize loads the dll and finds all the needed functions from it.
|
||||
func (i *OSIntegration) Initialize() error {
|
||||
// Try to load dll
|
||||
err := i.loadDLL()
|
||||
if err != nil {
|
||||
log.Errorf("integration: failed to load dll: %s", err)
|
||||
|
||||
callbackLock := sync.Mutex{}
|
||||
// listen for event from the updater and try to load again if any.
|
||||
i.instance.Updates().EventResourcesUpdated.AddCallback("core-dll-loader", func(wc *mgr.WorkerCtx, s struct{}) (cancel bool, err error) {
|
||||
// Make sure no multiple callas are executed at the same time.
|
||||
callbackLock.Lock()
|
||||
defer callbackLock.Unlock()
|
||||
|
||||
// Try to load again.
|
||||
err = i.loadDLL()
|
||||
if err != nil {
|
||||
log.Errorf("integration: failed to load dll: %s", err)
|
||||
} else {
|
||||
log.Info("integration: initialize successful after updater event")
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
|
||||
} else {
|
||||
log.Info("integration: initialize successful")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *OSIntegration) loadDLL() error {
|
||||
// Find path to the dll.
|
||||
file, err := updates.GetPlatformFile("dll/portmaster-core.dll")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Load the DLL.
|
||||
i.os.dll, err = windows.LoadDLL(file.Path())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load dll: %q", err)
|
||||
}
|
||||
|
||||
// Enumerate all needed dll functions.
|
||||
i.os.etwFunctions, err = initializeETW(i.os.dll)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Notify listeners
|
||||
i.OnInitializedEvent.Submit(struct{}{})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanUp releases any resources allocated during initialization.
|
||||
func (i *OSIntegration) CleanUp() error {
|
||||
if i.os.dll != nil {
|
||||
return i.os.dll.Release()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetETWInterface return struct containing all the ETW related functions, and nil if it was not loaded yet
|
||||
func (i *OSIntegration) GetETWInterface() *ETWFunctions {
|
||||
return i.os.etwFunctions
|
||||
}
|
49
service/integration/module.go
Normal file
49
service/integration/module.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/updates"
|
||||
)
|
||||
|
||||
// OSIntegration module provides special integration with the OS.
|
||||
type OSIntegration struct {
|
||||
m *mgr.Manager
|
||||
|
||||
OnInitializedEvent *mgr.EventMgr[struct{}]
|
||||
|
||||
//nolint:unused
|
||||
os OSSpecific
|
||||
|
||||
instance instance
|
||||
}
|
||||
|
||||
// New returns a new OSIntegration module.
|
||||
func New(instance instance) (*OSIntegration, error) {
|
||||
m := mgr.New("OSIntegration")
|
||||
module := &OSIntegration{
|
||||
m: m,
|
||||
OnInitializedEvent: mgr.NewEventMgr[struct{}]("on-initialized", m),
|
||||
instance: instance,
|
||||
}
|
||||
|
||||
return module, nil
|
||||
}
|
||||
|
||||
// Manager returns the module manager.
|
||||
func (i *OSIntegration) Manager() *mgr.Manager {
|
||||
return i.m
|
||||
}
|
||||
|
||||
// Start starts the module.
|
||||
func (i *OSIntegration) Start() error {
|
||||
return i.Initialize()
|
||||
}
|
||||
|
||||
// Stop stops the module.
|
||||
func (i *OSIntegration) Stop() error {
|
||||
return i.CleanUp()
|
||||
}
|
||||
|
||||
type instance interface {
|
||||
Updates() *updates.Updates
|
||||
}
|
|
@ -397,28 +397,32 @@ func (e *Entity) getDomainLists(ctx context.Context) {
|
|||
}
|
||||
|
||||
func splitDomain(domain string) []string {
|
||||
domain = strings.Trim(domain, ".")
|
||||
suffix, _ := publicsuffix.PublicSuffix(domain)
|
||||
if suffix == domain {
|
||||
// Get suffix.
|
||||
d := strings.TrimSuffix(domain, ".")
|
||||
suffix, icann := publicsuffix.PublicSuffix(d)
|
||||
if suffix == d {
|
||||
return []string{domain}
|
||||
}
|
||||
|
||||
domainWithoutSuffix := domain[:len(domain)-len(suffix)]
|
||||
domainWithoutSuffix = strings.Trim(domainWithoutSuffix, ".")
|
||||
|
||||
splitted := strings.FieldsFunc(domainWithoutSuffix, func(r rune) bool {
|
||||
// Split all subdomain into labels.
|
||||
labels := strings.FieldsFunc(d[:len(d)-len(suffix)], func(r rune) bool {
|
||||
return r == '.'
|
||||
})
|
||||
|
||||
domains := make([]string, 0, len(splitted))
|
||||
for idx := range splitted {
|
||||
|
||||
d := strings.Join(splitted[idx:], ".") + "." + suffix
|
||||
if d[len(d)-1] != '.' {
|
||||
d += "."
|
||||
}
|
||||
domains = append(domains, d)
|
||||
// Build list of all domains up to the public suffix.
|
||||
domains := make([]string, 0, len(labels)+1)
|
||||
for idx := range labels {
|
||||
domains = append(
|
||||
domains,
|
||||
strings.Join(labels[idx:], ".")+"."+suffix+".",
|
||||
)
|
||||
}
|
||||
|
||||
// If the suffix is not a real TLD, but a public suffix, add it to the list.
|
||||
if !icann {
|
||||
domains = append(domains, suffix+".")
|
||||
}
|
||||
|
||||
return domains
|
||||
}
|
||||
|
||||
|
|
33
service/intel/entity_test.go
Normal file
33
service/intel/entity_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package intel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var splitDomainTestCases = [][]string{
|
||||
// Regular registered domains and subdomains.
|
||||
{"example.com."},
|
||||
{"www.example.com.", "example.com."},
|
||||
{"sub.domain.example.com.", "domain.example.com.", "example.com."},
|
||||
{"example.co.uk."},
|
||||
{"www.example.co.uk.", "example.co.uk."},
|
||||
|
||||
// TLD or public suffix: Return as is.
|
||||
{"com."},
|
||||
{"googleapis.com."},
|
||||
|
||||
// Public suffix domains: Return including public suffix.
|
||||
{"test.googleapis.com.", "googleapis.com."},
|
||||
{"sub.domain.googleapis.com.", "domain.googleapis.com.", "googleapis.com."},
|
||||
}
|
||||
|
||||
func TestSplitDomain(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, testCase := range splitDomainTestCases {
|
||||
splitted := splitDomain(testCase[0])
|
||||
assert.Equal(t, testCase, splitted, "result must match")
|
||||
}
|
||||
}
|
|
@ -50,7 +50,7 @@ var (
|
|||
var cache = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
CacheSize: 2 ^ 8,
|
||||
CacheSize: 256,
|
||||
})
|
||||
|
||||
// getFileFunc is the function used to get a file from
|
||||
|
@ -146,7 +146,7 @@ func persistRecords(startJob func(func() error), records <-chan record.Record) {
|
|||
|
||||
timePerEntity := time.Since(start) / time.Duration(cnt)
|
||||
speed := float64(time.Second) / float64(timePerEntity)
|
||||
log.Debugf("processed %d entities in %s with %s / entity (%.2f entities/second)", cnt, time.Since(start), timePerEntity, speed)
|
||||
log.Debugf("intel/filterlists: processed %d entities in %s with %s / entity (%.2f entities/second)", cnt, time.Since(start), timePerEntity, speed)
|
||||
}
|
||||
|
||||
batch := database.NewInterface(&database.Options{Local: true, Internal: true})
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
|
||||
"github.com/safing/portmaster/base/database"
|
||||
"github.com/safing/portmaster/base/database/query"
|
||||
"github.com/safing/portmaster/base/database/storage"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/updater"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
|
@ -158,9 +159,25 @@ func performUpdate(ctx context.Context) error {
|
|||
|
||||
func removeAllObsoleteFilterEntries(wc *mgr.WorkerCtx) error {
|
||||
log.Debugf("intel/filterlists: cleanup task started, removing obsolete filter list entries ...")
|
||||
n, err := cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where(
|
||||
// TODO(ppacher): remember the timestamp we started the last update
|
||||
// and use that rather than "one hour ago"
|
||||
|
||||
// TODO: Remember the timestamp we started the last update and use that rather than "one hour ago".
|
||||
|
||||
// First try to purge with PurgeOlderThan.
|
||||
n, err := cache.PurgeOlderThan(wc.Ctx(), filterListKeyPrefix, time.Now().Add(-time.Hour))
|
||||
switch {
|
||||
case err == nil:
|
||||
// Success!
|
||||
log.Debugf("intel/filterlists: successfully removed %d obsolete entries", n)
|
||||
return nil
|
||||
case errors.Is(err, database.ErrNotImplemented) || errors.Is(err, storage.ErrNotImplemented):
|
||||
// Try next method.
|
||||
default:
|
||||
// Return error.
|
||||
return err
|
||||
}
|
||||
|
||||
// Try with regular purge.
|
||||
n, err = cache.Purge(wc.Ctx(), query.New(filterListKeyPrefix).Where(
|
||||
query.Where("UpdatedAt", query.LessThan, time.Now().Add(-time.Hour).Unix()),
|
||||
))
|
||||
if err != nil {
|
||||
|
|
|
@ -4,7 +4,7 @@ import "time"
|
|||
|
||||
// SleepyTicker is wrapper over time.Ticker that respects the sleep mode of the module.
|
||||
type SleepyTicker struct {
|
||||
ticker time.Ticker
|
||||
ticker *time.Ticker
|
||||
normalDuration time.Duration
|
||||
sleepDuration time.Duration
|
||||
sleepMode bool
|
||||
|
@ -16,7 +16,7 @@ type SleepyTicker struct {
|
|||
// If sleepDuration is set to 0 ticker will not tick during sleep.
|
||||
func NewSleepyTicker(normalDuration time.Duration, sleepDuration time.Duration) *SleepyTicker {
|
||||
st := &SleepyTicker{
|
||||
ticker: *time.NewTicker(normalDuration),
|
||||
ticker: time.NewTicker(normalDuration),
|
||||
normalDuration: normalDuration,
|
||||
sleepDuration: sleepDuration,
|
||||
sleepMode: false,
|
||||
|
|
57
service/mgr/sleepyticker_test.go
Normal file
57
service/mgr/sleepyticker_test.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package mgr
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSleepyTickerStop(t *testing.T) {
|
||||
normalDuration := 100 * time.Millisecond
|
||||
sleepDuration := 200 * time.Millisecond
|
||||
|
||||
st := NewSleepyTicker(normalDuration, sleepDuration)
|
||||
st.Stop() // no panic expected here
|
||||
}
|
||||
|
||||
func TestSleepyTicker(t *testing.T) {
|
||||
normalDuration := 100 * time.Millisecond
|
||||
sleepDuration := 200 * time.Millisecond
|
||||
|
||||
st := NewSleepyTicker(normalDuration, sleepDuration)
|
||||
|
||||
// Test normal mode
|
||||
select {
|
||||
case <-st.Wait():
|
||||
// Expected tick
|
||||
case <-time.After(normalDuration + 50*time.Millisecond):
|
||||
t.Error("expected tick in normal mode")
|
||||
}
|
||||
|
||||
// Test sleep mode
|
||||
st.SetSleep(true)
|
||||
select {
|
||||
case <-st.Wait():
|
||||
// Expected tick
|
||||
case <-time.After(sleepDuration + 50*time.Millisecond):
|
||||
t.Error("expected tick in sleep mode")
|
||||
}
|
||||
|
||||
// Test sleep mode with sleepDuration == 0
|
||||
st = NewSleepyTicker(normalDuration, 0)
|
||||
st.SetSleep(true)
|
||||
select {
|
||||
case <-st.Wait():
|
||||
t.Error("did not expect tick when sleepDuration is 0")
|
||||
case <-time.After(normalDuration):
|
||||
// Expected no tick
|
||||
}
|
||||
|
||||
// Test stopping the ticker
|
||||
st.Stop()
|
||||
select {
|
||||
case <-st.Wait():
|
||||
t.Error("did not expect tick after stopping the ticker")
|
||||
case <-time.After(normalDuration):
|
||||
// Expected no tick
|
||||
}
|
||||
}
|
|
@ -224,8 +224,8 @@ func handleRequest(ctx context.Context, w dns.ResponseWriter, request *dns.Msg)
|
|||
}
|
||||
|
||||
// Save the request as open, as we don't know if there will be a connection or not.
|
||||
network.SaveOpenDNSRequest(q, rrCache, conn)
|
||||
firewall.UpdateIPsAndCNAMEs(q, rrCache, conn)
|
||||
network.SaveOpenDNSRequest(q, rrCache, conn)
|
||||
|
||||
case network.VerdictUndeterminable:
|
||||
fallthrough
|
||||
|
|
|
@ -19,6 +19,7 @@ import (
|
|||
"github.com/safing/portmaster/base/config"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
"github.com/safing/portmaster/service/netquery/orm"
|
||||
"github.com/safing/portmaster/service/network"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
|
@ -127,7 +128,7 @@ type (
|
|||
// Note that write connections are serialized by the Database object before being
|
||||
// handed over to SQLite.
|
||||
func New(dbPath string) (*Database, error) {
|
||||
historyParentDir := dataroot.Root().ChildDir("databases", 0o700)
|
||||
historyParentDir := dataroot.Root().ChildDir("databases", utils.AdminOnlyPermission)
|
||||
if err := historyParentDir.Ensure(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ensure database directory exists: %w", err)
|
||||
}
|
||||
|
@ -225,7 +226,7 @@ func (db *Database) Close() error {
|
|||
|
||||
// VacuumHistory rewrites the history database in order to purge deleted records.
|
||||
func VacuumHistory(ctx context.Context) (err error) {
|
||||
historyParentDir := dataroot.Root().ChildDir("databases", 0o700)
|
||||
historyParentDir := dataroot.Root().ChildDir("databases", utils.AdminOnlyPermission)
|
||||
if err := historyParentDir.Ensure(); err != nil {
|
||||
return fmt.Errorf("failed to ensure database directory exists: %w", err)
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
@ -18,6 +19,7 @@ import (
|
|||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network/netutils"
|
||||
"github.com/safing/portmaster/service/network/packet"
|
||||
"github.com/safing/portmaster/service/network/reference"
|
||||
"github.com/safing/portmaster/service/process"
|
||||
_ "github.com/safing/portmaster/service/process/tags"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
|
@ -536,12 +538,41 @@ func (conn *Connection) GatherConnectionInfo(pkt packet.Packet) (err error) {
|
|||
|
||||
// Find domain and DNS context of entity.
|
||||
if conn.Entity.Domain == "" && conn.process.Profile() != nil {
|
||||
profileScope := conn.process.Profile().LocalProfile().ID
|
||||
// check if we can find a domain for that IP
|
||||
ipinfo, err := resolver.GetIPInfo(conn.process.Profile().LocalProfile().ID, pkt.Info().RemoteIP().String())
|
||||
ipinfo, err := resolver.GetIPInfo(profileScope, pkt.Info().RemoteIP().String())
|
||||
if err != nil {
|
||||
// Try again with the global scope, in case DNS went through the system resolver.
|
||||
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" && err != nil {
|
||||
// On windows domains may come with delay.
|
||||
if module.instance.Resolver().IsDisabled() && conn.shouldWaitForDomain() {
|
||||
// Flush the dns listener buffer and try again.
|
||||
for i := range 4 {
|
||||
err = module.instance.DNSMonitor().Flush()
|
||||
if err != nil {
|
||||
// Error flushing, dont try again.
|
||||
break
|
||||
}
|
||||
// Try with profile scope
|
||||
ipinfo, err = resolver.GetIPInfo(profileScope, pkt.Info().RemoteIP().String())
|
||||
if err == nil {
|
||||
log.Tracer(pkt.Ctx()).Debugf("network: found domain with scope (%s) from dnsmonitor after %d tries", profileScope, +1)
|
||||
break
|
||||
}
|
||||
// Try again with the global scope
|
||||
ipinfo, err = resolver.GetIPInfo(resolver.IPInfoProfileScopeGlobal, pkt.Info().RemoteIP().String())
|
||||
if err == nil {
|
||||
log.Tracer(pkt.Ctx()).Debugf("network: found domain from dnsmonitor after %d tries", i+1)
|
||||
break
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
lastResolvedDomain := ipinfo.MostRecentDomain()
|
||||
if lastResolvedDomain != nil {
|
||||
|
@ -869,3 +900,17 @@ func (conn *Connection) String() string {
|
|||
return fmt.Sprintf("%s -> %s", conn.process, conn.Entity.IP)
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *Connection) shouldWaitForDomain() bool {
|
||||
// Should wait for Global Unicast, outgoing and not ICMP connections
|
||||
switch {
|
||||
case conn.Entity.IPScope != netutils.Global:
|
||||
return false
|
||||
case conn.Inbound:
|
||||
return false
|
||||
case reference.IsICMP(conn.Entity.Protocol):
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -9,10 +9,12 @@ import (
|
|||
"sync/atomic"
|
||||
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/service/firewall/interception/dnsmonitor"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/network/state"
|
||||
"github.com/safing/portmaster/service/profile"
|
||||
"github.com/safing/portmaster/service/resolver"
|
||||
)
|
||||
|
||||
// Events.
|
||||
|
@ -188,4 +190,6 @@ func New(instance instance) (*Network, error) {
|
|||
|
||||
type instance interface {
|
||||
Profile() *profile.ProfileModule
|
||||
Resolver() *resolver.ResolverModule
|
||||
DNSMonitor() *dnsmonitor.DNSMonitor
|
||||
}
|
||||
|
|
|
@ -106,11 +106,12 @@ func checkError(packet gopacket.Packet, info *Info) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Parse parses an IP packet and saves the information in the given packet object.
|
||||
func Parse(packetData []byte, pktBase *Base) (err error) {
|
||||
// ParseLayer3 parses an IP packet and saves the information in the given packet object.
|
||||
func ParseLayer3(packetData []byte, pktBase *Base) (err error) {
|
||||
if len(packetData) == 0 {
|
||||
return errors.New("empty packet")
|
||||
}
|
||||
|
||||
pktBase.layer3Data = packetData
|
||||
|
||||
ipVersion := packetData[0] >> 4
|
||||
|
@ -155,6 +156,62 @@ func Parse(packetData []byte, pktBase *Base) (err error) {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ParseLayer4 parses an layer 4 packet and saves the information in the given packet object.
|
||||
func ParseLayer4(packetData []byte, pktBase *Base) (err error) {
|
||||
if len(packetData) == 0 {
|
||||
return errors.New("empty packet")
|
||||
}
|
||||
|
||||
var layer gopacket.LayerType
|
||||
switch pktBase.info.Protocol {
|
||||
case ICMP:
|
||||
layer = layers.LayerTypeICMPv4
|
||||
case IGMP:
|
||||
layer = layers.LayerTypeIGMP
|
||||
case TCP:
|
||||
layer = layers.LayerTypeTCP
|
||||
case UDP:
|
||||
layer = layers.LayerTypeUDP
|
||||
case ICMPv6:
|
||||
layer = layers.LayerTypeICMPv6
|
||||
case UDPLite:
|
||||
return fmt.Errorf("UDPLite not supported")
|
||||
case RAW:
|
||||
return fmt.Errorf("RAW protocol not supported")
|
||||
case AnyHostInternalProtocol61:
|
||||
return fmt.Errorf("AnyHostInternalProtocol61 protocol not supported")
|
||||
default:
|
||||
return fmt.Errorf("protocol not supported")
|
||||
}
|
||||
|
||||
packet := gopacket.NewPacket(packetData, layer, gopacket.DecodeOptions{
|
||||
Lazy: true,
|
||||
NoCopy: true,
|
||||
})
|
||||
|
||||
availableDecoders := []func(gopacket.Packet, *Info) error{
|
||||
parseTCP,
|
||||
parseUDP,
|
||||
// parseUDPLite, // We don't yet support udplite.
|
||||
parseICMPv4,
|
||||
parseICMPv6,
|
||||
parseIGMP,
|
||||
checkError,
|
||||
}
|
||||
|
||||
for _, dec := range availableDecoders {
|
||||
if err := dec(packet, pktBase.Info()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pktBase.layers = packet
|
||||
if transport := packet.TransportLayer(); transport != nil {
|
||||
pktBase.layer5Data = transport.LayerPayload()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
genIPProtocolFromLayerType()
|
||||
}
|
||||
|
|
|
@ -256,7 +256,7 @@ func loadProcess(ctx context.Context, key string, pInfo *processInfo.Process) (*
|
|||
// Username
|
||||
process.UserName, err = pInfo.UsernameWithContext(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("process: failed to get Username for p%d: %w", pInfo.Pid, err)
|
||||
log.Tracer(ctx).Warningf("process: failed to get username (PID %d): %s", pInfo.Pid, err)
|
||||
}
|
||||
|
||||
// TODO: User Home
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/safing/portmaster/base/database/migration"
|
||||
"github.com/safing/portmaster/base/dataroot"
|
||||
"github.com/safing/portmaster/base/log"
|
||||
"github.com/safing/portmaster/base/utils"
|
||||
_ "github.com/safing/portmaster/service/core/base"
|
||||
"github.com/safing/portmaster/service/mgr"
|
||||
"github.com/safing/portmaster/service/profile/binmeta"
|
||||
|
@ -70,7 +71,7 @@ func prep() error {
|
|||
}
|
||||
|
||||
// Setup icon storage location.
|
||||
iconsDir := dataroot.Root().ChildDir("databases", 0o0700).ChildDir("icons", 0o0700)
|
||||
iconsDir := dataroot.Root().ChildDir("databases", utils.AdminOnlyPermission).ChildDir("icons", utils.AdminOnlyPermission)
|
||||
if err := iconsDir.Ensure(); err != nil {
|
||||
return fmt.Errorf("failed to create/check icons directory: %w", err)
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue