Rename some functions

This commit is contained in:
Melon 2023-08-22 13:48:07 +01:00
parent 5e8f9c7e5e
commit 8051f9a017
Signed by: melon
GPG Key ID: 6C9D970C50D26A25
9 changed files with 11 additions and 638 deletions

View File

@ -1,6 +0,0 @@
package routing
type ServeHeaderItem interface {
HeaderKey() string
HeaderValue() string
}

View File

@ -1,262 +0,0 @@
package routing
import (
"code.mrmelon54.com/melon/summer-utils/tables/web"
"code.mrmelon54.com/melon/summer-utils/utils"
"errors"
"fmt"
"github.com/gorilla/mux"
"github.com/kjk/common/filerotate"
"log"
"net/http"
"net/http/httputil"
"sync"
"xorm.io/xorm"
)
var ErrAlreadyCompiling = errors.New("router: already compiling")
var theIncrementor uint64
type TableRouter struct {
router *mux.Router
db *xorm.Engine
proxy *httputil.ReverseProxy
rLock *sync.RWMutex // lock for router
cLock *sync.Mutex // lock for internalCompile
accessLog *log.Logger
fourEighteen func(rw http.ResponseWriter, req *http.Request)
}
func New(db *xorm.Engine, proxy *httputil.ReverseProxy, accessLog *filerotate.File, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) (*TableRouter, error) {
r := &TableRouter{
router: mux.NewRouter(),
db: db,
proxy: proxy,
rLock: &sync.RWMutex{},
cLock: &sync.Mutex{},
accessLog: log.New(accessLog, "", log.LstdFlags),
fourEighteen: fourEighteenError,
}
// run internalCompile inline to generate the initial router
err := r.internalCompile(r.router)
return r, err
}
func (t *TableRouter) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
// checks the router is not locked by Compile while swapping router
t.rLock.RLock()
theIncrementor++
z := theIncrementor
r := t.router
t.rLock.RUnlock()
t.accessLog.Printf("[TableRouter %d] Serving '%s' - '%s'\n", z, req.Host, req.URL.String())
r.ServeHTTP(rw, req)
t.accessLog.Printf("[TableRouter %d] Served request\n", z)
}
func (t *TableRouter) Compile() {
// start the goroutine and leave
go func() {
// create a new router and call internalCompile
router := mux.NewRouter()
err := t.internalCompile(router)
if err != nil {
// log compile errors
log.Printf("[TableRouter] Compile failed: %s\n", err)
return
}
// lock while replacing the router
t.rLock.Lock()
t.router = router
t.rLock.Unlock()
}()
}
func (t *TableRouter) internalCompile(router *mux.Router) error {
if !t.cLock.TryLock() {
return ErrAlreadyCompiling
}
defer t.cLock.Unlock()
router.NotFoundHandler = http.HandlerFunc(t.fourEighteen)
log.Println("[TableRouter] Pulling from database")
var (
apiDomain []web.ApiDomain
apiRoute []web.ApiRoute
httpDefaultHost []web.HttpDefaultHost
httpRoutes []web.HttpRoute
httpServices []web.HttpService
httpRedirects []web.HttpRedirect
httpRouteHeaders []web.HttpRouteHeader
httpServiceHeaders []web.HttpServiceHeader
)
if err := t.db.Find(&apiDomain, web.ApiDomain{Enabled: utils.PBool(true)}); err != nil {
return err
}
if err := t.db.Find(&apiRoute, web.ApiRoute{Enabled: utils.PBool(true)}); err != nil {
return err
}
if err := t.db.Find(&httpDefaultHost); err != nil {
return err
}
if err := t.db.Find(&httpRoutes, web.HttpRoute{Enabled: utils.PBool(true)}); err != nil {
return err
}
if err := t.db.Find(&httpServices, web.HttpService{Enabled: utils.PBool(true)}); err != nil {
return err
}
if err := t.db.Find(&httpRedirects, web.HttpRedirect{Enabled: utils.PBool(true)}); err != nil {
return err
}
if err := t.db.Find(&httpRouteHeaders); err != nil {
return err
}
if err := t.db.Find(&httpServiceHeaders); err != nil {
return err
}
log.Println("[TableRouter] Mapping default hosts")
mapDefaultHost := make(map[string]web.HttpDefaultHost)
for _, i := range httpDefaultHost {
if _, ok := mapDefaultHost[i.Key]; ok {
return fmt.Errorf("DefaultHost[%s] already exists", i.Key)
}
mapDefaultHost[i.Key] = i
}
log.Println("[TableRouter] Mapping api routes to domains")
mapApiRoute := make(map[uint64][]web.ApiRoute)
for _, i := range apiRoute {
if mapApiRoute[i.DomainId] == nil {
mapApiRoute[i.DomainId] = make([]web.ApiRoute, 0)
}
mapApiRoute[i.DomainId] = append(mapApiRoute[i.DomainId], i)
}
log.Println("[TableRouter] Mapping modified headers")
mapRouteHeaders := make(map[uint64][]ServeHeaderItem)
mapServiceHeaders := make(map[uint64][]ServeHeaderItem)
for _, i := range httpRouteHeaders {
if mapRouteHeaders[i.RouteId] == nil {
mapRouteHeaders[i.RouteId] = make([]ServeHeaderItem, 0)
}
mapRouteHeaders[i.RouteId] = append(mapRouteHeaders[i.RouteId], i)
}
for _, i := range httpServiceHeaders {
if mapServiceHeaders[i.ServiceId] == nil {
mapServiceHeaders[i.ServiceId] = make([]ServeHeaderItem, 0)
}
mapServiceHeaders[i.ServiceId] = append(mapServiceHeaders[i.ServiceId], i)
}
log.Println("[TableRouter] Adding api routes")
for _, i := range apiDomain {
routes := mapApiRoute[i.Id]
if routes == nil {
continue
}
subR := router.Host(i.Domain).Subrouter()
for _, j := range routes {
pre := fmt.Sprintf("/v%d/%s", j.Version, j.Route)
hostD, portD, pathD, _, refD, err := utils.ParseSymbolicHost(j.Dst)
if err != nil {
return fmt.Errorf("%w: '%s'", err, j.Dst)
}
if refD {
if err := resolveRef(mapDefaultHost, &hostD, &portD, &pathD); err != nil {
return err
}
}
subR.PathPrefix(pre).Handler(ServeRoute{Host: hostD, Port: portD, Path: pathD, PathPre: pre, SecureMode: j.IsSecureMode(), IgnoreCert: j.IsIgnoreCert(), Proxy: t.proxy, Cors: true})
}
}
log.Println("[TableRouter] Adding redirects")
for _, i := range httpRedirects {
host, _, path, flags, _, err := utils.ParseSymbolicHost(i.Src)
if err != nil {
return fmt.Errorf("%w: '%s'", err, i.Src)
}
hostD, portD, pathD, flagsD, _, err := utils.ParseSymbolicHost(i.Target)
z := router.Host(host)
if path != "" {
if utils.Contains[string](flags, "prefix") {
z = z.PathPrefix(path)
} else {
z = z.Path(path)
}
} else {
z = z.PathPrefix("/")
}
if portD != 0 {
hostD = fmt.Sprintf("%s:%d", hostD, portD)
}
z.Handler(ServeRedirect{Target: hostD, Path: pathD, Prefix: path, Flags: flagsD})
}
log.Println("[TableRouter] Adding routes")
for _, i := range httpRoutes {
hostS, _, pathS, _, _, err := utils.ParseSymbolicHost(i.Src)
if err != nil {
return fmt.Errorf("%w: '%s'", err, i.Src)
}
hostD, portD, pathD, _, refD, err := utils.ParseSymbolicHost(i.Dst)
if err != nil {
return fmt.Errorf("%w: '%s'", err, i.Dst)
}
if refD {
if err := resolveRef(mapDefaultHost, &hostD, &portD, &pathD); err != nil {
return err
}
}
z := router.Host(hostS)
if pathS != "" {
z = z.Path(pathS)
}
z.Handler(ServeRoute{Host: hostD, Port: portD, Path: pathD, Headers: mapRouteHeaders[i.Id], AbsPath: i.IsAbsPath(), SecureMode: i.IsSecureMode(), ForwardHost: i.IsForwardHost(), IgnoreCert: i.IsIgnoreCert(), Proxy: t.proxy})
}
log.Println("[TableRouter] Adding services")
for _, i := range httpServices {
hostD, portD, pathD, _, refD, err := utils.ParseSymbolicHost(i.Dst)
if err != nil {
return err
}
if refD {
if err := resolveRef(mapDefaultHost, &hostD, &portD, &pathD); err != nil {
return err
}
}
router.Host(i.Host).Handler(ServeRoute{Host: hostD, Port: portD, Path: pathD, Headers: mapServiceHeaders[i.Id], AbsPath: i.IsAbsPath(), SecureMode: i.IsSecureMode(), ForwardHost: i.IsForwardHost(), IgnoreCert: i.IsIgnoreCert(), Proxy: t.proxy})
}
// well I guess we are done
log.Println("[TableRouter] Finished compiling, updated routes will be available shortly")
return nil
}
func resolveRef(mapDefaultHost map[string]web.HttpDefaultHost, host *string, port *uint16, path *string) error {
if y, ok := mapDefaultHost[(*host)[1:]]; ok {
a1, a2, a3, _, _, err := utils.ParseSymbolicHost(y.Dst)
if err != nil {
return err
}
*host = a1
if *port == 0 {
*port = a2
}
if *path == "" {
*path = a3
}
}
return nil
}

