mirror of
https://github.com/safing/portbase
synced 2025-09-01 18:19:57 +00:00
Release to master
This commit is contained in:
commit
d825f813ba
57 changed files with 3495 additions and 670 deletions
87
.github/workflows/go.yml
vendored
Normal file
87
.github/workflows/go.yml
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- develop
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.15'
|
||||
|
||||
# nektos/act does not have sudo install but we need it on GH actions so
|
||||
# try to install it.
|
||||
- name: Install sudo
|
||||
run: bash -c "apt-get update || true ; apt-get install sudo || true"
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
- name: Install git and gcc
|
||||
run: sudo bash -c "apt-get update && apt-get install -y git gcc libc6-dev"
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.29
|
||||
only-new-issues: true
|
||||
args: -c ./.golangci.yml
|
||||
skip-go-installation: true
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Install golint
|
||||
run: bash -c "GOBIN=$(pwd) go get -u golang.org/x/lint/golint"
|
||||
|
||||
- name: Run golint
|
||||
run: ./golint -set_exit_status -min_confidence 1.0 ./...
|
||||
|
||||
- name: Run gofmt
|
||||
run: bash -c "test -z $(gofmt -s -l .)"
|
||||
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: '^1.15'
|
||||
|
||||
# nektos/act does not have sudo install but we need it on GH actions so
|
||||
# try to install it.
|
||||
- name: Install sudo
|
||||
run: bash -c "apt-get update || true ; apt-get install sudo || true"
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
- name: Install git and gcc
|
||||
run: sudo bash -c "apt-get update && apt-get install -y git gcc libc6-dev"
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Test
|
||||
run: ./test --test-only
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,4 +2,5 @@ portbase
|
|||
apitest
|
||||
misc
|
||||
|
||||
go.mod.*
|
||||
vendor
|
||||
|
|
|
@ -8,6 +8,8 @@ linters:
|
|||
- whitespace
|
||||
- wsl
|
||||
- gomnd
|
||||
- goerr113
|
||||
- testpackage
|
||||
|
||||
linters-settings:
|
||||
godox:
|
||||
|
|
|
@ -17,7 +17,7 @@ branches:
|
|||
install:
|
||||
- go get -d -u github.com/golang/dep
|
||||
- go install github.com/golang/dep/cmd/dep
|
||||
- dep ensure
|
||||
- go mod download
|
||||
- ./test install
|
||||
|
||||
script: ./test --scripted
|
||||
|
|
400
Gopkg.lock
generated
400
Gopkg.lock
generated
|
@ -1,400 +0,0 @@
|
|||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9cac35beaf0b218e41036e92ec8f664146227a6549a3bd307bb7b223cb40313b"
|
||||
name = "github.com/AndreasBriese/bbloom"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "46b345b51c9667fcbaad862a370d73bd7aa802b6"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:6146fda730c18186631e91e818d995e759e7cbe27644d6871ccd469f6865c686"
|
||||
name = "github.com/StackExchange/wmi"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "cbe66965904dbe8a6cd589e2298e5d8b986bd7dd"
|
||||
version = "1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3b186fcaa1c0b374249e76601cd4d5c8c7f0712f8ad419f2e8288c7b9e7ed520"
|
||||
name = "github.com/aead/serpent"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "fba169763ea663f7496376e5cdf709e4c7504704"
|
||||
version = "v0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5680f8c40e48f07cb77aece3165a866aaf8276305258b3b70db8ec7ad6ddb78d"
|
||||
name = "github.com/armon/go-radix"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "1a2de0c21c94309923825da3df33a4381872c795"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:baf770c4efa1883bb5e444614e85b8028bbad33913aca290a43298f65d9df485"
|
||||
name = "github.com/bluele/gcache"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "bc40bd6538339bd4be8cb0b48fc33fdf012ddb6e"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:d25acc7560ed91f825cb9b01a1e945bb1117cdcaba19077137e2d9ffc9cf6d05"
|
||||
name = "github.com/cespare/xxhash"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "d7df74196a9e781ede915320c11c378c1b2f3a1f"
|
||||
version = "v2.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:0deddd908b6b4b768cfc272c16ee61e7088a60f7fe2f06c547bd3d8e1f8b8e77"
|
||||
name = "github.com/davecgh/go-spew"
|
||||
packages = ["spew"]
|
||||
pruneopts = ""
|
||||
revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:238e2deec0c24dca94461b2f72fe05eba40063b653677e60901af56a6fd5148b"
|
||||
name = "github.com/dgraph-io/badger"
|
||||
packages = [
|
||||
".",
|
||||
"options",
|
||||
"pb",
|
||||
"skl",
|
||||
"table",
|
||||
"trie",
|
||||
"y",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "8760a5018cd670a5cecc79a7e927ecaf47acc434"
|
||||
version = "v1.6.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:1d9a7f60f0ff31ba3336a9c1349c212e7076845b537c053a1703a26730145167"
|
||||
name = "github.com/dgraph-io/ristretto"
|
||||
packages = ["z"]
|
||||
pruneopts = ""
|
||||
revision = "dbc185e050f48c5190a78b4a07829d9c10afca21"
|
||||
version = "v0.0.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f1a75a8e00244e5ea77ff274baa9559eb877437b240ee7b278f3fc560d9f08bf"
|
||||
name = "github.com/dustin/go-humanize"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "9f541cc9db5d55bce703bd99987c9d5cb8eea45e"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b6581f9180e0f2d5549280d71819ab951db9d511478c87daca95669589d505c0"
|
||||
name = "github.com/go-ole/go-ole"
|
||||
packages = [
|
||||
".",
|
||||
"oleutil",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "97b6244175ae18ea6eef668034fd6565847501c9"
|
||||
version = "v1.2.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fd608a9f543f30dd5a086b2cdd0bce71b65bca62798ff7ce21348806cdf1412d"
|
||||
name = "github.com/gofrs/uuid"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "abfe1881e60ef34074c1b8d8c63b42565c356ed6"
|
||||
version = "v3.3.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:02c7a8570f619bdb5620e8f6a16407455c8e63433d8a9e829f618813dc7f89a5"
|
||||
name = "github.com/golang/protobuf"
|
||||
packages = ["proto"]
|
||||
pruneopts = ""
|
||||
revision = "d04d7b157bb510b1e0c10132224b616ac0e26b17"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:20dc576ad8f98fe64777c62f090a9b37dd67c62b23fe42b429c2c41936aa8a9c"
|
||||
name = "github.com/google/renameio"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "f0e32980c006571efd537032e5f9cd8c1a92819e"
|
||||
version = "v0.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:fbab76ba211c99fcd45a481a32530efc229f3510fd94889f361dcaf13ff05fe0"
|
||||
name = "github.com/gorilla/mux"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "75dcda0896e109a2a22c9315bca3bb21b87b2ba5"
|
||||
version = "v1.7.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:5122b0b5a933bd0e16b887aa7844933c8a09fb5ba9b5f5651253cb5336c6462a"
|
||||
name = "github.com/gorilla/websocket"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "b65e62901fc1c0d968042419e74789f6af455eb9"
|
||||
version = "v1.4.2"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:eaed935e3637c60ad9897e54ab3419c18b91775d6e3af339dec54aeefb48b8d6"
|
||||
name = "github.com/hashicorp/errwrap"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "7b00e5db719c64d14dd0caaacbd13e76254d02c0"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c6e569ffa34fcd24febd3562bff0520a104d15d1a600199cb3141debf2e58c89"
|
||||
name = "github.com/hashicorp/go-multierror"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "2004d9dba6b07a5b8d133209244f376680f9d472"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2f0c811248aeb64978037b357178b1593372439146bda860cb16f2c80785ea93"
|
||||
name = "github.com/hashicorp/go-version"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "ac23dc3fea5d1a983c43f6a0f6e2c13f0195d8bd"
|
||||
version = "v1.2.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be"
|
||||
name = "github.com/inconshreveable/mousetrap"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
version = "v1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:c45802472e0c06928cd997661f2af610accd85217023b1d5f6331bebce0671d3"
|
||||
name = "github.com/pkg/errors"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "614d223910a179a466c1767a985424175c39b465"
|
||||
version = "v0.9.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:256484dbbcd271f9ecebc6795b2df8cad4c458dd0f5fd82a8c2fa0c29f233411"
|
||||
name = "github.com/pmezard/go-difflib"
|
||||
packages = ["difflib"]
|
||||
pruneopts = ""
|
||||
revision = "792786c7400a136282c1664665ae0a8db921c6c2"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:90da107a52bdacf25384ae6fc2889df9cf3c956ecde1561825b13ba70a8283b5"
|
||||
name = "github.com/seehuhn/fortuna"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "6f13c9c4f925cf5dd33c070a2f0fbe596385917b"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bf9a02016247817c7ca7dc0d42ee608ffdef20de1474f6d676cfd80a64068a38"
|
||||
name = "github.com/seehuhn/sha256d"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "4a65999787a5902349359436a10df5fe59a10a64"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:16f319cf21ddf49f27b3a2093d68316840dc25ec5c2a0a431a4a4fc01ea707e2"
|
||||
name = "github.com/shirou/gopsutil"
|
||||
packages = [
|
||||
"cpu",
|
||||
"host",
|
||||
"internal/common",
|
||||
"mem",
|
||||
"net",
|
||||
"process",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "a81cf97fce2300934e6c625b9917103346c26ba3"
|
||||
version = "v2.20.4"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:bff75d4f1a2d2c4b8f4b46ff5ac230b80b5fa49276f615900cba09fe4c97e66e"
|
||||
name = "github.com/spf13/cobra"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a684a6d7f5e37385d954dd3b5a14fc6912c6ab9d"
|
||||
version = "v1.0.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:688428eeb1ca80d92599eb3254bdf91b51d7e232fead3a73844c1f201a281e51"
|
||||
name = "github.com/spf13/pflag"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "2e9d26c8c37aae03e3f9d4e90b7116f5accb7cab"
|
||||
version = "v1.0.5"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:83fd2513b9f6ae0997bf646db6b74e9e00131e31002116fda597175f25add42d"
|
||||
name = "github.com/stretchr/testify"
|
||||
packages = [
|
||||
"assert",
|
||||
"require",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "f654a9112bbeac49ca2cd45bfbe11533c4666cf8"
|
||||
version = "v1.6.1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:86e6712cfd4070a2120c03fcec41cfcbbc51813504a74e28d74479edfaf669ee"
|
||||
name = "github.com/tevino/abool"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "9b9efcf221b50905aab9bbabd3daed56dc10f339"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:029181b60f6ea672544d102f81d7d9508a45a70e17ce8a649535959e0249d56b"
|
||||
name = "github.com/tidwall/gjson"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "f042915ca17de35980544c91ab2c8ceb73b682f2"
|
||||
version = "v1.6.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:72511ec1089fee111c995492d1d390a38ac7ab888aabdb1188985f2a1719c599"
|
||||
name = "github.com/tidwall/match"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "33827db735fff6510490d69a8622612558a557ed"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:3d4deb9e8160077721a79fc1b5a6ce27016c98f9f8e631090d5af8f50120ddf0"
|
||||
name = "github.com/tidwall/pretty"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "b2475501f89994f7ea30b3c94ba86b49079961fe"
|
||||
version = "v1.0.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:b749bf81c4c595c1218489d2fa4d90e4a953a5fd11420722a2f57c0c5beecb92"
|
||||
name = "github.com/tidwall/sjson"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "11cb24d8421de3e1bb5c5efb066a03037150568d"
|
||||
version = "v1.1.1"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:2a591e844f6019284ded9f5095e53764c3d69bb8e7d1cf2b318f1b7e25864508"
|
||||
name = "go.etcd.io/bbolt"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "68cc10a767ea1c6b9e8dcb9847317ff192d6d974"
|
||||
version = "v1.3.4"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:305d718b88fcd3b251b910416367de49af1e7944a9a17efabedab5f0ba7745de"
|
||||
name = "golang.org/x/net"
|
||||
packages = [
|
||||
"internal/timeseries",
|
||||
"trace",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "0ba52f642ac2f9371a88bfdde41f4b4e195a37c0"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:10d47e7094ce8dd202cca920e4c58a68ba1d113908c30fb0cc8590b7d333a348"
|
||||
name = "golang.org/x/sync"
|
||||
packages = ["errgroup"]
|
||||
pruneopts = ""
|
||||
revision = "67f06af15bc961c363a7260195bcd53487529a21"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:bf837d996e7dfe7b819cbe53c8c9733e93228577f0561e43996b9ef0ea8a68a9"
|
||||
name = "golang.org/x/sys"
|
||||
packages = [
|
||||
"internal/unsafeheader",
|
||||
"unix",
|
||||
"windows",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "05986578812163b26672dabd9b425240ae2bb0ad"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:f3bbd8ea54cde834a67fc50f27cbdf35eb950225953fb304891be068ba82d163"
|
||||
name = "google.golang.org/protobuf"
|
||||
packages = [
|
||||
"encoding/prototext",
|
||||
"encoding/protowire",
|
||||
"internal/descfmt",
|
||||
"internal/descopts",
|
||||
"internal/detrand",
|
||||
"internal/encoding/defval",
|
||||
"internal/encoding/messageset",
|
||||
"internal/encoding/tag",
|
||||
"internal/encoding/text",
|
||||
"internal/errors",
|
||||
"internal/fieldnum",
|
||||
"internal/fieldsort",
|
||||
"internal/filedesc",
|
||||
"internal/filetype",
|
||||
"internal/flags",
|
||||
"internal/genname",
|
||||
"internal/impl",
|
||||
"internal/mapsort",
|
||||
"internal/pragma",
|
||||
"internal/set",
|
||||
"internal/strs",
|
||||
"internal/version",
|
||||
"proto",
|
||||
"reflect/protoreflect",
|
||||
"reflect/protoregistry",
|
||||
"runtime/protoiface",
|
||||
"runtime/protoimpl",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "d165be301fb1e13390ad453281ded24385fd8ebc"
|
||||
version = "v1.23.0"
|
||||
|
||||
[[projects]]
|
||||
branch = "v3"
|
||||
digest = "1:0aa137e32b369fbb8c0f4d579e653c72e3431b6cc8cb3c19d6a21a14209031fd"
|
||||
name = "gopkg.in/yaml.v3"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "a5ece683394c3b88d90572e44d36c93aea492c2c"
|
||||
|
||||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
input-imports = [
|
||||
"github.com/aead/serpent",
|
||||
"github.com/armon/go-radix",
|
||||
"github.com/bluele/gcache",
|
||||
"github.com/davecgh/go-spew/spew",
|
||||
"github.com/dgraph-io/badger",
|
||||
"github.com/gofrs/uuid",
|
||||
"github.com/google/renameio",
|
||||
"github.com/gorilla/mux",
|
||||
"github.com/gorilla/websocket",
|
||||
"github.com/hashicorp/go-multierror",
|
||||
"github.com/hashicorp/go-version",
|
||||
"github.com/seehuhn/fortuna",
|
||||
"github.com/shirou/gopsutil/host",
|
||||
"github.com/spf13/cobra",
|
||||
"github.com/stretchr/testify/assert",
|
||||
"github.com/stretchr/testify/require",
|
||||
"github.com/tevino/abool",
|
||||
"github.com/tidwall/gjson",
|
||||
"github.com/tidwall/sjson",
|
||||
"go.etcd.io/bbolt",
|
||||
"golang.org/x/sync/errgroup",
|
||||
"golang.org/x/sys/windows",
|
||||
]
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
25
Gopkg.toml
25
Gopkg.toml
|
@ -1,25 +0,0 @@
|
|||
# Gopkg.toml example
|
||||
#
|
||||
# Refer to https://golang.github.io/dep/docs/Gopkg.toml.html
|
||||
# for detailed Gopkg.toml documentation.
|
||||
#
|
||||
# required = ["github.com/user/thing/cmd/thing"]
|
||||
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project"
|
||||
# version = "1.0.0"
|
||||
#
|
||||
# [[constraint]]
|
||||
# name = "github.com/user/project2"
|
||||
# branch = "dev"
|
||||
# source = "github.com/myfork/project2"
|
||||
#
|
||||
# [[override]]
|
||||
# name = "github.com/x/y"
|
||||
# version = "2.4.0"
|
||||
#
|
||||
# [prune]
|
||||
# non-go = false
|
||||
# go-tests = true
|
||||
# unused-packages = true
|
30
api/auth_wrapper.go
Normal file
30
api/auth_wrapper.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package api
|
||||
|
||||
import "net/http"
|
||||
|
||||
// WrapInAuthHandler wraps a simple http.HandlerFunc into a handler that
|
||||
// exposes the required API permissions for this handler.
|
||||
func WrapInAuthHandler(fn http.HandlerFunc, read, write Permission) http.Handler {
|
||||
return &wrappedAuthenticatedHandler{
|
||||
HandlerFunc: fn,
|
||||
read: read,
|
||||
write: write,
|
||||
}
|
||||
}
|
||||
|
||||
type wrappedAuthenticatedHandler struct {
|
||||
http.HandlerFunc
|
||||
|
||||
read Permission
|
||||
write Permission
|
||||
}
|
||||
|
||||
// ReadPermission returns the read permission for the handler.
|
||||
func (wah *wrappedAuthenticatedHandler) ReadPermission(r *http.Request) Permission {
|
||||
return wah.read
|
||||
}
|
||||
|
||||
// WritePermission returns the write permission for the handler.
|
||||
func (wah *wrappedAuthenticatedHandler) WritePermission(r *http.Request) Permission {
|
||||
return wah.write
|
||||
}
|
|
@ -4,47 +4,128 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tevino/abool"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/rng"
|
||||
)
|
||||
|
||||
const (
|
||||
sessionCookieName = "Portmaster-API-Token"
|
||||
sessionCookieTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
validTokens = make(map[string]time.Time)
|
||||
validTokensLock sync.Mutex
|
||||
apiKeys = make(map[string]*AuthToken)
|
||||
apiKeysLock sync.Mutex
|
||||
|
||||
authFnLock sync.Mutex
|
||||
authFn Authenticator
|
||||
authFnSet = abool.New()
|
||||
authFn AuthenticatorFunc
|
||||
|
||||
// ErrAPIAccessDeniedMessage should be returned by Authenticator functions in
|
||||
// order to signify a blocked request, including a error message for the user.
|
||||
sessions = make(map[string]*session)
|
||||
sessionsLock sync.Mutex
|
||||
|
||||
// ErrAPIAccessDeniedMessage should be wrapped by errors returned by
|
||||
// AuthenticatorFunc in order to signify a blocked request, including a error
|
||||
// message for the user. This is an empty message on purpose, as to allow the
|
||||
// function to define the full text of the error shown to the user.
|
||||
ErrAPIAccessDeniedMessage = errors.New("")
|
||||
)
|
||||
|
||||
const (
|
||||
cookieName = "Portmaster-API-Token"
|
||||
// Permission defines an API requests permission.
|
||||
type Permission int8
|
||||
|
||||
cookieTTL = 5 * time.Minute
|
||||
const (
|
||||
// NotFound declares that the operation does not exist.
|
||||
NotFound Permission = -2
|
||||
|
||||
// Dynamic declares that the operation requires permission to be processed,
|
||||
// but anyone can execute the operation, as it reacts to permissions itself.
|
||||
Dynamic Permission = -1
|
||||
|
||||
// NotSupported declares that the operation is not supported.
|
||||
NotSupported Permission = 0
|
||||
|
||||
// PermitAnyone declares that anyone can execute the operation without any
|
||||
// authentication.
|
||||
PermitAnyone Permission = 1
|
||||
|
||||
// PermitUser declares that the operation may be executed by authenticated
|
||||
// third party applications that are categorized as representing a simple
|
||||
// user and is limited in access.
|
||||
PermitUser Permission = 2
|
||||
|
||||
// PermitAdmin declares that the operation may be executed by authenticated
|
||||
// third party applications that are categorized as representing an
|
||||
// administrator and has broad in access.
|
||||
PermitAdmin Permission = 3
|
||||
|
||||
// PermitSelf declares that the operation may only be executed by the
|
||||
// software itself and its own (first party) components.
|
||||
PermitSelf Permission = 4
|
||||
)
|
||||
|
||||
// Authenticator is a function that can be set as the authenticator for the API endpoint. If none is set, all requests will be permitted.
|
||||
type Authenticator func(ctx context.Context, s *http.Server, r *http.Request) (err error)
|
||||
// AuthenticatorFunc is a function that can be set as the authenticator for the
|
||||
// API endpoint. If none is set, all requests will have full access.
|
||||
// The returned AuthToken represents the permissions that the request has.
|
||||
type AuthenticatorFunc func(r *http.Request, s *http.Server) (*AuthToken, error)
|
||||
|
||||
// AuthToken represents either a set of required or granted permissions.
|
||||
// All attributes must be set when the struct is built and must not be changed
|
||||
// later. Functions may be called at any time.
|
||||
// The Write permission implicitly also includes reading.
|
||||
type AuthToken struct {
|
||||
Read Permission
|
||||
Write Permission
|
||||
}
|
||||
|
||||
type session struct {
|
||||
sync.Mutex
|
||||
|
||||
token *AuthToken
|
||||
validUntil time.Time
|
||||
}
|
||||
|
||||
// Expired returns whether the session has expired.
|
||||
func (sess *session) Expired() bool {
|
||||
sess.Lock()
|
||||
defer sess.Unlock()
|
||||
|
||||
return time.Now().After(sess.validUntil)
|
||||
}
|
||||
|
||||
// Refresh refreshes the validity of the session with the given TTL.
|
||||
func (sess *session) Refresh(ttl time.Duration) {
|
||||
sess.Lock()
|
||||
defer sess.Unlock()
|
||||
|
||||
sess.validUntil = time.Now().Add(ttl)
|
||||
}
|
||||
|
||||
// AuthenticatedHandler defines the handler interface to specify custom
|
||||
// permission for an API handler. The returned permission is the required
|
||||
// permission for the request to proceed.
|
||||
type AuthenticatedHandler interface {
|
||||
ReadPermission(*http.Request) Permission
|
||||
WritePermission(*http.Request) Permission
|
||||
}
|
||||
|
||||
// SetAuthenticator sets an authenticator function for the API endpoint. If none is set, all requests will be permitted.
|
||||
func SetAuthenticator(fn Authenticator) error {
|
||||
func SetAuthenticator(fn AuthenticatorFunc) error {
|
||||
if module.Online() {
|
||||
return ErrAuthenticationImmutable
|
||||
}
|
||||
|
||||
authFnLock.Lock()
|
||||
defer authFnLock.Unlock()
|
||||
|
||||
if authFn != nil {
|
||||
if !authFnSet.SetToIf(false, true) {
|
||||
return ErrAuthenticationAlreadySet
|
||||
}
|
||||
|
||||
|
@ -52,94 +133,397 @@ func SetAuthenticator(fn Authenticator) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tracer := log.Tracer(r.Context())
|
||||
func authenticateRequest(w http.ResponseWriter, r *http.Request, targetHandler http.Handler) *AuthToken {
|
||||
tracer := log.Tracer(r.Context())
|
||||
|
||||
// get authenticator
|
||||
authFnLock.Lock()
|
||||
authenticator := authFn
|
||||
authFnLock.Unlock()
|
||||
// Check if request is read only.
|
||||
readRequest := isReadMethod(r.Method)
|
||||
|
||||
// permit if no authenticator set
|
||||
if authenticator == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
// Get required permission for target handler.
|
||||
requiredPermission := PermitSelf
|
||||
if authdHandler, ok := targetHandler.(AuthenticatedHandler); ok {
|
||||
if readRequest {
|
||||
requiredPermission = authdHandler.ReadPermission(r)
|
||||
} else {
|
||||
requiredPermission = authdHandler.WritePermission(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to do any authentication at all.
|
||||
switch requiredPermission {
|
||||
case NotFound:
|
||||
// Not found.
|
||||
tracer.Trace("api: authenticated handler reported: not found")
|
||||
http.Error(w, "Not found.", http.StatusNotFound)
|
||||
return nil
|
||||
case NotSupported:
|
||||
// A read or write permission can be marked as not supported.
|
||||
tracer.Trace("api: authenticated handler reported: not supported")
|
||||
http.Error(w, "Method not allowed.", http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
case PermitAnyone:
|
||||
// Don't process permissions, as we don't need them.
|
||||
tracer.Tracef("api: granted %s access to public handler", r.RemoteAddr)
|
||||
return &AuthToken{
|
||||
Read: PermitAnyone,
|
||||
Write: PermitAnyone,
|
||||
}
|
||||
case Dynamic:
|
||||
// Continue processing permissions, but treat as PermitAnyone.
|
||||
requiredPermission = PermitAnyone
|
||||
}
|
||||
|
||||
// The required permission must match the request permission values after
|
||||
// handling the specials.
|
||||
if requiredPermission < PermitAnyone || requiredPermission > PermitSelf {
|
||||
tracer.Warningf(
|
||||
"api: handler returned invalid permission: %s (%d)",
|
||||
requiredPermission,
|
||||
requiredPermission,
|
||||
)
|
||||
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Authenticate request.
|
||||
token, handled := checkAuth(w, r, requiredPermission > PermitAnyone)
|
||||
switch {
|
||||
case handled:
|
||||
return nil
|
||||
case token == nil:
|
||||
// Use default permissions.
|
||||
token = &AuthToken{
|
||||
Read: PermitAnyone,
|
||||
Write: PermitAnyone,
|
||||
}
|
||||
}
|
||||
|
||||
// Get effective permission for request.
|
||||
var requestPermission Permission
|
||||
if readRequest {
|
||||
requestPermission = token.Read
|
||||
} else {
|
||||
requestPermission = token.Write
|
||||
}
|
||||
|
||||
// Check for valid request permission.
|
||||
if requestPermission < PermitAnyone || requestPermission > PermitSelf {
|
||||
tracer.Warningf(
|
||||
"api: authenticator returned invalid permission: %s (%d)",
|
||||
requestPermission,
|
||||
requestPermission,
|
||||
)
|
||||
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check permission.
|
||||
if requestPermission < requiredPermission {
|
||||
// If the token is strictly public, return an authentication request.
|
||||
if token.Read == PermitAnyone && token.Write == PermitAnyone {
|
||||
w.Header().Set("WWW-Authenticate", "Bearer realm=Portmaster API")
|
||||
http.Error(w, "Authorization required.", http.StatusUnauthorized)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check existing auth cookie
|
||||
c, err := r.Cookie(cookieName)
|
||||
if err == nil {
|
||||
// get token
|
||||
validTokensLock.Lock()
|
||||
validUntil, valid := validTokens[c.Value]
|
||||
validTokensLock.Unlock()
|
||||
// Otherwise just inform of insufficient permissions.
|
||||
http.Error(w, "Insufficient permissions.", http.StatusForbidden)
|
||||
return nil
|
||||
}
|
||||
|
||||
// check if token is valid
|
||||
if valid && time.Now().Before(validUntil) {
|
||||
tracer.Tracef("api: auth token %s is valid, refreshing", c.Value)
|
||||
// refresh cookie
|
||||
validTokensLock.Lock()
|
||||
validTokens[c.Value] = time.Now().Add(cookieTTL)
|
||||
validTokensLock.Unlock()
|
||||
// continue
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
tracer.Tracef("api: granted %s access to protected handler", r.RemoteAddr)
|
||||
|
||||
tracer.Tracef("api: provided auth token %s is invalid", c.Value)
|
||||
}
|
||||
|
||||
// get auth decision
|
||||
err = authenticator(r.Context(), server, r)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrAPIAccessDeniedMessage) {
|
||||
tracer.Warningf("api: denying api access to %s", r.RemoteAddr)
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
} else {
|
||||
tracer.Warningf("api: authenticator failed: %s", err)
|
||||
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// generate new token
|
||||
token, err := rng.Bytes(32) // 256 bit
|
||||
if err != nil {
|
||||
tracer.Warningf("api: failed to generate random token: %s", err)
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
}
|
||||
tokenString := base64.RawURLEncoding.EncodeToString(token)
|
||||
// write new cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: tokenString,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: int(cookieTTL.Seconds()),
|
||||
})
|
||||
// save cookie
|
||||
validTokensLock.Lock()
|
||||
validTokens[tokenString] = time.Now().Add(cookieTTL)
|
||||
validTokensLock.Unlock()
|
||||
|
||||
// serve
|
||||
tracer.Tracef("api: granted %s, assigned auth token %s", r.RemoteAddr, tokenString)
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
// Make a copy of the AuthToken in order mitigate the handler poisoning the
|
||||
// token, as changes would apply to future requests.
|
||||
return &AuthToken{
|
||||
Read: token.Read,
|
||||
Write: token.Write,
|
||||
}
|
||||
}
|
||||
|
||||
func cleanAuthTokens(_ context.Context, _ *modules.Task) error {
|
||||
validTokensLock.Lock()
|
||||
defer validTokensLock.Unlock()
|
||||
func checkAuth(w http.ResponseWriter, r *http.Request, authRequired bool) (token *AuthToken, handled bool) {
|
||||
// Return highest possible permissions in dev mode.
|
||||
if devMode() {
|
||||
return &AuthToken{
|
||||
Read: PermitSelf,
|
||||
Write: PermitSelf,
|
||||
}, false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for token, validUntil := range validTokens {
|
||||
if now.After(validUntil) {
|
||||
delete(validTokens, token)
|
||||
// Check for valid API key.
|
||||
token = checkAPIKey(r)
|
||||
if token != nil {
|
||||
return token, false
|
||||
}
|
||||
|
||||
// Check for valid session cookie.
|
||||
token = checkSessionCookie(r)
|
||||
if token != nil {
|
||||
return token, false
|
||||
}
|
||||
|
||||
// Check if an external authentication method is available.
|
||||
if !authFnSet.IsSet() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Authenticate externally.
|
||||
token, err := authFn(r, server)
|
||||
if err != nil {
|
||||
// Check if the authentication process failed internally.
|
||||
if !errors.Is(err, ErrAPIAccessDeniedMessage) {
|
||||
log.Tracer(r.Context()).Errorf("api: authenticator failed: %s", err)
|
||||
http.Error(w, "Internal server error during authentication.", http.StatusInternalServerError)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
// Return authentication failure message if authentication is required.
|
||||
if authRequired {
|
||||
log.Tracer(r.Context()).Warningf("api: denying api access to %s", r.RemoteAddr)
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return nil, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Abort if no token is returned.
|
||||
if token == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Create session cookie for authenticated request.
|
||||
err = createSession(w, r, token)
|
||||
if err != nil {
|
||||
log.Tracer(r.Context()).Warningf("api: failed to create session: %s", err)
|
||||
}
|
||||
return token, false
|
||||
}
|
||||
|
||||
func checkAPIKey(r *http.Request) *AuthToken {
|
||||
// Get API key from request.
|
||||
key := r.Header.Get("Authorization")
|
||||
if key == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse API key.
|
||||
switch {
|
||||
case strings.HasPrefix(key, "Bearer "):
|
||||
key = strings.TrimPrefix(key, "Bearer ")
|
||||
case strings.HasPrefix(key, "Basic "):
|
||||
user, pass, _ := r.BasicAuth()
|
||||
key = user + pass
|
||||
default:
|
||||
log.Tracer(r.Context()).Tracef(
|
||||
"api: provided api key type %s is unsupported", strings.Split(key, " ")[0],
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
apiKeysLock.Lock()
|
||||
defer apiKeysLock.Unlock()
|
||||
|
||||
// Check if the provided API key exists.
|
||||
token, ok := apiKeys[key]
|
||||
if !ok {
|
||||
log.Tracer(r.Context()).Tracef(
|
||||
"api: provided api key %s... is unknown", key[:4],
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
func updateAPIKeys(_ context.Context, _ interface{}) error {
|
||||
apiKeysLock.Lock()
|
||||
defer apiKeysLock.Unlock()
|
||||
|
||||
log.Debug("api: importing possibly updated API keys from config")
|
||||
|
||||
// Delete current keys.
|
||||
for k := range apiKeys {
|
||||
delete(apiKeys, k)
|
||||
}
|
||||
|
||||
// Parse new keys.
|
||||
for _, key := range configuredAPIKeys() {
|
||||
u, err := url.Parse(key)
|
||||
if err != nil {
|
||||
log.Errorf("api: failed to parse configured API key %s: %s", key, err)
|
||||
continue
|
||||
}
|
||||
if u.Path == "" {
|
||||
log.Errorf("api: malformed API key %s: missing path section", key)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create token with default permissions.
|
||||
token := &AuthToken{
|
||||
Read: PermitAnyone,
|
||||
Write: PermitAnyone,
|
||||
}
|
||||
|
||||
// Update with configured permissions.
|
||||
q := u.Query()
|
||||
// Parse read permission.
|
||||
readPermission, err := parseAPIPermission(q.Get("read"))
|
||||
if err != nil {
|
||||
log.Errorf("api: invalid API key %s: %s", key, err)
|
||||
continue
|
||||
}
|
||||
token.Read = readPermission
|
||||
// Parse write permission.
|
||||
writePermission, err := parseAPIPermission(q.Get("write"))
|
||||
if err != nil {
|
||||
log.Errorf("api: invalid API key %s: %s", key, err)
|
||||
continue
|
||||
}
|
||||
token.Write = writePermission
|
||||
|
||||
// Save token.
|
||||
apiKeys[u.Path] = token
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkSessionCookie(r *http.Request) *AuthToken {
|
||||
// Get session cookie from request.
|
||||
c, err := r.Cookie(sessionCookieName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if session cookie is registered.
|
||||
sessionsLock.Lock()
|
||||
sess, ok := sessions[c.Value]
|
||||
sessionsLock.Unlock()
|
||||
if !ok {
|
||||
log.Tracer(r.Context()).Tracef("api: provided session cookie %s is unknown", c.Value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if session is still valid.
|
||||
if sess.Expired() {
|
||||
log.Tracer(r.Context()).Tracef("api: provided session cookie %s has expired", c.Value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Refresh session and return.
|
||||
sess.Refresh(sessionCookieTTL)
|
||||
log.Tracer(r.Context()).Tracef("api: session cookie %s is valid, refreshing", c.Value)
|
||||
return sess.token
|
||||
}
|
||||
|
||||
func createSession(w http.ResponseWriter, r *http.Request, token *AuthToken) error {
|
||||
// Generate new session key.
|
||||
secret, err := rng.Bytes(32) // 256 bit
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sessionKey := base64.RawURLEncoding.EncodeToString(secret)
|
||||
|
||||
// Set token cookie in response.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: sessionKey,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
// Create session.
|
||||
sess := &session{
|
||||
token: token,
|
||||
}
|
||||
sess.Refresh(sessionCookieTTL)
|
||||
|
||||
// Save session.
|
||||
sessionsLock.Lock()
|
||||
defer sessionsLock.Unlock()
|
||||
sessions[sessionKey] = sess
|
||||
log.Tracer(r.Context()).Debug("api: issued session cookie")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanSessions(_ context.Context, _ *modules.Task) error {
|
||||
sessionsLock.Lock()
|
||||
defer sessionsLock.Unlock()
|
||||
|
||||
for sessionKey, sess := range sessions {
|
||||
if sess.Expired() {
|
||||
delete(sessions, sessionKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deleteSession(sessionKey string) {
|
||||
sessionsLock.Lock()
|
||||
defer sessionsLock.Unlock()
|
||||
|
||||
delete(sessions, sessionKey)
|
||||
}
|
||||
|
||||
func isReadMethod(method string) bool {
|
||||
switch method {
|
||||
case http.MethodGet, http.MethodHead, http.MethodOptions:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseAPIPermission(s string) (Permission, error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "", "anyone":
|
||||
return PermitAnyone, nil
|
||||
case "user":
|
||||
return PermitUser, nil
|
||||
case "admin":
|
||||
return PermitAdmin, nil
|
||||
default:
|
||||
return PermitAnyone, fmt.Errorf("invalid permission: %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
func (p Permission) String() string {
|
||||
switch p {
|
||||
case NotSupported:
|
||||
return "NotSupported"
|
||||
case Dynamic:
|
||||
return "Dynamic"
|
||||
case PermitAnyone:
|
||||
return "PermitAnyone"
|
||||
case PermitUser:
|
||||
return "PermitUser"
|
||||
case PermitAdmin:
|
||||
return "PermitAdmin"
|
||||
case PermitSelf:
|
||||
return "PermitSelf"
|
||||
case NotFound:
|
||||
return "NotFound"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Role returns a string representation of the permission role.
|
||||
func (p Permission) Role() string {
|
||||
switch p {
|
||||
case PermitAnyone:
|
||||
return "Anyone"
|
||||
case PermitUser:
|
||||
return "User"
|
||||
case PermitAdmin:
|
||||
return "Admin"
|
||||
case PermitSelf:
|
||||
return "Self"
|
||||
default:
|
||||
return "Invalid"
|
||||
}
|
||||
}
|
||||
|
|
195
api/authentication_test.go
Normal file
195
api/authentication_test.go
Normal file
|
@ -0,0 +1,195 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testToken = new(AuthToken)
|
||||
)
|
||||
|
||||
func testAuthenticator(r *http.Request, s *http.Server) (*AuthToken, error) {
|
||||
switch {
|
||||
case testToken.Read == -127 || testToken.Write == -127:
|
||||
return nil, errors.New("test error")
|
||||
case testToken.Read == -128 || testToken.Write == -128:
|
||||
return nil, fmt.Errorf("%wdenied", ErrAPIAccessDeniedMessage)
|
||||
default:
|
||||
return testToken, nil
|
||||
}
|
||||
}
|
||||
|
||||
type testAuthHandler struct {
|
||||
Read Permission
|
||||
Write Permission
|
||||
}
|
||||
|
||||
func (ah *testAuthHandler) ReadPermission(r *http.Request) Permission {
|
||||
return ah.Read
|
||||
}
|
||||
|
||||
func (ah *testAuthHandler) WritePermission(r *http.Request) Permission {
|
||||
return ah.Write
|
||||
}
|
||||
|
||||
func (ah *testAuthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if request is as expected.
|
||||
ar := GetAPIRequest(r)
|
||||
switch {
|
||||
case ar == nil:
|
||||
http.Error(w, "ar == nil", http.StatusInternalServerError)
|
||||
case ar.AuthToken == nil:
|
||||
http.Error(w, "ar.AuthToken == nil", http.StatusInternalServerError)
|
||||
default:
|
||||
http.Error(w, "auth success", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func makeAuthTestPath(reading bool, p Permission) string {
|
||||
if reading {
|
||||
return fmt.Sprintf("/test/auth/read/%s", p)
|
||||
}
|
||||
return fmt.Sprintf("/test/auth/write/%s", p)
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Set test authenticator.
|
||||
err := SetAuthenticator(testAuthenticator)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissions(t *testing.T) { //nolint:gocognit
|
||||
testHandler := &mainHandler{
|
||||
mux: mainMux,
|
||||
}
|
||||
|
||||
// Define permissions that need testing.
|
||||
permissionsToTest := []Permission{
|
||||
NotSupported,
|
||||
PermitAnyone,
|
||||
PermitUser,
|
||||
PermitAdmin,
|
||||
PermitSelf,
|
||||
Dynamic,
|
||||
NotFound,
|
||||
100, // Test a too high value.
|
||||
-100, // Test a too low value.
|
||||
-127, // Simulate authenticator failure.
|
||||
-128, // Simulate authentication denied message.
|
||||
}
|
||||
|
||||
// Register test handlers.
|
||||
for _, p := range permissionsToTest {
|
||||
RegisterHandler(makeAuthTestPath(true, p), &testAuthHandler{Read: p})
|
||||
RegisterHandler(makeAuthTestPath(false, p), &testAuthHandler{Write: p})
|
||||
}
|
||||
|
||||
// Test all the combinations.
|
||||
for _, requestPerm := range permissionsToTest {
|
||||
for _, handlerPerm := range permissionsToTest {
|
||||
for _, method := range []string{
|
||||
http.MethodGet,
|
||||
http.MethodHead,
|
||||
http.MethodPost,
|
||||
http.MethodPut,
|
||||
} {
|
||||
|
||||
// Set request permission for test requests.
|
||||
reading := isReadMethod(method)
|
||||
if reading {
|
||||
testToken.Read = requestPerm
|
||||
testToken.Write = NotSupported
|
||||
} else {
|
||||
testToken.Read = NotSupported
|
||||
testToken.Write = requestPerm
|
||||
}
|
||||
|
||||
// Evaluate expected result.
|
||||
var expectSuccess bool
|
||||
switch {
|
||||
case handlerPerm == PermitAnyone:
|
||||
// This is fast-tracked. There are not additional checks.
|
||||
expectSuccess = true
|
||||
case handlerPerm == Dynamic:
|
||||
// This is turned into PermitAnyone in the authenticator.
|
||||
// But authentication is still processed and the result still gets
|
||||
// sanity checked!
|
||||
if requestPerm >= PermitAnyone &&
|
||||
requestPerm <= PermitSelf {
|
||||
expectSuccess = true
|
||||
}
|
||||
// Another special case is when the handler requires permission to be
|
||||
// processed but the authenticator fails to authenticate the request.
|
||||
// In this case, a fallback token with PermitAnyone is used.
|
||||
if requestPerm == -128 {
|
||||
// -128 is used to simulate a permission denied message.
|
||||
expectSuccess = true
|
||||
}
|
||||
case handlerPerm <= NotSupported:
|
||||
// Invalid handler permission.
|
||||
case handlerPerm > PermitSelf:
|
||||
// Invalid handler permission.
|
||||
case requestPerm <= NotSupported:
|
||||
// Invalid request permission.
|
||||
case requestPerm > PermitSelf:
|
||||
// Invalid request permission.
|
||||
case requestPerm < handlerPerm:
|
||||
// Valid, but insufficient request permission.
|
||||
default:
|
||||
expectSuccess = true
|
||||
}
|
||||
|
||||
if expectSuccess {
|
||||
|
||||
// Test for success.
|
||||
if !assert.HTTPBodyContains(
|
||||
t,
|
||||
testHandler.ServeHTTP,
|
||||
method,
|
||||
makeAuthTestPath(reading, handlerPerm),
|
||||
nil,
|
||||
"auth success",
|
||||
) {
|
||||
t.Errorf(
|
||||
"%s with %s (%d) to handler %s (%d)",
|
||||
method,
|
||||
requestPerm, requestPerm,
|
||||
handlerPerm, handlerPerm,
|
||||
)
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Test for error.
|
||||
if !assert.HTTPError(t,
|
||||
testHandler.ServeHTTP,
|
||||
method,
|
||||
makeAuthTestPath(reading, handlerPerm),
|
||||
nil,
|
||||
) {
|
||||
t.Errorf(
|
||||
"%s with %s (%d) to handler %s (%d)",
|
||||
method,
|
||||
requestPerm, requestPerm,
|
||||
handlerPerm, handlerPerm,
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionDefinitions(t *testing.T) {
|
||||
if NotSupported != 0 {
|
||||
t.Fatalf("NotSupported must be zero, was %v", NotSupported)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package client
|
||||
|
||||
// message types
|
||||
// Message Types.
|
||||
const (
|
||||
msgRequestGet = "get"
|
||||
msgRequestQuery = "query"
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
// Client errors
|
||||
// Client errors.
|
||||
var (
|
||||
ErrMalformedMessage = errors.New("malformed message")
|
||||
)
|
||||
|
|
|
@ -7,15 +7,20 @@ import (
|
|||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Config Keys
|
||||
// Config Keys.
|
||||
const (
|
||||
CfgDefaultListenAddressKey = "core/listenAddress"
|
||||
CfgAPIKeys = "core/apiKeys"
|
||||
)
|
||||
|
||||
var (
|
||||
listenAddressFlag string
|
||||
listenAddressConfig config.StringOption
|
||||
defaultListenAddress string
|
||||
|
||||
configuredAPIKeys config.StringArrayOption
|
||||
|
||||
devMode config.BoolOption
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -58,11 +63,30 @@ func registerConfig() error {
|
|||
}
|
||||
listenAddressConfig = config.GetAsString(CfgDefaultListenAddressKey, getDefaultListenAddress())
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "API Keys",
|
||||
Key: CfgAPIKeys,
|
||||
Description: "Define API keys for priviledged access to the API. Every entry is a separate API key with respective permissions. Format is `<key>?read=<perm>&write=<perm>`. Permissions are `anyone`, `user` and `admin`, and may be omitted.",
|
||||
OptType: config.OptTypeStringArray,
|
||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||
ReleaseLevel: config.ReleaseLevelStable,
|
||||
DefaultValue: []string{},
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: 514,
|
||||
config.CategoryAnnotation: "Development",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
configuredAPIKeys = config.GetAsStringArray(CfgAPIKeys, []string{})
|
||||
|
||||
devMode = config.Concurrent.GetAsBool(config.CfgDevModeKey, false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDefaultAPIListenAddress sets the default listen address for the API.
|
||||
func SetDefaultAPIListenAddress(address string) {
|
||||
|
||||
defaultListenAddress = address
|
||||
}
|
||||
|
|
|
@ -42,7 +42,13 @@ var (
|
|||
)
|
||||
|
||||
func init() {
|
||||
RegisterHandleFunc("/api/database/v1", startDatabaseAPI) // net/http pattern matching only this exact path
|
||||
RegisterHandler("/api/database/v1", WrapInAuthHandler(
|
||||
startDatabaseAPI,
|
||||
// Default to admin read/write permissions until the database gets support
|
||||
// for api permissions.
|
||||
PermitAdmin,
|
||||
PermitAdmin,
|
||||
))
|
||||
}
|
||||
|
||||
// DatabaseAPI is a database API instance.
|
||||
|
@ -93,7 +99,7 @@ func startDatabaseAPI(w http.ResponseWriter, r *http.Request) {
|
|||
go new.handler()
|
||||
go new.writer()
|
||||
|
||||
log.Infof("api request: init websocket %s %s", r.RemoteAddr, r.RequestURI)
|
||||
log.Tracer(r.Context()).Infof("api request: init websocket %s %s", r.RemoteAddr, r.RequestURI)
|
||||
}
|
||||
|
||||
func (api *DatabaseAPI) handler() {
|
||||
|
@ -278,7 +284,7 @@ func (api *DatabaseAPI) handleGet(opID []byte, key string) {
|
|||
|
||||
r, err := api.db.Get(key)
|
||||
if err == nil {
|
||||
data, err = marshalRecord(r)
|
||||
data, err = marshalRecord(r, true)
|
||||
}
|
||||
if err != nil {
|
||||
api.send(opID, dbMsgTypeError, err.Error(), nil)
|
||||
|
@ -336,7 +342,7 @@ func (api *DatabaseAPI) processQuery(opID []byte, q *query.Query) (ok bool) {
|
|||
// process query feed
|
||||
if r != nil {
|
||||
// process record
|
||||
data, err := marshalRecord(r)
|
||||
data, err := marshalRecord(r, true)
|
||||
if err != nil {
|
||||
api.send(opID, dbMsgTypeWarning, err.Error(), nil)
|
||||
}
|
||||
|
@ -412,7 +418,7 @@ func (api *DatabaseAPI) processSub(opID []byte, sub *database.Subscription) {
|
|||
// process sub feed
|
||||
if r != nil {
|
||||
// process record
|
||||
data, err := marshalRecord(r)
|
||||
data, err := marshalRecord(r, true)
|
||||
if err != nil {
|
||||
api.send(opID, dbMsgTypeWarning, err.Error(), nil)
|
||||
continue
|
||||
|
@ -625,7 +631,7 @@ func (api *DatabaseAPI) shutdown() {
|
|||
|
||||
// marsharlRecords locks and marshals the given record, additionally adding
|
||||
// metadata and returning it as json.
|
||||
func marshalRecord(r record.Record) ([]byte, error) {
|
||||
func marshalRecord(r record.Record, withDSDIdentifier bool) ([]byte, error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
|
@ -651,10 +657,12 @@ func marshalRecord(r record.Record) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Add JSON identifier again.
|
||||
formatID := varint.Pack8(record.JSON)
|
||||
finalData := make([]byte, 0, len(formatID)+len(jsonData))
|
||||
finalData = append(finalData, formatID...)
|
||||
finalData = append(finalData, jsonData...)
|
||||
|
||||
return finalData, nil
|
||||
if withDSDIdentifier {
|
||||
formatID := varint.Pack8(record.JSON)
|
||||
finalData := make([]byte, 0, len(formatID)+len(jsonData))
|
||||
finalData = append(finalData, formatID...)
|
||||
finalData = append(finalData, jsonData...)
|
||||
return finalData, nil
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
|
10
api/doc.go
Normal file
10
api/doc.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
Package api provides an API for integration with other components of the same software package and also third party components.
|
||||
|
||||
It provides direct database access as well as a simpler way to register API endpoints. You can of course also register raw `http.Handler`s directly.
|
||||
|
||||
Optional authentication guards registered handlers. This is achieved by attaching functions to the `http.Handler`s that are registered, which allow them to specify the required permissions for the handler.
|
||||
|
||||
The permissions are divided into the roles and assume a single user per host. The Roles are User, Admin and Self. User roles are expected to have mostly read access and react to notifications or system events, like a system tray program. The Admin role is meant for advanced components that also change settings, but are restricted so they cannot break the software. Self is reserved for internal use with full access.
|
||||
*/
|
||||
package api
|
335
api/endpoints.go
Normal file
335
api/endpoints.go
Normal file
|
@ -0,0 +1,335 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Endpoint describes an API Endpoint.
|
||||
// Path and at least one permission are required.
|
||||
// As is exactly one function.
|
||||
type Endpoint struct {
|
||||
Path string
|
||||
MimeType string
|
||||
Read Permission
|
||||
Write Permission
|
||||
|
||||
// ActionFunc is for simple actions with a return message for the user.
|
||||
ActionFunc ActionFunc `json:"-"`
|
||||
|
||||
// DataFunc is for returning raw data that the caller for further processing.
|
||||
DataFunc DataFunc `json:"-"`
|
||||
|
||||
// StructFunc is for returning any kind of struct.
|
||||
StructFunc StructFunc `json:"-"`
|
||||
|
||||
// RecordFunc is for returning a database record. It will be properly locked
|
||||
// and marshalled including metadata.
|
||||
RecordFunc RecordFunc `json:"-"`
|
||||
|
||||
// HandlerFunc is the raw http handler.
|
||||
HandlerFunc http.HandlerFunc `json:"-"`
|
||||
|
||||
// Documentation Metadata.
|
||||
|
||||
Name string
|
||||
Description string
|
||||
Parameters []Parameter
|
||||
}
|
||||
|
||||
// Parameter describes a parameterized variation of an endpoint.
|
||||
type Parameter struct {
|
||||
Method string
|
||||
Field string
|
||||
Value string
|
||||
Description string
|
||||
}
|
||||
|
||||
type (
|
||||
// ActionFunc is for simple actions with a return message for the user.
|
||||
ActionFunc func(ar *Request) (msg string, err error)
|
||||
|
||||
// DataFunc is for returning raw data that the caller for further processing.
|
||||
DataFunc func(ar *Request) (data []byte, err error)
|
||||
|
||||
// StructFunc is for returning any kind of struct.
|
||||
StructFunc func(ar *Request) (i interface{}, err error)
|
||||
|
||||
// RecordFunc is for returning a database record. It will be properly locked
|
||||
// and marshalled including metadata.
|
||||
RecordFunc func(ar *Request) (r record.Record, err error)
|
||||
)
|
||||
|
||||
// MIME Types.
|
||||
const (
|
||||
MimeTypeJSON string = "application/json"
|
||||
MimeTypeText string = "text/plain"
|
||||
|
||||
apiV1Path = "/api/v1/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterHandler(apiV1Path+"{endpointPath:.+}", &endpointHandler{})
|
||||
}
|
||||
|
||||
var (
|
||||
endpoints = make(map[string]*Endpoint)
|
||||
endpointsLock sync.RWMutex
|
||||
|
||||
// ErrInvalidEndpoint is returned when an invalid endpoint is registered.
|
||||
ErrInvalidEndpoint = errors.New("endpoint is invalid")
|
||||
|
||||
// ErrAlreadyRegistered is returned when there already is an endpoint with
|
||||
// the same path registered.
|
||||
ErrAlreadyRegistered = errors.New("an endpoint for this path is already registered")
|
||||
)
|
||||
|
||||
func getAPIContext(r *http.Request) (apiEndpoint *Endpoint, apiRequest *Request) {
|
||||
// Get request context and check if we already have an action cached.
|
||||
apiRequest = GetAPIRequest(r)
|
||||
if apiRequest == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var ok bool
|
||||
apiEndpoint, ok = apiRequest.HandlerCache.(*Endpoint)
|
||||
if ok {
|
||||
return apiEndpoint, apiRequest
|
||||
}
|
||||
|
||||
// If not, get the action from the registry.
|
||||
endpointPath, ok := apiRequest.URLVars["endpointPath"]
|
||||
if !ok {
|
||||
return nil, apiRequest
|
||||
}
|
||||
|
||||
endpointsLock.RLock()
|
||||
defer endpointsLock.RUnlock()
|
||||
|
||||
apiEndpoint, ok = endpoints[endpointPath]
|
||||
if ok {
|
||||
// Cache for next operation.
|
||||
apiRequest.HandlerCache = apiEndpoint
|
||||
}
|
||||
return apiEndpoint, apiRequest
|
||||
}
|
||||
|
||||
// RegisterEndpoint registers a new endpoint. An error will be returned if it
|
||||
// does not pass the sanity checks.
|
||||
func RegisterEndpoint(e Endpoint) error {
|
||||
if err := e.check(); err != nil {
|
||||
return fmt.Errorf("%w: %s", ErrInvalidEndpoint, err)
|
||||
}
|
||||
|
||||
endpointsLock.Lock()
|
||||
defer endpointsLock.Unlock()
|
||||
|
||||
_, ok := endpoints[e.Path]
|
||||
if ok {
|
||||
return ErrAlreadyRegistered
|
||||
}
|
||||
|
||||
endpoints[e.Path] = &e
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Endpoint) check() error {
|
||||
// Check path.
|
||||
if strings.TrimSpace(e.Path) == "" {
|
||||
return errors.New("path is missing")
|
||||
}
|
||||
|
||||
// Check permissions.
|
||||
if e.Read < Dynamic || e.Read > PermitSelf {
|
||||
return errors.New("invalid read permission")
|
||||
}
|
||||
if e.Write < Dynamic || e.Write > PermitSelf {
|
||||
return errors.New("invalid write permission")
|
||||
}
|
||||
|
||||
// Check functions.
|
||||
var defaultMimeType string
|
||||
fnCnt := 0
|
||||
if e.ActionFunc != nil {
|
||||
fnCnt++
|
||||
defaultMimeType = MimeTypeText
|
||||
}
|
||||
if e.DataFunc != nil {
|
||||
fnCnt++
|
||||
defaultMimeType = MimeTypeText
|
||||
}
|
||||
if e.StructFunc != nil {
|
||||
fnCnt++
|
||||
defaultMimeType = MimeTypeJSON
|
||||
}
|
||||
if e.RecordFunc != nil {
|
||||
fnCnt++
|
||||
defaultMimeType = MimeTypeJSON
|
||||
}
|
||||
if e.HandlerFunc != nil {
|
||||
fnCnt++
|
||||
defaultMimeType = MimeTypeText
|
||||
}
|
||||
if fnCnt != 1 {
|
||||
return errors.New("only one function may be set")
|
||||
}
|
||||
|
||||
// Set default mime type.
|
||||
if e.MimeType == "" {
|
||||
e.MimeType = defaultMimeType
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportEndpoints exports the registered endpoints. The returned data must be
|
||||
// treated as immutable.
|
||||
func ExportEndpoints() []*Endpoint {
|
||||
endpointsLock.RLock()
|
||||
defer endpointsLock.RUnlock()
|
||||
|
||||
// Copy the map into a slice.
|
||||
eps := make([]*Endpoint, 0, len(endpoints))
|
||||
for _, ep := range endpoints {
|
||||
eps = append(eps, ep)
|
||||
}
|
||||
|
||||
sort.Sort(sortByPath(eps))
|
||||
return eps
|
||||
}
|
||||
|
||||
type sortByPath []*Endpoint
|
||||
|
||||
func (eps sortByPath) Len() int { return len(eps) }
|
||||
func (eps sortByPath) Less(i, j int) bool { return eps[i].Path < eps[j].Path }
|
||||
func (eps sortByPath) Swap(i, j int) { eps[i], eps[j] = eps[j], eps[i] }
|
||||
|
||||
type endpointHandler struct{}
|
||||
|
||||
var _ AuthenticatedHandler = &endpointHandler{} // Compile time interface check.
|
||||
|
||||
// ReadPermission returns the read permission for the handler.
|
||||
func (eh *endpointHandler) ReadPermission(r *http.Request) Permission {
|
||||
apiEndpoint, _ := getAPIContext(r)
|
||||
if apiEndpoint != nil {
|
||||
return apiEndpoint.Read
|
||||
}
|
||||
return NotFound
|
||||
}
|
||||
|
||||
// WritePermission returns the write permission for the handler.
|
||||
func (eh *endpointHandler) WritePermission(r *http.Request) Permission {
|
||||
apiEndpoint, _ := getAPIContext(r)
|
||||
if apiEndpoint != nil {
|
||||
return apiEndpoint.Write
|
||||
}
|
||||
return NotFound
|
||||
}
|
||||
|
||||
// ServeHTTP handles the http request.
|
||||
func (eh *endpointHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
apiEndpoint, apiRequest := getAPIContext(r)
|
||||
if apiEndpoint == nil || apiRequest == nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodHead:
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
case http.MethodPost, http.MethodPut:
|
||||
// Read body data.
|
||||
inputData, ok := readBody(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
apiRequest.InputData = inputData
|
||||
case http.MethodGet:
|
||||
// Nothing special to do here.
|
||||
case http.MethodOptions:
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
default:
|
||||
http.Error(w, "Unsupported method for the actions API.", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute action function and get response data
|
||||
var responseData []byte
|
||||
var err error
|
||||
|
||||
switch {
|
||||
case apiEndpoint.ActionFunc != nil:
|
||||
var msg string
|
||||
msg, err = apiEndpoint.ActionFunc(apiRequest)
|
||||
if err == nil {
|
||||
responseData = []byte(msg)
|
||||
}
|
||||
|
||||
case apiEndpoint.DataFunc != nil:
|
||||
responseData, err = apiEndpoint.DataFunc(apiRequest)
|
||||
|
||||
case apiEndpoint.StructFunc != nil:
|
||||
var v interface{}
|
||||
v, err = apiEndpoint.StructFunc(apiRequest)
|
||||
if err == nil && v != nil {
|
||||
responseData, err = json.Marshal(v)
|
||||
}
|
||||
|
||||
case apiEndpoint.RecordFunc != nil:
|
||||
var rec record.Record
|
||||
rec, err = apiEndpoint.RecordFunc(apiRequest)
|
||||
if err == nil && r != nil {
|
||||
responseData, err = marshalRecord(rec, false)
|
||||
}
|
||||
|
||||
case apiEndpoint.HandlerFunc != nil:
|
||||
apiEndpoint.HandlerFunc(w, r)
|
||||
return
|
||||
|
||||
default:
|
||||
http.Error(w, "Internal server error: Missing handler.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for handler error.
|
||||
if err != nil {
|
||||
http.Error(w, "Internal server error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write response.
|
||||
w.Header().Set("Content-Type", apiEndpoint.MimeType+"; charset=utf-8")
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(responseData)))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err = w.Write(responseData)
|
||||
if err != nil {
|
||||
log.Tracer(r.Context()).Warningf("api: failed to write response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func readBody(w http.ResponseWriter, r *http.Request) (inputData []byte, ok bool) {
|
||||
// Check for too long content in order to prevent death.
|
||||
if r.ContentLength > 20000000 { // 20MB
|
||||
http.Error(w, "Too much input data.", http.StatusRequestEntityTooLarge)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Read and close body.
|
||||
inputData, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read body: "+err.Error(), http.StatusInternalServerError)
|
||||
return nil, false
|
||||
}
|
||||
return inputData, true
|
||||
}
|
24
api/endpoints_config.go
Normal file
24
api/endpoints_config.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/config"
|
||||
)
|
||||
|
||||
func registerConfigEndpoints() error {
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "config/options",
|
||||
Read: PermitAnyone,
|
||||
MimeType: MimeTypeJSON,
|
||||
StructFunc: listConfig,
|
||||
Name: "Export Configuration Options",
|
||||
Description: "Returns a list of all registered configuration options and their metadata. This does not include the current active or default settings.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listConfig(ar *Request) (i interface{}, err error) {
|
||||
return config.ExportOptions(), nil
|
||||
}
|
108
api/endpoints_debug.go
Normal file
108
api/endpoints_debug.go
Normal file
|
@ -0,0 +1,108 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
|
||||
"github.com/safing/portbase/utils/debug"
|
||||
)
|
||||
|
||||
func registerDebugEndpoints() error {
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "ping",
|
||||
Read: PermitAnyone,
|
||||
ActionFunc: ping,
|
||||
Name: "Ping",
|
||||
Description: "Pong.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "debug/stack",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: getStack,
|
||||
Name: "Get Goroutine Stack",
|
||||
Description: "Returns the current goroutine stack.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "debug/stack/print",
|
||||
Read: PermitAnyone,
|
||||
ActionFunc: printStack,
|
||||
Name: "Print Goroutine Stack",
|
||||
Description: "Prints the current goroutine stack to stdout.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "debug/info",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: debugInfo,
|
||||
Name: "Get Debug Information",
|
||||
Description: "Returns debugging information, including the version and platform info, errors, logs and the current goroutine stack.",
|
||||
Parameters: []Parameter{{
|
||||
Method: http.MethodGet,
|
||||
Field: "style",
|
||||
Value: "github",
|
||||
Description: "Specify the formatting style. The default is simple markdown formatting.",
|
||||
}},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ping responds with pong.
|
||||
func ping(ar *Request) (msg string, err error) {
|
||||
return "Pong.", nil
|
||||
}
|
||||
|
||||
// getStack returns the current goroutine stack.
|
||||
func getStack(_ *Request) (data []byte, err error) {
|
||||
buf := &bytes.Buffer{}
|
||||
err = pprof.Lookup("goroutine").WriteTo(buf, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// printStack prints the current goroutine stack to stderr.
|
||||
func printStack(_ *Request) (msg string, err error) {
|
||||
_, err = fmt.Fprint(os.Stderr, "===== PRINTING STACK =====\n")
|
||||
if err == nil {
|
||||
err = pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
|
||||
}
|
||||
if err == nil {
|
||||
_, err = fmt.Fprint(os.Stderr, "===== END OF STACK =====\n")
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "stack printed to stdout", nil
|
||||
}
|
||||
|
||||
// debugInfo returns the debugging information for support requests.
|
||||
func debugInfo(ar *Request) (data []byte, err error) {
|
||||
// Create debug information helper.
|
||||
di := new(debug.Info)
|
||||
di.Style = ar.Request.URL.Query().Get("style")
|
||||
|
||||
// Add debug information.
|
||||
di.AddVersionInfo()
|
||||
di.AddPlatformInfo(ar.Context())
|
||||
di.AddLastReportedModuleError()
|
||||
di.AddLastUnexpectedLogs()
|
||||
di.AddGoroutineStack()
|
||||
|
||||
// Return data.
|
||||
return di.Bytes(), nil
|
||||
}
|
134
api/endpoints_meta.go
Normal file
134
api/endpoints_meta.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func registerMetaEndpoints() error {
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "endpoints",
|
||||
Read: PermitAnyone,
|
||||
MimeType: MimeTypeJSON,
|
||||
DataFunc: listEndpoints,
|
||||
Name: "Export API Endpoints",
|
||||
Description: "Returns a list of all registered endpoints and their metadata.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "auth/permissions",
|
||||
Read: Dynamic,
|
||||
StructFunc: permissions,
|
||||
Name: "View Current Permissions",
|
||||
Description: "Returns the current permissions assigned to the request.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "auth/bearer",
|
||||
Read: Dynamic,
|
||||
HandlerFunc: authBearer,
|
||||
Name: "Request HTTP Bearer Auth",
|
||||
Description: "Returns an HTTP Bearer Auth request, if not authenticated.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "auth/basic",
|
||||
Read: Dynamic,
|
||||
HandlerFunc: authBasic,
|
||||
Name: "Request HTTP Basic Auth",
|
||||
Description: "Returns an HTTP Basic Auth request, if not authenticated.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := RegisterEndpoint(Endpoint{
|
||||
Path: "auth/reset",
|
||||
Read: PermitAnyone,
|
||||
HandlerFunc: authReset,
|
||||
Name: "Reset Authenticated Session",
|
||||
Description: "Resets authentication status internally and in the browser.",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func listEndpoints(ar *Request) (data []byte, err error) {
|
||||
data, err = json.Marshal(ExportEndpoints())
|
||||
return
|
||||
}
|
||||
|
||||
func permissions(ar *Request) (i interface{}, err error) {
|
||||
if ar.AuthToken == nil {
|
||||
return nil, errors.New("authentication token missing")
|
||||
}
|
||||
|
||||
return struct {
|
||||
Read Permission
|
||||
Write Permission
|
||||
ReadRole string
|
||||
WriteRole string
|
||||
}{
|
||||
Read: ar.AuthToken.Read,
|
||||
Write: ar.AuthToken.Write,
|
||||
ReadRole: ar.AuthToken.Read.Role(),
|
||||
WriteRole: ar.AuthToken.Write.Role(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func authBearer(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if authenticated by checking read permission.
|
||||
ar := GetAPIRequest(r)
|
||||
if ar.AuthToken.Read != PermitAnyone {
|
||||
TextResponse(w, r, "Authenticated.")
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with desired authentication header.
|
||||
w.Header().Set("WWW-Authenticate", "Bearer realm=Portmaster API")
|
||||
http.Error(w, "Authorization required.", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func authBasic(w http.ResponseWriter, r *http.Request) {
|
||||
// Check if authenticated by checking read permission.
|
||||
ar := GetAPIRequest(r)
|
||||
if ar.AuthToken.Read != PermitAnyone {
|
||||
TextResponse(w, r, "Authenticated.")
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with desired authentication header.
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=Portmaster API")
|
||||
http.Error(w, "Authorization required.", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
func authReset(w http.ResponseWriter, r *http.Request) {
|
||||
// Get session cookie from request and delete session if exists.
|
||||
c, err := r.Cookie(sessionCookieName)
|
||||
if err == nil {
|
||||
deleteSession(c.Value)
|
||||
}
|
||||
|
||||
// Delete session and cookie.
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
MaxAge: -1, // MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
|
||||
})
|
||||
|
||||
// Request client to also reset all data.
|
||||
w.Header().Set("Clear-Site-Data", "*")
|
||||
|
||||
// Set HTTP Auth Realm without requesting authorization.
|
||||
w.Header().Set("WWW-Authenticate", "None realm=Portmaster API")
|
||||
|
||||
// Reply with 401 Unauthorized in order to clear HTTP Basic Auth data.
|
||||
http.Error(w, "Session deleted.", http.StatusUnauthorized)
|
||||
}
|
156
api/endpoints_test.go
Normal file
156
api/endpoints_test.go
Normal file
|
@ -0,0 +1,156 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
successMsg = "endpoint api success"
|
||||
failedMsg = "endpoint api failed"
|
||||
)
|
||||
|
||||
type actionTestRecord struct {
|
||||
record.Base
|
||||
sync.Mutex
|
||||
Msg string
|
||||
}
|
||||
|
||||
func TestEndpoints(t *testing.T) {
|
||||
testHandler := &mainHandler{
|
||||
mux: mainMux,
|
||||
}
|
||||
|
||||
// ActionFn
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/action",
|
||||
Read: PermitAnyone,
|
||||
ActionFunc: func(_ *Request) (msg string, err error) {
|
||||
return successMsg, nil
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/action", nil, successMsg)
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/action-err",
|
||||
Read: PermitAnyone,
|
||||
ActionFunc: func(_ *Request) (msg string, err error) {
|
||||
return "", errors.New(failedMsg)
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/action-err", nil, failedMsg)
|
||||
|
||||
// DataFn
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/data",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: func(_ *Request) (data []byte, err error) {
|
||||
return []byte(successMsg), nil
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/data", nil, successMsg)
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/data-err",
|
||||
Read: PermitAnyone,
|
||||
DataFunc: func(_ *Request) (data []byte, err error) {
|
||||
return nil, errors.New(failedMsg)
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/data-err", nil, failedMsg)
|
||||
|
||||
// StructFn
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/struct",
|
||||
Read: PermitAnyone,
|
||||
StructFunc: func(_ *Request) (i interface{}, err error) {
|
||||
return &actionTestRecord{
|
||||
Msg: successMsg,
|
||||
}, nil
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/struct", nil, successMsg)
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/struct-err",
|
||||
Read: PermitAnyone,
|
||||
StructFunc: func(_ *Request) (i interface{}, err error) {
|
||||
return nil, errors.New(failedMsg)
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/struct-err", nil, failedMsg)
|
||||
|
||||
// RecordFn
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/record",
|
||||
Read: PermitAnyone,
|
||||
RecordFunc: func(_ *Request) (r record.Record, err error) {
|
||||
r = &actionTestRecord{
|
||||
Msg: successMsg,
|
||||
}
|
||||
r.CreateMeta()
|
||||
return r, nil
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/record", nil, successMsg)
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/record-err",
|
||||
Read: PermitAnyone,
|
||||
RecordFunc: func(_ *Request) (r record.Record, err error) {
|
||||
return nil, errors.New(failedMsg)
|
||||
},
|
||||
}))
|
||||
assert.HTTPBodyContains(t, testHandler.ServeHTTP, "GET", apiV1Path+"test/record-err", nil, failedMsg)
|
||||
}
|
||||
|
||||
func TestActionRegistration(t *testing.T) {
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{}))
|
||||
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
Read: NotFound,
|
||||
}))
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
Read: PermitSelf + 1,
|
||||
}))
|
||||
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
Write: NotFound,
|
||||
}))
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
Write: PermitSelf + 1,
|
||||
}))
|
||||
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
}))
|
||||
|
||||
assert.Error(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
ActionFunc: func(_ *Request) (msg string, err error) {
|
||||
return successMsg, nil
|
||||
},
|
||||
DataFunc: func(_ *Request) (data []byte, err error) {
|
||||
return []byte(successMsg), nil
|
||||
},
|
||||
}))
|
||||
|
||||
assert.NoError(t, RegisterEndpoint(Endpoint{
|
||||
Path: "test/err",
|
||||
ActionFunc: func(_ *Request) (msg string, err error) {
|
||||
return successMsg, nil
|
||||
},
|
||||
}))
|
||||
}
|
50
api/main.go
50
api/main.go
|
@ -2,7 +2,10 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
|
@ -10,9 +13,11 @@ import (
|
|||
|
||||
var (
|
||||
module *modules.Module
|
||||
|
||||
exportEndpoints bool
|
||||
)
|
||||
|
||||
// API Errors
|
||||
// API Errors.
|
||||
var (
|
||||
ErrAuthenticationAlreadySet = errors.New("the authentication function has already been set")
|
||||
ErrAuthenticationImmutable = errors.New("the authentication function can only be set before the api has started")
|
||||
|
@ -20,24 +25,47 @@ var (
|
|||
|
||||
func init() {
|
||||
module = modules.Register("api", prep, start, stop, "database", "config")
|
||||
|
||||
flag.BoolVar(&exportEndpoints, "export-api-endpoints", false, "export api endpoint registry and exit")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
if exportEndpoints {
|
||||
modules.SetCmdLineOperation(exportEndpointsCmd)
|
||||
}
|
||||
|
||||
if getDefaultListenAddress() == "" {
|
||||
return errors.New("no default listen address for api available")
|
||||
}
|
||||
return registerConfig()
|
||||
|
||||
if err := registerConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := registerDebugEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := registerConfigEndpoints(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return registerMetaEndpoints()
|
||||
}
|
||||
|
||||
func start() error {
|
||||
logFlagOverrides()
|
||||
go Serve()
|
||||
|
||||
_ = updateAPIKeys(module.Ctx, nil)
|
||||
err := module.RegisterEventHook("config", "config change", "update API keys", updateAPIKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// start api auth token cleaner
|
||||
authFnLock.Lock()
|
||||
defer authFnLock.Unlock()
|
||||
if authFn != nil {
|
||||
module.NewTask("clean api auth tokens", cleanAuthTokens).Repeat(time.Minute)
|
||||
if authFnSet.IsSet() {
|
||||
module.NewTask("clean api sessions", cleanSessions).Repeat(5 * time.Minute)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -49,3 +77,13 @@ func stop() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportEndpointsCmd() error {
|
||||
data, err := json.MarshalIndent(ExportEndpoints(), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
|
58
api/main_test.go
Normal file
58
api/main_test.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portbase/dataroot"
|
||||
"github.com/safing/portbase/modules"
|
||||
|
||||
// API depends on the database for the database api.
|
||||
_ "github.com/safing/portbase/database/dbmodule"
|
||||
)
|
||||
|
||||
func init() {
|
||||
defaultListenAddress = "127.0.0.1:8817"
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// enable module for testing
|
||||
module.Enable()
|
||||
|
||||
// tmp dir for data root (db & config)
|
||||
tmpDir, err := ioutil.TempDir("", "portbase-testing-")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create tmp dir: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// initialize data dir
|
||||
err = dataroot.Initialize(tmpDir, 0755)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to initialize data root: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// start modules
|
||||
var exitCode int
|
||||
err = modules.Start()
|
||||
if err != nil {
|
||||
// starting failed
|
||||
fmt.Fprintf(os.Stderr, "failed to setup test: %s\n", err)
|
||||
exitCode = 1
|
||||
} else {
|
||||
// run tests
|
||||
exitCode = m.Run()
|
||||
}
|
||||
|
||||
// shutdown
|
||||
_ = modules.Shutdown()
|
||||
if modules.GetExitStatusCode() != 0 {
|
||||
exitCode = modules.GetExitStatusCode()
|
||||
fmt.Fprintf(os.Stderr, "failed to cleanly shutdown test: %s\n", err)
|
||||
}
|
||||
// clean up and exit
|
||||
os.RemoveAll(tmpDir)
|
||||
os.Exit(exitCode)
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Middleware is a function that can be added as a middleware to the API endpoint.
|
||||
type Middleware func(next http.Handler) http.Handler
|
||||
|
||||
type mwHandler struct {
|
||||
handlers []Middleware
|
||||
final http.Handler
|
||||
}
|
||||
|
||||
func (mwh *mwHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
handlerLock.RLock()
|
||||
defer handlerLock.RUnlock()
|
||||
|
||||
// final handler
|
||||
handler := mwh.final
|
||||
|
||||
// build middleware chain
|
||||
// loop in reverse to build the handler chain in the correct order
|
||||
for i := len(mwh.handlers) - 1; i >= 0; i-- {
|
||||
handler = mwh.handlers[i](handler)
|
||||
}
|
||||
|
||||
// start
|
||||
handler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// ModuleWorker is an http middleware that wraps the request in a module worker.
|
||||
func ModuleWorker(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_ = module.RunWorker("http request", func(_ context.Context) error {
|
||||
next.ServeHTTP(w, r)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// LogTracer is an http middleware that attaches a log tracer to the request context.
|
||||
func LogTracer(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, tracer := log.AddTracer(r.Context())
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
tracer.Submit()
|
||||
})
|
||||
}
|
57
api/request.go
Normal file
57
api/request.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Request is a support struct to pool more request related information.
|
||||
type Request struct {
|
||||
// Request is the http request.
|
||||
*http.Request
|
||||
|
||||
// InputData contains the request body for write operations.
|
||||
InputData []byte
|
||||
|
||||
// Route of this request.
|
||||
Route *mux.Route
|
||||
|
||||
// URLVars contains the URL variables extracted by the gorilla mux.
|
||||
URLVars map[string]string
|
||||
|
||||
// AuthToken is the request-side authentication token assigned.
|
||||
AuthToken *AuthToken
|
||||
|
||||
// HandlerCache can be used by handlers to cache data between handlers within a request.
|
||||
HandlerCache interface{}
|
||||
}
|
||||
|
||||
// apiRequestContextKey is a key used for the context key/value storage.
|
||||
type apiRequestContextKey struct{}
|
||||
|
||||
var (
|
||||
requestContextKey = apiRequestContextKey{}
|
||||
)
|
||||
|
||||
// GetAPIRequest returns the API Request of the given http request.
|
||||
func GetAPIRequest(r *http.Request) *Request {
|
||||
ar, ok := r.Context().Value(requestContextKey).(*Request)
|
||||
if ok {
|
||||
return ar
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TextResponse writes a text response.
|
||||
func TextResponse(w http.ResponseWriter, r *http.Request, text string) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, err := fmt.Fprintln(w, text)
|
||||
if err != nil {
|
||||
log.Tracer(r.Context()).Warningf("api: failed to write text response: %s", err)
|
||||
}
|
||||
}
|
109
api/router.go
109
api/router.go
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
@ -15,17 +16,6 @@ var (
|
|||
// gorilla mux
|
||||
mainMux = mux.NewRouter()
|
||||
|
||||
// middlewares
|
||||
middlewareHandler = &mwHandler{
|
||||
final: mainMux,
|
||||
handlers: []Middleware{
|
||||
ModuleWorker,
|
||||
LogTracer,
|
||||
RequestLogger,
|
||||
authMiddleware,
|
||||
},
|
||||
}
|
||||
|
||||
// main server and lock
|
||||
server = &http.Server{}
|
||||
handlerLock sync.RWMutex
|
||||
|
@ -45,18 +35,14 @@ func RegisterHandleFunc(path string, handleFunc func(http.ResponseWriter, *http.
|
|||
return mainMux.HandleFunc(path, handleFunc)
|
||||
}
|
||||
|
||||
// RegisterMiddleware registers a middle function with the API endoint.
|
||||
func RegisterMiddleware(middleware Middleware) {
|
||||
handlerLock.Lock()
|
||||
defer handlerLock.Unlock()
|
||||
middlewareHandler.handlers = append(middlewareHandler.handlers, middleware)
|
||||
}
|
||||
|
||||
// Serve starts serving the API endpoint.
|
||||
func Serve() {
|
||||
// configure server
|
||||
server.Addr = listenAddressConfig()
|
||||
server.Handler = middlewareHandler
|
||||
server.Handler = &mainHandler{
|
||||
// TODO: mainMux should not be modified anymore.
|
||||
mux: mainMux,
|
||||
}
|
||||
|
||||
// start serving
|
||||
log.Infof("api: starting to listen on %s", server.Addr)
|
||||
|
@ -76,7 +62,86 @@ func Serve() {
|
|||
}
|
||||
}
|
||||
|
||||
// GetMuxVars wraps github.com/gorilla/mux.Vars in order to mitigate context key issues in multi-repo projects.
|
||||
func GetMuxVars(r *http.Request) map[string]string {
|
||||
return mux.Vars(r)
|
||||
type mainHandler struct {
|
||||
mux *mux.Router
|
||||
}
|
||||
|
||||
func (mh *mainHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
_ = module.RunWorker("http request", func(_ context.Context) error {
|
||||
return mh.handle(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (mh *mainHandler) handle(w http.ResponseWriter, r *http.Request) error {
|
||||
// Setup context trace logging.
|
||||
ctx, tracer := log.AddTracer(r.Context())
|
||||
// Add request context.
|
||||
apiRequest := &Request{
|
||||
Request: r,
|
||||
}
|
||||
ctx = context.WithValue(ctx, requestContextKey, apiRequest)
|
||||
// Add context back to request.
|
||||
r = r.WithContext(ctx)
|
||||
lrw := NewLoggingResponseWriter(w, r)
|
||||
|
||||
tracer.Tracef("api request: %s ___ %s", r.RemoteAddr, r.RequestURI)
|
||||
defer func() {
|
||||
// Log request status.
|
||||
if lrw.Status != 0 {
|
||||
// If lrw.Status is 0, the request may have been hijacked.
|
||||
tracer.Debugf("api request: %s %d %s", lrw.Request.RemoteAddr, lrw.Status, lrw.Request.RequestURI)
|
||||
}
|
||||
tracer.Submit()
|
||||
}()
|
||||
|
||||
// Get handler for request.
|
||||
// Gorilla does not support handling this on our own very well.
|
||||
// See github.com/gorilla/mux.ServeHTTP for reference.
|
||||
var match mux.RouteMatch
|
||||
var handler http.Handler
|
||||
if mh.mux.Match(r, &match) {
|
||||
handler = match.Handler
|
||||
apiRequest.Route = match.Route
|
||||
apiRequest.URLVars = match.Vars
|
||||
}
|
||||
|
||||
// Be sure that URLVars always is a map.
|
||||
if apiRequest.URLVars == nil {
|
||||
apiRequest.URLVars = make(map[string]string)
|
||||
}
|
||||
|
||||
// Check authentication.
|
||||
apiRequest.AuthToken = authenticateRequest(lrw, r, handler)
|
||||
if apiRequest.AuthToken == nil {
|
||||
// Authenticator already replied.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add security headers.
|
||||
if !devMode() {
|
||||
w.Header().Set(
|
||||
"Content-Security-Policy",
|
||||
"default-src 'self'; "+
|
||||
"style-src 'self' 'unsafe-inline'; "+
|
||||
"img-src 'self' data:",
|
||||
)
|
||||
w.Header().Set("Referrer-Policy", "no-referrer")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("X-Frame-Options", "deny")
|
||||
w.Header().Set("X-XSS-Protection", "1; mode=block")
|
||||
} else {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
}
|
||||
|
||||
// Handle request.
|
||||
switch {
|
||||
case handler != nil:
|
||||
handler.ServeHTTP(lrw, r)
|
||||
case errors.Is(match.MatchErr, mux.ErrMethodMismatch):
|
||||
http.Error(lrw, "Method not allowed.", http.StatusMethodNotAllowed)
|
||||
default: // handler == nil or other error
|
||||
http.Error(lrw, "Not found.", http.StatusNotFound)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
39
config/devmode.go
Normal file
39
config/devmode.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
// Configuration Keys.
|
||||
var (
|
||||
CfgDevModeKey = "core/devMode"
|
||||
defaultDevMode bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&defaultDevMode, "devmode", false, "enable development mode")
|
||||
}
|
||||
|
||||
func logDevModeOverride() {
|
||||
if defaultDevMode {
|
||||
log.Warning("config: development mode is enabled by default by the -devmode flag")
|
||||
}
|
||||
}
|
||||
|
||||
func registerDevModeOption() error {
|
||||
return Register(&Option{
|
||||
Name: "Development Mode",
|
||||
Key: CfgDevModeKey,
|
||||
Description: "In Development Mode, security restrictions are lifted/softened to enable unrestricted access for debugging and testing purposes.",
|
||||
OptType: OptTypeBool,
|
||||
ExpertiseLevel: ExpertiseLevelDeveloper,
|
||||
ReleaseLevel: ReleaseLevelStable,
|
||||
DefaultValue: defaultDevMode,
|
||||
Annotations: Annotations{
|
||||
DisplayOrderAnnotation: 512,
|
||||
CategoryAnnotation: "Development",
|
||||
},
|
||||
})
|
||||
}
|
|
@ -14,7 +14,7 @@ import (
|
|||
// to change deep configuration settings.
|
||||
type ExpertiseLevel uint8
|
||||
|
||||
// Expertise Level constants
|
||||
// Expertise Level constants.
|
||||
const (
|
||||
ExpertiseLevelUser ExpertiseLevel = 0
|
||||
ExpertiseLevelExpert ExpertiseLevel = 1
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
@ -17,6 +19,8 @@ const (
|
|||
var (
|
||||
module *modules.Module
|
||||
dataRoot *utils.DirStructure
|
||||
|
||||
exportConfig bool
|
||||
)
|
||||
|
||||
// SetDataRoot sets the data root from which the updates module derives its paths.
|
||||
|
@ -29,6 +33,8 @@ func SetDataRoot(root *utils.DirStructure) {
|
|||
func init() {
|
||||
module = modules.Register("config", prep, start, nil, "database")
|
||||
module.RegisterEvent(configChangeEvent)
|
||||
|
||||
flag.BoolVar(&exportConfig, "export-config-options", false, "export configuration registry and exit")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
|
@ -37,6 +43,16 @@ func prep() error {
|
|||
return errors.New("data root is not set")
|
||||
}
|
||||
|
||||
if exportConfig {
|
||||
modules.SetCmdLineOperation(exportConfigCmd)
|
||||
}
|
||||
|
||||
logDevModeOverride()
|
||||
err := registerDevModeOption()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -54,3 +70,13 @@ func start() error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func exportConfigCmd() error {
|
||||
data, err := json.MarshalIndent(ExportOptions(), "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = os.Stdout.Write(data)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ type PossibleValue struct {
|
|||
// future well-known annotation additions do not conflict
|
||||
// with vendor/product/package specific annoations.
|
||||
//
|
||||
// Format: <vendor/package>:<scope>:<identifier>
|
||||
// Format: <vendor/package>:<scope>:<identifier> //.
|
||||
type Annotations map[string]interface{}
|
||||
|
||||
// Well known annotations defined by this package.
|
||||
|
@ -144,7 +144,7 @@ type ValueRequirement struct {
|
|||
Value interface{}
|
||||
}
|
||||
|
||||
// Values for the DisplayHintAnnotation
|
||||
// Values for the DisplayHintAnnotation.
|
||||
const (
|
||||
// DisplayHintOneOf is used to mark an option
|
||||
// as a "select"-style option. That is, only one of
|
||||
|
@ -263,7 +263,7 @@ func (option *Option) SetAnnotation(key string, value interface{}) {
|
|||
option.Annotations[key] = value
|
||||
}
|
||||
|
||||
// GetAnnotation returns the value of the annotation key
|
||||
// GetAnnotation returns the value of the annotation key.
|
||||
func (option *Option) GetAnnotation(key string) (interface{}, bool) {
|
||||
option.Lock()
|
||||
defer option.Unlock()
|
||||
|
|
|
@ -3,6 +3,7 @@ package config
|
|||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
@ -29,6 +30,23 @@ func ForEachOption(fn func(opt *Option) error) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ExportOptions exports the registered options. The returned data must be
|
||||
// treated as immutable.
|
||||
// The data does not include the current active or default settings.
|
||||
func ExportOptions() []*Option {
|
||||
optionsLock.RLock()
|
||||
defer optionsLock.RUnlock()
|
||||
|
||||
// Copy the map into a slice.
|
||||
opts := make([]*Option, 0, len(options))
|
||||
for _, opt := range options {
|
||||
opts = append(opts, opt)
|
||||
}
|
||||
|
||||
sort.Sort(sortByKey(opts))
|
||||
return opts
|
||||
}
|
||||
|
||||
// GetOption returns the option with name or an error
|
||||
// if the option does not exist. The caller should lock
|
||||
// the returned option itself for further processing.
|
||||
|
|
|
@ -61,6 +61,27 @@ func (i *Interface) DelayedCacheWriter(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
// ClearCache clears the read cache.
|
||||
func (i *Interface) ClearCache() {
|
||||
// Check if cache is in use.
|
||||
if i.cache == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear all cache entries.
|
||||
i.cache.Purge()
|
||||
}
|
||||
|
||||
// FlushCache writes (and thus clears) the write cache.
|
||||
func (i *Interface) FlushCache() {
|
||||
// Check if write cache is in use.
|
||||
if i.options.DelayCachedWrites != "" {
|
||||
return
|
||||
}
|
||||
|
||||
i.flushWriteCache(0)
|
||||
}
|
||||
|
||||
func (i *Interface) flushWriteCache(percentThreshold int) {
|
||||
i.writeCacheLock.Lock()
|
||||
defer i.writeCacheLock.Unlock()
|
||||
|
|
44
go.mod
Normal file
44
go.mod
Normal file
|
@ -0,0 +1,44 @@
|
|||
module github.com/safing/portbase
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect
|
||||
github.com/VictoriaMetrics/metrics v1.12.3
|
||||
github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6
|
||||
github.com/armon/go-radix v1.0.0
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dgraph-io/badger v1.6.1
|
||||
github.com/go-ole/go-ole v1.2.4 // indirect
|
||||
github.com/gofrs/uuid v3.3.0+incompatible
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/gorilla/mux v1.7.4
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/hashicorp/go-version v1.2.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/seehuhn/fortuna v1.0.1
|
||||
github.com/shirou/gopsutil v2.20.4+incompatible
|
||||
github.com/spf13/cobra v1.0.0
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.6.1
|
||||
github.com/tevino/abool v1.0.0
|
||||
github.com/tidwall/gjson v1.6.0
|
||||
github.com/tidwall/sjson v1.1.1
|
||||
go.etcd.io/bbolt v1.3.4
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f
|
||||
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
// The follow-up commit removes Windows support.
|
||||
// TODO: Check how we want to handle this in the future, possibly ingest
|
||||
// needed functionality into here.
|
||||
github.com/google/renameio v0.1.1-0.20200217212219-353f81969824
|
||||
)
|
273
go.sum
Normal file
273
go.sum
Normal file
|
@ -0,0 +1,273 @@
|
|||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
|
||||
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/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/VictoriaMetrics/metrics v1.12.3 h1:Fe6JHC6MSEKa+BtLhPN8WIvS+HKPzMc2evEpNeCGy7I=
|
||||
github.com/VictoriaMetrics/metrics v1.12.3/go.mod h1:Z1tSfPfngDn12bTfZSCqArT3OPY3u88J12hSoOhuiRE=
|
||||
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/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
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/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833 h1:yCfXxYaelOyqnia8F/Yng47qhmfC9nKTRIbYRrRueq4=
|
||||
github.com/bluele/gcache v0.0.0-20190518031135-bc40bd653833/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
|
||||
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
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-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
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/dgraph-io/badger v1.6.1 h1:w9pSFNSdq/JPM1N12Fz/F/bzo993Is1W+Q7HjPzi7yg=
|
||||
github.com/dgraph-io/badger v1.6.1/go.mod h1:FRmFw3uxvcpa8zG3Rxs0th+hCLIuaQg8HlNV5bjgnuU=
|
||||
github.com/dgraph-io/ristretto v0.0.2 h1:a5WaUrDa0qm0YrAAS1tUykT5El3kt62KNZZeMxQn3po=
|
||||
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84=
|
||||
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
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/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/renameio v0.1.1-0.20200217212219-353f81969824 h1:9q700G0beHecUuiZOuKgNqNsGQixTeDLnzVZ5nsW3lc=
|
||||
github.com/google/renameio v0.1.1-0.20200217212219-353f81969824/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
|
||||
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
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.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI=
|
||||
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
|
||||
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
|
||||
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/seehuhn/fortuna v1.0.1 h1:lu9+CHsmR0bZnx5Ay646XvCSRJ8PJTi5UYJwDBX68H0=
|
||||
github.com/seehuhn/fortuna v1.0.1/go.mod h1:LX8ubejCnUoT/hX+1aKUtbKls2H6DRkqzkc7TdR3iis=
|
||||
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 v2.20.4+incompatible h1:cMT4rxS55zx9NVUnCkrmXCsEB/RNfG9SwHY9evtX8Ng=
|
||||
github.com/shirou/gopsutil v2.20.4+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||
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 v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
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.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
|
||||
github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
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.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tevino/abool v1.0.0 h1:5hlcsW0yartQp609pbLLrE/s3ZNm2k/F7YSGuqJxpbM=
|
||||
github.com/tevino/abool v1.0.0/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
|
||||
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
|
||||
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8=
|
||||
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U=
|
||||
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/valyala/fastrand v1.0.0 h1:LUKT9aKer2dVQNUi3waewTbKV+7H17kvWFNKs2ObdkI=
|
||||
github.com/valyala/fastrand v1.0.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
|
||||
github.com/valyala/histogram v1.1.2 h1:vOk5VrGjMBIoPR5k6wA8vBaC8toeJ8XO0yfRjFEc1h8=
|
||||
github.com/valyala/histogram v1.1.2/go.mod h1:CZAr6gK9dbD7hYx2s8WSPh0p5x5wETjC+2b3PJVtEdg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI=
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/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=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20190522155817-f3200d17e092/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-20200520182314-0ba52f642ac2 h1:eDrdRpKgkcCqKZQwyZRyeFZgfqt37SL7Kv3tok06cKE=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974 h1:IX6qOQeG5uLjB/hjjwjedwfjND0hgjPMMyO1RoIXQNI=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd h1:/e+gpKk9r3dJobndpTytxS2gOy6m5uvpg+ISQoEcusQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/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.0.0-20210115202250-e0d201561e39 h1:BTs2GMGSMWpgtCpv1CE7vkJTv7XcHdcLLnAMu7UbgTY=
|
||||
golang.org/x/tools v0.0.0-20210115202250-e0d201561e39/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c h1:grhR+C34yXImVGp7EzNk+DTIk+323eIUWOmEevy6bDo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -191,6 +192,8 @@ StackTrace:
|
|||
|
||||
// if currentLine and line are _not_ equal, output currentLine
|
||||
adapter.Write(currentLine, duplicates)
|
||||
// add to unexpected logs
|
||||
addUnexpectedLogs(currentLine)
|
||||
// reset duplicate counter
|
||||
duplicates = 0
|
||||
// set new currentLine
|
||||
|
@ -203,6 +206,8 @@ StackTrace:
|
|||
// write final line
|
||||
if currentLine != nil {
|
||||
adapter.Write(currentLine, duplicates)
|
||||
// add to unexpected logs
|
||||
addUnexpectedLogs(currentLine)
|
||||
}
|
||||
// reset state
|
||||
currentLine = nil //nolint:ineffassign
|
||||
|
@ -231,3 +236,60 @@ func finalizeWriting() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Last Unexpected Logs
|
||||
|
||||
var (
|
||||
lastUnexpectedLogs [10]string
|
||||
lastUnexpectedLogsIndex int
|
||||
lastUnexpectedLogsLock sync.Mutex
|
||||
)
|
||||
|
||||
func addUnexpectedLogs(line *logLine) {
|
||||
// Add main line.
|
||||
if line.level >= WarningLevel {
|
||||
addUnexpectedLogLine(line)
|
||||
return
|
||||
}
|
||||
|
||||
// Check for unexpected lines in the tracer.
|
||||
if line.tracer != nil {
|
||||
for _, traceLine := range line.tracer.logs {
|
||||
if traceLine.level >= WarningLevel {
|
||||
// Add full trace.
|
||||
addUnexpectedLogLine(line)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addUnexpectedLogLine(line *logLine) {
|
||||
lastUnexpectedLogsLock.Lock()
|
||||
defer lastUnexpectedLogsLock.Unlock()
|
||||
|
||||
// Format line and add to logs.
|
||||
lastUnexpectedLogs[lastUnexpectedLogsIndex] = formatLine(line, 0, false)
|
||||
|
||||
// Increase index and wrap back to start.
|
||||
lastUnexpectedLogsIndex = (lastUnexpectedLogsIndex + 1) % len(lastUnexpectedLogs)
|
||||
}
|
||||
|
||||
// GetLastUnexpectedLogs returns the last 10 log lines of level Warning an up.
|
||||
func GetLastUnexpectedLogs() []string {
|
||||
lastUnexpectedLogsLock.Lock()
|
||||
defer lastUnexpectedLogsLock.Unlock()
|
||||
|
||||
// Make a copy and return.
|
||||
len := len(lastUnexpectedLogs)
|
||||
start := lastUnexpectedLogsIndex
|
||||
logsCopy := make([]string, 0, len)
|
||||
// Loop from mid-to-mid.
|
||||
for i := start; i < start+len; i++ {
|
||||
if lastUnexpectedLogs[i%len] != "" {
|
||||
logsCopy = append(logsCopy, lastUnexpectedLogs[i%len])
|
||||
}
|
||||
}
|
||||
|
||||
return logsCopy
|
||||
}
|
||||
|
|
135
metrics/api.go
Normal file
135
metrics/api.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
)
|
||||
|
||||
func registerAPI() error {
|
||||
api.RegisterHandler("/metrics", &metricsAPI{})
|
||||
|
||||
return api.RegisterEndpoint(api.Endpoint{
|
||||
Path: "metrics/list",
|
||||
Read: api.PermitAnyone,
|
||||
MimeType: api.MimeTypeJSON,
|
||||
DataFunc: func(*api.Request) ([]byte, error) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
return json.Marshal(registry)
|
||||
},
|
||||
Name: "Export Registered Metrics",
|
||||
Description: "List all registered metrics with their metadata.",
|
||||
})
|
||||
}
|
||||
|
||||
type metricsAPI struct{}
|
||||
|
||||
func (m *metricsAPI) ReadPermission(*http.Request) api.Permission { return api.Dynamic }
|
||||
|
||||
func (m *metricsAPI) WritePermission(*http.Request) api.Permission { return api.NotSupported }
|
||||
|
||||
func (m *metricsAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Get API Request for permission and query.
|
||||
ar := api.GetAPIRequest(r)
|
||||
if ar == nil {
|
||||
http.Error(w, "Missing API Request.", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Get expertise level from query.
|
||||
expertiseLevel := config.ExpertiseLevelDeveloper
|
||||
switch ar.Request.URL.Query().Get("level") {
|
||||
case config.ExpertiseLevelNameUser:
|
||||
expertiseLevel = config.ExpertiseLevelUser
|
||||
case config.ExpertiseLevelNameExpert:
|
||||
expertiseLevel = config.ExpertiseLevelExpert
|
||||
case config.ExpertiseLevelNameDeveloper:
|
||||
expertiseLevel = config.ExpertiseLevelDeveloper
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
WriteMetrics(w, ar.AuthToken.Read, expertiseLevel)
|
||||
}
|
||||
|
||||
// WriteMetrics writes all metrics that match the given permission and
|
||||
// expertiseLevel to the given writer.
|
||||
func WriteMetrics(w io.Writer, permission api.Permission, expertiseLevel config.ExpertiseLevel) {
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
// Write all matching metrics.
|
||||
for _, metric := range registry {
|
||||
if permission >= metric.Opts().Permission &&
|
||||
expertiseLevel >= metric.Opts().ExpertiseLevel {
|
||||
metric.WritePrometheus(w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeMetricsTo(ctx context.Context, url string) error {
|
||||
// First, collect metrics into buffer.
|
||||
buf := &bytes.Buffer{}
|
||||
WriteMetrics(buf, api.PermitSelf, config.ExpertiseLevelDeveloper)
|
||||
|
||||
// Check if there is something to send.
|
||||
if buf.Len() == 0 {
|
||||
log.Debugf("metrics: not pushing metrics, nothing to send")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create request
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Send.
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check return status.
|
||||
if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get and return error.
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf(
|
||||
"got %s while writing metrics to %s: %s",
|
||||
resp.Status,
|
||||
url,
|
||||
body,
|
||||
)
|
||||
}
|
||||
|
||||
func metricsWriter(ctx context.Context) error {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
err := writeMetricsTo(ctx, pushURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
153
metrics/metric.go
Normal file
153
metrics/metric.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
|
||||
vm "github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// PrometheusFormatRequirement is required format defined by prometheus for
|
||||
// metric and label names.
|
||||
const PrometheusFormatRequirement = "[a-zA-Z_][a-zA-Z0-9_]*"
|
||||
|
||||
var prometheusFormat = regexp.MustCompile(PrometheusFormatRequirement)
|
||||
|
||||
// Metric represents one or more metrics.
|
||||
type Metric interface {
|
||||
ID() string
|
||||
LabeledID() string
|
||||
Opts() *Options
|
||||
WritePrometheus(w io.Writer)
|
||||
}
|
||||
|
||||
type metricBase struct {
|
||||
Identifier string
|
||||
Labels map[string]string
|
||||
LabeledIdentifier string
|
||||
Options *Options
|
||||
set *vm.Set
|
||||
}
|
||||
|
||||
// Options can be used to set advanced metric settings.
|
||||
type Options struct {
|
||||
// Name defines an optional human readable name for the metric.
|
||||
Name string
|
||||
|
||||
// AlertLimit defines an upper limit that triggers an alert.
|
||||
AlertLimit float64
|
||||
|
||||
// AlertTimeframe defines an optional timeframe in seconds for which the
|
||||
// AlertLimit should be interpreted in.
|
||||
AlertTimeframe float64
|
||||
|
||||
// Permission defines the permission that is required to read the metric.
|
||||
Permission api.Permission
|
||||
|
||||
// ExpertiseLevel defines the expertise level that the metric is meant for.
|
||||
ExpertiseLevel config.ExpertiseLevel
|
||||
|
||||
// Persist enabled persisting the metric on shutdown and loading the previous
|
||||
// value at start. This is only supported for counters.
|
||||
Persist bool
|
||||
}
|
||||
|
||||
func newMetricBase(id string, labels map[string]string, opts Options) (*metricBase, error) {
|
||||
// Check formats.
|
||||
if !prometheusFormat.MatchString(id) {
|
||||
return nil, fmt.Errorf("metric name %q must match %s", id, PrometheusFormatRequirement)
|
||||
}
|
||||
for labelName := range labels {
|
||||
if !prometheusFormat.MatchString(labelName) {
|
||||
return nil, fmt.Errorf("metric label name %q must match %s", labelName, PrometheusFormatRequirement)
|
||||
}
|
||||
}
|
||||
|
||||
// Check permission.
|
||||
if opts.Permission < api.PermitAnyone {
|
||||
// Default to PermitUser.
|
||||
opts.Permission = api.PermitUser
|
||||
}
|
||||
|
||||
// Create metric base.
|
||||
base := &metricBase{
|
||||
Identifier: id,
|
||||
Labels: labels,
|
||||
Options: &opts,
|
||||
set: vm.NewSet(),
|
||||
}
|
||||
base.LabeledIdentifier = base.buildLabeledID()
|
||||
return base, nil
|
||||
}
|
||||
|
||||
// ID returns the given ID of the metric.
|
||||
func (m *metricBase) ID() string {
|
||||
return m.Identifier
|
||||
}
|
||||
|
||||
// LabeledID returns the Prometheus-compatible labeled ID of the metric.
|
||||
func (m *metricBase) LabeledID() string {
|
||||
return m.LabeledIdentifier
|
||||
}
|
||||
|
||||
// Opts returns the metric options. They may not be modified.
|
||||
func (m *metricBase) Opts() *Options {
|
||||
return m.Options
|
||||
}
|
||||
|
||||
// WritePrometheus writes the metric in the prometheus format to the given writer.
|
||||
func (m *metricBase) WritePrometheus(w io.Writer) {
|
||||
m.set.WritePrometheus(w)
|
||||
}
|
||||
|
||||
func (m *metricBase) buildLabeledID() string {
|
||||
// Because we use the namespace and the global flags here, we need to flag
|
||||
// them as immutable.
|
||||
registryLock.Lock()
|
||||
defer registryLock.Unlock()
|
||||
firstMetricRegistered = true
|
||||
|
||||
// Build ID from Identifier.
|
||||
metricID := strings.TrimSpace(strings.ReplaceAll(m.Identifier, "/", "_"))
|
||||
|
||||
// Add namespace to ID.
|
||||
if metricNamespace != "" {
|
||||
metricID = metricNamespace + "_" + metricID
|
||||
}
|
||||
|
||||
// Return now if no labels are defined.
|
||||
if len(globalLabels) == 0 && len(m.Labels) == 0 {
|
||||
return metricID
|
||||
}
|
||||
|
||||
// Add global labels to the custom ones, if they don't exist yet.
|
||||
for labelName, labelValue := range globalLabels {
|
||||
if _, ok := m.Labels[labelName]; !ok {
|
||||
m.Labels[labelName] = labelValue
|
||||
}
|
||||
}
|
||||
|
||||
// Render labels into a slice and sort them in order to make the labeled ID
|
||||
// reproducible.
|
||||
labels := make([]string, 0, len(m.Labels))
|
||||
for labelName, labelValue := range m.Labels {
|
||||
labels = append(labels, fmt.Sprintf("%s=%q", labelName, labelValue))
|
||||
}
|
||||
sort.Strings(labels)
|
||||
|
||||
// Return fully labaled ID.
|
||||
return fmt.Sprintf("%s{%s}", metricID, strings.Join(labels, ","))
|
||||
}
|
||||
|
||||
// Split metrics into sets, according to the API Auth Levels, which will also correspond to the UI Mode levels. SPN // nodes will also allow public access to metrics with the permission "PermitAnyone".
|
||||
// Save "life-long" metrics on shutdown and load them at start.
|
||||
// Generate the correct metric name and labels.
|
||||
// Expose metrics via http, but also via the runtime DB in order to push metrics to the UI.
|
||||
// The UI will have to parse the prometheus metrics format and will not be able to immediately present historical data, // but data will have to be built.
|
||||
// Provide the option to push metrics to a prometheus push gateway, this is especially helpful when gathering data from // loads of SPN nodes.
|
44
metrics/metric_counter.go
Normal file
44
metrics/metric_counter.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
vm "github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// Counter is a counter metric.
|
||||
type Counter struct {
|
||||
*metricBase
|
||||
*vm.Counter
|
||||
}
|
||||
|
||||
// NewCounter registers a new counter metric.
|
||||
func NewCounter(id string, labels map[string]string, opts *Options) (*Counter, error) {
|
||||
// Ensure that there are options.
|
||||
if opts == nil {
|
||||
opts = &Options{}
|
||||
}
|
||||
|
||||
// Make base.
|
||||
base, err := newMetricBase(id, labels, *opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create metric struct.
|
||||
m := &Counter{
|
||||
metricBase: base,
|
||||
}
|
||||
|
||||
// Create metric in set
|
||||
m.Counter = m.set.NewCounter(m.LabeledID())
|
||||
|
||||
// Register metric.
|
||||
err = register(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load state.
|
||||
m.loadState()
|
||||
|
||||
return m, nil
|
||||
}
|
41
metrics/metric_gauge.go
Normal file
41
metrics/metric_gauge.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
vm "github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// Gauge is a gauge metric.
|
||||
type Gauge struct {
|
||||
*metricBase
|
||||
*vm.Gauge
|
||||
}
|
||||
|
||||
// NewGauge registers a new gauge metric.
|
||||
func NewGauge(id string, labels map[string]string, fn func() float64, opts *Options) (*Gauge, error) {
|
||||
// Ensure that there are options.
|
||||
if opts == nil {
|
||||
opts = &Options{}
|
||||
}
|
||||
|
||||
// Make base.
|
||||
base, err := newMetricBase(id, labels, *opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create metric struct.
|
||||
m := &Gauge{
|
||||
metricBase: base,
|
||||
}
|
||||
|
||||
// Create metric in set
|
||||
m.Gauge = m.set.NewGauge(m.LabeledID(), fn)
|
||||
|
||||
// Register metric.
|
||||
err = register(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
41
metrics/metric_histogram.go
Normal file
41
metrics/metric_histogram.go
Normal file
|
@ -0,0 +1,41 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
vm "github.com/VictoriaMetrics/metrics"
|
||||
)
|
||||
|
||||
// Histogram is a histogram metric.
|
||||
type Histogram struct {
|
||||
*metricBase
|
||||
*vm.Histogram
|
||||
}
|
||||
|
||||
// NewHistogram registers a new histogram metric.
|
||||
func NewHistogram(id string, labels map[string]string, opts *Options) (*Histogram, error) {
|
||||
// Ensure that there are options.
|
||||
if opts == nil {
|
||||
opts = &Options{}
|
||||
}
|
||||
|
||||
// Make base.
|
||||
base, err := newMetricBase(id, labels, *opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create metric struct.
|
||||
m := &Histogram{
|
||||
metricBase: base,
|
||||
}
|
||||
|
||||
// Create metric in set
|
||||
m.Histogram = m.set.NewHistogram(m.LabeledID())
|
||||
|
||||
// Register metric.
|
||||
err = register(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
40
metrics/metric_info.go
Normal file
40
metrics/metric_info.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/portbase/info"
|
||||
)
|
||||
|
||||
func registerInfoMetric() error {
|
||||
meta := info.GetInfo()
|
||||
_, err := NewGauge(
|
||||
"info",
|
||||
map[string]string{
|
||||
"version": checkUnknown(meta.Version),
|
||||
"commit": checkUnknown(meta.Commit),
|
||||
"build_options": checkUnknown(meta.BuildOptions),
|
||||
"build_user": checkUnknown(meta.BuildUser),
|
||||
"build_host": checkUnknown(meta.BuildHost),
|
||||
"build_date": checkUnknown(meta.BuildDate),
|
||||
"build_source": checkUnknown(meta.BuildSource),
|
||||
"go_os": runtime.GOOS,
|
||||
"go_arch": runtime.GOARCH,
|
||||
"go_version": runtime.Version(),
|
||||
"go_compiler": runtime.Compiler,
|
||||
},
|
||||
func() float64 {
|
||||
return 1
|
||||
},
|
||||
nil,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func checkUnknown(s string) string {
|
||||
if strings.Contains(s, "unknown") {
|
||||
return "unknown"
|
||||
}
|
||||
return s
|
||||
}
|
48
metrics/metric_runtime.go
Normal file
48
metrics/metric_runtime.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
vm "github.com/VictoriaMetrics/metrics"
|
||||
"github.com/safing/portbase/api"
|
||||
"github.com/safing/portbase/config"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registryLock.Lock()
|
||||
defer registryLock.Unlock()
|
||||
|
||||
registry = append(registry, &runtimeMetrics{})
|
||||
}
|
||||
|
||||
var runtimeOpts = &Options{
|
||||
Name: "Golang Runtime",
|
||||
Permission: api.PermitAdmin,
|
||||
ExpertiseLevel: config.ExpertiseLevelDeveloper,
|
||||
}
|
||||
|
||||
type runtimeMetrics struct{}
|
||||
|
||||
func (r *runtimeMetrics) ID() string {
|
||||
return "_runtime"
|
||||
}
|
||||
|
||||
func (r *runtimeMetrics) LabeledID() string {
|
||||
return "_runtime"
|
||||
}
|
||||
|
||||
func (r *runtimeMetrics) Opts() *Options {
|
||||
return runtimeOpts
|
||||
}
|
||||
|
||||
func (r *runtimeMetrics) Permission() api.Permission {
|
||||
return runtimeOpts.Permission
|
||||
}
|
||||
|
||||
func (r *runtimeMetrics) ExpertiseLevel() config.ExpertiseLevel {
|
||||
return runtimeOpts.ExpertiseLevel
|
||||
}
|
||||
|
||||
func (r *runtimeMetrics) WritePrometheus(w io.Writer) {
|
||||
vm.WriteProcessMetrics(w)
|
||||
}
|
140
metrics/module.go
Normal file
140
metrics/module.go
Normal file
|
@ -0,0 +1,140 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/safing/portbase/modules"
|
||||
)
|
||||
|
||||
var (
|
||||
module *modules.Module
|
||||
|
||||
registry []Metric
|
||||
registryLock sync.RWMutex
|
||||
|
||||
firstMetricRegistered bool
|
||||
metricNamespace string
|
||||
globalLabels = make(map[string]string)
|
||||
|
||||
pushURL string
|
||||
metricInstance string
|
||||
|
||||
// ErrAlreadyStarted is returned when an operation is only valid before the
|
||||
// first metric is registered, and is called after.
|
||||
ErrAlreadyStarted = errors.New("can only be changed before first metric is registered")
|
||||
|
||||
// ErrAlreadyRegistered is returned when a metric with the same ID is
|
||||
// registered again.
|
||||
ErrAlreadyRegistered = errors.New("metric already registered")
|
||||
|
||||
// ErrAlreadySet is returned when a value is already set and cannot be changed.
|
||||
ErrAlreadySet = errors.New("already set")
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&pushURL, "push-metrics", "", "URL to push prometheus metrics to")
|
||||
flag.StringVar(&metricInstance, "metrics-instance", "", "Set the global instance label")
|
||||
|
||||
module = modules.Register("metrics", prep, start, stop, "database", "api")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
// Add metric instance name as global variable if set.
|
||||
if metricInstance != "" {
|
||||
if err := AddGlobalLabel("instance", metricInstance); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return registerInfoMetric()
|
||||
}
|
||||
|
||||
func start() error {
|
||||
if err := registerAPI(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if pushURL != "" {
|
||||
module.StartServiceWorker("metric pusher", 0, metricsWriter)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stop() error {
|
||||
storePersistentMetrics()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func register(m Metric) error {
|
||||
registryLock.Lock()
|
||||
defer registryLock.Unlock()
|
||||
|
||||
// Check if metric ID is already registered.
|
||||
for _, registeredMetric := range registry {
|
||||
if m.LabeledID() == registeredMetric.LabeledID() {
|
||||
return ErrAlreadyRegistered
|
||||
}
|
||||
}
|
||||
|
||||
// Add new metric to registry and sort it.
|
||||
registry = append(registry, m)
|
||||
sort.Sort(byLabeledID(registry))
|
||||
|
||||
// Set flag that first metric is now registered.
|
||||
firstMetricRegistered = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetNamespace sets the namespace for all metrics. It is prefixed to all
|
||||
// metric IDs.
|
||||
// It must be set before any metric is registered.
|
||||
// Does not affect golang runtime metrics.
|
||||
func SetNamespace(namespace string) error {
|
||||
// Lock registry and check if a first metric is already registered.
|
||||
registryLock.Lock()
|
||||
defer registryLock.Unlock()
|
||||
if firstMetricRegistered {
|
||||
return ErrAlreadyStarted
|
||||
}
|
||||
|
||||
// Check if the namespace is already set.
|
||||
if metricNamespace != "" {
|
||||
return ErrAlreadySet
|
||||
}
|
||||
|
||||
metricNamespace = namespace
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddGlobalLabel adds a global label to all metrics.
|
||||
// Global labels must be added before any metric is registered.
|
||||
// Does not affect golang runtime metrics.
|
||||
func AddGlobalLabel(name, value string) error {
|
||||
// Lock registry and check if a first metric is already registered.
|
||||
registryLock.Lock()
|
||||
defer registryLock.Unlock()
|
||||
if firstMetricRegistered {
|
||||
return ErrAlreadyStarted
|
||||
}
|
||||
|
||||
// Check format.
|
||||
if !prometheusFormat.MatchString(name) {
|
||||
return fmt.Errorf("metric label name %q must match %s", name, PrometheusFormatRequirement)
|
||||
}
|
||||
|
||||
globalLabels[name] = value
|
||||
return nil
|
||||
}
|
||||
|
||||
type byLabeledID []Metric
|
||||
|
||||
func (r byLabeledID) Len() int { return len(r) }
|
||||
func (r byLabeledID) Less(i, j int) bool { return r[i].LabeledID() < r[j].LabeledID() }
|
||||
func (r byLabeledID) Swap(i, j int) { r[i], r[j] = r[j], r[i] }
|
151
metrics/persistence.go
Normal file
151
metrics/persistence.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
package metrics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/tevino/abool"
|
||||
)
|
||||
|
||||
var (
|
||||
storage *metricsStorage
|
||||
storageKey string
|
||||
storageInit = abool.New()
|
||||
storageLoaded = abool.New()
|
||||
|
||||
db = database.NewInterface(&database.Options{
|
||||
Local: true,
|
||||
Internal: true,
|
||||
})
|
||||
|
||||
// ErrAlreadyInitialized is returned when trying to initialize an option
|
||||
// more than once.
|
||||
ErrAlreadyInitialized = errors.New("already initialized")
|
||||
)
|
||||
|
||||
type metricsStorage struct {
|
||||
sync.Mutex
|
||||
record.Base
|
||||
|
||||
Start time.Time
|
||||
Counters map[string]uint64
|
||||
}
|
||||
|
||||
// EnableMetricPersistence enables metric persistence for metrics that opted
|
||||
// for it. They given key is the database key where the metric data will be
|
||||
// persisted.
|
||||
// This call also directly loads the stored data from the database.
|
||||
// The returned error is only about loading the metrics, not about enabling
|
||||
// persistence.
|
||||
// May only be called once.
|
||||
func EnableMetricPersistence(key string) error {
|
||||
// Check if already initialized.
|
||||
if !storageInit.SetToIf(false, true) {
|
||||
return ErrAlreadyInitialized
|
||||
}
|
||||
|
||||
// Set storage key.
|
||||
storageKey = key
|
||||
|
||||
// Load metrics from storage.
|
||||
var err error
|
||||
storage, err = getMetricsStorage(key)
|
||||
switch {
|
||||
case err == nil:
|
||||
// Continue.
|
||||
case errors.Is(err, database.ErrNotFound):
|
||||
return nil
|
||||
default:
|
||||
return err
|
||||
}
|
||||
storageLoaded.Set()
|
||||
|
||||
// Load saved state for all counter metrics.
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
for _, m := range registry {
|
||||
counter, ok := m.(*Counter)
|
||||
if ok {
|
||||
counter.loadState()
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Counter) loadState() {
|
||||
// Check if we can and should load the state.
|
||||
if !storageLoaded.IsSet() || !c.Opts().Persist {
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(storage.Counters[c.LabeledID()])
|
||||
}
|
||||
|
||||
func storePersistentMetrics() {
|
||||
// Check if persistence is enabled.
|
||||
if !storageInit.IsSet() || storageKey == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Create new storage.
|
||||
newStorage := &metricsStorage{
|
||||
Start: time.Now(),
|
||||
Counters: make(map[string]uint64),
|
||||
}
|
||||
newStorage.SetKey(storageKey)
|
||||
// Copy values from previous version.
|
||||
if storageLoaded.IsSet() {
|
||||
newStorage.Start = storage.Start
|
||||
}
|
||||
|
||||
registryLock.RLock()
|
||||
defer registryLock.RUnlock()
|
||||
|
||||
// Export all counter metrics.
|
||||
for _, m := range registry {
|
||||
if m.Opts().Persist {
|
||||
counter, ok := m.(*Counter)
|
||||
if ok {
|
||||
newStorage.Counters[m.LabeledID()] = counter.Get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save to database.
|
||||
err := db.Put(newStorage)
|
||||
if err != nil {
|
||||
log.Warningf("metrics: failed to save metrics storage to db: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func getMetricsStorage(key string) (*metricsStorage, error) {
|
||||
r, err := db.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// unwrap
|
||||
if r.IsWrapped() {
|
||||
// only allocate a new struct, if we need it
|
||||
new := &metricsStorage{}
|
||||
err = record.Unwrap(r, new)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return new, nil
|
||||
}
|
||||
|
||||
// or adjust type
|
||||
new, ok := r.(*metricsStorage)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("record not of type *metricsStorage, but %T", r)
|
||||
}
|
||||
return new, nil
|
||||
}
|
1
metrics/test/.gitignore
vendored
Normal file
1
metrics/test/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
data
|
4
metrics/test/README.md
Normal file
4
metrics/test/README.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
# Testing metrics
|
||||
|
||||
You can spin up a test setup for pushing and viewing metrics with `docker-compose up`.
|
||||
Then use the flag `--push-metrics http://127.0.0.1:8428/api/v1/import/prometheus` to push metrics.
|
36
metrics/test/docker-compose.yml
Normal file
36
metrics/test/docker-compose.yml
Normal file
|
@ -0,0 +1,36 @@
|
|||
version: '3.8'
|
||||
|
||||
networks:
|
||||
pm-metrics-test-net:
|
||||
|
||||
services:
|
||||
|
||||
victoriametrics:
|
||||
container_name: pm-metrics-test-victoriametrics
|
||||
image: victoriametrics/victoria-metrics
|
||||
command:
|
||||
- '--storageDataPath=/storage'
|
||||
ports:
|
||||
- 8428:8428
|
||||
volumes:
|
||||
- ./data/victoriametrics:/storage
|
||||
networks:
|
||||
- pm-metrics-test-net
|
||||
restart: always
|
||||
|
||||
grafana:
|
||||
container_name: pm-metrics-test-grafana
|
||||
image: grafana/grafana
|
||||
command:
|
||||
- '--config=/etc/grafana/provisioning/config.ini'
|
||||
depends_on:
|
||||
- "victoriametrics"
|
||||
ports:
|
||||
- 3000:3000
|
||||
volumes:
|
||||
- ./data/grafana:/var/lib/grafana
|
||||
- ./grafana:/etc/grafana/provisioning
|
||||
- ./dashboards:/dashboards
|
||||
networks:
|
||||
- pm-metrics-test-net
|
||||
restart: always
|
10
metrics/test/grafana/config.ini
Normal file
10
metrics/test/grafana/config.ini
Normal file
|
@ -0,0 +1,10 @@
|
|||
[auth]
|
||||
disable_login_form = true
|
||||
disable_signout_menu = true
|
||||
|
||||
[auth.basic]
|
||||
enabled = false
|
||||
|
||||
[auth.anonymous]
|
||||
enabled = true
|
||||
org_role = Admin
|
11
metrics/test/grafana/dashboards/portmaster.yml
Normal file
11
metrics/test/grafana/dashboards/portmaster.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Portmaster'
|
||||
folder: 'Portmaster'
|
||||
disableDeletion: true
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /dashboards
|
||||
foldersFromFilesStructure: true
|
8
metrics/test/grafana/datasources/datasource.yml
Normal file
8
metrics/test/grafana/datasources/datasource.yml
Normal file
|
@ -0,0 +1,8 @@
|
|||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: VictoriaMetrics
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://pm-metrics-test-victoriametrics:8428
|
||||
isDefault: true
|
10
modules/cmd.go
Normal file
10
modules/cmd.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package modules
|
||||
|
||||
var (
|
||||
cmdLineOperation func() error
|
||||
)
|
||||
|
||||
// SetCmdLineOperation sets a command line operation to be executed instead of starting the system. This is useful when functions need all modules to be prepared for a special operation.
|
||||
func SetCmdLineOperation(fn func() error) {
|
||||
cmdLineOperation = fn
|
||||
}
|
|
@ -11,7 +11,8 @@ import (
|
|||
var (
|
||||
errorReportingChannel chan *ModuleError
|
||||
reportToStdErr = true
|
||||
reportingLock sync.RWMutex
|
||||
lastReportedError *ModuleError
|
||||
reportingLock sync.Mutex
|
||||
)
|
||||
|
||||
// ModuleError wraps a panic, error or message into an error that can be reported.
|
||||
|
@ -67,10 +68,37 @@ func (me *ModuleError) Error() string {
|
|||
return me.Message
|
||||
}
|
||||
|
||||
// Format returns the error formatted in key/value form.
|
||||
func (me *ModuleError) Format() string {
|
||||
return fmt.Sprintf(
|
||||
`Message: %s
|
||||
Timestamp: %s
|
||||
ModuleName: %s
|
||||
TaskName: %s
|
||||
TaskType: %s
|
||||
Severity: %s
|
||||
PanicValue: %s
|
||||
StackTrace:
|
||||
|
||||
%s
|
||||
`,
|
||||
me.Message,
|
||||
time.Now(),
|
||||
me.ModuleName,
|
||||
me.TaskName,
|
||||
me.TaskType,
|
||||
me.Severity,
|
||||
me.PanicValue,
|
||||
me.StackTrace,
|
||||
)
|
||||
}
|
||||
|
||||
// Report reports the error through the configured reporting channel.
|
||||
func (me *ModuleError) Report() {
|
||||
reportingLock.RLock()
|
||||
defer reportingLock.RUnlock()
|
||||
reportingLock.Lock()
|
||||
defer reportingLock.Unlock()
|
||||
|
||||
lastReportedError = me
|
||||
|
||||
if errorReportingChannel != nil {
|
||||
select {
|
||||
|
@ -83,27 +111,8 @@ func (me *ModuleError) Report() {
|
|||
// default to writing to stderr
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
`===== Error Report =====
|
||||
Message: %s
|
||||
Timestamp: %s
|
||||
ModuleName: %s
|
||||
TaskName: %s
|
||||
TaskType: %s
|
||||
Severity: %s
|
||||
PanicValue: %s
|
||||
StackTrace:
|
||||
|
||||
%s
|
||||
===== End of Report =====
|
||||
`,
|
||||
me.Message,
|
||||
time.Now(),
|
||||
me.ModuleName,
|
||||
me.TaskName,
|
||||
me.TaskType,
|
||||
me.Severity,
|
||||
me.PanicValue,
|
||||
me.StackTrace,
|
||||
"===== Error Report =====\n%s\n===== End of Report =====\n",
|
||||
me.Format(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -133,3 +142,11 @@ func SetStdErrReporting(on bool) {
|
|||
|
||||
reportToStdErr = on
|
||||
}
|
||||
|
||||
// GetLastReportedError returns the last reported module error.
|
||||
func GetLastReportedError() *ModuleError {
|
||||
reportingLock.Lock()
|
||||
defer reportingLock.Unlock()
|
||||
|
||||
return lastReportedError
|
||||
}
|
||||
|
|
|
@ -74,6 +74,16 @@ func Start() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// execute command if available
|
||||
if cmdLineOperation != nil {
|
||||
err := cmdLineOperation()
|
||||
if err != nil {
|
||||
SetExitStatusCode(1)
|
||||
fmt.Fprintf(os.Stderr, "cmdline operation failed: %s\n", err)
|
||||
}
|
||||
return ErrCleanExit
|
||||
}
|
||||
|
||||
// start logging
|
||||
log.EnableScheduling()
|
||||
err = log.Start()
|
||||
|
|
61
test
61
test
|
@ -6,6 +6,7 @@ scripted=0
|
|||
goUp="\\e[1A"
|
||||
fullTestFlags="-short"
|
||||
install=0
|
||||
testonly=0
|
||||
|
||||
function help {
|
||||
echo "usage: $0 [command] [options]"
|
||||
|
@ -17,6 +18,7 @@ function help {
|
|||
echo ""
|
||||
echo "options:"
|
||||
echo " --scripted dont jump console lines (still use colors)"
|
||||
echo " --test-only skip linters and only run tests"
|
||||
echo " [package] run tests only on this package"
|
||||
}
|
||||
|
||||
|
@ -92,6 +94,10 @@ while true; do
|
|||
goUp=""
|
||||
shift 1
|
||||
;;
|
||||
"--test-only")
|
||||
testonly=1
|
||||
shift 1
|
||||
;;
|
||||
"install")
|
||||
install=1
|
||||
shift 1
|
||||
|
@ -117,8 +123,8 @@ if [[ $install -eq 1 ]]; then
|
|||
echo "$ go get -u golang.org/x/lint/golint"
|
||||
go get -u golang.org/x/lint/golint
|
||||
# TODO: update golangci-lint version regularly
|
||||
echo "$ curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0"
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
|
||||
echo "$ curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.29.0"
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.29.0
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
@ -127,26 +133,27 @@ if [[ $(which go) == "" ]]; then
|
|||
echo "go command not found"
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(which gofmt) == "" ]]; then
|
||||
echo "gofmt command not found"
|
||||
exit 1
|
||||
if [[ $testonly -eq 0 ]]; then
|
||||
if [[ $(which gofmt) == "" ]]; then
|
||||
echo "gofmt command not found"
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(which golint) == "" ]]; then
|
||||
echo "golint command not found"
|
||||
echo "install with: go get -u golang.org/x/lint/golint"
|
||||
echo "or run: ./test install"
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(which golangci-lint) == "" ]]; then
|
||||
echo "golangci-lint command not found"
|
||||
echo "install with: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z"
|
||||
echo "don't forget to specify the version you want"
|
||||
echo "or run: ./test install"
|
||||
echo ""
|
||||
echo "alternatively, install the current dev version with: go get -u github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [[ $(which golint) == "" ]]; then
|
||||
echo "golint command not found"
|
||||
echo "install with: go get -u golang.org/x/lint/golint"
|
||||
echo "or run: ./test install"
|
||||
exit 1
|
||||
fi
|
||||
if [[ $(which golangci-lint) == "" ]]; then
|
||||
echo "golangci-lint command not found"
|
||||
echo "install with: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin vX.Y.Z"
|
||||
echo "don't forget to specify the version you want"
|
||||
echo "or run: ./test install"
|
||||
echo ""
|
||||
echo "alternatively, install the current dev version with: go get -u github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# target selection
|
||||
if [[ "$1" == "" ]]; then
|
||||
# get all packages
|
||||
|
@ -170,10 +177,12 @@ echo "running tests for ${platformInfo//$'\n'/ }:"
|
|||
for package in $packages; do
|
||||
echo ""
|
||||
echo $package
|
||||
checkformat $package
|
||||
run golint -set_exit_status -min_confidence 1.0 $package
|
||||
run go vet $package
|
||||
run golangci-lint run $GOPATH/src/$package
|
||||
if [[ $testonly -eq 0 ]]; then
|
||||
checkformat $package
|
||||
run golint -set_exit_status -min_confidence 1.0 $package
|
||||
run go vet $package
|
||||
run golangci-lint run $GOPATH/src/$package
|
||||
fi
|
||||
run go test -cover $fullTestFlags $package
|
||||
done
|
||||
|
||||
|
@ -184,4 +193,4 @@ if [[ $errors -gt 0 ]]; then
|
|||
else
|
||||
echo "succeeded with $warnings warnings"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
|
@ -5,6 +5,10 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
|
||||
// Version is fixed to commit 353f8196982447d8b12c64f69530e657331e3dbc.
|
||||
// The follow-up commit removes Windows support.
|
||||
// TOOD: Check how we want to handle this in the future, possibly ingest
|
||||
// needed functionality into here.
|
||||
"github.com/google/renameio"
|
||||
)
|
||||
|
||||
|
|
182
utils/debug/debug.go
Normal file
182
utils/debug/debug.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
package debug
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/shirou/gopsutil/host"
|
||||
)
|
||||
|
||||
// Info gathers debugging information and stores everything in a buffer in
|
||||
// order to write it to somewhere later. It directly inherits a bytes.Buffer,
|
||||
// so you can also use all these functions too.
|
||||
type Info struct {
|
||||
bytes.Buffer
|
||||
Style string
|
||||
}
|
||||
|
||||
// InfoFlag defines possible options for adding sections to a Info.
|
||||
type InfoFlag int
|
||||
|
||||
const (
|
||||
// NoFlags does nothing.
|
||||
NoFlags InfoFlag = 0
|
||||
|
||||
// UseCodeSection wraps the section content in a markdown code section.
|
||||
UseCodeSection InfoFlag = 1
|
||||
|
||||
// AddContentLineBreaks adds a line breaks after each line of content,
|
||||
// except for the last.
|
||||
AddContentLineBreaks InfoFlag = 2
|
||||
)
|
||||
|
||||
func useCodeSection(flags InfoFlag) bool {
|
||||
return flags&UseCodeSection > 0
|
||||
}
|
||||
|
||||
func addContentLineBreaks(flags InfoFlag) bool {
|
||||
return flags&AddContentLineBreaks > 0
|
||||
}
|
||||
|
||||
// AddSection adds a debug section to the Info. The result is directly
|
||||
// written into the buffer.
|
||||
func (di *Info) AddSection(name string, flags InfoFlag, content ...string) {
|
||||
// Check if we need a spacer.
|
||||
if di.Len() > 0 {
|
||||
di.WriteString("\n\n")
|
||||
}
|
||||
|
||||
// Write section to buffer.
|
||||
|
||||
// Write section header.
|
||||
if di.Style == "github" {
|
||||
di.WriteString(fmt.Sprintf("<details>\n<summary>%s</summary>\n\n", name))
|
||||
} else {
|
||||
di.WriteString(fmt.Sprintf("**%s**:\n\n", name))
|
||||
}
|
||||
|
||||
// Write section content.
|
||||
if useCodeSection(flags) {
|
||||
// Write code header: Needs one empty line between previous data.
|
||||
di.WriteString("```\n")
|
||||
}
|
||||
for i, part := range content {
|
||||
di.WriteString(part)
|
||||
if addContentLineBreaks(flags) && i < len(content)-1 {
|
||||
di.WriteString("\n")
|
||||
}
|
||||
}
|
||||
if useCodeSection(flags) {
|
||||
// Write code footer: Needs one empty line between next data.
|
||||
di.WriteString("\n```\n")
|
||||
}
|
||||
|
||||
// Write section header.
|
||||
if di.Style == "github" {
|
||||
di.WriteString("\n</details>")
|
||||
}
|
||||
}
|
||||
|
||||
// AddVersionInfo adds version information from the info pkg.
|
||||
func (di *Info) AddVersionInfo() {
|
||||
di.AddSection(
|
||||
"Version "+info.Version(),
|
||||
UseCodeSection,
|
||||
info.FullVersion(),
|
||||
)
|
||||
}
|
||||
|
||||
// AddPlatformInfo adds OS and platform information.
|
||||
func (di *Info) AddPlatformInfo(ctx context.Context) {
|
||||
// Get information from the system.
|
||||
info, err := host.InfoWithContext(ctx)
|
||||
if err != nil {
|
||||
di.AddSection(
|
||||
"Platform Information",
|
||||
NoFlags,
|
||||
fmt.Sprintf("Failed to get: %s", err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we want to add virtulization information.
|
||||
var virtInfo string
|
||||
if info.VirtualizationRole == "guest" {
|
||||
if info.VirtualizationSystem != "" {
|
||||
virtInfo = fmt.Sprintf("VM: %s", info.VirtualizationSystem)
|
||||
} else {
|
||||
virtInfo = "VM: unidentified"
|
||||
}
|
||||
}
|
||||
|
||||
// Add section.
|
||||
di.AddSection(
|
||||
fmt.Sprintf("Platform: %s %s", info.Platform, info.PlatformVersion),
|
||||
UseCodeSection|AddContentLineBreaks,
|
||||
fmt.Sprintf("System: %s %s (%s) %s", info.Platform, info.OS, info.PlatformFamily, info.PlatformVersion),
|
||||
fmt.Sprintf("Kernel: %s %s", info.KernelVersion, info.KernelArch),
|
||||
virtInfo,
|
||||
)
|
||||
}
|
||||
|
||||
// AddGoroutineStack adds the current goroutine stack.
|
||||
func (di *Info) AddGoroutineStack() {
|
||||
buf := new(bytes.Buffer)
|
||||
err := pprof.Lookup("goroutine").WriteTo(buf, 1)
|
||||
if err != nil {
|
||||
di.AddSection(
|
||||
"Goroutine Stack",
|
||||
NoFlags,
|
||||
fmt.Sprintf("Failed to get: %s", err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Add section.
|
||||
di.AddSection(
|
||||
"Goroutine Stack",
|
||||
UseCodeSection,
|
||||
buf.String(),
|
||||
)
|
||||
}
|
||||
|
||||
// AddLastReportedModuleError adds the last reported module error, if one exists.
|
||||
func (di *Info) AddLastReportedModuleError() {
|
||||
me := modules.GetLastReportedError()
|
||||
if me == nil {
|
||||
di.AddSection("No Module Error", NoFlags)
|
||||
return
|
||||
}
|
||||
|
||||
di.AddSection(
|
||||
"Module Error",
|
||||
UseCodeSection,
|
||||
me.Format(),
|
||||
)
|
||||
}
|
||||
|
||||
// AddLastUnexpectedLogs adds the last 10 unexpected log lines, if any.
|
||||
func (di *Info) AddLastUnexpectedLogs() {
|
||||
lines := log.GetLastUnexpectedLogs()
|
||||
|
||||
// Check if there is anything at all.
|
||||
if len(lines) == 0 {
|
||||
di.AddSection("No Unexpected Logs", NoFlags)
|
||||
return
|
||||
}
|
||||
|
||||
di.AddSection(
|
||||
"Unexpected Logs",
|
||||
UseCodeSection|AddContentLineBreaks,
|
||||
append(
|
||||
lines,
|
||||
fmt.Sprintf("%s CURRENT TIME", time.Now().Format("060102 15:04:05.000")),
|
||||
)...,
|
||||
)
|
||||
}
|
|
@ -11,6 +11,7 @@ var (
|
|||
nameOnly = regexp.MustCompile("^[A-Za-z0-9]+$")
|
||||
delimitersAtStart = regexp.MustCompile("^[^A-Za-z0-9]+")
|
||||
delimitersOnly = regexp.MustCompile("^[^A-Za-z0-9]+$")
|
||||
removeQuotes = strings.NewReplacer(`"`, ``, `'`, ``)
|
||||
)
|
||||
|
||||
// GenerateBinaryNameFromPath generates a more human readable binary name from
|
||||
|
@ -67,6 +68,11 @@ func GenerateBinaryNameFromPath(path string) string {
|
|||
func cleanFileDescription(fileDescr string) string {
|
||||
fields := strings.Fields(fileDescr)
|
||||
|
||||
// Clean out and `"` and `'`.
|
||||
for i := range fields {
|
||||
fields[i] = removeQuotes.Replace(fields[i])
|
||||
}
|
||||
|
||||
// If there is a 1 or 2 character delimiter field, only use fields before it.
|
||||
endIndex := len(fields)
|
||||
for i, field := range fields {
|
||||
|
|
|
@ -26,6 +26,8 @@ func TestCleanFileDescription(t *testing.T) {
|
|||
assert.Equal(t, "Product Name", cleanFileDescription("Product Name :: Does this and that."))
|
||||
assert.Equal(t, "/ Product Name", cleanFileDescription("/ Product Name"))
|
||||
assert.Equal(t, "Product", cleanFileDescription("Product / Name"))
|
||||
assert.Equal(t, "Software 2", cleanFileDescription("Software 2"))
|
||||
assert.Equal(t, "Launcher for Software 2", cleanFileDescription("Launcher for 'Software 2'"))
|
||||
assert.Equal(t, "", cleanFileDescription(". / Name"))
|
||||
assert.Equal(t, "", cleanFileDescription(". "))
|
||||
assert.Equal(t, "", cleanFileDescription("."))
|
||||
|
|
|
@ -13,7 +13,7 @@ func runPowershellCmd(script string) (output string, err error) {
|
|||
"powershell.exe",
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
script,
|
||||
"[System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8\n"+script,
|
||||
)
|
||||
|
||||
// Create and assign output buffers.
|
||||
|
|
Loading…
Add table
Reference in a new issue