diff --git a/cmd/http.go b/cmd/http.go index d613ab5..e9b49fd 100644 --- a/cmd/http.go +++ b/cmd/http.go @@ -11,6 +11,7 @@ import ( lm "github.com/loophole/cli/internal/app/loophole/models" "github.com/loophole/cli/internal/pkg/communication" "github.com/loophole/cli/internal/pkg/token" + "github.com/loophole/cli/internal/pkg/updatecheck" "github.com/spf13/cobra" ) @@ -29,7 +30,7 @@ To expose port running on some local host e.g. 192.168.1.20 use 'loophole http < idToken := token.GetIdToken() communication.ApplicationStart(loggedIn, idToken) - checkVersion() + updatecheck.CheckForUpdates() localEndpointSpecs.Host = "127.0.0.1" if len(args) > 1 { diff --git a/cmd/path.go b/cmd/path.go index 205d8f5..9106f46 100644 --- a/cmd/path.go +++ b/cmd/path.go @@ -9,6 +9,7 @@ import ( lm "github.com/loophole/cli/internal/app/loophole/models" "github.com/loophole/cli/internal/pkg/communication" "github.com/loophole/cli/internal/pkg/token" + "github.com/loophole/cli/internal/pkg/updatecheck" "github.com/spf13/cobra" ) @@ -26,7 +27,7 @@ To expose local directory (e.g. /data/my-data) simply use 'loophole path /data/m idToken := token.GetIdToken() communication.ApplicationStart(loggedIn, idToken) - checkVersion() + updatecheck.CheckForUpdates() dirEndpointSpecs.Path = args[0] quitChannel := make(chan bool) diff --git a/cmd/root.go b/cmd/root.go index a99feff..c8853f8 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,6 +10,7 @@ import ( "github.com/loophole/cli/config" "github.com/loophole/cli/internal/pkg/cache" + "github.com/loophole/cli/internal/pkg/communication" "github.com/mattn/go-colorable" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -56,6 +57,10 @@ func initLogger() { // Execute runs command parsing chain func Execute() { rootCmd.Version = fmt.Sprintf("%s (%s)", config.Config.Version, config.Config.CommitHash) + err := config.SetupViperConfig() + if err != nil { + communication.Error(fmt.Sprintf("Error while setting up viper: %s", err.Error())) + } if err := rootCmd.Execute(); err != nil { os.Exit(1) diff --git a/cmd/virtual-serve.go b/cmd/virtual-serve.go index fdb6004..84da4c0 100644 --- a/cmd/virtual-serve.go +++ b/cmd/virtual-serve.go @@ -10,10 +10,8 @@ import ( "strings" "github.com/beevik/guid" - "github.com/blang/semver/v4" "github.com/loophole/cli/config" lm "github.com/loophole/cli/internal/app/loophole/models" - "github.com/loophole/cli/internal/pkg/apiclient" "github.com/loophole/cli/internal/pkg/cache" "github.com/loophole/cli/internal/pkg/communication" "github.com/loophole/cli/internal/pkg/inpututil" @@ -90,24 +88,3 @@ func parseBasicAuthFlags(flagset *pflag.FlagSet) error { return nil } - -func checkVersion() { - availableVersion, err := apiclient.GetLatestAvailableVersion() - if err != nil { - communication.Debug("There was a problem obtaining info response, skipping further checking") - return - } - currentVersionParsed, err := semver.Make(config.Config.Version) - if err != nil { - communication.Debug(fmt.Sprintf("Cannot parse current version '%s' as semver version, skipping further checking", config.Config.Version)) - return - } - availableVersionParsed, err := semver.Make(availableVersion.Version) - if err != nil { - communication.Debug(fmt.Sprintf("Cannot parse available version '%s' as semver version, skipping further checking", availableVersion)) - return - } - if currentVersionParsed.LT(availableVersionParsed) { - communication.NewVersionAvailable(availableVersion.Version) - } -} diff --git a/cmd/webdav.go b/cmd/webdav.go index 75cea17..e162531 100644 --- a/cmd/webdav.go +++ b/cmd/webdav.go @@ -9,6 +9,7 @@ import ( lm "github.com/loophole/cli/internal/app/loophole/models" "github.com/loophole/cli/internal/pkg/communication" "github.com/loophole/cli/internal/pkg/token" + "github.com/loophole/cli/internal/pkg/updatecheck" "github.com/spf13/cobra" ) @@ -28,7 +29,7 @@ To expose local directory via webdav (e.g. /data/my-data) simply use 'loophole w idToken := token.GetIdToken() communication.ApplicationStart(loggedIn, idToken) - checkVersion() + updatecheck.CheckForUpdates() webdavEndpointSpecs.Path = args[0] quitChannel := make(chan bool) diff --git a/config/config.go b/config/config.go index 122efc8..bac41ad 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,13 @@ package config import ( + "fmt" + "time" + "github.com/loophole/cli/internal/app/loophole/models" + "github.com/loophole/cli/internal/pkg/cache" + "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" ) // OAuthConfig defined OAuth settings shape @@ -33,3 +39,35 @@ type ApplicationConfig struct { APIEndpoint models.Endpoint `json:"apiConfig"` GatewayEndpoint models.Endpoint `json:"gatewayConfig"` } + +func SetupViperConfig() error { + viper.SetDefault("lastreminder", time.Time{}) //date of last reminder, default is zero value for time + viper.SetDefault("availableversion", Config.Version) //last seen latest version + viper.SetDefault("remindercount", 3) //counts to zero, then switches from prompt to notification reminder + viper.SetConfigName("config") + viper.SetConfigType("json") + viper.AddConfigPath(cache.GetLocalStorageDir("config")) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { //create a config if none exist yet + err = SaveViperConfig() + if err != nil { + return err + } + } else { + return err + } + } + return nil +} + +func SaveViperConfig() error { + home, err := homedir.Dir() + if err != nil { + return err + } + err = viper.WriteConfigAs(fmt.Sprintf("%s/.loophole/config.json", home)) + if err != nil { + return err + } + return nil +} diff --git a/go.mod b/go.mod index 45c25ca..8bba1a1 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,13 @@ require ( github.com/mdp/qrterminal v1.0.1 github.com/mitchellh/go-homedir v1.1.0 github.com/ncruces/zenity v0.5.2 - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 + github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.19.0 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 + github.com/spf13/viper v1.4.0 github.com/zserge/lorca v0.1.9 golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 golang.org/x/net v0.0.0-20200301022130-244492dfa37a diff --git a/go.sum b/go.sum index faa032b..07aeb21 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= @@ -17,7 +18,6 @@ github.com/briandowns/spinner v1.11.1/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= -github.com/coreos/etcd v3.3.10+incompatible h1:jFneRYjIvLMLhDLCzuTuU4rSJUjRplcJQ7pD7MnhC04= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -38,7 +38,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobuffalo/here v0.6.2 h1:ZtCqC7F9ou3moLbYfHM1Tj+gwHGgWhjyRjVjsir9BE0= github.com/gobuffalo/here v0.6.2/go.mod h1:D75Sq0p2BVHdgQu3vCRsXbg85rx943V19urJpqAVWjI= @@ -66,7 +65,6 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= @@ -96,6 +94,8 @@ github.com/ncruces/zenity v0.5.2/go.mod h1:FPwYbb/qb/eMG2psReJl+L1+0LtXeDkj4R+pi github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= +github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -132,7 +132,6 @@ github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -190,15 +189,12 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= @@ -220,14 +216,12 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= diff --git a/internal/app/loophole/models/CommunicationMechanism.go b/internal/app/loophole/models/CommunicationMechanism.go new file mode 100644 index 0000000..853c2a0 --- /dev/null +++ b/internal/app/loophole/models/CommunicationMechanism.go @@ -0,0 +1,40 @@ +package models + +import authModels "github.com/loophole/cli/internal/pkg/token/models" + +// Mechanism is a type defining interface for loophole communication +type CommunicationMechanism interface { + Debug(message string) + Info(message string) + Warn(message string) + Error(message string) + Fatal(message string) + + TunnelDebug(tunnelID string, message string) + TunnelInfo(tunnelID string, message string) + TunnelWarn(tunnelID string, message string) + TunnelError(tunnelID string, message string) + + ApplicationStart(loggedIn bool, idToken string) + ApplicationStop(feedbackFormURL ...string) + + TunnelStart(tunnelID string) + + TunnelStartSuccess(remoteConfig RemoteEndpointSpecs, localEndpoint string, qr ...bool) + TunnelStartFailure(tunnelID string, err error) + + TunnelStopSuccess(tunnelID string) + + LoginStart(authModels.DeviceCodeSpec) + LoginSuccess(idToken string) + LoginFailure(err error) + + LogoutSuccess() + LogoutFailure(err error) + + LoadingStart(tunnelID string, loaderMessage string) + LoadingSuccess(tunnelID string) + LoadingFailure(tunnelID string, err error) + + NewVersionAvailable(availableVersion string) +} diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go index 6d29b36..7497ccd 100644 --- a/internal/pkg/cache/cache.go +++ b/internal/pkg/cache/cache.go @@ -5,7 +5,7 @@ import ( "os" "path" - "github.com/loophole/cli/internal/pkg/communication" + "github.com/loophole/cli/internal/pkg/logger" "github.com/mitchellh/go-homedir" ) @@ -13,13 +13,13 @@ import ( func GetLocalStorageDir(directoryName string) string { home, err := homedir.Dir() if err != nil { - communication.Fatal(fmt.Sprintf("Error reading user home directory: %s", err.Error())) + logger.CommunicationMechanism.Fatal(fmt.Sprintf("Error reading user home directory: %s", err.Error())) } dirName := path.Join(home, ".loophole", directoryName) err = os.MkdirAll(dirName, os.ModePerm) if err != nil { - communication.Fatal(fmt.Sprintf("Error creating local cache directory: %s", err.Error())) + logger.CommunicationMechanism.Fatal(fmt.Sprintf("Error creating local cache directory: %s", err.Error())) } return dirName } @@ -28,12 +28,12 @@ func GetLocalStorageDir(directoryName string) string { func GetLocalStorageFile(fileName string, directoryName string) string { home, err := homedir.Dir() if err != nil { - communication.Fatal(fmt.Sprintf("Error reading user home directory: %s", err.Error())) + logger.CommunicationMechanism.Fatal(fmt.Sprintf("Error reading user home directory: %s", err.Error())) } dirName := path.Join(home, ".loophole", directoryName) err = os.MkdirAll(dirName, os.ModePerm) if err != nil { - communication.Fatal(fmt.Sprintf("Error creating local cache directory: %s", err.Error())) + logger.CommunicationMechanism.Fatal(fmt.Sprintf("Error creating local cache directory: %s", err.Error())) } return path.Join(dirName, fileName) diff --git a/internal/pkg/communication/communication.go b/internal/pkg/communication/communication.go index f435bf1..0580f32 100644 --- a/internal/pkg/communication/communication.go +++ b/internal/pkg/communication/communication.go @@ -1,148 +1,110 @@ package communication import ( + "github.com/loophole/cli/config" coreModels "github.com/loophole/cli/internal/app/loophole/models" + "github.com/loophole/cli/internal/pkg/logger" authModels "github.com/loophole/cli/internal/pkg/token/models" ) -var defaultLogger = NewStdOutLogger() -var communicationMechanism Mechanism = defaultLogger - -// Mechanism is a type defining interface for loophole communication -type Mechanism interface { - Debug(message string) - Info(message string) - Warn(message string) - Error(message string) - Fatal(message string) - - TunnelDebug(tunnelID string, message string) - TunnelInfo(tunnelID string, message string) - TunnelWarn(tunnelID string, message string) - TunnelError(tunnelID string, message string) - - ApplicationStart(loggedIn bool, idToken string) - ApplicationStop() - - TunnelStart(tunnelID string) - - TunnelStartSuccess(remoteConfig coreModels.RemoteEndpointSpecs, localEndpoint string) - TunnelStartFailure(tunnelID string, err error) - - TunnelStopSuccess(tunnelID string) - - LoginStart(authModels.DeviceCodeSpec) - LoginSuccess(idToken string) - LoginFailure(err error) - - LogoutSuccess() - LogoutFailure(err error) - - LoadingStart(tunnelID string, loaderMessage string) - LoadingSuccess(tunnelID string) - LoadingFailure(tunnelID string, err error) - - NewVersionAvailable(availableVersion string) -} - // SetCommunicationMechanism is communication mechanism switcher -func SetCommunicationMechanism(mechanism Mechanism) { - communicationMechanism = mechanism +func SetCommunicationMechanism(mechanism coreModels.CommunicationMechanism) { + logger.CommunicationMechanism = mechanism } // TunnelDebug is debug level logger in context of a tunnel func TunnelDebug(tunnelID string, message string) { - communicationMechanism.TunnelDebug(tunnelID, message) + logger.CommunicationMechanism.TunnelDebug(tunnelID, message) } // TunnelInfo is info level logger in context of a tunnel func TunnelInfo(tunnelID string, message string) { - communicationMechanism.TunnelInfo(tunnelID, message) + logger.CommunicationMechanism.TunnelInfo(tunnelID, message) } // TunnelWarn is warn level logger in context of a tunnel func TunnelWarn(tunnelID string, message string) { - communicationMechanism.TunnelWarn(tunnelID, message) + logger.CommunicationMechanism.TunnelWarn(tunnelID, message) } // TunnelError is error level logger in context of a tunnel func TunnelError(tunnelID string, message string) { - communicationMechanism.TunnelError(tunnelID, message) + logger.CommunicationMechanism.TunnelError(tunnelID, message) } // Debug is debug level logger func Debug(message string) { - communicationMechanism.Debug(message) + logger.CommunicationMechanism.Debug(message) } // Info is info level logger func Info(message string) { - communicationMechanism.Info(message) + logger.CommunicationMechanism.Info(message) } // Warn is warn level logger func Warn(message string) { - communicationMechanism.Warn(message) + logger.CommunicationMechanism.Warn(message) } // Error is error level logger func Error(message string) { - communicationMechanism.Error(message) + logger.CommunicationMechanism.Error(message) } // Fatal is fatal level logger, which should cause application to stop func Fatal(message string) { - communicationMechanism.Fatal(message) + logger.CommunicationMechanism.Fatal(message) } // ApplicationStart is the application startup welcome communicate func ApplicationStart(loggedIn bool, idToken string) { - communicationMechanism.ApplicationStart(loggedIn, idToken) + logger.CommunicationMechanism.ApplicationStart(loggedIn, idToken) } // ApplicationStop is the application startup goodbye communicate func ApplicationStop() { - communicationMechanism.ApplicationStop() + logger.CommunicationMechanism.ApplicationStop(config.Config.FeedbackFormURL) } // LoginStart is the communicate to notify about login process being started func LoginStart(deviceCodeSpec authModels.DeviceCodeSpec) { - communicationMechanism.LoginStart(deviceCodeSpec) + logger.CommunicationMechanism.LoginStart(deviceCodeSpec) } // LoginSuccess is the application success login communicate func LoginSuccess(idToken string) { - communicationMechanism.LoginSuccess(idToken) + logger.CommunicationMechanism.LoginSuccess(idToken) } // LoginFailure is the application login failure communicate func LoginFailure(err error) { - communicationMechanism.LoginFailure(err) + logger.CommunicationMechanism.LoginFailure(err) } // LogoutSuccess is the notification logout success communicate func LogoutSuccess() { - communicationMechanism.LogoutSuccess() + logger.CommunicationMechanism.LogoutSuccess() } // LogoutFailure is the notification logout failure communicate func LogoutFailure(err error) { - communicationMechanism.LogoutFailure(err) + logger.CommunicationMechanism.LogoutFailure(err) } // TunnelStart is the notification about tunnel registration success func TunnelStart(tunnelID string) { - communicationMechanism.TunnelStart(tunnelID) + logger.CommunicationMechanism.TunnelStart(tunnelID) } // TunnelStartSuccess is the notification about tunnel being started succesfully func TunnelStartSuccess(remoteConfig coreModels.RemoteEndpointSpecs, localEndpoint string) { - communicationMechanism.TunnelStartSuccess(remoteConfig, localEndpoint) + logger.CommunicationMechanism.TunnelStartSuccess(remoteConfig, localEndpoint, config.Config.Display.QR) } // TunnelStartFailure is the notification about tunnel failing to start func TunnelStartFailure(tunnelID string, err error) { - communicationMechanism.TunnelStartFailure(tunnelID, err) + logger.CommunicationMechanism.TunnelStartFailure(tunnelID, err) } // TunnelRestart is the notification about tunnel being restarted @@ -150,25 +112,25 @@ func TunnelRestart(tunnelID string) {} // TunnelStopSuccess is the notification about tunnel being shut down func TunnelStopSuccess(tunnelID string) { - communicationMechanism.TunnelStopSuccess(tunnelID) + logger.CommunicationMechanism.TunnelStopSuccess(tunnelID) } // LoadingStart is the notification about some loading process being started func LoadingStart(tunnelID string, loaderMessage string) { - communicationMechanism.LoadingStart(tunnelID, loaderMessage) + logger.CommunicationMechanism.LoadingStart(tunnelID, loaderMessage) } // LoadingSuccess is the notification about started loading process being finished successfully func LoadingSuccess(tunnelID string) { - communicationMechanism.LoadingSuccess(tunnelID) + logger.CommunicationMechanism.LoadingSuccess(tunnelID) } // LoadingFailure is the notification about started loading process being finished with failure func LoadingFailure(tunnelID string, err error) { - communicationMechanism.LoadingFailure(tunnelID, err) + logger.CommunicationMechanism.LoadingFailure(tunnelID, err) } // NewVersionAvailable is a communicate being sent if new version of the application is available func NewVersionAvailable(availableVersion string) { - communicationMechanism.NewVersionAvailable(availableVersion) + logger.CommunicationMechanism.NewVersionAvailable(availableVersion) } diff --git a/internal/pkg/communication/websocket.go b/internal/pkg/communication/websocket.go index ee5e9e0..9ce467b 100644 --- a/internal/pkg/communication/websocket.go +++ b/internal/pkg/communication/websocket.go @@ -8,6 +8,7 @@ import ( "github.com/gorilla/websocket" "github.com/loophole/cli/config" coreModels "github.com/loophole/cli/internal/app/loophole/models" + "github.com/loophole/cli/internal/pkg/logger" authModels "github.com/loophole/cli/internal/pkg/token/models" "github.com/loophole/cli/internal/pkg/urlmaker" "github.com/mitchellh/go-homedir" @@ -47,7 +48,7 @@ const ( ) // NewWebsocketLogger is websocket mechanism constructor -func NewWebsocketLogger(wsClient *websocket.Conn) Mechanism { +func NewWebsocketLogger(wsClient *websocket.Conn) coreModels.CommunicationMechanism { logger := websocketLogger{ wsClient: wsClient, } @@ -164,7 +165,7 @@ func (l *websocketLogger) write(websocketMessage interface{}) { err := l.wsClient.WriteJSON(websocketMessage) if err != nil { - defaultLogger.Fatal("Communication over websocket is failing") + logger.DefaultLogger.Fatal("Communication over websocket is failing") } } @@ -243,7 +244,7 @@ func (l *websocketLogger) Fatal(message string) { }) zenity.Error(message) - defaultLogger.Fatal(message) + logger.DefaultLogger.Fatal(message) } func (l *websocketLogger) ApplicationStart(loggedIn bool, idToken string) { @@ -264,7 +265,7 @@ func (l *websocketLogger) ApplicationStart(loggedIn bool, idToken string) { l.write(websocketMessage) } -func (l *websocketLogger) ApplicationStop() { +func (l *websocketLogger) ApplicationStop(s ...string) { //s is only defined to fulfill the interface l.write(appStopMessage{ Type: MessageTypeAppStop, }) @@ -277,7 +278,7 @@ func (l *websocketLogger) TunnelStart(tunnelID string) { }) } -func (l *websocketLogger) TunnelStartSuccess(remoteConfig coreModels.RemoteEndpointSpecs, localEndpoint string) { +func (l *websocketLogger) TunnelStartSuccess(remoteConfig coreModels.RemoteEndpointSpecs, localEndpoint string, b ...bool) { //b is only defined to fulfill the interface siteAddrs := []string{} siteAddrs = append(siteAddrs, urlmaker.GetSiteURL("https", remoteConfig.SiteID, remoteConfig.Domain)) diff --git a/internal/pkg/communication/logger.go b/internal/pkg/logger/logger.go similarity index 92% rename from internal/pkg/communication/logger.go rename to internal/pkg/logger/logger.go index fa52893..433965d 100644 --- a/internal/pkg/communication/logger.go +++ b/internal/pkg/logger/logger.go @@ -1,4 +1,4 @@ -package communication +package logger import ( "fmt" @@ -8,7 +8,6 @@ import ( "github.com/briandowns/spinner" "github.com/logrusorgru/aurora" - "github.com/loophole/cli/config" coreModels "github.com/loophole/cli/internal/app/loophole/models" authModels "github.com/loophole/cli/internal/pkg/token/models" "github.com/loophole/cli/internal/pkg/urlmaker" @@ -17,6 +16,9 @@ import ( "github.com/rs/zerolog/log" ) +var DefaultLogger = NewStdOutLoggerMechanism() +var CommunicationMechanism coreModels.CommunicationMechanism = DefaultLogger + type stdoutLogger struct { colorableOutput io.Writer loader *spinner.Spinner @@ -24,7 +26,7 @@ type stdoutLogger struct { } // NewStdOutLogger is stdout mechanism constructor -func NewStdOutLogger() Mechanism { +func NewStdOutLoggerMechanism() coreModels.CommunicationMechanism { logger := stdoutLogger{ colorableOutput: colorable.NewColorableStdout(), } @@ -95,13 +97,13 @@ func (l *stdoutLogger) ApplicationStart(loggedIn bool, idToken string) { fmt.Fprintln(l.colorableOutput) fmt.Fprintln(l.colorableOutput) } -func (l *stdoutLogger) ApplicationStop() { +func (l *stdoutLogger) ApplicationStop(feedbackFormURL ...string) { l.messageMutex.Lock() defer l.messageMutex.Unlock() l.divider() fmt.Fprint(l.colorableOutput, "Goodbye") - fmt.Fprintln(l.colorableOutput, aurora.Cyan(fmt.Sprintf("Thank you for using Loophole. Please give us your feedback via %s and help us improve our services.", config.Config.FeedbackFormURL))) + fmt.Fprintln(l.colorableOutput, aurora.Cyan(fmt.Sprintf("Thank you for using Loophole. Please give us your feedback via %s and help us improve our services.", feedbackFormURL[0]))) } func (l *stdoutLogger) TunnelStart(tunnelID string) { @@ -110,7 +112,7 @@ func (l *stdoutLogger) TunnelStart(tunnelID string) { log.Debug().Msg("Tunnel starting up...") } -func (l *stdoutLogger) TunnelStartSuccess(remoteConfig coreModels.RemoteEndpointSpecs, localEndpoint string) { +func (l *stdoutLogger) TunnelStartSuccess(remoteConfig coreModels.RemoteEndpointSpecs, localEndpoint string, qr ...bool) { l.messageMutex.Lock() defer l.messageMutex.Unlock() @@ -122,7 +124,7 @@ func (l *stdoutLogger) TunnelStartSuccess(remoteConfig coreModels.RemoteEndpoint fmt.Fprint(l.colorableOutput, " -> ") fmt.Fprint(l.colorableOutput, aurora.Green(localEndpoint)) - if config.Config.Display.QR { + if qr[0] { fmt.Fprintln(l.colorableOutput, "") fmt.Fprintln(l.colorableOutput, "") @@ -207,7 +209,7 @@ func (l *stdoutLogger) LoadingFailure(tunnelID string, err error) { func (l *stdoutLogger) NewVersionAvailable(availableVersion string) { l.messageMutex.Lock() defer l.messageMutex.Unlock() - fmt.Fprint(l.colorableOutput, aurora.Cyan(fmt.Sprintf("There is new version available, to get it please visit %s", + fmt.Fprintln(l.colorableOutput, aurora.Cyan(fmt.Sprintf("There is new version available, to get it please visit %s", fmt.Sprintf("https://github.com/loophole/cli/releases/tag/%s", availableVersion)))) } diff --git a/internal/pkg/updatecheck/updatecheck.go b/internal/pkg/updatecheck/updatecheck.go new file mode 100644 index 0000000..48182d1 --- /dev/null +++ b/internal/pkg/updatecheck/updatecheck.go @@ -0,0 +1,112 @@ +package updatecheck + +import ( + "fmt" + "runtime" + "time" + + "github.com/blang/semver/v4" + "github.com/loophole/cli/config" + "github.com/loophole/cli/internal/pkg/apiclient" + "github.com/loophole/cli/internal/pkg/communication" + "github.com/ncruces/zenity" + "github.com/pkg/browser" + "github.com/spf13/viper" +) + +func CheckForUpdates() { + availableVersion, err := apiclient.GetLatestAvailableVersion() + if err != nil { + communication.Debug("There was a problem obtaining info response, skipping further checking") + return + } + currentVersionParsed, err := semver.Make(config.Config.Version) + if err != nil { + communication.Debug(fmt.Sprintf("Cannot parse current version '%s' as semver version, skipping further checking", config.Config.Version)) + return + } + availableVersionParsed, err := semver.Make(availableVersion.Version) + if err != nil { + communication.Debug(fmt.Sprintf("Cannot parse available version '%s' as semver version, skipping further checking", availableVersion)) + return + } + if currentVersionParsed.LT(availableVersionParsed) { + if config.Config.ClientMode == "cli" { + communication.NewVersionAvailable(availableVersion.Version) + } else { + remind, usePrompt, err := remindAgainCheck(availableVersionParsed) + if err != nil { + communication.Error(err.Error()) //errors in determining the type reminder should be noted, but not interrupt the program + } + if !remind { + return + } + if usePrompt { //either use a notification that the user needs to click away, or use a notification they can ignore + downloadlink := getDownloadLink(availableVersion.Version) + openLink := false //needs to be declared here instead of below with := so we can still have access to err outside of this scope + openLink, err = zenity.Question(fmt.Sprintf("A new version is available for you at \n%s \n Do you want to open this link in your browser?", downloadlink), zenity.NoWrap(), zenity.Title("New version available!")) + if openLink { + browser.OpenURL(downloadlink) + } + } else { + downloadlink := "https://loophole.cloud/download" //this notification isn't clickable, so the link should be something the user can remember + err = zenity.Notify(fmt.Sprintf("A new version is available for you, please visit \n%s \n", downloadlink), zenity.Title("New version available!")) + } + if err != nil { + communication.Debug(err.Error()) //errors in showing a download link should be noted, but not interrupt the program + } + } + } +} + +func getDownloadLink(availableVersion string) string { + archiveType := ".tar.gz" + operatingSystem := runtime.GOOS + architecture := runtime.GOARCH + if operatingSystem == "windows" { + archiveType = ".zip" + } else if operatingSystem == "darwin" { + operatingSystem = "macos" //rename for use in download url + } + if architecture == "amd64" { + architecture = "64bit" + } else if architecture == "386" { + architecture = "32bit" + } else { + communication.Error("There was an error detecting your system architecture.") //if arch is unexpected, only link to the release page + return fmt.Sprintf("https://github.com/loophole/cli/releases/tag/%s", availableVersion) + } + link := fmt.Sprintf("https://github.com/loophole/cli/releases/download/%s/loophole-desktop_%s_%s_%s%s", availableVersion, availableVersion, operatingSystem, architecture, archiveType) + return link +} + +func remindAgainCheck(availableVersionParsed semver.Version) (bool, bool, error) { + lastSeenLatestVersion, err := semver.Make(viper.GetString("availableversion")) + if availableVersionParsed.GT(lastSeenLatestVersion) { //reset reminder count if new version is out + viper.Set("availableversion", availableVersionParsed.String()) + viper.Set("remindercount", 3) + } + if err != nil { + return true, false, err + } + lastReminder := viper.GetTime("lastreminder") + if (lastReminder.Year() < time.Now().Year()) || (lastReminder.YearDay() < time.Now().YearDay()) { //check if reminder has been done today + viper.Set("lastreminder", time.Now()) + err = config.SaveViperConfig() + if err != nil { + return true, false, err + } + if viper.GetInt("remindercount") < 1 { + return true, false, nil + } else { + viper.Set("remindercount", viper.GetInt("remindercount")-1) + err = config.SaveViperConfig() + if err != nil { + return true, false, err + } + return true, true, nil + } + } + + return false, false, nil +} diff --git a/ui/ui.go b/ui/ui.go index 653a479..6efb784 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -16,11 +16,13 @@ import ( "github.com/gorilla/websocket" + "github.com/loophole/cli/config" "github.com/loophole/cli/internal/app/loophole" lm "github.com/loophole/cli/internal/app/loophole/models" "github.com/loophole/cli/internal/pkg/cache" "github.com/loophole/cli/internal/pkg/communication" "github.com/loophole/cli/internal/pkg/token" + "github.com/loophole/cli/internal/pkg/updatecheck" ) var upgrader = websocket.Upgrader{} // use default options @@ -234,6 +236,10 @@ func websocketHandler(w http.ResponseWriter, r *http.Request) { // Display shows the main app window func Display() { + err := config.SetupViperConfig() + if err != nil { + communication.Error(fmt.Sprintf("Error while setting up viper: %s", err.Error())) + } chromeLocation := lorca.LocateChrome() if chromeLocation == "" { message := "Chrome/Chromium >= 70 is required." @@ -259,6 +265,7 @@ func Display() { } defer ui.Close() + updatecheck.CheckForUpdates() <-ui.Done() }