diff --git a/cli.go b/cli.go index e4584d2..01906c3 100644 --- a/cli.go +++ b/cli.go @@ -5,6 +5,7 @@ package main import ( "github.com/loophole/cli/cmd" "github.com/loophole/cli/config" + "github.com/loophole/cli/internal/pkg/closehandler" ) var ( @@ -18,5 +19,6 @@ func main() { config.Config.CommitHash = commit config.Config.ClientMode = mode - cmd.Execute() + c := closehandler.SetupCloseHandler("https://forms.gle/K9ga7FZB3deaffnV7") + cmd.Execute(c) } diff --git a/cmd/root.go b/cmd/root.go index a99feff..ac4165c 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/prompts" "github.com/mattn/go-colorable" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -17,12 +18,19 @@ import ( "github.com/spf13/cobra" ) +var signalChan chan os.Signal + +var alreadyRunning bool + var rootCmd = &cobra.Command{ Use: "loophole", Short: "Loophole - End to end TLS encrypted TCP communication between you and your clients", Long: "Loophole - End to end TLS encrypted TCP communication between you and your clients", Run: func(cmd *cobra.Command, args []string) { - cmd.Help() + if !alreadyRunning { + alreadyRunning = true + prompts.StartInteractivePrompt(httpCmd.Root(), signalChan) + } }, } @@ -54,10 +62,13 @@ func initLogger() { } // Execute runs command parsing chain -func Execute() { +func Execute(c chan os.Signal) { rootCmd.Version = fmt.Sprintf("%s (%s)", config.Config.Version, config.Config.CommitHash) - if err := rootCmd.Execute(); err != nil { - os.Exit(1) + signalChan = c + if !alreadyRunning { + if err := rootCmd.Execute(); err != nil { + signalChan <- nil + } } } diff --git a/cmd/virtual-serve.go b/cmd/virtual-serve.go index 2307478..954dee1 100644 --- a/cmd/virtual-serve.go +++ b/cmd/virtual-serve.go @@ -33,8 +33,7 @@ func initServeCommand(serveCmd *cobra.Command) { serveCmd.PersistentFlags().StringVarP(&remoteEndpointSpecs.IdentityFile, "identity-file", "i", fmt.Sprintf("%s/id_rsa", sshDir), "private key path") serveCmd.MarkFlagFilename("identity-file") - - serveCmd.PersistentFlags().StringVar(&remoteEndpointSpecs.SiteID, "hostname", "", "custom hostname you want to run service on") + serveCmd.PersistentFlags().StringVarP(&remoteEndpointSpecs.SiteID, "hostname", "c", "", "custom hostname you want to run service on") serveCmd.PersistentFlags().BoolVar(&config.Config.Display.QR, "qr", false, "use if you want a QR version of your url to be shown") serveCmd.PersistentFlags().StringVarP(&remoteEndpointSpecs.BasicAuthUsername, basicAuthUsernameFlagName, "u", "", "Basic authentication username to protect site with") diff --git a/go.mod b/go.mod index b93ac14..ce64ce0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/loophole/cli go 1.20 require ( + github.com/AlecAivazis/survey/v2 v2.2.4 github.com/abbot/go-http-auth v0.4.0 github.com/beevik/guid v0.0.0-20170504223318-d0ea8faecee0 github.com/blang/semver/v4 v4.0.0 @@ -28,8 +29,10 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/fatih/color v1.7.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/kr/pretty v0.2.0 // indirect github.com/mattn/go-isatty v0.0.8 // indirect + github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/russross/blackfriday/v2 v2.0.1 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect golang.org/x/image v0.5.0 // indirect diff --git a/go.sum b/go.sum index 75f1ca3..c6b79b2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,10 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/AlecAivazis/survey/v2 v2.2.4 h1:OAh6g17JmXsjVVHTnfQFEi6K+YZX6mrC+pT8IPkUlpk= +github.com/AlecAivazis/survey/v2 v2.2.4/go.mod h1:9FJRdMdDm8rnT+zHVbvQT2RTSTLq0Ttd6q3Vl2fahjk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= +github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/abbot/go-http-auth v0.4.0 h1:QjmvZ5gSC7jm3Zg54DqWE/T5m1t2AfDu6QlXJT0EVT0= github.com/abbot/go-http-auth v0.4.0/go.mod h1:Cz6ARTIzApMJDzh5bRMSUou6UMSp0IEXg9km/ci7TJM= @@ -24,6 +29,7 @@ github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfc github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= @@ -51,10 +57,14 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= +github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 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= @@ -63,6 +73,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.4 h1:5Myjjh3JY/NaAi4IsUbHADytDyl1VE1Y9PXDlL+P/VQ= +github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= @@ -75,6 +87,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mdp/qrterminal v1.0.1 h1:07+fzVDlPuBlXS8tB0ktTAyf+Lp1j2+2zK3fBOL5b7c= github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= @@ -122,7 +136,9 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= @@ -139,6 +155,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= @@ -178,6 +195,7 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h 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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/pkg/closehandler/closehandler.go b/internal/pkg/closehandler/closehandler.go index 8e2ae29..3f432fa 100644 --- a/internal/pkg/closehandler/closehandler.go +++ b/internal/pkg/closehandler/closehandler.go @@ -1,10 +1,14 @@ package closehandler import ( + "fmt" + "io/ioutil" "os" "os/signal" + "strings" "syscall" + "github.com/loophole/cli/internal/pkg/cache" "github.com/loophole/cli/internal/pkg/communication" "github.com/loophole/cli/internal/pkg/inpututil" "golang.org/x/term" @@ -12,9 +16,10 @@ import ( var successfulConnectionOccured bool = false var terminalState *term.State = &term.State{} +var interactiveArgs string = "" // SetupCloseHandler ensures that CTRL+C inputs are properly processed, restoring the terminal state from not displaying entered characters where necessary -func SetupCloseHandler(feedbackFormURL string) { +func SetupCloseHandler(feedbackFormURL string) chan os.Signal { var terminalState *term.State c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) @@ -32,7 +37,26 @@ func SetupCloseHandler(feedbackFormURL string) { if terminalState != nil { term.Restore(int(os.Stdin.Fd()), terminalState) } + if interactiveArgs != "" { + communication.Info(fmt.Sprintf("Next time, add the following to loophole to start a tunnel with the same settings: %s", interactiveArgs)) + if strings.Contains(interactiveArgs, "--basic-auth-username") { + communication.Info("If you want to provide the password as well instead of typing it into the terminal, also add --basic-auth-password ") + } + argFile := cache.GetLocalStorageFile("lastArgs", "logs") + ioutil.WriteFile(argFile, []byte(interactiveArgs), 0644) + } communication.ApplicationStop() os.Exit(0) }() + return c +} + +//SuccessfulConnectionOccured sets the corresponding boolean to true, enabling the display of the feedback form URL after closing the CLI +func SuccessfulConnectionOccured() { + successfulConnectionOccured = true +} + +//SaveArguments generated by interactive mode to display after closing to teach the user how to get the same results easier next time +func SaveArguments(args []string) { + interactiveArgs = strings.Join(args, " ") } diff --git a/internal/pkg/communication/logger.go b/internal/pkg/communication/logger.go index fa52893..f55561a 100644 --- a/internal/pkg/communication/logger.go +++ b/internal/pkg/communication/logger.go @@ -99,7 +99,7 @@ func (l *stdoutLogger) ApplicationStop() { l.messageMutex.Lock() defer l.messageMutex.Unlock() l.divider() - fmt.Fprint(l.colorableOutput, "Goodbye") + fmt.Fprint(l.colorableOutput, "Goodbye\n") 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))) } diff --git a/internal/pkg/prompts/prompts.go b/internal/pkg/prompts/prompts.go new file mode 100644 index 0000000..de612b5 --- /dev/null +++ b/internal/pkg/prompts/prompts.go @@ -0,0 +1,237 @@ +package prompts + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "regexp" + "strconv" + "strings" + "unicode" + + "github.com/AlecAivazis/survey/v2" + "github.com/loophole/cli/internal/pkg/cache" + "github.com/loophole/cli/internal/pkg/closehandler" + "github.com/loophole/cli/internal/pkg/communication" + "github.com/spf13/cobra" +) + +//Possible answers for prompts and error messages +const ( + AnswerTunnelTypeHTTP string = "Expose an HTTP Port" + AnswerTunnelTypePath string = "Expose a local path" + AnswerTunnelTypeWebDAV string = "Expose a local path with WebDAV" + AnswerYes string = "Yes" + AnswerNo string = "No" + PortRangeErrorMsg string = "port must be between 0-65535" + PathValidityErrorMsg string = "enter an existing path without any quotation marks" +) + +func getPortPrompt() []*survey.Question { + return []*survey.Question{ + { + Name: "port", + Prompt: &survey.Input{Message: "Please enter the http port you want to expose: "}, + Validate: func(val interface{}) error { + if port, ok := val.(string); !ok { + return errors.New(PortRangeErrorMsg) + } else { //else is necessary here to keep access to port + n, err := strconv.Atoi(port) + if err != nil { + return errors.New(PortRangeErrorMsg) + } + if (n < 0) || (n > 65535) { + return errors.New(PortRangeErrorMsg) + } + } + + return nil + }, + }, + } +} + +func getPathPrompt() []*survey.Question { + return []*survey.Question{ + { + Name: "path", + Prompt: &survey.Input{Message: "Please enter the path you want to expose: "}, + Validate: func(val interface{}) error { + if path, ok := val.(string); !ok { + return errors.New(PathValidityErrorMsg) + } else { //else is necessary here to keep access to path + _, err := os.Stat(path) + if err == nil { + return nil + } + return errors.New(PathValidityErrorMsg) + } + }, + }, + } +} + +func getLastArgsPrompt(lastArgs string) *survey.Select { + return &survey.Select{ + Message: fmt.Sprintf("Your last settings were: '%s', would you like to reuse them?", lastArgs), + Options: []string{AnswerYes, AnswerNo}, + } +} + +func getInitialPrompt() *survey.Select { + return &survey.Select{ + Message: "Welcome to loophole. What do you want to do?", + Options: []string{AnswerTunnelTypeHTTP, AnswerTunnelTypePath, AnswerTunnelTypeWebDAV}, + } +} + +func askBasicAuth(signalChan chan os.Signal) string { + res := "" + prompt := &survey.Select{ + Message: "Do you want to secure your tunnel using a username and password?", + Options: []string{AnswerNo, AnswerYes}, + } + var usernamePrompt = []*survey.Question{ + { + Name: "username", + Prompt: &survey.Input{Message: "Please enter the username you want to use: "}, //not asking for a password since it's already implemented in virtual-serve + }, + } + err := survey.AskOne(prompt, &res) + if err != nil { + signalChan <- nil + } + if res == AnswerYes { + err = survey.Ask(usernamePrompt, &res) + if err != nil { + os.Exit(1) + return err.Error() + } + } else { + return "" + } + return res +} + +func askHostname(signalChan chan os.Signal) string { + res := "" + prompt := &survey.Select{ + Message: "Do you want to use a custom hostname?", + Options: []string{AnswerNo, AnswerYes}, + } + var hostnamePrompt = []*survey.Question{ + { + Name: "hostname", + Prompt: &survey.Input{Message: "Please enter the hostname you want to use: "}, + Validate: func(val interface{}) error { + var validChars = regexp.MustCompile(`^[a-z][a-z0-9]{0,30}$`).MatchString + if hostname, ok := val.(string); !ok || len(hostname) > 31 || !validChars(hostname) || !unicode.IsLetter(rune(hostname[0])) { + return errors.New("hostname must be up to 31 characters, may only contain lowercase letters and numbers and must start with a letter") + } + + return nil + }, + }, + } + err := survey.AskOne(prompt, &res) + if err != nil { + signalChan <- nil + } + if res == AnswerYes { + err = survey.Ask(hostnamePrompt, &res) + if err != nil { + os.Exit(1) + return err.Error() + } + } else { + return "" + } + return res +} + +func StartInteractivePrompt(cmd *cobra.Command, signalChan chan os.Signal) { + argPath := cache.GetLocalStorageFile("lastArgs", "logs") + var lastArgs string = "" + if _, err := os.Stat(argPath); err == nil { + argBytes, err := ioutil.ReadFile(argPath) + if err != nil { + communication.Fatal("Error reading last used arguments:" + err.Error()) + } + lastArgs = string(argBytes) + } + var lastArgsPrompt = getLastArgsPrompt(lastArgs) + var initialPrompt = getInitialPrompt() + var portPrompt = getPortPrompt() + var pathPrompt = getPathPrompt() + + var res string + var exposePort int + var exposePath string + var arguments []string + + if lastArgs != "" { + err := survey.AskOne(lastArgsPrompt, &res) + if err != nil { + signalChan <- nil + } + if res == AnswerYes { + cmd.SetArgs(strings.Split(lastArgs, " ")) //needs validation + cmd.Execute() + os.Exit(1) + } + } + err := survey.AskOne(initialPrompt, &res) + + if err != nil { + signalChan <- nil + } + switch res { + case AnswerTunnelTypeHTTP: + err = survey.Ask(portPrompt, &exposePort) + if err != nil { + signalChan <- nil + } + arguments = []string{"http", strconv.Itoa(exposePort)} + case AnswerTunnelTypePath: + err = survey.Ask(pathPrompt, &exposePath) + if err != nil { + signalChan <- nil + } + arguments = []string{"path", exposePath} + case AnswerTunnelTypeWebDAV: + err = survey.Ask(pathPrompt, &exposePath) + if err != nil { + signalChan <- nil + } + arguments = []string{"webdav", exposePath} + } + + hostname := askHostname(signalChan) + if hostname != "" { + arguments = append(arguments, "--hostname", hostname) + } + basicAuth := askBasicAuth(signalChan) + if basicAuth != "" { + arguments = append(arguments, "--basic-auth-username", basicAuth) + } + cmd.SetArgs(arguments) + + var argumentsWithQuotes []string + //setting the path argument in code doesn't work when it contains quotation marks, + //but they do need to be there when entered as a standalone command in a command line if the path contains spaces + //so, we give a copy of the arguments to the closehandler, with the path in quotation marks, where necessary + if strings.Contains(exposePath, " ") { + for i := 0; i < len(arguments); i++ { + if arguments[i] == exposePath { + argumentsWithQuotes = append(argumentsWithQuotes, fmt.Sprintf("'%s'", exposePath)) + } else { + argumentsWithQuotes = append(argumentsWithQuotes, arguments[i]) + } + } + closehandler.SaveArguments(argumentsWithQuotes) + } else { + closehandler.SaveArguments(arguments) + } + cmd.Execute() +}