View File

@ -1,59 +0,0 @@
package routing
import (
"code.mrmelon54.com/melon/summer-utils/utils"
"net/http"
"net/url"
"path"
"strings"
)
var redirectCodeMap = map[string]int{
"temp": http.StatusTemporaryRedirect,
"perm": http.StatusPermanentRedirect,
}
type ServeRedirect struct {
Target string
Path string
Prefix string
Flags []string
}
func (s ServeRedirect) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if s.Prefix == "/" {
s.Prefix = ""
}
var p2 string
if s.Path == "" {
p2 = req.URL.Path
} else {
if !strings.HasPrefix(req.URL.Path, s.Prefix) {
utils.RespondHttpStatus(rw, http.StatusExpectationFailed)
return
}
p2 = path.Join(s.Path, req.URL.Path[len(s.Prefix):])
if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(p2, "/") {
p2 += "/"
}
}
if !strings.HasPrefix(p2, "/") {
p2 = "/" + p2
}
u := url.URL{Scheme: "https", Host: s.Target, Path: p2}
// Update temp/perm status code with flag
c := http.StatusFound
if s.Flags != nil {
outerLoop:
for k, v := range redirectCodeMap {
for _, i := range s.Flags {
if k == i {
c = v
break outerLoop
}
}
}
}
http.Redirect(rw, req, u.String(), c)
}

