From 453123a1c3572b18ac8aa35f47af76f71a333a79 Mon Sep 17 00:00:00 2001 From: Dennis Trautwein Date: Sat, 22 Oct 2022 19:10:06 +0100 Subject: [PATCH] Add GUI punchr --- cmd/client/cmd.go | 159 +-------------- go.mod | 18 ++ go.sum | 62 +++++- gui/client/client.go | 182 +++++++++++++++++ gui/client/glove-active.png | Bin 0 -> 32905 bytes gui/client/glove-inactive.png | Bin 0 -> 15779 bytes pkg/client/app.go | 91 +++++++++ pkg/client/cmd.go | 190 ++++++++++++++++++ {cmd => pkg}/client/host.go | 2 +- {cmd => pkg}/client/punchr.go | 5 +- {cmd => pkg}/client/rcmgr.go | 2 +- {cmd => pkg}/client/state.go | 2 +- .../000015_create_routers_table.up.sql | 2 +- pkg/models/routers.go | 6 +- pkg/models/routers_test.go | 2 +- 15 files changed, 555 insertions(+), 168 deletions(-) create mode 100644 gui/client/client.go create mode 100644 gui/client/glove-active.png create mode 100644 gui/client/glove-inactive.png create mode 100644 pkg/client/app.go create mode 100644 pkg/client/cmd.go rename {cmd => pkg}/client/host.go (99%) rename {cmd => pkg}/client/punchr.go (99%) rename {cmd => pkg}/client/rcmgr.go (99%) rename {cmd => pkg}/client/state.go (99%) diff --git a/cmd/client/cmd.go b/cmd/client/cmd.go index ef9be2b..4500fad 100644 --- a/cmd/client/cmd.go +++ b/cmd/client/cmd.go @@ -2,178 +2,23 @@ package main import ( "context" - "fmt" - "net/http" _ "net/http/pprof" "os" "os/signal" "syscall" - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus/promhttp" log "github.com/sirupsen/logrus" - "github.com/urfave/cli/v2" -) -var ( - version = "dev" // set via goreleaser - commit = "" // set via goreleaser + "github.com/dennis-tra/punchr/pkg/client" ) func main() { - shortCommit := commit - if len(shortCommit) > 7 { - shortCommit = shortCommit[:7] - } - - app := &cli.App{ - Name: "punchrclient", - Usage: "A libp2p host that is capable of DCUtR.", - UsageText: "punchrclient [global options] command [command options] [arguments...]", - Action: RootAction, - Version: fmt.Sprintf("%s+%s", version, shortCommit), - Flags: []cli.Flag{ - &cli.StringFlag{ - Name: "telemetry-host", - Usage: "To which network address should the telemetry (prometheus, pprof) server bind", - EnvVars: []string{"PUNCHR_CLIENT_TELEMETRY_HOST"}, - Value: "localhost", - DefaultText: "localhost", - }, - &cli.StringFlag{ - Name: "telemetry-port", - Usage: "On which port should the telemetry (prometheus, pprof) server listen", - EnvVars: []string{"PUNCHR_CLIENT_TELEMETRY_PORT"}, - Value: "12001", - DefaultText: "12001", - }, - &cli.StringFlag{ - Name: "server-host", - Usage: "Where does the the punchr server listen", - EnvVars: []string{"PUNCHR_CLIENT_SERVER_HOST"}, - Value: "punchr.dtrautwein.eu", - DefaultText: "punchr.dtrautwein.eu", - }, - &cli.StringFlag{ - Name: "server-port", - Usage: "On which port listens the punchr server", - EnvVars: []string{"PUNCHR_CLIENT_SERVER_PORT"}, - Value: "443", - DefaultText: "443", - }, - &cli.BoolFlag{ - Name: "server-ssl", - Usage: "Whether or not to use a SSL connection to the server.", - EnvVars: []string{"PUNCHR_CLIENT_SERVER_SSL"}, - Value: true, - DefaultText: "true", - }, - &cli.BoolFlag{ - Name: "server-ssl-skip-verify", - Usage: "Whether or not to skip SSL certificate verification.", - EnvVars: []string{"PUNCHR_CLIENT_SERVER_SSL_SKIP_VERIFY"}, - Value: false, - DefaultText: "false", - }, - &cli.IntFlag{ - Name: "host-count", - Usage: "How many libp2p hosts should be used to hole punch", - EnvVars: []string{"PUNCHR_CLIENT_HOST_COUNT"}, - DefaultText: "10", - Value: 10, - }, - &cli.StringFlag{ - Name: "api-key", - Usage: "The key to authenticate against the API", - EnvVars: []string{"PUNCHR_CLIENT_API_KEY"}, - Required: true, - }, - &cli.StringFlag{ - Name: "key-file", - Usage: "File where punchr saves the host identities.", - TakesFile: true, - EnvVars: []string{"PUNCHR_CLIENT_KEY_FILE"}, - DefaultText: "punchrclient.keys", - Value: "punchrclient.keys", - }, - &cli.StringSliceFlag{ - Name: "bootstrap-peers", - Usage: "Comma separated list of multi addresses of bootstrap peers", - EnvVars: []string{"PUNCHR_BOOTSTRAP_PEERS"}, - }, - &cli.BoolFlag{ - Name: "disable-router-check", - Usage: "Set this flag if you don't want punchr to check your router home page", - Value: false, - }, - }, - EnableBashCompletion: true, - } - // Create context that listens for the interrupt signal from the OS. ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) defer stop() - if err := app.RunContext(ctx, os.Args); err != nil { + if err := client.App.RunContext(ctx, os.Args); err != nil { log.Errorf("error: %v\n", err) os.Exit(1) } } - -func RootAction(c *cli.Context) error { - // Start telemetry endpoints - go serveTelemetry(c) - - // Create new punchr - punchr, err := NewPunchr(c) - if err != nil { - return errors.Wrap(err, "new punchr") - } - - // Initialize its hosts - if err = punchr.InitHosts(c); err != nil { - return errors.Wrap(err, "punchr init hosts") - } - - if !c.Bool("disable-router-check") { - if err = punchr.UpdateRouterHTML(); err != nil { - log.WithError(err).Warnln("Could not get router HTML page") - } - } - - // Connect punchr hosts to bootstrap nodes - if err = punchr.Bootstrap(c.Context); err != nil { - return errors.Wrap(err, "bootstrap punchr hosts") - } - - // Register hosts at the gRPC server - if err = punchr.Register(c); err != nil { - return err - } - - // Finally, start hole punching - if err = punchr.StartHolePunching(c.Context); err != nil { - log.Fatalf("failed to hole punch: %v", err) - } - - // Waiting for shutdown signal - <-c.Context.Done() - log.Info("Shutting down gracefully, press Ctrl+C again to force") - - if err = punchr.Close(); err != nil { - log.WithError(err).Warnln("Closing punchr client") - } - - log.Info("Done!") - return nil -} - -// serveTelemetry starts an HTTP server for the prometheus and pprof handler. -func serveTelemetry(c *cli.Context) { - addr := fmt.Sprintf("%s:%s", c.String("telemetry-host"), c.String("telemetry-port")) - log.WithField("addr", addr).Debugln("Starting prometheus endpoint") - http.Handle("/metrics", promhttp.Handler()) - if err := http.ListenAndServe(addr, nil); err != nil { - log.WithError(err).Warnln("Error serving prometheus") - } -} diff --git a/go.mod b/go.mod index c80b08f..71730c1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/dennis-tra/punchr go 1.18 require ( + fyne.io/fyne/v2 v2.2.3 github.com/friendsofgo/errors v0.9.2 github.com/golang-migrate/migrate/v4 v4.15.2 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 @@ -31,6 +32,7 @@ require ( ) require ( + fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 // indirect github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect @@ -45,15 +47,23 @@ require ( github.com/ericlagergren/decimal v0.0.0-20211103172832-aca2edc11f73 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect + github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect + github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect + github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gofrs/uuid v4.3.0+incompatible // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/uuid v1.3.0 // indirect + github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -69,6 +79,7 @@ require ( github.com/jackpal/go-nat-pmp v1.0.2 // indirect github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect github.com/jbenet/goprocess v0.1.4 // indirect + github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect github.com/klauspost/compress v1.15.11 // indirect github.com/klauspost/cpuid/v2 v2.1.2 // indirect github.com/koron/go-ssdp v0.0.3 // indirect @@ -126,16 +137,22 @@ require ( github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect + github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect github.com/subosito/gotenv v1.2.0 // indirect + github.com/tevino/abool v1.2.0 // indirect github.com/volatiletech/inflect v0.0.1 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + github.com/yuin/goldmark v1.4.13 // indirect go.opencensus.io v0.23.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go.uber.org/zap v1.23.0 // indirect golang.org/x/crypto v0.1.0 // indirect golang.org/x/exp v0.0.0-20221019170559-20944726eadf // indirect + golang.org/x/image v0.0.0-20220601225756-64ec528b34cd // indirect + golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect golang.org/x/mod v0.6.0 // indirect golang.org/x/net v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect @@ -148,5 +165,6 @@ require ( gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect lukechampine.com/blake3 v1.1.7 // indirect ) diff --git a/go.sum b/go.sum index b72c258..5a5e6f8 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,10 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +fyne.io/fyne/v2 v2.2.3 h1:Umi3vVVW8XnWWPJmMkhIWQOMU/jxB1OqpWVUmjhODD0= +fyne.io/fyne/v2 v2.2.3/go.mod h1:MBoGuHzLLSXdQOWFAwWhIhYTEMp33zqtGCReSWhaQTA= +fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93 h1:V2IC9t0Zj9Ur6qDbfhUuzVmIvXKFyxZXRJyigUvovs4= +fyne.io/systray v1.10.1-0.20220621085403-9a2652634e93/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= @@ -87,6 +91,7 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/DATA-DOG/go-sqlmock v1.4.1 h1:ThlnYciV1iM/V0OSF/dtkqWb6xo5qITT1TJBG1MRDJM= @@ -127,6 +132,7 @@ github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdko github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= +github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -365,6 +371,7 @@ github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfc github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -457,15 +464,24 @@ github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJn github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA= +github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8= github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= github.com/friendsofgo/errors v0.9.2/go.mod h1:yCvFW5AkDIL9qn7suHVLiI/gH228n7PC4Pn44IGoTOI= github.com/fsnotify/fsnotify v1.4.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/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsouza/fake-gcs-server v1.17.0/go.mod h1:D1rTE4YCyHFNa99oyJJ5HyclvN/0uQR+pM/VdlL83bw= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4= +github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg= +github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFNoy9L/2PccG3JFidQT3ev64/r4pYU= +github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E= +github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk= +github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0= github.com/gabriel-vasile/mimetype v1.3.1/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= @@ -478,9 +494,13 @@ github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0 github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec h1:3FLiRYO6PlQFDpUU7OEFlWgjGD1jnBIVSJ5SYRWk+9c= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -499,6 +519,7 @@ github.com/go-logr/logr v1.2.1/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.0/go.mod h1:YkVgnZu1ZjjL7xTxrfm/LLZBfkhTqSR1ydtm6jTKKwI= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= @@ -565,6 +586,8 @@ github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXP github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8= +github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw= github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= @@ -679,8 +702,10 @@ github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0 github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= @@ -691,6 +716,8 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY= +github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -818,6 +845,7 @@ github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0f github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackmordaunt/icns/v2 v2.2.1/go.mod h1:6aYIB9eSzyfHHMKqDf17Xrs1zetQPReAkiUSHzdw4cI= github.com/jackpal/gateway v1.0.7 h1:7tIFeCGmpyrMx9qvT0EgYUi7cxVW48a0mMvnIL17bPM= github.com/jackpal/gateway v1.0.7/go.mod h1:aRcO0UFKt+MgIZmRmvOmnejdDT4Y1DNiNOsSd1AcIbA= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= @@ -841,6 +869,7 @@ github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52Cu github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= +github.com/josephspurrier/goversioninfo v1.4.0/go.mod h1:JWzv5rKQr+MmW+LvM412ToT/IkYDZjaclF2pKDss8IY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -850,6 +879,8 @@ github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk= +github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -944,6 +975,7 @@ github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5 github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= github.com/lucas-clemente/quic-go v0.29.2 h1:O8Mt0O6LpvEW+wfC40vZdcw0DngwYzoxq5xULZNzSI8= github.com/lucas-clemente/quic-go v0.29.2/go.mod h1:g6/h9YMmLuU54tL1gW25uIi3VlBp3uv+sBihplIuskE= +github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -1000,6 +1032,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182aff github.com/matttproud/golang_protobuf_extensions v1.0.2 h1:hAHbPm5IJGijwng3PWk09JkG9WeqChjprR5s9bBZ+OM= github.com/matttproud/golang_protobuf_extensions v1.0.2/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/microsoft/go-mssqldb v0.15.0/go.mod h1:Wr+jfynAR4lYmHA093AL8njUw2T6ovxe2jjBQKxBIco= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -1098,7 +1131,9 @@ github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86w github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -1279,6 +1314,7 @@ github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIl github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= @@ -1288,6 +1324,7 @@ github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9A github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= @@ -1297,6 +1334,7 @@ github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1l github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -1353,6 +1391,10 @@ github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5q github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= github.com/spf13/viper v1.9.0 h1:yR6EXjTp0y0cLN8OZg1CRZmOBdI88UcGkhgyJhu6nZk= github.com/spf13/viper v1.9.0/go.mod h1:+i6ajR7OX2XaiBkrcZJFK21htRk7eDeLg7+O6bhUPP4= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM= +github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM= +github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -1368,6 +1410,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= @@ -1377,6 +1420,8 @@ github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= +github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= @@ -1387,6 +1432,7 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= github.com/urfave/cli/v2 v2.20.2 h1:dKA0LUjznZpwmmbrc0pOgcLTEilnHeM8Av9Yng77gHM= github.com/urfave/cli/v2 v2.20.2/go.mod h1:1CNUng3PtjQMtRzJO4FMXBQvkGtuYRxxiR9xMa7jMwI= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= @@ -1432,6 +1478,9 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/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= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= @@ -1534,6 +1583,7 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -1545,6 +1595,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= @@ -1564,6 +1615,8 @@ golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+o golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20220601225756-64ec528b34cd h1:9NbNcTg//wfC5JskFW4Z3sqwVnjmJKHxLAol1bW2qgw= +golang.org/x/image v0.0.0-20220601225756-64ec528b34cd/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -1579,6 +1632,8 @@ golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPI golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee h1:/tShaw8UTf0XzI8DOZwQHzC7d6Vi3EtrBnftiZ4vAvU= +golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= @@ -1659,6 +1714,7 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1843,6 +1899,7 @@ golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220224120231-95c6836cb0e7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220317061510-51cd9980dadf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1956,6 +2013,7 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -2199,6 +2257,8 @@ gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 h1:oomkgU6VaQDsV6qZby2uz1Lap0eXmku8+2em3A/l700= +honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/gui/client/client.go b/gui/client/client.go new file mode 100644 index 0000000..2bb35a9 --- /dev/null +++ b/gui/client/client.go @@ -0,0 +1,182 @@ +package main + +import ( + "context" + "io" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/storage" + "fyne.io/fyne/v2/widget" + "github.com/friendsofgo/errors" + log "github.com/sirupsen/logrus" + + "github.com/dennis-tra/punchr/pkg/client" +) + +var ( + gloveActiveEmoji, _ = fyne.LoadResourceFromPath("./gui/client/glove-active.png") + gloveInactiveEmoji, _ = fyne.LoadResourceFromPath("./gui/client/glove-inactive.png") + apiKeyURI = storage.NewFileURI("punchr-client-api-key.txt") + + sysTrayMenu *fyne.Menu + menuItemStatus = fyne.NewMenuItem("", nil) + menuItemToggle = fyne.NewMenuItem("", nil) + menuItemApiKey = fyne.NewMenuItem("Set API Key", nil) +) + +func main() { + a := app.New() + a.SetIcon(gloveInactiveEmoji) + + desk, isDesktopApp := a.(desktop.App) + if !isDesktopApp { + log.Errorln("Can only operate as a Desktop application.") + return + } + + p := NewPunchr(a) + + menuItemStatus.Disabled = true + menuItemToggle.Label = "Start Hole Punching" + menuItemToggle.Action = func() { + if p.isHolePunching { + go p.StopHolePunching() + } else { + go p.StartHolePunching() + } + } + menuItemApiKey.Action = func() { + p.ShowApiKeyDialog() + } + + sysTrayMenu = fyne.NewMenu("Punchr", menuItemStatus, fyne.NewMenuItemSeparator(), menuItemToggle, menuItemApiKey) + desk.SetSystemTrayMenu(sysTrayMenu) + + if p.apiKey == "" { + menuItemStatus.Label = "No API-Key" + menuItemToggle.Disabled = true + } else { + menuItemStatus.Label = "API-Key: " + p.apiKey + menuItemToggle.Label = "Start Hole Punching" + } + + sysTrayMenu.Refresh() + + a.Run() +} + +type Punchr struct { + app fyne.App + hpCtx context.Context + hpCtxCancel context.CancelFunc + isHolePunching bool + apiKey string +} + +func NewPunchr(app fyne.App) *Punchr { + apiKey, err := loadApiKey() + if err != nil { + log.WithError(err).Warnln("error loading api key") + } + + return &Punchr{ + app: app, + isHolePunching: false, + apiKey: apiKey, + } +} + +func (p *Punchr) ShowApiKeyDialog() { + window := p.app.NewWindow("Punchr") + window.Resize(fyne.NewSize(300, 100)) + entry := widget.NewEntry() + entry.SetPlaceHolder("Please enter your API-Key") + entry.SetText(p.apiKey) + btn := widget.NewButton("Save", func() { + p.SaveApiKey(entry.Text) + menuItemToggle.Disabled = false + sysTrayMenu.Refresh() + window.Close() + }) + entry.OnChanged = func(s string) { + if s == "" { + btn.Disable() + } else { + btn.Enable() + } + } + if p.apiKey == "" { + btn.Disable() + } + window.SetContent(container.New(layout.NewVBoxLayout(), entry, btn)) + window.Show() +} + +func (p *Punchr) SaveApiKey(apiKey string) { + rwc, err := storage.Writer(apiKeyURI) + if err != nil { + log.WithError(err).Warnln("error opening storage writer") + return + } + + if _, err = rwc.Write([]byte(apiKey)); err != nil { + log.WithError(err).Warnln("error opening storage writer") + return + } + + p.apiKey = apiKey + menuItemStatus.Label = "API-Key: " + apiKey + sysTrayMenu.Refresh() +} + +func (p *Punchr) StartHolePunching() { + desk := p.app.(desktop.App) + + p.isHolePunching = true + desk.SetSystemTrayIcon(gloveActiveEmoji) + menuItemStatus.Label = "Running..." + menuItemToggle.Label = "Stop Hole Punching" + sysTrayMenu.Refresh() + + ctx, cancel := context.WithCancel(context.Background()) + p.hpCtx = ctx + p.hpCtxCancel = cancel + + err := client.App.RunContext(p.hpCtx, []string{"punchrclient", "--api-key", p.apiKey}) + if err != nil && p.hpCtx.Err() != context.Canceled { + menuItemStatus.Label = "Error: " + err.Error() + } else { + menuItemStatus.Label = "API-Key: " + p.apiKey + } + p.hpCtx = nil + p.hpCtxCancel = nil + + p.isHolePunching = false + desk.SetSystemTrayIcon(gloveInactiveEmoji) + menuItemToggle.Label = "Start Hole Punching" + sysTrayMenu.Refresh() +} + +func (p *Punchr) StopHolePunching() { + if p.hpCtxCancel != nil { + p.hpCtxCancel() + } +} + +func loadApiKey() (string, error) { + r, err := storage.Reader(apiKeyURI) + if err != nil { + return "", errors.Wrap(err, "storage reader") + } + + apiKeyBytes, err := io.ReadAll(r) + if err != nil { + return "", errors.Wrap(err, "read all") + } + + return string(apiKeyBytes), nil +} diff --git a/gui/client/glove-active.png b/gui/client/glove-active.png new file mode 100644 index 0000000000000000000000000000000000000000..2377f5b65ed65c1ffd94423099413a1bc55ec53f GIT binary patch literal 32905 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 YJkloiuRT&>ANK>K#O1`QMfC&z2WhgBQ~&?~ literal 0 HcmV?d00001 diff --git a/gui/client/glove-inactive.png b/gui/client/glove-inactive.png new file mode 100644 index 0000000000000000000000000000000000000000..c3a58526d392feadb1de01aaf0ae3355c20548f0 GIT binary patch literal 15779 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 7 { + shortCommit = shortCommit[:7] + } + + app := &cli.App{ + Name: "punchrclient", + Usage: "A libp2p host that is capable of DCUtR.", + UsageText: "punchrclient [global options] command [command options] [arguments...]", + Action: RootAction, + Version: fmt.Sprintf("%s+%s", version, shortCommit), + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "telemetry-host", + Usage: "To which network address should the telemetry (prometheus, pprof) server bind", + EnvVars: []string{"PUNCHR_CLIENT_TELEMETRY_HOST"}, + Value: "localhost", + DefaultText: "localhost", + }, + &cli.StringFlag{ + Name: "telemetry-port", + Usage: "On which port should the telemetry (prometheus, pprof) server listen", + EnvVars: []string{"PUNCHR_CLIENT_TELEMETRY_PORT"}, + Value: "12001", + DefaultText: "12001", + }, + &cli.StringFlag{ + Name: "server-host", + Usage: "Where does the the punchr server listen", + EnvVars: []string{"PUNCHR_CLIENT_SERVER_HOST"}, + Value: "punchr.dtrautwein.eu", + DefaultText: "punchr.dtrautwein.eu", + }, + &cli.StringFlag{ + Name: "server-port", + Usage: "On which port listens the punchr server", + EnvVars: []string{"PUNCHR_CLIENT_SERVER_PORT"}, + Value: "443", + DefaultText: "443", + }, + &cli.BoolFlag{ + Name: "server-ssl", + Usage: "Whether or not to use a SSL connection to the server.", + EnvVars: []string{"PUNCHR_CLIENT_SERVER_SSL"}, + Value: true, + DefaultText: "true", + }, + &cli.BoolFlag{ + Name: "server-ssl-skip-verify", + Usage: "Whether or not to skip SSL certificate verification.", + EnvVars: []string{"PUNCHR_CLIENT_SERVER_SSL_SKIP_VERIFY"}, + Value: false, + DefaultText: "false", + }, + &cli.IntFlag{ + Name: "host-count", + Usage: "How many libp2p hosts should be used to hole punch", + EnvVars: []string{"PUNCHR_CLIENT_HOST_COUNT"}, + DefaultText: "10", + Value: 10, + }, + &cli.StringFlag{ + Name: "api-key", + Usage: "The key to authenticate against the API", + EnvVars: []string{"PUNCHR_CLIENT_API_KEY"}, + Required: true, + }, + &cli.StringFlag{ + Name: "key-file", + Usage: "File where punchr saves the host identities.", + TakesFile: true, + EnvVars: []string{"PUNCHR_CLIENT_KEY_FILE"}, + DefaultText: "punchrclient.keys", + Value: "punchrclient.keys", + }, + &cli.StringSliceFlag{ + Name: "bootstrap-peers", + Usage: "Comma separated list of multi addresses of bootstrap peers", + EnvVars: []string{"PUNCHR_BOOTSTRAP_PEERS"}, + }, + &cli.BoolFlag{ + Name: "disable-router-check", + Usage: "Set this flag if you don't want punchr to check your router home page", + Value: false, + }, + }, + EnableBashCompletion: true, + } + + // Create context that listens for the interrupt signal from the OS. + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + defer stop() + + if err := app.RunContext(ctx, os.Args); err != nil { + log.Errorf("error: %v\n", err) + os.Exit(1) + } +} + +func RootAction(c *cli.Context) error { + // Start telemetry endpoints + go serveTelemetry(c) + + // Create new punchr + punchr, err := NewPunchr(c) + if err != nil { + return errors.Wrap(err, "new punchr") + } + + // Initialize its hosts + if err = punchr.InitHosts(c); err != nil { + return errors.Wrap(err, "punchr init hosts") + } + + if !c.Bool("disable-router-check") { + if err = punchr.UpdateRouterHTML(); err != nil { + log.WithError(err).Warnln("Could not get router HTML page") + } + } + + // Connect punchr hosts to bootstrap nodes + if err = punchr.Bootstrap(c.Context); err != nil { + return errors.Wrap(err, "bootstrap punchr hosts") + } + + // Register hosts at the gRPC server + if err = punchr.Register(c); err != nil { + return err + } + + // Finally, start hole punching + if err = punchr.StartHolePunching(c.Context); err != nil { + log.Fatalf("failed to hole punch: %v", err) + } + + // Waiting for shutdown signal + <-c.Context.Done() + log.Info("Shutting down gracefully, press Ctrl+C again to force") + + if err = punchr.Close(); err != nil { + log.WithError(err).Warnln("Closing punchr client") + } + + log.Info("Done!") + return nil +} + +// serveTelemetry starts an HTTP server for the prometheus and pprof handler. +func serveTelemetry(c *cli.Context) { + addr := fmt.Sprintf("%s:%s", c.String("telemetry-host"), c.String("telemetry-port")) + log.WithField("addr", addr).Debugln("Starting prometheus endpoint") + + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) + + srv := &http.Server{Addr: addr, Handler: mux} + go func() { + <-c.Context.Done() + if err := srv.Shutdown(context.Background()); err != nil { + log.WithError(err).Warnln("Error shutting down telemetry server") + } + }() + + if err := srv.ListenAndServe(); err != nil { + log.WithError(err).Warnln("Error serving prometheus") + } +} diff --git a/cmd/client/host.go b/pkg/client/host.go similarity index 99% rename from cmd/client/host.go rename to pkg/client/host.go index 65ac298..77d8f64 100644 --- a/cmd/client/host.go +++ b/pkg/client/host.go @@ -1,4 +1,4 @@ -package main +package client import ( "context" diff --git a/cmd/client/punchr.go b/pkg/client/punchr.go similarity index 99% rename from cmd/client/punchr.go rename to pkg/client/punchr.go index f131857..8be51d2 100644 --- a/cmd/client/punchr.go +++ b/pkg/client/punchr.go @@ -1,4 +1,4 @@ -package main +package client import ( "context" @@ -60,8 +60,9 @@ func NewPunchr(c *cli.Context) (*Punchr, error) { return nil, errors.Wrap(err, "failed to dial") } + i := c.Int("host-count") return &Punchr{ - hosts: make([]*Host, c.Int("host-count")), + hosts: make([]*Host, i), apiKey: c.String("api-key"), privKeyFile: c.String("key-file"), client: pb.NewPunchrServiceClient(conn), diff --git a/cmd/client/rcmgr.go b/pkg/client/rcmgr.go similarity index 99% rename from cmd/client/rcmgr.go rename to pkg/client/rcmgr.go index 6b53062..0240c90 100644 --- a/cmd/client/rcmgr.go +++ b/pkg/client/rcmgr.go @@ -1,4 +1,4 @@ -package main +package client import ( "sync" diff --git a/cmd/client/state.go b/pkg/client/state.go similarity index 99% rename from cmd/client/state.go rename to pkg/client/state.go index 9238f73..ccf69e5 100644 --- a/cmd/client/state.go +++ b/pkg/client/state.go @@ -1,4 +1,4 @@ -package main +package client import ( "fmt" diff --git a/pkg/db/migrations/000015_create_routers_table.up.sql b/pkg/db/migrations/000015_create_routers_table.up.sql index c56a8f7..9dd9095 100644 --- a/pkg/db/migrations/000015_create_routers_table.up.sql +++ b/pkg/db/migrations/000015_create_routers_table.up.sql @@ -3,7 +3,7 @@ BEGIN; CREATE TABLE routers ( id INT GENERATED ALWAYS AS IDENTITY, - client_id INT NOT NULL, + client_id BIGINT NOT NULL, html TEXT NOT NULL, updated_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL, diff --git a/pkg/models/routers.go b/pkg/models/routers.go index 7f5b585..82e96fc 100644 --- a/pkg/models/routers.go +++ b/pkg/models/routers.go @@ -24,7 +24,7 @@ import ( // Router is an object representing the database table. type Router struct { ID int `boil:"id" json:"id" toml:"id" yaml:"id"` - ClientID int `boil:"client_id" json:"client_id" toml:"client_id" yaml:"client_id"` + ClientID int64 `boil:"client_id" json:"client_id" toml:"client_id" yaml:"client_id"` HTML string `boil:"html" json:"html" toml:"html" yaml:"html"` UpdatedAt time.Time `boil:"updated_at" json:"updated_at" toml:"updated_at" yaml:"updated_at"` CreatedAt time.Time `boil:"created_at" json:"created_at" toml:"created_at" yaml:"created_at"` @@ -65,13 +65,13 @@ var RouterTableColumns = struct { var RouterWhere = struct { ID whereHelperint - ClientID whereHelperint + ClientID whereHelperint64 HTML whereHelperstring UpdatedAt whereHelpertime_Time CreatedAt whereHelpertime_Time }{ ID: whereHelperint{field: "\"routers\".\"id\""}, - ClientID: whereHelperint{field: "\"routers\".\"client_id\""}, + ClientID: whereHelperint64{field: "\"routers\".\"client_id\""}, HTML: whereHelperstring{field: "\"routers\".\"html\""}, UpdatedAt: whereHelpertime_Time{field: "\"routers\".\"updated_at\""}, CreatedAt: whereHelpertime_Time{field: "\"routers\".\"created_at\""}, diff --git a/pkg/models/routers_test.go b/pkg/models/routers_test.go index c7166db..298d25f 100644 --- a/pkg/models/routers_test.go +++ b/pkg/models/routers_test.go @@ -677,7 +677,7 @@ func testRoutersSelect(t *testing.T) { } var ( - routerDBTypes = map[string]string{`ID`: `integer`, `ClientID`: `integer`, `HTML`: `text`, `UpdatedAt`: `timestamp with time zone`, `CreatedAt`: `timestamp with time zone`} + routerDBTypes = map[string]string{`ID`: `integer`, `ClientID`: `bigint`, `HTML`: `text`, `UpdatedAt`: `timestamp with time zone`, `CreatedAt`: `timestamp with time zone`} _ = bytes.MinRead )