1
0
mirror of https://github.com/greenpau/caddy-security.git synced 2025-04-18 08:04:02 +03:00

feature: trusted logout redirect uri directives

This commit is contained in:
Paul Greenberg 2024-03-14 14:12:12 -04:00
parent 774cb2bb60
commit e564acb917
10 changed files with 363 additions and 68 deletions

View File

@ -67,10 +67,16 @@ module github.com/greenpau/caddy-security
go 1.16
require (
github.com/greenpau/go-authcrunch v1.0.40
github.com/greenpau/go-authcrunch v1.0.49
)
replace github.com/greenpau/go-authcrunch v1.0.40 => /home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
replace github.com/greenpau/go-authcrunch v1.0.49 => /home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
```
Alternatively:
```bash
go mod edit -replace github.com/greenpau/go-authcrunch@v1.0.49=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch@v1.0.48
```
Then, modify `Makefile` such that that replacement passes to `xcaddy` builder:
@ -79,7 +85,7 @@ Then, modify `Makefile` such that that replacement passes to `xcaddy` builder:
@mkdir -p ../xcaddy-$(PLUGIN_NAME) && cd ../xcaddy-$(PLUGIN_NAME) && \
xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/caddy \
--with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR) \
--with github.com/greenpau/go-authcrunch@v1.0.40=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
--with github.com/greenpau/go-authcrunch@v1.0.49=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
```
Once all the necessary packages are installed, you should be ready to compile

View File

