diff --git a/core/config.go b/core/config.go index 31eb3c28c..28fb24841 100644 --- a/core/config.go +++ b/core/config.go @@ -3,7 +3,11 @@ package core import ( "flag" + locale "github.com/Xuanwo/go-locale" + "golang.org/x/exp/slices" + "github.com/safing/portbase/config" + "github.com/safing/portbase/log" ) // Configuration Keys. @@ -13,6 +17,8 @@ var ( CfgNetworkServiceKey = "core/networkService" defaultNetworkServiceMode bool + + CfgLocaleKey = "core/locale" ) func init() { @@ -41,5 +47,66 @@ func registerConfig() error { return err } + if err := config.Register(&config.Option{ + Name: "Time and Date Format", + Key: CfgLocaleKey, + Description: "Configures the time and date format for the user interface. Selection is an example and correct formatting in the UI is a continual work in progress.", + OptType: config.OptTypeString, + ExpertiseLevel: config.ExpertiseLevelUser, + ReleaseLevel: config.ReleaseLevelStable, + DefaultValue: getDefaultLocale(), + PossibleValues: []config.PossibleValue{ + { + Name: "24h DD-MM-YYYY", + Value: enGBLocale, + }, + { + Name: "12h MM/DD/YYYY", + Value: enUSLocale, + }, + }, + Annotations: config.Annotations{ + config.CategoryAnnotation: "User Interface", + config.DisplayHintAnnotation: config.DisplayHintOneOf, + config.RequiresUIReloadAnnotation: true, + }, + }); err != nil { + return err + } + return nil } + +func getDefaultLocale() string { + // Get locales from system. + detectedLocales, err := locale.DetectAll() + if err != nil { + log.Warningf("core: failed to detect locale: %s", err) + return enGBLocale + } + + // log.Debugf("core: detected locales: %s", detectedLocales) + + // Check if there is a locale that corresponds to the en-US locale. + for _, detectedLocale := range detectedLocales { + if slices.Contains[[]string, string](defaultEnUSLocales, detectedLocale.String()) { + return enUSLocale + } + } + + // Otherwise, return the en-GB locale as default. + return enGBLocale +} + +var ( + enGBLocale = "en-GB" + enUSLocale = "en-US" + + defaultEnUSLocales = []string{ + "en-AS", // English (American Samoa) + "en-GU", // English (Guam) + "en-UM", // English (U.S. Minor Outlying Islands) + "en-US", // English (United States) + "en-VI", // English (U.S. Virgin Islands) + } +) diff --git a/go.mod b/go.mod index 5244d774f..6135b0263 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/safing/portmaster go 1.20 require ( + github.com/Xuanwo/go-locale v1.1.0 github.com/agext/levenshtein v1.2.3 github.com/cilium/ebpf v0.11.0 github.com/coreos/go-iptables v0.7.0 @@ -15,12 +16,13 @@ require ( github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-version v1.6.0 github.com/jackc/puddle/v2 v2.2.1 - github.com/miekg/dns v1.1.55 + github.com/miekg/dns v1.1.56 + github.com/mitchellh/go-server-timing v1.0.1 github.com/oschwald/maxminddb-golang v1.12.0 github.com/safing/jess v0.3.1 - github.com/safing/portbase v0.17.3 - github.com/safing/portmaster-android/go v0.0.0-20230605085256-6abf4c495626 - github.com/safing/spn v0.6.18-prep + github.com/safing/portbase v0.17.4 + github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec + github.com/safing/spn v0.6.19 github.com/shirou/gopsutil v3.21.11+incompatible github.com/spf13/cobra v1.7.0 github.com/spkg/zipfs v0.7.1 @@ -28,10 +30,10 @@ require ( github.com/tannerryan/ring v1.1.2 github.com/tevino/abool v1.2.0 github.com/umahmood/haversine v0.0.0-20151105152445-808ab04add26 - golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 - golang.org/x/net v0.14.0 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 + golang.org/x/net v0.15.0 golang.org/x/sync v0.3.0 - golang.org/x/sys v0.11.0 + golang.org/x/sys v0.12.0 zombiezen.com/go/sqlite v0.13.1 ) @@ -46,10 +48,12 @@ require ( github.com/danieljoos/wincred v1.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect github.com/fxamacker/cbor v1.5.1 // indirect github.com/fxamacker/cbor/v2 v2.5.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect + github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f // indirect github.com/google/btree v1.1.2 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.3.1 // indirect @@ -60,7 +64,7 @@ require ( github.com/josharian/native v1.1.0 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mdlayher/netlink v1.7.2 // indirect - github.com/mdlayher/socket v0.4.1 // indirect + github.com/mdlayher/socket v0.5.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mr-tron/base58 v1.2.0 // indirect @@ -86,10 +90,11 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zalando/go-keyring v0.2.3 // indirect go.etcd.io/bbolt v1.3.7 // indirect - golang.org/x/crypto v0.12.0 // indirect + golang.org/x/crypto v0.13.0 // indirect golang.org/x/mod v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect + golang.org/x/tools v0.13.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gvisor.dev/gvisor v0.0.0-20220817001344-846276b3dbc5 // indirect diff --git a/go.sum b/go.sum index b220c6833..776f11389 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,4 @@ +cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AlecAivazis/survey/v2 v2.3.6/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= 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= @@ -8,6 +9,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/VictoriaMetrics/metrics v1.22.2/go.mod h1:rAr/llLpEnAdTehiNlUxKgnjcOuROSzpw0GvjpEbvFc= github.com/VictoriaMetrics/metrics v1.24.0 h1:ILavebReOjYctAGY5QU2F9X0MYvkcrG3aEn2RKa1Zkw= github.com/VictoriaMetrics/metrics v1.24.0/go.mod h1:eFT25kvsTidQFHb6U0oa0rTrDRdz4xTYjpL8+UPohys= +github.com/Xuanwo/go-locale v1.1.0 h1:51gUxhxl66oXAjI9uPGb2O0qwPECpriKQb2hl35mQkg= +github.com/Xuanwo/go-locale v1.1.0/go.mod h1:UKrHoZB3FPIk9wIG2/tVSobnHgNnceGSH3Y8DY5cASs= 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= @@ -24,6 +27,7 @@ github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2 github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= 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= github.com/brianvoe/gofakeit v3.18.0+incompatible h1:wDOmHc9DLG4nRjUVVaxA+CEglKOW72Y5+4WNxUIkjM8= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -58,23 +62,29 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn 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/felixge/httpsnoop v1.0.0/go.mod h1:3+D9sFq0ahK/JeJPhCBUV1xlf4/eIYrUQaxulT0VzX8= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/florianl/go-conntrack v0.4.0 h1:TlYkxytdwgVayfU0cKwkHurQA0Rd1ZSEBRckRYDUu18= github.com/florianl/go-conntrack v0.4.0/go.mod h1:iPDx4oIats2T7X7Jm3PFyRCJM1GfZhJaSHOWROYOrE8= github.com/florianl/go-nfqueue v1.3.1 h1:khQ9fYCrjbu5CF8dZF55G2RTIEIQRI0Aj5k3msJR6Gw= github.com/florianl/go-nfqueue v1.3.1/go.mod h1:aHWbgkhryJxF5XxYvJ3oRZpdD4JP74Zu/hP1zuhja+M= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +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/fxamacker/cbor v1.5.1 h1:XjQWBgdmQyqimslUh5r4tUGmoqzHmBFQOImkWGi2awg= github.com/fxamacker/cbor v1.5.1/go.mod h1:3aPGItF174ni7dDzd6JZ206H8cmr4GDNBGpPa971zsU= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= +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-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-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= @@ -82,18 +92,25 @@ github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx github.com/gofrs/uuid v4.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 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/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/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +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.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -107,10 +124,14 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= 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= @@ -118,8 +139,10 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +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/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +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.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -139,6 +162,8 @@ github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXp github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo= github.com/jsimonetti/rtnetlink v0.0.0-20211022192332-93da33804786/go.mod h1:v4hqbTdfQngbVSZJVWUhGE/lbTFf9jb+ygmNUDQMuOs= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -146,9 +171,12 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 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/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/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= @@ -174,15 +202,18 @@ github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/ github.com/mdlayher/socket v0.0.0-20211007213009-516dcbdf0267/go.mod h1:nFZ1EtZYK8Gi/k6QNu7z7CgO20i/4ExeQswwWuPmG/g= github.com/mdlayher/socket v0.1.0/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= -github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= -github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= +github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= -github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= 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= +github.com/mitchellh/go-server-timing v1.0.1 h1:f00/aIe8T3MrnLhQHu3tSWvnwc5GV/p5eutuu3hF/tE= +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.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= @@ -190,6 +221,7 @@ github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= +github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -208,16 +240,12 @@ github.com/safing/jess v0.3.1 h1:cMZVhi2whW/YdD98MPLeLIWJndQ7o2QVt2HefQ/ByFA= github.com/safing/jess v0.3.1/go.mod h1:aj73Eot1zm2ETkJuw9hJlIO8bRom52uBbsCHemvlZmA= github.com/safing/portbase v0.15.2/go.mod h1:5bHi99fz7Hh/wOsZUOI631WF9ePSHk57c4fdlOMS91Y= github.com/safing/portbase v0.16.2/go.mod h1:mzNCWqPbO7vIYbbK5PElGbudwd2vx4YPNawymL8Aro8= -github.com/safing/portbase v0.17.2 h1:HzJkURMmXkv30wMHB7xJ+Z5U5aTMe+EzvlHavKoKkos= -github.com/safing/portbase v0.17.2/go.mod h1:1cVgDZIsPiqM5b+K88Kshir5PGIvsftYkx7y1x925+8= -github.com/safing/portbase v0.17.3 h1:LLV2kq4mli2phHFHxigTkIoOjConieMTWsDyi9kJd00= -github.com/safing/portbase v0.17.3/go.mod h1:1cVgDZIsPiqM5b+K88Kshir5PGIvsftYkx7y1x925+8= -github.com/safing/portmaster-android/go v0.0.0-20230605085256-6abf4c495626 h1:olc/REnUdpJN/Gmz8B030OxLpMYxyPDTrDILNEw0eKs= -github.com/safing/portmaster-android/go v0.0.0-20230605085256-6abf4c495626/go.mod h1:abwyAQrZGemWbSh/aCD9nnkp0SvFFf/mGWkAbOwPnFE= -github.com/safing/spn v0.6.17 h1:3Lu1cpTcy8zYhA/2UEfeG08Rx1nlwIj1aobSfNXXgUI= -github.com/safing/spn v0.6.17/go.mod h1:2CuZfJJazIYyMDrhiwX2eFal0urQyLiX8rXLvJiCTcw= -github.com/safing/spn v0.6.18-prep h1:e6jjDFVsOh9B7YQLCjfCgqbCHiHOxRdpjXi5gR+85rA= -github.com/safing/spn v0.6.18-prep/go.mod h1:flegLqCJjFQ0uDB39GMWrIttJga5cSeNea2G6XlKRJ0= +github.com/safing/portbase v0.17.4 h1:4RhItvFujwdfLQVfwvB+VYER33AT//Ywv317Vj01TEQ= +github.com/safing/portbase v0.17.4/go.mod h1:suLPSjOTqA7iDLozis5OI7PSw+wqJNT8SLvdBhRPlqI= +github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec h1:oSJY1seobofPwpMoJRkCgXnTwfiQWNfGMCPDfqgAEfg= +github.com/safing/portmaster-android/go v0.0.0-20230830120134-3226ceac3bec/go.mod h1:abwyAQrZGemWbSh/aCD9nnkp0SvFFf/mGWkAbOwPnFE= +github.com/safing/spn v0.6.19 h1:z4i8hb5FGKjmgSzA4MzJ8mOc0hYp11zgXzujrHwwV5k= +github.com/safing/spn v0.6.19/go.mod h1:LRWLManSXHTViiDqU2qNy3w07auMuadOnVW8wAB/Cgw= 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= @@ -226,18 +254,27 @@ 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/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.7 h1:I6tZjLXD2Q1kjvNbIzB1wvQBsXmKXiVrhpRE8ZjP5jY= +github.com/smartystreets/goconvey v1.6.7/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 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/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= 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= @@ -310,10 +347,10 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20221010152910-d6f0a8c073c2/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= -golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -323,6 +360,7 @@ golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -343,8 +381,10 @@ golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qx 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-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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= @@ -382,6 +422,7 @@ golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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= @@ -396,29 +437,39 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220919170432-7a66f970e087/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/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-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 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.12.1-0.20230815132531-74c255bcf846 h1:Vve/L0v7CXXuxUmaMGIEK/dEeq7uiqb5qBgQrZzIE7E= -golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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= +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= +google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= diff --git a/netquery/database.go b/netquery/database.go index aad649ce3..807c00974 100644 --- a/netquery/database.go +++ b/netquery/database.go @@ -57,6 +57,14 @@ type ( writeConn *sqlite.Conn } + // BatchExecute executes multiple queries in one transaction. + BatchExecute struct { + ID string + SQL string + Params map[string]any + Result *[]map[string]any + } + // Conn is a network connection that is stored in a SQLite database and accepted // by the *Database type of this package. This also defines, using the ./orm package, // the table schema and the model that is exposed via the runtime database as well as @@ -325,6 +333,22 @@ func (db *Database) Execute(ctx context.Context, sql string, args ...orm.QueryOp }) } +// ExecuteBatch executes multiple custom SQL query using a read-only connection against the SQLite +// database used by db. +func (db *Database) ExecuteBatch(ctx context.Context, batches []BatchExecute) error { + return db.withConn(ctx, func(conn *sqlite.Conn) error { + merr := new(multierror.Error) + + for _, batch := range batches { + if err := orm.RunQuery(ctx, conn, batch.SQL, orm.WithNamedArgs(batch.Params), orm.WithResult(batch.Result)); err != nil { + merr.Errors = append(merr.Errors, fmt.Errorf("%s: %w", batch.ID, err)) + } + } + + return merr.ErrorOrNil() + }) +} + // CountRows returns the number of rows stored in the database. func (db *Database) CountRows(ctx context.Context) (int, error) { var result []struct { diff --git a/netquery/module_api.go b/netquery/module_api.go index c51f9a3aa..396471862 100644 --- a/netquery/module_api.go +++ b/netquery/module_api.go @@ -7,6 +7,7 @@ import ( "time" "github.com/hashicorp/go-multierror" + servertiming "github.com/mitchellh/go-server-timing" "github.com/safing/portbase/api" "github.com/safing/portbase/config" @@ -81,6 +82,11 @@ func (m *module) prepare() error { IsDevMode: config.Concurrent.GetAsBool(config.CfgDevModeKey, false), } + batchHander := &BatchQueryHandler{ + Database: m.Store, + IsDevMode: config.Concurrent.GetAsBool(config.CfgDevModeKey, false), + } + chartHandler := &ChartHandler{ Database: m.Store, } @@ -93,7 +99,20 @@ func (m *module) prepare() error { Read: api.PermitUser, // Needs read+write as the query is sent using POST data. Write: api.PermitUser, // Needs read+write as the query is sent using POST data. BelongsTo: m.Module, - HandlerFunc: queryHander.ServeHTTP, + HandlerFunc: servertiming.Middleware(queryHander, nil).ServeHTTP, + }); err != nil { + return fmt.Errorf("failed to register API endpoint: %w", err) + } + + if err := api.RegisterEndpoint(api.Endpoint{ + Name: "Batch Query Connections", + Description: "Batch query the in-memory sqlite connection database.", + Path: "netquery/query/batch", + MimeType: "application/json", + Read: api.PermitUser, // Needs read+write as the query is sent using POST data. + Write: api.PermitUser, // Needs read+write as the query is sent using POST data. + BelongsTo: m.Module, + HandlerFunc: servertiming.Middleware(batchHander, nil).ServeHTTP, }); err != nil { return fmt.Errorf("failed to register API endpoint: %w", err) } @@ -105,7 +124,7 @@ func (m *module) prepare() error { MimeType: "application/json", Write: api.PermitUser, BelongsTo: m.Module, - HandlerFunc: chartHandler.ServeHTTP, + HandlerFunc: servertiming.Middleware(chartHandler, nil).ServeHTTP, }); err != nil { return fmt.Errorf("failed to register API endpoint: %w", err) } diff --git a/netquery/orm/decoder.go b/netquery/orm/decoder.go index 24126e97a..21ce61468 100644 --- a/netquery/orm/decoder.go +++ b/netquery/orm/decoder.go @@ -289,7 +289,7 @@ func decodeBasic() DecodeFunc { // if we have the column definition available we // use the target go type from there. if colDef != nil { - valueKind = normalizeKind(colDef.GoType.Kind()) + valueKind = NormalizeKind(colDef.GoType.Kind()) // if we have a column definition we try to convert the value to // the actual Go-type that was used in the model. @@ -458,10 +458,11 @@ func runDecodeHooks(colIdx int, colDef *ColumnDef, stmt Stmt, fieldDef reflect.S // to their base type. func getKind(val reflect.Value) reflect.Kind { kind := val.Kind() - return normalizeKind(kind) + return NormalizeKind(kind) } -func normalizeKind(kind reflect.Kind) reflect.Kind { +// NormalizeKind returns a normalized kind of the given kind. +func NormalizeKind(kind reflect.Kind) reflect.Kind { switch { case kind >= reflect.Int && kind <= reflect.Int64: return reflect.Int diff --git a/netquery/orm/encoder.go b/netquery/orm/encoder.go index ef86b842d..6dcd0e684 100644 --- a/netquery/orm/encoder.go +++ b/netquery/orm/encoder.go @@ -123,7 +123,7 @@ func encodeBasic() EncodeFunc { val = val.Elem() } - switch normalizeKind(kind) { //nolint:exhaustive + switch NormalizeKind(kind) { //nolint:exhaustive case reflect.String, reflect.Float64, reflect.Bool, @@ -156,6 +156,8 @@ func DatetimeEncoder(loc *time.Location) EncodeFunc { val = reflect.Indirect(val) } + normalizedKind := NormalizeKind(valType.Kind()) + // we only care about "time.Time" here var t time.Time switch { @@ -179,6 +181,19 @@ func DatetimeEncoder(loc *time.Location) EncodeFunc { return nil, false, fmt.Errorf("failed to parse time as RFC3339: %w", err) } + case (normalizedKind == reflect.Int || normalizedKind == reflect.Uint || normalizedKind == reflect.Float64) && colDef.IsTime: + seconds := int64(0) + switch normalizedKind { //nolint:exhaustive // Previous switch case assures these types. + case reflect.Int: + seconds = val.Int() + case reflect.Uint: + seconds = int64(val.Uint()) + case reflect.Float64: + seconds = int64(val.Float()) + } + + t = time.Unix(seconds, 0) + default: // we don't care ... return nil, false, nil diff --git a/netquery/orm/schema_builder.go b/netquery/orm/schema_builder.go index 6aba2a1f7..018a55e17 100644 --- a/netquery/orm/schema_builder.go +++ b/netquery/orm/schema_builder.go @@ -176,7 +176,7 @@ func getColumnDef(fieldType reflect.StructField) (*ColumnDef, error) { } def.GoType = ft - kind := normalizeKind(ft.Kind()) + kind := NormalizeKind(ft.Kind()) switch kind { //nolint:exhaustive case reflect.Int, reflect.Uint: diff --git a/netquery/query.go b/netquery/query.go index 1e60b7fab..804f85fbc 100644 --- a/netquery/query.go +++ b/netquery/query.go @@ -5,10 +5,12 @@ import ( "encoding/json" "fmt" "io" + "reflect" "sort" "strings" "github.com/hashicorp/go-multierror" + "golang.org/x/exp/slices" "zombiezen.com/go/sqlite" "github.com/safing/portmaster/netquery/orm" @@ -38,11 +40,15 @@ type ( Equal interface{} Matcher struct { - Equal interface{} `json:"$eq,omitempty"` - NotEqual interface{} `json:"$ne,omitempty"` - In []interface{} `json:"$in,omitempty"` - NotIn []interface{} `json:"$notIn,omitempty"` - Like string `json:"$like,omitempty"` + Equal interface{} `json:"$eq,omitempty"` + NotEqual interface{} `json:"$ne,omitempty"` + In []interface{} `json:"$in,omitempty"` + NotIn []interface{} `json:"$notIn,omitempty"` + Like string `json:"$like,omitempty"` + Greater *float64 `json:"$gt,omitempty"` + GreaterOrEqual *float64 `json:"$ge,omitempty"` + Less *float64 `json:"$lt,omitempty"` + LessOrEqual *float64 `json:"$le,omitempty"` } Count struct { @@ -80,23 +86,6 @@ type ( Value string `json:"value"` } - QueryRequestPayload struct { - Select Selects `json:"select"` - Query Query `json:"query"` - OrderBy OrderBys `json:"orderBy"` - GroupBy []string `json:"groupBy"` - TextSearch *TextSearch `json:"textSearch"` - // A list of databases to query. If left empty, - // both, the LiveDatabase and the HistoryDatabase are queried - Databases []DatabaseName `json:"databases"` - - Pagination - - selectedFields []string - whitelistedFields []string - paramMap map[string]interface{} - } - QueryActiveConnectionChartPayload struct { Query Query `json:"query"` TextSearch *TextSearch `json:"textSearch"` @@ -258,6 +247,22 @@ func (match Matcher) Validate() error { found++ } + if match.Greater != nil { + found++ + } + + if match.GreaterOrEqual != nil { + found++ + } + + if match.Less != nil { + found++ + } + + if match.LessOrEqual != nil { + found++ + } + if found == 0 { return fmt.Errorf("no conditions specified") } @@ -305,12 +310,30 @@ func (match Matcher) toSQLConditionClause(ctx context.Context, suffix string, co var placeholder []string for idx, value := range values { - encodedValue, err := orm.EncodeValue(ctx, &colDef, value, encoderConfig) - if err != nil { - errs.Errors = append(errs.Errors, - fmt.Errorf("failed to encode %v for column %s: %w", value, colDef.Name, err), - ) - return + var ( + encodedValue any + err error + ) + + kind := orm.NormalizeKind(reflect.TypeOf(value).Kind()) + isNumber := slices.Contains([]reflect.Kind{ + reflect.Uint, + reflect.Int, + reflect.Float64, + }, kind) + + // if we query a time-field that is queried as a number, don't do any encoding + // here as the orm.DateTimeEncoder would convert the number to a string. + if colDef.IsTime && colDef.Type == sqlite.TypeText && isNumber { + encodedValue = value + } else { + encodedValue, err = orm.EncodeValue(ctx, &colDef, value, encoderConfig) + if err != nil { + errs.Errors = append(errs.Errors, + fmt.Errorf("failed to encode %v for column %s: %w", value, colDef.Name, err), + ) + return + } } uniqKey := fmt.Sprintf(":%s%s%d", key, suffix, idx) @@ -318,10 +341,31 @@ func (match Matcher) toSQLConditionClause(ctx context.Context, suffix string, co params[uniqKey] = encodedValue } + // NOTE(ppacher): for now we assume that the type of each element of values + // is the same. We also can be sure that there is always at least one value. + // + // TODO(ppacher): if we start supporting values of different types here + // we need to revisit the whole behavior as we might need to do more boolean + // expression nesting to support that. + kind := orm.NormalizeKind(reflect.TypeOf(values[0]).Kind()) + isNumber := slices.Contains([]reflect.Kind{ + reflect.Uint, + reflect.Int, + reflect.Float64, + }, kind) + + // if this is a time column that is stored in "text" format and the provided + // value is a number type, we need to wrap the property in a strftime() method + // call. + nameStmt := colDef.Name + if colDef.IsTime && colDef.Type == sqlite.TypeText && isNumber { + nameStmt = fmt.Sprintf("strftime('%%s', %s)+0", nameStmt) + } + if len(placeholder) == 1 && !list { - queryParts = append(queryParts, fmt.Sprintf("%s %s %s", colDef.Name, operator, placeholder[0])) + queryParts = append(queryParts, fmt.Sprintf("%s %s %s", nameStmt, operator, placeholder[0])) } else { - queryParts = append(queryParts, fmt.Sprintf("%s %s ( %s )", colDef.Name, operator, strings.Join(placeholder, ", "))) + queryParts = append(queryParts, fmt.Sprintf("%s %s ( %s )", nameStmt, operator, strings.Join(placeholder, ", "))) } } @@ -345,6 +389,22 @@ func (match Matcher) toSQLConditionClause(ctx context.Context, suffix string, co add("LIKE", "like", false, match.Like) } + if match.Greater != nil { + add(">", "gt", false, *match.Greater) + } + + if match.GreaterOrEqual != nil { + add(">=", "ge", false, *match.GreaterOrEqual) + } + + if match.Less != nil { + add("<", "lt", false, *match.Less) + } + + if match.LessOrEqual != nil { + add("<=", "le", false, *match.LessOrEqual) + } + if len(queryParts) == 0 { // this is an empty matcher without a single condition. // we convert that to a no-op TRUE value diff --git a/netquery/query_handler.go b/netquery/query_handler.go index bca2eac37..8e704d3e8 100644 --- a/netquery/query_handler.go +++ b/netquery/query_handler.go @@ -2,7 +2,6 @@ package netquery import ( "bytes" - "context" "encoding/json" "errors" "fmt" @@ -10,9 +9,9 @@ import ( "net/http" "regexp" "strings" - "time" - "golang.org/x/exp/slices" + "github.com/hashicorp/go-multierror" + servertiming "github.com/mitchellh/go-server-timing" "github.com/safing/portbase/log" "github.com/safing/portmaster/netquery/orm" @@ -28,18 +27,34 @@ type ( IsDevMode func() bool Database *Database } + + // BatchQueryHandler implements http.Handler and allows to perform SQL + // query and aggregate functions on Database in batches. + BatchQueryHandler struct { + IsDevMode func() bool + Database *Database + } ) func (qh *QueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { - start := time.Now() - requestPayload, err := qh.parseRequest(req) + timing := servertiming.FromContext(req.Context()) + + timingQueryParsed := timing.NewMetric("query_parsed"). + WithDesc("Query has been parsed"). + Start() + + requestPayload, err := parseQueryRequestPayload[QueryRequestPayload](req) if err != nil { http.Error(resp, err.Error(), http.StatusBadRequest) return } - queryParsed := time.Since(start) + timingQueryParsed.Stop() + + timingQueryBuilt := timing.NewMetric("query_built"). + WithDesc("The SQL query has been built"). + Start() query, paramMap, err := requestPayload.generateSQL(req.Context(), qh.Database.Schema) if err != nil { @@ -48,7 +63,11 @@ func (qh *QueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { return } - sqlQueryBuilt := time.Since(start) + timingQueryBuilt.Stop() + + timingQueryExecute := timing.NewMetric("sql_exec"). + WithDesc("SQL query execution time"). + Start() // actually execute the query against the database and collect the result var result []map[string]interface{} @@ -63,7 +82,7 @@ func (qh *QueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { return } - sqlQueryFinished := time.Since(start) + timingQueryExecute.Stop() // send the HTTP status code resp.WriteHeader(http.StatusOK) @@ -84,12 +103,6 @@ func (qh *QueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { "orderBy": requestPayload.OrderBy, "groupBy": requestPayload.GroupBy, "selects": requestPayload.Select, - "times": map[string]interface{}{ - "start_time": start, - "query_parsed_after": queryParsed.String(), - "query_built_after": sqlQueryBuilt.String(), - "query_executed_after": sqlQueryFinished.String(), - }, } } else { resultBody = make(map[string]interface{}) @@ -108,304 +121,128 @@ func (qh *QueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { } } -func (qh *QueryHandler) parseRequest(req *http.Request) (*QueryRequestPayload, error) { //nolint:dupl - var body io.Reader +func (batch *BatchQueryHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + timing := servertiming.FromContext(req.Context()) - switch req.Method { - case http.MethodPost, http.MethodPut: - body = req.Body - case http.MethodGet: - body = strings.NewReader(req.URL.Query().Get("q")) - default: - return nil, fmt.Errorf("invalid HTTP method") - } + timingQueryParsed := timing.NewMetric("query_parsed"). + WithDesc("Query has been parsed"). + Start() - var requestPayload QueryRequestPayload - blob, err := io.ReadAll(body) + requestPayload, err := parseQueryRequestPayload[BatchQueryRequestPayload](req) if err != nil { - return nil, fmt.Errorf("failed to read body" + err.Error()) - } - - body = bytes.NewReader(blob) - - dec := json.NewDecoder(body) - dec.DisallowUnknownFields() + http.Error(resp, err.Error(), http.StatusBadRequest) - if err := json.Unmarshal(blob, &requestPayload); err != nil && !errors.Is(err, io.EOF) { - return nil, fmt.Errorf("invalid query: %w", err) + return } - return &requestPayload, nil -} - -func (req *QueryRequestPayload) generateSQL(ctx context.Context, schema *orm.TableSchema) (string, map[string]interface{}, error) { - if err := req.prepareSelectedFields(ctx, schema); err != nil { - return "", nil, fmt.Errorf("perparing selected fields: %w", err) - } + timingQueryParsed.Stop() - // build the SQL where clause from the payload query - whereClause, paramMap, err := req.Query.toSQLWhereClause( - ctx, - "", - schema, - orm.DefaultEncodeConfig, - ) - if err != nil { - return "", nil, fmt.Errorf("generating where clause: %w", err) - } + response := make(map[string][]map[string]any, len(*requestPayload)) - req.mergeParams(paramMap) + batches := make([]BatchExecute, 0, len(*requestPayload)) - if req.TextSearch != nil { - textClause, textParams, err := req.TextSearch.toSQLConditionClause(ctx, schema, "", orm.DefaultEncodeConfig) - if err != nil { - return "", nil, fmt.Errorf("generating text-search clause: %w", err) - } + for key, query := range *requestPayload { - if textClause != "" { - if whereClause != "" { - whereClause += " AND " - } + timingQueryBuilt := timing.NewMetric("query_built_" + key). + WithDesc("The SQL query has been built"). + Start() - whereClause += textClause + sql, paramMap, err := query.generateSQL(req.Context(), batch.Database.Schema) + if err != nil { + http.Error(resp, err.Error(), http.StatusBadRequest) - req.mergeParams(textParams) + return } - } - groupByClause, err := req.generateGroupByClause(schema) - if err != nil { - return "", nil, fmt.Errorf("generating group-by clause: %w", err) - } + timingQueryBuilt.Stop() - orderByClause, err := req.generateOrderByClause(schema) - if err != nil { - return "", nil, fmt.Errorf("generating order-by clause: %w", err) + var result []map[string]any + batches = append(batches, BatchExecute{ + ID: key, + SQL: sql, + Params: paramMap, + Result: &result, + }) } - selectClause := req.generateSelectClause() + timingQueryExecute := timing.NewMetric("sql_exec"). + WithDesc("SQL query execution time"). + Start() - if whereClause != "" { - whereClause = "WHERE " + whereClause - } - - // if no database is specified we default to LiveDatabase only. - if len(req.Databases) == 0 { - req.Databases = []DatabaseName{LiveDatabase} - } + status := http.StatusOK + if err := batch.Database.ExecuteBatch(req.Context(), batches); err != nil { + status = http.StatusInternalServerError - sources := make([]string, len(req.Databases)) - for idx, db := range req.Databases { - sources[idx] = fmt.Sprintf("SELECT * FROM %s.connections %s", db, whereClause) - } - - source := strings.Join(sources, " UNION ") - - query := `SELECT ` + selectClause + ` FROM ( ` + source + ` ) ` - - query += " " + groupByClause + " " + orderByClause + " " + req.Pagination.toSQLLimitOffsetClause() - - return strings.TrimSpace(query), req.paramMap, nil -} - -func (req *QueryRequestPayload) prepareSelectedFields(ctx context.Context, schema *orm.TableSchema) error { - for idx, s := range req.Select { - var field string - - switch { - case s.Count != nil: - field = s.Count.Field - case s.Distinct != nil: - field = *s.Distinct - case s.Sum != nil: - if s.Sum.Field != "" { - field = s.Sum.Field - } else { - field = "*" - } - case s.Min != nil: - if s.Min.Field != "" { - field = s.Min.Field - } else { - field = "*" - } - default: - field = s.Field - } - - colName := "*" - if field != "*" || (s.Count == nil && s.Sum == nil) { - var err error - - colName, err = req.validateColumnName(schema, field) - if err != nil { - return err - } - } - - switch { - case s.Count != nil: - as := s.Count.As - if as == "" { - as = fmt.Sprintf("%s_count", colName) - } - distinct := "" - if s.Count.Distinct { - distinct = "DISTINCT " + var merr *multierror.Error + if errors.As(err, &merr) { + for _, e := range merr.Errors { + resp.Header().Add("X-Query-Error", e.Error()) } - req.selectedFields = append( - req.selectedFields, - fmt.Sprintf("COUNT(%s%s) AS %s", distinct, colName, as), - ) - req.whitelistedFields = append(req.whitelistedFields, as) - - case s.Sum != nil: - if s.Sum.As == "" { - return fmt.Errorf("missing 'as' for $sum") - } - - var ( - clause string - params map[string]any - ) - - if s.Sum.Field != "" { - clause = s.Sum.Field - } else { - var err error - clause, params, err = s.Sum.Condition.toSQLWhereClause(ctx, fmt.Sprintf("sel%d", idx), schema, orm.DefaultEncodeConfig) - if err != nil { - return fmt.Errorf("in $sum: %w", err) - } - } - - req.mergeParams(params) - req.selectedFields = append( - req.selectedFields, - fmt.Sprintf("SUM(%s) AS %s", clause, s.Sum.As), - ) - req.whitelistedFields = append(req.whitelistedFields, s.Sum.As) - - case s.Min != nil: - if s.Min.As == "" { - return fmt.Errorf("missing 'as' for $min") - } - - var ( - clause string - params map[string]any - ) - - if s.Min.Field != "" { - clause = field - } else { - var err error - clause, params, err = s.Min.Condition.toSQLWhereClause(ctx, fmt.Sprintf("sel%d", idx), schema, orm.DefaultEncodeConfig) - if err != nil { - return fmt.Errorf("in $min: %w", err) - } - } - - req.mergeParams(params) - req.selectedFields = append( - req.selectedFields, - fmt.Sprintf("MIN(%s) AS %s", clause, s.Min.As), - ) - req.whitelistedFields = append(req.whitelistedFields, s.Min.As) - - case s.Distinct != nil: - req.selectedFields = append(req.selectedFields, fmt.Sprintf("DISTINCT %s", colName)) - req.whitelistedFields = append(req.whitelistedFields, colName) + } else { + // Should not happen, ExecuteBatch always returns a multierror.Error + resp.WriteHeader(status) - default: - req.selectedFields = append(req.selectedFields, colName) + return } } - return nil -} - -func (req *QueryRequestPayload) mergeParams(params map[string]any) { - if req.paramMap == nil { - req.paramMap = make(map[string]any) - } + timingQueryExecute.Stop() - for key, value := range params { - req.paramMap[key] = value + // collect the results + for _, b := range batches { + response[b.ID] = *b.Result } -} -func (req *QueryRequestPayload) generateGroupByClause(schema *orm.TableSchema) (string, error) { - if len(req.GroupBy) == 0 { - return "", nil - } + // send the HTTP status code + resp.WriteHeader(status) - groupBys := make([]string, len(req.GroupBy)) - for idx, name := range req.GroupBy { - colName, err := req.validateColumnName(schema, name) - if err != nil { - return "", err - } + // prepare the result encoder. + enc := json.NewEncoder(resp) + enc.SetEscapeHTML(false) + enc.SetIndent("", " ") - groupBys[idx] = colName - } - groupByClause := "GROUP BY " + strings.Join(groupBys, ", ") + // and finally stream the response + if err := enc.Encode(response); err != nil { + // we failed to encode the JSON body to resp so we likely either already sent a + // few bytes or the pipe was already closed. In either case, trying to send the + // error using http.Error() is non-sense. We just log it out here and that's all + // we can do. + log.Errorf("failed to encode JSON response: %s", err) - // if there are no explicitly selected fields we default to the - // group-by columns as that's what's expected most of the time anyway... - if len(req.selectedFields) == 0 { - req.selectedFields = append(req.selectedFields, groupBys...) + return } - - return groupByClause, nil } -func (req *QueryRequestPayload) generateSelectClause() string { - selectClause := "*" - if len(req.selectedFields) > 0 { - selectClause = strings.Join(req.selectedFields, ", ") - } - - return selectClause -} +func parseQueryRequestPayload[T any](req *http.Request) (*T, error) { //nolint:dupl + var ( + body io.Reader + requestPayload T + ) -func (req *QueryRequestPayload) generateOrderByClause(schema *orm.TableSchema) (string, error) { - if len(req.OrderBy) == 0 { - return "", nil + switch req.Method { + case http.MethodPost, http.MethodPut: + body = req.Body + case http.MethodGet: + body = strings.NewReader(req.URL.Query().Get("q")) + default: + return nil, fmt.Errorf("invalid HTTP method") } - orderBys := make([]string, len(req.OrderBy)) - for idx, sort := range req.OrderBy { - colName, err := req.validateColumnName(schema, sort.Field) - if err != nil { - return "", err - } - - if sort.Desc { - orderBys[idx] = fmt.Sprintf("%s DESC", colName) - } else { - orderBys[idx] = fmt.Sprintf("%s ASC", colName) - } + blob, err := io.ReadAll(body) + if err != nil { + return nil, fmt.Errorf("failed to read body" + err.Error()) } - return "ORDER BY " + strings.Join(orderBys, ", "), nil -} - -func (req *QueryRequestPayload) validateColumnName(schema *orm.TableSchema, field string) (string, error) { - colDef := schema.GetColumnDef(field) - if colDef != nil { - return colDef.Name, nil - } + body = bytes.NewReader(blob) - if slices.Contains(req.whitelistedFields, field) { - return field, nil - } + dec := json.NewDecoder(body) + dec.DisallowUnknownFields() - if slices.Contains(req.selectedFields, field) { - return field, nil + if err := json.Unmarshal(blob, &requestPayload); err != nil && !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("invalid query: %w", err) } - return "", fmt.Errorf("column name %q not allowed", field) + return &requestPayload, nil } // Compile time check. diff --git a/netquery/query_request.go b/netquery/query_request.go new file mode 100644 index 000000000..b4a07041c --- /dev/null +++ b/netquery/query_request.go @@ -0,0 +1,306 @@ +package netquery + +import ( + "context" + "fmt" + "strings" + + "golang.org/x/exp/slices" + + "github.com/safing/portmaster/netquery/orm" +) + +type ( + // QueryRequestPayload describes the payload of a netquery query. + QueryRequestPayload struct { + Select Selects `json:"select"` + Query Query `json:"query"` + OrderBy OrderBys `json:"orderBy"` + GroupBy []string `json:"groupBy"` + TextSearch *TextSearch `json:"textSearch"` + // A list of databases to query. If left empty, + // both, the LiveDatabase and the HistoryDatabase are queried + Databases []DatabaseName `json:"databases"` + + Pagination + + selectedFields []string + whitelistedFields []string + paramMap map[string]interface{} + } + + // BatchQueryRequestPayload describes the payload of a batch netquery + // query. The map key is used in the response to identify the results + // for each query of the batch request. + BatchQueryRequestPayload map[string]QueryRequestPayload +) + +func (req *QueryRequestPayload) generateSQL(ctx context.Context, schema *orm.TableSchema) (string, map[string]interface{}, error) { + if err := req.prepareSelectedFields(ctx, schema); err != nil { + return "", nil, fmt.Errorf("perparing selected fields: %w", err) + } + + // build the SQL where clause from the payload query + whereClause, paramMap, err := req.Query.toSQLWhereClause( + ctx, + "", + schema, + orm.DefaultEncodeConfig, + ) + if err != nil { + return "", nil, fmt.Errorf("generating where clause: %w", err) + } + + req.mergeParams(paramMap) + + if req.TextSearch != nil { + textClause, textParams, err := req.TextSearch.toSQLConditionClause(ctx, schema, "", orm.DefaultEncodeConfig) + if err != nil { + return "", nil, fmt.Errorf("generating text-search clause: %w", err) + } + + if textClause != "" { + if whereClause != "" { + whereClause += " AND " + } + + whereClause += textClause + + req.mergeParams(textParams) + } + } + + groupByClause, err := req.generateGroupByClause(schema) + if err != nil { + return "", nil, fmt.Errorf("generating group-by clause: %w", err) + } + + orderByClause, err := req.generateOrderByClause(schema) + if err != nil { + return "", nil, fmt.Errorf("generating order-by clause: %w", err) + } + + selectClause := req.generateSelectClause() + + if whereClause != "" { + whereClause = "WHERE " + whereClause + } + + // if no database is specified we default to LiveDatabase only. + if len(req.Databases) == 0 { + req.Databases = []DatabaseName{LiveDatabase} + } + + sources := make([]string, len(req.Databases)) + for idx, db := range req.Databases { + sources[idx] = fmt.Sprintf("SELECT * FROM %s.connections %s", db, whereClause) + } + + source := strings.Join(sources, " UNION ") + + query := `SELECT ` + selectClause + ` FROM ( ` + source + ` ) ` + + query += " " + groupByClause + " " + orderByClause + " " + req.Pagination.toSQLLimitOffsetClause() + + return strings.TrimSpace(query), req.paramMap, nil +} + +func (req *QueryRequestPayload) prepareSelectedFields(ctx context.Context, schema *orm.TableSchema) error { + for idx, s := range req.Select { + var field string + + switch { + case s.Count != nil: + field = s.Count.Field + case s.Distinct != nil: + field = *s.Distinct + case s.Sum != nil: + if s.Sum.Field != "" { + field = s.Sum.Field + } else { + field = "*" + } + case s.Min != nil: + if s.Min.Field != "" { + field = s.Min.Field + } else { + field = "*" + } + default: + field = s.Field + } + + colName := "*" + if field != "*" || (s.Count == nil && s.Sum == nil) { + var err error + + colName, err = req.validateColumnName(schema, field) + if err != nil { + return err + } + } + + switch { + case s.Count != nil: + as := s.Count.As + if as == "" { + as = fmt.Sprintf("%s_count", colName) + } + distinct := "" + if s.Count.Distinct { + distinct = "DISTINCT " + } + req.selectedFields = append( + req.selectedFields, + fmt.Sprintf("COUNT(%s%s) AS %s", distinct, colName, as), + ) + req.whitelistedFields = append(req.whitelistedFields, as) + + case s.Sum != nil: + if s.Sum.As == "" { + return fmt.Errorf("missing 'as' for $sum") + } + + var ( + clause string + params map[string]any + ) + + if s.Sum.Field != "" { + clause = s.Sum.Field + } else { + var err error + clause, params, err = s.Sum.Condition.toSQLWhereClause(ctx, fmt.Sprintf("sel%d", idx), schema, orm.DefaultEncodeConfig) + if err != nil { + return fmt.Errorf("in $sum: %w", err) + } + } + + req.mergeParams(params) + req.selectedFields = append( + req.selectedFields, + fmt.Sprintf("SUM(%s) AS %s", clause, s.Sum.As), + ) + req.whitelistedFields = append(req.whitelistedFields, s.Sum.As) + + case s.Min != nil: + if s.Min.As == "" { + return fmt.Errorf("missing 'as' for $min") + } + + var ( + clause string + params map[string]any + ) + + if s.Min.Field != "" { + clause = field + } else { + var err error + clause, params, err = s.Min.Condition.toSQLWhereClause(ctx, fmt.Sprintf("sel%d", idx), schema, orm.DefaultEncodeConfig) + if err != nil { + return fmt.Errorf("in $min: %w", err) + } + } + + req.mergeParams(params) + req.selectedFields = append( + req.selectedFields, + fmt.Sprintf("MIN(%s) AS %s", clause, s.Min.As), + ) + req.whitelistedFields = append(req.whitelistedFields, s.Min.As) + + case s.Distinct != nil: + req.selectedFields = append(req.selectedFields, fmt.Sprintf("DISTINCT %s", colName)) + req.whitelistedFields = append(req.whitelistedFields, colName) + + default: + req.selectedFields = append(req.selectedFields, colName) + } + } + + return nil +} + +func (req *QueryRequestPayload) mergeParams(params map[string]any) { + if req.paramMap == nil { + req.paramMap = make(map[string]any) + } + + for key, value := range params { + req.paramMap[key] = value + } +} + +func (req *QueryRequestPayload) generateGroupByClause(schema *orm.TableSchema) (string, error) { + if len(req.GroupBy) == 0 { + return "", nil + } + + groupBys := make([]string, len(req.GroupBy)) + for idx, name := range req.GroupBy { + colName, err := req.validateColumnName(schema, name) + if err != nil { + return "", err + } + + groupBys[idx] = colName + } + groupByClause := "GROUP BY " + strings.Join(groupBys, ", ") + + // if there are no explicitly selected fields we default to the + // group-by columns as that's what's expected most of the time anyway... + if len(req.selectedFields) == 0 { + req.selectedFields = append(req.selectedFields, groupBys...) + } + + return groupByClause, nil +} + +func (req *QueryRequestPayload) generateSelectClause() string { + selectClause := "*" + if len(req.selectedFields) > 0 { + selectClause = strings.Join(req.selectedFields, ", ") + } + + return selectClause +} + +func (req *QueryRequestPayload) generateOrderByClause(schema *orm.TableSchema) (string, error) { + if len(req.OrderBy) == 0 { + return "", nil + } + + orderBys := make([]string, len(req.OrderBy)) + for idx, sort := range req.OrderBy { + colName, err := req.validateColumnName(schema, sort.Field) + if err != nil { + return "", err + } + + if sort.Desc { + orderBys[idx] = fmt.Sprintf("%s DESC", colName) + } else { + orderBys[idx] = fmt.Sprintf("%s ASC", colName) + } + } + + return "ORDER BY " + strings.Join(orderBys, ", "), nil +} + +func (req *QueryRequestPayload) validateColumnName(schema *orm.TableSchema, field string) (string, error) { + colDef := schema.GetColumnDef(field) + if colDef != nil { + return colDef.Name, nil + } + + if slices.Contains(req.whitelistedFields, field) { + return field, nil + } + + if slices.Contains(req.selectedFields, field) { + return field, nil + } + + return "", fmt.Errorf("column name %q not allowed", field) +} diff --git a/resolver/resolver_test.go b/resolver/resolver_test.go index a03b3eb97..397a914cd 100644 --- a/resolver/resolver_test.go +++ b/resolver/resolver_test.go @@ -119,7 +119,7 @@ func TestPublicSuffix(t *testing.T) { testSuffix(t, "golang.dev.", "golang.dev.", true) testSuffix(t, "golang.net.", "golang.net.", true) testSuffix(t, "play.golang.org.", "golang.org.", true) - testSuffix(t, "gophers.in.space.museum.", "in.space.museum.", true) + testSuffix(t, "gophers.in.space.museum.", "space.museum.", true) testSuffix(t, "0emm.com.", "0emm.com.", true) testSuffix(t, "a.0emm.com.", "", true) testSuffix(t, "b.c.d.0emm.com.", "c.d.0emm.com.", true)