View File

@ -1,78 +0,0 @@
package routing
import (
"github.com/stretchr/testify/assert"
httpTest "github.com/stretchr/testify/http"
"net/http"
"testing"
)
func TestServeRedirect_Location(t *testing.T) {
redirectLocationCheck(t, "https://b.example.com/", "https://a.example.com", ServeRedirect{
Target: "b.example.com",
Path: "/",
Prefix: "/",
Flags: nil,
})
redirectLocationCheck(t, "https://b.example.com/", "https://a.example.com", ServeRedirect{
Target: "b.example.com",
Path: "",
Prefix: "/",
Flags: nil,
})
redirectLocationCheck(t, "https://b.example.com/world", "https://a.example.com/hello", ServeRedirect{
Target: "b.example.com",
Path: "/world",
Prefix: "/hello",
Flags: nil,
})
redirectLocationCheck(t, "https://b.example.com/goodbye/world", "https://a.example.com/hello/world", ServeRedirect{
Target: "b.example.com",
Path: "/goodbye",
Prefix: "/hello",
Flags: []string{"prefix"},
})
}
func TestServeRedirect_StatusCode(t *testing.T) {
redirectCodeCheck(t, http.StatusFound, ServeRedirect{
Target: "a.example.com",
Path: "",
Prefix: "",
Flags: nil,
})
redirectCodeCheck(t, http.StatusFound, ServeRedirect{
Target: "a.example.com",
Path: "",
Prefix: "",
Flags: []string{},
})
redirectCodeCheck(t, http.StatusTemporaryRedirect, ServeRedirect{
Target: "a.example.com",
Path: "",
Prefix: "",
Flags: []string{"temp"},
})
redirectCodeCheck(t, http.StatusPermanentRedirect, ServeRedirect{
Target: "a.example.com",
Path: "",
Prefix: "",
Flags: []string{"perm"},
})
}
func redirectLocationCheck(t *testing.T, expected string, input string, serve ServeRedirect) {
rw := &httpTest.TestResponseWriter{}
req, err := http.NewRequest(http.MethodGet, input, nil)
assert.NoError(t, err)
serve.ServeHTTP(rw, req)
assert.Equal(t, expected, rw.Header().Get("Location"))
}
func redirectCodeCheck(t *testing.T, expected int, serve ServeRedirect) {
rw := &httpTest.TestResponseWriter{}
req, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
assert.NoError(t, err)
serve.ServeHTTP(rw, req)
assert.Equal(t, expected, rw.StatusCode)
}