@ -8,78 +8,93 @@ BUILD_DATE:=$(shell date +"%Y-%m-%d")
BUILD_DIR:=$(shell pwd)
CADDY_VERSION="v2.7.5"
all: info
@mkdir -p bin/
@rm -rf ./bin/authcrunch
@#rm -rf ../xcaddy-$(PLUGIN_NAME)/*
@#mkdir -p ../xcaddy-$(PLUGIN_NAME) && cd ../xcaddy-$(PLUGIN_NAME) &&
@# xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/caddy
@# --with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR)
@# --with github.com/greenpau/caddy-trace@latest
@#--with github.com/greenpau/go-authcrunch@v1.0.40=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
@go build -v -o ./bin/authcrunch cmd/authcrunch/main.go;
@./bin/authcrunch version
@#bin/caddy run -config assets/config/Caddyfile
@for f in `find ./assets -type f -name 'Caddyfile'`; do bin/authcrunch fmt --overwrite $$f; done
all: info build
@echo "$@: complete"
.PHONY: info
info:
@echo "DEBUG: Version: $(PLUGIN_VERSION), Branch: $(GIT_BRANCH), Revision: $(GIT_COMMIT)"
@echo "DEBUG: Build on $(BUILD_DATE) by $(BUILD_USER)"
.PHONY: build
build:
@mkdir -p bin/
@rm -rf ./bin/authcrunch
@go build -v -o ./bin/authcrunch cmd/authcrunch/main.go;
@./bin/authcrunch version
@for f in `find ./assets -type f -name 'Caddyfile'`; do bin/authcrunch fmt --overwrite $$f; done
@echo "$@: complete"
.PHONY: devbuild
devbuild:
@mkdir -p bin/
@rm -rf ./bin/authcrunch
@rm -rf ../xcaddy-$(PLUGIN_NAME)/*
@mkdir -p ../xcaddy-$(PLUGIN_NAME) && cd ../xcaddy-$(PLUGIN_NAME) && \
xcaddy build $(CADDY_VERSION) --output ../$(PLUGIN_NAME)/bin/authcrunch \
--with github.com/greenpau/caddy-security@$(LATEST_GIT_COMMIT)=$(BUILD_DIR) \
--with github.com/greenpau/caddy-trace@latest \
--with github.com/greenpau/go-authcrunch@v1.0.49=/home/greenpau/dev/go/src/github.com/greenpau/go-authcrunch
@go build -v -o ./bin/authcrunch cmd/authcrunch/main.go;
@./bin/authcrunch version
@echo "$@: complete"
.PHONY: linter
linter:
@echo "DEBUG: started $@"
@echo "$@: started"
@#golint -set_exit_status ./...
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: test
test: covdir linter
@echo "$@: started"
@echo "DEBUG: started $@"
@go test -v -coverprofile=.coverage/coverage.out ./...
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: ctest
ctest: covdir linter
@echo "$@: started"
@echo "DEBUG: started $@"
@#time richgo test -v -coverprofile=.coverage/coverage.out ./...
@time richgo test -v -coverprofile=.coverage/coverage.out ./*.go
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: covdir
covdir:
@echo "DEBUG: started $@"
@echo "$@: started"
@mkdir -p .coverage
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: bindir
bindir:
@echo "DEBUG: started $@"
@echo "$@: started"
@mkdir -p bin/
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: coverage
coverage: covdir
@echo "DEBUG: started $@"
@echo "$@: started"
@go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html
@go test -covermode=count -coverprofile=.coverage/coverage.out ./...
@go tool cover -func=.coverage/coverage.out | grep -v "100.0"
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: clean
clean:
@echo "DEBUG: started $@"
@echo "$@: started"
@rm -rf .coverage/
@rm -rf bin/
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: qtest
qtest: covdir
@echo "DEBUG: started $@"
@echo "$@: started"
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestApp ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAppConfig ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileIdentity ./*.go
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileSingleSignOnProvider ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileSingleSignOnProvider ./*.go
@time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileAuthenticationMisc ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileCredentials ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileMessaging ./*.go
@#time richgo test -v -coverprofile=.coverage/coverage.out -run TestParseCaddyfileIdentit* ./*.go
@ -89,21 +104,21 @@ qtest: covdir
@#go test -v -coverprofile=.coverage/coverage.out -run Test* ./pkg/services/...
@go tool cover -html=.coverage/coverage.out -o .coverage/coverage.html
@go tool cover -func=.coverage/coverage.out | grep -v "100.0"
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: dep
dep:
@echo "DEBUG: started $@"
@echo "$@: started"
@go install golang.org/x/lint/golint@latest
@go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
@#go install github.com/goreleaser/goreleaser@latest
@go install github.com/greenpau/versioned/cmd/versioned@latest
@go install github.com/kyoh86/richgo@latest
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: release
release:
@echo "Making release"
@echo "$@: started"
@go mod tidy;
@go mod verify;
@if [ $(GIT_BRANCH) != "main" ]; then echo "cannot release to non-main branch $(GIT_BRANCH)" && false; fi
@ -119,20 +134,22 @@ release:
@@echo "If necessary, run the following commands:"
@echo " git push --delete origin v$(PLUGIN_VERSION)"
@echo " git tag --delete v$(PLUGIN_VERSION)"
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: logo
logo:
@echo "$@: started"
@mkdir -p assets/docs/images
@gm convert -background black -font Bookman-Demi \
-size 640x320 "xc:black" \
-pointsize 72 \
-draw "fill white gravity center text 0,0 'caddy\nsecurity'" \
assets/docs/images/logo.png
@echo "DEBUG: completed $@"
@echo "$@: complete"
.PHONY: license
license:
@echo "$@: started"
@for f in `find ./ -type f -name '*.go'`; do versioned -addlicense -copyright="Paul Greenberg greenpau@outlook.com" -year=2022 -filepath=$$f; done
@assets/scripts/generate_downloads.sh
@echo "DEBUG: completed $@"
@echo "$@: complete"

View File

@ -3,7 +3,7 @@
local_certs
http_port 8080
https_port 8443
admin localhost:2999
admin off
security {
credentials root@localhost {
@ -22,6 +22,24 @@
local identity store localdb {
realm local
path assets/config/users.json
user webadmin {
name Webmaster
email webadmin@localhost.localdomain
password "$2a$10$VLCDIncXaRFshFTGcz2aP.q.gR0O6y1i6mVDks/7WmE3JKLjPD.wu" overwrite
roles authp/admin authp/user
}
user jsmith {
name John Smith
email jsmith@localhost.localdomain
password "My@Password123"
roles "authp/user" "dash"
}
user mstone {
name Mia Stone
email mstone@localhost.localdomain
password "My@Password123"
roles "authp/user" "dash"
}
}
user registration localdbRegistry {
@ -50,6 +68,7 @@
action add role authp/user
ui link "Portal Settings" /auth/settings icon "las la-cog"
}
trust logout redirect uri domain prefix google path suffix /
}
authorization policy mypolicy {
@ -69,11 +88,15 @@
authenticate * with myportal
}
route /favicon.ico {
respond "not found" 404
}
route /xauth* {
authenticate * with myportal
}
route /app {
route /app* {
authorize with mypolicy
file_server {
root ./assets/config

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<h1>App</h1>
<p><a href="https://localhost:8443/auth/logout?redirect_uri=https://google.com/">logout with redirect</a></p>
</body>
</html>

View File

@ -36,37 +36,39 @@ const (
//
// Syntax:
//
// authentication portal <name> {
// authentication portal <name> {
//
// crypto key sign-verify <shared_secret>
// crypto key sign-verify <shared_secret>
//
// ui {
// template <login|portal> <file_path>
// logo_url <file_path|url_path>
// logo_description <value>
// custom css path <path>
// custom js path <path>
// custom html header path <path>
// static_asset <uri> <content_type> <path>
// allow settings for role <role>
// }
// ui {
// template <login|portal> <file_path>
// logo_url <file_path|url_path>
// logo_description <value>
// custom css path <path>
// custom js path <path>
// custom html header path <path>
// static_asset <uri> <content_type> <path>
// allow settings for role <role>
// }
//
// cookie domain <name>
// cookie path <name>
// cookie lifetime <seconds>
// cookie samesite <lax|strict|none>
// cookie insecure <on|off>
// cookie domain <name>
// cookie path <name>
// cookie lifetime <seconds>
// cookie samesite <lax|strict|none>
// cookie insecure <on|off>
//
// validate source address
// validate source address
//
// enable source ip tracking
// enable admin api
// enable identity store <name>
// enable identity provider <name>
// enable sso provider <name>
// enable user registration <name>
// }
// enable source ip tracking
// enable admin api
// enable identity store <name>
// enable identity provider <name>
// enable sso provider <name>
// enable user registration <name>
//
// trust logout redirect uri domain [exact|partial|prefix|suffix|regex] <domain_name> path [exact|partial|prefix|suffix|regex] <path>
//
// }
func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer, cfg *authcrunch.Config) error {
// rootDirective is config key prefix.
var rootDirective string
@ -109,7 +111,7 @@ func parseCaddyfileAuthentication(d *caddyfile.Dispenser, repl *caddy.Replacer,
if err := parseCaddyfileAuthPortalTransform(d, repl, p, rootDirective, v); err != nil {
return err
}
case "enable", "validate":
case "enable", "validate", "trust":
if err := parseCaddyfileAuthPortalMisc(d, repl, p, rootDirective, k, v); err != nil {
return err
}

View File

@ -18,6 +18,7 @@ import (
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/greenpau/go-authcrunch/pkg/authn"
"github.com/greenpau/go-authcrunch/pkg/redirects"
"strings"
)
@ -69,6 +70,52 @@ func parseCaddyfileAuthPortalMisc(h *caddyfile.Dispenser, repl *caddy.Replacer,
default:
return h.Errf("%s directive %q is unsupported", rootDirective, v)
}
case "trust":
switch {
case strings.Contains(v, "logout redirect uri"):
var domainMatchType, domain, pathMatchType, path string
argp := 3
for argp < len(args) {
switch args[argp] {
case "domain", "path":
if hasMatchTypeKeywords(args[argp+1]) {
if !arrayElementExists(args, argp+2) {
return h.Errf("%s directive %q is malformed", rootDirective, v)
}
if args[argp] == "domain" {
domainMatchType = args[argp+1]
domain = args[argp+2]
} else {
pathMatchType = args[argp+1]
path = args[argp+2]
}
argp++
} else {
if args[argp] == "domain" {
domain = args[argp+1]
domainMatchType = "exact"
} else {
path = args[argp+1]
pathMatchType = "exact"
}
}
argp++
default:
return h.Errf("%s directive %q has unsupported key %s", rootDirective, v, args[argp])
}
argp++
}
redirectURIConfig, err := redirects.NewRedirectURIMatchConfig(domainMatchType, domain, pathMatchType, path)
if err != nil {
return h.Errf("%s directive %q erred: %v", rootDirective, v, err)
}
portal.TrustedLogoutRedirectURIConfigs = append(portal.TrustedLogoutRedirectURIConfigs, redirectURIConfig)
case v == "":
return h.Errf("%s directive has no value", rootDirective)
default:
return h.Errf("%s directive %q is unsupported", rootDirective, v)
}
}
return nil
}

View File

@ -0,0 +1,134 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
import (
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
"github.com/google/go-cmp/cmp"
"github.com/tidwall/gjson"
"os"
"path"
"testing"
)
func TestParseCaddyfileAuthenticationMisc(t *testing.T) {
localDbEnvVar := "TMP_LOCAL_DB_PATH"
glbTmpDir := path.Join(os.TempDir(), "tmp-caddy-security")
err := os.Mkdir(glbTmpDir, 0750)
if err != nil && !os.IsExist(err) {
t.Fatal(err)
}
tmpDir, err := os.MkdirTemp(glbTmpDir, "caddyfile-tests")
if err != nil {
t.Fatalf("failed creating temporary directory: %v", err)
}
localDbPath := path.Join(tmpDir, "users.json")
t.Logf("Local database path: %s", localDbPath)
defer os.RemoveAll(tmpDir)
testcases := []struct {
name string
d *caddyfile.Dispenser
want string
shouldErr bool
err error
}{
{
name: "test valid authentication portal config with valid trust logout redirect uri",
d: caddyfile.NewTestDispenser(`
security {
local identity store localdb {
realm local
path {env.TMP_LOCAL_DB_PATH}
user webadmin {
name Webmaster
email webadmin@localhost.localdomain
# echo -n 'Td45@4d269b7ec2f5ffd31ee5' | bcrypt-cli -c 10
password "$2a$10$VLCDIncXaRFshFTGcz2aP.q.gR0O6y1i6mVDks/7WmE3JKLjPD.wu" overwrite
roles authp/admin authp/user
}
}
authentication portal myportal {
enable identity store localdb
trust logout redirect uri domain authcrunch.com path /foo/bar
trust logout redirect uri domain prefix authcrunch path suffix /foo
}
}`),
want: `{ "want": [
{
"cookie_config": {},
"identity_stores": ["localdb"],
"name": "myportal",
"token_grantor_options": {},
"trusted_logout_redirect_uri_configs": [
{
"domain": "authcrunch.com",
"domain_match_type": "exact",
"path": "/foo/bar",
"path_match_type": "exact"
},
{
"domain": "authcrunch",
"domain_match_type": "prefix",
"path": "/foo",
"path_match_type": "suffix"
}
],
"token_validator_options": {},
"ui": {}
}
]}`,
},
}
t.Setenv(localDbEnvVar, localDbPath)
defer os.Unsetenv(localDbEnvVar)
for _, tc := range testcases {
defer os.Unsetenv(localDbPath)
t.Run(tc.name, func(t *testing.T) {
app, err := parseCaddyfile(tc.d, nil)
if err != nil {
if !tc.shouldErr {
t.Fatalf("expected success, got: %v", err)
}
if diff := cmp.Diff(err.Error(), tc.err.Error()); diff != "" {
t.Fatalf("unexpected error: %v, want: %v", err, tc.err)
}
return
}
if tc.shouldErr {
t.Fatalf("unexpected success, want: %v", tc.err)
}
tmpGot := gjson.Get(string(app.(httpcaddyfile.App).Value), "config.authentication_portals")
tmpWant := gjson.Get(tc.want, "want")
got := unpack(t, "{\"config\": "+tmpGot.String()+"}")
want := unpack(t, "{\"config\": "+tmpWant.String()+"}")
if diff := cmp.Diff(want, got); diff != "" {
t.Logf("JSON: %v", string(app.(httpcaddyfile.App).Value))
t.Errorf("TestParseCaddyfileAuthenticationMisc() mismatch (-want +got):\n%s", diff)
}
})
}
}

43
caddyfile_utils.go Normal file
View File

@ -0,0 +1,43 @@
// Copyright 2022 Paul Greenberg greenpau@outlook.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package security
func hasWord(s string, arr []string) bool {
for _, el := range arr {
if s == el {
return true
}
}
return false
}
func hasMatchTypeKeywords(s string) bool {
keywords := []string{"exact", "partial", "prefix", "suffix", "regex"}
return hasWord(s, keywords)
}
func lastArrayElement(args []string, argp int) bool {
if (len(args) - 1) == argp {
return true
}
return false
}
func arrayElementExists(args []string, argp int) bool {
if len(args) > argp {
return true
}
return false
}

5
go.mod
View File

@ -7,7 +7,8 @@ require (
github.com/google/go-cmp v0.6.0
github.com/google/uuid v1.6.0
github.com/greenpau/caddy-trace v1.1.13
github.com/greenpau/go-authcrunch v1.0.48
github.com/greenpau/go-authcrunch v1.0.49
github.com/tidwall/gjson v1.17.1
go.uber.org/zap v1.27.0
)
@ -119,6 +120,8 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/urfave/cli v1.22.14 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/yuin/goldmark v1.5.6 // indirect

10
go.sum
View File

@ -181,8 +181,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
github.com/greenpau/caddy-trace v1.1.13 h1:sQveqzDt+O1/ZpfpJddXJ5S2UHtcvSVDKZRgZ9wGo/k=
github.com/greenpau/caddy-trace v1.1.13/go.mod h1:K6lD24evCjgCFqz8KYJbXMvMAQ8wFEG0+Z4EEqDU1dA=
github.com/greenpau/go-authcrunch v1.0.48 h1:91ZrZ8y/Iiz/Ub+H3gR1QSXaTaitsq6JvQnmmNjtTWI=
github.com/greenpau/go-authcrunch v1.0.48/go.mod h1:aBk4NeGAUCcKrySpGtviflt/oHzR08cXSISDf/ABmW0=
github.com/greenpau/go-authcrunch v1.0.49 h1:QAgILZcE3e2kGflqUlO/OkGMu2wyBcwXwAMITl/Iho0=
github.com/greenpau/go-authcrunch v1.0.49/go.mod h1:aBk4NeGAUCcKrySpGtviflt/oHzR08cXSISDf/ABmW0=
github.com/greenpau/versioned v1.0.30 h1:QILUlfTSyJnhT8Gw9lLonZmuP5ahNQoJizw7mo30IQ4=
github.com/greenpau/versioned v1.0.30/go.mod h1:rtFCvaWWNbMH4CJnje/xicgmrM63j++rUh5juSu0k/A=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.0 h1:RtRsiaGvWxcwd8y3BiRZxsylPT8hLWZ5SPcfI+3IDNk=
@ -420,6 +420,12 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046 h1:8rUlviSVOEe7TMk7W0gIPrW8MqEzYfZHpsNWSf8s2vg=
github.com/tailscale/tscert v0.0.0-20230806124524-28a91b69a046/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=