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/list.go b/cmd/list.go new file mode 100644 index 0000000..eac5d81 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,29 @@ +// +build !desktop + +package cmd + +import ( + "fmt" + + "github.com/loophole/cli/internal/pkg/communication" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// listCommand represents the command that lists previously used hostnames +var listCommand = &cobra.Command{ + Use: "list", + Short: "Show used hostnames", + Long: `Show previously used hostnames that were successfully used.`, + Run: func(cmd *cobra.Command, args []string) { + hostnames := viper.GetStringSlice("usedhostnames") + communication.Info(fmt.Sprintf("The following %d hostnames have been used:", len(hostnames))) + for _, hostname := range hostnames { + communication.Info(hostname) + } + }, +} + +func init() { + rootCmd.AddCommand(listCommand) +} diff --git a/cmd/list_clear.go b/cmd/list_clear.go new file mode 100644 index 0000000..680a088 --- /dev/null +++ b/cmd/list_clear.go @@ -0,0 +1,24 @@ +// +build !desktop + +package cmd + +import ( + "github.com/loophole/cli/config" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// listclearCommand represents the command that clears the list of previously used hostnames +var listclearCommand = &cobra.Command{ + Use: "clear", + Short: "Delete the current list of saved hostnames", + Long: `Delete the current list of saved hostnames`, + Run: func(cmd *cobra.Command, args []string) { + viper.Set("usedhostnames", []string{}) + config.SaveViperConfig() + }, +} + +func init() { + listCommand.AddCommand(listclearCommand) +} diff --git a/cmd/list_locked.go b/cmd/list_locked.go new file mode 100644 index 0000000..c9adb1d --- /dev/null +++ b/cmd/list_locked.go @@ -0,0 +1,33 @@ +// +build !desktop + +package cmd + +import ( + "fmt" + + "github.com/loophole/cli/internal/pkg/apiclient" + "github.com/loophole/cli/internal/pkg/communication" + "github.com/spf13/cobra" +) + +// listlockedCommand represents the command that lists the hostnames that are currently locked for the user +var listlockedCommand = &cobra.Command{ + Use: "locked", + Short: "List the hostnames that are currently locked for you.", + Long: `List the hostnames that are currently locked for you.`, + Run: func(cmd *cobra.Command, args []string) { + hostnames, err := apiclient.GetLockedHostnames() + if err != nil { + communication.Error(fmt.Sprintf("Error while trying to retrieve locked hostnames: %s", err.Error())) + return + } + communication.Info(fmt.Sprintf("The following %d hostnames are currently locked for you:", len(hostnames))) + for _, hostname := range hostnames { + communication.Info(hostname) + } + }, +} + +func init() { + listCommand.AddCommand(listlockedCommand) +} diff --git a/cmd/list_toggle.go b/cmd/list_toggle.go new file mode 100644 index 0000000..b6c94d1 --- /dev/null +++ b/cmd/list_toggle.go @@ -0,0 +1,34 @@ +// +build !desktop + +package cmd + +import ( + "github.com/loophole/cli/config" + "github.com/loophole/cli/internal/pkg/communication" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// listtoggleCommand represents the command that toggles whether we save previously used hostnames +var listtoggleCommand = &cobra.Command{ + Use: "toggle", + Short: "Stop or start saving used hostnames.", + Long: `Stop or start saving used hostnames. +By default, a list of hostnames you successfully used is saved locally for your convenience. +With this command, you can stop it or resume it if you stopped it before. +This function is not related to the timed reservation of hostnames.`, + Run: func(cmd *cobra.Command, args []string) { + oldState := viper.GetBool("savehostnames") + viper.Set("savehostnames", !oldState) + config.SaveViperConfig() + if oldState { + communication.Info("Hostname saving is now turned off.") + } else { + communication.Info("Hostname saving is now turned on.") + } + }, +} + +func init() { + listCommand.AddCommand(listtoggleCommand) +} 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..97a62d3 100644 --- a/config/config.go +++ b/config/config.go @@ -1,7 +1,12 @@ package config import ( + "fmt" + "time" + "github.com/loophole/cli/internal/app/loophole/models" + "github.com/mitchellh/go-homedir" + "github.com/spf13/viper" ) // OAuthConfig defined OAuth settings shape @@ -33,3 +38,41 @@ type ApplicationConfig struct { APIEndpoint models.Endpoint `json:"apiConfig"` GatewayEndpoint models.Endpoint `json:"gatewayConfig"` } + +func SetupViperConfig() error { + home, err := homedir.Dir() + if err != nil { + return err + } + viper.SetDefault("lastreminder", time.Time{}) //date of last reminder, default is zero value for time + viper.SetDefault("availableversion", "1.0.0-beta.14") //last seen latest version + viper.SetDefault("remindercount", 3) //counts to zero, then switches from prompt to notification reminder + viper.SetDefault("savehostnames", true) + viper.SetDefault("usedhostnames", []string{}) + viper.SetConfigName("config") + viper.SetConfigType("json") + viper.AddConfigPath(fmt.Sprintf("%s/.loophole/", home)) + 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/loophole.go b/internal/app/loophole/loophole.go index 6f63d1c..e9bb67b 100644 --- a/internal/app/loophole/loophole.go +++ b/internal/app/loophole/loophole.go @@ -14,6 +14,7 @@ import ( "github.com/loophole/cli/internal/pkg/httpserver" "github.com/loophole/cli/internal/pkg/keys" "github.com/loophole/cli/internal/pkg/urlmaker" + "github.com/spf13/viper" "golang.org/x/crypto/ssh" ) @@ -70,6 +71,15 @@ func handleClient(tunnelID string, client net.Conn, local net.Conn) { <-chDone } +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + func registerDomain(publicKey *ssh.PublicKey, requestedSiteID string, tunnelID string) (*apiclient.RegistrationSuccessResponse, error) { communication.LoadingStart(tunnelID, "Registering your domain...") registrationResult, err := apiclient.RegisterSite(*publicKey, requestedSiteID) @@ -85,6 +95,16 @@ func registerDomain(publicKey *ssh.PublicKey, requestedSiteID string, tunnelID s } return nil, err } + if viper.GetBool("savehostnames") { + hostnames := viper.GetStringSlice("usedhostnames") + if !contains(hostnames, requestedSiteID) && requestedSiteID != "" { + viper.Set("usedhostnames", append(hostnames, requestedSiteID)) + err := viper.WriteConfig() + if err != nil { + communication.Error(fmt.Sprintf("Error occured while saving config: %s\n", err.Error())) + } + } + } communication.LoadingSuccess(tunnelID) return registrationResult, nil } diff --git a/internal/pkg/apiclient/apiclient.go b/internal/pkg/apiclient/apiclient.go index 93f896c..9dfe57b 100644 --- a/internal/pkg/apiclient/apiclient.go +++ b/internal/pkg/apiclient/apiclient.go @@ -5,8 +5,10 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io/ioutil" "net" "net/http" + "regexp" "runtime" "time" @@ -50,6 +52,46 @@ var getAccessToken = token.GetAccessToken var tokenWasRefreshed = false var apiURL = config.Config.APIEndpoint.URI() +// GetLockedHostnames is a function used to obtain the locked hostnames for the user +func GetLockedHostnames() ([]string, error) { + accessToken, err := getAccessToken() + if err != nil { + return nil, err + } + req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/site/locked", apiURL), nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", userAgent()) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken)) + var netTransport = &http.Transport{ + Dial: (&net.Dialer{ + Timeout: 10 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, + } + var netClient = &http.Client{ + Timeout: time.Second * 30, + Transport: netTransport, + } + + resp, err := netClient.Do(req) + if err != nil { + return nil, err + } + respBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + respString := string(respBytes) + reg := regexp.MustCompile(`\[*",*"*\]*`) + hostnames := reg.Split(respString, -1) + if len(hostnames) > 2 { + hostnames = hostnames[1 : len(hostnames)-1] + } + return hostnames, nil +} + // RegisterSite is a funtion used to obtain site id and register keys in the gateway func RegisterSite(publicKey ssh.PublicKey, requestedSiteID string) (*RegistrationSuccessResponse, error) { publicKeyString := publicKey.Type() + " " + base64.StdEncoding.EncodeToString(publicKey.Marshal()) diff --git a/internal/pkg/communication/logger.go b/internal/pkg/communication/logger.go index fa52893..cd36035 100644 --- a/internal/pkg/communication/logger.go +++ b/internal/pkg/communication/logger.go @@ -207,7 +207,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..33e71fc --- /dev/null +++ b/internal/pkg/updatecheck/updatecheck.go @@ -0,0 +1,113 @@ +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) + fmt.Println(link) + 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() }