View File

@ -1,126 +0,0 @@
package routing
import (
"bytes"
"code.mrmelon54.com/melon/azalea/proxy"
"code.mrmelon54.com/melon/summer-utils/utils"
"fmt"
"github.com/gorilla/handlers"
"io"
"log"
"net/http"
"net/url"
"path"
"strings"
)
var serveApiCors = handlers.CORS(
handlers.AllowCredentials(),
handlers.AllowedOrigins([]string{"*"}),
handlers.AllowedHeaders([]string{"Content-Type", "Authorization"}),
handlers.AllowedMethods([]string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodConnect,
http.MethodOptions,
http.MethodTrace,
}),
)
type ServeRoute struct {
Host string
Port uint16
Path string
PathPre string // prefix from http route specific call
Headers []ServeHeaderItem
Proxy http.Handler
AbsPath bool
SecureMode bool
ForwardHost bool
IgnoreCert bool
Cors bool
}
func (s ServeRoute) IsIgnoreCert() bool { return s.IgnoreCert }
func (s ServeRoute) UpdateHeaders(header http.Header) {
for _, i := range s.Headers {
header.Set(i.HeaderKey(), i.HeaderValue())
}
}
func (s ServeRoute) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
if s.Cors {
serveApiCors(http.HandlerFunc(s.internalServeHTTP)).ServeHTTP(rw, req)
} else {
s.internalServeHTTP(rw, req)
}
}
func (s ServeRoute) internalServeHTTP(rw http.ResponseWriter, req *http.Request) {
scheme := "http"
if s.SecureMode {
scheme = "https"
if s.Port == 0 {
s.Port = 443
}
} else {
if s.Port == 0 {
s.Port = 80
}
}
var p1 string
if s.AbsPath {
p1 = path.Join(s.Path)
} else {
if !strings.HasPrefix(req.URL.Path, s.PathPre) {
utils.RespondHttpStatus(rw, http.StatusExpectationFailed)
return
}
p1 = path.Join(s.Path, req.URL.Path[len(s.PathPre):])
// add suffix "/" to prevent redirect issues
// the path.Join() above calls path.Clean() and strips the trailing slash
// so this just adds the trailing slash back in
if strings.HasSuffix(req.URL.Path, "/") && !strings.HasSuffix(p1, "/") {
p1 += "/"
}
}
// If path is empty then correct to /
if p1 == "" {
p1 = "/"
}
buf := new(bytes.Buffer)
if req.Body != nil {
_, _ = io.Copy(buf, req.Body)
}
u := url.URL{
Scheme: scheme,
Host: fmt.Sprintf("%s:%d", s.Host, s.Port),
Path: p1,
RawQuery: req.URL.RawQuery,
}
req2, err := http.NewRequest(req.Method, u.String(), buf)
if err != nil {
log.Printf("[ServeRoute::ServerHTTP()] Error generating new request: %s\n", err)
utils.RespondHttpStatus(rw, http.StatusBadGateway)
return
}
for k, v := range req.Header {
if k == "Host" {
continue
}
req2.Header[k] = v
}
if s.ForwardHost {
req2.Host = req.Host
}
s.Proxy.ServeHTTP(rw, proxy.SetReverseProxyHost(req2, s))
}

View File

@ -1,105 +0,0 @@
package routing
import (
"bytes"
"github.com/stretchr/testify/assert"
httpTest "github.com/stretchr/testify/http"
"net/http"
"testing"
)
var routeLoopProxy = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Location", req.URL.String())
rw.Header().Set("Host", req.Host)
})
func TestServeRoute_Location(t *testing.T) {
// Minimal test
routeLocationCheck(t, "http://localhost:8080/", "", "https://a.example.com", ServeRoute{
Host: "localhost",
Port: 8080,
})
routeLocationCheck(t, "http://localhost:80/goodbye/world", "", "https://a.example.com/world", ServeRoute{
Host: "localhost",
Port: 0,
Path: "/goodbye",
})
// ForwardHost test
routeLocationCheck(t, "http://localhost:80/", "a.example.com", "https://a.example.com", ServeRoute{
Host: "localhost",
Port: 0,
ForwardHost: true,
})
routeLocationCheck(t, "http://localhost:8080/goodbye/world", "a.example.com", "https://a.example.com/world", ServeRoute{
Host: "localhost",
Port: 8080,
Path: "/goodbye",
ForwardHost: true,
})
// SecureMode test
routeLocationCheck(t, "https://localhost:443/", "a.example.com", "https://a.example.com", ServeRoute{
Host: "localhost",
Port: 0,
ForwardHost: true,
SecureMode: true,
})
routeLocationCheck(t, "https://localhost:8443/goodbye/world", "a.example.com", "https://a.example.com/world", ServeRoute{
Host: "localhost",
Port: 8443,
Path: "/goodbye",
ForwardHost: true,
SecureMode: true,
})
// AbsPath test
routeLocationCheck(t, "http://localhost:80/", "a.example.com", "https://a.example.com", ServeRoute{
Host: "localhost",
Port: 0,
ForwardHost: true,
AbsPath: true,
})
routeLocationCheck(t, "http://localhost:8080/goodbye", "a.example.com", "https://a.example.com/world", ServeRoute{
Host: "localhost",
Port: 8080,
Path: "/goodbye",
ForwardHost: true,
AbsPath: true,
})
// TrailingSlash test
routeLocationCheck(t, "http://localhost:80/", "a.example.com", "https://a.example.com/", ServeRoute{
Host: "localhost",
Port: 0,
ForwardHost: true,
})
routeLocationCheck(t, "http://localhost:8080/goodbye/world/", "a.example.com", "https://a.example.com/world/", ServeRoute{
Host: "localhost",
Port: 8080,
Path: "/goodbye",
ForwardHost: true,
})
// Query test
routeLocationCheck(t, "http://localhost:80/?raw=true", "a.example.com", "https://a.example.com/?raw=true", ServeRoute{
Host: "localhost",
Port: 0,
ForwardHost: true,
})
routeLocationCheck(t, "http://localhost:8080/goodbye/world/?raw=true", "a.example.com", "https://a.example.com/world/?raw=true", ServeRoute{
Host: "localhost",
Port: 8080,
Path: "/goodbye",
ForwardHost: true,
})
}
func routeLocationCheck(t *testing.T, expected, host, input string, serve ServeRoute) {
serve.Proxy = routeLoopProxy
rw := &httpTest.TestResponseWriter{}
req, err := http.NewRequest(http.MethodGet, input, new(bytes.Buffer))
assert.NoError(t, err)
serve.ServeHTTP(rw, req)
assert.Equal(t, expected, rw.Header().Get("Location"))
}

View File

@ -19,7 +19,7 @@ import (
"xorm.io/xorm"
)
func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.TableRouter, domainCheck *domainChecker.DomainChecker, faviconMap *favicons.Favicons, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) *http.Server {
func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.SyncRouter, domainCheck *domainChecker.DomainChecker, faviconMap *favicons.Favicons, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) *http.Server {
router := mux.NewRouter()
setupHttpsRouter(router, tableRouter, domainCheck, faviconMap, conf, fourEighteenError)
@ -63,7 +63,7 @@ func NewHttpsServer(listen string, db *xorm.Engine, tableRouter *routing.TableRo
return s
}
func setupHttpsRouter(router *mux.Router, tableRouter *routing.TableRouter, domainCheck *domainChecker.DomainChecker, faviconMap *favicons.Favicons, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) {
func setupHttpsRouter(router *mux.Router, tableRouter *routing.SyncRouter, domainCheck *domainChecker.DomainChecker, faviconMap *favicons.Favicons, conf config.AzaleaConfig, fourEighteenError func(rw http.ResponseWriter, req *http.Request)) {
faviconColor := favicon.NewColor()
router.Use(setupRateLimiter(conf.RateLimit))

3
go.mod
View File

@ -5,6 +5,7 @@ go 1.20
require (
code.mrmelon54.com/melon/certgen v0.0.0-20220830133534-0fb4cb7e67d1
code.mrmelon54.com/melon/summer-utils v0.0.1
github.com/gbrlsnchs/radix v1.0.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/kjk/common v0.0.0-20220705191345-4e210bd3860d
@ -14,6 +15,7 @@ require (
github.com/mrmelon54/png2ico v1.0.0
github.com/sethvargo/go-limiter v0.7.2
github.com/stretchr/testify v1.8.2
github.com/yousuf64/shift v0.4.0
golang.org/x/net v0.9.0
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
xorm.io/xorm v1.3.2
@ -29,6 +31,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/gbrlsnchs/color v0.1.0 // indirect
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81 // indirect
github.com/go-sql-driver/mysql v1.7.0 // indirect
github.com/goccy/go-json v0.8.1 // indirect

6
go.sum
View File

@ -111,6 +111,10 @@ github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUv
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/gbrlsnchs/color v0.1.0 h1:kqGI5bcsfjpkIhVL1g4IpuCA5DL+lnF1mFy8XZ7ffKg=
github.com/gbrlsnchs/color v0.1.0/go.mod h1:DqmJ75IHg1obs9e8r0r7Q691hcywJBRUYtbxu/rBuWg=
github.com/gbrlsnchs/radix v1.0.0 h1:z0HafxDLhD8mWXiVb3Zo1TB9zJerNNIKBb+BMr071PQ=
github.com/gbrlsnchs/radix v1.0.0/go.mod h1:Rv0ueYu+grgwp1/ea0UUKeUlqMOqRXzf/3Ud2hFLB+E=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-fonts/dejavu v0.1.0 h1:JSajPXURYqpr+Cu8U9bt8K+XcACIHWqWrvWCKyeFmVQ=
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
@ -497,6 +501,8 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yousuf64/shift v0.4.0 h1:jiuYPa9KGyTShM08GEcSRSaZeO2jPEIpdqaplsRtk8g=
github.com/yousuf64/shift v0.4.0/go.mod h1:D9b+mj37s3goL48EGX2oCrYHW4rg4c3Il2w6fukjxas=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=